Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Future: Improve Luau type checking #909

Open
helgoboss opened this issue Jan 21, 2024 · 0 comments
Open

Future: Improve Luau type checking #909

helgoboss opened this issue Jan 21, 2024 · 0 comments
Labels
enhancement New feature or request low priority realearn Related to ReaLearn wait Not actionable at the moment. Waiting for something.

Comments

@helgoboss
Copy link
Owner

The Luau type checking introduced in #908 works great already but there are a few things that could be improved. However, I think they require Luau improvements.

Here's the file in which I did a bunch of type checking experiments. It contains comments which things are still lacking.

--!strict

--- This is an experiment how to do table merging while also merging (accumulating) the types.
--- This could be very useful when merging partial mappins as the type checker could
--- tell us if something is wrong or missing for the merged object to be a valid mapping.
---
--- Turns out it's possible to some degree but with two caveats:
---
--- 1. Doesn't work if the merging is done via metatable / operator overloading (bla + foo).
--- 2. Even if using normal functions or self functions without metatable, it's a bit clunky
---    due to the generic types not being able to be recursive with another type parameter.
---
---
--- We would need something along the lines of:
--- - https://github.com/luau-lang/luau/issues/393
--- - https://github.com/luau-lang/rfcs/pull/5


local common = {}


type ArbitraryTable = { [any]: any }

--- Creates a shallow clone of a table.
---
--- Typing not ideal because of https://github.com/luau-lang/luau/issues/392#issuecomment-1050344334
function common.clone<T>(t: T): T
    if type(t) ~= "table" then
        return t
    end
	local new_table = {}
	for k, v in t :: ArbitraryTable do
		new_table[k] = v
	end
	return new_table :: any
end

type Mergeable = { __add: <A, B>(t1: A, t2: B) -> A & B }
local mergeable: Mergeable
type MetatableMergeable = typeof(setmetatable({}, mergeable))

--- Makes it possible to merge this table with another one via "+" operator.
---
--- Typing not ideal because of https://github.com/luau-lang/luau/issues/392#issuecomment-1050344334
function common.make_mergeable<V>(t: V): V & MetatableMergeable
    if type(t) ~= "table" then
        return t :: any
    end
	local metatable = {
		__add = common.merged,
	}
	setmetatable(t :: ArbitraryTable, metatable)
	return t
end

type Merge<A> = { merge: <B>(self: A, other: B) -> A & B }

function common.install_merge<A>(t: A): A & Merge<A>
	return t :: any
end

--- Caveat 2: We can't use Merge2<A&B> as a result but must add another type (non-recursive type).
--- That also means the maximum number of merges is limited. Not a big issue though, we just need
--- to write e.g. 10 Merge types and that's it.
type Merge2<A> = { merge: <B>(self: A, other: B) -> A & B & Merge3<A&B> }
type Merge3<A> = { merge: <B>(self: A, other: B) -> A & B & Merge4<A&B>}
type Merge4<A> = { merge: <B>(self: A, other: B) -> A & B }


function common.install_merge2<A>(t: A): A & Merge2<A>
	return t :: any
end


--- Returns a new table that is the result of merging t2 into t1.
---
--- Values in t2 have precedence.
---
--- The result will be mergeable as well. This is good for "modifier chaining".
---
--- Typing not ideal because of https://github.com/luau-lang/luau/issues/392#issuecomment-1050344334
function common.merged<A, B>(t1: A, t2: B): A & B
    if type(t1) ~= "table" or type(t2) ~= "table" then
        return t1 :: any
    end
	local result = common.clone(t1 :: ArbitraryTable)
	for key, new_value in pairs(t2 :: ArbitraryTable) do
		local old_value = result[key]
		if old_value and type(old_value) == "table" and type(new_value) == "table" then
			-- Merge table value as well
			result[key] = common.merged(old_value, new_value) :: any
		else
			-- Simple use new value
			result[key] = new_value
		end
	end
	return common.make_mergeable(result :: any)
end

-- Tests

-- Try merging with function call (inconvenient): CHECK

type Combined = {
    foo: number,
    bla: string,
    man: number,
    tan: number,
    klos: string,
}

local bla = {
    foo = 5,
    bla = "asd"
}
local foo = {
    tan = 4
}
local zubi = {
    man = 0,
    bla = "oho"
}
local ending = {
    klos = "hallo"
}
local combined: Combined = common.merged(common.merged(common.merged(bla, foo), zubi), ending)

-- Try merging with self method, no metatable: CHECK

local mergeInstalled = common.install_merge(bla)
local combined2: Combined = common.install_merge(common.install_merge(mergeInstalled:merge(foo)):merge(zubi)):merge(ending)

-- Try merging with self method, no metatable, result also mergable: CHECK

local mergeInstalled2 = common.install_merge2(bla)
local combined2: Combined = mergeInstalled2:merge(foo):merge(zubi):merge(ending)

-- Try with metatable +: NONONO

type PartialMappingImpl<A> = {
    __index: A,
    __add: (PartialMappingImpl<A>, number) -> A,
}
type PartialMappingMetatable<A> = typeof(setmetatable(
    {}, 
    {} :: PartialMappingImpl<A>
))

type PartialMapping<A> = PartialMappingMetatable<A> & A

local PartialMapping = {}
PartialMapping.__index = PartialMapping

function PartialMapping.new<A>(t: A): PartialMapping<A>
    -- local self = t
    -- return setmetatable(self, PartialMapping)
    return t :: any
end

-- function PartialMapping:__add(b)
-- end

local bla = {
    foo = 5,
    bla = "asd",
}

local foo = {
    tan = 4
}
local zubi = {
    man = 0,
    bla = "oho"
}
local ending = {
    klos = "hallo"
}

type CombinedLight = {
    foo: number,
    bla: string,
    -- tan: number,
}

local blaPartial = PartialMapping.new(bla)

--- Caveat 1: Using operator overloading somehow not possible. Type checker doesn't 
--- understand that the type has a + operator if we define type PartialMapping<A> as PartialMappingMetatable<A> & A (an intersection).
--- Without the intersection, it will not transport the fields of A. I think what we need is a generic metatable.
local result: CombinedLight = blaPartial + foo
@helgoboss helgoboss added enhancement New feature or request low priority wait Not actionable at the moment. Waiting for something. labels Jan 21, 2024
@helgoboss helgoboss added the realearn Related to ReaLearn label Jul 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request low priority realearn Related to ReaLearn wait Not actionable at the moment. Waiting for something.
Projects
None yet
Development

No branches or pull requests

1 participant