Problem
The call-site inliner's growth veto (Note [Bounded call-site inlining growth]) speculates, measures, and reverts per rewrite sweep at expression granularity: a sweep that grows an expression past before + max 16 (before/4) IR nodes is discarded and redone with paste tiers disarmed. Two kinds of growth slip under that dial: growth that accumulates across fixpoint rounds (each round re-measures against its own input, so N rounds compound), and pastes that are node-modest but line-heavy when printed. The Note documents the blind spot and already names the fix: "catching it needs the measurement at fixpoint convergence rather than per sweep, where transient growth is invisible and the dial could drop."
Admitting n-ary worker pastes (#245, PR #337) widened the exposure: on product-type transformer chains nothing ever folds, so each pasted worker body survives whole. Golden.LongStackBind grew +55% bytes (+743 printed lines) and Golden.LongStateBind +14% (+202 lines). Verbatim from Golden.LongStackBind.Test/golden.lua, one of ~50 identical chain steps:
-- before
return Golden_LongStackBind_Test_bindStateT.bind(Golden_LongStackBind_Test_put(Golden_LongStackBind_Test_add_S_w(x141, 1)))(function( )
-- after
return Golden_LongStackBind_Test_bindStateT.bind(function()
return (Golden_LongStackBind_Test_monadExceptT.Applicative0()).pure({
Data_Unit_unit,
x141 + 1
})
end)(function()
Each step trades one call for the closure put allocated per call anyway — allocations unchanged, one call fewer, but the body is duplicated at every step instead of shared behind the binding, and the per-sweep dial never sees the sum.
Approach
Add a second measurement at specialize-fixpoint convergence: compare each expression's size at fixpoint exit against its size at fixpoint entry, and on overrun re-run that expression's fixpoint with the heuristic tiers disarmed one rung at a time, reusing the CurriedPastesOnly ladder from PR #337 (n-ary worker tier first, all heuristic tiers second). The per-sweep veto stays — it bounds transient growth within a round — while the convergence check catches the compounding the Note describes. Calibrate the convergence allowance against the corpus the same way the per-sweep dial was (LongEitherBind must still collapse; LongStackBind must be rejected).
Prerequisites / Relations
Builds on the growth veto (#221) and the fallback ladder introduced with the n-ary worker tier (#245, PR #337). The blind spot is pre-existing (Golden.LongStateBind grew under it before #245); #245 raised the stakes.
Verification / Measurement
Golden.LongStackBind and Golden.LongStateBind return toward their pre-#245 sizes while the collapses #245 won stay collapsed (Golden.Primops constant folds, Golden.MaybeChain's literal 42, Bench.CtorBuild's in-place builds). A fixpoint-granularity case joins the #221 spec block (a chain whose per-round growth stays under the per-sweep dial but compounds past the convergence allowance). Corpus size and bench differential re-run through the #172 harness.
Problem
The call-site inliner's growth veto (
Note [Bounded call-site inlining growth]) speculates, measures, and reverts per rewrite sweep at expression granularity: a sweep that grows an expression pastbefore + max 16 (before/4)IR nodes is discarded and redone with paste tiers disarmed. Two kinds of growth slip under that dial: growth that accumulates across fixpoint rounds (each round re-measures against its own input, so N rounds compound), and pastes that are node-modest but line-heavy when printed. The Note documents the blind spot and already names the fix: "catching it needs the measurement at fixpoint convergence rather than per sweep, where transient growth is invisible and the dial could drop."Admitting n-ary worker pastes (#245, PR #337) widened the exposure: on product-type transformer chains nothing ever folds, so each pasted worker body survives whole.
Golden.LongStackBindgrew +55% bytes (+743 printed lines) andGolden.LongStateBind+14% (+202 lines). Verbatim fromGolden.LongStackBind.Test/golden.lua, one of ~50 identical chain steps:Each step trades one call for the closure
putallocated per call anyway — allocations unchanged, one call fewer, but the body is duplicated at every step instead of shared behind the binding, and the per-sweep dial never sees the sum.Approach
Add a second measurement at specialize-fixpoint convergence: compare each expression's size at fixpoint exit against its size at fixpoint entry, and on overrun re-run that expression's fixpoint with the heuristic tiers disarmed one rung at a time, reusing the
CurriedPastesOnlyladder from PR #337 (n-ary worker tier first, all heuristic tiers second). The per-sweep veto stays — it bounds transient growth within a round — while the convergence check catches the compounding the Note describes. Calibrate the convergence allowance against the corpus the same way the per-sweep dial was (LongEitherBind must still collapse; LongStackBind must be rejected).Prerequisites / Relations
Builds on the growth veto (#221) and the fallback ladder introduced with the n-ary worker tier (#245, PR #337). The blind spot is pre-existing (
Golden.LongStateBindgrew under it before #245); #245 raised the stakes.Verification / Measurement
Golden.LongStackBindandGolden.LongStateBindreturn toward their pre-#245 sizes while the collapses #245 won stay collapsed (Golden.Primopsconstant folds,Golden.MaybeChain's literal42,Bench.CtorBuild's in-place builds). A fixpoint-granularity case joins the#221spec block (a chain whose per-round growth stays under the per-sweep dial but compounds past the convergence allowance). Corpus size and bench differential re-run through the #172 harness.