You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#227 left one residual closure on the mk half. An effectful uncurried definition whose body is a statement sequence compiles to an n-ary literal that runs its magic-do chunk through an IIFE. From Golden.UncurriedLift.Test, logTwice = mkEffectFn2 \a b -> do log a; log b emits as:
M.Golden_UncurriedLift_Test_logTwice=function(a, b)
localEffect_Console_foreign=M.Effect_Console_foreignreturn (function()
local_=Effect_Console_foreign.log(a)()
returnEffect_Console_foreign.log(b)()
end)()
end
One closure and one extra call per invocation, down from the opaque wrapper's two closures and three calls, but still short of the zero-closure target the pure half reached.
Why the pipeline cannot fix this today
The IR sees AppN (Abs _ (Let …)) [EffectRunArg]: the run of a literal do-chunk thunk. Beta reduction refuses every effect-run redex by design, because merging a chunk into its parent would defeat magic-do's size management (#19). The codegen fusion from #226 lowers (\_ -> fn(a, …)) EffectRunArg straight to fn(a, …), but it is restricted to thunk bodies that are a single AppN, since only those lower to an expression. A statement-sequence body lowers to statements, so the general marker erasure kicks in and emits the (function() … end)() call.
Approach
A Lua-level rewrite, in Language.PureScript.Backend.Lua.Optimizer: when the last statement of a function body is return (function() <stmts> end)(), splice <stmts> in place of the return. Tail position is what makes the splice safe: every return inside <stmts> produced a value the parent returned immediately anyway, so returning from the parent directly is observably the same.
Conditions the rule has to check:
the called literal takes no parameters and <stmts> never references ...;
no local declared in <stmts> collides with a name the tail of the parent scope still reads (uniquification upstream makes IR-derived names distinct, but codegen-introduced locals such as the M-table aliases need an explicit check);
a local budget: the parent's local count plus the spliced locals must stay clearly under Lua's 200-local limit. Magic-do chunked the statements precisely so that no single function accumulates every local (Function at line XXX has more than NNN upvalues #19), and an unconditional splice would undo that chunking one tail call at a time. The M-table history (Function at line XXX has more than NNN upvalues #19 again) says any revisit of this area has to be budget-aware.
The existing foldFieldProjectionThroughScopeCall already pattern-matches the scope-call shape for projections, so the matching machinery has precedent; the new rule differs in splicing statements upward rather than pushing a projection inward.
Prerequisites / Relations
The rule is independent of the lifter and of the marker-driven magic-do redesign tracked in #228: it operates on emitted Lua, after all chunking decisions are made. #227 produced the residual shape; the codegen fusion from #226 covers only single-AppN thunk bodies.
Verification / Measurement
logTwice-shaped definitions (any mkEffectFnN/mkSTFnN with a do-block body) drop to zero closures per call, and any other tail IIFE the codegen produces benefits as well. The splice is visible in Golden.UncurriedLift.Test's golden.lua; eval goldens unchanged.
Problem
#227 left one residual closure on the mk half. An effectful uncurried definition whose body is a statement sequence compiles to an n-ary literal that runs its magic-do chunk through an IIFE. From
Golden.UncurriedLift.Test,logTwice = mkEffectFn2 \a b -> do log a; log bemits as:One closure and one extra call per invocation, down from the opaque wrapper's two closures and three calls, but still short of the zero-closure target the pure half reached.
Why the pipeline cannot fix this today
The IR sees
AppN (Abs _ (Let …)) [EffectRunArg]: the run of a literal do-chunk thunk. Beta reduction refuses every effect-run redex by design, because merging a chunk into its parent would defeat magic-do's size management (#19). The codegen fusion from #226 lowers(\_ -> fn(a, …)) EffectRunArgstraight tofn(a, …), but it is restricted to thunk bodies that are a singleAppN, since only those lower to an expression. A statement-sequence body lowers to statements, so the general marker erasure kicks in and emits the(function() … end)()call.Approach
A Lua-level rewrite, in
Language.PureScript.Backend.Lua.Optimizer: when the last statement of a function body isreturn (function() <stmts> end)(), splice<stmts>in place of thereturn. Tail position is what makes the splice safe: everyreturninside<stmts>produced a value the parent returned immediately anyway, so returning from the parent directly is observably the same.Conditions the rule has to check:
<stmts>never references...;<stmts>collides with a name the tail of the parent scope still reads (uniquification upstream makes IR-derived names distinct, but codegen-introduced locals such as the M-table aliases need an explicit check);The existing
foldFieldProjectionThroughScopeCallalready pattern-matches the scope-call shape for projections, so the matching machinery has precedent; the new rule differs in splicing statements upward rather than pushing a projection inward.Prerequisites / Relations
The rule is independent of the lifter and of the marker-driven magic-do redesign tracked in #228: it operates on emitted Lua, after all chunking decisions are made. #227 produced the residual shape; the codegen fusion from #226 covers only single-
AppNthunk bodies.Verification / Measurement
logTwice-shaped definitions (anymkEffectFnN/mkSTFnNwith a do-block body) drop to zero closures per call, and any other tail IIFE the codegen produces benefits as well. The splice is visible inGolden.UncurriedLift.Test'sgolden.lua; eval goldens unchanged.