Skip to content

Commit c0b8b71

Browse files
feat(l13): region-imports carrier — proposal 0003 accepted, codec + link-graph verifier + producer emission (#140) (#198)
Increment 1 of 3 on the independence construction path (owner-ratified sequence). Closes the **L13 positive-form** gap: `typedwasm.region-imports` now exists end-to-end — codec + module-local pass + `verify_link_graph` cross-module `SchemaSub` certification in `typed-wasm-verify` (behind `unstable-l13-imports`), and producer emission from real `.twasm` source (`import region Entity from "physics" { … }`) in `typed-wasm-codegen`. The examples/02 killer feature — one module exports a schema, others import subsets, agreement certified before anything runs — round-trips in-process for the first time. - Proposal 0003: `[draft]` → `[accepted]` (2026-07-07) → **ADR-0007**; wire format as specced (LEB128 + u16le version), with an implementation note pinning sibling-codec conventions. - `target_region` v1.1 high-bit value-space extension implemented (`TARGET_REGION_IMPORT_BIT`), non-breaking for v1 consumers. - Full `RegionImportsError` taxonomy from the proposal's §Consumer obligations; 15 new L13 tests + 5 codec tests; **162 workspace tests, 0 failures**. - Union-merge rule for repeated `(producer, region)` imports in one file (examples/02 holds three conceptual modules) — keeps the carrier's uniqueness obligation. - Cross-repo adoption issues for affinescript + ephapax drafted in `docs/developer/cross-repo-issues-proposal-0003-adoption.adoc` (automated filing was permission-blocked — ready to paste). - Parser module header rewritten per ADR-0006 (still said "stopgap until the AffineScript front-end"). Part of #140 (remaining there: producer-emitted c5_real differential fixture pair; per-module split of multi-file sources). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 229a4d8 commit c0b8b71

19 files changed

Lines changed: 1342 additions & 29 deletions

LEVEL-STATUS.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# SPDX-License-Identifier: CC-BY-SA-4.0
2+
<!-- Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> -->
23
# typed-wasm Level Achievement Status
34

45
## Versioning Scheme (revised 2026-04-13 — typed-wasm-first rollout)
@@ -143,7 +144,7 @@ pins affinescript SHA for drift detection).
143144
| L7 (aliasing) | **YES** | `verify_function` per-path use-range |
144145
| L10 (linearity) | **YES** | `verify_function` per-path use-range |
145146
| L13 (module isolation, negative form) | **YES** | `verify_from_module`, gated on ownership-section presence (PR #37, 2026-05-19) |
146-
| L13 (cross-module schema agreement, positive form) | **proposal-stage** | `typedwasm.region-imports` carrier — proposal 0003 `[draft]` (`docs/proposals/0003`); blocked on producer-side multi-module emission |
147+
| L13 (cross-module schema agreement, positive form) | **YES (import-bound, carrier-backed)** | `typedwasm.region-imports` carrier — proposal 0003 `[accepted]` 2026-07-07 → ADR-0007; `verify_region_imports_from_module` (module-local) + `verify_link_graph` (cross-module `SchemaSub``CompatCertificate`s); gated `cargo feature = "unstable-l13-imports"`; in-tree producer emits from `import region … from "…" { … }` source (`tests/example02.rs`) |
147148
| L2 (region binding) | **YES** (carrier-backed) | `verify_access_sites_from_module` PR #109; reads `typedwasm.regions` + `typedwasm.access-sites` (proposals 0001 + 0002 `[accepted]` 2026-05-30; codec PR #107; gated `cargo feature = "unstable-l2"`) |
148149
| L3–L6 (type-compat, null, bounds, result-type) | **YES** (carrier-backed, schema half) | `typedwasm.regions` codec PR #107; cross-checks against `Region.idr::WasmType`, `Pointer.idr::Nullability`, cardinality. Per-access enforcement gated on producer codegen of access-sites (`affinescript#462`, `ephapax#251`). |
149150
| L15 (resource capabilities, L15-A/B) | **YES** (carrier-backed) | `verify_capabilities_from_module` PR #109; reads `typedwasm.capabilities` (proposal 0001 `[accepted]`; codec PR #107; gated `cargo feature = "unstable-l15"`). L15-C deferred to proposal 0004 `[draft]`. |
@@ -158,9 +159,11 @@ pins affinescript SHA for drift detection).
158159
(access-sites codegen, open), `ephapax#221` (Ty::Borrow surfacing,
159160
open), `ephapax#251` (access-sites codegen, filed 2026-05-30),
160161
`ephapax#250` (Codegen dead-fields cleanup, ✅ merged 2026-05-30).
161-
2. **L13 cross-module (positive form)** — proposal 0003 `[draft]`
162-
(`docs/proposals/0003`). Gated on producer-side multi-module
163-
emission (AffineScript Roadmap C3 / Ephapax not yet on roadmap).
162+
2. **L13 cross-module (positive form)** — DONE in-tree (2026-07-07):
163+
proposal 0003 `[accepted]` → ADR-0007; codec + `verify_link_graph`
164+
behind `unstable-l13-imports`; in-tree producer emits from source.
165+
Sibling-producer adoption (AffineScript Roadmap C3 / Ephapax)
166+
tracked by cross-repo issues.
164167
3. **L15-C (call-graph monotonicity)** — proposal 0004 `[draft]`
165168
(`docs/proposals/0004`). Gated on producer-side L15-A emission
166169
(Roadmap C2 not started in either producer).

