Skip to content

Commit cc0d9cc

Browse files
feat(frontend): region.scan closure lowering + is_null/opt<T> — the last statement stubs fall (#201)
Next construction increment (owner: "yes to your 1"). `region.scan $h [where pred] -> |e| { … }` now lowers to a bounded loop over the region's declared instances (`region Name[N]` counts are now recorded — previously discarded), with the predicate's bare field idents resolving to pinned typed loads off the current-instance address and `$e` binding that handle for the body. `is_null`/`!` land under a documented v0 null-is-zero convention, so `opt<T>` check-then-unwrap flows lower too. Expressions gain the bitwise tier (`(flags & 1) == 1`). Coverage jump: `physics_step` (10 pinned sites), `ai_decision` (9 — opt unwrap, indexed access, early returns), `collect_visible`, `count_active_enemies`, and `get_inventory_weight` all compile for real. Remaining stubs are handle-typed locals and embedded-region paths only. Gates: full verifier stack over examples 01/02 (validate + L7/L10 + L2 bounds + L2 access-typing) and a wasmi execution test proving a where-predicated scan visits exactly the matching instances. **173 workspace tests, 0 failures.** 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent a0f50ac commit cc0d9cc

4 files changed

Lines changed: 323 additions & 18 deletions

File tree

crates/typed-wasm-codegen/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ cargo test -p typed-wasm-codegen
5252
| WAT (text) emission | **emitted** via `--emit wat\|both` (#125) |
5353
| `typedwasm.ownership` (L7/L10) | **emitted** for any `own`/`&mut`/`&` source discipline (parser-recorded, incl. Linear returns) and for the multi-module callee (#128) |
5454
| Multi-module (linear boundary) | **emitted** — callee export + caller import round-trip through `verify_cross_module` (#128) |
55-
| Function-body lowering | **real statement lowering**`let`, assignment, `if`/`else`, `while`, indexed `region.get`/`region.set`, `cast<>` (wasmi-executed, `tests/example04.rs`); `region.scan` and `opt<T>` unwraps still stub |
55+
| Function-body lowering | **real statement lowering**`let`, assignment, `if`/`else`, `while`, indexed access, `cast<>`, `region.scan … -> \|e\| { … }` with `where` predicates, `is_null`/`opt<T>` (v0 null = 0) — wasmi-executed (`tests/example04.rs`, `tests/scan_lowering.rs`); remaining stubs: handle-typed locals, embedded-region paths |
5656

5757
## Where this goes next
5858

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333
//! and `emit` writes the carrier (exercised by `tests/example03.rs`).
3434
//! * Function bodies lower for real where the statement lowerer covers
3535
//! them (`let`, assignment, `if`/`else`, `while`, indexed access,
36-
//! `cast<>` — see `parser.rs`); `region.scan` closures and `opt<T>`
37-
//! null-checks still fall back to type-correct representative stubs.
36+
//! `cast<>`, `region.scan` closures with `where` predicates,
37+
//! `is_null` under the v0 null-is-zero convention — see `parser.rs`);
38+
//! handle-typed locals and embedded-region field paths still fall
39+
//! back to type-correct representative stubs.
3840
3941
use typed_wasm_verify::section::{
4042
build_access_sites_section_payload, AccessSiteEntry, ACCESS_SITE_UNPINNED, NO_TARGET_REGION,
@@ -257,6 +259,8 @@ pub enum Op {
257259
BrIf(u32),
258260
Return,
259261
I32Eqz,
262+
I32And,
263+
I32Or,
260264
I32Add,
261265
I32Sub,
262266
I32Mul,
@@ -472,6 +476,8 @@ fn op_to_instruction(op: Op) -> Instruction<'static> {
472476
Op::BrIf(d) => Instruction::BrIf(d),
473477
Op::Return => Instruction::Return,
474478
Op::I32Eqz => Instruction::I32Eqz,
479+
Op::I32And => Instruction::I32And,
480+
Op::I32Or => Instruction::I32Or,
475481
Op::I32Add => Instruction::I32Add,
476482
Op::I32Sub => Instruction::I32Sub,
477483
Op::I32Mul => Instruction::I32Mul,

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

Lines changed: 180 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ struct Parser<'a> {
3737
funcs: Vec<crate::Func>,
3838
ownership: Vec<(usize, Vec<crate::Ownership>, crate::Ownership)>,
3939
region_imports: Vec<typed_wasm_verify::RegionImportEntry>,
40+
/// Declared instance count per region (`region Name[N]`; 1 if bare).
41+
/// Parallel to `regions`. Bounds `region.scan` loops and sizes the
42+
/// synthesised memory.
43+
region_instances: Vec<u64>,
44+
/// True when the source declared `memory { … }` — a synthesised
45+
/// memory must never override it.
46+
memory_declared: bool,
4047
}
4148

4249
impl<'a> Parser<'a> {
@@ -51,6 +58,8 @@ impl<'a> Parser<'a> {
5158
funcs: Vec::new(),
5259
ownership: Vec::new(),
5360
region_imports: Vec::new(),
61+
region_instances: Vec::new(),
62+
memory_declared: false,
5463
}
5564
}
5665

@@ -177,11 +186,12 @@ impl<'a> Parser<'a> {
177186
let name = self.parse_ident();
178187
self.skip_whitespace();
179188

180-
// Parse optional array specifier: region Name[N]. We don't yet track
181-
// region cardinality, so the parsed size is discarded.
189+
// Parse optional array specifier: region Name[N] — the declared
190+
// instance count (bounds `region.scan`, sizes synthesised memory).
191+
let mut instances = 1u64;
182192
if self.peek_char('[') {
183193
self.expect("[")?;
184-
let _n: u64 = self.parse_number()?;
194+
instances = self.parse_number()?.max(1);
185195
self.expect("]")?;
186196
}
187197

@@ -273,6 +283,7 @@ impl<'a> Parser<'a> {
273283
fields,
274284
byte_size,
275285
});
286+
self.region_instances.push(instances);
276287

277288
self.skip_whitespace();
278289
Ok(())
@@ -510,6 +521,7 @@ impl<'a> Parser<'a> {
510521
}
511522

512523
fn parse_memory(&mut self) -> Result<(), String> {
524+
self.memory_declared = true;
513525
self.expect("memory")?;
514526
let _name = self.parse_ident();
515527
self.skip_whitespace();
@@ -1022,12 +1034,21 @@ impl<'a> Parser<'a> {
10221034
/// cannot point past the declared memory and trap at runtime. Only fills in
10231035
/// a memory when the module declared none; a declared memory is the author's.
10241036
fn ensure_memory_for_region(&mut self, region: usize) {
1025-
if self.memory.is_none() {
1026-
let bytes = self.regions.get(region).map_or(0, |r| r.byte_size as u64);
1027-
self.memory = Some(Memory {
1028-
min_pages: bytes.div_ceil(65536).max(1),
1029-
max_pages: None,
1030-
});
1037+
if self.memory_declared {
1038+
return; // never override a source-declared memory
1039+
}
1040+
let bytes = self.regions.get(region).map_or(0, |r| r.byte_size as u64)
1041+
* self.region_instances.get(region).copied().unwrap_or(1);
1042+
let pages = bytes.div_ceil(65536).max(1);
1043+
match &mut self.memory {
1044+
Some(m) if m.min_pages >= pages => {}
1045+
Some(m) => m.min_pages = pages,
1046+
None => {
1047+
self.memory = Some(Memory {
1048+
min_pages: pages,
1049+
max_pages: None,
1050+
})
1051+
}
10311052
}
10321053
}
10331054

@@ -1452,6 +1473,10 @@ struct Lower {
14521473
accesses: Vec<crate::AccessSite>,
14531474
/// Regions touched — memory is synthesised only on successful lowering.
14541475
used_regions: Vec<usize>,
1476+
/// Active `region.scan` contexts, innermost last: (region index,
1477+
/// slot of the current-instance address local). Bare idents in scan
1478+
/// predicates/bodies resolve to fields of the innermost scan region.
1479+
scan_stack: Vec<(usize, u32)>,
14551480
}
14561481

14571482
impl Lower {
@@ -1481,6 +1506,7 @@ impl<'a> Parser<'a> {
14811506
ops: Vec::new(),
14821507
accesses: Vec::new(),
14831508
used_regions: Vec::new(),
1509+
scan_stack: Vec::new(),
14841510
};
14851511
// Prologue: copy each region param into a base local (its single
14861512
// use), bind scalar params by name.
@@ -1542,7 +1568,10 @@ impl<'a> Parser<'a> {
15421568
if self.try_keyword("set") {
15431569
return self.lower_region_set(cx).map(|_| false);
15441570
}
1545-
return None; // alloc / free / scan / place: unsupported here
1571+
if self.try_keyword("scan") {
1572+
return self.lower_region_scan(cx, results).map(|_| false);
1573+
}
1574+
return None; // alloc / free / place: unsupported here
15461575
}
15471576
if self.try_keyword("let") {
15481577
self.try_keyword("mut");
@@ -1729,12 +1758,84 @@ impl<'a> Parser<'a> {
17291758
Some(())
17301759
}
17311760

1732-
// --- Typed expression lowering (precedence: cmp > add > mul > primary).
1761+
/// `region.scan $h [where <pred>] -> |e| { body }` — a bounded loop
1762+
/// over the region's declared instances. Bare idents in the
1763+
/// predicate resolve to fields of the scanned region; `$e` in the
1764+
/// body is the current instance's handle.
1765+
fn lower_region_scan(&mut self, cx: &mut Lower, results: &[Wty]) -> Option<()> {
1766+
if !self.try_char('$') {
1767+
return None;
1768+
}
1769+
let hname = self.parse_ident();
1770+
let &(region, base) = cx.handles.get(&hname)?;
1771+
let instances = i32::try_from(*self.region_instances.get(region)?).ok()?;
1772+
let stride = i32::try_from(self.regions.get(region)?.byte_size).ok()?;
1773+
1774+
let i_slot = cx.alloc(Wty::I32);
1775+
let addr_slot = cx.alloc(Wty::I32);
1776+
1777+
// i = 0; block { loop { i < N eqz br_if 1; addr = base + i*stride; …
1778+
cx.ops.push(crate::Op::I32Const(0));
1779+
cx.ops.push(crate::Op::LocalSet(i_slot));
1780+
cx.ops.push(crate::Op::Block);
1781+
cx.ops.push(crate::Op::Loop);
1782+
cx.ops.push(crate::Op::LocalGet(i_slot));
1783+
cx.ops.push(crate::Op::I32Const(instances));
1784+
cx.ops.push(crate::Op::I32LtS);
1785+
cx.ops.push(crate::Op::I32Eqz);
1786+
cx.ops.push(crate::Op::BrIf(1));
1787+
cx.ops.push(crate::Op::LocalGet(base));
1788+
cx.ops.push(crate::Op::LocalGet(i_slot));
1789+
cx.ops.push(crate::Op::I32Const(stride));
1790+
cx.ops.push(crate::Op::I32Mul);
1791+
cx.ops.push(crate::Op::I32Add);
1792+
cx.ops.push(crate::Op::LocalSet(addr_slot));
1793+
1794+
cx.scan_stack.push((region, addr_slot));
1795+
1796+
let has_pred = self.try_keyword("where");
1797+
if has_pred {
1798+
if self.lower_expr(cx, None)? != Wty::I32 {
1799+
cx.scan_stack.pop();
1800+
return None;
1801+
}
1802+
cx.ops.push(crate::Op::If);
1803+
}
1804+
1805+
let ok = (|| {
1806+
if !self.try_str("->") || !self.try_char('|') {
1807+
return None;
1808+
}
1809+
let ename = self.parse_ident();
1810+
if ename.is_empty() || !self.try_char('|') || !self.try_char('{') {
1811+
return None;
1812+
}
1813+
cx.handles.insert(ename, (region, addr_slot));
1814+
self.lower_block(cx, results)?;
1815+
Some(())
1816+
})();
1817+
cx.scan_stack.pop();
1818+
ok?;
1819+
1820+
if has_pred {
1821+
cx.ops.push(crate::Op::End);
1822+
}
1823+
cx.ops.push(crate::Op::LocalGet(i_slot));
1824+
cx.ops.push(crate::Op::I32Const(1));
1825+
cx.ops.push(crate::Op::I32Add);
1826+
cx.ops.push(crate::Op::LocalSet(i_slot));
1827+
cx.ops.push(crate::Op::Br(0));
1828+
cx.ops.push(crate::Op::End);
1829+
cx.ops.push(crate::Op::End);
1830+
Some(())
1831+
}
1832+
1833+
// --- Typed expression lowering (precedence: cmp > band > add > mul > primary).
17331834
// `expected` seeds literal typing; operands of a binary op must agree
17341835
// exactly (no implicit promotion — a mismatch bails the whole body).
17351836

17361837
fn lower_expr(&mut self, cx: &mut Lower, expected: Option<Wty>) -> Option<Wty> {
1737-
let lty = self.lower_add(cx, expected)?;
1838+
let lty = self.lower_band(cx, expected)?;
17381839
self.skip_whitespace();
17391840
let cmp = if self.try_str("==") {
17401841
"=="
@@ -1753,7 +1854,7 @@ impl<'a> Parser<'a> {
17531854
} else {
17541855
return Some(lty);
17551856
};
1756-
if self.lower_add(cx, Some(lty))? != lty {
1857+
if self.lower_band(cx, Some(lty))? != lty {
17571858
return None;
17581859
}
17591860
use crate::Op::*;
@@ -1781,13 +1882,39 @@ impl<'a> Parser<'a> {
17811882
Some(Wty::I32)
17821883
}
17831884

1885+
/// Bitwise tier (`&`, `|`) between comparison and additive — i32
1886+
/// only. The examples parenthesise (`(flags & 1) == 1`), so the
1887+
/// C-precedence footgun does not arise.
1888+
fn lower_band(&mut self, cx: &mut Lower, expected: Option<Wty>) -> Option<Wty> {
1889+
let ty = self.lower_add(cx, expected)?;
1890+
loop {
1891+
self.skip_whitespace();
1892+
let and = match self.src.as_bytes().get(self.pos) {
1893+
Some(&b'&') if self.src.as_bytes().get(self.pos + 1) != Some(&b'&') => true,
1894+
Some(&b'|') if self.src.as_bytes().get(self.pos + 1) != Some(&b'|') => false,
1895+
_ => return Some(ty),
1896+
};
1897+
if ty != Wty::I32 {
1898+
return None;
1899+
}
1900+
self.pos += 1;
1901+
if self.lower_add(cx, Some(Wty::I32))? != Wty::I32 {
1902+
return None;
1903+
}
1904+
cx.ops.push(if and { crate::Op::I32And } else { crate::Op::I32Or });
1905+
}
1906+
}
1907+
17841908
fn lower_add(&mut self, cx: &mut Lower, expected: Option<Wty>) -> Option<Wty> {
17851909
let ty = self.lower_mul(cx, expected)?;
17861910
loop {
17871911
self.skip_whitespace();
17881912
let op = match self.src.as_bytes().get(self.pos) {
17891913
Some(&b'+') => crate::Op::I32Add,
1790-
Some(&b'-') => crate::Op::I32Sub,
1914+
// `->` is the scan/get arrow, never subtraction.
1915+
Some(&b'-') if self.src.as_bytes().get(self.pos + 1) != Some(&b'>') => {
1916+
crate::Op::I32Sub
1917+
}
17911918
_ => return Some(ty),
17921919
};
17931920
let plus = matches!(op, crate::Op::I32Add);
@@ -1839,6 +1966,15 @@ impl<'a> Parser<'a> {
18391966
fn lower_primary(&mut self, cx: &mut Lower, expected: Option<Wty>) -> Option<Wty> {
18401967
self.skip_whitespace();
18411968
let &b = self.src.as_bytes().get(self.pos)?;
1969+
if b == b'!' && self.src.as_bytes().get(self.pos + 1) != Some(&b'=') {
1970+
// Logical not on an i32 truth value.
1971+
self.pos += 1;
1972+
if self.lower_primary(cx, Some(Wty::I32))? != Wty::I32 {
1973+
return None;
1974+
}
1975+
cx.ops.push(crate::Op::I32Eqz);
1976+
return Some(Wty::I32);
1977+
}
18421978
if b == b'(' {
18431979
self.pos += 1;
18441980
let ty = self.lower_expr(cx, expected)?;
@@ -1864,6 +2000,20 @@ impl<'a> Parser<'a> {
18642000
cx.ops.push(crate::Op::I32Const(0));
18652001
return Some(Wty::I32);
18662002
}
2003+
"is_null" => {
2004+
// v0 null representation: 0 — matching wasm's null
2005+
// pointer convention (`opt<T>` carries `Nullability` in
2006+
// the regions carrier; a richer representation is an L4
2007+
// follow-up). `is_null(x)` = `x == 0`.
2008+
if !self.try_char('(') {
2009+
return None;
2010+
}
2011+
if self.lower_expr(cx, Some(Wty::I32))? != Wty::I32 || !self.try_char(')') {
2012+
return None;
2013+
}
2014+
cx.ops.push(crate::Op::I32Eqz);
2015+
return Some(Wty::I32);
2016+
}
18672017
"cast" => {
18682018
if !self.try_char('<') {
18692019
return None;
@@ -1892,8 +2042,23 @@ impl<'a> Parser<'a> {
18922042
cx.ops.push(crate::Op::LocalGet(slot));
18932043
return Some(wty);
18942044
}
2045+
// Inside a `region.scan`, a bare ident is a field of the scanned
2046+
// region — a typed load off the current-instance address.
2047+
if let Some(&(region, addr)) = cx.scan_stack.last() {
2048+
if let Some((field_idx, offset, scalar)) = self.resolve_field(region, &ident) {
2049+
cx.ops.push(crate::Op::LocalGet(addr));
2050+
let load_at = cx.ops.len();
2051+
cx.ops.push(scalar_load_op(&scalar, offset as u64));
2052+
cx.accesses.push(crate::AccessSite {
2053+
region,
2054+
field: field_idx,
2055+
instr_index: Some(load_at),
2056+
});
2057+
return Some(wty_from_scalar(&scalar));
2058+
}
2059+
}
18952060
self.pos = save;
1896-
None // unknown ident: is_null / sizeof / handle-as-value / call
2061+
None // unknown ident: sizeof / handle-as-value / call
18972062
}
18982063

18992064
/// Numeric literal, typed by `expected` (default: `.`-bearing → F32,

0 commit comments

Comments
 (0)