|
| 1 | +// SPDX-License-Identifier: MPL-2.0 |
| 2 | +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | + |
| 4 | += Proposal 0001: Multi-Producer Carrier Sections for L2–L6 and L15 |
| 5 | +:toc: preamble |
| 6 | +:icons: font |
| 7 | + |
| 8 | +[cols="1,3"] |
| 9 | +|=== |
| 10 | +| Status | draft |
| 11 | +| Author | hyperpolymath |
| 12 | +| Date | 2026-05-27 |
| 13 | +| Last-updated | 2026-05-27 |
| 14 | +| Tracks issue | https://github.com/hyperpolymath/typed-wasm/issues/34[#34] |
| 15 | +| Phase | Phase 2 (https://github.com/hyperpolymath/typed-wasm/issues/50[#50]) — multi-producer adoption |
| 16 | +|=== |
| 17 | + |
| 18 | +== Context |
| 19 | + |
| 20 | +The post-codegen verifier (`crates/typed-wasm-verify/`) enforces **L7 |
| 21 | +(aliasing)**, **L10 (linearity)**, and **L13 (module isolation, negative |
| 22 | +form)** on emitted wasm today. The only carrier it consults is the |
| 23 | +`typedwasm.ownership` custom section (4-kind: `Unrestricted` / |
| 24 | +`Linear` / `SharedBorrow` / `ExclBorrow` per function signature; renamed |
| 25 | +from `affinescript.ownership` in PR #65 to make it producer-neutral). |
| 26 | + |
| 27 | +`typedwasm.ownership` does not contain region/field schema, pointer |
| 28 | +nullability, bounds cardinality, or capability data. Without that |
| 29 | +data, **L2 (region-binding), L3 (type-compatible access), L4 (null |
| 30 | +safety), L5 (bounds-proof), L6 (result-type)**, and **L15 (resource |
| 31 | +capabilities)** cannot be re-checked on emitted bytes — the source-level |
| 32 | +checker (`MultiModule.idr`, `ResourceCapabilities.idr`, plus the |
| 33 | +AffineScript `Checker`) is the only line of defence. |
| 34 | + |
| 35 | +The Idris2 spec side already models all the required structure: |
| 36 | + |
| 37 | +* `Region.idr` — `WasmType`, `Field`, `Schema`, `FieldIn` |
| 38 | +* `MultiModule.idr` — `SchemaSub`, `ModuleCompat`, `noSpoofing` |
| 39 | +* `Pointer.idr` — `PtrKind` (Owning / Borrowing / Exclusive), |
| 40 | + `Nullability` (NonNull / Nullable) |
| 41 | +* `ResourceCapabilities.idr` — `CapabilitySet`, `DistinctCaps`, |
| 42 | + `ContainedIn` |
| 43 | + |
| 44 | +So the gap is purely on the **wire** side: there is nowhere to put any |
| 45 | +of this information when AffineScript or Ephapax emits wasm. This is |
| 46 | +an **ABI** problem, not an enforcement problem. |
| 47 | + |
| 48 | +L14 (session protocols) and L16 (agent choreography) are out of scope |
| 49 | +of this proposal: their language-surface work |
| 50 | +(`session`/`choreography` declarations) is not yet emittable by any |
| 51 | +producer; carrying their state on the wire is premature. |
| 52 | + |
| 53 | +== Proposal |
| 54 | + |
| 55 | +Add **two new producer-neutral custom sections** to the typed-wasm |
| 56 | +ABI, distinct from `typedwasm.ownership`, both versioned and |
| 57 | +lenient-readable in the same shape as the existing codec. |
| 58 | + |
| 59 | +. `typedwasm.regions` — carries everything L2–L6 need. |
| 60 | +. `typedwasm.capabilities` — carries the L15 capability lattice. |
| 61 | + |
| 62 | +The two are split (rather than fused into one omnibus section) so a |
| 63 | +producer can adopt L2–L6 without also committing to L15, and so the |
| 64 | +wire formats can evolve independently. All three sections |
| 65 | +(`typedwasm.ownership`, `typedwasm.regions`, `typedwasm.capabilities`) |
| 66 | +are independently optional from the verifier's perspective; absence is |
| 67 | +"no claim made," not "claim of compliance." |
| 68 | + |
| 69 | +=== Wire format — `typedwasm.regions` |
| 70 | + |
| 71 | +Little-endian, byte-aligned, lenient on truncation (matches |
| 72 | +`section.rs::LenientReader`). |
| 73 | + |
| 74 | +---- |
| 75 | +u16le version (= 1) |
| 76 | +u32le region_count |
| 77 | +for each region (in index order, 0..region_count-1): |
| 78 | + u32le name_len |
| 79 | + u8[] name (UTF-8, no NUL terminator) |
| 80 | + u32le field_count |
| 81 | + for each field (in declaration order): |
| 82 | + u32le field_name_len |
| 83 | + u8[] field_name (UTF-8) |
| 84 | + u8 kind (0 = Scalar, |
| 85 | + 1 = PtrOwning, |
| 86 | + 2 = PtrBorrow, |
| 87 | + 3 = PtrExclusive) |
| 88 | + u8 wasm_ty (0 = U8, 1 = U16, 2 = U32, 3 = U64, |
| 89 | + 4 = I8, 5 = I16, 6 = I32, 7 = I64, |
| 90 | + 8 = F32, 9 = F64, 10 = WBool, |
| 91 | + 0xFF = N/A — set for pointer kinds) |
| 92 | + u32le target_region (0xFFFFFFFF if kind = Scalar; |
| 93 | + else index into this section's region table) |
| 94 | + u8 nullability (0 = NonNull, 1 = Nullable) |
| 95 | + u32le cardinality (1 = single value, n>1 = fixed-length array |
| 96 | + of n elements, 0 = unbounded / |
| 97 | + dynamic-bounds — verifier treats as L5 |
| 98 | + "needs runtime check") |
| 99 | + u32le region_byte_size (∑ over fields of cardinality × sizeOf(wasm_ty); |
| 100 | + redundant w.r.t. field table; verifier |
| 101 | + cross-checks) |
| 102 | +---- |
| 103 | + |
| 104 | +Mapping to the spec types: |
| 105 | + |
| 106 | +[cols="1,3"] |
| 107 | +|=== |
| 108 | +| Wire field | Idris2 spec |
| 109 | +| `region_count`, `region` table | `List Region` (lookup-by-index) |
| 110 | +| `field_count`, `field` table | `Schema` (per region) |
| 111 | +| `name`, `field_name` | `String` (via `fieldName`) |
| 112 | +| `wasm_ty` | `WasmType` (11-variant enum) |
| 113 | +| `kind` | `PtrKind` (extended with `Scalar`) |
| 114 | +| `nullability` | `Nullability` |
| 115 | +| `cardinality` | feeds `InBounds idx count` for L5 |
| 116 | +| `target_region` | foreign-key into `Region` table (L2) |
| 117 | +|=== |
| 118 | + |
| 119 | +=== Wire format — `typedwasm.capabilities` |
| 120 | + |
| 121 | +---- |
| 122 | +u16le version (= 1) |
| 123 | +u32le capability_count |
| 124 | +for each capability (in index order): |
| 125 | + u32le name_len |
| 126 | + u8[] name (UTF-8) |
| 127 | +u32le function_count |
| 128 | +for each function: |
| 129 | + u32le func_idx (index into wasm function section) |
| 130 | + u32le required_count |
| 131 | + u32le[required_count] required_capability_indices |
| 132 | + (indices into capability table above; |
| 133 | + must be strictly increasing → trivially |
| 134 | + encodes `DistinctCaps`) |
| 135 | +---- |
| 136 | + |
| 137 | +Mapping to `ResourceCapabilities.idr`: |
| 138 | + |
| 139 | +* The flat capability table = `CapabilitySet`. |
| 140 | +* Strictly-increasing required indices structurally encode |
| 141 | + `DistinctCaps` (no duplicates, fixed comparison order). |
| 142 | +* Per-function `required_capability_indices` ⊆ capability-table is the |
| 143 | + raw data the verifier feeds to `ContainedIn`. |
| 144 | +* L15-C (call-graph monotonicity / `CallCompatible`) is **deferred to |
| 145 | + v1.4.x** (per LEVEL-STATUS) and needs a separate per-call-site grant |
| 146 | + record; out of scope for v1. |
| 147 | + |
| 148 | +=== Versioning policy |
| 149 | + |
| 150 | +Both new sections start with `u16le version = 1`. Future versions: |
| 151 | + |
| 152 | +* **Additive** changes (new trailing fields per record) MAY ship as |
| 153 | + the same major version, since the lenient reader will silently |
| 154 | + ignore the new bytes on a v1 consumer. This is the same forward- |
| 155 | + compatibility contract as `typedwasm.ownership`. |
| 156 | +* **Breaking** changes bump the version and the verifier rejects |
| 157 | + unknown versions with a fresh `OwnershipError`-style variant. |
| 158 | +* `typedwasm.ownership` is unchanged by this proposal. Adding a |
| 159 | + version prefix to it is desirable but is its own change (latent |
| 160 | + issue — track separately). |
| 161 | + |
| 162 | +=== Producer obligations |
| 163 | + |
| 164 | +A conforming producer that emits either new section MUST: |
| 165 | + |
| 166 | +. Emit it with a value of `version` it knows the consumer accepts. |
| 167 | +. Use **stable region indices** within a single module (used as |
| 168 | + foreign keys both in this section and, eventually, by L14/L16 |
| 169 | + protocol carriers). |
| 170 | +. Either emit *all* fields of *all* regions it accesses, or emit |
| 171 | + *none* — partial carriers are interpreted as "no claim made about |
| 172 | + unlisted fields" and L2 will flag any access to an unlisted field. |
| 173 | +. For L15: emit only capabilities actually declared by the source |
| 174 | + module; do not synthesise. |
| 175 | + |
| 176 | +=== Consumer obligations (verifier) |
| 177 | + |
| 178 | +`crates/typed-wasm-verify/` will gain: |
| 179 | + |
| 180 | +. `section.rs` — `parse_regions_section_payload`, |
| 181 | + `build_regions_section_payload`, `parse_capabilities_section_payload`, |
| 182 | + `build_capabilities_section_payload`. Same lenient-reader pattern. |
| 183 | +. `verify.rs` — new passes `verify_region_binding` (L2), |
| 184 | + `verify_type_compat` (L3), `verify_null_safety` (L4), |
| 185 | + `verify_bounds` (L5), `verify_result_type` (L6), |
| 186 | + `verify_capabilities` (L15). |
| 187 | +. `lib.rs` — new `OwnershipError` variants per failure mode, plus a |
| 188 | + top-level `VerifyError::UnsupportedCarrierVersion`. |
| 189 | + |
| 190 | +Each pass is gated behind a cargo feature |
| 191 | +(`unstable-l2`, …, `unstable-l15`) until the proposal is accepted, so |
| 192 | +the v1.0 verifier surface is unchanged. |
| 193 | + |
| 194 | +=== Coordination with downstream producers |
| 195 | + |
| 196 | +This proposal is filed in `typed-wasm` because the verifier and the |
| 197 | +Idris2 spec live here. It MUST be reviewed by: |
| 198 | + |
| 199 | +* https://github.com/hyperpolymath/affinescript[`affinescript`] — |
| 200 | + `lib/tw_verify.ml`, `lib/tw_interface.ml`, and the AffineScript |
| 201 | + codegen path that today emits `typedwasm.ownership`. |
| 202 | +* https://github.com/hyperpolymath/ephapax[`ephapax`] — |
| 203 | + `src/ephapax-wasm/` (emits the same `typedwasm.ownership` section). |
| 204 | +* Any third-party producer named in Phase 2 (TypedQLiser; LLVM |
| 205 | + lowering guide consumers). |
| 206 | + |
| 207 | +No producer should ship either new section until the cross-repo |
| 208 | +review is closed and the spec is promoted from `[draft]` to |
| 209 | +`[accepted]`. |
| 210 | + |
| 211 | +== Alternatives considered |
| 212 | + |
| 213 | +=== A. One omnibus section |
| 214 | + |
| 215 | +Fuse regions + capabilities into a single `typedwasm.metadata` |
| 216 | +section. |
| 217 | + |
| 218 | +*Rejected.* L2–L6 and L15 are orthogonal concerns and have different |
| 219 | +adoption curves: an early producer may want regions without yet |
| 220 | +declaring capabilities, or vice versa. A single section forces both |
| 221 | +to land at the same version. Wire-format evolution is also entangled |
| 222 | +— a breaking change to the capability layout invalidates region data |
| 223 | +that was fine on its own. |
| 224 | + |
| 225 | +=== B. Reuse `typedwasm.ownership` |
| 226 | + |
| 227 | +Extend the existing section's per-entry payload with optional fields. |
| 228 | + |
| 229 | +*Rejected.* The ownership section is keyed by function index and |
| 230 | +carries per-function signatures; regions and capabilities are |
| 231 | +module-scoped (regions) or have a different per-function shape |
| 232 | +(required-caps lists). Cramming them in would either break the |
| 233 | +current parser or require a sub-record discriminator on every entry. |
| 234 | +The split-section approach keeps each codec linear and per-concern. |
| 235 | + |
| 236 | +=== C. Reuse a wasm-component-model interface type |
| 237 | + |
| 238 | +Encode region schemas as wasm component-model record types and let |
| 239 | +the component-model machinery carry them. |
| 240 | + |
| 241 | +*Rejected for v1.* Component-model adoption in the typed-wasm |
| 242 | +producer set is too early — AffineScript and Ephapax both currently |
| 243 | +target core wasm. Worth revisiting in Phase 4 (Tooling / DX) once |
| 244 | +`wasm-tools component new` is in the producer cookbook. |
| 245 | + |
| 246 | +=== D. Embed in `name` section |
| 247 | + |
| 248 | +Use the standard `name` custom section's free-form sub-sections to |
| 249 | +piggy-back schema data. |
| 250 | + |
| 251 | +*Rejected.* The `name` section is for debug names; tools strip it |
| 252 | +under `-Os`. Schema/capability data is load-bearing for |
| 253 | +verification, not debug info, and must not be optimised out. |
| 254 | + |
| 255 | +== Open questions |
| 256 | + |
| 257 | +. **String table.** The format above duplicates region names if a |
| 258 | + field's `target_region` is referenced from multiple fields. Worth |
| 259 | + it to add a single shared string table at the front of the |
| 260 | + section? Trade-off: smaller modules (~5–15% on representative |
| 261 | + AffineScript fixtures, untested) vs. an extra indirection in the |
| 262 | + parser. Default: ship v1 inline, add string table in v2 only if |
| 263 | + measurements justify it. |
| 264 | + |
| 265 | +. **Cardinality `0 = unbounded` sentinel.** Should the verifier treat |
| 266 | + unbounded as L5-unprovable (downgrade), or refuse to admit the |
| 267 | + section at all? Source-side AffineScript today doesn't emit |
| 268 | + unbounded regions; the sentinel is reserved for future dynamic- |
| 269 | + bounds work. Default: downgrade with a verifier diagnostic, do |
| 270 | + not refuse. |
| 271 | + |
| 272 | +. **Endianness of UTF-8 strings.** Trivially answered (none — UTF-8 is |
| 273 | + a byte stream) but worth being explicit in the spec to avoid |
| 274 | + producer drift. |
| 275 | + |
| 276 | +. **Per-function vs per-call-site capability annotations.** L15-A |
| 277 | + (required-caps per function) is enough for v1; L15-C (per-call-site |
| 278 | + grants) needs a separate annotation that ideally lives in a third |
| 279 | + section (`typedwasm.capability-grants`) so the v1.4 → v1.4.x |
| 280 | + transition is purely additive. Defer the wire format for grants to |
| 281 | + a follow-up proposal. |
| 282 | + |
| 283 | +. **Interaction with `noSpoofing` (L13 cross-module).** A multi-module |
| 284 | + setting needs the importing module's region table to refer to the |
| 285 | + exporting module's region by something other than a local index. |
| 286 | + Two options: (a) a separate `typedwasm.region-imports` section |
| 287 | + that mirrors wasm's standard `(import …)` syntax; or (b) extend the |
| 288 | + per-region record with a `(producer-name, region-name)` pair when |
| 289 | + cross-module. Out of scope of v1 (single-module L2–L6 is the v1 |
| 290 | + milestone); needs its own proposal. |
| 291 | + |
| 292 | +== Acceptance criteria |
| 293 | + |
| 294 | +. Reviewed and signed off by maintainers of `affinescript` and |
| 295 | + `ephapax` (the only current producers of `typedwasm.ownership`). |
| 296 | +. Wire format reviewed against the Idris2 spec types in `Region.idr`, |
| 297 | + `Pointer.idr`, `ResourceCapabilities.idr` — every wire field maps |
| 298 | + cleanly to a spec witness. |
| 299 | +. A draft codec lands in `crates/typed-wasm-verify/src/section.rs` |
| 300 | + behind cargo features (`unstable-l2`, `unstable-l15`); round-trip |
| 301 | + tests show parse(build(x)) = x for non-empty fixtures. |
| 302 | +. A draft section in |
| 303 | + `spec/type-safety-levels-for-wasm.adoc` documents the new sections |
| 304 | + alongside the existing `typedwasm.ownership` description. |
| 305 | +. Cross-repo issues are filed against `affinescript` and `ephapax` |
| 306 | + for codegen of the new sections (out of scope of *this* proposal — |
| 307 | + this proposal only defines the wire format). |
| 308 | + |
| 309 | +After acceptance, this file is promoted to an ADR under |
| 310 | +`docs/decisions/` (per `docs/proposals/README.adoc`'s status legend) |
| 311 | +and the level-table in `LEVEL-STATUS.md` gains "verifier (carrier- |
| 312 | +backed)" status columns for L2–L6 and L15. |
| 313 | + |
| 314 | +== Related |
| 315 | + |
| 316 | +* Issue https://github.com/hyperpolymath/typed-wasm/issues/34[#34] — |
| 317 | + this proposal directly addresses it. |
| 318 | +* Issue https://github.com/hyperpolymath/typed-wasm/issues/35[#35] — |
| 319 | + L13 isolation Rust mirror (part 1 landed in PR #37; part 2 / C5.1 |
| 320 | + is independent). |
| 321 | +* Issue https://github.com/hyperpolymath/typed-wasm/issues/50[#50] — |
| 322 | + Phase 2 (multi-producer adoption); producer specification work |
| 323 | + consumes this proposal's output. |
| 324 | +* PR https://github.com/hyperpolymath/typed-wasm/pull/65[#65] — |
| 325 | + rename `affinescript.ownership` → `typedwasm.ownership` |
| 326 | + (precedent for producer-neutral naming). |
| 327 | +* `LEVEL-STATUS.md` lines 109–143 — current verifier coverage |
| 328 | + description. |
| 329 | +* `crates/typed-wasm-verify/src/section.rs` — existing codec to |
| 330 | + mirror. |
| 331 | +* `src/abi/TypedWasm/ABI/Region.idr`, |
| 332 | + `src/abi/TypedWasm/ABI/Pointer.idr`, |
| 333 | + `src/abi/TypedWasm/ABI/MultiModule.idr`, |
| 334 | + `src/abi/TypedWasm/ABI/ResourceCapabilities.idr` — spec types. |
0 commit comments