Problem
The inliner decides with a flat model: exact free-reference counts (Note [Incremental free-reference counting]), a single inlineSizeBudget = 64 node ceiling, and isInlinableExpr (a node is inlinable if it carries Always, is a bare Ref, or is a non-recursive literal). Nothing captures how costly an expression is to duplicate, or where a reference sits. So one size threshold gates a field projection and a 60-node lambda the same way, and a reference evaluated unconditionally is treated like one hidden behind an if or captured in a closure.
Two small per-node analysis lattices make the same calls sharper:
Complexity = Trivial < Deref < KnownSize < NonTrivial: Deref is a field projection; KnownSize a lambda or a non-empty record/array literal.
Capture = CaptureNone < CaptureBranch < CaptureClosure: whether a reference is reached unconditionally, only under a branch, or from inside a closure.
With per-reference usage (total, captured, arities, call/access/case counts) alongside them, an inliner takes Trivial/Deref unconditionally while refusing to duplicate NonTrivial work under a branch or a closure.
Approach
Add a Complexity lattice and a Capture flag to the analysis already threaded for reference counting, computed bottom-up in the same traversal. Feed both into the shared inlining guard (isInlinableExpr / withBinding / betaReduce, which share one guard per Note [Beta reduction and local inlining share an inlining guard]): inline Trivial/Deref regardless of use count; allow a small KnownSize body when used once or closed; refuse to duplicate a NonTrivial body reached under CaptureBranch/CaptureClosure. The 64-node budget stays as the ceiling for whatever is left.
This is analysis enrichment, not a pipeline reshape. No new pass, no move toward NBE. The lattices live in the annotation; the fixpoint framework and pass contracts are untouched.
Prerequisites / Relations
Independent to land. It is an enabler: #240 (scalar-replacement unpacking) needs the same exact-use accounting to prove an aggregate is read only field-wise, and #239 (ref-cell unboxing) needs the Capture flag to prove a cell does not escape. It also sharpens the existing inliner tuning in #211 and #221 and the CSE sharing condition in #183, without depending on any of them.
Verification / Measurement
A focused optimizer test: a Deref used at several sites inlines at all of them with no size gate, while a NonTrivial body sitting under a branch stays shared. The corpus effect is size-shaped and shows up through the #172 counters (fewer shared trivial bindings, no growth from duplicated non-trivial work); eval goldens must not move, since the change is to which bindings dissolve, not to semantics.
Problem
The inliner decides with a flat model: exact free-reference counts (Note [Incremental free-reference counting]), a single
inlineSizeBudget = 64node ceiling, andisInlinableExpr(a node is inlinable if it carriesAlways, is a bareRef, or is a non-recursive literal). Nothing captures how costly an expression is to duplicate, or where a reference sits. So one size threshold gates a field projection and a 60-node lambda the same way, and a reference evaluated unconditionally is treated like one hidden behind anifor captured in a closure.Two small per-node analysis lattices make the same calls sharper:
Complexity = Trivial < Deref < KnownSize < NonTrivial:Derefis a field projection;KnownSizea lambda or a non-empty record/array literal.Capture = CaptureNone < CaptureBranch < CaptureClosure: whether a reference is reached unconditionally, only under a branch, or from inside a closure.With per-reference usage (
total,captured, arities, call/access/case counts) alongside them, an inliner takesTrivial/Derefunconditionally while refusing to duplicateNonTrivialwork under a branch or a closure.Approach
Add a
Complexitylattice and aCaptureflag to the analysis already threaded for reference counting, computed bottom-up in the same traversal. Feed both into the shared inlining guard (isInlinableExpr/withBinding/betaReduce, which share one guard per Note [Beta reduction and local inlining share an inlining guard]): inlineTrivial/Derefregardless of use count; allow a smallKnownSizebody when used once or closed; refuse to duplicate aNonTrivialbody reached underCaptureBranch/CaptureClosure. The64-node budget stays as the ceiling for whatever is left.This is analysis enrichment, not a pipeline reshape. No new pass, no move toward NBE. The lattices live in the annotation; the fixpoint framework and pass contracts are untouched.
Prerequisites / Relations
Independent to land. It is an enabler: #240 (scalar-replacement unpacking) needs the same exact-use accounting to prove an aggregate is read only field-wise, and #239 (ref-cell unboxing) needs the
Captureflag to prove a cell does not escape. It also sharpens the existing inliner tuning in #211 and #221 and the CSE sharing condition in #183, without depending on any of them.Verification / Measurement
A focused optimizer test: a
Derefused at several sites inlines at all of them with no size gate, while aNonTrivialbody sitting under a branch stays shared. The corpus effect is size-shaped and shows up through the #172 counters (fewer shared trivial bindings, no growth from duplicated non-trivial work); eval goldens must not move, since the change is to which bindings dissolve, not to semantics.