WCET phase 5 (#778): data-dependent masked-ceiling loop certificates#839
Merged
Conversation
Extend the --wcet-hints scry seam past const-trip/const-depth to a
DATA-DEPENDENT masked bound (`i REL (x & K)`). The masked value is
entry-independently bounded to [0, K] for ANY runtime x (x & K in [0,K]),
so synth DERIVES the worst-case trip as the MAX over both endpoints of the
bound domain (rhs=K and rhs=0, both required to terminate) — a single
endpoint is unsound for count-down shapes. Hint-gated + derived-not-trusted,
mirroring the equality-exit and recursion-depth seams:
- new Sym::Masked{mask} tracked from `And rd,_,#K` (K>=0, sign-bit clear so
x&K stays non-negative); base identity irrelevant (x&K<=K for any x)
- eval_pred accepts (Slot, Masked) -> Pred{rhs:K, masked_ceiling:Some(K)},
surviving the SetCond->cmp#0 indirection via Sym::Bool
- masked_exit_index: both-endpoints max + Eq/Ne |step|==1 restriction;
always hint-gated
- new WcetLoopBoundSource::MaskCeiling so the sidecar states the extra
data-dependent-ceiling assumption (distinct from HintVerified)
Decline moved not deleted: unhinted masked loop still declines `loop`;
unmasked `i < param` still rejects `hint-unverifiable-induction` (param is
Sym::Top, never Masked); too-low hint rejects `hint-below-derived-trip`.
Frozen-safe: WCET reads the final stream, .text byte-identical w/ or w/o hints.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Execute compiled masked-ceiling loops under unicorn (Thumb-2): bound >= actual for the worst-case input, entry-independence across the masked domain, and the count-DOWN case whose worst case is the rhs=0 endpoint (proves both-endpoints max is not the naive single-endpoint undercount). Plus decline-honesty: unhinted masked stays declined, too-low hint rejected, UNMASKED i<param stays declined hint-unverifiable-induction (the mask is the discriminator). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
wcet_bound_gate.rs: 5 phase-5 fixtures (red-first) — unhinted masked declines `loop`; count-up correct hint bounds (trip 7, source mask-ceiling, 262 cyc); count-DOWN uses both-endpoints (trip 10 not the naive 3, 339 cyc); too-low hint rejected hint-below-derived-trip; UNMASKED i<param stays declined hint-unverifiable-induction (the mask is the discriminator — decline moved, not deleted). claims.yaml: pin the both-endpoints max (hi.max(lo), both required to terminate) under SYNTH-WCET-CYCLE-MODEL — a single-endpoint regression would undercount. ci.yml: phase-5 note on the existing WCET gate step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
avrabe
enabled auto-merge (squash)
July 22, 2026 19:07
# Conflicts: # CHANGELOG.md
avrabe
added a commit
that referenced
this pull request
Jul 23, 2026
… endgame begins (#845) * chore(release): v0.50.0 — cross-backend verified core + the allocator endgame begins (7-lane hub) Wave 1: VCR-RA-003 RV32 alloc-validator (#815/#821), VCR-RA-003 ARM optimized-path joins deferred (#819→redo), VCR-WASM i64 batch Qed 585→591 (#814). Wave 2: RV32 memory.size/grow parity (#841), WCET ph5 data-dep masked-ceiling certs (#839), aarch64 void-block CF (#842), soundness sweep (WIP deferred), native i64 rem_u/rem_s modeling via ordeal-0.12 Urem/bvsrem (#844), #837 frame-backing i64-param, and the headline VCR-DEC-001 graph-colouring allocator spike flag-off (#843) — validated by VCR-RA-003, flag-off byte-identical. Plus ci auto-merge for dependabot (#838). Pin sweep 0.49.0→0.50.0; status.json/FEATURE_MATRIX regenerated; 25/25 claims; frozen 10/10. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * revert(verify): i64.rem_u/rem_s native BvTerm::Urem/bvsrem model (#844) — solver hang #844 replaced the I64RemU/I64RemS HAVOC in arm_semantics with a real 64-bit `dividend.bvsrem/bvurem(&divisor)`. That query is the hardest bitvector class there is, and it HANGS both solvers: the pure-Rust ordeal default (→ the CI "Test" job) and the Z3 differential (→ the "Z3 Verification" job). Both jobs have run 4-6h and timed out on EVERY CI run since #844 landed — the last green CI on main was 2026-07-20 (61acec9). Confirmed independently: the L8 rem test produced no output (hung) in salvage, and a separate agent's full workspace run reached 107 test binaries / 0 failures and stalled only on this synth-verify rem SMT test. This restores the pre-#844 havoc model (byte-invisible — verify-only, no codegen change, frozen anchors untouched by construction) to un-hang CI and unblock the v0.50.0 tag. Native i64 rem re-lands in v0.51 behind a per-query solver timeout so a hard bvurem degrades to `unknown` instead of hanging the whole suite. Tracking: #848 (re-land behind a per-query solver timeout). The term.rs/solver.rs BvTerm::Urem enum arms stay — they are the #836 ordeal-0.12 enum-completeness handlers (harmless; nothing in verify now generates the variant). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * fix(#849): pin ordeal =0.9.1 — 0.12 bvsrem/bvsdiv perf regression HANGS CI ROOT CAUSE of the 4-6h CI hangs (both the ordeal-default Test job and the Z3 Verification job) that have red-mained `main` since 2026-07-20: the ordeal 0.9.1 -> 0.12.0 dependabot bump (#825). 0.12 has a solver-performance regression on bvsrem/bvsdiv that hangs div/rem trap-preservation VCs — even the pre-existing 32-bit `rems_single_zero_guard_is_exactly_right` (I32RemS Sdiv+Mls, #753/v0.43) hangs >240s on 0.12 but runs <1s on 0.9.1. The last GREEN CI on main (61acec9, 2026-07-20 21:45:27) used ordeal 0.9.1 — #825 merged 3 seconds later. NO commit with ordeal 0.12 has ever passed CI; every subsequent lane auto-merged on the required-gate subset while Test/Z3 timed out. #844 (native i64 rem via 0.12 bvsrem/bvurem) made it far worse and was reverted (db0f1f2, re-land #848); this pin is the actual fix. Changes: - crates/synth-verify/Cargo.toml: ordeal "0.12.0" -> "=0.9.1" (exact last-green pin; do-not-bump note added). Cargo.lock: ordeal + ordeal-lrat 0.12.0 -> 0.9.1. - Remove the four #836 `BvTerm::Urem` match arms (term.rs x3, z3-gated solver.rs x1) — that variant exists only in ordeal 0.12. Verified: `cargo test -p synth-verify` = 236 tests, 0 failed, ~41s (was >6h timeout on 0.12). Byte-invisible — verify-only, no codegen change, frozen anchors untouched by construction. Follow-ups (tracked, non-blocking): report the bvsrem/bvsdiv regression upstream to ordeal; tighten .github/workflows/dependabot-auto-merge.yml (#838) to HOLD 0.x MINOR bumps (0.9->0.12 is breaking by 0.x semver but dependabot classified it "minor" and auto-merged it). See #849. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * docs(changelog): v0.50.0 — replace #844 i64-rem entry with the ordeal =0.9.1 pin (#849) #844's native i64 rem value model was reverted (it depended on ordeal 0.12, which has the bvsrem/bvsdiv perf regression that hangs CI). The real v0.50.0 change is the ordeal =0.9.1 pin. Do not advertise a reverted feature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * fix(oracle): rv32 mem-size/grow — read symbols from ELF .symtab, not disasm text The `rv32 memory.size / memory.grow execution oracle` (#841, VCR-SEL-005) has FAILED on every CI run it actually executed — "FAIL — no symbol for export sz ([])" — the `syms` map came back EMPTY on the ubuntu runner. It located function offsets by regex-parsing `synth disasm` TEXT (`^([0-9a-f]{8}) <(\w+)>:`), which matched nothing on Linux for the `--relocatable` RISC-V object even though it matched on macOS. It was merged ungated: main CI has been timing out since 2026-07-20 (ordeal-0.12 hang, #849), so this job was CANCELLED on every prior run and its true `failure` never surfaced until the hang was fixed. The codegen is correct — the oracle passes in every local configuration (fresh binary, old + latest wasmtime 47.0.1). Only the symbol LOOKUP was host-fragile. Fix: read function offsets from the ELF SYMBOL TABLE via pyelftools (`.symtab`, `st_value` = offset within `.text` for a relocatable object) — host-independent, the documented differential-harness lesson. The `.text` bytes were already read via pyelftools; this just does the addresses the same way. Drop the now-unused `re` import. Verified: same addresses, oracle still PASS locally (sz=3/grow0=3/grow2=-1). No codegen/frozen change. Follow-up (non-blocking): 10 other repro oracles still regex-parse disasm text (add_imm_large, bulk_memory_374, control_step_riscv, div_const, ...) — they pass on CI today but carry the same latent host-dependency; harden them to symtab. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L --------- Co-authored-by: Claude Opus 4.8 <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.
What
Extends the
--wcet-hintsscry seam (untrusted oracle + sound synth-side checker) past const-trip / const-depth to a DATA-DEPENDENT loop whose exit bound is a masked valuei REL (x & K). This is the named richer-certificate follow-up to WCET phase 4 (bounded recursion), applying the same untrusted-hint + sound-checker pattern to data-dependent loop shapes — no scry dependency, a synth-side seam.Refs #778, #242 (Track D schedulability).
The certificate design (what synth verifies)
The witness is an entry-independent masked ceiling, not merely a decreasing measure. A decreasing measure proves termination; a finite WCET number additionally needs an entry-independent upper bound on the measure's initial value. That discriminator is the mask:
x & K ∈ [0, K]for ANY runtimexwhenK's sign bit is clear (Sym::Masked { mask }, tracked fromAnd rd, _, #K). Base identity is irrelevant —x & K ≤ Kregardless of whatxholds (simpler and more robust than recursion's masked chain, which needs the base identity for its decrement).[0, K], so synth DERIVES the worst-case trip as the MAX over both endpoints (rhs = Kandrhs = 0), requiring both to terminate. A single endpoint is unsound for a count-DOWN loop — the fatal class (see below). The both-endpoints max is pinned inclaims.yamlunderSYNTH-WCET-CYCLE-MODEL.|step| == 1(else an interior bound value can be missed — the divisibility-divergence trap), carried over from the recursion module.loop; a--wcet-hintsloop_boundsentry only gates consumption; the emitted trip is synth's DERIVED ceiling (new sourcemask-ceiling), never the raw hint.bound ≥ actual evidence (unicorn Thumb-2 cross-check)
scripts/repro/wcet_phase5_778_masked_loop_soundness.pyexecutes the compiled loops and assertsbound ≥ executed insnsfor the worst-case input + entry-independence across the masked domain:maskloop(0xFFFFFFFF)= r0 21: 138 insns ≤ 262 cyc;maskloop(0): 19 ≤ 262.cd(0)= r0 10 (10 iterations — therhs=0worst case): 180 insns ≤ 339 cyc. A naive single-endpoint seed (rhs=7) would have derived trip 3 → a bound below the real 10-iteration execution. The both-endpoints max derives 10 — this is the load-bearing soundness case.invalid-cert-rejected evidence (red-first)
hint-below-derived-trip, function stays declined.i < param(no entry-independent ceiling) → still declinesloop+hint-unverifiable-inductioneven WITH a hint.paramisSym::Top, neverSym::Masked, soeval_predreturnsNone— the mask is the sole discriminator. This is the existingdata_dependent_hint_is_rejected_unverifiabletest staying green: proof the decline moved onto the masked shape, not widened to every runtime bound.loop(opt-in gate).before → after decline matrix
i < (x&K), no hintloopdeclineloopdecline (opt-in)loopdeclinemask-ceiling, derived trip)loopdeclineloopdecline +hint-below-derived-tripi < param, any hintloop+hint-unverifiable-inductionGates
crates/synth-cli/tests/wcet_bound_gate.rs: 5 new phase-5 fixtures (count-up accept, count-down both-endpoints, unhinted decline, too-low reject, unmasked reject). 39/39 gate tests pass.claims.yaml: pins the both-endpoints max (let (hi, lo) = (at(ceiling)?, at(0)?);) — a single-endpoint regression reddens claim-check. 25/25 claims hold..github/workflows/ci.yml: phase-5 note on the existing WCET gate step.Frozen + claim status
.textis byte-identical with or without--wcet-hints(verified). The sidecar is additive metadata.SYNTH-WCET-CYCLE-MODELid).🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L