luau icon indicating copy to clipboard operation
luau copied to clipboard

Callback arguments not inferred correctly on function overloads in new solver

Open avion-sandwich-gout-television-asterion opened this issue 1 year ago • 1 comments

I'm using Charm state management library on Roblox Studio, I enabled the long awaited new Beta solver feature. Although, it now requires me to explicitly cast the argument inside callbacks when I want to mutate a state. Which is not something I should do when I have to mutate state, I expect the argument inside the callback (referred as "old" in the examples) to be implicitly typed, or else it would be harder to work with more complex types like string constants.

This bug happens on the type solver beta, and worked perfectly before.

It seems to only happen when the arguments in the callback are from an overload:

type SetAtom<T> = (newValue: T) -> T -- May be any function maybe
type DispatchAtom<T> = (callback: (old: T) -> T) -> T -- I expect old to be T

type Atom<T> = DispatchAtom<T> -- Inferred correctly
type Atom<T> = DispatchAtom<T>&SetAtom<T> -- Not inferred correctly

Here is a replica of the bug by recreating the types seen in Charm:

--!strict
-- This type is used in Charm (https://github.com/littensy/charm)
-- Placeholder function
local function atom(initialValue)
	return function(...)
		return initialValue
	end
end

-- Set the state using a callback similar to most UI librairies with state management
type DispatchAtom<T> = (callback: (old: T) -> T) -> T

-- Set the state old fashionned way
type SetAtom<T> = (newValue: T) -> T

-- Get the state
type Molecule<T> = () -> T

-- An example of Charm state, which have 3 ways of interacting with it
type Atom<T> = Molecule<T>&DispatchAtom<T>&SetAtom<T>

local coins = atom(10) :: Atom<number>

local a = coins(10) -- OK
local b = coins() -- OK
local c = coins(function(old)
	-- old is inferred as unknown here, 
	-- old must be inferred as number
	-- this can be worked around by explicitly typing old as number
	-- will even throw a warning telling that the overload argument is not compatible
	return old + 1
end) -- Not OK
-- c is inferred as a number, OK I guess ?

You can also reproduce the issue by using Charm directly:

--!strict
local charm = require("charm")

local coins = charm.atom(10) --OK
local a = coins() --OK
local b = coins(10) --OK

local c = coins(function(old)
	-- old is still inferred as unknown, or even "a"
	return old + 1
end) -- Not OK

info

image

It seems that function overloads themselves makes Luau panic with various features! Just saw it with type literals! Be sure to throw some unit testing for overloads specifically :<

-- In most cases, it is inferred as string, and not a literal.
local gameState = charm.atom("Intermission") :: charm.Atom<"Intermission">
gameState("Intermission") -- Inferred as string, not OK!

local gameState = charm.atom("Intermission") :: charm.Atom<"Intermission"|"Game">
gameState("Intermission") -- Still not OK!

local gameState = charm.atom("Intermission"::"Intermission"|"Game")
gameState("Intermission") -- Still not OK!


-- Type casting to literals or any is a workaround
local gameState = charm.atom("Intermission") :: charm.Atom<"Intermission"|"Game">
gameState("Intermission" :: "Intermission") -- OK?!

local gameState = charm.atom("Intermission"::"Intermission"|"Game")
gameState("Intermission" :: "Intermission") -- OK?!

while task.wait(1) do
	local state = Defs.GlobalState.State -- Is a reference to a charm atom
	if state() == "Game" then -- Literal comparison works, OK
		state("Intermission") -- Not OK
	else
		state("Game") -- Not OK
	end
end

ghost avatar Sep 14 '24 08:09 ghost