Problem
Some hot loops pay a Lua function call per arithmetic operation because the floated dictionary-application binding (add = Data.Semiring.add semiringInt and friends, which purs floats to module level) survives as an uncurried worker instead of dissolving into the primop. The specialized tuple_fold loop compiles to
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)
where Bench_TupleFold_add_S_w = function(x, y) return x + y end. Under PUC Lua 5.1 this is the whole remaining gap on the tuple_fold benchmark: 0.169s with the worker calls against 0.057s for the ideal loop with inline +, so about 3x is left on the table. Pre-existing behavior, not introduced by #208: a main-branch build of the same module produces the same worker calls.
The behavior is inconsistent across modules, which is what makes it look like a missed fold rather than a policy decision:
Bench.CurriedStep (one use each of add/sub/eq): everything dissolves, the loop body is acc + i, i - 1.
Bench.TupleFold (two uses of add): add$w survives, both call sites in the loop pay a call.
Golden.SpecConstr.Test (four uses of add, four of sub): add dissolves everywhere into inline +, while sub$w survives at all four sites, in the same module.
Approach
Diagnose before fixing: trace the pipeline (runStepsTraced) on Bench.TupleFold and Golden.SpecConstr.Test to find where add's resolution diverges, and why sub behaves differently from add in the same module (different classes resolve through different foreigns: Data.Semiring.intAdd against Data.Ring.intSub). Candidate mechanisms: the use-once whole-binding inline is the only path that fires reliably; the call-site paste (inlineSaturatedCall) either does not reach these workers or its budget/complexity gate declines them; or the inline-env snapshot goes stale across specialize rounds. The fix is likely a narrow one: a worker whose body is a bare PrimBinOp over its parameters is the cheapest possible paste and should inline at every saturated site regardless of use count.
Prerequisites / Relations
None blocking. Composes with #208: call-pattern specialization removes the per-iteration box, this issue is about the residual call-per-op in the same loops. Same machinery family as #24 (uncurry worker/wrapper) and #180 (budgeted call-site inlining). The tuple_fold benchmark and its counter goldens land with PR #279.
Verification / Measurement
bench/run on tuple_fold under PUC Lua 5.1: the linked artifact closes toward the ideal 0.057s from the current 0.169s (the boxed baseline was 0.425s). The loop body in bench/_build/Bench.TupleFold.lua shows inline + instead of add$w calls, and Golden.SpecConstr.Test's sub$w sites fold the same way. Existing eval goldens stay byte-identical.
Open questions
- Why does
add dissolve in Golden.SpecConstr.Test (four uses) but not in Bench.TupleFold (two uses), while sub survives with four uses in the module where add dissolved? The use-count hypothesis fails in both directions, so the actual trigger is still unidentified.
Problem
Some hot loops pay a Lua function call per arithmetic operation because the floated dictionary-application binding (
add = Data.Semiring.add semiringIntand friends, which purs floats to module level) survives as an uncurried worker instead of dissolving into the primop. The specializedtuple_foldloop compiles towhere
Bench_TupleFold_add_S_w = function(x, y) return x + y end. Under PUC Lua 5.1 this is the whole remaining gap on thetuple_foldbenchmark: 0.169s with the worker calls against 0.057s for the ideal loop with inline+, so about 3x is left on the table. Pre-existing behavior, not introduced by #208: a main-branch build of the same module produces the same worker calls.The behavior is inconsistent across modules, which is what makes it look like a missed fold rather than a policy decision:
Bench.CurriedStep(one use each ofadd/sub/eq): everything dissolves, the loop body isacc + i, i - 1.Bench.TupleFold(two uses ofadd):add$wsurvives, both call sites in the loop pay a call.Golden.SpecConstr.Test(four uses ofadd, four ofsub):adddissolves everywhere into inline+, whilesub$wsurvives at all four sites, in the same module.Approach
Diagnose before fixing: trace the pipeline (
runStepsTraced) onBench.TupleFoldandGolden.SpecConstr.Testto find whereadd's resolution diverges, and whysubbehaves differently fromaddin the same module (different classes resolve through different foreigns:Data.Semiring.intAddagainstData.Ring.intSub). Candidate mechanisms: the use-once whole-binding inline is the only path that fires reliably; the call-site paste (inlineSaturatedCall) either does not reach these workers or its budget/complexity gate declines them; or the inline-env snapshot goes stale across specialize rounds. The fix is likely a narrow one: a worker whose body is a barePrimBinOpover its parameters is the cheapest possible paste and should inline at every saturated site regardless of use count.Prerequisites / Relations
None blocking. Composes with #208: call-pattern specialization removes the per-iteration box, this issue is about the residual call-per-op in the same loops. Same machinery family as #24 (uncurry worker/wrapper) and #180 (budgeted call-site inlining). The
tuple_foldbenchmark and its counter goldens land with PR #279.Verification / Measurement
bench/runontuple_foldunder PUC Lua 5.1: the linked artifact closes toward the ideal 0.057s from the current 0.169s (the boxed baseline was 0.425s). The loop body inbench/_build/Bench.TupleFold.luashows inline+instead ofadd$wcalls, andGolden.SpecConstr.Test'ssub$wsites fold the same way. Existing eval goldens stay byte-identical.Open questions
adddissolve inGolden.SpecConstr.Test(four uses) but not inBench.TupleFold(two uses), whilesubsurvives with four uses in the module whereadddissolved? The use-count hypothesis fails in both directions, so the actual trigger is still unidentified.