Skip to content

Commit 5d66f9d

Browse files
hyperpolymathclaude
andcommitted
docs(proposals): 0001 multi-producer carrier sections for L2–L6 and L15
Files issue #34 as the first entry under docs/proposals/. Proposes two new producer-neutral custom sections (typedwasm.regions, typedwasm.capabilities) alongside the existing typedwasm.ownership, so the post-codegen verifier can re-check L2–L6 and L15 on emitted bytes without unilateral wire-format changes by any one producer. Wire formats are versioned and lenient-readable in the same shape as the existing section codec. Mapping to Region.idr / Pointer.idr / ResourceCapabilities.idr is documented field-by-field. Includes alternatives considered (omnibus section, reuse of ownership, component- model, name section) and open questions (string table, unbounded cardinality, multi-module foreign keys). [draft] — requires cross-repo review by affinescript and ephapax (the two current producers of typedwasm.ownership) before promotion to [accepted] and ADR. Refs #34 Refs #35 (L13 part 1 already shipped via PR #37; this is the path for the remaining L2–L6 / L15 scope) Refs #50 (Phase 2 — multi-producer adoption) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 89d5707 commit 5d66f9d

2 files changed

Lines changed: 344 additions & 1 deletion

File tree

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
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.

docs/proposals/README.adoc

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@ Status legend:
1616

1717
== Current proposals
1818

19-
None yet. The strategic decisions (D1–D6) from
19+
[cols="1,4,1"]
20+
|===
21+
| Number | Title | Status
22+
23+
| link:0001-multi-producer-carrier-section.adoc[0001]
24+
| Multi-producer carrier sections for L2–L6 and L15
25+
| `[draft]`
26+
|===
27+
28+
The strategic decisions (D1–D6) from
2029
link:../PRODUCTION-PATH.adoc[`docs/PRODUCTION-PATH.adoc`] §"Load-bearing
2130
decisions" are tracked there directly; each will land an ADR under
2231
`docs/decisions/` when made rather than first landing as a proposal.

0 commit comments

Comments
 (0)