crates/typed-wasm-codegen/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ wasmprinter = "=0.252.0"
2525
# The producer reuses the verifier's own carrier encoders so the emitted
2626
# typedwasm.regions / typedwasm.access-sites bytes cannot drift from the
2727
# decoder the verifier runs. `unstable-l2` gates those encoders.
28-
typed-wasm-verify = { path = "../typed-wasm-verify", features = ["unstable-l2"] }
28+
typed-wasm-verify = { path = "../typed-wasm-verify", features = ["unstable-l2", "unstable-l13-imports"] }
2929

3030
[dev-dependencies]
3131
# Round-trip test validates the emitted module is well-formed wasm before

crates/typed-wasm-codegen/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,4 @@ cargo test -p typed-wasm-codegen
5858

5959
- **#127** — codegen coverage across all 10 levels × all 6 examples (and the front-end → IR JSON seam).
6060
- **#130** — promote the round-trip tests into the ECHIDNA property corpus.
61-
- **L13 positive-form / region-imports**shared-region schema agreement (`examples/02`); rides the `typedwasm.region-imports` carrier (proposal 0003 `[draft]`, no verifier pass yet).
61+
- **L13 positive-form / region-imports****done** (issue #140): `import region … from "…" { … }` parses into `Module::region_imports`, emits the `typedwasm.region-imports` carrier (proposal 0003 `[accepted]` / ADR-0007), and `verify_link_graph` certifies cross-module schema agreement (`tests/example02.rs`).

crates/typed-wasm-codegen/src/lib.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,13 @@ use typed_wasm_verify::section::{
3838
build_access_sites_section_payload, AccessSiteEntry, ACCESS_SITE_UNPINNED, NO_TARGET_REGION,
3939
};
4040
use typed_wasm_verify::{
41-
build_ownership_section_payload, build_regions_section_payload, CrossError, FieldEntry, FieldKind,
42-
Nullability, OwnershipError, OwnershipEntry, OwnershipKind, RegionEntry, VerifyError, WasmTy,
41+
build_ownership_section_payload, build_region_imports_section_payload,
42+
build_regions_section_payload, CrossError, FieldEntry, FieldKind,
43+
Nullability, OwnershipError, OwnershipEntry, OwnershipKind, RegionEntry,
44+
RegionImportEntry, VerifyError, WasmTy,
4345
verify_access_sites_from_module, verify_from_module,
4446
ACCESS_SITES_SECTION_NAME, OWNERSHIP_SECTION_NAME, REGIONS_SECTION_NAME,
47+
REGION_IMPORTS_SECTION_NAME,
4548
};
4649
use wasm_encoder::{
4750
CodeSection, CustomSection, EntityType, ExportKind, ExportSection, Function, FunctionSection,
@@ -321,6 +324,12 @@ pub struct Module {
321324
/// param_kinds, ret_kind)`. Emitted as the `typedwasm.ownership`
322325
/// carrier; empty = no L7/L10 carrier.
323326
pub ownership: Vec<(usize, Vec<Ownership>, Ownership)>,
327+
/// Cross-module region imports (`import region X from "module"`),
328+
/// each carrying the EXPECTED schema. Emitted as the
329+
/// `typedwasm.region-imports` carrier (proposal 0003 / ADR-0007,
330+
/// L13 positive form); empty = no carrier. Entries reuse the
331+
/// verifier's own type so bytes cannot drift from its decoder.
332+
pub region_imports: Vec<RegionImportEntry>,
324333
}
325334

326335
// ----------------------------------------------------------------------
@@ -522,13 +531,23 @@ pub fn emit(module: &Module) -> Vec<u8> {
522531
data: payload.as_slice().into(),
523532
});
524533
}
525-
if !region_entries.is_empty() {
534+
// Region imports require their dependent regions carrier (proposal
535+
// 0003 §Producer obligations #1) — emit regions (even if empty)
536+
// whenever an import table is present.
537+
if !region_entries.is_empty() || !module.region_imports.is_empty() {
526538
let payload = build_regions_section_payload(&region_entries);
527539
wasm.section(&CustomSection {
528540
name: REGIONS_SECTION_NAME.into(),
529541
data: payload.as_slice().into(),
530542
});
531543
}
544+
if !module.region_imports.is_empty() {
545+
let payload = build_region_imports_section_payload(&module.region_imports);
546+
wasm.section(&CustomSection {
547+
name: REGION_IMPORTS_SECTION_NAME.into(),
548+
data: payload.as_slice().into(),
549+
});
550+
}
532551
if !access_entries.is_empty() {
533552
let payload = build_access_sites_section_payload(&access_entries);
534553
wasm.section(&CustomSection {
@@ -670,6 +689,7 @@ pub fn example01() -> Module {
670689
imports: vec![],
671690
funcs,
672691
ownership: vec![],
692+
region_imports: vec![],
673693
}
674694
}
675695

@@ -820,6 +840,7 @@ pub fn paint_type_tile() -> Module {
820840
imports: vec![],
821841
funcs,
822842
ownership: vec![],
843+
region_imports: vec![],
823844
}
824845
}
825846

@@ -945,6 +966,7 @@ pub fn paint_type_layer() -> Module {
945966
imports: vec![],
946967
funcs,
947968
ownership: vec![],
969+
region_imports: vec![],
948970
}
949971
}
950972

@@ -986,6 +1008,7 @@ pub fn multimodule_callee() -> Module {
9861008
export: true,
9871009
}],
9881010
ownership: vec![(0, vec![Ownership::Linear], Ownership::Unrestricted)],
1011+
region_imports: vec![],
9891012
}
9901013
}
9911014

