Problem
A recursive function that carries a constructor accumulator allocates the box on every iteration only to take it apart at the top of the next one:
loop :: Tuple Int Int -> Int
loop acc = case acc of
Tuple s i
| i >= n -> s
| otherwise -> loop (Tuple (s + i) (i + 1))
Every iteration builds a fresh Tuple table and the next iteration's match immediately deconstructs it. Fold-shaped loops with Tuple/Maybe/Either accumulators are everyday PureScript, and the #177/#178/#180 cascades will produce more of these shapes as they collapse monadic chains into recursive functions.
Approach
GHC's SpecConstr (call-pattern specialisation): for a recursive binding whose body scrutinizes a parameter and whose recursive call sites pass a known constructor at that position, mint a specialized copy taking the constructor's fields as separate parameters, and rewrite those sites:
-- specialized worker: zero allocations per iteration
local loop_s = function(s, i)
if i >= n then return s else return loop_s(s + i, i + 1) end
end
-- entry wrapper stays: a boxed accumulator can still arrive from outside
local loop = function(acc) return loop_s(acc.value0, acc.value1) end
The machinery is the #24 discipline reapplied (mint a copy, rewrite the qualifying sites, keep the wrapper for the rest), with beta plus the #177 fold cleaning up the specialized body. Guards learned from GHC:
Prerequisites / Relations
Hard prerequisite: #177, the specialized body's deconstruction folds away through it. Composes with the landed #24 split (specialization applies to n-ary workers) and with #181 (after loopification, per-iteration allocation is the main remaining cost in these loops). #206 is the result-side dual: together they pass raw components through a loop with no box materialized in either direction.
Parked: pick this up once #177/#178/#180 have landed and the #172 counters show constructor allocations inside hot recursions.
Verification / Measurement
An allocation counter on a Tuple-accumulator fold benchmark drops to zero tables per iteration, eval goldens stay byte-identical, and golden.lua sizes confirm the code-growth cap holds.
Problem
A recursive function that carries a constructor accumulator allocates the box on every iteration only to take it apart at the top of the next one:
Every iteration builds a fresh
Tupletable and the next iteration's match immediately deconstructs it. Fold-shaped loops withTuple/Maybe/Eitheraccumulators are everyday PureScript, and the #177/#178/#180 cascades will produce more of these shapes as they collapse monadic chains into recursive functions.Approach
GHC's SpecConstr (call-pattern specialisation): for a recursive binding whose body scrutinizes a parameter and whose recursive call sites pass a known constructor at that position, mint a specialized copy taking the constructor's fields as separate parameters, and rewrite those sites:
The machinery is the #24 discipline reapplied (mint a copy, rewrite the qualifying sites, keep the wrapper for the rest), with beta plus the #177 fold cleaning up the specialized body. Guards learned from GHC:
-fspec-constr-countanalogue);Prerequisites / Relations
Hard prerequisite: #177, the specialized body's deconstruction folds away through it. Composes with the landed #24 split (specialization applies to n-ary workers) and with #181 (after loopification, per-iteration allocation is the main remaining cost in these loops). #206 is the result-side dual: together they pass raw components through a loop with no box materialized in either direction.
Parked: pick this up once #177/#178/#180 have landed and the #172 counters show constructor allocations inside hot recursions.
Verification / Measurement
An allocation counter on a Tuple-accumulator fold benchmark drops to zero tables per iteration, eval goldens stay byte-identical, and
golden.luasizes confirm the code-growth cap holds.