Skip to content

Measure call-site inlining growth at fixpoint convergence to catch diluted paste growth #287

Description

@Unisay

Follow-up from #221. The growth veto measures each rewrite sweep in isolation: optimizeExp compares an expression's size before and after one armed sweep and falls back to directed-only pastes when the sweep grew past its allowance. That measurement point has a structural blind spot, and one corpus case sits squarely in it.

The blind spot

A collapse that spans fixpoint rounds is indistinguishable, within one sweep, from growth that never collapses. The Either chain of LongEitherBind grows just under a fifth of its host when its binds paste, and folds only a round later once the environment carries its settled neighbours, so the per-sweep dial must stay above that transient (it is a quarter). Meanwhile the plain State chain of LongStateBind grows only ~10% in IR nodes (hundreds of tiny put/pure pastes diluted in one huge host expression) though far more in printed Lua: +275 lines against the pre-#180 baseline of 765. Any per-sweep dial low enough to catch the second freezes the first at its unfolded worst (measured: a 6.25% dial shrinks LongStateBind to 747 lines but blows LongEitherBind up from 59 to 698).

Proposed direction

Measure where transients are invisible: at the convergence of the specialize+dce fixpoint. Snapshot per-binding (and per-export) sizes on fixpoint entry; on exit, compare. An expression that converged larger than its allowance grew without ever collapsing, and no transient excuse exists at that point. Reverting the converged module directly would dangle references (a reverted RHS can name bindings DCE dropped mid-fixpoint), so the revert re-runs the fixpoint from the entry snapshot with the offending expressions vetoed (DirectedPastesOnly from round one). Each re-run adds at least one expression to the veto set, so the iteration is bounded; in practice one re-run should suffice.

Relation to the per-sweep veto

The convergence measurement complements rather than replaces #221's per-sweep veto: the per-sweep dial keeps a single pathological sweep from producing a huge intermediate at all, while the convergence pass judges the settled outcome. With the convergence pass in place the per-sweep dial could also be lowered aggressively, since a false per-sweep veto is repaired the moment the re-run keeps the collapse; that interaction should be measured, not assumed.

Verification

LongStateBind is the acceptance case: its chain should keep compact shared calls (747 lines at the probe dial, below the pre-inliner baseline). LongEitherBind (59 lines) and LongWriterBind are the guard cases: both must stay collapsed. Eval oracles across the corpus must stay byte-identical, and the optimizer's fixpoint cost on the golden corpus should be profiled before and after, since the re-run doubles the fixpoint for modules that trip.

Metadata

Metadata

Assignees

No one assigned

    Labels

    OptimisationA Compiler Optimisationarea: irIR / optimizer / DCE / inlinerenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions