Reproduction Steps
Type the following into a script and examine the script analysis output:
-- Example 1: Explicitly-defined parameter types for generic
-- functions become "freed" and leaks for some reason
--!strict
type MyObject = {
getReturnValue: <T>(cb: () -> T) -> T
}
local object: MyObject = {
getReturnValue = function<T>(cb: () -> T): T
return cb()
end,
}
-- OK
local x = object.getReturnValue(function()
return ""
end)
-- OK
local y = object.getReturnValue(function()
return 2
end)
type ComplexObject<T> = {
id: T,
nested: MyObject
}
-- Not OK; "Type '() -> a' could not be converted into
-- '() -> T'" / "Generic supertype escaping scope"
local complex: ComplexObject<string> = {
id = "Foo",
nested = object,
}
-- Example 2: The leaked type can be inferred by external
-- code and have its type written over
-- Cheating to exemplify the issue further:
local complex2: ComplexObject<string> = nil
-- OK
local x = complex2.nested.getReturnValue(function(): string
return ""
end)
-- Not OK; "Type 'number' could not be converted into 'string'"
-- The type of the generic function "getReturnValue" function has leaked
-- and it now thinks it should always return a string even though it's a
-- generic function that accepts/infers a type on every call.
local y = complex2.nested.getReturnValue(function()
return 3
end)
Expected Behavior
All of the examples should be valid. I am pretty sure this is a once-supported feature that has had a regression, as it’s affecting some of my existing code.
There was a point at which nested generics were not supported, but it had been announced months ago that they are now supported, so this is certainly a bug/regression:
(Quote from thread)
People may remember that back in April we announced generic functions, but then had to disable them. That was because DataBrain discovered a nasty interaction between typeof
and generics, which meant that it was possible to write code that needed nested generic functions, which weren’t supported back then.
Well, now we do support nested generic functions, so you can write code like
function mkPoint(x)
return function(y)
return { x = x, y = y }
end
end
and have Luau infer a type where a generic function returns a generic function
function mkPoint<X>(x : X) : <Y>(Y) -> Point<X,Y>
return function<Y>(y : Y) : Point<X,Y>
return { x = x, y = y }
end
end
For people who like jargon, Luau now supports Rank N Types , where previously it only supported Rank 1 Types.People may remember that back in April we announced generic functions, but then had to disable them. That was because DataBrain discovered a nasty interaction between typeof and generics, which meant that it was possible to write code that needed nested generic functions, which weren’t supported back then.
Well, now we do support nested generic functions, so you can write code like
function mkPoint(x)
return function(y)
return { x = x, y = y }
end
end
and have Luau infer a type where a generic function returns a generic function
function mkPoint<X>(x : X) : <Y>(Y) -> Point<X,Y>
return function<Y>(y : Y) : Point<X,Y>
return { x = x, y = y }
end
end
For people who like jargon, Luau now supports Rank N Types , where previously it only supported Rank 1 Types.
Actual Behavior
Internally, <T>() -> T
somewhere along the way gets converted to the isomorphic () -> a
, and hilarity ensues.
It seems that T
becomes a “freed” type (a
) and then is leaked when used in further code.
Workaround
Don’t use nested generic functions. Not an option for me at this point, because I have been relying on this being a supported feature.
Issue Area: Studio
Issue Type: Other
Impact: Very High
Frequency: Constantly
Date First Experienced: 2022-05-15 00:05:00 (-06:00)
Date Last Experienced: 2022-05-15 00:05:00 (-06:00)