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:
- 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
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
- 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.
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 —Ctoris deliberatelyNonTrivial("keeps a nullary singleton (Ctor []) shared through a binding instead of re-allocated at every use site"), andisNonRecursiveLiteraladmits only scalars — but three holes remain:Trivial, so the Deref tier duplicates them at any use count:resolveDictionaryProppastes 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 siteAlwayspaste duplicates anything, and the Linker annotates foreign accessorsJust Alwaysby 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
allocatesAtRootpredicate — root-levelLiteralObject/LiteralArrayof any size (Ctoralready landsNonTrivial) — consulted by every duplicating paste path: reclassify empty table literals out ofTrivial, gate theresolveDictionaryPropfield paste, and let the guard take precedence overAlways. 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 neverunnecessary 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 annotatedJust Alwaysand 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.