Skip to content

fix(rust-backend): lower the bare math builtins, and port ring-103 to a spec - #3401

Merged
gHashTag merged 1 commit into
masterfrom
fix/math-builtins-rust-c
Sep 7, 2026
Merged

fix(rust-backend): lower the bare math builtins, and port ring-103 to a spec#3401
gHashTag merged 1 commit into
masterfrom
fix/math-builtins-rust-c

Conversation

@gHashTag

@gHashTag gHashTag commented Sep 7, 2026

Copy link
Copy Markdown
Owner

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-rust emitted abs(x), sqrt(x), min(a, b) and their siblings as bare free functions. Rust has none of them as free functions:

error[E0425]: cannot find function `abs` in this scope

zig_builtin_to_rust already translated every one of them correctly. 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. The Zig backend closed this identical class at gen_expr, with a comment naming the same symptom.

Three guards, each added because a measurement demanded it

guard why evidence
declared_fns a spec may own the name 30 corpus declarations do — fn floor in 10 specs, fn abs in 9, fn cos in 3. Redirecting those is a wrong translation that compiles.
typed receiver a literal has no type (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.
not in a const sqrt is not const fn inside pub const K: f64 = ... every spelling fails; trading E0425 for E0015 is a new failure, not a repair.

declared_fns is collected by a new collect_declared_fns rather than reusing collect_fn_ret_types, which skips a declaration whose extra_return_type is 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.

log is deliberately excluded: Rust’s f64::log takes a base, so (x).log() is E0061. The existing @log arm 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.

  • 9 of 650 files’ emitted Rust changes; none crosses fail → pass. Each carries other blockers, matching this repository’s established shape that a rejected file has four or five error families, not one.
  • 1 file gains an error class, and it was proven pre-existing and newly reached, not introduced: the only diff between the two outputs is line 140 (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 declared mut.

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.t27 replaces the arithmetic of rings/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:

cases 20736  bit-exact 20736  mismatched 0
harness control (two near-equal f32 differ by bits): true
identity_witness  orig=true spec=true

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:

  • Scalar, not out-parameter. The natural model, specs/base/ternary_encoding.t27, lowers its out-parameter to trits: Vec<i32> taken by value and without mut, then assigns into it — that file’s generated Rust fails with 6 errors including E0382: use of moved value. So the port expresses the per-element update as a pure function and leaves the loop to the caller.
  • The error enum is gone. The original returns 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_fns guard fails a_spec_declaring_its_own_abs_keeps_its_own_abs — the spec’s own abs adds 100, so a wrong redirect prints 3 where 97 is correct — and leaves the other nine tests green. A test that only ever passes is not evidence.

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

… 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
gHashTag enabled auto-merge (squash) September 7, 2026 17:30
@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.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-09-07 17:30:40 UTC

Summary

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

These columns do not partition: 2 + 13 + 0 + 0 = 15, and there are 16 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=8dca9dd2df94 != 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).

@gHashTag
gHashTag merged commit da37c4e into master Sep 7, 2026
32 of 35 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-rust emits the math builtins as bare free functions, which Rust does not have

1 participant