Problem
unit = {} (v7.2.0, chosen so Array Unit survives — Lua tables cannot hold nil) is a reference-typed value: it allocates and carries object identity. That is what forced the v7.3.1 @inline unit never pin: without it the compiler's inliner can paste the constructor into use sites, allocating a fresh {} per use and breaking Lua-== identity against the shared binding. And the singleton was never ecosystem-wide anyway: a foreign module cannot reference the prelude binding (FFI files are self-contained header + return table), so Unit-returning FFI invents its own value — and a forgotten return silently yields nil, re-collapsing Array Unit at the FFI boundary:
-- a Unit-returning FFI thunk with no explicit return:
log = function(s) return function() print(s) end end
-- the thunk returns nil; an Array Unit collected from it collapses to {}
Proposal
Represent unit as an interned scalar:
-- Data/Unit.lua
return { unit = "unit" }
Lua interns strings, so every occurrence of the literal is one object per VM and comparison is pointer equality — a natural singleton with zero per-use allocation, the Lua analogue of purerl's unit atom. Value semantics dissolves the identity question instead of solving it.
Flip the pragma to @inline unit always: a scalar is legitimately free to paste, so the binding and accessor dissolve entirely and every use site carries the literal. Update the FFI guidance: a Unit-returning foreign returns the literal "unit".
Alternatives considered
nil is forbidden (table storage). false is falsy — the if x then FFI idiom would silently lose unit values. 0 is equivalent in cost but worse in debug output (print of a unit says unit with the string).
Risks / Migration
FFI dispatching on type(x) == "table" over polymorphic values will see a string (worth a grep across the forks); serialization of records with Unit fields (e.g. cjson under OpenResty) changes from {} to "unit" — both behaviors were unspecified. Nothing can have relied on unit identity (see Problem: FFI-side units were never the shared table). Migration: prelude release → package-set bump → pslua eval goldens catch regressions.
Problem
unit = {}(v7.2.0, chosen soArray Unitsurvives — Lua tables cannot holdnil) is a reference-typed value: it allocates and carries object identity. That is what forced the v7.3.1@inline unit neverpin: without it the compiler's inliner can paste the constructor into use sites, allocating a fresh{}per use and breaking Lua-==identity against the shared binding. And the singleton was never ecosystem-wide anyway: a foreign module cannot reference the prelude binding (FFI files are self-containedheader + return table), so Unit-returning FFI invents its own value — and a forgottenreturnsilently yieldsnil, re-collapsingArray Unitat the FFI boundary:Proposal
Represent unit as an interned scalar:
Lua interns strings, so every occurrence of the literal is one object per VM and comparison is pointer equality — a natural singleton with zero per-use allocation, the Lua analogue of purerl's
unitatom. Value semantics dissolves the identity question instead of solving it.Flip the pragma to
@inline unit always: a scalar is legitimately free to paste, so the binding and accessor dissolve entirely and every use site carries the literal. Update the FFI guidance: a Unit-returning foreign returns the literal"unit".Alternatives considered
nilis forbidden (table storage).falseis falsy — theif x thenFFI idiom would silently lose unit values.0is equivalent in cost but worse in debug output (printof a unit saysunitwith the string).Risks / Migration
FFI dispatching on
type(x) == "table"over polymorphic values will see a string (worth a grep across the forks); serialization of records with Unit fields (e.g. cjson under OpenResty) changes from{}to"unit"— both behaviors were unspecified. Nothing can have relied on unit identity (see Problem: FFI-side units were never the shared table). Migration: prelude release → package-set bump → pslua eval goldens catch regressions.