Recent type system regressions for generic-parametered functions

Reproduction Steps
Here is a repro with three examples of regressions to script analysis that appeared in an update a couple of weeks ago:

RegressionsRepro.rbxl (32.6 KB)


Example 1: Type relation no longer works between equivalent generic function types.

--!strict
local LeaksGenericParameterTypes = require(game.ServerScriptService.LeaksGenericParameterTypes)

local object: LeaksGenericParameterTypes.MyType = {
	-- Type <g1>(MyType, g1) -> g1 could not be converted into <g2>(MyType, g2) -> g2 ????
	-- The type relation on generic functions has regressed; this used to
	-- work just fine.
	Foo = function(self: LeaksGenericParameterTypes.MyType, x)
		return x
	end,
	Bar = function(self: LeaksGenericParameterTypes.MyType, str: string)
		
	end,
	CombineFoo = function(self: LeaksGenericParameterTypes.MyType, ...)
		return function() end
	end,
}


Example 2: The types of generic parameters are leaking across modules again. This used to be fixed.

--!strict
local LeaksGenericParameterTypes = require(game.ServerScriptService.LeaksGenericParameterTypes)

local object = LeaksGenericParameterTypes.CreateObject()

local i = 2
local alsoI = object:Foo(i)

local j = 'hello'
					-- Type 'string' could not be converted into 'number' ???
					-- The type of our generic parameter gXXXX is leaking across the module.
					-- This did not happen before, and is a regression.
local alsoJ = object:Foo(j)


Example 3: Type relation fails between a named type and itself in a certain example:

--!strict
-- Example 3:



local StateModule = require(game.ServerScriptService.StateModule)


-- Type '(Finalizer) -> ()' could not be converted into '((Finalizer -> ())?' ???
-- All we are doing here is passing a callback where a callback is optionally requested.
-- These types should relate and no warning should be emitted.
-- This is a recent regression, and happens when I try to work around the regression from examples
-- 1 and 2.
StateModule.SetListener('Foo', function(object: StateModule.Finalizer)
	object:AddTask(function() end)
	object:AddTask(workspace.ChildAdded:Connect(function()end))
end)

Read the code in the repro file carefully for more context as to what is going on here. This is just a minimal version of what I’ve encountered in my game’s code in practice.

Expected Behavior
There should be no warnings present in script analysis for all three examples.

Actual Behavior
There is a warning present for each example; these warnings only started showing up a couple weeks ago.

Workaround
Example 3 uses my attempt to workaround examples 1 and 2 using an actual practical example from my game (the Finalizer) module.

This workaround involves defining the generic function and types in separate statements to avoid generic types leaking across modules:

Unfortunately, this seems to make type relation unsound in a number of practical scenarios that affect my game’s code and some of my public libraries rather extensively.

My workaround for now is to not use generic function types, and instead, in the case of the Finalizer, type AddTask as (Finalizer, any) -> any. This really sucks as a workaround, because I just had made the switch from that less-typesafe type to the more accurate and typesafe typeof(function(self: Finalizer, t) return t end) (equivalent to <T>(Finalizer, T) -> T when generic function types finally get enabled). I have publicly released libraries with AddTask typed as typeof(function(self: Finalizer, t) return t end) because it used to work, and a recent update has caused this to not work anymore.

Issue Area: Studio
Issue Type: Other
Impact: High
Frequency: Constantly
Date First Experienced: 2021-06-25 00:06:00 (-06:00)
Date Last Experienced: 2021-07-07 00:07:00 (-06:00)

1 Like

Thanks for the bug report! This is a very interesting example, combining generics with recursive types. Once you unfold the type recursion you end up with nested generics, which is what’s causing the issue, since nested generics aren’t really supported yet.

Also, there’s an issue with how types are being displayed to the user, which is resulting in incomprehensible type errors, In this case the uninstantiated type Finalizer (with its generic methods) is being confused with the instantiated type (with non-generic methods), resulting in the Type '(Finalizer) -> ()' could not be converted into '((Finalizer) -> ())?' error.

The good news is this should all be addressed by the next round of work on generic functions, which is specifically about supporting nested generics. The bad news is that means waiting.

I suspect your workaround using any is the best bet for the moment.

2 Likes

Glad to see this is being worked on… however nested generics were supported at one point, and because of that I have public library code that used nested generics while they were functioning. This means that some other people’s code which depends on my code will be broken until this is fixed. So I guess I’m just mostly puzzled as to why there was a release that supported nested generics, and then this release was reverted after this already being a working feature for quite some time.

1 Like

“supported” is a strong word in engineering. It could have been some edge case of the implementation that was “working” by sheer coincidence at that point and they didn’t intentionally mean to enable that case.

Hello! The fix for this issue has been announced here: Luau Recap: September 2021. Let us know if this issue still occurs for you. Thank you!

1 Like

This topic was automatically closed after 6 days. New replies are no longer allowed.