Skip to content

fix(semantic): preserve Lua return rows#1065

Open
lewis6991 wants to merge 1 commit into
EmmyLuaLs:mainfrom
lewis6991:fix/semantic-return-rows
Open

fix(semantic): preserve Lua return rows#1065
lewis6991 wants to merge 1 commit into
EmmyLuaLs:mainfrom
lewis6991:fix/semantic-return-rows

Conversation

@lewis6991
Copy link
Copy Markdown
Collaborator

Problem

Lua return values were collapsed into one LuaType too early. That made row
shape depend on the consumer and blurred cases that Lua keeps distinct:

  • zero returned values vs one returned nil
  • exhausted fixed return slots vs an explicit returned value
  • empty or unbounded generic result rows such as R...

For example:

---@return
local function none() end

---@return nil
local function one_nil() end

arity(none())    -- zero arguments
arity(one_nil()) -- one nil argument

Solution

  • Store function and signature results as adjusted return rows.
  • Collapse a row only when a caller asks for a single expression value.
  • Centralize row merging, overload slot lookup, and return-count min/max logic.
  • Apply Lua expression-list adjustment from the shared inference path:
    • only the final expression may expand
    • non-final multi-returns use slot 0
    • exhausted fixed slots become nil
  • Update diagnostics to count adjusted rows instead of guessing from collapsed
    variadic types.

Validation

  • cargo test -p emmylua_code_analysis

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@lewis6991 lewis6991 force-pushed the fix/semantic-return-rows branch from 9a48d98 to 8b24b86 Compare May 8, 2026 16:18
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
@lewis6991 lewis6991 force-pushed the fix/semantic-return-rows branch from 8b24b86 to 0a94a23 Compare May 8, 2026 16:29
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