fix(#923): ArmSemantics silently no-oped 87 of 222 ArmOps — the verifier passed a lowering that is wrong on silicon - #975
Merged
Conversation
…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
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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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>
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.
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 ofArmOp's 222 variants.The clearest instance is a shipped rule, not a hypothetical.
sel_dsl::generated::rule_i32_rotlis default-on and Rocq-proved (VcrSelRules.rule_i32_rotl_correct, Qed), and it emits exactly: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 APIsynth verifyitself uses (TranslationValidator::verify_rule→verify_equivalence):That sequence returns
(x + y) & 0xFFon 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 saysencode_op's silent default "must never green-wash a trap derivation". But its defence was a hand-maintained allowlist mirroringencode_op's match arms, and the mirror had drifted —Rsb(that samerotlinstruction),I32TruncF32SandI32TruncF32Uwere 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.shlloweringAND r1,#31 ; LSL r0,r0,r1came backInvalid. 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::unmodeledinstead of ignoring it. The modeled set stays defined solely byencode_op's match arms, so it cannot rot. The value VC turns a set field intoUnsupportedOperation;exec_trap_subset_opre-checks it after every delegation, closing theRsbhole 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.rshas its own executor over decoded machine words and never touchesencode_op;fact_spec.rs'sencode_opcalls areWasmSemantics, 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, notRm mod 32and not all 32. This is the #682 class. WASM's mod-32 rule belongs to the LOWERING (the selector'sAND #31), never to the ARM model.3. Modeled alongside, all shipped-selector ops:
Rsb,Sxtb/Sxth/Uxtb/Uxth,I32TruncF32S/I32TruncF32U.Cmn/Movw/Movthad a DUPLICATE model insideexec_trap_subset_op; that copy is deleted and they delegate to the single one. D registers innew_symbolicwere 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.rsfed the raw 32-bitRmunder a comment conflating ARM'sRm<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
rule_i32_rotl(RSB+ROR)InvalidVerifiedADD;UXTBfori32.add(wrong on silicon)VerifiedInvalid+ counterexampleADD;POP(genuinely unmodeled)VerifiedErr(UnsupportedOperation), names the opInvalidVerifiedLSLfori32.shlInvalidInvalid— witnessRm = 0x4000_0080That last witness is one only the
<7:0>rule can produce:Rm mod 32 = 0(WASM: identity) butRm<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:
count_ones/leading_zeros/reverse_bits, IEEE-754<). A second implementation, not a mirror.Rn = 1,Rm = 0x100gives 1 underRm<7:0>and under mod-32 but 0 under raw-32;Rm = 0x120gives 0 underRm<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_addand all ten condition codes (every i32 comparison and the cmp→select lever ride on these, andCMNdrives thei32.div_sINT_MIN/−1 overflow guard, so a wrong C or V there is a wrong TRAP —update_flags_addhad ZERO coverage); the ordered VFP compares behind the #709/#756 trunc guards (NaN makes every ordered relation false,Geincluded; ±0.0 compare equal; negatives order by decreasing magnitude); 64-bit carry/borrow and the lexicographic compares' UNSIGNED low-word tiebreak.The
rotltest 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/UDIVby zero. ARM yields 0, SMT-LIBbvsdiv/bvudivare 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
Gedropping its NaN exclusion,i64.lt_susing 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:RORby rawRmand byRm<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 measuredInvalid → Verifiedtransition above; the discriminating mutation would be deleting theRsbarm.Coverage
cargo llvm-cov -p synth-verify --summary-only, identical command before and after, the "after" taken on this branch's HEAD: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--workspacefigure, and-palso excludes CI's separate--features z3-solver,armrun.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 assertedencode_opis "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.jsonunchanged).Gates
Run locally without a pipe:
cargo test --workspace✅ ·cargo clippy --workspace --all-targets -D warnings✅ ·cargo fmt --check✅ ·claim_check43/43 ✅. The last two commits add a test and docs;clippy/fmt/claim_checkwere re-run on the final tree, and CI'sTestjob covers the workspace run against the merge.Based on
cb80e60c. No[Unreleased]edit, no version bump, nostatus.jsonregeneration. PR only — not to be merged by this lane.🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L