Area: glua_check (expected to affect the language server too; the repro and diagnostics below are from glua_check)
Summary
A function whose return type resolves to ((1,2)|unknown) cannot be spread into another call's argument list. The unknown arm comes from an unannotated recursive function: the return type does not reach a fixpoint (the base case is return 1, 2), so it is inferred as ((1,2)|unknown). Spreading that into a two-parameter call produces two warnings - the first value types as the whole union, and the second parameter is reported missing, as if the arity might be 1.
Both are warnings, so this fails glua_check --warnings-as-errors with exit 1.
This is adjacent to the now-fixed #47, not a regression of it: #47's own cases - a plain multi-return spread, and the pcall form - are clean on 1.2.0. The surviving trigger here is specifically the unknown-tainted union from the unannotated recursion.
Minimal repro
Repro: bug3-recursive-multiret-spread.zip
Standalone - no workspace.library and no GLua annotations. .luarc.json is only {"runtime":{"version":"LuaJIT"}}.
---@param x number
---@param y number
local function takesTwo(x, y) end
-- unannotated recursive fn -> return infers as ((1,2)|unknown)
local function r2(n)
if n and n > 0 then return r2(n - 1) end
return 1, 2
end
takesTwo(r2(5))
glua_check --warnings-as-errors . on the repro reports exactly (line 28 is the takesTwo(r2(5)) line in repro.lua, below the header comment):
warning: expected `number` but found `((1,2)|unknown)`. [param-type-mismatch]
--> repro.lua:28:10
warning: expected 2 parameters but found 1. missing parameter: y [missing-parameter]
--> repro.lua:28:15
r2 always returns two numbers - the recursive arm returns r2's own result, and the base arm returns 1, 2. A fixpoint on the self-reference would give (number, number); instead the recursive arm is left as unknown and taints the union.
Two controls that isolate it
Both are in the repro file, and both are clean:
Real-world impact
TARDIS sh_screen_ui.lua lays out screen buttons with a recursive movebutton(button) that recurses (return movebutton(button)) and otherwise returns w, h; v:SetPos(movebutton(v)) spreads it. On 1.2.0 that fired the two warnings above and failed our --warnings-as-errors build. We added ---@return number, number (which it genuinely is) to clear it.
Low severity in practice - the fix is an annotation you would want anyway - but the inference producing unknown for a self-recursive multi-return, and that unknown then breaking an otherwise-valid spread, looked worth flagging while the multi-return area (#47) is fresh.
Expected
r2's return resolves to (number, number) via the base case, and takesTwo(r2(5)) spreads cleanly - matching the non-recursive and annotated forms, which already are clean.
Environment
glua_check 1.2.0
- annotations
gluals-annotations-prerelease @ aae7341 (not needed for the repro)
- Windows 11
Disclosure: this report was researched and written by Claude Code working in my repositories. Every claim in it was reproduced against a real workspace on 1.2.0 rather than asserted, and the two controls plus a re-run of #47's own cases were checked before filing. The r2 / takesTwo names are a reduction of the real movebutton layout function; the structure (recursive multi-return spread into a call) is the same.
Area:
glua_check(expected to affect the language server too; the repro and diagnostics below are fromglua_check)Summary
A function whose return type resolves to
((1,2)|unknown)cannot be spread into another call's argument list. Theunknownarm comes from an unannotated recursive function: the return type does not reach a fixpoint (the base case isreturn 1, 2), so it is inferred as((1,2)|unknown). Spreading that into a two-parameter call produces two warnings - the first value types as the whole union, and the second parameter is reported missing, as if the arity might be 1.Both are warnings, so this fails
glua_check --warnings-as-errorswith exit 1.This is adjacent to the now-fixed #47, not a regression of it: #47's own cases - a plain multi-return spread, and the
pcallform - are clean on 1.2.0. The surviving trigger here is specifically theunknown-tainted union from the unannotated recursion.Minimal repro
Repro: bug3-recursive-multiret-spread.zip
Standalone - no
workspace.libraryand no GLua annotations..luarc.jsonis only{"runtime":{"version":"LuaJIT"}}.glua_check --warnings-as-errors .on the repro reports exactly (line 28 is thetakesTwo(r2(5))line inrepro.lua, below the header comment):r2always returns two numbers - the recursive arm returnsr2's own result, and the base arm returns1, 2. A fixpoint on the self-reference would give(number, number); instead the recursive arm is left asunknownand taints the union.Two controls that isolate it
Both are in the repro file, and both are clean:
local function two() return 1, 2 end; takesTwo(two())) spreads fine - this is [Bug]: Expanding a multi-return into an argument list reportsexpected X but found X#47's now-fixed case.---@return number, numberspreads fine - annotating the return removes theunknownarm.Real-world impact
TARDIS
sh_screen_ui.lualays out screen buttons with a recursivemovebutton(button)that recurses (return movebutton(button)) and otherwise returnsw, h;v:SetPos(movebutton(v))spreads it. On 1.2.0 that fired the two warnings above and failed our--warnings-as-errorsbuild. We added---@return number, number(which it genuinely is) to clear it.Low severity in practice - the fix is an annotation you would want anyway - but the inference producing
unknownfor a self-recursive multi-return, and thatunknownthen breaking an otherwise-valid spread, looked worth flagging while the multi-return area (#47) is fresh.Expected
r2's return resolves to(number, number)via the base case, andtakesTwo(r2(5))spreads cleanly - matching the non-recursive and annotated forms, which already are clean.Environment
glua_check1.2.0gluals-annotations-prerelease@aae7341(not needed for the repro)Disclosure: this report was researched and written by Claude Code working in my repositories. Every claim in it was reproduced against a real workspace on 1.2.0 rather than asserted, and the two controls plus a re-run of #47's own cases were checked before filing. The
r2/takesTwonames are a reduction of the realmovebuttonlayout function; the structure (recursive multi-return spread into a call) is the same.