Problem
The linker binds every foreign name to an ObjectProp accessor annotated @inline always by default (Note [Inline annotations and inlining heuristics]), so each use site re-materializes a field read off the foreign table: Data_Show_foreign.showIntImpl(x). The default buys wrapper transparency, but per-use cost has always mildly favored the shared form for multi-use names, and stage 2 of #174 (PR #246) made the gap decisive: an accessor kept as a binding is now promoted to a chunk local, so every use is a register or upvalue access with zero table reads, while the dissolved form still pays a field read per site. Promotion also drops M entirely from most modules, which leaves foreign accessor reads as the only inter-binding references still going through a table, and stage-1 caching cannot reach them because it caches M.x shapes only.
Approach
Decide a better default for accessors with more than one use site, keeping always where it still wins. Candidate shapes: flip the effective default to never when the accessor has multiple uses (single-use names stay dissolved: no binding, no local slot), or drop the forced annotation and teach the use-count heuristic that a field read repeated N times is worth a shared binding. Constraints: ForeignLift-lifted accessors must keep always, since their bodies exist to beta-reduce at saturated sites; the alias bindings consume the promotion locals budget, so selection should flow through Promote's existing accounting; DCE is unaffected either way, since a never accessor is still an ObjectProp that prunes when unreachable; and magic-do recognition of Effect/ST chains must still fire after the flip changes which shape reaches it (#182, #228). Fork-side per-name pragmas remain the escape hatch in both directions, and #176 (flip @inline unit always) becomes a special case: with the flip plus promotion, every unit use is a reference to one promoted local holding the singleton.
Verification / Measurement
Goldens: a multi-use accessor materializes as a promoted local (local Data_Show_showIntImpl = Data_Show_foreign.showIntImpl) and call sites reference it; single-use accessors keep today's dissolved shape; eval goldens unchanged. The field-read-versus-local microbenchmark from ADR 0001 (2.4x on PUC 5.1) bounds the per-read win.
Prerequisites / Relations
Requires stage 2 (#174, PR #246). Related: #176 (unit-specific flip, allocation-motivated), #175 (why per-site duplication of table values is unsound), #171 (dissolving an alias to an @inline always binding multiplies the body across use sites).
Problem
The linker binds every foreign name to an
ObjectPropaccessor annotated@inline alwaysby default (Note [Inline annotations and inlining heuristics]), so each use site re-materializes a field read off the foreign table:Data_Show_foreign.showIntImpl(x). The default buys wrapper transparency, but per-use cost has always mildly favored the shared form for multi-use names, and stage 2 of #174 (PR #246) made the gap decisive: an accessor kept as a binding is now promoted to a chunk local, so every use is a register or upvalue access with zero table reads, while the dissolved form still pays a field read per site. Promotion also dropsMentirely from most modules, which leaves foreign accessor reads as the only inter-binding references still going through a table, and stage-1 caching cannot reach them because it cachesM.xshapes only.Approach
Decide a better default for accessors with more than one use site, keeping
alwayswhere it still wins. Candidate shapes: flip the effective default toneverwhen the accessor has multiple uses (single-use names stay dissolved: no binding, no local slot), or drop the forced annotation and teach the use-count heuristic that a field read repeated N times is worth a shared binding. Constraints: ForeignLift-lifted accessors must keepalways, since their bodies exist to beta-reduce at saturated sites; the alias bindings consume the promotion locals budget, so selection should flow through Promote's existing accounting; DCE is unaffected either way, since aneveraccessor is still anObjectPropthat prunes when unreachable; and magic-do recognition of Effect/ST chains must still fire after the flip changes which shape reaches it (#182, #228). Fork-side per-name pragmas remain the escape hatch in both directions, and #176 (flip@inline unit always) becomes a special case: with the flip plus promotion, everyunituse is a reference to one promoted local holding the singleton.Verification / Measurement
Goldens: a multi-use accessor materializes as a promoted local (
local Data_Show_showIntImpl = Data_Show_foreign.showIntImpl) and call sites reference it; single-use accessors keep today's dissolved shape; eval goldens unchanged. The field-read-versus-local microbenchmark from ADR 0001 (2.4x on PUC 5.1) bounds the per-read win.Prerequisites / Relations
Requires stage 2 (#174, PR #246). Related: #176 (unit-specific flip, allocation-motivated), #175 (why per-site duplication of table values is unsound), #171 (dissolving an alias to an
@inline alwaysbinding multiplies the body across use sites).