Skip to content

fix(spec): sim_time_ns must saturate, as its hand-written model always did - #3421

Merged
gHashTag merged 1 commit into
masterfrom
port/ring-101
Sep 7, 2026
Merged

fix(spec): sim_time_ns must saturate, as its hand-written model always did#3421
gHashTag merged 1 commit into
masterfrom
port/ring-101

Conversation

@gHashTag

@gHashTag gHashTag commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Closes #3420

rings/ring-090-rust says in its own doc comment that it is "faithful to the spec" specs/fpga/simulator.t27. It is not — and the spec is the one that is wrong.

Measured

A differential harness driving every shared function from both modules on the same inputs — 14 u32 values crossed with 3 name strings:

cases 1190  agree 1064  disagree 126
harness control (1 != 2): true
function disagree example
sim_time_ns 4 / 10 2_000_000_000 → hand 4294967295, spec 2820130816
sim_time_us 1 / 10 hand 4294967, spec 2820130
sim_time_ms 1 / 10 hand 4294, spec 2820
cycles_for_time_ns 0 / 10

Cause

The hand-written model widens to u64 and then saturates. The spec widened and then narrowed with a bare as u32, which wraps: at 2,000,000,000 cycles on the default 100 MHz clock, 20,000,000,000 ns becomes 2,820,130,816.

The spec states the assumption that fails, in a comment on the line above it:

Widen the intermediate to u64 and narrow the (small) result back.

It is not always small. With the guard: 1190 of 1190 agree.

Why this was worth looking for

Ten of the seventeen rings/* crates name a spec that exists in this repository, and the overlap is wildly uneven:

ring hand items shared only spec
ring-090 22 21 0 15 of 16 signatures byte-identical
ring-097 18 11 0
ring-099 9 4 0
ring-098 12 5 2 drifted both ways
ring-088 3 0 54 names gf16.t27, unrelated
ring-101 5 0 54 same

ring-090 is the closest pair in the repository — and it had still drifted in behaviour. A comparison of names and signatures alone would have called it a match; only running both did.

Tests

Two, added to the spec in its own language rather than around it — the saturating case and an exact case below the ceiling. Both reach the Zig output.

Two guards from the last two passes earned their keep here

Neither needed a human to remember:

…s did

Closes #3420

rings/ring-090-rust says in its own doc comment that it is "faithful to the
spec" specs/fpga/simulator.t27. A differential harness driving every shared
function from both modules on the same inputs -- 14 u32 values crossed with 3
name strings -- disagreed on 126 of 1190 cases.

  sim_time_ns          4/10   2_000_000_000 -> hand 4294967295, spec 2820130816
  sim_time_us          1/10                 -> hand 4294967,    spec 2820130
  sim_time_ms          1/10                 -> hand 4294,       spec 2820
  cycles_for_time_ns   0/10

The hand-written model widens to u64 and then saturates. The spec widened and
then narrowed with a bare `as u32`, which wraps: at 2_000_000_000 cycles on the
default 100 MHz clock, 20_000_000_000 ns becomes 2_820_130_816.

The spec states the assumption that fails, in a comment on the line above:
"Widen the intermediate to u64 and narrow the (small) result back." It is not
always small. With the guard, the harness reports 1190 of 1190 agreeing, and its
control confirms it can see a difference.

Context for why this was worth looking for. Ten of the seventeen rings/* crates
name a spec that exists in this repository, and the overlap between them is
wildly uneven:

  ring-090   22 hand items, 21 shared, 0 spec-only   <- 15/16 signatures identical
  ring-097   18 hand items, 11 shared, 0 spec-only
  ring-099    9 hand items,  4 shared, 0 spec-only
  ring-098   12 hand items,  5 shared, 2 spec-only
  ring-088    3 hand items,  0 shared               <- names gf16.t27, unrelated
  ring-101    5 hand items,  0 shared               <- same

ring-090 is the closest pair in the repository and it had still drifted in
BEHAVIOUR, which a comparison of names and signatures alone could never show.

Two tests are added to the spec in its own language rather than around it: the
saturating case and an exact case below the ceiling. Both reach the Zig output.

The seal was refreshed by `t27c seal --save`, which reported "and 1 other seal
file(s) naming the same spec" and updated both fpga_Simulator.json and
Simulator.json -- the repair from #3419 on its first real case, after
check_seal_currency.py from #3416 named the stale hashes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gHashTag
gHashTag enabled auto-merge (squash) September 7, 2026 21:48
@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 21:48:25 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)=9c910e4dfae2 != 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 5bc53f3 into master Sep 7, 2026
28 of 30 checks passed
gHashTag added a commit that referenced this pull request Sep 7, 2026
… needs (#3425)

Closes #3424
Refs #3420

check_ring_spec_drift.py reports CONVERGED when every shared function has an
identical signature, and carries a warning it earned: ring-090 measured
CONVERGED at 16 of 16 identical and still disagreed with its spec on 126 of
1190 differential cases. Only running both found it, and that harness was
written by hand for one pair.

This writes it. For a CONVERGED pair it emits a Rust program including both
modules, calls every shared function on the same synthesised inputs, and
compares.

  ring-090 / specs/fpga/simulator.t27    936 cases, all agree, 16 of 16, 0 refused
  ring-099 / specs/pipeline/e2e_test.t27  25 cases, all agree,  4 of 4,  0 refused

The control ran on the live pair, not only a fixture: re-introducing the exact
defect #3421 repaired -- deleting the saturation guard from the spec -- makes
the tool report `936 cases, agree 864, disagree 72`, name sim_time_ms, and exit
1. Restoring it returns 936/936 and exit 0.

Three design points, each because the naive version would have lied:

  * It REFUSES rather than skips. A parameter or return type it cannot
    synthesise is named and the run exits 2. A harness covering eight of
    sixteen functions and printing "agree" is worse than no harness, because
    the eight it dropped are where a difference hides.
  * A panic is a DISAGREEMENT. Both calls run under catch_unwind with
    -C overflow-checks=on. Without it the process dies with no output, which
    reads as "no cases run" rather than as the sharpest difference there is;
    a release build would wrap silently in both and hide it.
  * Structs are built by their PRODUCERS -- SimConfig comes from calling
    sim_config with the same grid -- so the generator needs no knowledge of
    fields, which is how the hand-written harness did it.

Stated rather than hidden: ring-099's 25 cases are thin. Its functions take
&mut slices and the generator passes zero-initialised buffers, so values are
compared after the call but the inputs are not varied. 25 is not the evidence
936 is.

Also reported rather than done silently: 55 inner doc-comment and attribute
lines are stripped from ring-090 (3 from ring-099) so the crate can be
`include!`d as a module.

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.

specs/fpga/simulator.t27 wraps where the hand-written ring-090 saturates: 126 of 1190 cases disagree

1 participant