Skip to content

Optimizer: never duplicate a reference-constructing expression (allocation/identity guard) #284

Description

@Unisay

Problem

Several inlining paths can duplicate an expression whose root constructs a fresh table. Duplication multiplies allocations and changes object identity — observable at the FFI boundary, where Lua == on tables is identity. The cost model already encodes the discipline in two places — Ctor is deliberately NonTrivial ("keeps a nullary singleton (Ctor []) shared through a binding instead of re-allocated at every use site"), and isNonRecursiveLiteral admits only scalars — but three holes remain:

  1. Empty table literals are classified Trivial, so the Deref tier duplicates them at any use count:
-- complexityOf
LiteralArray _ann exprs
  | null exprs  Trivial
LiteralObject _ann props
  | null props  Trivial
  1. resolveDictionaryProp pastes a field's value per site with only a size gate, so a table-valued field is re-constructed at every site:
{unit = {}}.unit    {}   -- a fresh table per use site
  1. The whole-binding Always paste duplicates anything, and the Linker annotates foreign accessors Just Always by default — so for a table-valued foreign export the hole is open by default, not by user request.

Today the prelude covers the one known instance with @inline unit never (prelude v7.3.1); any other table-valued FFI constant in any fork is one forgotten pragma away from per-site reallocation and broken identity.

Approach

An allocatesAtRoot predicate — root-level LiteralObject/LiteralArray of any size (Ctor already lands NonTrivial) — consulted by every duplicating paste path: reclassify empty table literals out of Trivial, gate the resolveDictionaryProp field paste, and let the guard take precedence over Always. The precedence flip is the deliberate part: duplicated work is something a user can sign for (behaviour-preserving in a pure language), duplicated identity is not — no amount of re-evaluation brings back pointer equality. Use-once relocation stays admitted on every path: one allocation, moved, is identity-safe.

Prerequisites / Relations

Makes the prelude's @inline unit never unnecessary for correctness; composes with #283 / purescript-lua/purescript-lua-prelude#15, which removes the shared value for unit entirely (either alone suffices for unit; this guard protects the general class). Golden churn expected where {} is currently re-emitted per site — overlaps with #275 (zero-method dictionaries threaded as empty-table arguments), which wants those sites gone rather than shared.

Verification / Measurement

Unit test: a top-level LiteralObject [] binding annotated Just Always and used at two or more sites survives as a shared binding (today it is pasted per site). Full golden run: eval goldens byte-identical; structural churn reviewed for shared-{} bindings appearing where constructors were previously duplicated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: 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