Skip to content

Commit 019db0e

Browse files
proofs(P59): OwnershipKind from_byte/to_byte round-trip — typed-wasm ADR-0002 carrier handshake (#277)
## Summary Closes proof-debt obligation **P59** from the comprehensive inventory: certify that the Rust-side `OwnershipKind::from_byte` / `to_byte` pair (at `src/ephapax-wasm/src/ownership.rs:55-67`) round-trips on every defined variant. The `typedwasm.ownership` wasm custom section is the carrier that crosses the ABI boundary to `hyperpolymath/typed-wasm:crates/typed-wasm-verify/src/section.rs` — the **ADR-0002 carrier handshake**. Ephapax emits, typed-wasm verifies. Both sides MUST agree byte-for-byte; otherwise the verifier observes a different ownership annotation than the producer emitted, breaking the cross-tool linearity soundness claim. ## What ships - **Three mechanically-checked tests** in `src/ephapax-wasm/src/ownership.rs`, `mod tests`: - `coq_bridge_p59_owner_byte_map_pinned` — asserts the exact wire byte for each of the four variants (0=Unrestricted, 1=Linear, 2=SharedBorrow, 3=ExclBorrow). Any change in the byte map is a wire-format break and trips this test. - `coq_bridge_p59_owner_round_trip_exhaustive` — direct case-split on the four variants; equivalent in strength to a property test on a finite domain. - `coq_bridge_p59_owner_round_trip` — the `proptest!` form of the same lemma. Mirrors the standard formulation used in `typed-wasm-verify`'s own property suite. - **`docs/coq-rust-bridge/ownership_round_trip.adoc`** — bridge spec mirroring `docs/coq-rust-bridge/is_linear_ty.adoc` (PR #273). Documents the wire format, the round-trip claim, the asymmetric decoder fallback (bytes `4..=255` → `Unrestricted`; NOT a round-trip violation since `to_byte` never emits those bytes), and cross-links to typed-wasm-verify's sister consumer. - **`src/ephapax-wasm/Cargo.toml`** — `proptest` dev-dep added from the workspace. - **SPDX flip** on `ownership.rs`: `PMPL-1.0-or-later` → `MPL-2.0` with the owner line `Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>` (estate language-policy MPL flip). ## Wire format pinned | Variant | Wire byte | |---|---| | `Unrestricted` | `0` | | `Linear` | `1` | | `SharedBorrow` | `2` | | `ExclBorrow` | `3` | ## Follow-ups (sister obligations) P59 is the simplest of a three-obligation arc; P60 + P61 ship as separate PRs: - **P60** — `ty_to_ownership_kind` monotone w.r.t. linearity. Combined with P28 (Rust ↔ Coq `is_linear_ty` bridge, landed in #273), this links the Coq linear type judgment to the emitted wire byte. - **P61** — full `OwnershipEntry` round-trip: `parse_ownership_section_payload(build_ownership_section_payload(es)) == es`. The existing `mixed_kinds_roundtrip` test exhibits one witness; P61 generalises to a property test on arbitrary entry lists. Together P59+P60+P61 form the typed-wasm ADR-0002 cross-tool soundness link. ## Test plan - [x] `cargo test -p ephapax-wasm --lib` — 89 passed, 0 failed (86 pre-existing + 3 new P59). - [x] GPG-signed commit (key `9639451754496E51D6B537CAD119017EBF695AB1`). - [x] SPDX header on touched Rust file includes `MPL-2.0` + owner line (pre-commit-hook compatible). - [x] No `formal/*.v` touched — pure Rust + AsciiDoc work. ## Cross-references - `PROOF-NEEDS.md` — proof obligation **P59** in the comprehensive inventory. - `docs/coq-rust-bridge/is_linear_ty.adoc` — sister bridge document (P28, PR #273). - `hyperpolymath/typed-wasm:crates/typed-wasm-verify/src/section.rs` — canonical reader on the verifier side. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 88b116d commit 019db0e

4 files changed

Lines changed: 274 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
= `OwnershipKind` — wire-format round-trip specification
4+
:toc:
5+
:toclevels: 2
6+
7+
This document closes proof-debt obligation **P59** from the
8+
comprehensive proof inventory: certify that the Rust-side
9+
`OwnershipKind::from_byte` / `to_byte` pair (at
10+
`src/ephapax-wasm/src/ownership.rs:55-67`) round-trips on every
11+
defined `OwnershipKind` variant.
12+
13+
The `typedwasm.ownership` wasm custom section is the carrier of
14+
ephapax's linearity discipline across the ABI boundary to
15+
`hyperpolymath/typed-wasm:crates/typed-wasm-verify/src/section.rs`.
16+
The wire format is the **ADR-0002 carrier handshake**: ephapax
17+
emits, typed-wasm verifies. Both sides MUST agree byte-for-byte;
18+
otherwise the verifier on the other side observes a different
19+
ownership annotation than the producer emitted, breaking
20+
soundness of the cross-tool linearity claim.
21+
22+
P59 is the first of a three-obligation arc:
23+
24+
* **P59** — `from_byte` / `to_byte` round-trip on the four-variant
25+
`OwnershipKind` enum (this document).
26+
* **P60** — `ty_to_ownership_kind` monotone w.r.t. linearity: any
27+
Rust `Ty` for which `Ty::is_linear()` holds elaborates to an
28+
`OwnershipKind` other than `Unrestricted`.
29+
* **P61** — `build_/parse_ownership_section_payload` round-trip on
30+
the full `OwnershipEntry` structure (the surrounding container).
31+
32+
== The wire format
33+
34+
Single-byte encoding, fixed at the value of the Rust discriminant:
35+
36+
[cols="1,1,1"]
37+
|===
38+
| Variant | Wire byte (decimal) | Wire byte (hex)
39+
40+
| `OwnershipKind::Unrestricted` | `0` | `0x00`
41+
| `OwnershipKind::Linear` | `1` | `0x01`
42+
| `OwnershipKind::SharedBorrow` | `2` | `0x02`
43+
| `OwnershipKind::ExclBorrow` | `3` | `0x03`
44+
|===
45+
46+
[NOTE]
47+
.Asymmetric decoder fallback
48+
====
49+
`from_byte` is **total** on `u8`. Any byte outside `0..=3` decodes
50+
to `Unrestricted`. This matches the OCaml
51+
`Tw_verify.parse_ownership_section_payload` reference and the
52+
Rust port in `typed-wasm-verify`. The fallback is **not** a
53+
round-trip violation because no `to_byte` call ever emits those
54+
bytes — the round-trip claim is asymmetric and runs only in the
55+
encoder-first direction:
56+
57+
for every `k : OwnershipKind`,
58+
`from_byte(to_byte(k)) == k`.
59+
====
60+
61+
== The two definitions, side by side
62+
63+
[cols="1,1"]
64+
|===
65+
| Rust producer (`ephapax-wasm`)
66+
| Rust consumer (`typed-wasm-verify`)
67+
68+
a|
69+
[source,rust]
70+
----
71+
pub enum OwnershipKind {
72+
Unrestricted = 0,
73+
Linear = 1,
74+
SharedBorrow = 2,
75+
ExclBorrow = 3,
76+
}
77+
78+
impl OwnershipKind {
79+
pub fn from_byte(b: u8) -> Self {
80+
match b {
81+
1 => OwnershipKind::Linear,
82+
2 => OwnershipKind::SharedBorrow,
83+
3 => OwnershipKind::ExclBorrow,
84+
_ => OwnershipKind::Unrestricted,
85+
}
86+
}
87+
88+
pub fn to_byte(self) -> u8 {
89+
self as u8
90+
}
91+
}
92+
----
93+
a|
94+
The sister type in
95+
`typed-wasm-verify::OwnershipKind` carries the SAME four
96+
variants with the SAME wire-byte values. The canonical source
97+
lives at
98+
`hyperpolymath/typed-wasm:crates/typed-wasm-verify/src/section.rs`.
99+
100+
Both definitions are deliberately duplicated, not shared via a
101+
common crate: ephapax intentionally does NOT depend on
102+
`typed-wasm-verify` (see the `Cargo.toml` note in
103+
`src/ephapax-wasm`). The wire format is the contract.
104+
|===
105+
106+
== The bridge claim
107+
108+
[NOTE]
109+
.Bridge claim (P59)
110+
====
111+
For every `k : OwnershipKind`,
112+
113+
`OwnershipKind::from_byte(k.to_byte()) == k`.
114+
115+
Equivalently, `to_byte` is a section of `from_byte`: the
116+
encoder followed by the decoder is the identity on the image of
117+
the encoder.
118+
====
119+
120+
The four-variant case-split is exhaustive — there are no other
121+
`OwnershipKind` constructors to consider, and `to_byte` emits a
122+
constant byte (`self as u8`) on each, so the proof is by direct
123+
computation:
124+
125+
[cols="1,1,1,1"]
126+
|===
127+
| `k` | `k.to_byte()` | `from_byte(...)` | round-trip OK?
128+
129+
| `Unrestricted` | `0` | `Unrestricted` | yes (fallback arm)
130+
| `Linear` | `1` | `Linear` | yes (arm 1)
131+
| `SharedBorrow` | `2` | `SharedBorrow` | yes (arm 2)
132+
| `ExclBorrow` | `3` | `ExclBorrow` | yes (arm 3)
133+
|===
134+
135+
== Mechanical verification
136+
137+
Three tests pin the bridge claim in
138+
`src/ephapax-wasm/src/ownership.rs`, `mod tests`:
139+
140+
. `coq_bridge_p59_owner_byte_map_pinned` — asserts the exact wire
141+
byte for each of the four variants. Any change in the byte map
142+
is a wire-format break and trips this test.
143+
. `coq_bridge_p59_owner_round_trip_exhaustive` — directly
144+
case-splits on the four variants and asserts
145+
`from_byte(to_byte(k)) == k`. Exhaustive ⇒ equivalent in
146+
strength to a property test on a finite domain.
147+
. `coq_bridge_p59_owner_round_trip` — the `proptest!` form of the
148+
same lemma. Mirrors the standard formulation used in
149+
`typed-wasm-verify`'s own property suite; ships alongside the
150+
exhaustive test for parity with the canonical consumer.
151+
152+
All three are deterministic, dependency-light, and complete in
153+
sub-millisecond.
154+
155+
== Cross-references
156+
157+
* `PROOF-NEEDS.md` — proof obligation **P59** in this session's
158+
inventory.
159+
* `src/ephapax-wasm/src/ownership.rs:55-67` — Rust definition of
160+
`from_byte` / `to_byte`.
161+
* `src/ephapax-wasm/src/ownership.rs`, `mod tests` — the three
162+
P59 tests landed in the same PR as this document.
163+
* `hyperpolymath/typed-wasm:crates/typed-wasm-verify/src/section.rs`
164+
— sister consumer; the canonical reader of the wire format.
165+
* `hyperpolymath/typed-wasm:docs/adr/0002-*.md` — ADR-0002 carrier
166+
handshake (if present; otherwise the ephapax-side
167+
`src/ephapax-wasm/src/ownership.rs` module header is the
168+
authoritative wire-format spec).
169+
* `docs/coq-rust-bridge/is_linear_ty.adoc` — sister bridge
170+
document for proof-debt P28, closed in PR #273.
171+
172+
== Future work
173+
174+
The full ADR-0002 cross-tool soundness link needs P60 + P61 in
175+
addition to this P59 round-trip:
176+
177+
* **P60** — `ty_to_ownership_kind` monotone w.r.t. linearity.
178+
Claim: if `Ty::is_linear(t)` then
179+
`ty_to_ownership_kind(t) != Unrestricted`. Combined with P28
180+
(Rust ↔ Coq `is_linear_ty` bridge), this links the Coq linear
181+
type judgment to the emitted wire byte.
182+
* **P61** — full `OwnershipEntry` round-trip: for every entry
183+
list `es : Vec<OwnershipEntry>`,
184+
`parse_ownership_section_payload(build_ownership_section_payload(&es)) == es`.
185+
The existing `mixed_kinds_roundtrip` test exhibits one witness;
186+
P61 generalises it to a property test on arbitrary entries.
187+
188+
P59 is the simplest of the three (1-day, four-variant case
189+
analysis); P60 and P61 are larger and ship in follow-up PRs.

src/ephapax-wasm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ serde_json = "1"
3232

3333
[dev-dependencies]
3434
wasmparser = "0.221"
35+
proptest = { workspace = true }

src/ephapax-wasm/src/ownership.rs

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
// SPDX-License-Identifier: PMPL-1.0-or-later
2-
// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
34
//
45
//! In-tree codec for the `typedwasm.ownership` wasm custom section.
56
//!
@@ -223,4 +224,84 @@ mod tests {
223224
assert_eq!(OwnershipKind::from_byte(2), OwnershipKind::SharedBorrow);
224225
assert_eq!(OwnershipKind::from_byte(3), OwnershipKind::ExclBorrow);
225226
}
227+
228+
// -----------------------------------------------------------------
229+
// Proof-debt P59: OwnershipKind::from_byte / to_byte round-trip.
230+
//
231+
// The `typedwasm.ownership` custom section crosses the ABI boundary
232+
// to `hyperpolymath/typed-wasm:crates/typed-wasm-verify/src/section.rs`
233+
// (ADR-0002 carrier handshake). The wire format MUST round-trip
234+
// cleanly on the four defined `OwnershipKind` variants, otherwise the
235+
// verifier on the other side observes a different value than the
236+
// producer emitted.
237+
//
238+
// Sister obligations:
239+
// P60 — `ty_to_ownership_kind` monotone w.r.t. linearity.
240+
// P61 — `build_/parse_ownership_section_payload` round-trip on
241+
// the full `OwnershipEntry` structure.
242+
//
243+
// The asymmetric corner (bytes 4..=255 collapse to `Unrestricted`) is
244+
// covered by `unknown_byte_maps_to_unrestricted` above; that is NOT
245+
// a round-trip violation because no `to_byte` call ever emits those
246+
// bytes — the round-trip claim is asymmetric and runs only in the
247+
// `kind -> byte -> kind` direction.
248+
// -----------------------------------------------------------------
249+
250+
/// Exhaustive round-trip on every defined `OwnershipKind` variant.
251+
/// The enum has exactly four constructors, so this case-analysis is
252+
/// just as strong as a property test — it pins the entire image of
253+
/// `to_byte` against `from_byte`.
254+
#[test]
255+
fn coq_bridge_p59_owner_round_trip_exhaustive() {
256+
use OwnershipKind::*;
257+
for k in [Unrestricted, Linear, SharedBorrow, ExclBorrow]
258+
.iter()
259+
.copied()
260+
{
261+
let byte = k.to_byte();
262+
let decoded = OwnershipKind::from_byte(byte);
263+
assert_eq!(
264+
decoded, k,
265+
"P59: from_byte(to_byte({:?})) = {:?}, want {:?}",
266+
k, decoded, k
267+
);
268+
}
269+
}
270+
271+
/// Pin the concrete wire byte for each variant. Coordinated with
272+
/// `typed-wasm-verify::OwnershipKind` — both sides must agree.
273+
/// Any change here is a wire-format break.
274+
#[test]
275+
fn coq_bridge_p59_owner_byte_map_pinned() {
276+
assert_eq!(OwnershipKind::Unrestricted.to_byte(), 0);
277+
assert_eq!(OwnershipKind::Linear.to_byte(), 1);
278+
assert_eq!(OwnershipKind::SharedBorrow.to_byte(), 2);
279+
assert_eq!(OwnershipKind::ExclBorrow.to_byte(), 3);
280+
}
281+
282+
proptest::proptest! {
283+
/// Round-trip lemma (proof-debt P59):
284+
/// For every `OwnershipKind` value `k`,
285+
/// `from_byte(to_byte(k)) == k`.
286+
///
287+
/// Mechanically certifies the wire-format encoder/decoder is a
288+
/// section retraction on the image of `to_byte`. Sister property
289+
/// of P60+P61. Because the enum is finite this proptest is
290+
/// equivalent in strength to the exhaustive case-split above;
291+
/// it is included to mirror the standard formulation used in
292+
/// `typed-wasm-verify`'s own property suite.
293+
#[test]
294+
fn coq_bridge_p59_owner_round_trip(idx in 0u8..4) {
295+
use OwnershipKind::*;
296+
let k = match idx {
297+
0 => Unrestricted,
298+
1 => Linear,
299+
2 => SharedBorrow,
300+
_ => ExclBorrow,
301+
};
302+
let byte = k.to_byte();
303+
let decoded = OwnershipKind::from_byte(byte);
304+
proptest::prop_assert_eq!(decoded, k);
305+
}
306+
}
226307
}

0 commit comments

Comments
 (0)