Skip to content

fix(resolve): make generation a function of its input, and map GF16 in Zig - #3411

Merged
gHashTag merged 1 commit into
masterfrom
probe-type-tables
Sep 7, 2026
Merged

fix(resolve): make generation a function of its input, and map GF16 in Zig#3411
gHashTag merged 1 commit into
masterfrom
probe-type-tables

Conversation

@gHashTag

@gHashTag gHashTag commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Closes #3410 · Refs #3408

The same spec and the same binary produced three distinct Zig outputs across ten runs, and four distinct C outputs across five.

spec gen gen-rust gen-c gen-verilog
igla/coder/_tmp_pipeline_import.t27 3 of 10 3 of 5 4 of 5 1
physics/sacred_verification.t27 3 of 6
physics/zamolodchikov_4d_conjecture.t27 2 of 6

Same line count every time — a pure permutation, verified by sort-comparing the two outputs, with an imported function landing in a different place. Nothing invented, nothing lost, and nothing reproducible.

Cause

use_resolve.rs:

let distinct: Vec<&Decl> = {
    let mut by_origin: HashMap<&str, &Decl> = HashMap::new();
    for d in candidates { by_origin.entry(d.origin.as_str()).or_insert(d); }
    by_origin.into_values().collect()      // randomised per process
};
let chosen = if distinct.len() == 1 || all_agree(&distinct) {
    Some(distinct[0])                      // a random pick among "agreeing" decls

all_agree compares normalised text, so two declarations can agree by that test and still differ byte for byte. Twenty lines below, the code already claims the property it does not have:

// Deterministic order: by origin, then by name, so regenerating a spec twice
// produces byte-identical output.
pulled.sort_by(..)

That sort is real. It orders the declarations that were chosen; it cannot undo a random choice made upstream of it.

Which of the two random walks matters — measured, not assumed

I first wrote two sorts. Then I mutated each:

mutation result
distinct sort removed test fails: 3 distinct outputs in 6 runs
frontier sort removed, distinct sort kept 0 of 492 importing specs vary, over 4 runs each

So the repair is one sort. The second was measured at zero across the whole corpus and removed rather than shipped — a guard whose removal changes nothing measurable is decoration, and the comment justifying it would outlive its reason. The measurement is recorded in the code where the guard would have been.

Separable, and in the same PR: GF16 in the Zig mapper

A bare GF16 reached 9 emitted Zig files at 95 sites, and Zig has no such type. The value is not a guess: the Rust backend maps it to u16 and the C backend already has "GF16" | "gf16" => "uint16_t". Zig was the one column disagreeing. It is the same named class as the float (W591) and f32 (W583) gaps already recorded in that mapper — "a scalar the corpus spells and the mapper never learned".

Filed rather than guessed

Probing every type spelling through all four backends turned up genuine disagreements about meaning, which I did not touch:

t27 Rust Zig C Verilog
float f64 f64 float (32-bit) 32-bit
double f64 f64 double 32-bit
tri unmapped unmapped unmapped 32-bit
uint u32 u32 uint (not a C type)

float computing at 64 bits in two backends and 32 in the other two is a decision about what the spelling means, not a gap with a determined answer.

Effect

  • rustc acceptance unchanged at 433 of 651.
  • Emitted Zig changes in 15 files: 11 GF16-only, 4 from the ordering repair, each a permutation of what it replaced.

Test

bootstrap/tests/deterministic_output.rs runs all four backends six times over the three specs that reproduced it and asserts one distinct output each. It skips loudly if a spec is absent, because an absent input is not a passing test, and asserts that at least one spec was checked so the test cannot pass by measuring nothing.

bootstrap/stage0/FROZEN_HASH updated in the same commit, as M5 requires.

…n Zig

Closes #3410

The same spec and the same binary produced three distinct Zig outputs across
ten runs, and four distinct C outputs across five. Same line count every time --
a pure permutation, verified by sort-comparing the outputs, with an imported
function landing in a different place.

Cause, in use_resolve.rs: `by_origin.into_values()` on a HashMap is randomised
per process, and `distinct[0]` immediately below is the declaration that
actually gets emitted whenever the candidates agree. `all_agree` compares
NORMALISED text, so two agreeing declarations can still differ byte for byte.
Twenty lines further down the code already says

    // Deterministic order: by origin, then by name, so regenerating a spec
    // twice produces byte-identical output.

That sort is real, and it cannot undo a random choice made upstream of it.

Which of the two random walks mattered was measured rather than assumed:

  * `distinct` sort removed        -> the test fails, 3 distinct in 6 runs.
  * a frontier sort removed, with
    the `distinct` sort kept       -> 0 of 492 importing specs vary.

So the repair is one sort. A second one was written, measured at zero across
the whole corpus, and REMOVED rather than shipped -- a guard whose removal
changes nothing is decoration, and the comment justifying it would outlive its
reason. The measurement is recorded where the guard would have been.

Also here, and separable: `GF16` was missing from the Zig type mapper, reaching
9 files at 95 sites as a bare name Zig does not know. The value is not a guess
-- the Rust backend maps it to `u16` and the C backend to `uint16_t`, so Zig was
the one column disagreeing. It is the same named class as the `float` (W591) and
`f32` (W583) gaps already recorded in that mapper.

Not fixed, and filed rather than guessed: `float` is f64 in Rust and Zig but
32-bit in C and in Verilog; `double` is f64 in three backends and 32-bit in
Verilog; `tri` is unmapped in Rust, C and Zig alike. Those are decisions about
what the spellings MEAN.

rustc acceptance is unchanged at 433 of 651. The emitted Zig changes in 15
files, 11 of them GF16-only and 4 from the ordering repair, each a permutation
of what it replaced.

FROZEN_HASH updated in the same commit, as M5 requires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gHashTag
gHashTag enabled auto-merge (squash) September 7, 2026 20:36
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-09-07 20:36:48 UTC

Summary

Status Count
Total Open PRs 16
PRs with Failing Checks 14
PRs with All Checks Green 2
READY 2
FAILING 14
PENDING 0
NO CHECKS YET 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fa7f44546531 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@gHashTag
gHashTag merged commit 7620ddd into master Sep 7, 2026
29 of 32 checks passed
gHashTag added a commit that referenced this pull request Sep 7, 2026
…rom now on (#3416)

Closes #3415

A seal records four `gen_hash_*` fields -- the hashes of what each backend
emitted. Nothing checked them.

Measured on master: 109 stale gen_hash_rust, 7 stale gen_hash_zig, 0 C, 0
Verilog, and 0 stale spec_hash. All 116 were left by five merged backend
repairs -- #3401, #3403, #3405, #3407, #3411 -- every one of them mine. None
touched a spec, so spec_hash stayed correct and every coverage and staleness
check in the repository stayed green while a sixth of the Rust seals described
output the compiler no longer produces.

The existing checks cover the other half. check_seal_coverage.py asks whether a
seal describes a spec that exists, unchanged at SOURCE. `Seal Staleness Warning`
is about the NMSE manifest and FROZEN_HASH, unrelated to .trinity/seals, and
exits 0 by design. `t27c seal --verify` answers this question exactly -- exit 1
with a precise MISMATCH line, exit 0 on a current seal, both used as controls
here -- and nothing called it across the corpus.

The refresh took three attempts and each failure was informative. `--save`
fixed 116 -> 58, not 0, because it writes to .trinity/seals/<module>.json, one
name, while 1313 seals cover 728 distinct specs and 547 specs carry more than
one seal file. The remaining 58 duplicates are rewritten in place. `sealed_at`
is left alone in those: rewriting a hash the tool itself just computed is not a
fresh certification event, and moving the timestamp would claim one.

gen_hash=none is a different debt and is counted apart -- 169 seals record it
for at least one backend, and --save refuses to overwrite them ("4 of 4
backends rejected it"). Conflating the two is how the 116 stayed invisible.

tools/check_seal_currency.py now asks the question. Its --self-check plants a
wrong hash on a scratch tree and requires exactly that seal to be reported,
because a zero from a check that cannot see is indistinguishable from a healthy
zero. It exits 2 when t27c is absent rather than 0: a check that could not run
has not passed.

Filed, not guessed: some duplicate seals are not named after a module at all.
specs/tri/utils/logger.t27 carries `"[]const u8".json`, `utils_"[]const
u8".json` and `utils_TriLogger.json` -- a seal named after a type string.

After this: 1318 seals scanned, 1055 current, 94 whose spec is gone, 169 sealed
with none, 0 stale.

Co-authored-by: lab <lab@example.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gen is not deterministic: the same spec and binary produce three different outputs

1 participant