Skip to content

Dissolve small pure workers into saturated call sites (#211) - #285

Merged
Unisay merged 3 commits into
mainfrom
issue-211/inline-small-pure-bindings
Jul 20, 2026
Merged

Dissolve small pure workers into saturated call sites (#211)#285
Unisay merged 3 commits into
mainfrom
issue-211/inline-small-pure-bindings

Conversation

@Unisay

@Unisay Unisay commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Closes #211.

Problem

Issue #211 predates two changes that reshaped its landscape: the Capture/Complexity inlining lattices (#231) and the bare-primop worker unfolding (#281). #281 already dissolves sub$w(v, 1) into v - 1, but its predicate demands a single primop node over Trivial operands, so anything one step richer kept paying a Lua call per saturated site:

local Golden_Uncurry_Test_alwaysFirst_S_w = function(x) return x end
local Golden_Uncurry_Test_adderOf_S_w = function(x, y)
  return function(y_S_229) return x + y + y_S_229 end
end
local Golden_Uncurry_Test_add3_S_w = function(x, y, z) return x + y + z end

(x + y) + z is a nested primop (the left operand is not Trivial), return x is not a primop at all, and a projection-chain operand like r.foo prices Deref, one notch above Trivial. All three declined.

Change

isBarePrimOpBody becomes isCheapWorkerBody: a tree of primops, equality tests and negations over leaves complexityOf classifies at most Deref (parameter references, scalar literals, cheap projection chains), possibly under nested lambdas. The paste is additionally bounded by smallInlineBudget — the constant whose documented meaning is exactly "duplication at every use site":

isCheapWorkerBody  RawExp ann  Bool
isCheapWorkerBody = \case
  PrimBinOp _ _ a b  isCheapWorkerBody a && isCheapWorkerBody b
  Eq _ a b  isCheapWorkerBody a && isCheapWorkerBody b
  PrimNot _ e  isCheapWorkerBody e
  AbsN _ _params body  isCheapWorkerBody body
  leaf  complexityOf leaf <= Deref

Everything else rides on existing machinery: the n-ary AbsN root is pasted under the original AppN, the exact-arity betaReduce consumes it in the same pass, and constantFolding finishes the job where arguments are literal. The former call sites in Golden.Uncurry.Test:

-- before
local _ = Effect_Console_logShow_S_w(Data_Show_showInt, Golden_Uncurry_Test_add3_S_w(1, 2, 3))()
local _ = Effect_Console_logShow_S_w(Data_Show_showInt, Golden_Uncurry_Test_adderOf_S_w(1, 2)(3))()
local _ = Effect_Console_logShow_S_w(Data_Show_showInt, Golden_Uncurry_Test_alwaysFirst_S_w(7, 8))()
-- after
local _ = Effect_Console_logShow_S_w(Data_Show_showInt, 6)()
local _ = Effect_Console_logShow_S_w(Data_Show_showInt, 6)()
local _ = Effect_Console_logShow_S_w(Data_Show_showInt, 7)()

and the partially-applied residual closure folds its constants too:

-- before
Data_Functor_foreign.arrayMap(function(add3_S_p3_S_258)
  return Golden_Uncurry_Test_add3_S_w(1, 2, add3_S_p3_S_258)
end)
-- after
Data_Functor_foreign.arrayMap(function(add3_S_p3_S_259)
  return 3 + add3_S_p3_S_259
end)

All three workers above are gone from the output entirely: once every saturated site is inlined, DCE drops the worker, and the late uncurry run leaves the surviving manifest chain alone because a binding with no saturated site is never split.

What deliberately stays shared

IfThenElse bodies are not admitted, deviating from the issue's original allowed list: an IfThenElse in expression position lowers through chunkToExpression to a per-call IIFE — the exact allocation the case-of-case rules of #203 exist to remove — so pasting a decision tree would trade a named call for a closure allocation plus a call. Application bodies (sumTo$w, evenSteps$w, logShow$w, the monad workers), allocating bodies (Ctor, non-empty literals, Let) and oversized trees also keep sharing, each declining through the complexityOf fallback or the budget. Value-position uses are untouched by construction: the rule fires only at saturated AppN heads, so the sharing-vs-Capture concern that #231 codified for the local tiers never arises (no lambda literal lands in a branch or closure position — it is consumed by betaReduce on the spot).

The issue's original sketch — extending the shared isInlinableExpr guard so the binding dissolves before uncurry — would have bypassed the CaptureNone gate of isDuplicatableClosedAbs and pasted lambda literals into value positions (the #204 trace-abort pathology). Keeping the admission per-call-site delivers the same end state for call uses while leaving every value use a shared reference, so this lands as a completion of #211 in the post-#231/#281 architecture rather than a literal transcription.

Testing

Unit tests mirror the #281 spec block: three positives (nested primop tree, parameter-selecting body, Deref-leaf operands — red before the change) and three negatives pinning the exclusions (application body, branching body, over-budget tree — green before and after). Golden churn is limited to Golden.Uncurry.Test, Golden.UncurriedLift.Test and Golden.Unbinding.Test; every eval oracle passes unchanged, and the full suite is stable across four hspec seeds (1, 42, 20260716, 999983).

Unisay added 2 commits July 16, 2026 15:13
#211)

The bare-primop unfolding (#281) required a single primop node over
Trivial operands, so a worker like add3$w(x, y, z) = (x + y) + z — or
one whose body is a bare parameter reference or a projection chain —
kept costing a Lua call per saturated site. The predicate now admits a
tree of primops, equality tests and negations over leaves of complexity
at most Deref, possibly under nested lambdas, with the paste bounded by
smallInlineBudget. IfThenElse bodies stay excluded: a decision tree in
expression position lowers to a per-call IIFE (issue #203), worse than
the shared worker call.
Uncurry.Test loses add3$w / alwaysFirst$w / adderOf$w — every
saturated site folds to the inline expression and constant arguments
fold at compile time (add3$w(1, 2, 3) emits 6, adderOf$w(1, 2)(3)
emits 6). UncurriedLift.Test folds the lifted uncurried add3 sites the
same way, and Unbinding.Test computes its constant-bodied worker's
result outright. Recursive workers (evenSteps$w, oddSteps$w, go$w)
keep sharing: their bodies apply a function. Eval oracles unchanged.

This comment was marked as outdated.

@Unisay Unisay self-assigned this Jul 16, 2026
- lib/Language/PureScript/Backend/IR/Optimizer.hs:1623 — use strict <
  against smallInlineBudget, matching isCheapProjection and
  isDuplicatableClosedAbs
  (#285 (comment))
- lib/Language/PureScript/Backend/IR/Optimizer.hs:1663 — reword
  isCheapWorkerBody haddock: allocation parity with the worker call,
  not "no allocation"
  (#285 (comment))
@Unisay
Unisay merged commit a2cb6c9 into main Jul 20, 2026
2 checks passed
@Unisay
Unisay deleted the issue-211/inline-small-pure-bindings branch July 20, 2026 07:32
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.

Inline small pure function bindings regardless of use count

2 participants