@@ -1017,6 +1040,7 @@ pub fn multimodule_caller(call_count: u32) -> Module {
10171040
export: false,
10181041
}],
10191042
ownership: vec![],
1043+
region_imports: vec![],
10201044
}
10211045
}
10221046

crates/typed-wasm-codegen/src/parser.rs

Lines changed: 151 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
// SPDX-License-Identifier: MPL-2.0
22
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
33
//
4-
//! Minimal .twasm text parser for codegen v0.
4+
//! The canonical `.twasm` front-end (ADR-0006).
55
//!
6-
//! This module provides a Rust-native parser for the typed-wasm surface syntax
7-
//! as a stopgap until the AffineScript front-end (ADR-0004, issue #127) lands.
8-
//! It is intentionally limited to the subset needed by paint-type schemas and
9-
//! example-01, not the full .twasm language.
6+
//! Hand-written Rust parser for the typed-wasm surface syntax: the
7+
//! `source → IR → emit → verify` reference path. Parses all six
8+
//! canonical `examples/*.twasm` (round-tripped by `tests/corpus.rs`),
9+
//! recording region schemas, ownership qualifiers (`own`/`&mut`/`&` →
10+
//! the `typedwasm.ownership` carrier) and cross-module region imports
11+
//! (`import region … from "…" { … }` → the `typedwasm.region-imports`
12+
//! carrier, ADR-0007).
1013
//!
11-
//! This parser does NOT duplicate the full AffineScript front-end — it only handles
12-
//! the specific schemas needed to unblock paint-type#39 and demonstrate the
13-
//! codegen path. Full .twasm parsing remains deferred to the AffineScript front-end.
14+
//! Known gaps against `spec/grammar.ebnf` are tracked debt (ADR-0006
15+
//! §Consequences): non-lowerable statement bodies fall back to
16+
//! type-correct stubs, and `if`/`else` / `region.scan` control flow is
17+
//! simplified. The AffineScript front-end (`src/parser/*.affine`) is a
18+
//! reference implementation, not a dependency.
1419
1520
use crate::{Field, FieldTy, Memory, Module, PtrKind, Region, Scalar, Wty};
1621
use std::collections::HashMap;
@@ -31,6 +36,7 @@ struct Parser<'a> {
3136
imports: Vec<crate::Import>,
3237
funcs: Vec<crate::Func>,
3338
ownership: Vec<(usize, Vec<crate::Ownership>, crate::Ownership)>,
39+
region_imports: Vec<typed_wasm_verify::RegionImportEntry>,
3440
}
3541

