Problem
The cell unboxing of #239 is a per-binding analysis: a cell qualifies only when every occurrence in its own Let scope is a recognised operation. A cell passed to a named function is a whole-value occurrence, so it stays boxed even when the callee only ever applies recognised operations to its parameter. Bench.RefLoop (the shape #239 targets) unboxes, but Bench.EffectStep — the same hot ST loop with the step factored into functions — does not, because ref flows into step/go as an argument:
go :: forall r. Int -> STRef r Int -> ST r Int
go i ref = do
step ref
if i <= 1 then STRef.read ref else go (i - 1) ref
run :: Int -> Int
run n = ST.run do
ref <- STRef.new 0
go n ref
Every iteration therefore keeps the cell-table field accesses (and, in step, the boxed read/write foreign calls), while the structurally identical inlined loop is straight-line arithmetic on a local after #239.
Approach
Interprocedural extension: a per-function summary saying "this parameter is only used through recognised cell operations (and passed to parameters with the same property)", computed bottom-up over the call graph with recursive groups resolved to a fixpoint. A cell then unboxes when every occurrence is a recognised operation or an argument in a summarised-safe position, and the callee is specialised (worker/wrapper style) to take the raw value with writes returned — which for ST is exactly a state-threading transformation. Substantially more machinery than #239; worth measuring demand first (the Bench.EffectStep counters give the baseline).
Relations
Extends #239. The specialisation half overlaps with the uncurrying worker/wrapper infrastructure (#200) and the CPR result split (#206), which already know how to rewrite call sites to workers with different conventions.
Verification
Bench.EffectStep's committed TNEW/trace oracles move: the cell table disappears and the hot loop trace-compiles. A golden pins a factored ref loop compiling without {value = …} tables.
Problem
The cell unboxing of #239 is a per-binding analysis: a cell qualifies only when every occurrence in its own
Letscope is a recognised operation. A cell passed to a named function is a whole-value occurrence, so it stays boxed even when the callee only ever applies recognised operations to its parameter.Bench.RefLoop(the shape #239 targets) unboxes, butBench.EffectStep— the same hot ST loop with the step factored into functions — does not, becauserefflows intostep/goas an argument:Every iteration therefore keeps the cell-table field accesses (and, in
step, the boxed read/write foreign calls), while the structurally identical inlined loop is straight-line arithmetic on a local after #239.Approach
Interprocedural extension: a per-function summary saying "this parameter is only used through recognised cell operations (and passed to parameters with the same property)", computed bottom-up over the call graph with recursive groups resolved to a fixpoint. A cell then unboxes when every occurrence is a recognised operation or an argument in a summarised-safe position, and the callee is specialised (worker/wrapper style) to take the raw value with writes returned — which for ST is exactly a state-threading transformation. Substantially more machinery than #239; worth measuring demand first (the
Bench.EffectStepcounters give the baseline).Relations
Extends #239. The specialisation half overlaps with the uncurrying worker/wrapper infrastructure (#200) and the CPR result split (#206), which already know how to rewrite call sites to workers with different conventions.
Verification
Bench.EffectStep's committed TNEW/trace oracles move: the cell table disappears and the hot loop trace-compiles. A golden pins a factored ref loop compiling without{value = …}tables.