fix(semantic): preserve Lua return rows#1065
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements a major refactor of the type inference engine to accurately model Lua's multi-return semantics by replacing single return types with "return rows" (Vec). A new return_row module provides logic for merging rows, adjusting result slots, and handling variadic tails, which is now integrated across closure analysis, generic instantiation, and assignment logic. These changes ensure that tail calls, nil padding in assignments, and arity preservation in higher-order functions are correctly handled. Additionally, diagnostic checkers for parameter and return counts were updated to utilize this new row-based logic, supported by an extensive suite of new test cases. I have no feedback to provide.
9a48d98 to
8b24b86
Compare
The old policy collapsed function results into one `LuaType`. Multiple
values were encoded inside a variadic type, while zero values and a
single `nil` value both flowed through many consumers as `LuaType::Nil`.
That made row arity depend on which caller happened to unwrap the type.
For example:
---@return
local function none() end
---@return nil
local function one_nil() end
arity(none()) -- should pass zero arguments
arity(one_nil()) -- should pass one nil argument
The new policy stores function and signature results as return rows. A
row is only collapsed when a caller asks for a single expression value.
Expression lists now apply Lua adjustment in one place: only the final
expression may expand, non-final multi-returns use slot 0, and exhausted
fixed slots become `nil`.
For example:
---@return string
local function one() end
local a, b = one()
-- a: string
-- b: nil
This keeps higher-order callable inference from turning an empty `R...`
into a nil return value:
---@Generic T, R
---@param f fun(...: T...): R...
---@param ... T...
---@return boolean, R...
local function wrap(f, ...) end
local ok, payload = wrap(none)
-- wrap(none) returns only `boolean`.
-- Assignment pads `payload` with nil.
Unbounded rows also remain rows until a concrete slot is requested:
---@param ... string
local function pass(...)
return ...
end
local first, second = pass("x", "y")
-- first: string
-- second: string
Centralize row merging, overload slot lookup, and return-count min/max
logic around the row representation. Diagnostics now count adjusted rows
instead of guessing from collapsed variadic types.
Assisted-by: Codex
8b24b86 to
0a94a23
Compare
Problem
Lua return values were collapsed into one
LuaTypetoo early. That made rowshape depend on the consumer and blurred cases that Lua keeps distinct:
nilR...For example:
Solution
nilvariadic types.
Validation
cargo test -p emmylua_code_analysis