Skip to content

feat(optimizer): collapse a tail-position IIFE into the enclosing function body (#230) - #293

Merged
Unisay merged 1 commit into
mainfrom
issue-230/collapse-tail-scope-call
Jul 24, 2026
Merged

feat(optimizer): collapse a tail-position IIFE into the enclosing function body (#230)#293
Unisay merged 1 commit into
mainfrom
issue-230/collapse-tail-scope-call

Conversation

@Unisay

@Unisay Unisay commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Closes #230.

Problem

#227 left one residual closure on the mk half: an effectful uncurried definition whose body is a statement sequence runs its magic-do chunk through a tail IIFE. From Golden.UncurriedLift.Test:

local Golden_UncurriedLift_Test_logTwice = function(a_S_673, b_S_674)
  return (function()
    local _ = Effect_Console_log(a_S_673)()
    return Effect_Console_log(b_S_674)()
  end)()
end

The IR cannot fix this (beta reduction refuses effect-run redexes by design to protect magic-do's chunking, and the #226 codegen fusion covers only single-AppN thunk bodies), so this lands as a Lua-level rewrite.

The rule

collapseTailScopeCall in Language.PureScript.Backend.Lua.Optimizer: when a function body's last statement is return (function() <stmts> end)(), splice <stmts> in place of the return:

local Golden_UncurriedLift_Test_logTwice = function(a_S_673, b_S_674)
  local _ = Effect_Console_log(a_S_673)()
  return Effect_Console_log(b_S_674)()
end

Tail position is what makes the splice safe. The parent's return call forwarded all of the call's results (an explicit Paren would adjust them to one and correctly fails the match), so after the splice the inner returns — or falling off the end, for zero results — produce the same values directly. Early returns among the leading statements exit the parent on their own paths either way, so unlike foldFieldProjectionThroughScopeCall the rule needs no early-return decline. The issue's name-collision condition is discharged by construction: the splice point is the parent's last statement, so no parent code follows it, and Lua's positional local scoping keeps every spliced reference resolving exactly as it did inside the closure — a re-declaration of a name the parent already binds is legal and shadows only from that point on.

Two conditions are checked. The spliced statements must not mention ... in their own scope — a no-parameter function cannot legally do so, but on such (only ever hand-written) input the splice would rebind ... to the parent's varargs instead of failing to load; the check reuses chunkScopeUsesVararg, now exported from Lua.Linker.Foreign. And a local budget (#19): the splice undoes exactly the chunking with which magic-do bounds any single function's locals, so the merged body (parameters plus activationLocalSlots, a proto-boundary-respecting over-approximation) must fit the same workingLocalCeiling the storage passes budget against. LuaLimits is threaded through the rewrite chain for this, which is why foldFieldProjectionThroughScopeCall and optimizeExpression now take the parameter.

Applied bottom-up by the existing driver, nested chunk chains collapse as far as the budget allows and no further: in Golden.LongDoBlock.Test the two adjacent magic-do chunks (150 + 149 locals against a ceiling of 180) keep their boundary, so that golden is unchanged.

Golden churn

Exactly one golden moves — Golden.UncurriedLift.Test/golden.lua, the diff shown above. Every other tail IIFE in the corpus is a correct decline: the remaining shapes are result-consuming double calls where the callee is not a function literal, e.g. from Golden.LongApplyChain.Test:

  return (function()
    ...
    return function(v1_S_626) ... end
  end)()(b_S_134)

Eval goldens and .ir goldens are untouched.

Verification

  • 8 new unit tests in Optimizer/Spec.hs: the splice, bottom-up cascading, splicing past a leading early return, zero-value fall-off, and declines for parameterized callees, budget overflow (asserted on both sides of a tiny custom LuaLimits), and activation-scope varargs (with the nested-proto non-decline counterpart).
  • cabal test all passes clean twice (fresh Hedgehog seeds), 1026 examples, 0 failures; fourmolu and hlint clean.

@Unisay Unisay self-assigned this Jul 24, 2026
@Unisay
Unisay merged commit 0f17915 into main Jul 24, 2026
2 checks passed
@Unisay
Unisay deleted the issue-230/collapse-tail-scope-call branch July 24, 2026 09:38
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.

Collapse a tail-position IIFE into the enclosing function body

1 participant