feat(verify): re-land native i64 rem value model behind a per-query solver deadline (closes #848) - #868
Conversation
…rd_bv panic + hold 0.x-minor bumps main did NOT COMPILE (3x E0004). Dependabot auto-merged ordeal 0.9.1 -> 0.16.1 (#864) with NO CI RUN, and the four `BvTerm::Urem` match arms — removed when we downgraded to 0.9.1 (4ad175e), since that version lacks the variant — were never restored. 1. Restore the arms: term.rs (bv_rank, canonicalize_bv rebuild, SMT-LIB bvurem formatter) + the z3-solver-gated solver.rs (bv_to_z3 bvurem). 2. FIX A LATENT PANIC found while restoring them: bv_rank gave `Udiv` and `Urem` BOTH rank 5, but `ord_bv` asserts (via `unreachable!`) that rank-equal variants are exhaustively matched — and there is no (Udiv, Urem) cross pair, so comparing a Udiv against a Urem PANICS. Reachable: the rem model is `rem = a - (a/b)*b`, so div and rem terms meet under a commutative op. Urem now has its own rank (6, shifting And..Ite up one) and the `(Urem, Urem)` pair is matched. 3. KEEP the 0.16.1 bump — it is CORRECT: verified the full synth-verify suite runs 236 tests / 0 failed / ~33s (vs ~41s on 0.9.1). ordeal 0.16.1 fixes the unsigned-bvurem blast regression (ordeal#101) that 0.16.0 still had, so the =0.9.1 pin is no longer needed. This UNBLOCKS #848/#849. 4. CLOSE THE DEPENDABOT GAP (it has now bitten twice on this same dep): a 0.x MINOR bump is BREAKING by convention (for 0.x the minor is the de-facto major) but Dependabot labels it `semver-minor`, so the auto-merge workflow let it through. dependabot-auto-merge.yml now HOLDS 0.x-minor bumps (label + explanatory comment). Version strings are passed via env, never interpolated into the script body (pull_request_target + contents:write). Verified: build clean, synth-verify 236/236 green in ~33s, fmt + clippy clean, workflow YAML parses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
#849) The #849 incident (4-6 h CI hangs, every run for three days) happened because there was NO wall-clock floor anywhere in the solver stack: the conflict budget does not map to time, and the div/rem trap VCs did not even go through the budgeted seam — `crate::trap` called `ordeal::trap::prove_trap_*`, which delegates to the UNBOUNDED `Solver::prove_valid`. Wire `Solver::check_with_deadline` (ordeal >=0.15) into BOTH seams: - `solver::OrdealSolver::check` now bounds by wall clock (default 15 s, `SYNTH_ORDEAL_DEADLINE_MS`; 0 disables and falls back to the existing `SYNTH_ORDEAL_MAX_CONFLICTS` conflict cap — ordeal's `Bound` is one-of, so a query carries a deadline OR a conflict cap, never both). - `trap::prove_trap_{equivalence,condition_equivalence}` now build the goal term themselves (`ot::trap_equivalence_vc` / `ot::trap_condition_equivalence`) and discharge it through the same bounded validity-as-UNSAT encoding, so the hardest VC class in the validator finally has a floor. - the Z3 differential oracle gets the identical budget via the `timeout` param — the "Z3 Verification" job hung on the same queries. Expiry degrades to `Unknown`, which every caller already treats conservatively (`ValidationResult::Unknown` / `TrapVerdict::Unknown`) — an undecided query is NEVER reported as `Verified`/`Preserved`. The certificate re-check and model self-check are untouched, so the bound costs completeness only, never soundness. DOCUMENTED LIMITATION (in the code): `check_with_deadline` bounds the SAT SEARCH, not bit-blasting/canonicalization. A query whose blast alone is pathological can still exceed the budget before the clock is consulted. This is a strong bound on the dominant cost, not a universal wall-clock guard. Refs #848, #849. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…loses #848) Reverts db0f1f2 (the #849 un-hang revert). ordeal 0.16.1 fixes BOTH blast regressions the revert was forced by — signed `bvsrem` (ordeal#97) and unsigned `bvurem` (ordeal#101) — and the previous commit puts a per-query wall-clock deadline under every solve, so a future solver cliff degrades to `Unknown` instead of hanging CI for six hours. `ArmSemantics` `I64RemU`/`I64RemS` compose the 64-bit operands from their register halves (`concat`), apply the native `bvurem`/`bvsrem`, and split the result back to the rd pair; the divide-by-zero trap is reconstructed from the pseudo-op's `elide_zero_guard` field. `verify_i64_rem_value_preservation` asserts the FULL obligation (trap clause AND guarded value clause) against the ABI pair R0:R1, so the value model is load-bearing rather than decorative. MEASURED on 0.16.1 with the model live: the four i64-rem tests (8 x 64-bit bvurem/bvsrem VCs) decide in 2.42 s serial. Under the reverted #844 the same tests never terminated. Non-vacuity is gated, not narrated: `i64_rem_wrong_destination_register_is_rejected` (rd = R2:R3 instead of the ABI R0:R1 -> Invalid) and `i64_rem_value_model_closes_a_trap_only_gap` (the OLD trap-only VC ACCEPTS the same wrong-destination lowering; the new value VC REJECTS it) pin the exact accepted-under-havoc -> rejected-now discriminator. Verify-only: no codegen path touched, frozen anchors untouched by construction. Closes #848. Supersedes #854. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Neither job had a `timeout-minutes`, which is why the #849 solver cliff burned 4-6 h per run for three days instead of failing fast. The per-query deadline (#848) is the inner floor; these are the outer one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…anism gate 15 s was too tight: it cut off a PRE-EXISTING legitimate query. Measured on a dev host (debug, ordeal 0.16.1), the expansion_validator popcnt-HAKMEM link-1 equivalence needs 40.6 s to decide — it is genuinely hard, not a cliff — and a 15 s budget degraded it to Unknown, turning a real proof into a spurious non-answer. The re-landed 64-bit bvurem/bvsrem i64-rem VCs, by contrast, take 2.4 s for all four tests. 300 s is ~7x the measured pacer (headroom for a slower, parallel-loaded CI runner) while still turning a solver cliff from 6 hours into 5 minutes; the outer CI timeout-minutes bounds the aggregate. Adds solver::tests::deadline_degrades_to_unknown_never_to_proven: a 1 ms budget on a hard query must yield Unknown and MUST NOT yield Unsat (which every caller reads as proven), with the same query at deadline-disabled deciding Unsat so the gate is not vacuous. Plus default_deadline_decides_the_corpus (the shipped budget must not starve ordinary queries). Also writes the [Unreleased] CHANGELOG entries for both halves of the lane. Refs #848, #849. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
The first cut re-proved the SAME hard instance unbounded as its non-vacuity control. Measured: negated mul-associativity runs >30 s unbounded at width 32, 16 AND 8 — it would have dominated the suite. Split the two legs instead: - bounded leg: width 8 at 1 ms -> must be Unknown, must NOT be Unsat, and the reason must NAME the deadline (a non-answer has to say why); - control: the SAME construction at width 3 -> Unsat in milliseconds. Associativity is an identity at every width, so the only way this gate could go vacuous is a coding error in the goal builder — which is exactly what the narrow control rules out. Re-proving the width-8 instance would cost >30 s for no extra information. Whole solver::tests module: 5 passed, 0.02 s. Refs #848, #849. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
The trap-preservation gate does NOT go through `solver::new_solver`, so SYNTH_SOLVER_DIFF never cross-checks the div/rem VCs against Z3. That means the CI "Z3 Verification" job hung in #849 because it runs the same ORDEAL tests, not because Z3 was slow on them — the z3 `timeout` param this PR adds is defence in depth for the value-VC path that does use the oracle, not a fix for the observed hang. Say so in trap.rs and the CHANGELOG rather than letting the table imply coverage that does not exist. Routing trap VCs through the differential seam is a named follow-up. Refs #848, #849. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
clippy -D warnings (needless_borrows_for_generic_args) on the new mul_assoc_goal helper. Workspace clippy --all-targets now clean. Refs #848. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…conflicts) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… the destination The shipped non-vacuity gate proves the DESTINATION field is load-bearing (rem into R2:R3 instead of the ABI R0:R1 -> Invalid). That leaves a strictly stronger class unpinned: a lowering that writes the RIGHT registers and preserves the ÷0 trap but computes the WRONG remainder. i64_rem_wrong_signedness_is_rejected closes it — rem_u lowered to the signed pseudo-op (and rem_s to the unsigned one), correct ABI destination, correct trap. Only a real bvurem/bvsrem value model can see the difference, and it does: both directions come back Invalid with the expected counterexamples (negative dividend for rem_u-as-rem_s, negative divisor for rem_s-as-rem_u). Under the HAVOC model both sides were unconstrained ⟹ Verified. Cost: the SAT search is ~23 s locally (12× under the 300 s deadline) but FREE in suite wall-clock — it runs behind the pre-existing 40 s popcnt-HAKMEM proof, which remains the long pole (lib suite 36.2 s, unchanged). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Independent verification (fresh worktree, re-verified from scratch)The authoring agent died on a session limit before its gate ran, so nothing below is inherited — every number was re-measured on 1. THE HARD GATE — the suite runs fast (this is the whole point)
End-to-end Per-class breakdown of the re-landed model:
No test anywhere near the 2-minute cliff threshold. ordeal 0.16.1 has no cliff on these queries. Verdict: PASS. 2. The z3 path builds and passesBoth the plain feature build and CI's exact differential invocation:
3. Non-vacuity — verified, and then strengthenedThe shipped gates do genuinely flip verdicts; I read the assertions and re-ran them:
Added in this review (7f6d579): the destination test pins the register file, not the arithmetic. A lowering that writes the right registers, preserves the ÷0 trap, and computes the wrong remainder would pass it. — a negative dividend and a negative divisor, the only region where 4. Deadline soundness — read against ordeal's own source, not just synth'sTraced
Mechanism non-vacuity — the one thing the committed test cannot show. Elapsed tracks the budget to within 35 ms at 120 s. The deadline is doing real wall-clock enforcement — the gate is not vacuous. (Also: the width-8 instance is >120 s hard, not the ">30 s" the comment claims — understated, so harmless.) 5. Is 300 s justified? Yes — with one caveat worth statingMeasured pacers on this host: slowest legitimate query is popcnt-HAKMEM at 39.8 s; the new signedness SAT search is 23 s; the i64-rem UNSAT VCs are sub-second. 300 s is ~7.5× the slowest legitimate query. A Caveat, stated plainly rather than fixed: the per-query deadline does not bound the aggregate. A future cliff that hits a whole VC class (say 8 queries × 300 s = 40 min) still exceeds what the The honest-scoping commit is also correct: the trap VCs build 6. Byte-invisibility and hygieneFrozen 10/10 confirms the verify-only claim: CHANGELOG re-read against the code: accurate, no overclaiming. I extended the non-vacuity paragraph to cover the new arithmetic-level gate and corrected "the four i64-rem tests" to name what those four actually cover. What I changed
Nothing was weakened to make anything pass. Not merged — evidence only. |
Addendum — the CI evidence, which beats any local measurementAll 46 checks pass on the final head (
That is the gate. The native i64-rem value model is back and the suite is fast on the machine class where it previously hung — 5 m against a 60 m outer guard, ~12× margin. Recommendation: this is safe to merge. Not merging, per instructions. Local hygiene note, unrelated to the change: my |
Closes #848. Supersedes #854 (which can be closed as superseded).
What
Two changes that belong together — the second is only safe because of the first.
1. A per-query wall-clock deadline on every SMT solve (the real insurance)
The #849 incident (4–6 h CI runs, every run for three days,
TestandZ3 Verification) happened because there was no wall-clock floor anywhere inthe stack. The existing conflict budget does not map to time, and — worse —
the div/rem trap VCs did not even go through the budgeted seam:
synth_verify::trap::prove_trap_*calledordeal::trap::prove_trap_*, whichdelegates to the unbounded
Solver::prove_valid. So the hardest VC class inthe whole validator had no bound at all.
Solver::check_with_deadline(ordeal ≥0.15) is now wired into both seams:solver::OrdealSolver::checkcheck_with_limit(1_000_000)conflictscheck_with_deadline(ms)trap::prove_trap_{equivalence,condition_equivalence}Solver::prove_validot::trap_equivalence_vc/ot::trap_condition_equivalence) and discharges it under the deadlinetimeoutparam, same budgetSYNTH_ORDEAL_DEADLINE_MS.0disables thedeadline and falls back to the pre-existing
SYNTH_ORDEAL_MAX_CONFLICTSconflict cap — ordeal's
Boundis one-of (None | Conflicts | Deadline), soa query carries a deadline or a conflict cap, never both.
CheckOutcome::Unknown→ValidationResult::Unknown/TrapVerdict::Unknown. An undecided query isnever reported
Verified/Preserved. ordeal's LRAT certificate gate andmodel self-check are untouched, so the bound costs completeness only.
check_with_deadlinebounds the SATsearch, not bit-blasting/canonicalization. A query whose blast alone is
pathological can still exceed the budget before the clock is consulted. This
is a strong bound on the dominant cost, not a universal wall-clock guard.
TestandZ3 Verificationjobs had notimeout-minutes— which is exactly why ordeal 0.12.0 solver-performance regression HANGS div/rem trap-preservation VCs — pinned back to 0.9.1 #849 cost days instead of minutes.Added (60 / 45).
Why 300 s and not the 10–15 s originally suggested
A tight budget converts a sound proof into a spurious
Unknown. Measured here(debug build, ordeal 0.16.1, dev host):
expansion_validatorpopcnt-HAKMEM link 1 (pre-existing)bvurem/bvsrem)A 15 s default cut off the popcnt query — a genuinely hard but perfectly
decidable obligation, nothing to do with rem. 300 s is ~7× the measured pacer
(headroom for a slower, parallel-loaded CI runner) and still turns a solver
cliff from 6 hours into 5 minutes; the new
timeout-minutesbounds theaggregate.
2. Re-land the native i64
rem_u/rem_svalue model (#844)Reverts
db0f1f2.ArmSemanticsno longer HAVOCs the i64 remainder:I64RemU/I64RemScompose the 64-bit operands from their register halves(
concat), apply the nativebvurem/bvsrem, and split back to therdpair,with the ÷0 trap reconstructed from the pseudo-op's
elide_zero_guardfield.verify_i64_rem_value_preservationasserts the full obligation (trap clauseand guarded value clause) against the ABI return pair
R0:R1.Safe to re-land now because ordeal 0.16.1 fixes both blast regressions —
signed
bvsrem(ordeal#97) and unsignedbvurem(ordeal#101) — and thedeadline above is the standing insurance against the next cliff.
Evidence
Non-vacuity (this is the point of the value model) — the wrong-destination
lowering (
rd = R2:R3instead of the ABIR0:R1) is REJECTED; under HAVOCit was accepted:
i64_rem_value_model_closes_a_trap_only_gapis the concrete discriminator: theold trap-only VC returns
Verifiedfor the wrong-destination lowering whilethe new value VC returns
Invalid— opposite verdicts on the same input.Deadline mechanism, red-first:
solver::tests::deadline_degrades_to_unknown_never_to_proven— a 1 ms budget ona hard query (width-8 mul associativity, >30 s unbounded) must yield
Unknown,must not yield
Unsat, and the reason must name the deadline; the sameconstruction at width 3 decides
Unsatin ms, so the gate is not vacuous.Suite timing — the hard gate (
cargo test -p synth-verify, debug, dev host):--features z3-solver,arm--features z3-solver,arm+SYNTH_SOLVER_DIFF=1Unknownfall-throughsNo test runs anywhere near the 2-minute intolerance threshold; the pacer is the
pre-existing popcnt query, not rem.
Byte-invisible: verify-only change, no codegen path touched;
cargo test -p synth-cli --test frozen_codegen_bytes10/10 unchanged.The 64-bit
rem_uVC shapeThe re-landed
rem_uVC is a directbvurem-vs-bvuremequivalence, not abvurem-vs-multiplicative-reconstruction one: both sides of the value clausebuild the identical
concatof the same four 32-bit half-symbols and applybvurem, so the valid case is structural. The hard work is the guarded-valuebvuremblast and thewrong-destination
Satsearch — which is what used to cross circuits.On 0.16.1 the whole class decides in 2.4 s.
🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L