Skip to content

Dissolving a bare-Ref alias to an @inline always binding multiplies the inlined body across all use sites #171

Description

@Unisay

Problem

Two individually sound inliner rules compose badly: the unconditional inlining of bare-Ref bindings and the Linker's auto-Always annotation on foreign accessors. Walking one value, intAdd, from the Linker to Lua shows the interaction.

  • Stage 0 — Linker emits. For every foreign name, foreignNamesBindings (lib/Language/PureScript/Backend/IR/Linker.hs) creates an accessor binding whose body is one projection out of the foreign module's table, annotated Inline.Always: Data.Semiring.intAdd = ObjectProp @Always (Ref Data.Semiring.foreign) "intAdd". The cost model: the body is one cheap field access, so copying it into each use site beats a top-level indirection, and DCE can then prune unused foreign names.
  • Stage 1 — a bare-ref alias appears. User code add = (+) becomes a dictionary projection that reduceObjectProp folds to a bare reference Main.add = Ref Data.Semiring.intAdd, used in two exports. At this point everything is optimal: one projection, two cheap references.
  • Stage 2 — the inliner dissolves the alias. isInlinableExpr (lib/Language/PureScript/Backend/IR/Optimizer.hs) treats any bare Ref as inlinable, so the alias is substituted into both sites and dropped. In isolation this is free (a reference replaced a reference), but the number of sites referencing the Always target went from one to two.
  • Stage 3 — Always fires at every site. When the inliner reaches intAdd, the annotation substitutes its body unconditionally into all sites and drops the binding, yielding N copies with two lookups per site instead of one shared alias with one lookup per site.

Neither rule is wrong alone: dissolving a bare ref is free until its target is Always, and Always duplication is cheap until there is more than one application point. The composition breaks both assumptions, and the alias — the better materialization point on both size and speed — is destroyed.

Approach

In the top-level inliner, decline the bare-Ref branch of isInlinableExpr when the target binding of the reference carries Always, so the alias stays the single materialization point (withBinding has the module's bindings in scope for that lookup). The heavier alternative is to stop annotating Linker accessors with Always altogether and let the ordinary heuristics decide.

Prerequisites / Relations

Optimizer bug, independent in the dependency graph. Related to #169 — both concern the Linker's auto-@inline always annotation — but distinct: #169 is an annotation leak in reduceObjectProp, this one is the bare-Ref alias interaction and fires with perfectly fresh reference counts (so it is also distinct from #142/#143). Scope note: for a user-written @inline always the N copies are the pragma's contract and are by design; the defect is confined to the interaction with the Linker's auto-annotation.

Verification / Measurement

A bare-Ref alias to an Always-annotated foreign accessor, used at several sites, stays a single shared binding rather than duplicating the projection into every use site. Reproducible independently of reduceObjectProp: build Main.add = Ref (Imported Data.Semiring.intAdd), export two uses, and the alias survives as one shared entry instead of intAdd's projection appearing at both exports.

Metadata

Metadata

Assignees

Labels

OptimisationA Compiler Optimisationarea: irIR / optimizer / DCE / inliner

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions