Skip to content

fix(types): validate table constructors with shared type checks - #1312

Draft
lewis6991 wants to merge 1 commit into
EmmyLuaLs:mainfrom
lewis6991:rewrite-generic-tables
Draft

lewis6991 wants to merge 1 commit into
EmmyLuaLs:mainfrom
lewis6991:rewrite-generic-tables

Conversation

@lewis6991

Copy link
Copy Markdown
Collaborator

Table literals passed to the standard table<K, V> type can report inject-field for valid entries, while invalid keys or values can go unchecked.

---@param values table<integer, string>
local function accept(values) return values end
accept({'a'})        -- valid
accept({name = 'a'}) -- invalid key
accept({1})          -- invalid value

Validate constructor contents through the shared type checker. The call-selection and recursive-type changes support this validation: they preserve the correct parameter contract and let nested checks finish without skipping invalid fields.

Table contents

  • Check array elements, dictionary entries, and record fields, including nested constructors reached through variables and module references.
  • Check literal keys and values exactly. Require one union alternative to accept the whole constructor and require every intersection component to accept it.
  • Check every non-nil value alternative. Follow Lua's rules for nil entries, integral float keys, and multiple results in the final unkeyed field.
  • Keep closed-class field checks for user-defined types, including classes named table.

Calls and diagnostics

Separate call checks could select different overloads, ignore required arguments, or lose generic bindings and method receiver offsets:

---@param values table<integer, string>
---@overload fun(values: table<string, integer>, required: boolean)
local function accept(values) return values end
accept({count = 1}, true)       -- valid
pcall(accept, {count = 1}, true) -- valid
accept({count = 1})             -- invalid

Filter table overloads by argument count, then use one candidate and its bindings for type and field errors. Share this selection between direct calls and pcall, and remove the duplicate call loops from the assignment and field checkers.

Report call type errors as param-type-mismatch. Prefer enabled field errors over a duplicate argument error, while retaining the type error when field diagnostics are disabled.

Recursive and optional values

Re-entry detection alone does not catch recursive steps that create different type identities:

---@alias Tree<T> table<string, string | Tree<T[]>>

Stabilize unused recursive arguments while preserving arguments that affect fields, including indirect dependencies. Reuse the shared comparison queue and mark constructor field traversal as structural. This lets cyclic module values terminate without hiding an invalid leaf.

Preserve optional object and mapped fields as unions with nil. Check absent required fields through the normal type checker, and preserve non-nil narrowing when the built-in type function guards the same access path.

@lewis6991
lewis6991 force-pushed the rewrite-generic-tables branch from 0e86a6f to 0467411 Compare October 20, 2026 10:14
Table contents:

Generic table literals were treated as instances of the standard table
class. Valid entries could report injected fields, while invalid keys or
values could go unchecked:

    ---@PARAM values table<integer, string>
    local function accept(values) return values end
    accept({'a'})        -- valid
    accept({name = 'a'}) -- invalid key
    accept({1})          -- invalid value

Validate arrays, dictionaries, and record fields in the shared type
checker. Keep constructor types through variables and module references.
Apply the same rules to assignments and calls:

- Check literal keys and values exactly.
- Require each constructor to match one whole union alternative.
- Require every intersection component to match.
- Check every non-nil value alternative; nil creates no entry.
- Apply Lua's rules for integral float keys and constructor results.

Only the standard table<K, V> type bypasses closed-class field checks.

Calls and diagnostics:

Separate call checks could select different overloads, ignore required
arguments, or lose generic bindings and method receiver offsets:

    ---@PARAM values table<integer, string>
    ---@overload fun(values: table<string, integer>, required: boolean)
    local function accept(values) return values end
    accept({count = 1}, true)       -- valid
    pcall(accept, {count = 1}, true) -- valid
    accept({count = 1})             -- invalid

Filter table overloads by argument count, then use one candidate and its
bindings for type and field errors. Share this selection between direct
calls and pcall. Remove the duplicate call loops from the assignment and
field checkers.

Report call type errors as param-type-mismatch. Prefer enabled field
errors over a duplicate argument error. Keep the type error when field
diagnostics are disabled.

Recursive aliases:

Re-entry detection alone cannot stop aliases that create a new type
identity on each recursive step:

    ---@alias Tree<T> table<string, string | Tree<T[]>>

Stabilize unused recursive arguments. Preserve arguments that affect
fields, including indirect dependencies between parameters. Reuse the
shared comparison queue and mark constructor field traversal as
structural. Cyclic module values can then terminate without hiding an
invalid leaf.

Optional fields:

Preserve optional object and mapped fields as unions with nil. Check
absent required fields through the normal type checker. Preserve non-nil
narrowing when the built-in type function guards the same access path.
@lewis6991
lewis6991 force-pushed the rewrite-generic-tables branch from 0467411 to f24b3ac Compare October 20, 2026 10:49
@lewis6991
lewis6991 marked this pull request as draft October 20, 2026 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant