Skip to content

Splice a discarded applied function literal, not only a tail-position one #359

Description

@Unisay

Problem

pslua represents an Effect/ST action as a nullary Lua thunk, and the magic-do pass lowers a do block into one such thunk holding a flat statement sequence. When the optimizer inlines an action's body at a call site, the result is a literal thunk that is built and immediately called — a closure allocation and a call whose only purpose is to run statements that could sit in the parent function directly.

The Lua-level optimizer already removes this, budgeted, in one position. collapseTailLiteralApplication (Language.PureScript.Backend.Lua.Optimizer) splices the literal's body into its parent when the merged body still fits the local-variable ceiling — the splice undoes exactly the chunking magic-do uses to keep any one function's locals under Lua 5.1's LUAI_MAXVARS of 200 (#19), so it re-checks that budget rather than assuming it:

collapseTailLiteralApplication  LuaLimits  RewriteRule
collapseTailLiteralApplication limits = \case
  Function params body
    | Just (leading, binding, spliced)  matchTailLiteralApplication body
    , not (chunkScopeUsesVararg spliced)
    , let merged = leading <> binding <> spliced
    , length params + activationLocalSlots merged
        <= workingLocalCeiling limits 

Its matcher, however, only recognises the literal in tail position — a function body whose last statement is a Return of the applied literal:

  matchTailLiteralApplication body = case reverse body of
    Ann (Return [Ann (FunctionCall (Ann (Function params spliced)) args)])
      : reverseLeading  do

A magic-do effect statement is not in tail position. It is a discarded local binding, because the statement's value is unused and only its effect matters, so the inlined action lands as local _ = (function() … end)() and the rule never sees it. From Golden.EffectWorkerThunk, where the two-statement body of runWith was pasted into its one call site:

  local _ = (function()
    local _ = Golden_EffectWorkerThunk_Test_tally_S_w("via", 3)
    return Effect_Console_log("ran")()
  end)()

Two locals in the parent's budget would admit the splice with room to spare, yet the closure is allocated and called every time this statement runs. Across the golden corpus there are 84 applied-literal sites in this discarded-binding position, against 153 in the tail position the existing rule covers.

Approach

Extend the reach of the existing rule (or add a sibling sharing its conditions) to an applied function literal that is the right-hand side of a statement whose value is discarded. Every guard the tail case already checks carries over unchanged and should be reused rather than restated: the vararg guard (chunkScopeUsesVararg), the parameter binding built from the call's arguments, and the length params + activationLocalSlots merged <= workingLocalCeiling limits budget that keeps the splice from undoing magic-do's chunking.

One condition is new, and it is what bounds the change. The tail case splices a body whose value becomes the parent's return value, so the body's return needs no rewriting. In statement position the value is discarded, so the body's trailing return e has to become a discarded statement of e — and any other return in the body, for instance inside an if branch, would after splicing return from the parent function instead of from the thunk. The rewrite must therefore be restricted to a body whose only return is its last statement, which is the shape magic-do itself emits; a body with branching returns keeps its thunk.

Verification / Measurement

The runWith site in Golden.EffectWorkerThunk is the minimal positive case: a two-local body with a single trailing return, whose local _ = (function() … end)() should become the two statements spliced into the enclosing block, with eval/golden.txt unchanged. A golden whose thunk body returns from inside a branch pins the negative side and is the one shape that must keep its closure. Because the parent of a spliced body can itself be spliced, the fixpoint behaviour the tail rule already documents (it re-applies itself to the merged function) needs the same treatment here.

A wall-clock number needs a benchmark whose hot loop executes such a statement; none of the current bench/macro specs does, since an inlined action in a hot loop tends to be in tail position. Whether the win is worth a new spec can be judged from the counter oracles first: each spliced site removes one FNEW from a function body, which bench/tools/fnew_census.lua counts statically and which aborts LuaJIT trace recording (NYI: bytecode FNEW) when it sits in a hot path.

Prerequisites / Relations

Independent. #342 widens a different guard — the code generator's own shed in Language.PureScript.Backend.Lua, which drops a literal thunk applied to the effect-run marker when the thunk's body is a call, and which #342 extends to any body that lowers to an expression. That shed cannot handle a body of statements at all, and explicitly leaves such bodies thunked; this issue is about the Lua-level optimizer that runs afterwards and can splice statements, given the budget. The two are complementary and neither blocks the other.

Metadata

Metadata

Assignees

No one assigned

    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