Skip to content

Commit c76eef9

Browse files
docs(proposals/0001): Criterion 2 appendix — wire↔Idris2-spec mapping (#110)
## Summary Closes proposal 0001 §"Acceptance criteria" Criterion 2 — the last write-up gate before `[review] → [accepted]`. Adds a 247-line "Appendix A — Wire-vs-Idris2-spec mapping" to `docs/proposals/0001-multi-producer-carrier-section.adoc`. ### Appendix structure | Section | Coverage | |---------|----------| | A.1 | `typedwasm.regions` wire fields ↔ `Region.idr` + `Pointer.idr` — per-field table (version, region_count, name, field_count, field_name, kind, wasm_ty, target_region, nullability, cardinality, region_byte_size) | | A.2 | `typedwasm.capabilities` wire fields ↔ `ResourceCapabilities.idr` — capability table + per-function required list; documents that strict-increasing `required[]` encodes DistinctCaps + ContainedIn + canonical-order at once | | A.3 | What is NOT carried on the wire (deliberate): `ContainedIn` proof witnesses (verifier reconstructs), `CallCompatible` (L15-C deferred to #96), `LayoutValid` (verifier recomputes), `SchemaEq` (single-module v1), `RegionDisjoint` (runtime concern), `ExclusiveWitness` (in ownership section) | | A.4 | Soundness shape — single NOTE listing per-emission proof obligations PR #107 (codec) + PR #109 (verifier) already enforce | Criterion 2 line in §"Acceptance criteria" updated to "*(Satisfied — see Appendix A below.)*". ### Why a write-up not a property test Criterion 2's text reads "every wire field maps cleanly to a spec witness" — a *write-up* satisfies it (and is what a reviewer reads). A future task (filed in the appendix's closing paragraph, not blocking `[accepted]`) is to encode the same mapping as Idris2 property tests in `src/abi/` (`WireSchemaEquiv.idr` showing `encode/decode` round-trips preserve the spec witnesses). ### Acceptance-roadmap state after this PR | | 0001 (regions+capabilities) | 0002 (access-sites) | |----------------------|------------------------------|----------------------| | Codec | ✅ #107 | ✅ #107 | | Verifier | ✅ #109 | ✅ #109 | | Spec doc | ✅ #108 | ✅ #108 | | Wire-vs-spec review | ✅ **this PR** | n/a | | Cross-repo issues | ✅ affinescript#444 + ephapax#221 | ✅ affinescript#462 / ephapax pending | | Open Qs | ⚠️ #94, #95, #96 still open | n/a | After this lands, proposal 0001's only remaining gate is the open questions converging (`WBool` wire-width pin, region-imports proposal placeholder, capability-grants v1.4.x). The acceptance criteria themselves are all green. ## Test plan - [x] Local proof-doc render check: appendix renders with cleanly-formatted cols=2,3,2 + cols=2,3 tables. - [x] Cross-reference links resolve (Region.idr:253, Region.idr:127, Pointer.idr:74, ResourceCapabilities.idr:67/104/218/241/248/312, etc.). - [x] No code changes — no Cargo build required. - [ ] Reviewer cross-check: confirm A.3 is exhaustive (no missed spec-side relation that v1 silently drops). ## Related - #34 (proposal 0001 umbrella) - #106 (acceptance roadmap tracker) - PR #104 ([draft] → [review]) - PR #107 (codec) / PR #108 (spec doc) / PR #109 (verifier) - #94 / #95 / #96 (acceptance preconditions filed) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6ed3ee7 commit c76eef9

1 file changed

Lines changed: 247 additions & 2 deletions

File tree

docs/proposals/0001-multi-producer-carrier-section.adoc

Lines changed: 247 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
| Status | review
1111
| Author | hyperpolymath
1212
| Date | 2026-05-27
13-
| Last-updated | 2026-05-30
13+
| Last-updated | 2026-05-30 (+ Criterion 2 wire↔spec review appendix)
1414
| Tracks issue | https://github.com/hyperpolymath/typed-wasm/issues/34[#34]
1515
| Phase | Phase 2 (https://github.com/hyperpolymath/typed-wasm/issues/50[#50]) — multi-producer adoption
1616
|===
@@ -352,7 +352,7 @@ verification, not debug info, and must not be optimised out.
352352
`ephapax` (the only current producers of `typedwasm.ownership`).
353353
. Wire format reviewed against the Idris2 spec types in `Region.idr`,
354354
`Pointer.idr`, `ResourceCapabilities.idr` — every wire field maps
355-
cleanly to a spec witness.
355+
cleanly to a spec witness. *(Satisfied — see Appendix A below.)*
356356
. A draft codec lands in `crates/typed-wasm-verify/src/section.rs`
357357
behind cargo features (`unstable-l2`, `unstable-l15`); round-trip
358358
tests show parse(build(x)) = x for non-empty fixtures.
@@ -389,3 +389,248 @@ backed)" status columns for L2–L6 and L15.
389389
`src/abi/TypedWasm/ABI/Pointer.idr`,
390390
`src/abi/TypedWasm/ABI/MultiModule.idr`,
391391
`src/abi/TypedWasm/ABI/ResourceCapabilities.idr` — spec types.
392+
393+
== Appendix A — Wire-vs-Idris2-spec mapping (Criterion 2)
394+
395+
This appendix satisfies acceptance Criterion 2 ("wire format reviewed
396+
against the Idris2 spec types — every wire field maps cleanly to a spec
397+
witness"). Each wire field below is paired with the Idris2 type / value
398+
that it encodes, and every cross-section invariant the wire carries is
399+
named against the spec lemma it corresponds to. The reference codec is
400+
`crates/typed-wasm-verify/src/section.rs` (landed in PR #107).
401+
402+
=== A.1 `typedwasm.regions` ↔ `Region.idr` + `Pointer.idr`
403+
404+
[cols="2,3,2", options="header"]
405+
|===
406+
| Wire field
407+
| Idris2 spec witness
408+
| Notes
409+
410+
| `u16le version = 1`
411+
| (codec invariant — no spec mapping)
412+
| Future major bumps land in a separate proposal; v1 verifier rejects
413+
unknown versions per `parse_regions_section_payload` (section.rs:418).
414+
415+
| `u32le region_count`
416+
| `length : List Region -> Nat` applied to the per-module region list
417+
| The wire-side list is dense (region IDs are 0..region_count-1);
418+
matches `Region.idr`'s record-of-list shape.
419+
420+
| Per-region `utf8 name`
421+
| `Region.regionName : String` (Region.idr:253)
422+
| UTF-8 byte sequence with explicit length prefix; no NUL terminator.
423+
String identity (not embedding) is what cross-module agreement
424+
reasons over.
425+
426+
| Per-region `u32le field_count`
427+
| `length : Schema -> Nat` where `Schema = List Field` (Region.idr:158)
428+
| Empty schema (`field_count = 0`) is admissible — corresponds to
429+
`LayoutNil` and a region with `schemaSize = 0`.
430+
431+
| Per-field `utf8 field_name`
432+
| `Field.MkField` name component (Region.idr:127); extracted by
433+
`fieldName : Field -> String` (line 132)
434+
| Field-name injectivity (Region.idr:360 `fieldNameInj`) holds on the
435+
Idris2 side; the wire format preserves the same identity because
436+
identical bytes ↔ identical `String`.
437+
438+
| Per-field `u8 kind` (0=Scalar, 1=PtrOwning, 2=PtrBorrow, 3=PtrExclusive)
439+
| `Pointer.idr::PtrKind` (line 39): Owning/Borrowing/Exclusive plus the
440+
out-of-band "not a pointer" case
441+
| `Scalar` is a wire-side discriminant carrying the "this field stores
442+
an inline value, not a pointer" case that `Pointer.idr` represents
443+
implicitly by not using `Ptr` at all. The bijection on the
444+
three pointer cases is exact.
445+
446+
| Per-field `u8 wasm_ty` (0..10 = U8..WBool, 0xFF = NotApplicable)
447+
| `Region.idr::WasmType` (line 34): 11 variants
448+
| `NotApplicable` (0xFF) is emitted when `kind != Scalar` (the field
449+
type semantics come from the target region). v1 has no `WBool`
450+
store-width pin (see open question #94 / typed-wasm#94).
451+
452+
| Per-field `u32le target_region`
453+
| Spec-side: `Ptr kind (lookupField target_region).schema life null`
454+
parameterisation (Pointer.idr:74)
455+
| Foreign key into the same section's region table. `NO_TARGET_REGION`
456+
(0xFFFFFFFF) sentinel when `kind == Scalar`. Verifier enforces
457+
`target_region < region_count` when `kind != Scalar` — this is the
458+
operational form of the spec-level `Schema` indexing on `Ptr`.
459+
460+
| Per-field `u8 nullability` (0=NonNull, 1=Nullable)
461+
| `Pointer.idr::Nullability` (via Levels.idr) — used as the `null`
462+
parameter of `Ptr kind schema life null`
463+
| The lenient decode (`Nullability::from_byte`) treats unknown bytes
464+
as `NonNull`, which is the safer interpretation per the spec's
465+
L4 (null safety) discipline.
466+
467+
| Per-field `u32le cardinality`
468+
| `Region.count : Nat` (Region.idr:258)
469+
| Sentinel `0 = unbounded/dynamic`; matches the spec only when
470+
cardinality > 0 (open question §"Cardinality `0 = unbounded`
471+
sentinel" defers full coverage to L5 verifier downgrade).
472+
473+
| Per-region `u32le region_byte_size`
474+
| `schemaSize : Schema -> Nat` (Region.idr:204)
475+
| Redundant w.r.t. the field list; verifier MAY recompute and compare
476+
(proposal §"Producer obligations" ¶4). When the producer's value
477+
diverges from the recomputed `schemaSize`, the verifier flags a
478+
carrier-self-inconsistency error.
479+
|===
480+
481+
=== A.2 `typedwasm.capabilities` ↔ `ResourceCapabilities.idr`
482+
483+
[cols="2,3,2", options="header"]
484+
|===
485+
| Wire field
486+
| Idris2 spec witness
487+
| Notes
488+
489+
| `u16le version = 1`
490+
| (codec invariant)
491+
| Same versioning policy as regions.
492+
493+
| `u32le capability_count`
494+
| `length : CapabilitySet -> Nat` applied to `ModuleCaps.declared`
495+
(ResourceCapabilities.idr:218)
496+
| The wire capability table IS the module's declared capability set —
497+
one wire entry per `CapabilityName` in `declared`.
498+
499+
| Per-capability `utf8 name`
500+
| `CapabilityName.capName : String` (ResourceCapabilities.idr:67)
501+
| The newtype wrapping discipline (`MkCapabilityName`) preserves
502+
identity on the wire as raw UTF-8 — type system on the spec side
503+
refuses to confuse capability names with arbitrary strings; wire
504+
side simply names the bytes.
505+
506+
| `u32le function_count`
507+
| Per-module count of `FunctionCaps owner` values
508+
| Not directly a spec witness; bridges wire to the per-function
509+
records below.
510+
511+
| Per-function `u32le func_idx`
512+
| (wasm function index — bridges spec to emitted wasm)
513+
| No direct spec mapping; the Idris2 side reasons in terms of
514+
`FunctionCaps owner` records, not wasm function indices. The wire
515+
side names the index because that is what the verifier needs to
516+
attach the requirement to a concrete code-section function.
517+
518+
| Per-function `u32le required_count`
519+
| `length : CapabilitySet -> Nat` applied to `FunctionCaps.required`
520+
(line 241)
521+
| Producer MAY emit 0 — corresponds to `pureFunctionCaps`
522+
(ResourceCapabilities.idr:248) and `FunctionCaps.required = []`.
523+
524+
| Per-function `u32le[] required` (strictly increasing)
525+
| `FunctionCaps.required : CapabilitySet` (line 241), with the
526+
strict-increasing invariant encoding `DistinctCaps` (line 104) AND
527+
serving as the verifier's foreign-key check
528+
| The strict-increasing invariant is the *operational counterpart* of
529+
three spec-side facts at once:
530+
531+
1. **`DistinctCaps required`** — no index appears twice. The
532+
strict-increasing form trivially implies uniqueness.
533+
534+
2. **`ContainedIn required declared`** (L15-B soundness, line 312)
535+
— each index < `capability_count`. The verifier's
536+
`verify_capabilities_from_module` pass (PR #109) checks this
537+
directly, raising `CapabilityIdxOutOfRange` on violation.
538+
539+
3. **Canonical-order producer obligation** — equal `required` sets
540+
have equal wire byte sequences (encoding determinism).
541+
542+
Parser-side `sort_unstable` + `dedup` (section.rs:760) is a
543+
defence-in-depth normalisation: a producer that regresses on the
544+
strict-increasing obligation still yields a verifier result
545+
consistent with the spec, but the wire-level guarantee is degraded.
546+
|===
547+
548+
=== A.3 What is NOT carried on the wire (deliberate)
549+
550+
The Idris2 spec types have several relations and proofs that v1 of the
551+
carriers deliberately does NOT encode:
552+
553+
[cols="2,3", options="header"]
554+
|===
555+
| Spec construct
556+
| Why not on the wire / where it lives
557+
558+
| `ContainedIn required declared`
559+
(L15-B explicit proof witness)
560+
| The verifier RECONSTRUCTS the proof obligation by checking each
561+
`required[i] < capability_count`. The reconstructed check is sound
562+
because the verifier IS the trusted base for L15-B on emitted
563+
wasm; carrying the explicit witness would duplicate the same
564+
information.
565+
566+
| `CallCompatible caller callee`
567+
(L15-C call-graph monotonicity)
568+
| Deferred to a separate `typedwasm.capability-grants` section
569+
(typed-wasm#96) in a v1.4.x follow-up proposal. v1 enforces L15-A
570+
(DistinctCaps) and L15-B (ContainedIn) only; L15-C falls back to
571+
the producer's source-level checker.
572+
573+
| `LayoutValid schema`
574+
(per-field offset fits within instance size; no field overlap)
575+
| The verifier RECOMPUTES `computeOffsets`/`schemaSize` from the
576+
wire-carried field list and compares to the wire-carried
577+
`region_byte_size`. The proof obligation is operational, not
578+
carried.
579+
580+
| `SchemaEq s1 s2`
581+
(cross-module schema equivalence — Region.idr:306)
582+
| v1 is single-module. Cross-module schema agreement is the explicit
583+
scope of a future `typedwasm.region-imports` follow-up (open
584+
question #5 / typedwasm#95) — the comparison logic uses the
585+
wire-carried region + field tables directly without an
586+
intermediate witness.
587+
588+
| `RegionDisjoint r1 r2`
589+
(byte-disjoint regions — Region.idr:439)
590+
| Runtime / loader concern; the wire format intentionally does not
591+
pin region base addresses (those are wasm-side `(memory ...)` init
592+
data). Disjointness is enforced at allocation time by the host
593+
runtime, not by the carrier verifier.
594+
595+
| `ExclusiveWitness schema`
596+
(Pointer.idr:163 — no-aliasing for exclusive pointers)
597+
| L7 (aliasing safety) is carried by the `typedwasm.ownership`
598+
section (Linear/SharedBorrow/ExclBorrow tags per parameter), NOT
599+
the regions carrier. The regions carrier only attests to schema
600+
structure, not to per-handle aliasing facts.
601+
|===
602+
603+
=== A.4 Soundness shape
604+
605+
Combining the per-field mappings (A.1) with the operational
606+
reconstructions (A.3) gives the following claim, which proposal 0001
607+
asserts (and the verifier in PR #109 + the codec in PR #107 enforce):
608+
609+
[NOTE]
610+
====
611+
For any module emitting `typedwasm.regions` and `typedwasm.capabilities`,
612+
**there exists a corresponding Idris2 spec-level
613+
`(Region[], ModuleCaps, FunctionCaps[])` value** whose constructor
614+
inhabitation requires exactly the proofs the verifier already checks at
615+
the wire level — namely:
616+
617+
* `WasmType` decoder soundness (lenient by design — unknown ⇒ safer
618+
default per A.1 nullability + WasmTy notes).
619+
* `FieldKind` ↔ `PtrKind` bijection on the three pointer cases.
620+
* `target_region` foreign-key check (when `kind != Scalar`).
621+
* `region_byte_size` self-consistency vs `schemaSize`.
622+
* `DistinctCaps` via strict-increasing `required[]` (degraded but
623+
recoverable via parser-side `sort + dedup`).
624+
* `ContainedIn required declared` via `required[i] < capability_count`
625+
(verifier pass — PR #109).
626+
627+
L15-C (`CallCompatible`), `SchemaEq` (cross-module), `RegionDisjoint`,
628+
and `ExclusiveWitness` are explicitly out of scope of v1 and tracked
629+
under the open-questions section above.
630+
====
631+
632+
This satisfies Criterion 2 *as a write-up*. A future task — not
633+
required for `[accepted]` — is to encode the same mapping as Idris2
634+
property tests (`src/abi/` would gain a `WireSchemaEquiv.idr` showing
635+
`encode/decode` round-trips preserve the spec witnesses listed in A.1
636+
+ A.2). That work is filed as a follow-up after acceptance.

0 commit comments

Comments
 (0)