You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two scanners over disjoint surfaces, both instructed to report the disagreement and never rule on which side is right. That framing was load-bearing: of this release's three known instances the resolution differed each time (doc wrong / code wrong / fixture wrong), so a scanner assuming "code wins" would have mis-filed two of three.
Examined ~930 claims mechanically. 30 genuine disagreements (~500 Rust doc comments → 20; ~430 prose claims → 10). Two already fixed in #945. This issue carries the rest.
TIER 1 — possible DEFECTS, need investigation before anything is edited
These are not assumed to be code bugs. Each is a claim the code appears to contradict in a way that would matter if the doc is the intended contract.
A. VFP encoder precondition holds for only ONE of the two selectors
arm_encoder.rs:7286 states "The SELECTOR guarantees dm is a dead temp, never a pinned param/local home (it copies a home into a fresh D-temp first)."
select_with_stack genuinely implements that copy (instruction_selector.rs:4505). select_default does not — it allocates from a bare round-robin counter with no home/liveness test (:7784, alloc_vfp_dreg:6020), and grep -c vfp_home over its body returns 0.
--relocatable forces select_default (per #197). If the encoder's overlapping VCVT write can land on a live f64 home there, this is a miscompile, not a doc bug.
B. "never S0" — index 0 is admitted
arm_encoder.rs:7280 / :7221: the 32-bit result stages through an S-alias "never S0, which may hold an unrelated live value (the #615 class)". The only guard is if dm_num > 7; encode_sreg(0)is S0, D0 is reachable (next_vfp_dreg: 0), and the encoder's own test records vmov s0, r3. Same #615 class the comment invokes.
C. --volatile-segment moves bytes while documented as inert
main.rs:471: "PHASE 1 = plumbing only … the emitted bytes are unchanged whether or not the flag is passed."
Measured by the scanner on scripts/repro/volatile_segment_543.wat, no SYNTH_* set: 196 B without the flag, 234 B with --volatile-segment 0x100:16. The Phase-2 back-off is already wired (arm_backend.rs:1432 const-CSE skipped when the list is non-empty; optimizer_bridge.rs:3213 base-CSE). The frozen-byte gates never exercise the flag, so nothing catches this.
D. WCET loop SP-tracking claims exhaustiveness over a wildcard
wcet_loops.rs:1036: "Exhaustive over every rd-carrying op the priced instruction set can contain." The match ends _ => false (:1089), and eight priced rd-carrying ops have no arm (I64SetCond, I64SetCondZ, I64Clz, I64Ctz, I64Popcnt, Str, Strb, Strh).
Those eight do not write SP, so the WCET bound is not currently wrong — but the stated tripwire does not exist: a new ArmOp that does write SP compiles fine and silently reads false. The parenthetical also says I64Popcnt is a whole-function LoopedExpansion decline; op_cost prices it.
TIER 2 — stale rationale on a live exclusion (safe today, wrong reason)
where
claim
source
wcet.rs:77
"the audited expansions push at most 3 registers, so this holds with margin"
PUSH {R0-R3} (0xB40F, 4 regs) is in the pricedI64Rotl/I64Rotr arms. CEIL_PER_HALFWORD = 5 then holds exactly (1+4), not with margin — and it is a claims.yaml-pinned soundness constant
validator_pattern.rs:94
i64 div/rem excluded because "synth lowers 64-bit division to a runtime-library call (__aeabi_ldivmod)"
The encoder expands them inline (arm_encoder.rs:5646, 64-round loop). grep -rn '__aeabi' crates/ finds only comments. Four ops are excluded from translation validation on a rationale that does not describe the lowering
static_data_addr.rs:83
"AArch64: N/A — every memory op loud-declines at selection"
aarch64/selector.rs:2186+ lowers I32Load/Store, I64Load/Store, the 8/16-bit variants. Premise false since #851
alloc_validator.rs:56
"on RV every op_dest == None op is control-flow / system / label / call"
Sb/Sh/Sw are None and none of those four. Conclusion survives (stores write no register) but on a different argument than the one written
selector.rs:1498 (RV)
"alloc_temp must never hand any of them out"
The exhaustion path returns self.temps[0] — by construction one of them (:1556). Safe only via the alloc_exhausted decline the doc omits
TIER 3 — docs that UNDERSTATE what ships
Every one makes synth look less capable than it is. That is why none was noticed.
aarch64/selector.rs:35 lists call_indirect, float rounding, f32/f64 load/store, i64→float converts as "deliberately still declined (loud-skip)" — all have real lowerings in the same file's shipped match. Same class in riscv/selector.rs:10 (br_table, cross-function calls listed out-of-scope, both lowered).
instruction_selector.rs:5037 — "the 40 Rocq-proved rules"; the shipped table has 50, including nine i64 ops the enumeration omits.
aarch64/selector.rs:272 — select_typed_cf says loop/if/br_table are "declined by name"; all three are lowered (:1679, :1779, :1802). The same wrapper hardcodes MemBounds::Unchecked (:301) while MemBounds' own doc says "there is no silent default".
main.rs:4538 — --shadow-stack-size: "a Some here is an honest Err, never a silent no-op". The surgery is implemented (:5031, :5210); a compile with --shadow-stack-size 4096exits 0 and shrinks .bss to 4240 B.
CLAUDE.md:150 — WasmCert pinned at "phases 1+2, 19 ops, 49 Qed"; actual 104 Qed, phase 3, 41 ops. The file that instructs every agent working here.
README.md:284 — names 6 i64 ops as an "op-level-only residual"; coq/STATUS.md and the .v file both record it CLOSED.
coq/STATUS.md — CI-gated headline 591 Qed, hand-maintained table totals 585, two rows understate their files (ArmSemantics 8 vs 14, VcrSelRules 42 vs 52), five .v files missing from the table entirely.
VcrSelRules.v:16 — "the 40 Qed themselves are the divergence gate"; there are 50.
FEATURE_MATRIX.md:72 — WasmCert bridge described as i32-only while carrying the i32+i64 count.
docs/status/PROJECT_STATUS.md — ~10 releases stale and pinned by nothing: 291 Qed (actual 591), 16 crates (18, omitting both RISC-V and AArch64 backends), "no bulk memory" while GI-MEM-002 is verified, "no multi-memory" while SYNTH-MATRIX-MULTI-MEMORY is pinned and green, Z3 named as the default validator (ordeal since v0.27). Its own Last updated date is wrong.
docs/status/POC_ACHIEVEMENTS.md:26 — "R0-R12" allocatable; the Verus spec allows R0-R8 (contracts.rs:45).
gale-integration.yaml:584 — cites map_operator; git log -S shows the symbol never existed (it is convert_operator).
The mechanism, and why no gate sees it
Most of Tier 3 is one thing: a machine-derived number moves while the hand-written sentence beside it does not.claim_check.py pins ~43 claims and re-derives counts; everything else drifts freely. It checks prose against prose — it passed 43/43 over a European-decimal-separator typo I introduced hours earlier.
Nothing in the repo checks prose against behaviour. That is the seam all 30 findings live in.
Both scanners recorded what they checked and found consistent, so this is not re-scanned: 57 CI job names, 6 Bazel targets, 20 SYNTH_* env vars incl. every default-on claim, 132 implemented/verified artifact blocks' file references, 100 Rust symbols, the frozen anchors, ORACLE_WIRING's ratchet floors (deliberately floors, not current counts — not a disagreement), and wcet.rs:119's *"EXHAUSTIVE with NO wildcard"` which genuinely holds.
Nothing here is edited yet. Tier 1 needs investigation first — deciding which side is right is the work, and doing it wrong in either direction is how a real defect gets papered over.
Two scanners over disjoint surfaces, both instructed to report the disagreement and never rule on which side is right. That framing was load-bearing: of this release's three known instances the resolution differed each time (doc wrong / code wrong / fixture wrong), so a scanner assuming "code wins" would have mis-filed two of three.
Examined ~930 claims mechanically. 30 genuine disagreements (~500 Rust doc comments → 20; ~430 prose claims → 10). Two already fixed in #945. This issue carries the rest.
TIER 1 — possible DEFECTS, need investigation before anything is edited
These are not assumed to be code bugs. Each is a claim the code appears to contradict in a way that would matter if the doc is the intended contract.
A. VFP encoder precondition holds for only ONE of the two selectors
arm_encoder.rs:7286states "The SELECTOR guaranteesdmis a dead temp, never a pinned param/local home (it copies a home into a fresh D-temp first)."select_with_stackgenuinely implements that copy (instruction_selector.rs:4505).select_defaultdoes not — it allocates from a bare round-robin counter with no home/liveness test (:7784,alloc_vfp_dreg:6020), andgrep -c vfp_homeover its body returns 0.--relocatableforcesselect_default(per #197). If the encoder's overlapping VCVT write can land on a live f64 home there, this is a miscompile, not a doc bug.B. "never S0" — index 0 is admitted
arm_encoder.rs:7280/:7221: the 32-bit result stages through an S-alias "never S0, which may hold an unrelated live value (the #615 class)". The only guard isif dm_num > 7;encode_sreg(0)is S0,D0is reachable (next_vfp_dreg: 0), and the encoder's own test recordsvmov s0, r3. Same #615 class the comment invokes.C.
--volatile-segmentmoves bytes while documented as inertmain.rs:471: "PHASE 1 = plumbing only … the emitted bytes are unchanged whether or not the flag is passed."Measured by the scanner on
scripts/repro/volatile_segment_543.wat, noSYNTH_*set: 196 B without the flag, 234 B with--volatile-segment 0x100:16. The Phase-2 back-off is already wired (arm_backend.rs:1432const-CSE skipped when the list is non-empty;optimizer_bridge.rs:3213base-CSE). The frozen-byte gates never exercise the flag, so nothing catches this.D. WCET loop SP-tracking claims exhaustiveness over a wildcard
wcet_loops.rs:1036: "Exhaustive over everyrd-carrying op the priced instruction set can contain." The match ends_ => false(:1089), and eight pricedrd-carrying ops have no arm (I64SetCond,I64SetCondZ,I64Clz,I64Ctz,I64Popcnt,Str,Strb,Strh).Those eight do not write SP, so the WCET bound is not currently wrong — but the stated tripwire does not exist: a new
ArmOpthat does write SP compiles fine and silently readsfalse. The parenthetical also saysI64Popcntis a whole-functionLoopedExpansiondecline;op_costprices it.TIER 2 — stale rationale on a live exclusion (safe today, wrong reason)
wcet.rs:77PUSH {R0-R3}(0xB40F, 4 regs) is in the pricedI64Rotl/I64Rotrarms.CEIL_PER_HALFWORD = 5then holds exactly (1+4), not with margin — and it is aclaims.yaml-pinned soundness constantvalidator_pattern.rs:94__aeabi_ldivmod)"arm_encoder.rs:5646, 64-round loop).grep -rn '__aeabi' crates/finds only comments. Four ops are excluded from translation validation on a rationale that does not describe the loweringstatic_data_addr.rs:83aarch64/selector.rs:2186+lowersI32Load/Store,I64Load/Store, the 8/16-bit variants. Premise false since #851alloc_validator.rs:56op_dest == Noneop is control-flow / system / label / call"Sb/Sh/SwareNoneand none of those four. Conclusion survives (stores write no register) but on a different argument than the one writtenselector.rs:1498(RV)alloc_tempmust never hand any of them out"self.temps[0]— by construction one of them (:1556). Safe only via thealloc_exhausteddecline the doc omitsTIER 3 — docs that UNDERSTATE what ships
Every one makes synth look less capable than it is. That is why none was noticed.
aarch64/selector.rs:35listscall_indirect, float rounding, f32/f64 load/store, i64→float converts as "deliberately still declined (loud-skip)" — all have real lowerings in the same file's shipped match. Same class inriscv/selector.rs:10(br_table, cross-function calls listed out-of-scope, both lowered).instruction_selector.rs:5037— "the 40 Rocq-proved rules"; the shipped table has 50, including nine i64 ops the enumeration omits.instruction_selector.rs:2285— "phase 1 has no VFP spilling, so an f32 expression deeper than the register file loud-skips its function". GI-FPU-002 + RA tail is now the ONLY gate between the falcon cascade and the M7 — 5 entry-point symbols; phase-2 D-register pressure is new in v0.52 (inline-f64 #869 lowering) #881 shipped spilling andarm_backend.rs:686retries with no env gate. The stale wording is also baked into a user-visible error string (:2296).aarch64/selector.rs:272—select_typed_cfsaysloop/if/br_tableare "declined by name"; all three are lowered (:1679,:1779,:1802). The same wrapper hardcodesMemBounds::Unchecked(:301) whileMemBounds' own doc says "there is no silent default".main.rs:4538—--shadow-stack-size: "aSomehere is an honest Err, never a silent no-op". The surgery is implemented (:5031,:5210); a compile with--shadow-stack-size 4096exits 0 and shrinks.bssto 4240 B.CLAUDE.md:150— WasmCert pinned at "phases 1+2, 19 ops, 49 Qed"; actual 104 Qed, phase 3, 41 ops. The file that instructs every agent working here.README.md:284— names 6 i64 ops as an "op-level-only residual";coq/STATUS.mdand the.vfile both record it CLOSED.coq/STATUS.md— CI-gated headline 591 Qed, hand-maintained table totals 585, two rows understate their files (ArmSemantics8 vs 14,VcrSelRules42 vs 52), five.vfiles missing from the table entirely.VcrSelRules.v:16— "the 40 Qed themselves are the divergence gate"; there are 50.CorrectnessI32.v:4— "29 Qed"; 31 (29 theorems + 2 helper lemmas).FEATURE_MATRIX.md:72— WasmCert bridge described as i32-only while carrying the i32+i64 count.docs/status/PROJECT_STATUS.md— ~10 releases stale and pinned by nothing: 291 Qed (actual 591), 16 crates (18, omitting both RISC-V and AArch64 backends), "no bulk memory" whileGI-MEM-002isverified, "no multi-memory" whileSYNTH-MATRIX-MULTI-MEMORYis pinned and green, Z3 named as the default validator (ordeal since v0.27). Its ownLast updateddate is wrong.docs/status/POC_ACHIEVEMENTS.md:26— "R0-R12" allocatable; the Verus spec allows R0-R8 (contracts.rs:45).gale-integration.yaml:584— citesmap_operator;git log -Sshows the symbol never existed (it isconvert_operator).The mechanism, and why no gate sees it
Most of Tier 3 is one thing: a machine-derived number moves while the hand-written sentence beside it does not.
claim_check.pypins ~43 claims and re-derives counts; everything else drifts freely. It checks prose against prose — it passed 43/43 over a European-decimal-separator typo I introduced hours earlier.Nothing in the repo checks prose against behaviour. That is the seam all 30 findings live in.
Both scanners recorded what they checked and found consistent, so this is not re-scanned: 57 CI job names, 6 Bazel targets, 20
SYNTH_*env vars incl. every default-on claim, 132implemented/verifiedartifact blocks' file references, 100 Rust symbols, the frozen anchors,ORACLE_WIRING's ratchet floors (deliberately floors, not current counts — not a disagreement), andwcet.rs:119's *"EXHAUSTIVE with NO wildcard"` which genuinely holds.Nothing here is edited yet. Tier 1 needs investigation first — deciding which side is right is the work, and doing it wrong in either direction is how a real defect gets papered over.