|
10 | 10 | | Status | review |
11 | 11 | | Author | hyperpolymath |
12 | 12 | | Date | 2026-05-27 |
13 | | -| Last-updated | 2026-05-30 |
| 13 | +| Last-updated | 2026-05-30 (+ Criterion 2 wire↔spec review appendix) |
14 | 14 | | Tracks issue | https://github.com/hyperpolymath/typed-wasm/issues/34[#34] |
15 | 15 | | Phase | Phase 2 (https://github.com/hyperpolymath/typed-wasm/issues/50[#50]) — multi-producer adoption |
16 | 16 | |=== |
@@ -352,7 +352,7 @@ verification, not debug info, and must not be optimised out. |
352 | 352 | `ephapax` (the only current producers of `typedwasm.ownership`). |
353 | 353 | . Wire format reviewed against the Idris2 spec types in `Region.idr`, |
354 | 354 | `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.)* |
356 | 356 | . A draft codec lands in `crates/typed-wasm-verify/src/section.rs` |
357 | 357 | behind cargo features (`unstable-l2`, `unstable-l15`); round-trip |
358 | 358 | tests show parse(build(x)) = x for non-empty fixtures. |
@@ -389,3 +389,248 @@ backed)" status columns for L2–L6 and L15. |
389 | 389 | `src/abi/TypedWasm/ABI/Pointer.idr`, |
390 | 390 | `src/abi/TypedWasm/ABI/MultiModule.idr`, |
391 | 391 | `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