Skip to content

Commit 7d991a7

Browse files
feat(frontend): real statement lowering — let/if/else/while/indexed access + parser skip_declaration fix (#199)
Increment 2 of 3 on the independence construction path. The statement lowerer takes ECS-style bodies (examples 03/04) from representative stubs to **real typed loads/stores with pinned access sites**: `let`, assignment, `if`/`else`, `while`, indexed `region.get/set $h[i] .f`, `cast<>`, typed expressions with precedence and no implicit promotion. Region params are copied to a base local once in the prologue, so `own`/`&mut` discipline survives arbitrarily many accesses under the verifier's per-path use counting. **Also fixes a real parser bug found by the new tests**: `skip_declaration` skipped a balanced `{…}` and then kept scanning for a `;`, so example-04's top-level `invariant … { … }` blocks swallowed every following declaration — ex04's functions were never parsed at all (its corpus "round-trip" was regions-only). After the fix ex04 parses 4 functions and 3 lower for real. Verification: all-passes gate (structural validation + L7/L10 + L2 bounds + L2 access-typing on the pinned sites) over both examples, plus a **wasmi execution gate** proving the lowered control flow computes correct results (both if-branches, loop entry/skip, cast round-trip). **166 workspace tests, 0 failures.** 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c0b8b71 commit 7d991a7

8 files changed

Lines changed: 891 additions & 12 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 | representative type-correct bodies, not full `region.scan`/indexing semantics |
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 |
5656

5757
## Where this goes next
5858

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

Lines changed: 133 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@
3131
//! emitted whenever the source declares discipline (`own` / `&mut` /
3232
//! `&` qualifiers) — the parser records them into [`Module::ownership`]
3333
//! and `emit` writes the carrier (exercised by `tests/example03.rs`).
34-
//! * Full function-body semantics (indexing, `region.scan`, null checks):
35-
//! v0 emits type-correct representative bodies, not the full lowering.
34+
//! * Function bodies lower for real where the statement lowerer covers
35+
//! 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.
3638
3739
use typed_wasm_verify::section::{
3840
build_access_sites_section_payload, AccessSiteEntry, ACCESS_SITE_UNPINNED, NO_TARGET_REGION,
@@ -47,7 +49,7 @@ use typed_wasm_verify::{
4749
REGION_IMPORTS_SECTION_NAME,
4850
};
4951
use wasm_encoder::{
50-
CodeSection, CustomSection, EntityType, ExportKind, ExportSection, Function, FunctionSection,
52+
BlockType, CodeSection, CustomSection, EntityType, ExportKind, ExportSection, Function, FunctionSection,
5153
ImportSection, Instruction, MemArg, MemorySection, MemoryType, Module as WasmModule,
5254
NameMap, NameSection, TypeSection, ValType,
5355
};
@@ -239,6 +241,61 @@ pub enum Op {
239241
Call(u32),
240242
/// Drop the top stack value — consumes a value exactly once.
241243
Drop,
244+
// --- Locals + control flow + arithmetic (front-end statement
245+
// lowering, ADR-0006: `let`, assignment, `if`/`else`, `while`) ---
246+
LocalSet(u32),
247+
LocalTee(u32),
248+
/// `block (empty)` — the `while` exit target.
249+
Block,
250+
/// `loop (empty)` — the `while` back-edge target.
251+
Loop,
252+
/// `if (empty)` — statement-position conditional (no result value).
253+
If,
254+
Else,
255+
End,
256+
Br(u32),
257+
BrIf(u32),
258+
Return,
259+
I32Eqz,
260+
I32Add,
261+
I32Sub,
262+
I32Mul,
263+
I32DivS,
264+
I32Eq,
265+
I32Ne,
266+
I32LtS,
267+
I32LeS,
268+
I32GtS,
269+
I32GeS,
270+
I64Add,
271+
I64Sub,
272+
I64Mul,
273+
F32Add,
274+
F32Sub,
275+
F32Mul,
276+
F32Div,
277+
F32Eq,
278+
F32Ne,
279+
F32Lt,
280+
F32Le,
281+
F32Gt,
282+
F32Ge,
283+
F64Add,
284+
F64Sub,
285+
F64Mul,
286+
F64Div,
287+
F64Eq,
288+
F64Ne,
289+
F64Lt,
290+
F64Le,
291+
F64Gt,
292+
F64Ge,
293+
/// `cast<i32>(f32/f64 expr)` — saturating-free truncation (traps on
294+
/// NaN/overflow, matching wasm core `i32.trunc_f*_s`).
295+
I32TruncF32S,
296+
I32TruncF64S,
297+
F32ConvertI32S,
298+
F64ConvertI32S,
242299
}
243300

244301
/// Ownership discipline for a function parameter, emitted into the
@@ -297,6 +354,11 @@ pub struct Func {
297354
pub name: String,
298355
pub params: Vec<Wty>,
299356
pub results: Vec<Wty>,
357+
/// Extra (non-param) locals, indexed after the params. Statement
358+
/// lowering allocates these for `let` bindings, `region.get`
359+
/// destinations, and the per-handle base-pointer copies that keep
360+
/// L7/L10 param-use counts at exactly one.
361+
pub locals: Vec<Wty>,
300362
pub body: Vec<Op>,
301363
pub accesses: Vec<AccessSite>,
302364
pub export: bool,
@@ -399,6 +461,54 @@ fn op_to_instruction(op: Op) -> Instruction<'static> {
399461
Op::F64Store { offset } => Instruction::F64Store(memarg(offset, 3)),
400462
Op::Call(i) => Instruction::Call(i),
401463
Op::Drop => Instruction::Drop,
464+
Op::LocalSet(i) => Instruction::LocalSet(i),
465+
Op::LocalTee(i) => Instruction::LocalTee(i),
466+
Op::Block => Instruction::Block(BlockType::Empty),
467+
Op::Loop => Instruction::Loop(BlockType::Empty),
468+
Op::If => Instruction::If(BlockType::Empty),
469+
Op::Else => Instruction::Else,
470+
Op::End => Instruction::End,
471+
Op::Br(d) => Instruction::Br(d),
472+
Op::BrIf(d) => Instruction::BrIf(d),
473+
Op::Return => Instruction::Return,
474+
Op::I32Eqz => Instruction::I32Eqz,
475+
Op::I32Add => Instruction::I32Add,
476+
Op::I32Sub => Instruction::I32Sub,
477+
Op::I32Mul => Instruction::I32Mul,
478+
Op::I32DivS => Instruction::I32DivS,
479+
Op::I32Eq => Instruction::I32Eq,
480+
Op::I32Ne => Instruction::I32Ne,
481+
Op::I32LtS => Instruction::I32LtS,
482+
Op::I32LeS => Instruction::I32LeS,
483+
Op::I32GtS => Instruction::I32GtS,
484+
Op::I32GeS => Instruction::I32GeS,
485+
Op::I64Add => Instruction::I64Add,
486+
Op::I64Sub => Instruction::I64Sub,
487+
Op::I64Mul => Instruction::I64Mul,
488+
Op::F32Add => Instruction::F32Add,
489+
Op::F32Sub => Instruction::F32Sub,
490+
Op::F32Mul => Instruction::F32Mul,
491+
Op::F32Div => Instruction::F32Div,
492+
Op::F32Eq => Instruction::F32Eq,
493+
Op::F32Ne => Instruction::F32Ne,
494+
Op::F32Lt => Instruction::F32Lt,
495+
Op::F32Le => Instruction::F32Le,
496+
Op::F32Gt => Instruction::F32Gt,
497+
Op::F32Ge => Instruction::F32Ge,
498+
Op::F64Add => Instruction::F64Add,
499+
Op::F64Sub => Instruction::F64Sub,
500+
Op::F64Mul => Instruction::F64Mul,
501+
Op::F64Div => Instruction::F64Div,
502+
Op::F64Eq => Instruction::F64Eq,
503+
Op::F64Ne => Instruction::F64Ne,
504+
Op::F64Lt => Instruction::F64Lt,
505+
Op::F64Le => Instruction::F64Le,
506+
Op::F64Gt => Instruction::F64Gt,
507+
Op::F64Ge => Instruction::F64Ge,
508+
Op::I32TruncF32S => Instruction::I32TruncF32S,
509+
Op::I32TruncF64S => Instruction::I32TruncF64S,
510+
Op::F32ConvertI32S => Instruction::F32ConvertI32S,
511+
Op::F64ConvertI32S => Instruction::F64ConvertI32S,
402512
}
403513
}
404514

@@ -443,7 +553,8 @@ pub fn emit(module: &Module) -> Vec<u8> {
443553
let global_idx = import_count + local_i as u32;
444554
funcs.function(global_idx);
445555

446-
let mut f = Function::new([]);
556+
let mut f =
557+
Function::new_with_locals_types(func.locals.iter().map(|l| l.to_val_type()));
447558
for op in &func.body {
448559
f.instruction(&op_to_instruction(*op));
449560
}
@@ -606,6 +717,7 @@ pub fn example01() -> Module {
606717
name: "get_player_hp".into(),
607718
params: vec![Wty::I32, Wty::I32],
608719
results: vec![Wty::I32],
720+
locals: vec![],
609721
body: vec![Op::LocalGet(0), Op::I32Load { offset: 0 }],
610722
accesses: vec![AccessSite {
611723
region: 1,
@@ -619,6 +731,7 @@ pub fn example01() -> Module {
619731
name: "damage_player".into(),
620732
params: vec![Wty::I32, Wty::I32, Wty::I32],
621733
results: vec![],
734+
locals: vec![],
622735
body: vec![Op::LocalGet(0), Op::LocalGet(2), Op::I32Store { offset: 0 }],
623736
accesses: vec![AccessSite {
624737
region: 1,
@@ -633,6 +746,7 @@ pub fn example01() -> Module {
633746
name: "get_enemy_target_hp".into(),
634747
params: vec![Wty::I32, Wty::I32, Wty::I32],
635748
results: vec![Wty::I32],
749+
locals: vec![],
636750
body: vec![Op::LocalGet(0), Op::I32Load { offset: 0 }],
637751
accesses: vec![
638752
AccessSite {
@@ -653,6 +767,7 @@ pub fn example01() -> Module {
653767
name: "count_active_enemies".into(),
654768
params: vec![Wty::I32],
655769
results: vec![Wty::I32],
770+
locals: vec![],
656771
body: vec![Op::I32Const(0)],
657772
accesses: vec![AccessSite {
658773
region: 2,
@@ -666,6 +781,7 @@ pub fn example01() -> Module {
666781
name: "move_player".into(),
667782
params: vec![Wty::I32, Wty::I32, Wty::F32, Wty::F32],
668783
results: vec![],
784+
locals: vec![],
669785
body: vec![
670786
Op::LocalGet(0),
671787
Op::LocalGet(2),
@@ -746,6 +862,7 @@ pub fn paint_type_tile() -> Module {
746862
name: "alloc_tile".into(),
747863
params: vec![Wty::I32, Wty::I32],
748864
results: vec![Wty::I32],
865+
locals: vec![],
749866
body: vec![Op::LocalGet(0), Op::Drop, Op::I32Const(0)],
750867
accesses: vec![],
751868
export: true,
@@ -756,6 +873,7 @@ pub fn paint_type_tile() -> Module {
756873
name: "free_tile".into(),
757874
params: vec![Wty::I32],
758875
results: vec![],
876+
locals: vec![],
759877
body: vec![Op::LocalGet(0), Op::Drop],
760878
accesses: vec![],
761879
export: true,
@@ -766,6 +884,7 @@ pub fn paint_type_tile() -> Module {
766884
name: "fill_tile".into(),
767885
params: vec![Wty::I32, Wty::I32, Wty::I32, Wty::I32, Wty::I32],
768886
results: vec![],
887+
locals: vec![],
769888
body: vec![
770889
Op::LocalGet(0), Op::Drop,
771890
Op::LocalGet(1), Op::Drop,
@@ -784,6 +903,7 @@ pub fn paint_type_tile() -> Module {
784903
name: "read_pixel".into(),
785904
params: vec![Wty::I32, Wty::I32, Wty::I32],
786905
results: vec![Wty::I32],
906+
locals: vec![],
787907
body: vec![
788908
Op::LocalGet(0), Op::Drop,
789909
Op::LocalGet(1), Op::Drop,
@@ -800,6 +920,7 @@ pub fn paint_type_tile() -> Module {
800920
name: "write_pixel".into(),
801921
params: vec![Wty::I32, Wty::I32, Wty::I32, Wty::I32, Wty::I32, Wty::I32, Wty::I32],
802922
results: vec![],
923+
locals: vec![],
803924
body: vec![
804925
Op::LocalGet(0), Op::Drop,
805926
Op::LocalGet(1), Op::Drop,
@@ -820,6 +941,7 @@ pub fn paint_type_tile() -> Module {
820941
name: "blit_tile".into(),
821942
params: vec![Wty::I32, Wty::I32],
822943
results: vec![],
944+
locals: vec![],
823945
body: vec![
824946
Op::LocalGet(0), Op::Drop,
825947
Op::LocalGet(1), Op::Drop,
@@ -893,6 +1015,7 @@ pub fn paint_type_layer() -> Module {
8931015
name: "stack_new".into(),
8941016
params: vec![],
8951017
results: vec![Wty::I32],
1018+
locals: vec![],
8961019
body: vec![Op::I32Const(0)],
8971020
accesses: vec![],
8981021
export: true,
@@ -902,6 +1025,7 @@ pub fn paint_type_layer() -> Module {
9021025
name: "stack_free".into(),
9031026
params: vec![Wty::I32],
9041027
results: vec![],
1028+
locals: vec![],
9051029
body: vec![Op::LocalGet(0), Op::Drop],
9061030
accesses: vec![],
9071031
export: true,
@@ -911,6 +1035,7 @@ pub fn paint_type_layer() -> Module {
9111035
name: "push_layer".into(),
9121036
params: vec![Wty::I32, Wty::I32, Wty::I32],
9131037
results: vec![Wty::I32],
1038+
locals: vec![],
9141039
body: vec![
9151040
Op::LocalGet(0), Op::Drop,
9161041
Op::LocalGet(1), Op::Drop,
@@ -929,6 +1054,7 @@ pub fn paint_type_layer() -> Module {
9291054
name: "get_id_at".into(),
9301055
params: vec![Wty::I32, Wty::I32],
9311056
results: vec![Wty::I32],
1057+
locals: vec![],
9321058
body: vec![
9331059
Op::LocalGet(0), Op::Drop,
9341060
Op::LocalGet(1), Op::Drop,
@@ -944,6 +1070,7 @@ pub fn paint_type_layer() -> Module {
9441070
name: "set_opacity".into(),
9451071
params: vec![Wty::I32, Wty::I32, Wty::I32],
9461072
results: vec![Wty::I32],
1073+
locals: vec![],
9471074
body: vec![
9481075
Op::LocalGet(0), Op::Drop,
9491076
Op::LocalGet(1), Op::Drop,
@@ -1003,6 +1130,7 @@ pub fn multimodule_callee() -> Module {
10031130
name: "consume".into(),
10041131
params: vec![Wty::I32],
10051132
results: vec![],
1133+
locals: vec![],
10061134
body: vec![Op::LocalGet(0), Op::Drop], // uses the Linear param once
10071135
accesses: vec![],
10081136
export: true,
@@ -1035,6 +1163,7 @@ pub fn multimodule_caller(call_count: u32) -> Module {
10351163
name: "use_resource".into(),
10361164
params: vec![Wty::I32],
10371165
results: vec![],
1166+
locals: vec![],
10381167
body,
10391168
accesses: vec![],
10401169
export: false,

0 commit comments

Comments
 (0)