Renumber minted binders before rendering the .ir goldens (#338) - #340
Merged
Conversation
#338) Every IR pass mints fresh binder names from one pipeline-global counter, so the index a name carries records how much supply the passes before it consumed rather than anything about the code it binds. The .ir goldens render those names verbatim and therefore diff on lines whose only change is the counter: one extra mint anywhere renumbers every name after it. The golden harness now renumbers each top-level site's minted binders in first-occurrence order before rendering, so a name is a function of its own site's structure. Per site, not per module: a site that mints one more name cannot shift the site printed after it. Within a site the renaming is a plain name-to-name map with no scope threading, which UniqueBinders licenses -- at most one binder there carries any given name. The pipeline itself is untouched, so generated Lua cannot move: the emitted chunks were already history-free via renumberChunk. Of 336 golden examples the 55 that moved are all golden.ir; no golden.lua or eval/golden.txt moved. Corpus-wide this collapses 413 distinct minted spellings to 136 and the largest index from 3033 to 6. The digit-run classifier is shared with the Lua emission renumberer, which differs only in mangling $ to _S_, so it moves to Language.PureScript.Backend.Renumber parameterized by the delimiter, carrying Note [Supply-drawn digit runs]. Each renumberer keeps its own scope discipline. Structural suffixes are untouched as before: f$w, f$p1, pong$sc1Tuple and uniquification's x0. Starting the supply at 1000 instead of 0 now leaves every golden byte-identical, while the unit tests pinning exact minted names see the shift -- the acceptance check the issue asks for.
Unisay
marked this pull request as ready for review
July 28, 2026 16:50
Unisay
marked this pull request as draft
July 28, 2026 16:50
Unisay
marked this pull request as ready for review
July 28, 2026 16:52
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 #338.
The churn
Every pass of the IR pipeline mints fresh binder names by drawing an index from one counter shared across the whole pipeline (
SupplyM, inLanguage.PureScript.Backend.IR.Supply). The index a name ends up carrying is therefore not a property of the code it binds — it records how many names the passes that happened to run earlier consumed. Insert one extra mint anywhere and every name minted after it shifts by one.The structural golden files (
golden.ir, a pretty-printed dump of the linked, optimized IR module used to pin what the optimizer produces) render those names verbatim, so they diff on lines whose only change is the counter. From PR #337, whose only semantic effect onGolden.LongWriterBind.Testwas one constructor worker dissolving, the.irdiff carried 94 changed lines, many of them purely this:Every optimizer PR pays that tax: the reviewer has to sort real rewrites from shifted counters, and modules whose IR did not meaningfully change churn anyway.
The fix
The golden harness now renumbers a module's minted binders before rendering it, so the printed indices are assigned in first-occurrence order and each name becomes a function of its own site's structure. The same line now reads:
Across the golden corpus this collapses 413 distinct minted spellings to 136, and the largest index from 3033 to 6 — a name's index is now the count of same-prefixed binders that precede it in its own site, not a pipeline high-water mark.
Why the harness rather than the pipeline
Issue #338 offered two shapes: stabilize the names where they are minted, or normalize them where the artifact is rendered. The first one's extra payoff — a stable shipped artifact — is already delivered:
renumberChunk(Language.PureScript.Backend.Lua.Renumber, landed in 83c083a) does exactly this renumbering on the final Lua chunk immediately before printing, and the emitted.luagoldens are consequently already history-free, with indices topping out at 7. What is left churning is only the IR dump, which nothing but the goldens renders. Normalizing at that render point keeps the compiler's own passes untouched, which is what makes "generated Lua is unchanged" a fact about the diff rather than a claim: of 336 golden examples, the 55 that moved are allgolden.ir, and nogolden.luaoreval/golden.txtmoved at all.Mechanism
renumberUberModule(new,Language.PureScript.Backend.IR.Renumber) walks each top-level site of anUberModule— the linker's flattening of every reachable binding, foreign binding and export into one module — and renumbers that site from its own fresh allocation. Per-site rather than module-wide is the whole point: a site that mints one more name cannot shift the site printed after it.Within a site the renaming is a plain name-to-name map applied to every occurrence, with no scope threading. What licenses that is the pipeline's global-uniqueness condition (
UniqueBinders, established by the entry passuniquifyNames): at most one binder in a site carries any given name, so a local reference belongs to the binder whose name it matches — the same reasoningsubstituteCopyMalready relies on.Per-site allocation is safe against cross-site capture because no name reachable from another site is ever renumbered. Top-level names come from source identifiers plus structural suffixes, and the corpus bears this out: no
QNamein any.irgolden carries a supply-drawn index. The one local reference a site may leave free is the runtime lazy factory, whose name holds no digit run and so is never an image of the renumbering.Which digit runs are history
Deciding that is the delicate part, and it was already solved once for the Lua side, where the mangling of
$to_S_is the only difference. Rather than copy a subtle heuristic and its rationale, the classifier moved toLanguage.PureScript.Backend.Renumber, parameterized by the delimiter, and both renumberers now call it — oneNote [Supply-drawn digit runs]instead of two. Each keeps its own scope discipline, which is where they genuinely differ: the Lua pass threads real Lua scopes and withholds spellings that occur free in the chunk, the IR one needs neither.A digit run is supply-drawn when it forms a whole delimited segment (
m$590, and the same run embedded in a derived name,$kont7$w) or terminates the tag of a prefix-minted name ($cse1413). Everything else is spelling and passes through, which is what keeps the structural suffixes readable: uncurrying's worker and parameter marks (f$w,f$p1), call-pattern specialization's shape mark (pong$sc1Tuple), uniquification's shadowing suffix (x0), and a source name that merely ends in a digit (add3).Verification
The acceptance criterion #338 names is that offsetting the supply start must leave the goldens byte-identical. Starting the pipeline's counter at 1000 instead of 0 does exactly that: all 336 golden examples pass with nothing rewritten. The offset provably took effect, because the unit tests that pin exact minted names see the shift and 25 of them fail on it:
Language.PureScript.Backend.IR.Renumber.Specpins the behaviour, each guard aimed at one property. Three tests fix the invariance directly, comparing a fixture built at supply offset 0 against the same fixture at offset 100, for suffix-minted binders, prefix-minted ones, and — over 300 generated expressions freshened at an arbitrary offset — any site at all:Two more pin per-site isolation, which no supply-history test can catch: they fail only if the allocation is shared across sites. Replacing the per-site allocation with a module-wide one to check they have teeth turns the second site's
v$0/f$0/x$0intov$1/f$1/x$1and both go red.The remaining guards are example-based: the two minting grammars, first-occurrence order within one prefix (
v$1437,v$921→v$0,v$1), a derived name landing on the index of the binder it embeds ($kont7and$kont7$w→$kont0and$kont0$w), the structural suffixes above left alone, and a foreign import's name list left alone since it holds the foreign file's export keys rather than binders. Three properties over 300 generated inputs each close it off: the renumbering is the identity on sites with no minted names, it is idempotent, and — the safety net that matters for a renaming — its output still satisfieslintWellScopedandlintUniqueBinders.cabal test allis green at 1215 examples.