Skip to content

RQ-64-CFOBLIG (#1057): Block/End — WASM half landed, ARM obligation stated with its obstruction kernel-pinned (done-when branch B) - #1187

Merged
avrabe merged 4 commits into
mainfrom
proof/cfoblig-1057
Sep 7, 2026
Merged

RQ-64-CFOBLIG (#1057): Block/End — WASM half landed, ARM obligation stated with its obstruction kernel-pinned (done-when branch B)#1187
avrabe merged 4 commits into
mainfrom
proof/cfoblig-1057

Conversation

@avrabe

@avrabe avrabe commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

RQ-64-CFOBLIG (#1057) — done-when branch B, taken deliberately

The artifact's done-when has two acceptable branches: a kernel-checked Block/End correspondence obligation reflected semantically in proof-inventory.json, OR the obstruction stated precisely enough to be VCR-ISA-001 input — with no existence-only theorem named *_correct. This PR takes branch B, and lands the half of the obligation that was never blocked.

Why B. The correspondence cannot be discharged against ArmSemantics.v as it stands, for reasons that are now kernel-checked facts rather than sentences. The fragment that IS statable today (forward, void, index-granular) would be blind to every closed control-flow miscompile (#483, #500, #509, #740, #930) — a green Qed there is exactly the overclaim the artifact forbids, so it was deliberately not written.

What the kernel checked (bazel test //coq:verify_proofs: rocq_proofs PASSED executed after each step, vcr_sel_rules_coverage PASSED; 630 -> 645 Qed / 2 Admitted, zero admit., nothing named *_correct in the new files)

  1. WasmInstructions.v gains Block / Loop / Br : nat / End_, one constructor per shipped WasmOp variant in the FLAT op-stream form the compiler consumes. End_ only because End is a reserved Rocq keyword. exec_wasm_instr declines all four via its catch-all; compile_wasm_to_arm (the one exhaustive match the addition broke, per the kernel) gets LOUD-DECLINE [UDF 255] arms with the reason written down.
  2. WasmBlocks.v, 11 Qed, WASM-only by design. split_block (nesting-aware scan to the matching End_), unwind_to (a taken branch restores the target block's entry stack — br is stack-polymorphic, the arity-0 half of the Direct selector miscompiles value-returning br_table — carried value dropped (affects --relocatable/shipped path) #509/thumb-2: br/br_if out of an if is not taken when its value operand is a block that branches (labels.wast) — silent wrong value, exit 0 #930 value class), the fuel-bounded structured executor exec_wasm_blocks (Block consumes WBranch 0 and resumes after End_, Loop re-enters, deeper branches propagate decremented, Br is an unconditional WBranch, a stray End_ is None). exec_wasm_blocks_structured_free proves it IS exec_wasm_seq on programs without the four constructors, so the executor every existing theorem and brif_correct are stated against is unchanged by construction. Computed non-vacuity: the SAME block, differing only in the branch constant, lands with an unwound empty stack or with a 7 on top; a counting loop reaches local 0 = 0; with fuel for one pass it declines rather than reporting a one-iteration result.
  3. BlockEndObligation.v, 4 Qed, NO correctness theorem. Model-gap pins, each of which stops compiling when VCR-ISA-001 closes its gap: exec_program_pc_B_not_taken / _BL_not_taken (the index executor runs an unconditional branch / a call as a fall-through, writing only the PC register), exec_program_pc_bcond_backward_falls_through (for off < 0 the taken and not-taken targets coincide), and arm_count_loop_body_runs_once (ADD;CMP #10;BNE -3 from R0 = 0 exits with R0 = 1 under exec_program_br — where WasmBlocks.v's executor iterates).

The obstruction, as VCR-ISA-001 input (file header, coq/STATUS.md section, roadmap entry)

# Obstruction Evidence Would have missed
O1 exec_program_pc intercepts only BCondOffset; B/BL write the PC REGISTER while the executor advances its INDEX — two disjoint program counters. Shipped Br = B label; gale's 31 Call rows share the shape kernel lemma every br, every call
O2 pc + 1 + Z.to_nat off, off < 0 -> pc + 1: a loop back-edge is a silent fall-through; fuel assumes forward-only kernel lemma + concrete loop every loop
O3 model pc is an instruction INDEX; arm_backend.rs resolves labels to BYTE offsets by summing the real encoder's sizes (Label = 0 B) until 16/32-bit widths converge measured in the shipped resolver #483 (mid-instruction landing), #740 (halved B<c>.W)
O4 state_correspondence (Compilation.v:692) is defined and used by ZERO theorem statements; every Qed is one instruction under fixed-register hypotheses grep: 1 definition / 0 statements anything spanning a block body
O5 the arity-1 join register is chosen lazily at the first branch edge (result_reg, edge_value_move); End emits a MOV only then — an atomic compile_wasm_to_arm End_ = [] would be the #1021 popcnt shape measured in select_with_stack.rs #509, #930

Supply list, in dependence order: (a) a byte-addressed executor with an encoder-derived size function admitting negative offsets, in which B/BL/BCondOffset are all executor-visible — O1, O2, O3 are ONE decision, not three; (b) step-counted fuel and the re-check of every exec_program_pc site (ArmSemantics 20, CorrectnessI32 27, CorrectnessBrIf 11, Compilation 1); (c) a simulation relation preserved across instructions; (d) blocktype arity and the join register in the model.

Reflected semantically in proof-inventory.json

Block/Loop/Br/End move from unmodeled to absent (modeled, unproven): summary modeled 138 -> 142, unmodeled 141 -> 137, absent 0 -> 4; qed 138 and result_correspondence 63 unchanged; the BrIf row byte-identical. One tier up, still uncovered for a consumer — nothing in the row can be read as coverage.

The join gained its ONE Rocq-keyword escape (End_ -> End). Red-first: the unpatched generator refused End_ with exit 1 on this tree. constructor_op strips exactly one underscore from a stem in ROCQ_RESERVED_WORDS = {End} and nothing else — Foo_ still refuses, so the escape cannot grow into the alias table the join exists to forbid. scripts/test_proof_inventory.py (7 tests) drives both branches plus the real tree's rows and is wired beside the freshness check in the claim-check job.

Ledger and docs moved together

claims.yaml: rocq_qed 630 -> 645 (with CLAUDE.md and coq/STATUS.md), "modeled": false 141 -> 137, and new SYNTH-BLOCKEND-OBSTRUCTION-1057 — exactly 4 Qed in the obstruction file, zero *_correct names and zero admits across the two new files, exactly 4 absent rows, the decision quoted from STATUS.md. Mutation-verified, each planted then restored: a planted Lemma planted_block_correct -> red; a planted extra Qed. -> red (the fields-equal cross-check caught it independently); the End row hand-flipped absent -> qed -> red (derived 3 != documented 4); restored -> 62/62 hold.

coq/STATUS.md: dated header entry, Total 645 / 2, per-file rows, the DSL-coverage control-flow row corrected (br_if is model-only via brif_correct; the pinned DSL-served total untouched), and a new "Block/End correspondence — obligation stated, obstruction pinned" section. artifacts/verified-codegen-roadmap.yaml: VCR-ISA-001 gains the dated input paragraph (no moving Qed figure cited; S-rule citations unchanged at 34). RQ-64-CFOBLIG.yaml: proposed -> implemented on branch B, verified-by written from measurements, names no PR and no live pinned value.

Gates re-derived on the merged tree (after rebasing over #1186)

claim_check.py claims.yaml 62/62 · proof_inventory.py --check 0 · test_proof_inventory.py 7/7 · status_evidence_check.py 0 failures, 7/7 window commits attributed, floor-prose restated 0 times · oracle_wiring_check.py 0 · rivet validate 40 cross-repo errors with AND without the roadmap paragraph, 0 ours under CI's filter · bazel test //coq:verify_proofs 2/2.

Not claimed

Any Block/End correspondence with ARM code; value-carrying blocks; If/Else/Return/BrTable/Call (unmodeled rows, unchanged). Deferring the ARM half a second time, now with the obstruction kernel-pinned, is the outcome the artifact named as acceptable.

Refs #1057

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 4 commits September 7, 2026 15:10
…and the structured executor exec_wasm_blocks (WASM half, 11 Qed, no ARM claim)

The WASM side of the Block/End correspondence obligation. Four constructors
mirror the shipped WasmOp::Block / Loop / Br(u32) / End in the FLAT op-stream
form the compiler consumes (End_ because `End` is a reserved Rocq keyword —
it names WasmOp::End and nothing else). exec_wasm_instr declines all four via
its catch-all (a block boundary has no per-instruction meaning; a Br is
always a transfer — #615 class); compile_wasm_to_arm gets LOUD-DECLINE arms
([UDF 255]) with the reason written down.

WasmBlocks.v: split_block (nesting-aware forward scan to the matching End_),
unwind_to (a taken branch restores the target block's entry stack — the
arity-0 value-dimension half of the #509/#930 class), and the fuel-bounded
structured executor exec_wasm_blocks: Block consumes WBranch 0 and resumes
after End_, Loop re-enters, deeper branches propagate decremented, Br is an
unconditional WBranch, BrIf is exec_wasm_seq's decision, a stray End_ is
None. Kernel-checked:
  - exec_wasm_blocks_structured_free: on programs without the four new
    constructors the structured executor IS exec_wasm_seq — the executor the
    600+ existing theorems and brif_correct are stated against is unchanged
    by construction;
  - split_block_block_free + the Block/Loop/Br unfolding lemmas the ARM-side
    obligation is stated against;
  - computed non-vacuity: the SAME block, differing only in the branch
    constant, lands with and without the body's tail (0-length unwound stack
    vs a 7 on top); a counting loop iterates to local 0 = 0; and with fuel
    for one pass it declines rather than reporting a one-iteration result.

Deliberately WASM-only: nothing here is a correspondence claim, no theorem
is named *_correct, no Admitted/admit. bazel test //coq:verify_proofs: 2/2
PASSED after the constructor addition rebuilt the 16 downstream files.

Refs #1057

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…nd its obstruction PINNED in the kernel (BlockEndObligation.v, 4 Qed, no correctness claim)

Branch B of the artifact's done-when: the correspondence cannot be
discharged against ArmSemantics.v today, and this file says exactly why in
terms the checker holds rather than a status file asserts. Nothing here is
named *_correct; nothing is admitted.

Kernel-checked model-gap pins (each stops compiling when VCR-ISA-001 closes
its gap — the intended red-first signal):
  - exec_program_pc_B_not_taken / exec_program_pc_BL_not_taken (O1): the
    index executor intercepts only BCondOffset; B/BL write the PC REGISTER
    while the executor advances its INDEX by one — two disjoint program
    counters, the shipped `Br` lowers to `B label`, and gale's 31 `Call`
    rows share the shape.
  - exec_program_pc_bcond_backward_falls_through (O2): for off < 0 the taken
    and not-taken targets coincide at pc + 1 — a loop back-edge is a silent
    fall-through, whatever the condition.
  - arm_count_loop_body_runs_once (O2, concrete): ADD;CMP #10;BNE -3 from
    R0 = 0 exits with R0 = 1 under exec_program_br — the model runs a
    counting loop's body exactly once, where WasmBlocks.v's
    ex_loop_counts_down_to_zero shows the WASM side iterating.

Written down in the file header as VCR-ISA-001 input, each tied to a lemma
or a measured shipped fact: O3 index-granular pc vs byte-resolved labels
(arm_backend.rs sums the real encoder's byte sizes, Label = 0 bytes; the
closed #483 mid-instruction landing and #740 halved-offset miscompiles are
invisible at index granularity), O4 no compositional state relation
(state_correspondence is defined in Compilation.v and used by zero theorem
statements), O5 the arity-1 join register is an allocator decision
(result_reg allocated lazily at the first br edge; #509/#930 lived there).
And the supply list, in dependence order: byte-addressed executor with an
encoder-derived size function admitting negative offsets (O1+O2+O3 are one
decision), step-counted fuel with the 59 exec_program_pc sites re-checked,
a cross-instruction simulation relation, blocktype arity + join register.

The forward/void/index-granular fragment IS statable and is deliberately
NOT proven: a green Qed that could not have caught #483, #500, #509, #740
or #930 is the overclaim this artifact forbids.

bazel test //coq:verify_proofs: rocq_proofs PASSED (executed, not cached),
vcr_sel_rules_coverage PASSED. Tree: 645 Qed / 2 Admitted.

Refs #1057

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…from "unmodeled" to "absent" (modeled, unproven); the join gains its one Rocq-keyword escape, unit-driven

proof-inventory.json now says, per row, what the tree establishes for the
four structured control-flow ops: modeled:true with the real constructor
name and status "absent" — the WASM side has executable semantics
(WasmBlocks.v) and no theorem relates them to ARM code, because the ARM
model cannot state the obligation (BlockEndObligation.v pins why). That is
one tier above "unmodeled" and still uncovered for a consumer; nothing in
the row can be read as coverage. summary: modeled 138 -> 142, unmodeled
141 -> 137, absent 0 -> 4; qed 138 and result_correspondence 63 unchanged;
the BrIf row is byte-identical.

`End` is a reserved Rocq vernacular keyword, so the model spells the
constructor `End_`. The exact-name join keeps refusing every other
mismatch: `constructor_op` strips exactly one trailing underscore from a
stem in ROCQ_RESERVED_WORDS = {End} and nothing else — `Foo_` for a
non-reserved `Foo` still refuses (red-first: the unpatched generator
refused `End_` with exit 1 on this tree). The row keeps
"constructor": "End_" under "op": "End". scripts/test_proof_inventory.py
drives both branches plus the real tree's End/Block/Loop/Br and BrIf rows
(7 tests) and is wired beside the freshness check in the claim-check job.

The ledger pins on the Qed total and the modeled:false count move in the
next commit together with the docs, as CLAUDE.md requires.

Refs #1057

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…630 -> 645 Qed, modeled:false 141 -> 137), the obstruction record pinned red-capable, VCR-ISA-001 given its input, artifact implemented on branch B

claims.yaml: rocq_qed count-eq pins and the CLAUDE.md/STATUS.md headline
text 630 -> 645; the proof-inventory `"modeled": false` count 141 -> 137;
new SYNTH-BLOCKEND-OBSTRUCTION-1057 — exactly 4 Qed in
BlockEndObligation.v, zero `*_correct` names and zero admits across the
two new files, exactly 4 `absent` manifest rows, and the decision quoted
from STATUS.md — so the obstruction record cannot rot silently in either
direction. Mutation-verified, each planted then restored: a planted
`Lemma planted_block_correct` -> red; a planted extra `Qed.` -> red (the
fields-equal cross-check caught it independently); the End row hand-
flipped absent -> qed -> red (`derived 3 != documented 4`); restored ->
62/62 hold. status.json + FEATURE_MATRIX.md regenerated by --emit-status.

coq/STATUS.md: dated header entry, Total 645 / 2 (the pattern appears
exactly twice, as pinned), per-file rows for WasmBlocks.v (11, Infra,
WASM-only) and BlockEndObligation.v (4, model-gap pins), the DSL-coverage
control-flow row (br_if is model-only via brif_correct; block/loop/br/end
modeled WASM-side, ARM obligation blocked; column totals 82/13, the pinned
DSL-served 60 untouched), and a new section "Block/End correspondence —
obligation stated, obstruction pinned" with the O1-O5 table (pinned-by /
would-have-missed) and the VCR-ISA-001 supply list in dependence order.

artifacts/verified-codegen-roadmap.yaml: VCR-ISA-001 gains "INPUT FROM
RQ-64-CFOBLIG (2026-09-07, #1057)" — the five gaps and what the item must
supply; no moving Qed/Admitted figure cited, so the S-rule citation count
is unchanged (34, re-derived). rivet validate: 40 cross-repo errors with
and without the paragraph, 0 ours under CI's filter.

artifacts/release-v0.64/RQ-64-CFOBLIG.yaml: proposed -> implemented on
done-when branch B, verified-by written from the measurements (kernel
results per file, the generator's exit-1 refusal before the keyword
escape, the 7 join tests, the three pin mutations); names no PR and no
live pinned value. status_evidence_check: 0 failures, 6/6 window commits
attributed, floor-prose restated 0 times.

Refs #1057

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@avrabe
avrabe force-pushed the proof/cfoblig-1057 branch from 4b650dd to 2c0e725 Compare September 7, 2026 13:10
@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant