Problem
pslua represents an Effect/ST action as a nullary Lua thunk, and the magic-do pass lowers a do block into one such thunk holding a flat statement sequence. When the optimizer inlines an action's body at a call site, the result is a literal thunk that is built and immediately called — a closure allocation and a call whose only purpose is to run statements that could sit in the parent function directly.
The Lua-level optimizer already removes this, budgeted, in one position. collapseTailLiteralApplication (Language.PureScript.Backend.Lua.Optimizer) splices the literal's body into its parent when the merged body still fits the local-variable ceiling — the splice undoes exactly the chunking magic-do uses to keep any one function's locals under Lua 5.1's LUAI_MAXVARS of 200 (#19), so it re-checks that budget rather than assuming it:
collapseTailLiteralApplication ∷ LuaLimits → RewriteRule
collapseTailLiteralApplication limits = \case
Function params body
| Just (leading, binding, spliced) ← matchTailLiteralApplication body
, not (chunkScopeUsesVararg spliced)
, let merged = leading <> binding <> spliced
, length params + activationLocalSlots merged
<= workingLocalCeiling limits →
Its matcher, however, only recognises the literal in tail position — a function body whose last statement is a Return of the applied literal:
matchTailLiteralApplication body = case reverse body of
Ann (Return [Ann (FunctionCall (Ann (Function params spliced)) args)])
: reverseLeading → do
A magic-do effect statement is not in tail position. It is a discarded local binding, because the statement's value is unused and only its effect matters, so the inlined action lands as local _ = (function() … end)() and the rule never sees it. From Golden.EffectWorkerThunk, where the two-statement body of runWith was pasted into its one call site:
local _ = (function()
local _ = Golden_EffectWorkerThunk_Test_tally_S_w("via", 3)
return Effect_Console_log("ran")()
end)()
Two locals in the parent's budget would admit the splice with room to spare, yet the closure is allocated and called every time this statement runs. Across the golden corpus there are 84 applied-literal sites in this discarded-binding position, against 153 in the tail position the existing rule covers.
Approach
Extend the reach of the existing rule (or add a sibling sharing its conditions) to an applied function literal that is the right-hand side of a statement whose value is discarded. Every guard the tail case already checks carries over unchanged and should be reused rather than restated: the vararg guard (chunkScopeUsesVararg), the parameter binding built from the call's arguments, and the length params + activationLocalSlots merged <= workingLocalCeiling limits budget that keeps the splice from undoing magic-do's chunking.
One condition is new, and it is what bounds the change. The tail case splices a body whose value becomes the parent's return value, so the body's return needs no rewriting. In statement position the value is discarded, so the body's trailing return e has to become a discarded statement of e — and any other return in the body, for instance inside an if branch, would after splicing return from the parent function instead of from the thunk. The rewrite must therefore be restricted to a body whose only return is its last statement, which is the shape magic-do itself emits; a body with branching returns keeps its thunk.
Verification / Measurement
The runWith site in Golden.EffectWorkerThunk is the minimal positive case: a two-local body with a single trailing return, whose local _ = (function() … end)() should become the two statements spliced into the enclosing block, with eval/golden.txt unchanged. A golden whose thunk body returns from inside a branch pins the negative side and is the one shape that must keep its closure. Because the parent of a spliced body can itself be spliced, the fixpoint behaviour the tail rule already documents (it re-applies itself to the merged function) needs the same treatment here.
A wall-clock number needs a benchmark whose hot loop executes such a statement; none of the current bench/macro specs does, since an inlined action in a hot loop tends to be in tail position. Whether the win is worth a new spec can be judged from the counter oracles first: each spliced site removes one FNEW from a function body, which bench/tools/fnew_census.lua counts statically and which aborts LuaJIT trace recording (NYI: bytecode FNEW) when it sits in a hot path.
Prerequisites / Relations
Independent. #342 widens a different guard — the code generator's own shed in Language.PureScript.Backend.Lua, which drops a literal thunk applied to the effect-run marker when the thunk's body is a call, and which #342 extends to any body that lowers to an expression. That shed cannot handle a body of statements at all, and explicitly leaves such bodies thunked; this issue is about the Lua-level optimizer that runs afterwards and can splice statements, given the budget. The two are complementary and neither blocks the other.
Problem
psluarepresents anEffect/STaction as a nullary Lua thunk, and the magic-do pass lowers adoblock into one such thunk holding a flat statement sequence. When the optimizer inlines an action's body at a call site, the result is a literal thunk that is built and immediately called — a closure allocation and a call whose only purpose is to run statements that could sit in the parent function directly.The Lua-level optimizer already removes this, budgeted, in one position.
collapseTailLiteralApplication(Language.PureScript.Backend.Lua.Optimizer) splices the literal's body into its parent when the merged body still fits the local-variable ceiling — the splice undoes exactly the chunking magic-do uses to keep any one function's locals under Lua 5.1'sLUAI_MAXVARSof 200 (#19), so it re-checks that budget rather than assuming it:Its matcher, however, only recognises the literal in tail position — a function body whose last statement is a
Returnof the applied literal:A magic-do effect statement is not in tail position. It is a discarded
localbinding, because the statement's value is unused and only its effect matters, so the inlined action lands aslocal _ = (function() … end)()and the rule never sees it. FromGolden.EffectWorkerThunk, where the two-statement body ofrunWithwas pasted into its one call site:Two locals in the parent's budget would admit the splice with room to spare, yet the closure is allocated and called every time this statement runs. Across the golden corpus there are 84 applied-literal sites in this discarded-binding position, against 153 in the tail position the existing rule covers.
Approach
Extend the reach of the existing rule (or add a sibling sharing its conditions) to an applied function literal that is the right-hand side of a statement whose value is discarded. Every guard the tail case already checks carries over unchanged and should be reused rather than restated: the vararg guard (
chunkScopeUsesVararg), the parameter binding built from the call's arguments, and thelength params + activationLocalSlots merged <= workingLocalCeiling limitsbudget that keeps the splice from undoing magic-do's chunking.One condition is new, and it is what bounds the change. The tail case splices a body whose value becomes the parent's return value, so the body's
returnneeds no rewriting. In statement position the value is discarded, so the body's trailingreturn ehas to become a discarded statement ofe— and any otherreturnin the body, for instance inside anifbranch, would after splicing return from the parent function instead of from the thunk. The rewrite must therefore be restricted to a body whose onlyreturnis its last statement, which is the shape magic-do itself emits; a body with branching returns keeps its thunk.Verification / Measurement
The
runWithsite inGolden.EffectWorkerThunkis the minimal positive case: a two-local body with a single trailing return, whoselocal _ = (function() … end)()should become the two statements spliced into the enclosing block, witheval/golden.txtunchanged. A golden whose thunk body returns from inside a branch pins the negative side and is the one shape that must keep its closure. Because the parent of a spliced body can itself be spliced, the fixpoint behaviour the tail rule already documents (it re-applies itself to the merged function) needs the same treatment here.A wall-clock number needs a benchmark whose hot loop executes such a statement; none of the current
bench/macrospecs does, since an inlined action in a hot loop tends to be in tail position. Whether the win is worth a new spec can be judged from the counter oracles first: each spliced site removes oneFNEWfrom a function body, whichbench/tools/fnew_census.luacounts statically and which aborts LuaJIT trace recording (NYI: bytecode FNEW) when it sits in a hot path.Prerequisites / Relations
Independent. #342 widens a different guard — the code generator's own shed in
Language.PureScript.Backend.Lua, which drops a literal thunk applied to the effect-run marker when the thunk's body is a call, and which #342 extends to any body that lowers to an expression. That shed cannot handle a body of statements at all, and explicitly leaves such bodies thunked; this issue is about the Lua-level optimizer that runs afterwards and can splice statements, given the budget. The two are complementary and neither blocks the other.