Skip to content

Growth veto: measure at fixpoint convergence to catch non-collapsing paste growth #339

Description

@Unisay

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    OptimisationA Compiler Optimisationarea: irIR / optimizer / DCE / inlinerduplicateThis issue or pull request already existsenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions