feat(aarch64): complete the scalar float surface — rounding, FP memory, i64 converts, GUARDED i64 truncations (#851) - #898
Conversation
Coordinator corrections applied (L1 / #890) — and one of them was a real defect here1. The gate I added could pass while its own oracle failed. Not hypothetical — reproduced against a stub that prints
Without 2. Also re-ran the unicorn-only path (what Adjacent finding, deliberately NOT fixed here (lane containment)Auditing every
They are materially safer than mine was — each one's last command is a Leaving them to L1/#890 rather than touching another lane's steps in a file we both edit. Flagging so the decision is explicit, not an oversight. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
) v0.54 L2 step 1 — the four encoder families the VCR-SEL-005 third-backend parity oracle names in its ROUNDING / FP_MEM / I64_TO_FP decline reasons: * `FRINT{P,M,Z,N}` s/d — round-to-integral with the mode pinned in the OPCODE (not FPCR.RMode), so the lowering cannot depend on ambient embedder state. FRINTN is round-to-nearest-TIES-TO-EVEN, which is what WASM §4.3.3 `fnearest` requires (FRINTA — ties-away — would be wrong). * `LDR/STR (SIMD&FP, unsigned offset)` s/d — the linear-memory FP access forms, same size/opc/scaled-imm12 shape as the GP family with the V-register bit set. * `SCVTF`/`UCVTF` x-source forms — the i64→float converts, exactly the already-shipped w-forms with `sf` set (mirroring the sdiv/sdiv64 pair). Every encoding is pinned to `clang -target aarch64-linux-gnu` ground truth in three new tests (assemble the mnemonic, objdump, read the 32-bit word), plus the two structural relations (d-form = s-form | ftype bit; x-form = w-form | sf) so a future edit of one half cannot silently drift from the other. Encoder only — no selector arm consumes these yet, so output bytes are unchanged for every currently-compiling module. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
v0.54 L2 step 2 — two of the four classes the VCR-SEL-005 third-backend
oracle enumerated:
* ROUNDING: `f{32,64}.{ceil,floor,trunc,nearest}` each lower to ONE
mode-pinned `FRINT{P,M,Z,N}`. TOTAL ops — no domain guard, and a guard
would be a defect (it would trap where WASM returns a value).
* I64_TO_FP: `f{32,64}.convert_i64_{s,u}` lower to the `x`-form
SCVTF/UCVTF. Also total.
Parity-gate entries flipped Err→Ok in the SAME commit (8 rounding + 4
converts) and the now-unused ROUNDING / I64_TO_FP reason constants DELETED —
a gap claim must not outlive the gap. No wildcard arm, nothing moved to
StructurallyExcluded. The selector's own stale claim
(`rounding_and_i64_float_converts_are_loud_declined`) is narrowed to the one
class still open (`trapping_i64_truncations_are_loud_declined`).
DECODER (the end-to-end half): `f32.{ceil,floor,trunc,nearest}` were dropped
at `_ => None`, so a real module using them loud-SKIPPED the whole function
before the selector ever saw it. Un-dropped, exactly as #538 m4 did for
f32.min/max — and with the same consequence for ARM32.
ARM32 now LOUD-DECLINES the four (previously unreachable, so this exposes no
regression): its `ArmOp::F32{Ceil,Floor,Trunc,Nearest}` pseudo-op is an
FPSCR-RMode + `VCVT.S32.F32` + `VCVT.F32.S32` ROUND-TRIP THROUGH i32, and
VCVT SATURATES — `ceil(1e30)` would give 2147483648.0, `ceil(±inf)` a finite
bound, `ceil(NaN)` 0.0, where WASM §4.3.3 returns 1e30 / ±inf / NaN. That is
the #709 more-total-than-WASM class; declining keeps it latent instead of
shipping it. The `f32_operations_test` cases that pinned the wrong lowering
as Ok are replaced by the decline assertion.
The #554 honesty fixture used `f64.floor` as its "deliberately declined float
op" — a claim this commit invalidates. Repointed at a value-carrying
(f32-result) `block`: still fully DECODED, still declined by the aarch64
SELECTOR, and the assertion is STRENGTHENED (stderr must come from the
selector AND name the reason, not merely contain "unsupported").
EXECUTION-VERIFIED on this arm64 host, MAP_JIT native call vs wasmtime,
bit-exact over 62 checks: the ties-to-even halfway table (0.5→0, 1.5→2,
2.5→2, 3.5→4 and negatives) CONFIRMS FRINTN is roundTiesToEven (FRINTA would
fail 0.5 and 2.5), plus ±0/±inf/NaN/1e30 passthrough and the i64 convert
rounding ties (2^53±1, 2^62, ±2^63, 2^64−1).
Gates: cargo test --workspace exit 0 (130 suites), clippy -D warnings exit 0,
fmt --check clean, frozen anchors 10/10 (no ARM golden moved — the four
rounding ops were undecodable, so no frozen fixture contains one).
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…oad/store (#851) v0.54 L2 step 3 — the third of the four classes the VCR-SEL-005 third-backend oracle enumerated. `f{32,64}.{load,store}` lower to the SIMD&FP unsigned-offset `ldr/str s|d` against the `x28` linear-memory base. BOUNDS, NOT AN AFTERTHOUGHT: the new `fload`/`fstore` closures reuse the SAME `form_ea` the integer accesses use, so an FP access inherits v0.52's #865 software bounds check verbatim — `uxtw(addr) + offset + size <= limit` proven (or `brk`) BEFORE the dereference, with the width folded into the compile-time constant K. That width matters: f64 gets K = limit − 8, so address 65532 is IN bounds for `f32.load` and OUT for `f64.load` on a one-page memory, and the oracle exercises exactly that split. Parity-gate entries flipped Err→Ok in the SAME commit (f32.load, f32.store, f64.load, f64.store) and the FP_MEM reason constant DELETED. No wildcard arm. Four selector tests pin the shape: the full bounds-checked f32.load sequence, the f64 width-aware K, scaled-imm12 offset folding (offset/4 for s, offset/8 for d), and the type-confusion guard (an i32 fed to `f32.store` ERRORS rather than storing the wrong register file). EXECUTION-VERIFIED vs wasmtime under unicorn (x28 = linear-memory base): 128/128 bit-exact over the {f32,f64} × {0, 4, 8, 100, 65532, 65535, 65536, 0xFFFFFFFF} × {1.5, ±0.0, ±inf, NaN, 3.14159, 1e30} store→load round-trip matrix — 56 of them OOB cases where synth traps exactly where wasmtime traps, and the in-bounds cases bit-exact including NaN payload and the sign of −0.0. Gates: aarch64 lib 111/111, parity oracle 8/8, clippy -D warnings exit 0, fmt clean, frozen anchors 10/10 (no ARM golden touched). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…ations (#851) v0.54 L2 step 4 — the soundness-critical class, and the last of the four the VCR-SEL-005 third-backend oracle enumerated. `i64.trunc_f{32,64}_{s,u}` now lower to the `x`-form FCVTZ{S,U} behind the #709 domain guard. WHY A GUARD AND NOT A BARE CONVERT: A64 FCVTZS/FCVTZU are MORE TOTAL than WASM. On NaN they return 0; out of range they SATURATE to INT64_MIN/MAX (or 0/UINT64_MAX). WASM §4.3.3 requires a TRAP for every one of those inputs. Emitting the bare instruction is not an approximation, it is a silent miscompile — the #633/#666/#709/#665/#642 class this project has shipped before. `trunc_guarded` gains a `dst64` parameter so the i64 forms reuse the proven shape verbatim (ordered `b.mi` hi check — FALSE for NaN, so NaN falls into the first `brk` — then the lo check, then the convert). THE BOUNDARY CONSTANTS, each justified rather than copied: * f32->s64 / f64->s64 lo = -2^63, INCLUSIVE (`b.ge`). -2^63 is exactly representable in both formats and truncates to a LEGAL INT64_MIN; a strict bound would trap it. * f64->s64 differs from the i32/f64 row, which needs the STRICT -(2^31)-1 because -2147483648.5 exists. At 2^63 the f64 ULP is 2048, so NO f64 lies in (-2^63-1, -2^63) — the inclusive bound is both exact and necessary. * u64 lo = -1.0, STRICT (`b.gt`): trunc_u(-0.5) = 0 is legal. * hi = 2^63 / 2^64, exclusive — NOT the 2^32 the i32 forms use, which would trap every legal value above 4294967295. Parity-gate entries flipped Err->Ok in the SAME commit (4) and the TRAP_TRUNC_I64 reason constant DELETED. The extended-surface gate now carries only SIMD / multi-memory / call_indirect declines: the SCALAR FLOAT SURFACE IS COMPLETE. The selector's own decline claim (`trapping_i64_truncations_are_loud_declined`) is replaced by three tests that pin the guard instead: two `brk`s per form with the convert strictly AFTER both, the inclusive-vs-strict signed lower bound (both formats), and the 2^64-not-2^32 unsigned upper bound. Execution evidence + CI wiring land in the next commit. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…n surface (#851) v0.54 L2 step 5 — the execution evidence for all four closed classes, wired into CI in the SAME commit (#890: v0.53 found three oracles that existed and ran nowhere). `aarch64_float_completion_851_differential.py` — 662 checks, 142 of them TRAP cases, bit-exact against wasmtime under TWO independent oracles: unicorn (UC_ARCH_ARM64, with x28 = linear-memory base) and, on an arm64 host, a real native call per case in a FORKED CHILD so an expected `brk #0` SIGTRAP is observed by the parent and a surprise one is survivable. The soundness-critical half is a full BOUNDARY TABLE, not spot checks. For each trapping i64 truncation: ±2^63 and 2^64 exactly, the nearest representable float strictly INSIDE each bound and the nearest strictly OUTSIDE it (stepped on the true f32 grid via an IEEE total-order key — plain `math.nextafter` walks the far finer f64 grid), ±0, ±inf and NaN. The static expect-trap column is itself validated against wasmtime first, so the table cannot drift vacuous. Rounding is compared BIT-EXACTLY over a halfway table (0.5, 1.5, 2.5, 3.5 and negatives, up to the largest representable halfway value in each format). That is what makes "FRINTN is ties-to-EVEN" a CHECK rather than a claim: a ties-away FRINTA lowering returns 1 and 3 for 0.5 and 2.5 and fails here. ±inf / NaN / 1e30 catch the other classic wrong lowering, a round-trip through an integer. The i64->float converts pin round-to-nearest-even at the 2^24 / 2^53 onsets; the FP-memory cases cover the width-aware bounds split (65532 in-bounds for f32, out for f64) with NaN payload and -0.0 sign preserved through the round-trip. PROVEN NON-VACUOUS BY MUTATION, both directions of wrong: * replace one guarded convert with a bare saturating one -> 18 failures (`A64=0x7fffffffffffffff wasmtime=TRAP`) — the silent miscompile; * make the signed lower bound off-by-one strict -> 4 failures (`A64=TRAP wasmtime=0x8000000000000000`) — the over-trap. The harness also carries its own floors (>=300 checks, >=40 trap cases, every class non-empty) and hard-fails if the compile SKIPS any function. CI: `set -o pipefail`, `tee`, and a grep + numeric assertion on the check and trap counts, so an oracle that silently stops exercising anything goes red instead of green. The decline-matrix honesty oracle is repointed: rounding, f32/f64 load/store, i64->float converts and the trapping i64 truncations are no longer declines, so keeping them there would be a stale claim. It now pins the STRUCTURAL declines end-to-end — call_indirect, br_table, writing a param local, globals, memory.fill, a value-carrying block, SIMD — 7/7 loud. (The param-write case needed a body that also READS the param: a write-only local 0 is indistinguishable from a fresh non-param local and lowers correctly as one.) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…loat completion (#851) v0.54 L2 step 6 — close the claims. gale's acceptance matrix (`scripts/repro/aarch64_matrix.sh`) exercises the 16 newly-lowering ops: f32/f64 rounding and the i64->float converts through reinterpret wrappers so the compare is BIT-EXACT (±0 sign and NaN passthrough included), and the four trapping i64 truncations through a new `trap64` helper that normalizes both sides (the JIT runner's signal handler prints TRAP; wasmtime exits non-zero with empty stdout) so a trap DISAGREEMENT is reported as a miscompile instead of vanishing into an unnoticed empty string. 45 -> 61 ops accepted, 119 -> 355 native checks, DECLINED FRONTIER NOW EMPTY. Red-first: the same bare-saturating-convert mutation makes the matrix exit 1 with 7 named `s=<saturated>,w=TRAP` miscompiles, so the new trap helper is not vacuous shell. f32/f64 LOAD/STORE are deliberately NOT in the matrix and the script says why: they need `x28` = linear-memory base on entry, which the bare JIT runner cannot establish. They are execution-verified under unicorn in the differential. `scripts/templates/feature_matrix.md.tmpl` (the TEMPLATE — the generated docs/status/FEATURE_MATRIX.md is regenerated by the coordinator, #805): the aarch64 row's Declines list no longer names rounding, f32/f64 load/store, i64->float converts or trapping i64-target truncations — those four claims are now false. The capability text states the scalar float surface is COMPLETE and names the evidence; the memory clause records that f32/f64 accesses are bounds-checked by the same path as the integer ones. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…851) Mechanical `python3 scripts/claim_check.py claims.yaml --emit-status` output — the claim-check gate goes red otherwise (the generated files must match their template + re-derived counts at every commit). aarch64_selector_ops: 161 -> 181 (the 20 newly-lowered WasmOp variants: 8 rounding + 4 i64->float converts + 4 trapping i64 truncations + 4 FP load/store) COORDINATOR (#805): re-run `--emit-status` ONCE after fan-in — this file will conflict with any other lane that also moves a counted claim. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…#890) Coordinator correction from lane L1, and it was a REAL defect in the step I shipped two commits ago — not a hypothetical. `set -o pipefail` ALONE does not fail the step. Without `-e`, the step's exit status is that of the LAST command, so the failing pipeline's non-zero was discarded and the trailing `[ ... ] || exit 1` count checks (which pass on a FAILING run, because a failing oracle still prints its full case counts) decided the verdict. Reproduced locally against a stub that prints `RESULT: FAIL (1)` and exits 1: old shape (bare pipefail) -> exit 0 GREEN while the oracle FAILED new shape (set -euo pipefail) -> exit 1 correctly RED Fixed by making the shell flags explicit (`set -euo pipefail`, not relying on the runner's defaults) AND by deriving the verdict from the script's OWN summary line — `grep -q '^RESULT: PASS'` — instead of from exit 0 alone. The non-collapsed count assertions stay as the anti-vacuity floor and are now bare tests, so `-e` aborts on them too. Re-verified green against the real oracle (662 checks, 142 trap cases). Also adds the `# ci-status: wired` header L1's new oracle-wiring gate requires on `scripts/repro/*.py` (docstring left intact — it is still the module docstring with a comment above it). Unrelated polish caught while verifying the ARM32 decline on all three target paths (m4f, m4f --relocatable, m7dp --relocatable — all loud, RV32 too): a line-continuation in the new diagnostic split "WASM-correct" as "WASM- correct" in the user-facing message. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
193958c to
52a8d87
Compare
…float # Conflicts: # CHANGELOG.md
3650122 to
8a7b78c
Compare
`synth-backend-aarch64` has not been an integer subset since v0.54. The row now names what actually ships and points at the generated feature matrix for the exact surface rather than restating it (a second copy of that list is how v0.54's cold review found a doc-honesty defect): * the complete scalar f32/f64 surface (v0.54 #898 — rounding, FP memory, i64 converts, guarded i64 truncations); * bounds-checked linear memory (default `--safety-bounds software`, #865); * WASM globals and `call_indirect` with all three §4.4.8 trap guards (v0.54 #899); * direct calls and full control flow. The row is the LAST place in README that described the backend by what it could not do; the intro paragraph and the feature matrix were already current. Note for whoever picks this up next: CLAUDE.md carries a byte-identical stale copy of this row. It is deliberately NOT touched here — that file is agent configuration and is not mine to edit on a lane brief. claim_check 37/37 (the aarch64 rows in the generated matrix are template- driven and unaffected — no generated doc was hand-edited). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… de-staled, VCR-VER-004 filed (#893) (#913) * fix(rivet): VCR-DEC-003 traces-to an ARTIFACT, not a GitHub issue number `traces-to: synth:396` was the one genuinely-ours rivet broken-link error: `synth:396` reads as "artifact 396 in repo synth", and no such artifact exists — an issue number used where an artifact id belongs. The traceability intent is preserved rather than deleted: synth#396's own body says "Tracked in rivet as VCR-COV-001, sibling to VCR-DBG-001", and VCR-COV-001's title carries "(synth #396)". So the link retargets to VCR-COV-001 (in-repo `traces-to` targets are already idiomatic in this file — VCR-SEL-001, VCR-RA-001, VCR-MEM-001, …), and `synth-396` joins the tags so the issue number stays discoverable as a reference instead of a resolvable target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * fix(rivet): GI-FPU-002 was declared TWICE — the `proposed` copy silently won The second genuinely-ours rivet error, which the lane brief did not know about because the local grep for it (`^ ERROR:`) misses filename-prefixed diagnostics: gale-integration.yaml: ERROR: [GI-FPU-002] artifact id 'GI-FPU-002' is declared more than once: ./artifacts/verified-codegen-roadmap.yaml and ./artifacts/gale-integration.yaml — the second definition silently overwrites the first This is the #893 class one level worse than a stale description: the requirement was declared in gale-integration.yaml (`status: proposed`, the original #369 ask) and AGAIN in verified-codegen-roadmap.yaml (`status: implemented`, the phase-1 delivery record added by PR #705). rivet loaded the `proposed` copy over the `implemented` one, so the traceability graph reported GI-FPU-002 as NOT STARTED while README/CHANGELOG report #369 CLOSED, f32 complete v0.41, f64 complete v0.43, and VFP register-file spilling shipped v0.53. Resolved by MERGING, not deleting — the two copies carried disjoint edges and disjoint evidence: * Survivor: gale-integration.yaml. That is the id's namespace home (GI-002 -> GI-FPU-001 -> GI-FPU-002 -> GI-FPU-VER-001 are one chain in that file; GI-FPU-002 was the ONLY GI-* artifact in the roadmap). It also already carried `derives-from GI-002`, `traces-to gale:369`, and the jess REQ-PIX-001 / AFD-024 Pixhawk linkage — all of which a straight delete of that side would have dropped. README names the roadmap the single source of truth for the VCR-* program's roadmap status, which GI-* is not. * Folded in: the roadmap copy's six-point phase-1 DELIVERED list and its full verification-criteria (the f32_vfp_619_differential RED->GREEN evidence, the m3 honest-reject direction, the f32_hardfloat_619.rs unit lock, and the recorded unicorn VMRS FPSCR->APSR emulator gap). * De-staled, since the merge had to pick one status anyway: `proposed` -> `implemented`, with the post-phase-1 evidence the roadmap copy predated — f64 complete v0.43 (#369 closed), v0.52 #869 inline i64<->float, v0.53 #881 VFP spilling (109 rows bit-identical to wasmtime) — and the two residuals stated as loud declines rather than implied away (`f32.{ceil,floor,trunc,nearest}` pending a real VRINT.F32 after v0.54 removed the unsound saturating-VCVT pseudo-op, and `i64.trunc_sat_f32_*` on single-precision FPUs). * Where the duplicate was, the roadmap now carries a pointer comment explaining why the id is not defined there. rivet: 52 -> 50 errors; NON-EXTERNAL errors 2 -> 0. Warning/info diagnostic sets are byte-identical before/after (no new class introduced). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * fix(#893): VCR-RA-004 was `proposed` for a resolver that shipped in v0.11.38 Half of #893: v0.53's VFP-spilling lane tagged its work `VCR-RA-004`, while artifacts/verified-codegen-roadmap.yaml still carried that id as `status: proposed`. The brief offered two resolutions — mint a new id for the v0.53 work, or flip VCR-RA-004 to implemented. The evidence decides it, and it is neither of the two things the ID-collision framing suggested: the resolver VCR-RA-004 describes shipped in **v0.11.38**, three years of releases before the lane that got blamed for overloading the id. `synth_synthesis::parallel_move` is verbatim what the artifact asks for — a pure, testable component that sequentializes a parallel move set with cycle detection, scratch selection from dead registers, and a guaranteed-progress fallback. The artifact's own tags already said `release-v0.11.38`; the CHANGELOG names it twice (v0.11.38 "Cycle-safe parallel-move resolver (`synth_synthesis::parallel_move`, VCR-RA-004)" and v0.11.39 "#327 — VCR-RA-004's resolver (v0.11.38) breaks cycles via a stack-scratch cell"). Only the status field was never flipped. Minting a second id would have created the collision the issue was trying to remove. So: `proposed` -> `implemented`, with the evidence written down instead of left in changelog prose — * the algorithm and its progress discipline (the size bound and the strictly-shrinking pending set are `assert!`s in the resolver, so an unbounded path aborts rather than emitting); * both consumers, each of which removed a real defect rather than only adding a component: v0.11.39 #327 arg-move marshalling (the old cycle-breaker demanded a callee-saved register AND miscompiled genuine 2-swaps by duplicating a value), and v0.53 #881 VFP register-file spilling (the falcon `S0..S15 all live` wall) — which is precisely the work the v0.53 notes tagged VCR-RA-004; * SWVER-022, a new sw-verification artifact linking `verifies` -> VCR-RA-004, so the right side of the V is closed by a typed link rather than by a paragraph. It records the run recipe and what each of the three criteria clauses is actually met by. `implemented`, NOT `verified`, deliberately. The property test the criteria demand does exist and does exactly what they specify — 2000 iterations over R0..R8 alternating full random permutations with partial move sets, each re-checked at scratch-set sizes 0/1/2 (6000 sequentializations) against a reference parallel semantics, plus 12 directed shape tests — verified locally, `cargo test -p synth-synthesis parallel_move` 13/13, real exit 0. But the second pitfall the artifact names, split points landing inside hot loops, is still bounded by ASSUMPTION (synth's straight-line segment scope) rather than by a check that fails when segments widen. That residual is now stated in both the requirement and SWVER-022 rather than implied away. rivet: non-external errors still 0; warnings 104 -> 103 (VCR-RA-004's "should be verified by at least one verification measure" WARN closed, no new warning introduced). claim_check 37/37. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * fix(#893): VCR-SEL-005 spans THREE backends, and both ledger counts had drifted Second half of #893. VCR-SEL-005's description still said the gate lowers probes on "BOTH the ARM (Thumb-2) and RISC-V (RV32IMAC) selectors" — it has covered a third, aarch64, since v0.53 (#883). SWVER-017, the verification artifact that is supposed to be the right-side evidence for exactly this requirement, carried the same claim in its title ("ARM vs RISC-V") and described a two-selector ledger. While correcting the backend count I checked the numbers the same documents assert, and both were stale in the same direction — they described gaps that have since CLOSED, which is the flattering direction and therefore the one worth checking: * The roadmap said "the KNOWN_DIVERGENCES ledger is now 5 Zbb + 16 new = 21 entries". The array holds 18: `memory.size`/`memory.grow` closed in v0.50 and `br_table` in v0.53 (#882). * `known_divergences`'s own doc comment said 19 (it had accounted for v0.50 but not #882). * `aarch64_known_divergences`'s doc comment said "leaving the SEVEN below" over an array of 5 — v0.54 (#899) closed `global.get`/`global.set` and removed the entries without updating the prose above them. All four now state what the arrays hold, with the counts' derivation written out so the next drift is visible, and a note at each site that the count must move with the array. The stale-entry check already forces a CLOSED gap to retire its ledger line; nothing forced the PROSE ABOUT the ledger to move with it, which is the #893 defect one layer up. Also recorded, because it is the part of the third-backend leg that is not just "one more backend": aarch64 gets a probed FLOAT/SIMD surface (`a64_extended_surface`, floor `probed >= 100`) that ARM and RV32 structurally cannot have — float is `StructurallyExcluded` from their leg because ARM float lowering is TARGET-parameterized (f32.add declines at fpu=None, lowers at Single/Double) and RV32 has no FPU, whereas the aarch64 backend has one fixed host profile, so both directions are assertable and a stale gap-claim is caught the same way a stale divergence is. Changes are prose and doc-comment only — no test logic touched. `cargo test -p synth-backend-riscv --test cross_backend_op_parity` 8/8, real exit 0. rivet non-external errors still 0; claim_check 37/37. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * docs(roadmap): file VCR-VER-004 — a shipped North-Star component had NO roadmap entry README calls artifacts/verified-codegen-roadmap.yaml "the single source of truth for roadmap status". VCR-VER-004 shipped in v0.54 and appeared in the CHANGELOG, the FEATURE_MATRIX template and the CI job list — but the roadmap had no entry for it at all, so the one document the README points at for "what is the state of the VCR-* program" was missing the release's headline validator. The entry records what it is and, more importantly, why it exists: v0.53 showed by mutation that emptying `cfg_exit_observable` makes the compiler leave a return value in the WRONG REGISTER and that BOTH per-compilation validators accept it (`validate_cfg_rewrite` -> Ok, VCR-RA-003 -> Consistent). Only execution caught it. `abi_contract::validate_abi_contract` is not a third file on the same axis — it differs on four axes (an obligation that cannot be emptied because it is `RETURN_CONTRACT_REGS = [R0, R1]` hard-named in its own source; forward rather than backward, so there is no seed whose empty set is a vacuous fixpoint; evidence that is a VALUE compared by greatest-fixpoint bisimulation rather than a name-pair; and a `(orig, rewritten)` signature that takes nothing from the pass). Its honest limit is in the entry, not implied away — all three residuals: (a) it GATES only the flag-off colouring allocator; on the default path it is a report-only audit held to a `Violated 0` CI floor, because gating a user's compile on a checker whose false-positive rate is measured rather than proven is a flip we have deliberately not taken; (b) memory is NOT in its obligation (complementary to `validate_cfg_rewrite`, not redundant with it); (c) THE OP MODEL IS STILL SHARED — def/use extraction runs through `liveness::reg_effect`, so a mismodeled op is a blind spot common to all three instruments. VCR-VER-004 closes the shared-CONTRACT hole, not the shared-OP-MODEL hole, and until `synth-verify`'s `ArmSemantics::encode_op` is pinned against it (VCR-ISA-001's Sail-derived semantics being the eventual anchor, now a typed `traces-to` link rather than a prose aside) "three independent validators" WOULD BE AN OVERCLAIM. Shaped to match its two siblings VCR-VER-003 / VCR-VER-761 exactly: `sys-verification`, `verifies -> VCR-001`, `method: translation-validation`, `preconditions`/`steps`/`pass-criteria`. That inherits two diagnostics those siblings already carry (the schema's `method` allowed-values does not list `translation-validation`, and `pass-criteria` is not a declared sys-verification field) — kept deliberately, because the fix for those is a rivet schema decision about the whole family, not a divergent shape for one member. rivet: 50 errors, non-external 0 (unchanged). Warnings 103 -> 105; the delta is exactly the three new-artifact diagnostics above, and the diagnostic-class diff against the lane's baseline shows no new KIND. claim_check 37/37. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * docs(README): the aarch64 crate row still said "integer subset" `synth-backend-aarch64` has not been an integer subset since v0.54. The row now names what actually ships and points at the generated feature matrix for the exact surface rather than restating it (a second copy of that list is how v0.54's cold review found a doc-honesty defect): * the complete scalar f32/f64 surface (v0.54 #898 — rounding, FP memory, i64 converts, guarded i64 truncations); * bounds-checked linear memory (default `--safety-bounds software`, #865); * WASM globals and `call_indirect` with all three §4.4.8 trap guards (v0.54 #899); * direct calls and full control flow. The row is the LAST place in README that described the backend by what it could not do; the intro paragraph and the feature matrix were already current. Note for whoever picks this up next: CLAUDE.md carries a byte-identical stale copy of this row. It is deliberately NOT touched here — that file is agent configuration and is not mine to edit on a lane brief. claim_check 37/37 (the aarch64 rows in the generated matrix are template- driven and unaffected — no generated doc was hand-edited). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * ci(#893): the Rivet Validation gate could not see either error this lane fixed Both defects this lane repaired were sitting in a tree with `Rivet Validation` green. That is not a coincidence — the job's filter had two holes, and each one swallowed exactly one of them. (1) `grep "^ ERROR:"` anchored on two-space-indented lines. rivet prefixes SOME diagnostics with the source file instead (`gale-integration.yaml: ERROR: …`), so that entire class was invisible to the gate — including "artifact id X is declared more than once … the second definition silently overwrites the first". That is how GI-FPU-002 could be `implemented` in one file and `proposed` in another, with rivet resolving it to `proposed`, and nothing complained. (2) The cross-repo exemption `targets '.*:.*' which does not exist` exempted any target containing a COLON. `synth:396` contains a colon. So a broken link in our OWN graph — an issue number written where an artifact id belongs — was classified as an unresolvable link into an un-rivet'd sibling repo and waved through. Fixed structurally rather than by allowlist: every line containing `ERROR:` is now considered, and the exemption is "a target carrying a FOREIGN prefix", with our own `synth:` prefix explicitly added back to the ours-count. A new sibling repo therefore needs no edit here, and our own prefix can never slip back into the exempt set. Failures now PRINT the offending lines instead of only a count, so the next one is self-diagnosing. RED-FIRST, end to end, by replaying this exact step (extracted from the YAML, not paraphrased) against both trees: * pre-fix artifacts (65417c0): exit 1, "Found 2 rivet validation errors that are OURS (not cross-repo)", both named; * fixed artifacts (this branch): exit 0, with the expected cross-repo warning still emitted. CAVEAT, stated rather than assumed: this was verified against local rivet 0.28.0, while the job pins 0.23.0. The duplicate-id diagnostic may not exist in 0.23.0 at all, in which case hole (1)'s fix is latent protection rather than an active check today; hole (2)'s fix is version-independent. If 0.23.0 emits some other filename-prefixed error we cannot see locally, this job is where it will surface — and surfacing it is the point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * docs(CHANGELOG): v0.55 L7 — traceability repair + the honest-N/A backlog Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * fix(#893): GI-FPU-VER-002 — close the gap the GI-FPU-002 status flip revealed Flipping GI-FPU-002 from `proposed` to `implemented` made rivet START checking its lifecycle coverage, which correctly reported that the requirement had NO verification artifact at all. The gap was not created by the flip — the wrong status was HIDING it, which is the same failure mode as the duplicate id itself, one rule further down. The evidence already existed and was already named in the requirement's own criteria; it simply had no typed `verifies` link. GI-FPU-VER-002 is that link, and it records what the verification actually is rather than asserting that some exists: * the f32 execution differential (48/48 bit-exact vs wasmtime on cortex-m4f, symbols read from the ELF SYMTAB per #489 rather than from host-dependent `synth disasm` text, FPU genuinely enabled via CPACR + FPEXC.EN); * the HONEST-REJECT direction in the same harness (cortex-m3 must still refuse) — a one-directional differential would pass equally well on a compiler that had quietly widened the FPU gate; * the unit-level pins that need no emulator (AAPCS-VFP S0/S1 homing, the swapped-VCVT signedness fix); * the v0.53 #881 spilled-VFP differential (109 rows, NaN-aware per WASM §4.3.3, internal `bl` resolved by a REAL link so an unresolved relocation cannot be silently skipped as a pass); * and the one part of the surface whose evidence is encoding-level ONLY — the f32 comparisons, because unicorn does not model the VMRS FPSCR→APSR flag transfer. Recorded, not omitted. Deliberately shaped `method: automated-test` + `steps.run`/`steps.coverage` rather than mirroring GI-FPU-VER-001's `method: test` + `pass-criteria`, which produce a WARN and an INFO against the schema. This adds ZERO new diagnostics. MEASURED, prompted by review asking whether `rivet coverage` — the SECOND step of the same CI job, which I had not exercised — moved: rivet coverage, real exit 0 both sides swe1-has-verification (sw-req) 31/60 (51.7%) -> 33/60 (55.0%) swe6-verifies-swe1 32/32 -> 34/34 sys5-verifies-sys2 49/49 -> 50/50 Overall (weighted) 90.3% -> 90.7% VCR-RA-004 and GI-FPU-002 both drop off the "lacking verification" list. Full diagnostic diff for the whole branch vs main is now exactly: −2 ERROR (both ours: synth:396, the duplicate id) −2 WARN (GI-FPU-002 and VCR-RA-004 "should be verified by", both closed) +2 WARN, +1 INFO (all three VCR-VER-004's, all of kinds its sibling sys-verification artifacts already carry) So: errors 52 -> 50 with ours 2 -> 0, and warnings net UNCHANGED at 104. Lifecycle coverage gaps 54 -> 56 — honest, not a regression: GI-FPU-002 and VCR-RA-004 are newly CHECKED because they are no longer `proposed`. Both were absent from the baseline list only because a wrong status exempted them. cargo fmt 0 / clippy 0 / test --workspace 0 (2675 passed) / claim_check 37/37. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * docs(CHANGELOG): record the two verification artifacts + the rivet coverage delta Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * docs(roadmap): disambiguate the aarch64 ledger count (5 entries, not 4 named) The prose grouped `local.set`+get and `local.tee` on a param local as one phrase over two separate ledger entries, so the sentence read as four items beside the count 5 — a small instance of exactly the prose-vs-array drift this paragraph exists to prevent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * docs(CLAUDE.md): de-stale the three aarch64 claims L7 correctly declined to touch this on a lane brief (agent configuration, not lane scope) and flagged it instead. Coordinator picking it up. CLAUDE.md carried a byte-identical copy of the stale README row the v0.54 cold review found, plus a third instance nobody had spotted: 1. header: "AArch64 (host-native, integer subset)" — the scalar float surface is complete as of v0.54. 2. crate map: "integer subset" — now i32/i64 core, complete scalar f32/f64, globals, call_indirect, bounds-checked linear memory. 3. VCR-VER-003 note: "AArch64 is N/A (no linear-memory ops in the integer subset)". The VERDICT is still right, the REASON is false — aarch64 has had bounds-checked linear-memory load/store since v0.52 (#865). It is N/A because it emits no data section and REFUSES data-carrying modules loudly (v0.53), so there is no served-vs-runtime image to compare. A correct conclusion resting on a false premise is the harder version of this defect: the sentence reads fine and the reasoning has rotted. Fourth copy of a list this project keeps duplicating (oracle, matrix row, CHANGELOG, CLAUDE.md). Generating the prose from the executable decline list is the standing fix; #911 is the nearest tracked version of it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * docs(CHANGELOG): record the CLAUDE.md aarch64 de-staling, incl. the rotted premise 0f0c232 landed the CLAUDE.md half of the aarch64 doc fix but no release note. Adding one — and specifically calling out its third finding, which is the only one of the four that is not a plain stale string: VCR-VER-003's aarch64 N/A note gave a FALSE REASON for a TRUE verdict ("no linear-memory ops in the integer subset" — aarch64 has had bounds-checked linear-memory load/store since v0.52 #865). It is N/A because it emits no data section and refuses data-carrying modules loudly, so there is no served-vs-runtime image to compare. That failure mode deserves the note more than the two string copies do: a stale "integer subset" reads wrong and invites a check, whereas a correct conclusion resting on a rotted premise still reads fine, so nothing prompts one. Both underlying facts re-verified against the generated feature matrix before writing this. claim_check 37/37 (CLAUDE.md is pinned by three ledger entries; unaffected). 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>
v0.54 lane L2. Closes the four classes v0.53's VCR-SEL-005 third-backend
op-parity oracle enumerated as
Err(reason)ina64_extended_surface. Everygate entry was flipped
Err→Ok(())in the same commit as the loweringand each now-unused reason constant deleted — no wildcard arm, nothing moved to
StructurallyExcluded. After this the aarch64 extended-surface gate carries onlySIMD / multi-memory /
call_indirect: the scalar float surface is complete.ROUNDINGFRINT{P,M,Z,N}s/dFP_MEMLDR/STRs/d, bounds-checkedI64_TO_FPSCVTF/UCVTFx-formsTRAP_TRUNC_I64FCVTZ{S,U}The soundness-critical half
A64
FCVTZS/FCVTZUare more total than WASM: NaN → 0, out of range →saturate to
INT64_MIN/MAX, where §4.3.3 requires a trap. So the trappingi64 truncations go behind the #709 two-sided guard, and the boundary constants
are justified rather than copied from the i32 rows:
b.ge) in both formats: −2^63 isexactly representable and truncates to a legal
INT64_MIN; a strict boundwould trap it. This differs from the i32/f64 row's strict −(2^31)−1, and
for a concrete reason: the f64 ULP at 2^63 is 2048, so no f64 exists in
(−2^63−1, −2^63), whereas −2147483648.5 does exist near 2^31.
b.gt):trunc_u(-0.5) = 0is legal.would trap every legal value above 4294967295.
FRINTNbeing round-to-nearest-ties-to-even (what WASMnearestrequires,vs
FRINTA's ties-away) is checked, not assumed: the halfway table0.5→0, 1.5→2, 2.5→2, 3.5→4 and negatives fails under
FRINTA.FP loads/stores go through the same
form_eaas the integer accesses, sothey inherit v0.52's #865 software bounds check verbatim, width-aware: address
65532 is in-bounds for
f32.loadand out forf64.loadon a one-page memory.Evidence
scripts/repro/aarch64_float_completion_851_differential.py— 662 checks,142 of them trap cases, bit-exact vs wasmtime under two oracles: unicorn
(x28 = linmem base) and native arm64 (each call in a forked child so an expected
SIGTRAPis observed). NaN compared NaN-aware per §4.3.3; values, traps and ±0signs bit-for-bit. The static expect-trap column is validated against wasmtime
first, so the table cannot drift vacuous.
Proven non-vacuous by mutation, both directions of wrong:
A64=0x7fffffffffffffff wasmtime=TRAP)A64=TRAP wasmtime=0x8000000000000000)CI-wired in the same commit with
set -o pipefail+ a numeric assertion on thecheck and trap counts (#890 — v0.53 found three oracles that ran nowhere).
gale's matrix: 45 → 61 ops accepted, 119 → 355 native checks, declined
frontier now EMPTY, including on-silicon trap agreement (its new
trap64helper is itself red-first verified: the same mutation makes it exit 1 with 7
named miscompiles).
Two things I had to move, not delete
f32.{ceil,floor,trunc,nearest}were dropped at the decoder, so the wholefunction loud-SKIPPED before any backend saw it. Un-dropped — which means
ARM32 must now LOUD-DECLINE them: its legacy pseudo-op is an FPSCR-RMode +
VCVT.S32.F32+VCVT.F32.S32round-trip through i32, and VCVTsaturates, so
ceil(1e30)→ 2147483648.0,ceil(±inf)→ a finite bound,ceil(NaN)→ 0.0. That thumb-2 f32: i32.trunc_f32_s/u silently saturate instead of trapping on NaN/Inf/out-of-range (bare VCVT, no guard) — WASM Core §4.3.3 soundness bug #709-class miscompile was latent only because the opwas undecodable; declining keeps it latent instead of shipping it. Exactly the
move Feature challenge: add an aarch64 (host-native) backend so output runs and debugs natively on arm64 dev machines #538 m4 made for
f32.min/max. Follow-up: a realVRINT.F32lowering.f64.flooras its "deliberately declinedfloat op" — a claim this PR invalidates. Repointed at a value-carrying
(f32-result)
block(still decoded, still declined at the selector) and theassertion strengthened: stderr must come from the aarch64 selector and
name the reason, not merely contain "unsupported". The decline-matrix oracle
likewise now pins the structural declines (7/7 loud).
Gates (real exit codes, no
| tail && echo OK)cargo test --workspace0 ·clippy --workspace --all-targets -D warnings0 ·fmt --check0 · frozen anchors 10/10 (no ARM golden moved — the fourrounding ops were undecodable, so no frozen fixture contains one) ·
claim_check34/34 · all 12 pre-existing aarch64 oracles still green.Left declined, honestly
call_indirect, import calls, value-carrying blocks/loops,br_table, writinga PARAM local, globals,
memory.copy/fill, v128/SIMD, multi-memory,>8args, float-result callees — all still loud, all still enumerated by the same
oracle.
🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L