Skip to content

feat(optimizer): splice a tail-position application of a function literal - #296

Merged
Unisay merged 2 commits into
mainfrom
issue-295/splice-tail-literal-application
Jul 24, 2026
Merged

feat(optimizer): splice a tail-position application of a function literal#296
Unisay merged 2 commits into
mainfrom
issue-295/splice-tail-literal-application

Conversation

@Unisay

@Unisay Unisay commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Closes #295.

What

Generalizes the tail-position IIFE collapse (#230) to an applied function literal with named parameters, and renames the rule accordingly: collapseTailScopeCallcollapseTailLiteralApplication. When a function body ends in return (function(p1, …) <stmts> end)(a1, …), the rule now binds the parameters as one simultaneous local and splices the body; the nullary shape degenerates to no binding statement, so #230's behaviour is the special case. On Golden.LongApplyChain.Test's applySecond — the shape PR #294 deliberately left behind, one closure allocation and one extra call per node of the 40-deep apply chain:

return (function(v1_S_626)
  if "Data.Maybe∷Maybe.Just" == v_S_625[1] then
    ...
  end
end)(b_S_134)

becomes straight-line:

local v1_S_626 = b_S_134
if "Data.Maybe∷Maybe.Just" == v_S_625[1] then
  ...
end

Why the local is exact

The one-statement local p1, … = a1, … is the literal translation of Lua's call binding: the whole initializer list evaluates before any name binds (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 evaluate and drop — the same adjustment rules the call performed. Unlike #294's fold, the arguments cross no scope boundary: the call site already was the parent's last statement, so they evaluate in the same environment at the same program point — which is also why ... among the arguments needs no check here.

Conditions on top of #230's (vararg check on the spliced statements, the #19 local budget — now counting the parameter binding — and the self-re-application): every parameter is ParamNamed with no name repeating, and a nullary literal applied to arguments declines (no local binds zero names, and dropping the arguments would drop their effects).

The second commit: re-collapse in expression position

The first commit alone moved only 3 goldens; Golden.Issue37.Test and Golden.StringCodePoints.Test — both named in #295 — did not budge. The reason: their redexes sit in scope calls in expression position (a table row, an == operand), and the tail return (function(fn1) … end)(f) inside those scope calls is built by foldCallThroughScopeCall itself at a depth the bottom-up driver has already passed — the callee literal is visited before the enclosing call node, so nobody revisits the rebuilt literal. A literal in a function tail recovers when the driver reaches the enclosing function (collapseTailLiteralApplication fires there and re-applies); a literal in expression position had no later chance.

The fold now hands each literal it rebuilds to collapseTailLiteralApplication directly — the same eager re-optimization foldFieldProjectionThroughScopeCall already does with its projected return, and the reason the fold now takes LuaLimits. In Golden.Issue37.Test:

[1] = (function()
  local Bind1_S_216 = Effect_monadEffect.Bind1()
  return (function(fn1_S_218)
    return Bind1_S_216.bind(fn1_S_218)(function()
      ...
    end)
  end)(f_S_6)
end)()

becomes

[1] = (function()
  local Bind1_S_216 = Effect_monadEffect.Bind1()
  local fn1_S_218 = f_S_6
  return Bind1_S_216.bind(fn1_S_218)(function()
    ...
  end)
end)()

In Golden.LongWriterBind.Test the chain composes end-to-end — fold, collapse, projection fold, table-constructor fold — reducing ((function(dictApplicative_S_518) return { pure = …, Apply0 = … } end)(Data_Identity_applicativeIdentity)).pure(42) to

local dictApplicative_S_518 = Data_Identity_applicativeIdentity
local a_S_519 = 42
return dictApplicative_S_518.pure(Data_Tuple_Tuple_S_w(a_S_519, dictMonoid_S_517.mempty))

The folded-away Apply0 row held the only read of Control_Monad_Writer_Trans_applyWriterT_S_w (2 occurrences on main → 1 now: just the initializer), so promoteChunk correctly declines to promote a zero-read binding — that is why local M = {} reappears in this golden with the now-dead initializer parked as M.Control_Monad_Writer_Trans_applyWriterT_S_w = …. One golden also lost a nesting level that main's own output would have collapsed if re-fed to the optimizer (Golden.ArrayOfUnits.Test's main): the nesting was fold residue at a passed depth, the class this commit removes.

Verification

  • 20 unit tests for the rule pair, written red-first: the parameterized splice, nil-fill via local x, y = 1, the bare-rule re-collapse through a parameterized merge, declines (nullary-with-arguments, vararg/unused/duplicate parameters, budget counting the bound parameters), and the fold's re-collapse in expression position. One existing fold expectation updated: the nested-scope-call re-fold now flattens to a single scope call (strictly better output, same purpose).
  • 7 golden.lua moved across the two commits (LongApplyChain, BugListGenericEq, ArrayOfUnits, Issue37, StringCodePoints, LongWriterBind); golden.ir untouched. All eval goldens byte-identical — the semantic oracle is never auto-accepted.
  • bench/ci --accept produced no diffs: the bench modules carried none of the residual shapes after feat(optimizer): fold a trailing call through a scope call #294.
  • cabal test all clean (1044 examples), fourmolu + hlint clean.

Unisay added 2 commits July 24, 2026 13:49
…eral (#295)

Generalize collapseTailScopeCall to an applied function literal with
named parameters: `return (function(p1, …) <stmts> end)(a1, …)` in tail
position splices as `local p1, … = a1, …; <stmts>`, the simultaneous
local reproducing Lua's call binding exactly. The nullary shape (#230)
degenerates to no binding statement. Renamed to
collapseTailLiteralApplication accordingly.
The pushed application may land on a returned function literal — a
beta-redex built at a depth the bottom-up driver has already passed.
A literal in a function tail re-collapses when the driver reaches the
enclosing function, but one in expression position (a table row, an
operand) had no later chance. foldCallThroughScopeCall now hands each
literal it rebuilds to collapseTailLiteralApplication directly.
@Unisay Unisay self-assigned this Jul 24, 2026
@Unisay
Unisay merged commit 3e48df3 into main Jul 24, 2026
2 checks passed
@Unisay
Unisay deleted the issue-295/splice-tail-literal-application branch July 24, 2026 14:16
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.

Splice a tail-position application of a function literal into the enclosing body

1 participant