Skip to content

Inline bare-primop workers at saturated call sites (#281) - #282

Merged
Unisay merged 4 commits into
mainfrom
issue-281/inline-primop-workers
Jul 16, 2026
Merged

Inline bare-primop workers at saturated call sites (#281)#282
Unisay merged 4 commits into
mainfrom
issue-281/inline-primop-workers

Conversation

@Unisay

@Unisay Unisay commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Closes #281.

Diagnosis

Traced the pipeline (runStepsTraced) on Bench.TupleFold and Golden.SpecConstr.Test as the issue asked. The divergence between add and sub comes down to the dictionary's whole-program use count, not the method's. When the dictionary is referenced exactly once in the übermodule, the pre-uncurry optimize+dce fixpoints resolve the floated binding all the way to a manifest lambda:

add = Data.Semiring.add semiringInt      -- the binding purs floats to module level
    = (λdict. dict.add) semiringInt      -- use-once paste of the accessor
    = { add = λx. λy. x + y,  }.add     -- use-once paste of the (single-use!) dictionary
    = λx. λy. x + y                      -- reduceObjectProp folds the projection

The early uncurry run then splits the now-manifest chain and rewrites the saturated sites to direct n-ary worker calls, and such a call is invisible to inlineSaturatedCall: unwindApp peels only unary App links, so the n-ary node stays whole in head position and matches no rule:

unwindApp (App (App (Ref f) a) b)  =  (Ref f, [a, b])        -- unary spine: rules fire
unwindApp (AppN (Ref add$w) [a, b])  =  (AppN  [a, b], [])  -- worker call: head is not a Ref, nothing fires

When the dictionary is multiply-used instead, the whole-binding paste never claims it; the floated add binding itself dissolves (its RHS is a Deref-tier projection), each site keeps a curried spine over the projection, and the specialize fixpoint folds it:

(semiringInt.add) s i      -- survives curried into the specialize fixpoint
   (λx. λy. x + y) s i    -- resolveDictionaryProp pastes the method
   s + i                  -- betaReduce

That is why add dissolved in Golden.SpecConstr.Test (its semiringInt is shared) while sub survived (its ringInt is single-use), and the mirror image in Bench.TupleFold — the issue's use-count hypothesis failed in both directions because the trigger sits one level up, on the dictionary.

Fix

inlineSaturatedCall gets an n-ary case (the narrow fix the issue anticipated): a saturated n-ary call of a worker whose body is a bare primop over trivial operands (PrimBinOp/Eq/PrimNot thereof, operands Trivial — parameter references or scalar literals) pastes the worker's lambda under the original AppN node, where the exact-arity betaReduce consumes it in the same bottom-up pass:

AppN (Ref add$w) [a, b]
   AppN (λ[x, y]. x + y) [a, b]   -- the paste, under the original AppN node
   a + b                          -- exact-arity betaReduce, same pass

Pasting under the AppN — rather than through rebuildSpine like the unary paths — is what keeps WellApplied intact, the concern pasteableRoot documents: rebuilt as a unary spine the paste would be an under-applied redex the lint rejects and exact-arity beta reduction never repairs:

App (App (λ[x, y]. x + y) a) b   -- one argument against two parameters: stuck

Since every operand of a bare primop body occurs exactly once, beta reduction always substitutes and never leaves a residual Let, so no IIFE can appear at the site.

Verification

  • New unit test reproduces the survival (red before the fix): two saturated sites of add = λx. λy. x + y — two, so the use-once inline cannot mask the bug — now leave no binding behind and fold both exports to the inline PrimBinOp.
  • Golden.SpecConstr.Test's sub$w sites fold to inline - exactly as the issue asks; 46 structural goldens move — workers dissolve, and constant folding cascades where sites turned literal, e.g. add$w(add$w(3, 4), 5)12 in Golden.UncurryCtor.Test. All eval goldens are byte-identical.
  • The tuple_fold loop body (bench/_build/Bench.TupleFold.lua) is the issue's target shape:
-- before (quoted from #281)
go_S_sc1Tuple_S_f1, go_S_sc1Tuple_S_f2 =
  Bench_TupleFold_add_S_w(go_S_sc1Tuple_S_f1, go_S_sc1Tuple_S_f2),
  Bench_TupleFold_add_S_w(go_S_sc1Tuple_S_f2, 1)

-- after
go_S_1_S_sc1Tuple_S_f1, go_S_1_S_sc1Tuple_S_f2 =
  go_S_1_S_sc1Tuple_S_f1 + go_S_1_S_sc1Tuple_S_f2, go_S_1_S_sc1Tuple_S_f2 + 1
  • bench/run on tuple_fold under PUC Lua 5.1: 0.169s → 0.062s against the 0.056s ideal loop, closing the ~3x gap; the other macro benchmarks are unchanged or slightly improved.
  • cabal test all passes, plus randomized specs stressed across five fixed seeds.

@Unisay
Unisay requested a review from Copilot July 16, 2026 10:00
@Unisay Unisay self-assigned this Jul 16, 2026

This comment was marked as outdated.

@Unisay
Unisay force-pushed the issue-281/inline-primop-workers branch from 1a3b751 to a7b7dd6 Compare July 16, 2026 10:18
Unisay added a commit that referenced this pull request Jul 16, 2026
- lib/Language/PureScript/Backend/IR/Optimizer.hs:1651 — reword the
  isBarePrimOpBody Haddock to state the actual criterion (operands
  classified Trivial by complexityOf), which is wider than the
  previously documented "parameter references or scalar literals"
  (#282 (comment))
@Unisay
Unisay enabled auto-merge July 16, 2026 11:39
Unisay added 4 commits July 16, 2026 14:11
)

An n-ary worker call minted by the uncurry split is not a unary spine,
so unwindApp left it whole and inlineSaturatedCall never saw it: a
floated dictionary application (add = Data.Semiring.add semiringInt)
that resolved to a manifest lambda before the early uncurry run kept
paying a Lua function call per arithmetic operation in hot loops. A
worker whose body is a bare primop over trivial operands is now pasted
at every saturated n-ary call site — under the original AppN node, so
the exact-arity beta reduction consumes it in the same pass.
- lib/Language/PureScript/Backend/IR/Optimizer.hs:1651 — reword the
  isBarePrimOpBody Haddock to state the actual criterion (operands
  classified Trivial by complexityOf), which is wider than the
  previously documented "parameter references or scalar literals"
  (#282 (comment))
The dissolved workers drop one function per affected artifact (FNEW
census and prototype counts shrink, trace reports compile one function
fewer at shifted line numbers); every workload result is unchanged.
@Unisay
Unisay force-pushed the issue-281/inline-primop-workers branch from 335a2c8 to 49d0fab Compare July 16, 2026 12:12
@Unisay
Unisay merged commit 332df7c into main Jul 16, 2026
2 checks passed
@Unisay
Unisay deleted the issue-281/inline-primop-workers branch July 16, 2026 12:12
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.

Arithmetic workers survive un-inlined in hot loop bodies

2 participants