Skip to content

Collapse a tail-position IIFE into the enclosing function body #230

Description

@Unisay

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 b emits as:

M.Golden_UncurriedLift_Test_logTwice = function(a, b)
  local Effect_Console_foreign = M.Effect_Console_foreign
  return (function()
    local _ = Effect_Console_foreign.log(a)()
    return Effect_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.

Metadata

Metadata

Assignees

Labels

OptimisationA Compiler Optimisationarea: codegenLua code generation / printingenhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions