Repository navigation
Fix component type inference - #30
Conversation
|
I think we should go with |
|
CHANGELOG.md should also note this change |
|
This is another issue that needs to be solved but I haven't been able to satisfy it yet type component<T> = { read __ctype: T } -- consider read vs read+write
local number: component<number> = nil :: any
local string: component<string> = nil :: any
local function accept_any(...: component<unknown>)
end
local function set<T>(ctype: component<T>, value: T)
end
accept_any(number, string) -- SHOULD PASS
set(number, 1) -- SHOULD PASS
set(number, "") -- SHOULD ERROR, BUT DOES NOT |
|
This satisfies the below cases, but does the conditional read/write of the --type component<T> = { read __ctype: T } -- consider read vs read+write
type function component(T: type): type
local key = types.singleton("__ctype")
local t = types.newtable()
if T.tag == "unknown" then
t:setreadproperty(key, T)
else
t:setproperty(key, T)
end
return t
end
local number: component<number> = nil :: any
local string: component<string> = nil :: any
local function accept_any(...: component<unknown>)
end
local function set<T>(ctype: component<T>, value: T)
end
accept_any(number, string) -- SHOULD PASS
set(number, 1) -- SHOULD PASS
set(number, "") -- SHOULD ERROR |
|
I did some experimenting and this is what I settled on --------------------------------------------------------------------------------
-- component type candidates
--------------------------------------------------------------------------------
--type component<T> = { read __ctype: T }
export type component<T> = { __ctype: T }
-- type component<T> = T
-- type function component(T: type): type
-- local key = types.singleton("__ctype")
-- local t = types.newtable()
-- if T.tag == "unknown" then
-- t:setreadproperty(key, T)
-- else
-- t:setproperty(key, T)
-- end
-- return t
-- end
export type unknown_component = { read __ctype: unknown }
--------------------------------------------------------------------------------
-- test functions
--------------------------------------------------------------------------------
-- Registry:clear()
local function clear(...: unknown_component)
end
-- Registry:set()
local function set<T>(ctype: component<T>, value: T)
end
-- Registry:get()
local function get<T>(ctype: component<T>): T
return nil :: any
end
-- hypothetical function
local function serdes(ctype: component<string>|component<number>): buffer
return nil :: any
end
--------------------------------------------------------------------------------
-- component definitions
--------------------------------------------------------------------------------
local function component<T>(): component<T>
local c = {
number = component<<number>>(),
string = component<<string>>(),
boolean = component<<boolean>>(),
unknown = component<<unknown>>(),
number_or_string = component<<number|string>>(),
}
--------------------------------------------------------------------------------
-- test cases
--------------------------------------------------------------------------------
-- SHOULD PASS
clear(c.number, c.string)
set(c.number, 1)
set(c.unknown, 1)
set(c.number_or_string, 1)
serdes(c.number)
-- SHOULD ERROR
set(c.number, "")
set(c.string, 1)All cases pass, we just need to also expose a special case for unknown components |
a3c4ca9 to
de94fe3
Compare
|
New revision to the example cases: The See latest comment |
|
Something like this seems to work --------------------------------------------------------------------------------
-- component type candidates
--------------------------------------------------------------------------------
--type component<T> = { read __ctype: T }
export type component<T> = { __ctype: T }
-- type component<T> = T
-- type function component(T: type): type
-- local key = types.singleton("__ctype")
-- local t = types.newtable()
-- if T.tag == "unknown" then
-- t:setreadproperty(key, T)
-- else
-- t:setproperty(key, T)
-- end
-- return t
-- end
-- todo: needs to handle case where a union of components are given
-- todo: needs to handle cases where a component is not passed (give proper error messages)
type function read(component: type): type
assert(component.tag == "table")
local key = types.singleton("__ctype")
local value = component:readproperty(key)
assert(value)
local readonly_component = types.newtable()
readonly_component:setreadproperty(key, value)
return readonly_component
end
--------------------------------------------------------------------------------
-- test functions
--------------------------------------------------------------------------------
-- Registry:clear()
local function clear(...: read<component<unknown>>)
end
-- Registry:set()
local function set<T>(ctype: component<T>, value: T)
end
-- Registry:get()
local function get<T>(ctype: read<component<T>>): T
return nil :: any
end
-- hypothetical function
local function serdes(ctype: component<string>|component<number>): buffer
return nil :: any
end
-- hypothetical function
local function serdes2(ctype: read<component<string|number>>): buffer
return nil :: any
end
--------------------------------------------------------------------------------
-- component definitions
--------------------------------------------------------------------------------
local function component<T>(): component<T>
local c = {
number = component<<number>>(),
string = component<<string>>(),
boolean = component<<boolean>>(),
unknown = component<<unknown>>(),
number_or_string = component<<number|string>>(),
}
--------------------------------------------------------------------------------
-- test cases
--------------------------------------------------------------------------------
-- SHOULD PASS
clear(c.number, c.string)
set(c.number, 1)
set(c.unknown, 1)
set(c.number_or_string, 1)
serdes(c.number)
serdes2(c.number)
local function get_test(ctype: read<component<unknown>>): unknown
return get(ctype) -- should pass
end
get_test(c.unknown)
get_test(c.number)
-- SHOULD ERROR
set(c.number, "")
local function set_test(ctype: read<component<number>>)
set(ctype, 1) -- should error
endNotable changes are that APIs that promise not to mutate a component, must be typed to accept |
|
Couple thoughts..
|
| type | why | |
|---|---|---|
set, insert, patch, find, copy |
component<T> |
takes T |
storage |
component<T> |
Pool.values is writable |
view |
component<A> |
View:patch() writes values |
add, get, try_get, on_add, on_change |
readonly_component<T> |
only produces T |
track, group |
readonly_component<A> |
iteration only |
has, remove, clear, is_tag, :exclude |
any_component |
T unused |
storage and view are exceptions to the rule above as they look like reads but return values that can later
write
|
I think Also
|
|
export type component<T> = { __ctype: T, read __component: true }
export type readonly_component<T> = { read __ctype: T, read __component: true }
export type any_component = readonly_component<unknown>
local function find<T>(ctype: readonly_component<T>, value: T): number?
return 0
end
local A = (nil :: any) :: component<number>
find(A, "hello") -- OKTechnically, it is legal (in the sense that it is type-safe) to do local function find<T>(ctype: component<T>, value: T): number?
return 0
end
local A = (nil :: any) :: component<number>
find(A, "hello") -- TypeError: Expected this to be 'number', but got 'string'
find(A, 1) -- OKAlternatives considered
|
de94fe3 to
a17947e
Compare
|
Updates ecr to use Looked into keeping the DocsUpdated docs as well, couple things looked like they were missing from prior changes so went ahead and added those, but happy to back those out if desired.. also linked already existing wiki pages into the public docs, not sure if they were intentionally left out? Typechecking testsAdds typechecking testing infra with a positive fixture and an Example passing outputExample failing output |
a17947e to
f2cee6c
Compare
centau
left a comment
There was a problem hiding this comment.
I don't think wiki section is needed.
A full type testing setup seems overkill, I would of just had a file of a few cases that is occasionally manually inspected. But we can keep it
f2cee6c to
89534c6
Compare
Component TypesECR components are runtime ids, but their exported types carry a phantom value type so Luau can check how each component is used. local Health = ecr.component<<number>>()
local Name = ecr.component<<string>>()
local Health = ecr.component(function()
return 100
end) -- component<number>
|
| Parameter use | Type | Examples |
|---|---|---|
| Consumes a value or exposes writable values | component<T> |
set, insert, patch, find, copy, storage, view |
| Only reads or produces values | read_component<T> |
add, get, try_get, on_add, on_change, track, group |
| Only checks component identity | read_component<unknown> |
has, remove, clear, is_tag, query exclusions |
storage and view are deliberately in the first row: even though they read component data initially, Pool.values and View:patch() provide later writable access.
The component marker
Both types include a read-only __component: true marker. It has no runtime representation: components remain plain ids. At type-check time, the marker prevents an erased read_component<unknown> from accepting arbitrary tables or strings.
Why this is not a type function
A type function such as read<component<T>> looks like it could derive the read-only view. In practice, user-defined type functions are evaluated forward: when T is still unknown, the new solver cannot reverse the function to infer it from an argument. It falls back to unknown, losing the precise result type of reads.
Keeping read_component<T> as a plain alias preserves inference.
Known limitation
ecr.name accepts a table of named components. Luau does not check named table literal fields against a string indexer, so a table literal's values cannot be fully validated there. The indexer is still needed to disambiguate ecr.name's table and lookup overloads.
Summary
Fixes component type inference on:
registry:setregistry:insertregistry:patchregistry:findregistry:copyCurrently, the behavior is:
With the changes, the type for
registry:set(and others) behave as expected:Breaking Changes
registry:view) with more than8components do not get types past the 8th componentregistry:track) with more than8components do not get types past the 8th componentregistry:group) with more than5components do not get types past the 5th componentecr.componentusage (typechecking only)Example Usage
Old
New
Notes
luau-lspv1.69.0