Skip to content

Commit 80ba314

Browse files
hyperpolymathclaude
andcommitted
feat(verify): Tier-2 access-typing self-certification (per-site type/width/offset)
The L2 verifier now independently DECODES the code section and checks that each pinned access site lands on a load/store of the target field's exact type, width, and static offset, in-region — discharging proposal 0002's deferred `AccessSiteMisalignment` at decode time, with zero producer trust. Producer (codegen): - AccessSite.offset:u32 -> instr_index:Option<usize>: a site pins the body instruction index of its load/store, or None for declared-only. - emit() writes the pinned index (or the ACCESS_SITE_UNPINNED sentinel) into the access-sites carrier; the parser's real reader/writer lowering pins Some(1)/Some(2); the 15 hand-written example01/paint-type sites become declared-only (honestly not type-checked). Verifier (typed-wasm-verify): - verify_access_typing_from_module + AccessTypingReport { type_verified, declared_only, errors }; AccessTypingError variants (type/offset mismatch, not-a-memory-op, index/region out of range, unresolvable). - WasmTy::byte_width(); the field-offset recompute mirrors codegen resolve_field (the T4 layout-equivalence lemma will prove they agree). - tw-verify prints "L2 access typing: N type-verified, M declared-only" so the assurance mode is knowable to anyone reading the output. Tests (tests/access_typing.rs): real lowering type-verifies (3/3); example01 is reported declared-only (6); five teeth (wrong width/offset, non-memory op, index OOB, out-of-region) each rejected with the matching variant; plus a correct-control. Mutation-checked: a width-blind verifier fails the width teeth. No regression across the existing 153-test suite. Docs: PROOF-NEEDS.md gains the codegen->verified-wasm assurance-ladder reconciliation (T1 execution gate landed, T2 here, T3-T5 open). Notes: - Corrected pre-existing owner-Copyright header drift on verify.rs/section.rs/lib.rs under one-time owner authorization; the strict pre-commit hook is retained. - One pre-existing clippy collapsible_match in verify_from_module's MemorySection arm is left for the separate lint chore PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fd788c4 commit 80ba314

8 files changed

Lines changed: 896 additions & 28 deletions

File tree

PROOF-NEEDS.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,53 @@ compile time" (true) and "here is a lemma proving it is forbidden"
2323
claims — a reviewer asking _"where is the theorem?"_ currently has no
2424
answer to point at.
2525

