feat(codegen): renumber compiler-minted locals at emission - #308
Merged
Conversation
Fresh names are drawn as base$N or $tagN from monotone supplies threaded through the whole pipeline, so any change that shifts supply consumption early in a module renames every later binder — alpha-only churn that inflates golden diffs of semantically identity changes (observed at scale in the #304 review: 26 modules moved, several in nothing but indices). A late Chunk → Chunk pass (Language.PureScript.Backend.Lua.Renumber) runs at the tail of optimizeChunk, after every name-minting transform and immediately before the printer. It renumbers every supply-drawn digit run occurring in a local binder in first-occurrence order (per base name, counted from 0) and rewrites the binder's references scope-consistently. Keying the assignment by the run's original spelling keeps a derived name in step with the binder it embeds: a recursive-group member b$5 and its dispatcher b$5$loop renumber together. Unbound (global) references are never rewritten, and allocation skips any spelling occurring free in the chunk, so the rewrite is capture-free alpha-renaming by construction. The differential spec pins the contract at the pipeline level: emission is byte-identical under a burned supply offset, for plain suffix-minted locals and for recursive groups with derived dispatcher names. Property specs pin idempotence and that chunks without supply-drawn names pass through untouched. The whole-corpus golden.lua renumbering is the intended one-time effect. golden.ir files and every hand-verified eval oracle are byte-identical, pinning the alpha-equivalence on the corpus. Closes #306
Unisay
marked this pull request as ready for review
July 26, 2026 19:08
The FNEW/TNEW censuses and trace reports key sites by line number in the linked bench artifacts; the emission-time renumbering shortens minted names, re-wrapping pretty-printed lines and shifting those line numbers. Site counts and shapes are unchanged. The record_set trace report additionally gains one compiled site (record_set.lua:19 JFUNCF, compiled 8 -> 9, aborts and blacklists unchanged): the renamed artifact's layout shifts LuaJIT's layout-random hot-counter aliasing, letting a site that previously aliased away reach its hot threshold. Regenerated with ./bench/ci --accept; the local censuses match the CI-computed ones byte-for-byte.
The trace report walked the spec harness's own bytecode alongside the artifact's, so the goldens also pinned the compile states of each spec's drive/ideal wrappers. Whether such a wrapper's entry compiles is an order race against its inner loop's counter — a pure function of per-process address layout — and for curried_step the emission renumbering shifted that layout into the marginal zone: raw trials flip between JFUNCF-present (aborts=1 compiled=4) and JFUNCF-absent (aborts=2 compiled=3) at p far enough from 0 and 1 that the majority vote itself flips per invocation, on CI and locally alike. Those spec-side states carry no information about emitted code, which is what the oracles exist to pin, so the report now records abort sites and bytecode end states of the artifact chunk only. With the filter, 8/8 raw curried_step trials are byte-identical and five consecutive ./bench/ci verification runs pass; the golden diff is purely subtractive (spec-side lines and recomputed counts). The artifact-side majority vote stays: export wrappers measured at p ~ 0.9 (the Bench.BindChain example in the header) still need it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #306.
Problem
Every fresh name the compiler mints carries an index drawn from a monotone supply threaded through the whole pipeline: suffix-minted names like
x$223(uniquification, inline-paste freshening) and prefix-minted names like$cse1413or the$sel/$adispatch locals of codegen, all mangled$→_S_on the way into the Lua AST. That index is pipeline history, not artifact structure — any upstream change that shifts how much supply earlier code consumes renames every later binder, so semantically identity changes churn golden files in nothing but indices (#304 review: of 26 moved modules, several changed only likev_S_1437 → v_S_1440).The new differential spec demonstrates the leak at the level it manifests, before the fix (
test/Language/PureScript/Backend/Lua/Renumber/Spec.hs, compiling the same IR twice with a burned supply offset of 100):Fix
A late
Chunk → Chunkpass,Language.PureScript.Backend.Lua.Renumber, wired at the tail ofoptimizeChunk(the shared seam bothBackend.compileModulesand the golden harness print through), after every name-minting Lua-level transform and immediately before the printer. The supply must stay global — the pipeline's unique-binders invariant depends on it — so nothing changes in the IR; only the emitted spelling is normalized.The pass renumbers every supply-drawn digit run occurring in a local binder in first-occurrence order — a fresh index per base name, counted from 0 — and rewrites the binder's references scope-consistently. A digit run is supply-drawn in exactly two shapes, mirroring the two minting grammars (see
Note [Supply-drawn digit runs]in the new module): a whole_S_-delimited segment (x_S_223, and the same run embedded mid-name in a derived dispatcherb_S_5_S_loop), or the run terminating the tag of a_S_-prefixed name (_S_cse1413,_S_a1). Digit runs anywhere else are spelling, not supply:add3keeps its3, SpecConstr's positional…_S_sc1Tuplekeeps its1.Two design points worth review attention:
b$5→b$5$loop), so a suffix-only rule would stabilize the member yet leave the dispatcher churning. Assignments are keyed by (piece-prefix, original run), sob_S_5andb_S_5_S_loopboth map through the same entry — the second differential test pins exactly this on a mutually tail-recursive group.repeat-until condition seeing body locals); an unbound reference is a global — an FFI file's stdlib or host-API read — and is never rewritten, and allocation skips any index whose spelling occurs free in the chunk, so a renamed binder cannot capture such a global either.Sample of the resulting normalization, quoted from the accepted
Golden.NativeLoopsGuard.Test/golden.lua:Verification
$sel/$alocals.repeat-until scoping, the free-spelling skip, and the untouched classes (unsuffixed names, positional digits, field names, table keys, global references).golden.luafiles (52 of them); everygolden.irand every hand-verifiedeval/golden.txtoracle is byte-identical and green, which pins the renaming as alpha-equivalence on the corpus. Luacheck stays clean;cabal test allgreen on the accepted state.Notes
x_S_1is a legal Lua and PureScript identifier) is renumbered too — consistently with its references, so semantics are unchanged; only the spelling in the output moves.