perf(#846): relational ranges take gpio-thin to 494 B — and the gate that could not see it - #976
Merged
Merged
Conversation
… the two CRL/CRH masks (502 -> 494 B) gale's gpio-thin driver has sat at 502 B / 3 residual #682 mod-32 shift masks since v0.52.0. The #846 analysis note called two of the three "recoverable in principle (correlated / predicate-aware ranges), but not soundly today" — this is that analysis. The two survivors are the STM32 CRL/CRH idiom `sel ? p*4 : p*4-32` where `p = pin & 0xf` and `sel = (p < 8)`. The result is < 32 on BOTH arms, but only through the CORRELATION between `sel` and the range of `p*4`. No value-range domain can see it: `p*4 - 32` WRAPS to 0xFFFFFFE0 for p = 0, so the meet of the two arms is all of u32 and `Lt32Facts` correctly declines. Rather than reach for a relational domain (or, worse, a syntactic `sel ? x : x-32` matcher — exactly the patch-accretion the north star forbids), phase 2 makes the correlation CONCRETE. `p = pin & 0xf` can only take the 16 submask values of 0xf whatever `pin` is; evaluate the straight-line prefix once per value and `sel` and `p*4` are computed TOGETHER in each case, so the impossible combination (sel = 1 with p*4 >= 32) is never considered. Both arms land in [0,28] in all 16 cases and < 32 follows by enumeration instead of by dataflow. SOUNDNESS — the abstraction over-approximates the reachable value set, so "every case < 32" implies "always < 32" (the #682 invariant): - every modeled op computes exact wrapping 32-bit semantics per case; every op NOT modeled exactly drops its results to top (any value), which can never contribute to a proof; - ONE symbolic seed only. A second masked-unknown is left top rather than Cartesian-producted: re-indexing every tracked vector at a second allocation is the one place a silent indexing bug could manufacture a wrong verdict. Documented precision limit, not a soundness one; - straight-line prefix only. The walk stops at the first label or branch, so nothing can jump into the analysed region and `j` is reachable only by falling through from entry; - a call drops every REGISTER but PRESERVES tracked [SP,#off] slots — the AAPCS frame premise synth's own allocator already depends on wherever it spills across a call. Any store that cannot be placed exactly (non-SP base, register offset, sub-word, SP adjustment) drops ALL slots; - flags are trusted for EXACTLY the instruction after the `Cmp` that set them. `Add`/`Sub`/`Mvn` really do encode as flag-setting `adds`/`subs`/ `mvns` in this stream (0x70, 0x9a, 0x9c), so "the last Cmp still owns the flags" is not safe across an arbitrary instruction; - register shift amounts are Rm<7:0>, NOT Rm & 31 — an amount >= 32 shifts everything out, and using & 31 could conclude < 32 when it is not; - `Movt` requires the PRIOR value known (never defaulted to 0); - R12's value survives exactly one instruction (the adjacent shift, whose value the rewrite preserves), so no conclusion rests on an R12 def this pass deletes; - phase 2 runs on phase 1's OUTPUT, so no conclusion rests on a `movw` that phase 1 deleted as dead. MEASURED, on gale's real 656 B gpio.loom.wasm (not the issue's stale number): before 502 B, 3 masks after 494 B, 1 mask (opt-out baseline 534 B) The single survivor is the load-bearing one, verified by disassembly at 0xd8: `and ip, r8, #0x1f` on `lsl.w r8, r6, #2` with `r6 = ldr.w [sp,#0x24]`, a frame-reloaded RAW param. For mode >= 8, `mode << 2 >= 32`; eliding it IS the #682 miscompile. It correctly stays: the reload is top, so every case is top. 494 = 502 - 2x4 (two 4-byte `and.w` deletions; the rewritten `lsl.w rD,rN,rK` is the same width as `lsl.w rD,rN,ip`). The issue's "498 B remaining headroom" was arithmetically inconsistent with its own `502 - 3x4 = 490` identity — 498 is ONE mask, not two. 490 remains unreachable and unsound, as #846 established. Frozen anchors: all 10 unchanged (Pattern C does not fire on control_step / flight_seam / flight_seam_flat / oracle_001). No refreeze. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…ach proven potent by mutation Mirrors the Lt32Facts landing discipline: the analysis is only worth what its must-KEEP cases are worth. The positive case is the real gpio_configure stream transcribed instruction-for-instruction (seed, spill, SetCond, call, reload, lsl #2, mvn-built -32, correlated SelectMove); every other case perturbs exactly one premise and asserts the mask SURVIVES: wider seed mask (0x3f: p*4 reaches 252) | correction arm removed raw frame-reloaded param amount | non-SP store before the reload sub-word store to the frame | flags one instruction stale a second masked-unknown (one-seed limit) | a merge point before the site movt onto an unknown prior Plus two that pin exact ARM semantics in the direction where getting them wrong ELIDES: register shift amounts are Rm<7:0> (an amount >= 32 yields 0, so a `& 31` model computes 256 and would decline — the test only passes under the correct rule), and MOVT is read-modify-write. GATE POTENCY — every guard was mutated and the suite confirmed RED: phase 2 disabled -> RED (3 tests) shift amount `& 0xff` -> `& 31` -> RED calls preserve registers -> RED flags survive across instructions -> RED unplaceable store keeps slots -> RED unmodeled op keeps slots -> RED seed allows any mask width -> RED movt assumes a zero prior -> RED (only after adding the unknown-prior case; the first draft of this test was BLIND to it) HONEST RESIDUAL, recorded in the test and the struct doc rather than papered over: a merge point is guarded TWICE — by `ends_straight_line_prefix` and, independently, by `Label` falling into the unmodeled-op arm that drops everything to top. Removing EITHER guard alone stays green, so that test pins the class, not the specific guard. END-TO-END NEGATIVE CONTROL (run, not assumed): force-eliding the load-bearing mask takes gpio-thin to 490 B / 0 masks — exactly the pre-#682 unsound lowering. `gpio_thin_846_differential.py` stayed GREEN at 75/75; only `i32_shift_mask_682_differential.py` went red, with 6 mismatches. See the next commit for why the gpio gate is blind here and what was done about it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… record what the gate CANNOT see The differential ran gpio_configure with mode hardcoded to 0x3, so the whole mode domain of the driver's largest function — and its second shift chain, `mode << 2` feeding the CRL/CRH mode-table lookup — went unexercised. This lane changed that function's bytes, so leaving its coverage flat would be a quiet regression. 75 -> 285 checks, 105 -> 525 trace events. More important than the count: this gate is BLIND to the one thing it looks like it should catch, and that is now written down in the script, the CI step, and the claims ledger instead of being left for a future reader to assume. Force-eliding gpio_configure's surviving `and ip,r8,#31` produces the pre-#682 490 B object — the exact miscompile #682 exists to prevent — and this differential reported 285/285 MATCHING. Measured, not argued. The reason is structural, so no input to this driver can discriminate it: - for mode <=u 6, `mode*4 <= 24` is already < 32 and the mask is a no-op; - for mode >u 6, the driver's own `cmp r6,#6 ; ite hi` REPLACES the table lookup with 0, discarding the shift's result entirely. The conditions are complementary, so the site is unobservable at the WASM level here. The real guard for it is the analysis DECLINING (it sees a top frame-reloaded param) plus i32_shift_mask_682_differential.py, which goes red with 6 mismatches under the same force-elide. The mode sweep's value is real but narrower than "it covers that site": it exercises the mode domain and would catch a regression in the mode-table lookup, the `>u 6` guard, or the CRL/CRH bit-position chain. Ledger + CI pin moved together with the count (75/75 -> 285/285), plus the `# ci-checks:` floor. FEATURE_MATRIX and its template said "two gpio-thin CRL/CRH sites still need relational ranges" — now false, so the claim is updated rather than the ledger loosened. claim_check 43/43. 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> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…-ranges lane Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…otency re-proven The scope caveat I pinned into SYNTH-GPIO-846-ORACLE-CI-WIRED spanned two lines, so it encoded the CI comment's WRAP COLUMN as well as its text: a harmless re-wrap would have reddened the gate for no reason. Replaced with a single-line substring. Potency re-proven both ways rather than assumed (the ledger entry in a PR titled "the gate that could not see it" had better not be decorative itself): clean = 43/43; changing one word inside the pinned string = 42/43 RED. So the evidence item genuinely binds to ci.yml's bytes. Also confirmed the DEBUG binary CI actually runs reproduces the headline numbers — every measurement in the PR body came from the release binary: `SYNTH=target/debug/synth` gives .text=494 B, 1 mask, 285/285, 525 trace events, identical to release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
avrabe
added a commit
that referenced
this pull request
Aug 14, 2026
…e 4 shipped (#979) Two things. The re-grades are bookkeeping; the back-fill is a finding. RE-GRADED to `implemented` (work merged on main): RQ-57-COUNTPARAMS #970 (#974) ARM+RV32 conditional-param miscompile RQ-57-GPIO #846 (#976) gpio-thin 502 -> 494 B RQ-57-ARMSEM #923 (#975) ArmSemantics no-oped 87 of 222 ops RQ-57-MCDC #912 (#978) MC/DC over synth's own decision logic RQ-57-BACKFILL: the back-fill was NOT performed, on evidence. 263 of 288 artifacts carry no `release:`. The prescribed derivation — "the first tag containing the artifact" — RUNS FINE (0 undecidable, 24 distinct tags) and answers the WRONG QUESTION. It yields when an artifact ENTERED THE PLAN; `release:` means the release the work is TARGETED AT or SHIPPED IN. The file supplied its own control case, which is what settles it: VCR-RA-001, hand-set release: v0.24.0 <- when the work shipped VCR-RA-001, mechanical rule v0.11.30 <- introducing commit's tag It is the ONLY artifact in verified-codegen-roadmap.yaml that already carried a `release:`, and the rule contradicts it. Sweeping the other 32 would have written 32 false values with the one correct value sitting beside them as the disproof. Scale, had it been applied blindly: 190 of the 263 resolve to v0.1.1 — the initial import, i.e. the standing requirement base (architecture, stakeholder and system requirements, component model, target platforms). Tagging those v0.1.1 asserts the whole foundational base was targeted at the first tag, and makes "what is in v0.1.1?" return 190 artifacts including work that shipped forty releases later. That is the corruption of the readiness query this artifact exists to prevent — so the artifact's own guardrail ("stays unassigned rather than guessed", the #911 lesson applied to planning data) decides it. CONVENTION, now documented in docs/release-process.md so the absence stops being re-filed as an unfinished chore: * per-release plan artifacts carry `release:` (they are work items; they do) * standing artifacts carry it ONLY where the shipping release is known, as VCR-RA-001 does * setting it on a standing artifact is a per-artifact judgement with CHANGELOG evidence, never a sweep A missing `release:` is a justified state. A wrong one is worse than a missing one. rivet: 50 errors / 166 warnings before AND after — unchanged. claim_check 43/43. Refs #912 Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
avrabe
added a commit
that referenced
this pull request
Aug 14, 2026
* chore(release): v0.57.0 assembly — "the checkers were the defects" Nine artifacts. In five of them the bug was in the machinery that checks the compiled code, not in the compiled code: #975 ArmSemantics silently no-oped 87 of 222 ops — a Rocq-proved, default-on rotl rule was "validated" by a model that executed neither of its instructions #976 the gpio differential CANNOT discriminate the miscompile it guards — complementary conditions, so no input to that driver can #969 writes_sp claimed exhaustiveness over a wildcard absorbing 175 of 222 #967 the "9 unattributed branches" were manufactured by witness's own hardcoded divergence text #979 the prescribed release: back-fill would have written 32 false entries, with the one correct pre-existing value beside them as the disproof The unifying property is that each of those checks COULD NOT FAIL. This release makes them able to fail and proves it by making them fail on purpose. Also fixed, and the most severe item: #974 — a conditionally-written parameter was demoted to a zero-init local on ARM and RISC-V. Exit 0, no decline, wrong code; on RISC-V it reads an UNINITIALISED stack slot (0xDEADBEEF under a poisoned stack), an information-disclosure shape. ARM behaves identically — which the issue predicted otherwise, and only execution settled. Release surfaces, all four swept and checker-confirmed at 0.57.0: Cargo.toml [workspace.package] + 10 path-dep pins MODULE.bazel, npm/package.json, Cargo.lock (cargo metadata) scripts/check_version_pins.py: OK Derived artifacts regenerated (--emit-status): artifacts/status.json, docs/status/FEATURE_MATRIX.md. Claim gate: 43/43. Open by design, named not hidden: #973 (ARM select miscompile, found only because a lane compiled ARM fixtures — which CI never does), #977 (ELF-magic flake, second sighting), #938 (breaking object 0.x-minor bump, auto-merge disabled), #912 (open with four remaining: items). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * fix(release): act on the v0.57.0 cold review — 8 accuracy defects, 4 of them mine Cold review of the assembled release. Nothing blocked the tag; everything below is accuracy. Four of the eight were errors in the CHANGELOG I had just written, which is the reason the review exists. THE GENERALIZABLE FINDING, and it is pointed given this release's theme: `check_generated_fresh` byte-compares the RENDERED FEATURE_MATRIX against the TEMPLATE. `render_feature_matrix` only substitutes `{{...}}` fields, so the gate proves the render is faithful to the template — and NEVER that the template is faithful to the code. Every stale number below lives in template prose no substitution touches. In a release titled "the checkers were the defects", that is the checker that cannot fail. Three independent stale numbers survived a green 43/43. USER-FACING FALSE, verified by compiling rather than by reading: FEATURE_MATRIX listed "writing a PARAM local in a LEAF function" as a LOUD DECLINE on aarch64. #971 shipped exactly that. A leaf `local.set` on a param compiles: 32 bytes of machine code, exit 0. Also corrected in the same row: homing is no longer non-leaf-only, and the float-param decline widened with it. Fixed in the TEMPLATE (the render is generated) + regen. MY CHANGELOG ERRORS: * "145 of the 175 pre-declined / 30 reachable" matched no partition. The shipped source (wcet_loops.rs:1232) says 142 give up with `true`, leaving 33. Re-derived: 142/33. Corrected. * "demoted to a zero-initialised local" is wrong for the two backends the entry is about — zero-init is gated on first-access-being-a-READ, and in the cond-write shape the first access IS the write, so nothing initialises the slot. That is WHY it reads poison; the old wording made an information-disclosure bug sound like a benign wrong value, and contradicted the entry's own next sentence. * "Nine artifacts" — there are ten, and RQ-57-DOCSWEEP (#946/#968) had NO CHANGELOG entry at all despite touching CLAUDE.md, coq/STATUS.md, PROJECT_STATUS.md, the matrix template and eight source files. Added. * "Five in-tree oracles took that opt-in" — eight scripts plus three Rust tests. All eight carry floors, but `i64_param_518_riscv_loudskip`'s is `compiles >= 1`, which is a floor and NOT the "tight" one the paragraph claimed for the set. Named rather than folded into the claim. STALE COUNTS (the template-prose class above): ORACLE_WIRING.md, the matrix template and claims.yaml all said "137 oracles / 295,621 emulator entries". Re-derived independently — and the reviewer's number and mine agree exactly: 144 oracles / 296,059. Both `count-min` pins moved 137 -> 144 with them (same `emulations >=` pattern, two sibling claims); the pinned verbatim texts moved too, or the ledger would have gone red against its own corrected doc. REVERSE STALENESS (a doc calling SHIPPED work missing): `synth verify` declines shift rules citing "SMT modeling of the variable-shift register encoding is an open gap". #975 CLOSED that gap — it modelled LslReg/LsrReg/AsrReg/RorReg as Rm<7:0> (ARMv7-M A7.7.68/70/12/117) and moved five lowerings Invalid -> Verified. Both comments corrected to say what is true: the modelling gap is closed, the remaining decline is a WIRING residual. Behaviour deliberately unchanged — rewiring the rule table is a verification-surface change, not release assembly. Filed as #981. ARTIFACT: RQ-57-PROVGAP still asserted "9 object branches with no WASM origin" as fact while its own PR disproved it. Outcome recorded, as RQ-57-BACKFILL already did. docs/architecture/CRATE_STRUCTURE.md said 18 crates; there are 19. A RECURRENCE — PROJECT_STATUS.md cites this exact drift as why it was gutted in the #946 sweep, and one file over it was live again. Gates after: claim_check 43/43, check_version_pins OK at 0.57.0, cargo check -p synth-cli rc=0. Refs #980, #981 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * style: rustfmt the #975 decline-reason comment (indent 14 -> 12) My own miss: I ran cargo check on the edited file but not cargo fmt, and Format is a required context. The comment content is unchanged — only the indentation rustfmt wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <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.
Closes the last recoverable part of #846 (artifact RQ-57-GPIO): gale's
gpio-thindriver goes 502 B → 494 B, and the two CRL/CRH masks the issuecalled "recoverable in principle, but not soundly today" are now proven.
Measured, on gale's real 656 B
gpio.loom.wasm.textand r12,#0x1fSYNTH_SHIFT_MASK_ELIDE=0)Baseline re-measured on current main first — 502 B, matching the issue.
494, not the issue's 498. Each mask is a 4-byte
and.wand the rewrittenlsl.w rD,rN,rKis the same width aslsl.w rD,rN,ip, so two elisions are−8 B. The issue's "498 B remaining headroom" was arithmetically inconsistent
with its own
502 − 3×4 = 490identity — 498 is one mask, not two. 490remains unreachable and unsound, exactly as #846 established.
What the analysis is
The two survivors are the STM32 CRL/CRH idiom
sel ? p*4 : p*4−32wherep = pin & 0xfandsel = (p < 8). The result is< 32on both arms, butonly through the correlation between
seland the range ofp*4. Novalue-range domain can see it:
p*4 − 32wraps to0xFFFFFFE0forp = 0,so the meet of the two arms is all of
u32andLt32Factscorrectly declines.Rather than reach for a relational domain — or, worse, a syntactic
sel ? x : x−32matcher, exactly the patch-accretion the north star forbids —phase 2 makes the correlation concrete.
p = pin & 0xfcan only take the16 submask values of
0xfwhateverpinis; evaluate the straight-line prefixonce per value and
selandp*4are computed together in each case, so theimpossible combination (
sel = 1withp*4 ≥ 32) is never considered. Botharms land in
[0,28]in all 16 cases.The abstraction over-approximates the reachable value set, so "every case
< 32" implies "always< 32" — the #682 invariant, discharged by enumerationinstead of by dataflow. Unmodeled ops drop to ⊤; one seed only (a second
masked-unknown is left ⊤ rather than Cartesian-producted — documented precision
limit, not a soundness one); straight-line prefix only; calls preserve tracked
[SP,#off]slots (the AAPCS premise synth's own allocator already depends on)but drop every register; flags trusted for exactly the instruction after the
Cmp; register shift amounts areRm<7:0>, notRm & 31;Movtis RMW withno defaulted prior; R12 lives exactly one instruction. Phase 2 runs on phase
1's output so no conclusion can rest on a
movwphase 1 deleted.The surviving mask is the load-bearing one
Verified by disassembling the shipped object, not assumed. At
0xd8:and ip, r8, #0x1fonlsl.w r8, r6, #2withr6 = ldr.w [sp,#0x24]— aframe-reloaded raw param. For
mode ≥ 8,mode << 2 ≥ 32; eliding it isthe #682 miscompile. It stays because the reload is ⊤.
Gate potency — and a gate that was blind
Every guard was mutated and the suite confirmed red: phase 2 disabled,
& 0xff → & 31, calls preserving registers, flags surviving instructions,unplaceable stores keeping slots, unmodeled ops keeping slots, seed width
unbounded,
movtassuming a zero prior. Themovtcontrol survived thefirst draft of its test — the test was blind to it, and the unknown-prior
case was added because of that.
End-to-end negative control, run rather than assumed: force-eliding the
load-bearing mask produces the pre-#682 490 B / 0 masks object — the exact
miscompile — and
gpio_thin_846_differential.pyreported every checkmatching, both at its old 75 and at its new 285. Only
i32_shift_mask_682_differential.pywent red (6 mismatches).The reason is structural, so no input to this driver can discriminate it: for
mode ≤u 6the amountmode*4 ≤ 24is already< 32; formode >u 6thedriver's own
cmp r6,#6 ; ite hidiscards the shift's result. The conditionsare complementary. That is now written into the script, the CI step and the
claims ledger rather than left for a future reader to assume.
Coverage
gpio_configureran withmodehardcoded to0x3, leaving the whole modedomain of the driver's largest function unexercised — and this PR changes that
function's bytes. Added a mode sweep: 75 → 285 checks, 105 → 525 trace
events. Ledger pin, CI grep and the
# ci-checks:floor moved with it.Gates
cargo test --workspace— 2796 passed, 0 failed; all 10 frozen anchorsunchanged (phase 2 does not fire on control_step / flight_seam /
flight_seam_flat / oracle_001), so no refreeze
cargo clippy --workspace --all-targets -- -D warnings— cleancargo fmt --check— cleanclaim_check— 43/43gpio_thin_846_differential.py— 285/285, 494 Bi32_shift_mask_682_differential.py— green on both pathsFEATURE_MATRIX (and its template) said "two
gpio-thinCRL/CRH sites stillneed relational ranges" — now false, so the claim was updated rather than
the ledger loosened.
Refs #846.
🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L