3642
impl<'a> Parser<'a> {
@@ -44,6 +50,7 @@ impl<'a> Parser<'a> {
4450
imports: Vec::new(),
4551
funcs: Vec::new(),
4652
ownership: Vec::new(),
53+
region_imports: Vec::new(),
4754
}
4855
}
4956

@@ -78,6 +85,7 @@ impl<'a> Parser<'a> {
7885
imports: self.imports,
7986
funcs: self.funcs,
8087
ownership: self.ownership,
88+
region_imports: self.region_imports,
8189
})
8290
}
8391

@@ -1242,36 +1250,163 @@ impl<'a> Parser<'a> {
12421250
self.expect("import")?;
12431251
self.skip_whitespace();
12441252
// Optional `region` keyword: `import region Name from "module" ...`
1245-
if self.peek_word("region") {
1253+
let is_region_import = if self.peek_word("region") {
12461254
self.expect("region")?;
12471255
self.skip_whitespace();
1248-
}
1249-
let _name = self.parse_ident();
1256+
true
1257+
} else {
1258+
false
1259+
};
1260+
let name = self.parse_ident();
12501261
self.skip_whitespace();
12511262
self.expect("from")?;
12521263
self.skip_whitespace();
12531264
// Module source: a quoted string ("game_server") or a bare ident.
1254-
if self.peek_char('"') {
1265+
let producer_module = if self.peek_char('"') {
12551266
self.expect("\"")?;
1267+
let start = self.pos;
12561268
while self.pos < self.src.len() && self.src.as_bytes()[self.pos] != b'"' {
12571269
self.pos += 1;
12581270
}
1271+
let module = self.src.get(start..self.pos).unwrap_or("").to_string();
12591272
self.expect("\"")?;
1273+
module
12601274
} else {
1261-
let _module = self.parse_ident();
1262-
}
1275+
self.parse_ident()
1276+
};
12631277
self.skip_whitespace();
1264-
// Either a re-declaration body `{ ... }` (multi-module) or a `;`.
1278+
// Either an expected-schema body `{ ... }` (multi-module region
1279+
// import, L13 positive form — recorded into the
1280+
// `typedwasm.region-imports` carrier) or a bare `;`.
12651281
if self.peek_char('{') {
12661282
self.expect("{")?;
1267-
self.skip_to_brace_close();
1283+
if is_region_import {
1284+
let expected = self.parse_expected_import_fields()?;
1285+
self.record_region_import(producer_module, name, expected)?;
1286+
} else {
1287+
self.skip_to_brace_close();
1288+
}
12681289
} else if self.peek_char(';') {
12691290
self.expect(";")?;
1291+
if is_region_import {
1292+
// No expected schema listed: the import asserts presence
1293+
// only (zero expected fields — vacuous agreement).
1294+
self.record_region_import(producer_module, name, Vec::new())?;
1295+
}
1296+
}
1297+
Ok(())
1298+
}
1299+
1300+
/// Parse the field list of an `import region … from "…" { … }` body
1301+
/// into EXPECTED-schema entries (proposal 0003). Cursor is just past
1302+
/// the `{`; consumes through the matching `}`. v1 restriction:
1303+
/// scalar fields only.
1304+
fn parse_expected_import_fields(
1305+
&mut self,
1306+
) -> Result<Vec<typed_wasm_verify::FieldEntry>, String> {
1307+
let mut fields = Vec::new();
1308+
loop {
1309+
self.skip_whitespace();
1310+
if self.peek_char('}') {
1311+
self.expect("}")?;
1312+
break;
1313+
}
1314+
if self.pos >= self.src.len() {
1315+
return Err("unterminated import region body".into());
1316+
}
1317+
let fname = self.parse_ident();
1318+
if fname.is_empty() {
1319+
return Err("expected field name in import region body".into());
1320+
}
1321+
self.skip_whitespace();
1322+
self.expect(":")?;
1323+
self.skip_whitespace();
1324+
let (fty, cardinality) = self.parse_field_type()?;
1325+
let wasm_ty = match fty {
1326+
FieldTy::Scalar(s) => scalar_to_wire_ty(&s),
1327+
FieldTy::Ptr { .. } => {
1328+
return Err(format!(
1329+
"import region field '{fname}' is pointer-typed — \
1330+
pointer fields in imported region schemas are not \
1331+
supported in v1 (proposal 0003)"
1332+
));
1333+
}
1334+
};
1335+
fields.push(typed_wasm_verify::FieldEntry {
1336+
name: fname,
1337+
kind: typed_wasm_verify::FieldKind::Scalar,
1338+
wasm_ty,
1339+
target_region: crate::NO_TARGET_REGION,
1340+
nullability: typed_wasm_verify::Nullability::NonNull,
1341+
cardinality,
1342+
});
1343+
self.skip_whitespace();
1344+
if self.peek_char(';') {
1345+
self.expect(";")?;
1346+
}
1347+
}
1348+
Ok(fields)
1349+
}
1350+
1351+
/// Record a region import, union-merging repeated imports of the
1352+
/// same `(producer, region)` pair — a single `.twasm` file may hold
1353+
/// several conceptual modules (examples/02) that each declare their
1354+
/// own expected subset. Conflicting declarations for the same field
1355+
/// are a source error; the emitted carrier keeps pairs unique
1356+
/// (proposal 0003 `DuplicateImport` obligation).
1357+
fn record_region_import(
1358+
&mut self,
1359+
producer_module: String,
1360+
region_name: String,
1361+
expected: Vec<typed_wasm_verify::FieldEntry>,
1362+
) -> Result<(), String> {
1363+
if let Some(existing) = self
1364+
.region_imports
1365+
.iter_mut()
1366+
.find(|e| e.producer_module == producer_module && e.region_name == region_name)
1367+
{
1368+
for f in expected {
1369+
match existing.expected_fields.iter().find(|e| e.name == f.name) {
1370+
None => existing.expected_fields.push(f),
1371+
Some(prev) if *prev == f => {}
1372+
Some(prev) => {
1373+
return Err(format!(
1374+
"conflicting expected types for imported field \
1375+
'{}.{}' from \"{}\": {:?} vs {:?}",
1376+
region_name, f.name, producer_module, prev.wasm_ty, f.wasm_ty
1377+
));
1378+
}
1379+
}
1380+
}
1381+
} else {
1382+
self.region_imports.push(typed_wasm_verify::RegionImportEntry {
1383+
producer_module,
1384+
region_name,
1385+
expected_fields: expected,
1386+
});
12701387
}
12711388
Ok(())
12721389
}
12731390
}
12741391

