luau-lang / luau

A fast, small, safe, gradually typed embeddable scripting language derived from Lua
https://luau-lang.org
MIT License
3.79k stars 349 forks source link

Metamethods with generics having their generics infered as the self type if the self type isn't a generic itself #1262

Open kalrnlo opened 1 month ago

kalrnlo commented 1 month ago

With the code below when using the __call metamethod instead of export.retry, all of the generics get infered to the self type of unknown, when it would be expected for the generics to still be generics.


local function retry<T..., A...>(max_attempts: number, callback: (T...) -> (A...), ...: T...): (boolean, A...)
    local results = table.pack(pcall(callback, ...))

    if not results[1] then
        local attempts = 1

        repeat
            results = table.pack(pcall(callback, ...))
            attempts += 1
        until results[1] or attempts == max_attempts
    end
    return table.unpack(results) :: any
end

local function retry_call<T..., A...>(self: unknown, max_attempts: number, callback: (T...) -> (A...), ...: T...): (boolean, A...)
    return retry(max_attempts, callback, ...)
end

local mt = {
    __call = retry_call
}
mt.__index = mt
local tbl = {
    retry = retry
}

-- first argument of setmetatable even if its given a table throws a type error saying its not a table
local export: typeof(setmetatable(tbl, mt)) = setmetatable(tbl :: any, mt)

-- No error, works as expected, with the generics being infered properly
local success, result = export.retry(10, function(bar: number)
    return bar * 2
end, 10)

--[[
    TypeError: Type '(number) -> number' could not be converted into
    '(...unknown) -> (...unknown)'
    caused by:
    Type 'unknown' could not be converted into 'number'
--]]
local success, result = export(10, function(foo: number)
    return foo + 1
end, 10)

The issue with the metamethod type gets fixed when the self type itself is a generic, with the solver properly infering the functions generics

local function retry_call<S, T..., A...>(self: S, max_attempts: number, callback: (T...) -> (A...), ...: T...): (boolean, A...)
    return retry(max_attempts, callback, ...)
end