feat(verify): expansion-level certifying validation for the i64 pseudo-ops (#667 move 2, #242)#674
Merged
Merged
Conversation
…o-ops (#667 move 2, #242) The validator-pattern checker certified (WasmOp, [ArmOp]) selections, but the i64 pseudo-ops (I64Mul, the I64SetCond family, Clz/Ctz/Popcnt, the shifts/rotates) are expanded to multi-instruction Thumb-2 sequences inside the encoder — exactly where the #615/#632/#633 miscompiles lived — and the expansions were unvalidated (the pseudo-op-level check models them with their own reference semantics, which is circular about the expansion). New crates/synth-verify/src/expansion_validator.rs: - decodes the LITERAL bytes the shipped encoder emits (small Thumb-2 subset decoder, loud Err on anything unmodeled — never a silent skip) - symbolically executes the sequence: guarded (if-converted) execution with path conditions for forward branches, IT-block predication, NZCV flags (CMP/SUBS/SBCS/ADDS), and a concrete-offset stack model for the scratch PUSH/POP (the #632 surface) - discharges UNSAT(wasm_op_semantics != sequence_semantics) through the certificate-checked BvSolver (ordeal default, LRAT-checked Unsat) Covered (green on the shipped encoder, canonical + high-reg variants): I64Mul, I64SetCond x10, I64SetCondZ/I64Eqz, I64Clz, I64Ctz, I64Popcnt, I64Shl/ShrU/ShrS, I64Rotl/Rotr (#610 fixed-ABI wrapper included). Held out honestly: I64DivS/DivU/RemS/RemU — 64-round long-division loops (backward branches); the decoder rejects them loudly, so the held-out surface cannot green-wash. The #633 missing-guard shape is consequently not yet expressible (needs loop support + UDF-reachability/trap-state modeling) — documented in docs/validator-pattern.md as the named next increment. Oracles: - RED on the literal #632 bug shape (popcnt total materialised into a register inside the POP {R3,R4,R5} restore set) — counterexample found - RED on a wrong-condition compare and on a Clz expansion with the +32 arm nulled - GREEN on every shipped expansion; synth verify now prints one certificate line per pseudo-op ("I64Popcnt expansion certified: 48 instrs, 180 bytes [unsat, LRAT-checked]") Tractability notes (measured): i64.mul is checked against the textbook schoolbook limb form (the symbolic schoolbook<->64-bit bvmul identity bit-blasts past any budget; pinned on an edge-heavy concrete grid); popcnt uses a two-link chain (HAKMEM fold == bit-sum proved symbolically once, sequence == HAKMEM fold per validation) because the one-link query does not converge. Verify-path only: no selector/encoder/codegen change; frozen anchors untouched. cargo test --workspace 2062 passed / 0 failed; fmt + clippy -D warnings clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
avrabe
added a commit
that referenced
this pull request
Jul 8, 2026
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.
Part of #667 (move 2: extend the certifying validator) / epic #242 Track C.
What
The
validator_patternchecker certifies(WasmOp, [ArmOp])selections — but the i64 pseudo-ops are expanded to multi-instruction Thumb-2 sequences inside the encoder, and the pseudo-op-level check models them with their own reference semantics (circular about the expansion). The expansion is exactly where this cycle's miscompiles lived: #615 (A32 silent NOP), #632 (popcnt result clobbered by the scratch-restorePOP), #633 (missing i64div_soverflow guard).New
crates/synth-verify/src/expansion_validator.rsputs the shipped encoder's literal bytes on the checked path:Erron anything unmodeled, never a silent skip),pop {r3,r4,r5}— silent wrong value #632 surface),UNSAT(wasm_op_semantics ≠ sequence_semantics)through the certificate-checkedBvSolver(ordeal default per feat(verify): ordeal 0.4 solver backend behind a thin trait — Z3 demoted to differential oracle (#553) #595/chore(verify): drop static-link-z3 from the default build/CI path (#553 steps 3/4) #621 — everyUnsatcarries an LRAT proof checked before it is reported; Z3 stays the feature-gated differential oracle).Because the checked artifact is the encoder's own output, encoder↔validator drift is unrepresentable — there is no second hand-maintained copy of the expansion.
Coverage
I64MulI64SetCond×10 (EQ/NE/LT/LE/GT/GE/LO/LS/HI/HS)I64SetCondZ(i64.eqz)I64Clz/I64Ctz+32armI64PopcntI64Shl/I64ShrU/I64ShrSI64Rotl/I64RotrI64DivS/DivU/RemS/RemUHeld out honestly: the div/rem cores contain backward branches (loops); the forward-only DAG executor rejects them with a loud decode error (
backward branch (loop) — held out, needs loop unrolling), so the held-out surface cannot green-wash (testdiv_family_is_held_out_loudly). Sound coverage needs bounded unrolling (64 × ~10 instrs is past the bit-blasting budget) or invariant reasoning. Consequently the #633 missing-guard shape is not yet expressible at this layer: the guard sits inside the div expansion, and trap-guard equivalence needs UDF-reachability (trap-state) modeling on top of loop support — documented indocs/validator-pattern.mdas the named next increment.Red-on-#632-shape evidence
red_632_popcnt_result_in_restore_set_failssplices the literal pre-fix tail from the #632 disassembly (ADDS R5,R4,R5; POP {R3,R4,R5}; MOV rd,R5) onto the shipped popcount body (the shipped tail —ADD.W R12,R4,R5; POP; MOV rd,R12— is byte-pinned so the test screams if the encoder changes):(counterexample: the sequence returns the caller's dead R5 — the solver's model distinguishes via the unconstrained initial register, i.e. exactly the "stale stack garbage" the issue observed). Also red: a wrong-condition compare (
i64.lt_svs the GE sequence) and a Clz expansion with the+32arm nulled. Green in the same suite: every shipped expansion, plus high-register variants (rd=R8 → MOV.W path #311; popcnt rd=R5, i.e. a destination inside the restore set — the exact #632 trigger — certifies on the fixed encoder).Certificate logging at verify time
synth verify(verify feature path, ARM backend) now certifies all 20 covered expansions per run, one certificate line each:Tractability notes (measured, documented in the module)
bvmulidentity does not converge (>400 s — same line as the documented i32rem_sMLS scope-out); it is pinned on an edge-heavy concrete grid vs Rust'su64multiply.∀w. hakmem(w) == bit-sum(w)proved symbolically once (~7 s, unit test), thensequence == hakmem(lo)+hakmem(hi)per validation (23 s). The one-link query (sequence vs bit-sum over both words) does not converge (>570 s).CLZ(RBIT(x)).Oracles / gates
cargo test --workspace: 2066 passed / 0 failed (post-rebase ondf982f1)crates/synth-backend/tests/i64_expansion_certification.rs, ~50 s)cargo fmt --check+cargo clippy --workspace --all-targets -- -D warningsclean (also-p synth-cli --features verify)🤖 Generated with Claude Code