fix(rust-backend): lower the bare math builtins, and port ring-103 to a spec - #3401
Merged
Conversation
… a spec Closes #3400 gen-rust emitted `abs(x)`, `sqrt(x)`, `min(a, b)` and their siblings as bare free functions. Rust has none of them as free functions, so rustc answered "cannot find function `abs` in this scope". `zig_builtin_to_rust` already translated every one of them; its first line returns None for a name without the `@` sigil, and specs write them bare, so the table was never asked. The repair routes the bare name through that same table rather than restating it, so the two spellings cannot drift apart. Three guards, each added because a measurement demanded it: * `declared_fns`. 30 corpus declarations give one of these names to a spec's own function (`fn floor` in 10 specs, `fn abs` in 9). Redirecting those to a Rust method is a wrong translation that COMPILES. Collected by a new `collect_declared_fns` rather than reusing `collect_fn_ret_types`, which skips a declaration whose return type is empty -- and 3 of those 30 use the Zig-style return syntax with no `->`. * A typed receiver. `(5.0).sqrt()` is E0689 on an ambiguous `{float}`, and so is `((2.0 / 3.141592653589793)).sqrt()`; the first version of this guard tested for a single literal token and still admitted the compound form. * Not inside a `const`. `sqrt` is not a `const fn`, so every spelling fails there; trading E0425 for E0015 is a new failure, not a repair. `log` is excluded: Rust's `f64::log` takes a base, so `(x).log()` is E0061. The existing `@log` arm has the same defect and is filed in #3400, not changed here under an unrelated title. Measured, and the number is not flattering: corpus-wide rustc acceptance of the emitted Rust is 429 of 650 before and 429 of 650 after -- delta zero. Nine files' output changes and none crosses fail to pass; each carries other blockers. One file gains an error class, and that was proven to be a pre-existing defect newly REACHED rather than introduced: the only diff between the two outputs is line 140, and the offending line 143 is byte-identical in both, because E0425 is a name-resolution error that aborts before the borrow checker runs. The value is the precondition. specs/rings/ring_103_phi_sgd.t27 is the first real port of hand-written code to a spec: it replaces the arithmetic of rings/ring-103-rust/src/lib.rs, and the generated Rust agrees with the hand-written original bit-exactly on 20,736 inputs -- NaN, both infinities, both zeros, subnormals, clip disabled, negative learning rate -- compared as f32 bit patterns, with a harness control proving the comparison can see a one-bit difference. Three tests in backend_behaviour.rs, which compiles and RUNS the output rather than reading its text. Mutation-checked: disabling the declared_fns guard fails `a_spec_declaring_its_own_abs_keeps_its_own_abs` and leaves the other nine green. FROZEN_HASH updated in the same commit, as M5 requires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gHashTag
enabled auto-merge (squash)
September 7, 2026 17:30
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Contributor
This was referenced Sep 7, 2026
Closed
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>
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 #3400
Two things, and the second is the point: a backend defect, and the first real port of hand-written code to a t27 spec that is verified against its original.
The defect
gen-rustemittedabs(x),sqrt(x),min(a, b)and their siblings as bare free functions. Rust has none of them as free functions:zig_builtin_to_rustalready translated every one of them correctly. Its first line returnsNonefor a name without the@sigil, and specs write them bare — so the table was never asked. The repair routes the bare name through that same table rather than restating it, so the two spellings cannot drift apart. The Zig backend closed this identical class atgen_expr, with a comment naming the same symptom.Three guards, each added because a measurement demanded it
declared_fnsfn floorin 10 specs,fn absin 9,fn cosin 3. Redirecting those is a wrong translation that compiles.(5.0).sqrt()is E0689 on{float}— and so is((2.0 / 3.141592653589793)).sqrt(). The first version of this guard tested a single literal token and still admitted the compound form, leaving E0689 in 2 files.constsqrtis notconst fnpub const K: f64 = ...every spelling fails; trading E0425 for E0015 is a new failure, not a repair.declared_fnsis collected by a newcollect_declared_fnsrather than reusingcollect_fn_ret_types, which skips a declaration whoseextra_return_typeis empty — and 3 of those 30 use the Zig-style return syntax with no->, so that map would have been narrower than its subject exactly where it matters.logis deliberately excluded: Rust’sf64::logtakes a base, so(x).log()is E0061. The existing@logarm carries the same defect; filed in #3400 rather than changed here under an unrelated title.What it is worth, stated without flattery
Corpus-wide rustc acceptance of the emitted Rust: 429 of 650 before, 429 of 650 after — delta zero.
sqrt((mean + 1e-6))→((mean + 1e-6)).sqrt()), while the offending line 143 is byte-identical in both. E0425 is a name-resolution error that aborts before the borrow checker runs, so fixing it let the file reach borrow-checking and surface an out-parameter that was never declaredmut.An earlier run of this same measurement read 0 of 650 for both binaries. That was the instrument, not the compiler: the filenames carried
.t27, and rustc derives the crate name from the filename and rejects the dot, so nothing was ever compiled. The figures above come from a run whose control compiles a valid file and rejects an invalid one.The point: the first verified port
specs/rings/ring_103_phi_sgd.t27replaces the arithmetic ofrings/ring-103-rust/src/lib.rs— a phi-tempered SGD step. Its generated Rust and the hand-written original were run on the same inputs and compared as f32 bit patterns:The grid crosses 24 weights × 24 gradients × 6 learning rates × 6 clips, and includes NaN, both infinities, both zeros, subnormals,
f32::MAX/MIN, a disabled clip and a negative learning rate. The control line is there because a comparison that cannot fail proves nothing.Two deliberate shape choices, both forced by measurement:
specs/base/ternary_encoding.t27, lowers its out-parameter totrits: Vec<i32>taken by value and withoutmut, then assigns into it — that file’s generated Rust fails with 6 errors includingE0382: use of moved value. So the port expresses the per-element update as a pure function and leaves the loop to the caller.Err(LengthMismatch)on a length disagreement. With fixed-size arrays that is a static property, so the spec does not model it. That is a semantic difference, stated here rather than buried.Tests
Three, in
bootstrap/tests/backend_behaviour.rs— the file that compiles and runs the output instead of reading its text, which exists because four earlier defects were invisible to text-reading tests.Mutation-checked rather than merely green: disabling the
declared_fnsguard failsa_spec_declaring_its_own_abs_keeps_its_own_abs— the spec’s ownabsadds 100, so a wrong redirect prints3where97is correct — and leaves the other nine tests green. A test that only ever passes is not evidence.bootstrap/stage0/FROZEN_HASHis updated in the same commit, as M5 requires.