Skip to content

fix(#923): ArmSemantics silently no-oped 87 of 222 ArmOps — the verifier passed a lowering that is wrong on silicon - #975

Merged
avrabe merged 6 commits into
mainfrom
test/arm-semantics-coverage-923
Aug 14, 2026
Merged

fix(#923): ArmSemantics silently no-oped 87 of 222 ArmOps — the verifier passed a lowering that is wrong on silicon#975
avrabe merged 6 commits into
mainfrom
test/arm-semantics-coverage-923

Conversation

@avrabe

@avrabe avrabe commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Lane 9 of the v0.57 hub, artifact RQ-57-ARMSEM, issue #923 step 2 ("write the tests the split says are real"). The brief pointed at crates/synth-verify/src/arm_semantics.rs — measured in v0.56 at 47.4 % line coverage, the largest genuine coverage gap in the repo — and said: if you find the MODEL is wrong rather than merely untested, that outranks all the coverage work.

It was wrong. In both directions.

The finding

ArmSemantics::encode_op's _ => {} default arm left the state UNCHANGED for any op it had no arm for — 87 of ArmOp's 222 variants.

The clearest instance is a shipped rule, not a hypothetical. sel_dsl::generated::rule_i32_rotl is default-on and Rocq-proved (VcrSelRules.rule_i32_rotl_correct, Qed), and it emits exactly:

RSB rs, rm, #32
ROR rd, rn, rs

Both of those instructions were in the dropped 87. The Rocq proof was never in question; the SMT model simply executed neither instruction, so its opinion of this rule was worth nothing in either direction — it could not have caught a wrong rotl, and it did reject the right one.

The same silent default is a false-ACCEPT channel, which is the direction that matters: a lowering that computes the right value and then destroys it is invisible to the value VC. Measured on v0.57 main, through the public API synth verify itself uses (TranslationValidator::verify_ruleverify_equivalence):

i32.add  →  ADD r0,r0,r1 ; UXTB r0,r0        =>  Verified

That sequence returns (x + y) & 0xFF on silicon. Graded honestly: latent, not a live miscompile — the CLI's own rule table uses only 8 ops (Add/Sub/Mul/And/Orr/Eor/Cmp/SetCond), all modeled, and declines shift rules explicitly. It is reachable through the public API and through any future rule, which is exactly how #682 got in.

The sharper half, and why this is a defect in shipped code rather than a gap: the trap path had already noticed the hazard. exec_trap_subset_op's doc says encode_op's silent default "must never green-wash a trap derivation". But its defence was a hand-maintained allowlist mirroring encode_op's match arms, and the mirror had drifted — Rsb (that same rotl instruction), I32TruncF32S and I32TruncF32U were all allowlisted to DELEGATE to a model that does not implement them. The guard was inert for exactly those three.

And the false-ALARM side explains a comment the repo has been living with: the shipped i32.shl lowering AND r1,#31 ; LSL r0,r0,r1 came back Invalid. That is the real reason the CLI rule table declines every shift rule, under a comment blaming "a different ARM op encoding."

The fix

1. One source of truth, not two. The default arm now RECORDS the first unmodeled op in ArmState::unmodeled instead of ignoring it. The modeled set stays defined solely by encode_op's match arms, so it cannot rot. The value VC turns a set field into UnsupportedOperation; exec_trap_subset_op re-checks it after every delegation, closing the Rsb hole generically — for future allowlist entries too.

Those are the only two non-test consumers of encode_op (verified by grep): translation_validator.rs's value VC and the trap subset. expansion_validator.rs has its own executor over decoded machine words and never touches encode_op; fact_spec.rs's encode_op calls are WasmSemantics, a different model.

2. Register-amount shifts modeled FAITHFULLY. ARMv7-M A7.7.68/70/12/117 give shift_n = UInt(Rm<7:0>) — the low EIGHT bits, not Rm mod 32 and not all 32. This is the #682 class. WASM's mod-32 rule belongs to the LOWERING (the selector's AND #31), never to the ARM model.

3. Modeled alongside, all shipped-selector ops: Rsb, Sxtb/Sxth/Uxtb/Uxth, I32TruncF32S/I32TruncF32U. Cmn/Movw/Movt had a DUPLICATE model inside exec_trap_subset_op; that copy is deleted and they delegate to the single one. D registers in new_symbolic were allocated 32 bits wide; they are 64-bit registers.

4. Both of this crate's ARM models now state the same shift rule. validator_pattern.rs fed the raw 32-bit Rm under a comment conflating ARM's Rm<7:0> with WASM's mod-32. That one can only false-ALARM, never false-accept (for the model to bless a lowering the amount must be in [0,31], exactly where the two coincide) — no verdict changes, confirmed by the suite. Fixed anyway: two models stating different rules for one instruction means at most one of them is evidence, and a reader cannot tell which.

Verdicts, same entry point

sequence before after
shipped Rocq-proved rule_i32_rotl (RSB+ROR) Invalid Verified
ADD;UXTB for i32.add (wrong on silicon) Verified Invalid + counterexample
ADD;POP (genuinely unmodeled) Verified Err(UnsupportedOperation), names the op
5 shipped shift/rotate lowerings Invalid Verified
unmasked LSL for i32.shl Invalid Invalid — witness Rm = 0x4000_0080

That last witness is one only the <7:0> rule can produce: Rm mod 32 = 0 (WASM: identity) but Rm<7:0> = 128 ≥ 32 (ARM: zero).

The tests

25 tests, written against ARMv7-M (DDI 0403E.b), never against the model's own output. Two disciplines:

  • Independent oracle — expected values come from Rust (signed vs unsigned comparison, wrapping arithmetic, count_ones/leading_zeros/reverse_bits, IEEE-754 <). A second implementation, not a mirror.
  • Discriminating vectors — chosen to SEPARATE the ARM rule from the ones it gets confused with. With Rn = 1, Rm = 0x100 gives 1 under Rm<7:0> and under mod-32 but 0 under raw-32; Rm = 0x120 gives 0 under Rm<7:0> but 1 under mod-32. Together they pin <7:0> against BOTH wrong readings.

Prioritised by consequence: register-shift masking; update_flags_sub/update_flags_add and all ten condition codes (every i32 comparison and the cmp→select lever ride on these, and CMN drives the i32.div_s INT_MIN/−1 overflow guard, so a wrong C or V there is a wrong TRAP — update_flags_add had ZERO coverage); the ordered VFP compares behind the #709/#756 trunc guards (NaN makes every ordered relation false, Ge included; ±0.0 compare equal; negatives order by decreasing magnitude); 64-bit carry/borrow and the lexicographic compares' UNSIGNED low-word tiebreak.

The rotl test calls the shipped generator rather than transcribing its output, and asserts the emitted SHAPE before the verdict — so a change to the rule reaches the assertion instead of drifting away from it.

Deliberately NOT asserted, and said so in the file header: SDIV/UDIV by zero. ARM yields 0, SMT-LIB bvsdiv/bvudiv are total and yield something else, WASM traps, and the value clause is asserted only on the non-trapping path by the trap-gated VC. Pinning either answer would turn a scoped exclusion into a false claim.

Potency, checked rather than assumed

The first 24 passed on the first run — which is exactly when to ask whether they can fail. Eight mutations of the model, each confirmed present before and absent after replacement, each turning the suite RED: raw-32 shift amount (3 tests red), CMP carry polarity (2), CMN carry polarity, MOVT clobbering the low half, F32 Ge dropping its NaN exclusion, i64.lt_s using a signed low-word tiebreak, SXTB zero-extending, and removing the #923 unmodeled-op check.

The 25th (rotl) is not covered by that sweep, and shouldn't be claimed as such: ROR by raw Rm and by Rm<7:0> agree (period 32, and 256 is a multiple of 32), so the shift mutation cannot reach it. Its red-first evidence is the measured Invalid → Verified transition above; the discriminating mutation would be deleting the Rsb arm.

Coverage

cargo llvm-cov -p synth-verify --summary-only, identical command before and after, the "after" taken on this branch's HEAD:

lines regions
before 1179/2481 = 47.52 % 52.55 %
after 1475/2548 = 57.89 % 62.23 %

The brief's "2481 lines, 47.4 %" reproduced exactly — 2481 is llvm-cov's coverable-line count, not wc -l (3606). Not comparable to the CI job's --workspace figure, and -p also excludes CI's separate --features z3-solver,arm run.

Residual, stated not hidden

73 ArmOp variants remain unmodeled (was 87): 41 MVE vector ops, 32 others — flag-setting/carry forms (Adds/Adc/Subs/Sbc/Mla), subword and symbol memory, branch and stack ops. All 73 now DECLINE loudly instead of passing. Three docs asserted encode_op is "a genuinely second model of the same operations"; that claim was more generous than the code, and is amended with the measured before/after in the same PR (FEATURE_MATRIX via its template; artifacts/status.json unchanged).

Gates

Run locally without a pipe: cargo test --workspace ✅ · cargo clippy --workspace --all-targets -D warnings ✅ · cargo fmt --check ✅ · claim_check 43/43 ✅. The last two commits add a test and docs; clippy/fmt/claim_check were re-run on the final tree, and CI's Test job covers the workspace run against the merge.

Based on cb80e60c. No [Unreleased] edit, no version bump, no status.json regeneration. PR only — not to be merged by this lane.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 6 commits August 14, 2026 15:49
…decline, and model the shipped ones

`ArmSemantics::encode_op`'s `_ => {}` default arm left the state UNCHANGED for
any op it had no arm for. That is the false-ACCEPT direction, not the safe one:
a lowering that computes the right value and then destroys it is invisible to
the value VC. Measured on v0.57 main, through the public API `synth verify`
itself uses (`TranslationValidator::verify_rule` → `verify_equivalence`):

    i32.add  →  ADD r0,r0,r1 ; UXTB r0,r0        =>  Verified

That sequence returns `(x + y) & 0xFF` on silicon.

The trap path had already noticed the hazard — `exec_trap_subset_op`'s doc says
`encode_op`'s silent default "must never green-wash a trap derivation" — but its
defence was a hand-maintained allowlist mirroring `encode_op`'s arms, and the
mirror had drifted: `Rsb` (a SHIPPED sel-DSL rule's instruction), `I32TruncF32S`
and `I32TruncF32U` were all allowlisted to DELEGATE to a model that does not
implement them. The guard was inert for exactly those three.

The same gap in the other direction made the model reject CORRECT code: the
shipped `i32.shl` lowering `AND r1,#31 ; LSL r0,r0,r1` came back `Invalid`,
which is the real reason the CLI rule table declines every shift rule.

Fixes, in the order they matter:

1. One source of truth, not two. The default arm now RECORDS the first
   unmodeled op in `ArmState::unmodeled` instead of ignoring it. The modeled
   set stays defined solely by `encode_op`'s match arms, so it cannot rot.
   - the value VC turns a set field into `UnsupportedOperation`;
   - `exec_trap_subset_op` re-checks it after every delegation, so an
     allowlisted-but-unmodeled op is a loud decline (closes the `Rsb` hole
     generically, for future allowlist entries too).

2. Register-amount shifts modeled FAITHFULLY: ARMv7-M A7.7.68/70/12/117 give
   `shift_n = UInt(Rm<7:0>)` — the low EIGHT bits, not `Rm mod 32` and not all
   32. This is the #682 class. WASM's mod-32 rule belongs to the LOWERING (the
   selector's `AND #31`), never to the ARM model. Verified live: the unmasked
   lowering is now rejected with counterexample `Rm = 0x40000080`, a witness
   only the `<7:0>` rule can produce (`Rm mod 32 = 0`, `Rm<7:0> = 128 ≥ 32`).

3. Modeled alongside, all shipped-selector ops: `Rsb`, `Sxtb`/`Sxth`/`Uxtb`/
   `Uxth`, `I32TruncF32S`/`I32TruncF32U`. `Cmn`/`Movw`/`Movt` had a DUPLICATE
   model inside `exec_trap_subset_op`; that copy is deleted and the ops now
   delegate to the single model in `encode_op`.

Verdicts after, same entry point:
    ADD;UXTB    Verified -> Invalid (caught, with counterexample)
    ADD;POP     Verified -> Err(UnsupportedOperation) (loud decline)
    5 correct shift/rotate lowerings   Invalid -> Verified

Residual, stated not hidden: MVE vector ops, subword/symbol memory ops, branch
and stack ops remain unmodeled — but they now DECLINE instead of passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…n against the ARM ARM, not against the model

A test that asserts the model agrees with itself adds coverage and zero safety.
Every assertion here is written against ARMv7-M (DDI 0403E.b) behaviour, with
two disciplines making that concrete:

* INDEPENDENT ORACLE — expected values come from Rust (signed vs unsigned
  comparison, wrapping arithmetic, `count_ones`/`leading_zeros`/`reverse_bits`,
  IEEE-754 `<`), a second implementation rather than a mirror of the model.
* DISCRIMINATING VECTORS — chosen to SEPARATE the ARM rule from the rules it
  gets confused with, since a vector in the easy range agrees with every
  plausible-but-wrong model. The shift table is the clearest case: with Rn = 1,
  `Rm = 0x100` gives 1 under `Rm<7:0>` and 1 under mod-32 but 0 under raw-32,
  while `Rm = 0x120` gives 0 under `Rm<7:0>` and 1 under mod-32. Together the
  two vectors pin `<7:0>` against BOTH wrong readings.

Prioritised by consequence, not by line count:

* register-amount shifts (#682 class, decided in this file);
* `update_flags_sub`/`update_flags_add` + all ten condition codes — every i32
  comparison and the cmp→select lever ride on these, and `CMN` drives the
  `i32.div_s` INT_MIN/-1 overflow guard, so a wrong C or V there is a wrong
  TRAP. `update_flags_add` had ZERO coverage. `CMP 1, -1` is the vector that
  separates the C-reading conditions from the N/V-reading ones;
* the ordered VFP compares behind the #709/#756 trunc guards: NaN makes every
  ordered relation false (`Ge` included — a `!(a<b)` implementation gets this
  wrong), ±0.0 compare equal, negatives order by decreasing magnitude;
* 64-bit pairs: carry/borrow across the pair, and the lexicographic compares'
  UNSIGNED low-word tiebreak;
* the #923 regressions in both directions, plus a drift guard asserting every
  trap-subset delegate is actually modeled.

DELIBERATELY NOT ASSERTED, and said so in the file header: SDIV/UDIV by zero.
ARM yields 0, SMT-LIB bvsdiv/bvudiv are total and yield something else, WASM
traps, and the value clause is asserted only on the non-trapping path by the
trap-gated VC. Pinning either answer would turn a scoped exclusion into a false
claim. The division tests stop where ARM, SMT and WASM all agree.

POTENCY CHECKED, not assumed — all 24 passed on the first run, which is exactly
when to ask whether they can fail. Eight mutations of the model, each confirmed
present before and absent after replacement, each turning the suite RED:
raw-32 shift amount (3 tests red), CMP carry polarity (2), CMN carry polarity,
MOVT clobbering the low half, F32 Ge dropping its NaN exclusion, i64.lt_s using
a signed low-word tiebreak, SXTB zero-extending, and removing the #923
unmodeled-op check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
`validator_pattern.rs` (the VCR-RA-003 whole-function validator) fed the RAW
32-bit `Rm` to its SMT shift and carried a comment saying "the selector is
responsible for masking the amount mod 32" — conflating ARM's `Rm<7:0>` with
WASM's mod-32. Neither is what the executor did.

Consequence, graded honestly: this one can only ever false-ALARM, never
false-accept. For the model to bless a lowering it must match the WASM
reference for ALL `Rm`, which forces the amount into `[0,31]`, exactly where
the raw value and `Rm<7:0>` coincide; and an unmasked lowering is rejected by
both readings. So no verdict changes, and the full synth-verify suite confirms
it (267 tests, unchanged).

Fixed anyway, because the point of this lane is that a model is evidence about
silicon: two models in one crate stating different rules for one instruction
means at most one of them is evidence, and a reader cannot tell which. They now
share a helper and a citation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Three places assert that `synth-verify`'s `ArmSemantics::encode_op` is "a
genuinely second model of the same operations" — the VCR-VER-004 independence
caveat in FEATURE_MATRIX (via its template), `abi_contract.rs`'s module doc, and
the roadmap entry. The claim was load-bearing (it is what keeps "three
independent validators" from being an overclaim) and it was more generous than
the code: 87 of `ArmOp`'s 222 variants were modeled as doing nothing.

Amended with the measured number, before and after, and with the residual split
by kind (41 MVE, 32 others) rather than left as a category list. FEATURE_MATRIX
is generated, so the edit lands in `scripts/templates/feature_matrix.md.tmpl`
and the committed copy is regenerated; `artifacts/status.json` is unchanged (no
counts moved). claim_check 43/43.

Also completes the trap-subset drift guard to cover EVERY delegate on the
allowlist, not 21 of the 29. A test that guards against a hand-maintained
mirror must not be a hand-maintained mirror of part of it; the runtime
`delegate_to_encode_op` check is the real guard, and the test now says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…-instruction

The sharpest instance of the finding, taken verbatim from the shipped table
rather than paraphrased. `sel_dsl::generated::rule_i32_rotl` is a DEFAULT-ON,
Rocq-proved selector rule (`VcrSelRules.rule_i32_rotl_correct`, Qed) and it
emits exactly `RSB rs, rm, #32` then `ROR rd, rn, rs` — BOTH of which were in
the silently-dropped 87.

The Rocq proof was never in question. The SMT model simply executed neither
instruction, so its opinion of this rule was worth nothing in either direction:
it could not have caught a wrong rotl, and it would have rejected the right one.

The test calls the generator rather than transcribing its output, so a change to
the shipped rule reaches the assertion instead of drifting away from it, and it
asserts the emitted SHAPE before the verdict so a shape change fails loudly with
"re-derive this test rather than loosening it" instead of silently verifying
something else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.03960% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-verify/src/arm_semantics.rs 95.45% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit 572b101 into main Aug 14, 2026
57 checks passed
@avrabe
avrabe deleted the test/arm-semantics-coverage-923 branch August 14, 2026 15:59
avrabe added a commit that referenced this pull request Aug 14, 2026
…e 4 shipped (#979)

Two things. The re-grades are bookkeeping; the back-fill is a finding.

RE-GRADED to `implemented` (work merged on main):
  RQ-57-COUNTPARAMS  #970  (#974)   ARM+RV32 conditional-param miscompile
  RQ-57-GPIO         #846  (#976)   gpio-thin 502 -> 494 B
  RQ-57-ARMSEM       #923  (#975)   ArmSemantics no-oped 87 of 222 ops
  RQ-57-MCDC         #912  (#978)   MC/DC over synth's own decision logic

RQ-57-BACKFILL: the back-fill was NOT performed, on evidence.

263 of 288 artifacts carry no `release:`. The prescribed derivation — "the
first tag containing the artifact" — RUNS FINE (0 undecidable, 24 distinct
tags) and answers the WRONG QUESTION. It yields when an artifact ENTERED THE
PLAN; `release:` means the release the work is TARGETED AT or SHIPPED IN.

The file supplied its own control case, which is what settles it:

  VCR-RA-001, hand-set          release: v0.24.0   <- when the work shipped
  VCR-RA-001, mechanical rule   v0.11.30           <- introducing commit's tag

It is the ONLY artifact in verified-codegen-roadmap.yaml that already carried a
`release:`, and the rule contradicts it. Sweeping the other 32 would have
written 32 false values with the one correct value sitting beside them as the
disproof.

Scale, had it been applied blindly: 190 of the 263 resolve to v0.1.1 — the
initial import, i.e. the standing requirement base (architecture, stakeholder
and system requirements, component model, target platforms). Tagging those
v0.1.1 asserts the whole foundational base was targeted at the first tag, and
makes "what is in v0.1.1?" return 190 artifacts including work that shipped
forty releases later. That is the corruption of the readiness query this
artifact exists to prevent — so the artifact's own guardrail ("stays
unassigned rather than guessed", the #911 lesson applied to planning data)
decides it.

CONVENTION, now documented in docs/release-process.md so the absence stops
being re-filed as an unfinished chore:
  * per-release plan artifacts carry `release:` (they are work items; they do)
  * standing artifacts carry it ONLY where the shipping release is known, as
    VCR-RA-001 does
  * setting it on a standing artifact is a per-artifact judgement with
    CHANGELOG evidence, never a sweep

A missing `release:` is a justified state. A wrong one is worse than a missing
one.

rivet: 50 errors / 166 warnings before AND after — unchanged. claim_check 43/43.

Refs #912


Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
avrabe added a commit that referenced this pull request Aug 14, 2026
…of them mine

Cold review of the assembled release. Nothing blocked the tag; everything below
is accuracy. Four of the eight were errors in the CHANGELOG I had just written,
which is the reason the review exists.

THE GENERALIZABLE FINDING, and it is pointed given this release's theme:
`check_generated_fresh` byte-compares the RENDERED FEATURE_MATRIX against the
TEMPLATE. `render_feature_matrix` only substitutes `{{...}}` fields, so the gate
proves the render is faithful to the template — and NEVER that the template is
faithful to the code. Every stale number below lives in template prose no
substitution touches. In a release titled "the checkers were the defects", that
is the checker that cannot fail. Three independent stale numbers survived a
green 43/43.

USER-FACING FALSE, verified by compiling rather than by reading:
  FEATURE_MATRIX listed "writing a PARAM local in a LEAF function" as a LOUD
  DECLINE on aarch64. #971 shipped exactly that. A leaf `local.set` on a param
  compiles: 32 bytes of machine code, exit 0. Also corrected in the same row:
  homing is no longer non-leaf-only, and the float-param decline widened with
  it. Fixed in the TEMPLATE (the render is generated) + regen.

MY CHANGELOG ERRORS:
  * "145 of the 175 pre-declined / 30 reachable" matched no partition. The
    shipped source (wcet_loops.rs:1232) says 142 give up with `true`, leaving
    33. Re-derived: 142/33. Corrected.
  * "demoted to a zero-initialised local" is wrong for the two backends the
    entry is about — zero-init is gated on first-access-being-a-READ, and in
    the cond-write shape the first access IS the write, so nothing initialises
    the slot. That is WHY it reads poison; the old wording made an
    information-disclosure bug sound like a benign wrong value, and contradicted
    the entry's own next sentence.
  * "Nine artifacts" — there are ten, and RQ-57-DOCSWEEP (#946/#968) had NO
    CHANGELOG entry at all despite touching CLAUDE.md, coq/STATUS.md,
    PROJECT_STATUS.md, the matrix template and eight source files. Added.
  * "Five in-tree oracles took that opt-in" — eight scripts plus three Rust
    tests. All eight carry floors, but `i64_param_518_riscv_loudskip`'s is
    `compiles >= 1`, which is a floor and NOT the "tight" one the paragraph
    claimed for the set. Named rather than folded into the claim.

STALE COUNTS (the template-prose class above):
  ORACLE_WIRING.md, the matrix template and claims.yaml all said "137 oracles /
  295,621 emulator entries". Re-derived independently — and the reviewer's
  number and mine agree exactly: 144 oracles / 296,059. Both `count-min` pins
  moved 137 -> 144 with them (same `emulations >=` pattern, two sibling claims);
  the pinned verbatim texts moved too, or the ledger would have gone red
  against its own corrected doc.

REVERSE STALENESS (a doc calling SHIPPED work missing):
  `synth verify` declines shift rules citing "SMT modeling of the variable-shift
  register encoding is an open gap". #975 CLOSED that gap — it modelled
  LslReg/LsrReg/AsrReg/RorReg as Rm<7:0> (ARMv7-M A7.7.68/70/12/117) and moved
  five lowerings Invalid -> Verified. Both comments corrected to say what is
  true: the modelling gap is closed, the remaining decline is a WIRING residual.
  Behaviour deliberately unchanged — rewiring the rule table is a
  verification-surface change, not release assembly. Filed as #981.

ARTIFACT:
  RQ-57-PROVGAP still asserted "9 object branches with no WASM origin" as fact
  while its own PR disproved it. Outcome recorded, as RQ-57-BACKFILL already did.

  docs/architecture/CRATE_STRUCTURE.md said 18 crates; there are 19. A
  RECURRENCE — PROJECT_STATUS.md cites this exact drift as why it was gutted in
  the #946 sweep, and one file over it was live again.

Gates after: claim_check 43/43, check_version_pins OK at 0.57.0,
cargo check -p synth-cli rc=0.

Refs #980, #981

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
avrabe added a commit that referenced this pull request Aug 14, 2026
My own miss: I ran cargo check on the edited file but not cargo fmt, and
Format is a required context. The comment content is unchanged — only the
indentation rustfmt wanted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
avrabe added a commit that referenced this pull request Aug 14, 2026
* chore(release): v0.57.0 assembly — "the checkers were the defects"

Nine artifacts. In five of them the bug was in the machinery that checks the
compiled code, not in the compiled code:

  #975  ArmSemantics silently no-oped 87 of 222 ops — a Rocq-proved,
        default-on rotl rule was "validated" by a model that executed neither
        of its instructions
  #976  the gpio differential CANNOT discriminate the miscompile it guards —
        complementary conditions, so no input to that driver can
  #969  writes_sp claimed exhaustiveness over a wildcard absorbing 175 of 222
  #967  the "9 unattributed branches" were manufactured by witness's own
        hardcoded divergence text
  #979  the prescribed release: back-fill would have written 32 false entries,
        with the one correct pre-existing value beside them as the disproof

The unifying property is that each of those checks COULD NOT FAIL. This
release makes them able to fail and proves it by making them fail on purpose.

Also fixed, and the most severe item: #974 — a conditionally-written parameter
was demoted to a zero-init local on ARM and RISC-V. Exit 0, no decline, wrong
code; on RISC-V it reads an UNINITIALISED stack slot (0xDEADBEEF under a
poisoned stack), an information-disclosure shape. ARM behaves identically —
which the issue predicted otherwise, and only execution settled.

Release surfaces, all four swept and checker-confirmed at 0.57.0:
  Cargo.toml [workspace.package] + 10 path-dep pins
  MODULE.bazel, npm/package.json, Cargo.lock (cargo metadata)
  scripts/check_version_pins.py: OK

Derived artifacts regenerated (--emit-status): artifacts/status.json,
docs/status/FEATURE_MATRIX.md. Claim gate: 43/43.

Open by design, named not hidden: #973 (ARM select miscompile, found only
because a lane compiled ARM fixtures — which CI never does), #977 (ELF-magic
flake, second sighting), #938 (breaking object 0.x-minor bump, auto-merge
disabled), #912 (open with four remaining: items).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* fix(release): act on the v0.57.0 cold review — 8 accuracy defects, 4 of them mine

Cold review of the assembled release. Nothing blocked the tag; everything below
is accuracy. Four of the eight were errors in the CHANGELOG I had just written,
which is the reason the review exists.

THE GENERALIZABLE FINDING, and it is pointed given this release's theme:
`check_generated_fresh` byte-compares the RENDERED FEATURE_MATRIX against the
TEMPLATE. `render_feature_matrix` only substitutes `{{...}}` fields, so the gate
proves the render is faithful to the template — and NEVER that the template is
faithful to the code. Every stale number below lives in template prose no
substitution touches. In a release titled "the checkers were the defects", that
is the checker that cannot fail. Three independent stale numbers survived a
green 43/43.

USER-FACING FALSE, verified by compiling rather than by reading:
  FEATURE_MATRIX listed "writing a PARAM local in a LEAF function" as a LOUD
  DECLINE on aarch64. #971 shipped exactly that. A leaf `local.set` on a param
  compiles: 32 bytes of machine code, exit 0. Also corrected in the same row:
  homing is no longer non-leaf-only, and the float-param decline widened with
  it. Fixed in the TEMPLATE (the render is generated) + regen.

MY CHANGELOG ERRORS:
  * "145 of the 175 pre-declined / 30 reachable" matched no partition. The
    shipped source (wcet_loops.rs:1232) says 142 give up with `true`, leaving
    33. Re-derived: 142/33. Corrected.
  * "demoted to a zero-initialised local" is wrong for the two backends the
    entry is about — zero-init is gated on first-access-being-a-READ, and in
    the cond-write shape the first access IS the write, so nothing initialises
    the slot. That is WHY it reads poison; the old wording made an
    information-disclosure bug sound like a benign wrong value, and contradicted
    the entry's own next sentence.
  * "Nine artifacts" — there are ten, and RQ-57-DOCSWEEP (#946/#968) had NO
    CHANGELOG entry at all despite touching CLAUDE.md, coq/STATUS.md,
    PROJECT_STATUS.md, the matrix template and eight source files. Added.
  * "Five in-tree oracles took that opt-in" — eight scripts plus three Rust
    tests. All eight carry floors, but `i64_param_518_riscv_loudskip`'s is
    `compiles >= 1`, which is a floor and NOT the "tight" one the paragraph
    claimed for the set. Named rather than folded into the claim.

STALE COUNTS (the template-prose class above):
  ORACLE_WIRING.md, the matrix template and claims.yaml all said "137 oracles /
  295,621 emulator entries". Re-derived independently — and the reviewer's
  number and mine agree exactly: 144 oracles / 296,059. Both `count-min` pins
  moved 137 -> 144 with them (same `emulations >=` pattern, two sibling claims);
  the pinned verbatim texts moved too, or the ledger would have gone red
  against its own corrected doc.

REVERSE STALENESS (a doc calling SHIPPED work missing):
  `synth verify` declines shift rules citing "SMT modeling of the variable-shift
  register encoding is an open gap". #975 CLOSED that gap — it modelled
  LslReg/LsrReg/AsrReg/RorReg as Rm<7:0> (ARMv7-M A7.7.68/70/12/117) and moved
  five lowerings Invalid -> Verified. Both comments corrected to say what is
  true: the modelling gap is closed, the remaining decline is a WIRING residual.
  Behaviour deliberately unchanged — rewiring the rule table is a
  verification-surface change, not release assembly. Filed as #981.

ARTIFACT:
  RQ-57-PROVGAP still asserted "9 object branches with no WASM origin" as fact
  while its own PR disproved it. Outcome recorded, as RQ-57-BACKFILL already did.

  docs/architecture/CRATE_STRUCTURE.md said 18 crates; there are 19. A
  RECURRENCE — PROJECT_STATUS.md cites this exact drift as why it was gutted in
  the #946 sweep, and one file over it was live again.

Gates after: claim_check 43/43, check_version_pins OK at 0.57.0,
cargo check -p synth-cli rc=0.

Refs #980, #981

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* style: rustfmt the #975 decline-reason comment (indent 14 -> 12)

My own miss: I ran cargo check on the edited file but not cargo fmt, and
Format is a required context. The comment content is unchanged — only the
indentation rustfmt wanted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.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.

1 participant