feat(optimizer): fold a trailing call through a scope call - #294
Merged
Conversation
This was referenced Jul 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #293 (merge that first; this PR's base is
issue-230/collapse-tail-scope-call).Problem
#293 collapses a scope call sitting directly in tail position, but the corpus's dominant residual shape is one step removed: a scope call whose result is immediately applied — "pick a thunk, then run it". From
Golden.UncurryEffect.Test:Here the applied call
(…)()(), not the scope call itself, is the tail expression, so the #293 rule correctly declines, and every invocation still pays a closure allocation and an extra call.The rule
foldCallThroughScopeCallrewrites(function() …; return e end)()(args)to(function() …; return e(args) end)(), pushing the trailing application into the tail returns — recursively through a branching tail (IfThenElse/Do), which is exactly where thunk selection lives. Evaluation order is preserved: the leading statements run first, the returned expression is evaluated before the arguments in both forms, andreturn e(args)is a tail call, so even the activation depth at the moment the result runs is unchanged.The rule declines on: an early
returnamong leading statements (a path the fold would not cover); a fall-off path in the tail chain (the original callsniland errors, a partial fold would returnnil); a multi-valued tail return (the call consumes the first value only, but the fold cannot drop the other results' effects); arguments mentioning...(rebound inside the no-parameter callee); an argument free name that a body local would capture (checked via the newdeclaredNamesInActivationagainstnamesInBlock, now imported fromLocalize); and non-atomic arguments on a branching tail — they are duplicated syntactically into every return site (still evaluated at most once, since a single branch runs), so only names and literals keep that duplication trivial.The exposed plain scope call is then spliced away by
collapseTailScopeCallfrom #293. That required one amendment: the fold builds tail scope calls at depths the bottom-up driver has already passed, socollapseTailScopeCallnow re-applies itself to the merged function, re-checking the vararg and budget conditions each round. Without it,Golden.UncurryCtor.Test's main body kept one residual IIFE level; with it, the shape above becomes:containsReturnis factored out offoldFieldProjectionThroughScopeCall'swhereclause to the module level, since both folds need the same early-return test.Golden churn
Thirteen
golden.luafiles shrink;.irand eval goldens are untouched. Highlights:UncurryEffect.countdown_S_w(above) reaches a zero-closure tail;TailRecM2Shadow'suntilEpredicate drops its per-iteration selection closure;LongApplyChain.applySecond_S_wandBugListGenericEq.eqlose the wrapping scope call around their curried result (the remaining(function(v1) … end)(b)beta-redex is a candidate for a separate literal-application splice, not attempted here); the long-bind family (LongEitherBind,LongExceptBind,LongStackBind,LongWriterBind,RecGroupOrder,UncurryCtor,ArrayOfUnits,Issue37,StringCodePoints) runs its final effect directly instead of through a returned thunk.Verification
Optimizer/Spec.hscovering the fold (plain tail, branching tail, nested re-fold, and each decline), plus a bare-rule test for thecollapseTailScopeCallself-re-application and an end-to-end composition test throughoptimizeStatement.cabal test allpasses clean twice (fresh Hedgehog seeds), 1037 examples, 0 failures — eval goldens byte-identical, luacheck and theluac/interpreter differential specs green;fourmoluandhlintclean.