26+
## RECONCILIATION 2026-06-16 (codegen climb — the second assurance axis: T1 execution gate landed, T2 in progress)
27+
28+
> **The honest summary above concerns the _Idris-model_ axis ("where is the
29+
> theorem?"). This block introduces the _codegen→verified-wasm_ axis — the
30+
> assurance that the Rust producer's emitted bytes are what an independent
31+
> verifier accepts and a wasm engine executes correctly. The two axes are
32+
> complementary: the Idris ABI proves the _protocol_; the climb proves the
33+
> _running implementation honours it_. (Module count is now 22, not the
34+
> "eleven" of the 2026-04-13 summary — later reconciliations supersede it.)**
35+
>
36+
> **The assurance ladder (`.twasm` source → IR → emitted wasm → verified):**
37+
>
38+
> | Tier | Obligation | Status (2026-06-16) |
39+
> |---|---|---|
40+
> | **T1** Execution gate | emitted store/load bodies _compute_ the intended memory semantics (width, offset, sign/zero-extension, no-clobber) |**landed**`crates/typed-wasm-codegen/tests/execute_lowering.rs` instantiates the lowered module in **wasmi** (pure-Rust, runs in CI without a system wasmtime) and round-trips every scalar width + proves a narrow store touches **only** its own bytes. Proven non-vacuous by mutation (reintroducing a narrow-width bug fails the gate). |
41+
> | **T2** Verifier self-certification | the verifier independently _decodes the code section_ and checks each pinned access lands on a load/store of the field's exact type/width/offset, in-region | 🟡 **in progress** (this session). Producer half: access sites pinned to a body-instruction index (`AccessSite.instr_index: Option<usize>`; the hand-built `example01` / paint-type representative sites are carried **declared-only**, honestly _not_ type-checked). Verifier half: `verify_access_typing_from_module` + `AccessTypingReport { type_verified, declared_only, errors }`, surfaced by `tw-verify`. Closes proposal-0002's deferred `AccessSiteMisalignment`. |
42+
> | **T3** Parser totality | the `.twasm` parser never panics on any input | ⚠️ **partial** — v0 hardened against malformed/truncated input (PR #168), but **3 pre-existing arithmetic panics remain** (div-by-zero / overflow in `parse_array_size_expr` + `compute_region_byte_size`). Deferred to a hardening PR. |
43+
> | **T4** Layout-equivalence lemma | `Layout/ABI.idr` field offsets **=** Rust `resolve_field` offsets |**open** — two independent implementations of the same field-offset arithmetic, with no proof they agree. The next formal bridge. |
44+
> | **T5a** Verifier↔spec link | the Rust verifier _refines_ `VerifierSpec.idr` |**trusted-base** — ADR-0005 pins audit invariants (I1 provenance / I2 witness fidelity / I3 module shape) at the `MkTrustedFixture` boundary; the Rust verifier remains the trusted base. Constructive soundness (path 2) and WasmCert tie-back (path 1) are future superseding ADRs. |
45+
> | **T5b** Wasm-semantics tie-back | emitted bytes mean what we claim under a mechanised wasm semantics (WasmCert-Isabelle/Coq) |**open** — multi-week external dependency. T1's execution gate is the empirical stand-in. |
46+
>
47+
> **Climb position.** Front-end staircase (languages → typed-region IR) is
48+
> stop-gapped by the `.twasm` parser seam (ADR-0004 _Proposed_; real
49+
> AffineScript/ephapax AST bridge deferred). Back-end staircase (IR → verified
50+
> wasm) is where the climb is: Steps 1 (field readers, #169) + 2 (field writers
51+
> + width-exact scalar accesses, #171) merged; the T1 gate landed alongside;
52+
> T2 self-certification is the active work, then the T4 layout lemma, then the
53+
> T5a verifier↔spec link.
54+
>
55+
> **Ground truth re-verified 2026-06-16:** `idris2 --build src/abi/typed-wasm.ipkg`
56+
> → exit 0, **22/22 modules**; zero `believe_me` / `postulate` / `sorry` /
57+
> `Admitted` / `assert_total` / holes (every scanner hit is a docstring
58+
> disclaimer). The `docs/proof-debt.md` zero-debt invariant holds.
59+
>
60+
> **Governance note.** The whole `crates/typed-wasm-verify/src/` (`verify.rs`,
61+
> `section.rs`, `lib.rs`) carried the SPDX tag but was **missing the literal
62+
> owner `Copyright` line** — invisible drift that trips the strict pre-commit
63+
> hook on any commit touching those files (so T2's verifier half was
64+
> commit-blocked). Surfaced and corrected under a one-time owner authorisation
65+
> this session; the hook is kept strict.
66+
>
67+
> **No `believe_me` / `assert_total` / `postulate` / `sorry` / `assert_smaller`
68+
> introduced; `%default total` preserved. T1 is an executable test, not a proof
69+
> obligation; T2 is a decode-time check _inside_ the (trusted-base) Rust
70+
> verifier — it strengthens the trusted base's self-consistency, it does not
71+
> reduce the trusted base.**
72+
2673
## RECONCILIATION 2026-06-16 (A15/A16 — estate-axis accommodation)
2774

2875
> L11/L12/Echo have been **cross-documented and mirrored to the canonical

crates/typed-wasm-codegen/src/lib.rs

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
//! v0 emits type-correct representative bodies, not the full lowering.
3636
3737
use typed_wasm_verify::section::{
38-
build_access_sites_section_payload, AccessSiteEntry, NO_TARGET_REGION,
38+
build_access_sites_section_payload, AccessSiteEntry, ACCESS_SITE_UNPINNED, NO_TARGET_REGION,
3939
};
4040
use typed_wasm_verify::{
4141
build_ownership_section_payload, build_regions_section_payload, CrossError, FieldEntry, FieldKind,
@@ -269,18 +269,22 @@ pub struct Import {
269269
pub results: Vec<Wty>,
270270
}
271271

272-
/// A typed access site: the load/store at `offset` (bytes into the function
273-
/// body) reaches `region`'s `field`. Recorded into `typedwasm.access-sites`.
272+
/// A typed access site: a load/store reaching `region`'s `field`,
273+
/// recorded into `typedwasm.access-sites`.
274274
#[derive(Debug, Clone, Copy)]
275275
pub struct AccessSite {
276276
/// Index into [`Module::regions`].
277277
pub region: usize,
278278
/// Index into the target region's field list.
279279
pub field: usize,
280-
/// Instruction byte offset within the function body. v0 uses
281-
/// representative offsets; the verifier does not check offset
282-
/// alignment (proposal 0002 defers `AccessSiteMisalignment`).
283-
pub offset: u32,
280+
/// The 0-based instruction index (operator position) in the function
281+
/// body that this site pins — the load/store the verifier's L2
282+
/// access-typing pass checks against the field's type/width/offset.
283+
/// `None` = declared-only: the producer asserts the field is reached
284+
/// but does not pin a concrete instruction (representative /
285+
/// hand-written bodies). Emitted as [`ACCESS_SITE_UNPINNED`] on the
286+
/// wire. Closes proposal 0002's deferred `AccessSiteMisalignment`.
287+
pub instr_index: Option<usize>,
284288
}
285289

286290
/// A function: a typed signature, a body, and the typed access sites its
@@ -444,7 +448,12 @@ pub fn emit(module: &Module) -> Vec<u8> {
444448
for site in &func.accesses {
445449
access_entries.push(AccessSiteEntry {
446450
func_idx: global_idx,
447-
instruction_byte_offset: site.offset,
451+
// The wire slot carries the pinned instruction index, or
452+
// the unpinned sentinel for declared-only sites.
453+
instruction_byte_offset: site
454+
.instr_index
455+
.map(|k| k as u32)
456+
.unwrap_or(ACCESS_SITE_UNPINNED),
448457
region_id: site.region as u32,
449458
field_id: site.field as u32,
450459
});
@@ -582,7 +591,7 @@ pub fn example01() -> Module {
582591
accesses: vec![AccessSite {
583592
region: 1,
584593
field: 0,
585-
offset: 6,
594+
instr_index: None,
586595
}],
587596
export: true,
588597
},
@@ -595,7 +604,7 @@ pub fn example01() -> Module {
595604
accesses: vec![AccessSite {
596605
region: 1,
597606
field: 0,
598-
offset: 6,
607+
instr_index: None,
599608
}],
600609
export: true,
601610
},
@@ -610,12 +619,12 @@ pub fn example01() -> Module {
610619
AccessSite {
611620
region: 2,
612621
field: 2,
613-
offset: 6,
622+
instr_index: None,
614623
}, // Enemies.target
615624
AccessSite {
616625
region: 1,
617626
field: 0,
618-
offset: 9,
627+
instr_index: None,
619628
}, // Players.hp
620629
],
621630
export: true,
@@ -629,7 +638,7 @@ pub fn example01() -> Module {
629638
accesses: vec![AccessSite {
630639
region: 2,
631640
field: 4,
632-
offset: 2,
641+
instr_index: None,
633642
}],
634643
export: true,
635644
},
@@ -646,7 +655,7 @@ pub fn example01() -> Module {
646655
accesses: vec![AccessSite {
647656
region: 1,
648657
field: 2,
649-
offset: 6,
658+
instr_index: None,
650659
}],
651660
export: true,
652661
},
@@ -745,7 +754,7 @@ pub fn paint_type_tile() -> Module {
745754
Op::LocalGet(4), Op::Drop,
746755
],
747756
accesses: vec![
748-
AccessSite { region: 2, field: 1, offset: 0 },
757+
AccessSite { region: 2, field: 1, instr_index: None },
749758
],
750759
export: true,
751760
},
@@ -761,7 +770,7 @@ pub fn paint_type_tile() -> Module {
761770
Op::I32Const(0),
762771
],
763772
accesses: vec![
764-
AccessSite { region: 2, field: 1, offset: 0 },
773+
AccessSite { region: 2, field: 1, instr_index: None },
765774
],
766775
export: true,
767776
},
@@ -781,7 +790,7 @@ pub fn paint_type_tile() -> Module {
781790
Op::LocalGet(6), Op::Drop,
782791
],
783792
accesses: vec![
784-
AccessSite { region: 2, field: 1, offset: 0 },
793+
AccessSite { region: 2, field: 1, instr_index: None },
785794
],
786795
export: true,
787796
},
@@ -796,7 +805,7 @@ pub fn paint_type_tile() -> Module {
796805
Op::LocalGet(1), Op::Drop,
797806
],
798807
accesses: vec![
799-
AccessSite { region: 2, field: 1, offset: 0 },
808+
AccessSite { region: 2, field: 1, instr_index: None },
800809
],
801810
export: true,
802811
},
@@ -888,9 +897,9 @@ pub fn paint_type_layer() -> Module {
888897
Op::I32Const(0),
889898
],
890899
accesses: vec![
891-
AccessSite { region: 2, field: 0, offset: 0 },
892-
AccessSite { region: 2, field: 1, offset: 0 },
893-
AccessSite { region: 2, field: 2, offset: 0 },
900+
AccessSite { region: 2, field: 0, instr_index: None },
901+
AccessSite { region: 2, field: 1, instr_index: None },
902+
AccessSite { region: 2, field: 2, instr_index: None },
894903
],
895904
export: true,
896905
},
@@ -905,7 +914,7 @@ pub fn paint_type_layer() -> Module {
905914
Op::I32Const(0),
906915
],
907916
accesses: vec![
908-
AccessSite { region: 2, field: 1, offset: 0 },
917+
AccessSite { region: 2, field: 1, instr_index: None },
909918
],
910919
export: true,
911920
},
@@ -921,7 +930,7 @@ pub fn paint_type_layer() -> Module {
921930
Op::I32Const(0),
922931
],
923932
accesses: vec![
924-
AccessSite { region: 2, field: 1, offset: 0 },
933+
AccessSite { region: 2, field: 1, instr_index: None },
925934
],
926935
export: true,
927936
},

crates/typed-wasm-codegen/src/parser.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,8 @@ impl<'a> Parser<'a> {
764764
let accesses = vec![crate::AccessSite {
765765
region,
766766
field: field_idx,
767-
offset: 0, // verifier does not check the byte offset (see verify.rs)
767+
// Body is [LocalGet, load]: the typed load is instruction index 1.
768+
instr_index: Some(1),
768769
}];
769770
Some((body, accesses))
770771
}
@@ -847,7 +848,8 @@ impl<'a> Parser<'a> {
847848
let accesses = vec![crate::AccessSite {
848849
region,
849850
field: field_idx,
850-
offset: 0, // verifier does not check the byte offset (see verify.rs)
851+
// Body is [LocalGet, push, store]: the typed store is index 2.
852+
instr_index: Some(2),
851853
}];
852854
Some((body, accesses))
853855
}

0 commit comments

Comments
 (0)