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
#293 and #294 eliminated the scope-call closures, but the trailing-call fold stops one step short: when the scope call's tail returned a function literal, the pushed application lands on that literal and stays there. From Golden.LongApplyChain.Test on current main, applySecond = lift2 (const identity) compiles to:
The (function(v1_S_626) … end)(b_S_134) tail is a beta-redex in Lua clothing: one closure allocation and one extra call per invocation — per chain node in the 40-deep apply chain of that golden. The same shape sits in Golden.BugListGenericEq.Test's eq (per list-element comparison), in Golden.StringCodePoints.Test's unsafeCodePointAt0 ordering scope ((function(y_S_1601) … end)(1)), and in Golden.Issue37.Test / Golden.ArrayOfUnits.Test.
Why the pipeline cannot fix this today
collapseTailScopeCall (#230) pattern-matches a nullary callee — FunctionCall (Function [] …) [] — because splicing a parameterized call needs a binding for the parameters, not just a statement splice. foldCallThroughScopeCall (#294) is what builds these shapes: it moves the application inward onto the returned literal, but it does not reduce it. And the IR's own beta reduction cannot see them — these literals only materialize during lowering and Lua-level rewriting.
Approach
Generalize collapseTailScopeCall: when the last statement of a function body is return (function(p1, …, pn) <stmts> end)(a1, …, an) and every parameter is named, splice as a single simultaneous binding followed by the body:
localp1, …, pn=a1, …, an<stmts>
The one-statement local is the exact translation of Lua's call binding: the whole right-hand list is evaluated before any name binds (so an argument reading an outer x can never see a parameter named x), a multi-valued expression last in the list expands, missing values fill with nil, and extra values are evaluated then discarded — the same adjustment rules a call performs. And unlike #294's fold, the arguments do not move across any scope boundary: the call site already was the parent's tail return, so they are evaluated in the same environment at the same program point. The tail-position safety argument from #230 carries over verbatim for <stmts>.
Conditions on top of the existing rule:
every parameter is ParamNamed: a ParamVararg cannot be bound by a local, and ParamUnused has no name to bind (IR-derived literals always name their parameters, so declining costs nothing);
zero parameters with nonempty arguments declines (local = a1 does not exist); zero parameters with zero arguments is exactly the current rule, so the natural implementation is a generalization where the binding statement degenerates to nothing;
the vararg check on <stmts> and the self-re-application after the merge are inherited unchanged.
A duplicate parameter name (function(x, x)) binds the last occurrence in Lua, and local x, x = a, b reproduces exactly that; still, declining on duplicates is one ordNub away and removes the need to reason about it.
The alias local this leaves behind (local v1_S_626 = b_S_134 above) is an accepted cost: one register slot inside the budget, no allocation, no call. Cleaning single-use aliases is a separate (IR- or Lua-level copy-propagation) concern.
Prerequisites / Relations
Builds directly on #230 (PR #293) and PR #294 — the fold is what exposes the redex in tail position. Independent of the magic-do redesign tracked in #228. Non-tail occurrences (a literal application in a local initializer, e.g. the v_S_625 header above) are out of scope: splicing those needs return-to-assignment rewriting, a different transform.
Verification / Measurement
applySecond_S_w becomes straight-line (zero closures per invocation):
The splice is visible in the LongApplyChain, BugListGenericEq, StringCodePoints, Issue37 and ArrayOfUnits goldens; eval goldens unchanged. The bench/ci FNEW census should drop wherever the shape sits in a function body, with the corresponding trace-abort sites disappearing from the trace reports.
Problem
#293 and #294 eliminated the scope-call closures, but the trailing-call fold stops one step short: when the scope call's tail returned a function literal, the pushed application lands on that literal and stays there. From
Golden.LongApplyChain.Teston current main,applySecond = lift2 (const identity)compiles to:The
(function(v1_S_626) … end)(b_S_134)tail is a beta-redex in Lua clothing: one closure allocation and one extra call per invocation — per chain node in the 40-deep apply chain of that golden. The same shape sits inGolden.BugListGenericEq.Test'seq(per list-element comparison), inGolden.StringCodePoints.Test'sunsafeCodePointAt0ordering scope ((function(y_S_1601) … end)(1)), and inGolden.Issue37.Test/Golden.ArrayOfUnits.Test.Why the pipeline cannot fix this today
collapseTailScopeCall(#230) pattern-matches a nullary callee —FunctionCall (Function [] …) []— because splicing a parameterized call needs a binding for the parameters, not just a statement splice.foldCallThroughScopeCall(#294) is what builds these shapes: it moves the application inward onto the returned literal, but it does not reduce it. And the IR's own beta reduction cannot see them — these literals only materialize during lowering and Lua-level rewriting.Approach
Generalize
collapseTailScopeCall: when the last statement of a function body isreturn (function(p1, …, pn) <stmts> end)(a1, …, an)and every parameter is named, splice as a single simultaneous binding followed by the body:The one-statement
localis the exact translation of Lua's call binding: the whole right-hand list is evaluated before any name binds (so an argument reading an outerxcan never see a parameter namedx), a multi-valued expression last in the list expands, missing values fill withnil, and extra values are evaluated then discarded — the same adjustment rules a call performs. And unlike #294's fold, the arguments do not move across any scope boundary: the call site already was the parent's tail return, so they are evaluated in the same environment at the same program point. The tail-position safety argument from #230 carries over verbatim for<stmts>.Conditions on top of the existing rule:
ParamNamed: aParamVarargcannot be bound by alocal, andParamUnusedhas no name to bind (IR-derived literals always name their parameters, so declining costs nothing);local = a1does not exist); zero parameters with zero arguments is exactly the current rule, so the natural implementation is a generalization where the binding statement degenerates to nothing;<stmts>and the self-re-application after the merge are inherited unchanged.A duplicate parameter name (
function(x, x)) binds the last occurrence in Lua, andlocal x, x = a, breproduces exactly that; still, declining on duplicates is oneordNubaway and removes the need to reason about it.The alias local this leaves behind (
local v1_S_626 = b_S_134above) is an accepted cost: one register slot inside the budget, no allocation, no call. Cleaning single-use aliases is a separate (IR- or Lua-level copy-propagation) concern.Prerequisites / Relations
Builds directly on #230 (PR #293) and PR #294 — the fold is what exposes the redex in tail position. Independent of the magic-do redesign tracked in #228. Non-tail occurrences (a literal application in a
localinitializer, e.g. thev_S_625header above) are out of scope: splicing those needs return-to-assignment rewriting, a different transform.Verification / Measurement
applySecond_S_wbecomes straight-line (zero closures per invocation):The splice is visible in the
LongApplyChain,BugListGenericEq,StringCodePoints,Issue37andArrayOfUnitsgoldens; eval goldens unchanged. Thebench/ciFNEW census should drop wherever the shape sits in a function body, with the corresponding trace-abort sites disappearing from the trace reports.