Problem
The record-surgery layer from #236 (PR #304) is a handwritten registry: a four-entry map in Language.PureScript.Backend.IR.RecordSurgery holding the compiler's beliefs about the semantics of the prelude fork's Record.Unsafe FFI, with the fork file remaining the authority. That split has three standing costs. First, the drift class is silent: a fork release that changes an entry's semantics keeps compiling and miscompiles quietly (bounded today by the registry's size and the golden eval oracle that runs the same functions down folded and unfolded paths, but bounded is not gone). Second, dynamic-label call sites — unsafeGet(key)(rec) in the generic-deriving family — stay opaque foreign calls paying two curried CALLs and a closure per read, because the registry only fires on static labels. Third, the planned record fork would need Builder FFI plus more registry entries, re-opening the question #304 deferred: upstream's Builder primitives mutate in place, so registry folds like copyRecord r → r are unsound unless the fork commits to pure-copy FFI — semantics underdetermined as long as the fork, not the compiler, owns them.
Approach
Wire the module in, GHC-style (wired-in ids like seq: compiler-defined, no library source consulted): ship a compiler-owned IR implementation of Record.Unsafe so the compiler is the semantics and the drift class dies by ownership. Concretely: (a) a record primop family — a dynamic-key read (get l r lowering to r[l]), a ternary copy-with-extend (set l v r lowering to a new runtime fixture; the existing pslua_object_update cannot serve because it iterates the base's keys and silently drops patches whose key is absent, while unsafeSet must add fields), a delete, and a membership test (lowering to r[l] ~= nil); (b) a builtin path in foreign resolution so the module's ForeignImport resolves internally instead of to a .lua file — until the fork side migrates, the builtin shadows the fork's file exactly as ForeignLift already shadows lifted exports; (c) the four fold arms from #304 transplanted as primop constant folds next to foldPrimBinOp, carrying the plainLabel side condition (the raw-vs-mangled key schism stays quarantined in the fold, since generated tables key by the makeSafe spelling while dynamic reads look up raw strings); (d) classification updates where new shapes meet existing analyses — scalar replacement (#240) must treat a dynamic-key read as a full read of the record, and the allocation/identity guard (#284) must classify copy-with-extend as reference-constructing. Fork-side migration (deleting Record/Unsafe.lua and pinning "prelude ≥ X requires pslua ≥ Y") is deliberately out of scope here and batches with the next coordinated set release; the record fork, created after this lands, ships Builder wired-in from day one and never carries Builder FFI at all.
Prerequisites / Relations
Blocked by #247, which establishes the exact pattern this issue scales up — extending the primop vocabulary with a lift target, folds, and a codegen lowering — at a fifth of the size. Relates to #283 as the precedent for the coordinated fork+compiler release the fork-side deletion should ride, to #240 and #284 as the analyses that must learn the new shapes, and to #251, which touches the same foreign-resolution code that grows the builtin path. Subsumes the Record.Builder registry extension deferred from #236 in PR #304, and for the record family it also subsumes the idea of a generic Lua-level inline+beta over small FFI bodies (the wired-in lowering already inlines the dynamic-label reads that pass would have caught).
Verification / Measurement
A dynamic-label unsafeGet golden (the Golden.BugListGenericEq shape, local get = Record_Unsafe_foreign.unsafeGet(key)) emits an inline rec[key] read with the two curried calls and the closure elided; every #304 golden stays byte-identical (the folds transplant, so golden.lua must not move); all hand-verified eval oracles unchanged; and, until the fork file is deleted, a differential test that the builtin lowering and the fork's Record/Unsafe.lua agree on the golden corpus, so the shadowing period cannot hide a semantics gap. Counter check through the #172 harness: the Bench.RecordSet censuses stay at their #304 values (no regression), and a dynamic-label micro variant shows the per-read FNEW disappearing from the function-body census.
Problem
The record-surgery layer from #236 (PR #304) is a handwritten registry: a four-entry map in
Language.PureScript.Backend.IR.RecordSurgeryholding the compiler's beliefs about the semantics of the prelude fork'sRecord.UnsafeFFI, with the fork file remaining the authority. That split has three standing costs. First, the drift class is silent: a fork release that changes an entry's semantics keeps compiling and miscompiles quietly (bounded today by the registry's size and the golden eval oracle that runs the same functions down folded and unfolded paths, but bounded is not gone). Second, dynamic-label call sites —unsafeGet(key)(rec)in the generic-deriving family — stay opaque foreign calls paying two curriedCALLs and a closure per read, because the registry only fires on static labels. Third, the plannedrecordfork would need Builder FFI plus more registry entries, re-opening the question #304 deferred: upstream's Builder primitives mutate in place, so registry folds likecopyRecord r → rare unsound unless the fork commits to pure-copy FFI — semantics underdetermined as long as the fork, not the compiler, owns them.Approach
Wire the module in, GHC-style (wired-in ids like
seq: compiler-defined, no library source consulted): ship a compiler-owned IR implementation ofRecord.Unsafeso the compiler is the semantics and the drift class dies by ownership. Concretely: (a) a record primop family — a dynamic-key read (get l rlowering tor[l]), a ternary copy-with-extend (set l v rlowering to a new runtime fixture; the existingpslua_object_updatecannot serve because it iterates the base's keys and silently drops patches whose key is absent, whileunsafeSetmust add fields), a delete, and a membership test (lowering tor[l] ~= nil); (b) a builtin path in foreign resolution so the module'sForeignImportresolves internally instead of to a.luafile — until the fork side migrates, the builtin shadows the fork's file exactly as ForeignLift already shadows lifted exports; (c) the four fold arms from #304 transplanted as primop constant folds next tofoldPrimBinOp, carrying theplainLabelside condition (the raw-vs-mangled key schism stays quarantined in the fold, since generated tables key by themakeSafespelling while dynamic reads look up raw strings); (d) classification updates where new shapes meet existing analyses — scalar replacement (#240) must treat a dynamic-key read as a full read of the record, and the allocation/identity guard (#284) must classify copy-with-extend as reference-constructing. Fork-side migration (deletingRecord/Unsafe.luaand pinning "prelude ≥ X requires pslua ≥ Y") is deliberately out of scope here and batches with the next coordinated set release; therecordfork, created after this lands, ships Builder wired-in from day one and never carries Builder FFI at all.Prerequisites / Relations
Blocked by #247, which establishes the exact pattern this issue scales up — extending the primop vocabulary with a lift target, folds, and a codegen lowering — at a fifth of the size. Relates to #283 as the precedent for the coordinated fork+compiler release the fork-side deletion should ride, to #240 and #284 as the analyses that must learn the new shapes, and to #251, which touches the same foreign-resolution code that grows the builtin path. Subsumes the Record.Builder registry extension deferred from #236 in PR #304, and for the record family it also subsumes the idea of a generic Lua-level inline+beta over small FFI bodies (the wired-in lowering already inlines the dynamic-label reads that pass would have caught).
Verification / Measurement
A dynamic-label
unsafeGetgolden (theGolden.BugListGenericEqshape,local get = Record_Unsafe_foreign.unsafeGet(key)) emits an inlinerec[key]read with the two curried calls and the closure elided; every #304 golden stays byte-identical (the folds transplant, sogolden.luamust not move); all hand-verified eval oracles unchanged; and, until the fork file is deleted, a differential test that the builtin lowering and the fork'sRecord/Unsafe.luaagree on the golden corpus, so the shadowing period cannot hide a semantics gap. Counter check through the #172 harness: theBench.RecordSetcensuses stay at their #304 values (no regression), and a dynamic-label micro variant shows the per-readFNEWdisappearing from the function-body census.