1392+
/// `.twasm` scalar type → wire `WasmTy` (typedwasm.regions enum).
1393+
fn scalar_to_wire_ty(s: &Scalar) -> typed_wasm_verify::WasmTy {
1394+
use typed_wasm_verify::WasmTy as W;
1395+
match s {
1396+
Scalar::I8 => W::I8,
1397+
Scalar::I16 => W::I16,
1398+
Scalar::I32 => W::I32,
1399+
Scalar::I64 => W::I64,
1400+
Scalar::U8 => W::U8,
1401+
Scalar::U16 => W::U16,
1402+
Scalar::U32 => W::U32,
1403+
Scalar::U64 => W::U64,
1404+
Scalar::F32 => W::F32,
1405+
Scalar::F64 => W::F64,
1406+
Scalar::Bool => W::WBool,
1407+
}
1408+
}
1409+
12751410
fn scalar_byte_size(s: &Scalar) -> u32 {
12761411
match s {
12771412
Scalar::I8 | Scalar::U8 => 1,

crates/typed-wasm-codegen/tests/access_typing.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ fn rig(region: Region, func: Func) -> Module {
9191
imports: vec![],
9292
funcs: vec![func],
9393
ownership: vec![],
94+
region_imports: vec![],
9495
}
9596
}
9697

crates/typed-wasm-codegen/tests/corpus.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ fn gen_valid(seed: u64) -> Module {
154154
imports: vec![],
155155
funcs,
156156
ownership: vec![],
157+
region_imports: vec![],
157158
}
158159
}
159160

@@ -619,6 +620,7 @@ fn one_func_module(kind: Ownership, body: Vec<Op>) -> Module {
619620
export: true,
620621
}],
621622
ownership: vec![(0, vec![kind], Ownership::Unrestricted)],
623+
region_imports: vec![],
622624
}
623625
}
624626

crates/typed-wasm-codegen/tests/errors.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ fn double_free_gives_named_actionable_message() {
3939
export: true,
4040
}],
4141
ownership: vec![(0, vec![Ownership::Linear], Ownership::Unrestricted)],
42+
region_imports: vec![],
4243
};
4344
let diagnostics = self_verify(&module).expect_err("double-free must be rejected");
4445
let joined = diagnostics.join("\n");

0 commit comments

Comments
 (0)