Skip to content

Commit 0c15133

Browse files
hyperpolymathclaude
andcommitted
feat(codegen): complete .twasm front-end for the example corpus (2/6 -> 6/6)
Extends the v0 front-end + codegen so all six canonical examples parse, emit valid wasm, and round-trip through the verifier (Phase 1 / #130): - parser: ptr<T>/unique<T>/ref<T> field pointers (03); optional/unnamed fn params + &mut/&/own region<T> borrow params (05, 06); general annotation-clause skip (cost_bound { ... } etc., 05); import region X from "..." {...}|; with quoted module source (06, 02); invariant { ... } region constraint blocks (02). - codegen: typed result stubs — Op::{I64Const,F32Const,F64Const} + mappings, so representative bodies type-check against f32/i64/f64 result types (03). - tests: parsed_example_corpus_round_trips round-trips all six examples (the #130 soundness gate over real .twasm, not just paint-type/example-01). Still v0 (type-correct representative bodies, not full region.get/set lowering; the six canonical examples, not arbitrary .twasm). 31/31 codegen tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 42321e7 commit 0c15133

3 files changed

Lines changed: 132 additions & 17 deletions

File tree

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@ impl Wty {
182182
pub enum Op {
183183
LocalGet(u32),
184184
I32Const(i32),
185+
I64Const(i64),
186+
F32Const(f32),
187+
F64Const(f64),
185188
I32Load {
186189
offset: u64,
187190
},
@@ -330,6 +333,9 @@ fn op_to_instruction(op: Op) -> Instruction<'static> {
330333
match op {
331334
Op::LocalGet(i) => Instruction::LocalGet(i),
332335
Op::I32Const(c) => Instruction::I32Const(c),
336+
Op::I64Const(c) => Instruction::I64Const(c),
337+
Op::F32Const(c) => Instruction::F32Const(c.into()),
338+
Op::F64Const(c) => Instruction::F64Const(c.into()),
333339
Op::I32Load { offset } => Instruction::I32Load(memarg(offset, 2)),
334340
Op::I32Store { offset } => Instruction::I32Store(memarg(offset, 2)),
335341
Op::F32Load { offset } => Instruction::F32Load(memarg(offset, 2)),

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

Lines changed: 103 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,21 @@ impl<'a> Parser<'a> {
208208
continue;
209209
}
210210

211+
// Check for a constraint block: `invariant { ... }` (and similar
212+
// region-body annotation blocks). Skipped — the verifier checks
213+
// the schema/carriers, not these source-level constraints.
214+
if self.peek_word("invariant") {
215+
self.expect("invariant")?;
216+
self.skip_whitespace();
217+
self.expect("{")?;
218+
self.skip_to_brace_close();
219+
self.skip_whitespace();
220+
if self.peek_char(';') {
221+
self.expect(";")?;
222+
}
223+
continue;
224+
}
225+
211226
let field_name = self.parse_ident();
212227
self.skip_whitespace();
213228
self.expect(":")?;
@@ -275,7 +290,31 @@ impl<'a> Parser<'a> {
275290
self.expect("<")?;
276291
nullable = true;
277292
}
278-
293+
294+
// Check for ptr<Region> / unique<Region> / ref<Region> field pointer
295+
// (e.g. `next: opt<ptr<FreeSlot>>`). Field-level region pointers, the
296+
// `@Region` form's keyword-with-angle-brackets sibling.
297+
if self.peek_word("ptr") || self.peek_word("unique") || self.peek_word("ref") {
298+
let kw = self.parse_ident();
299+
self.skip_whitespace();
300+
self.expect("<")?;
301+
self.skip_whitespace();
302+
let region_name = self.parse_ident();
303+
self.skip_whitespace();
304+
self.expect(">")?;
305+
if nullable {
306+
self.skip_whitespace();
307+
self.expect(">")?;
308+
}
309+
let kind = match kw.as_str() {
310+
"unique" => PtrKind::Exclusive,
311+
"ref" => PtrKind::Borrow,
312+
_ => PtrKind::Owning,
313+
};
314+
let target = self.region_map.get(&region_name).copied().unwrap_or(0);
315+
return Ok((FieldTy::Ptr { kind, target, nullable }, 1));
316+
}
317+
279318
// Check for @Region reference
280319
if self.peek_char('@') {
281320
self.expect("@")?;
@@ -529,11 +568,19 @@ impl<'a> Parser<'a> {
529568
break;
530569
}
531570

532-
// Parse parameter: name: type (skip name, just get type)
533-
let _param_name = self.parse_ident();
534-
self.skip_whitespace();
535-
self.expect(":")?;
571+
// Parse optional parameter name. Params may be named (`p: &mut
572+
// region<T>`, `dt: f32`) or unnamed (`&mut region<Particles>`,
573+
// `i32`). Detect a `name:` prefix; if absent, rewind and parse the
574+
// type directly.
575+
let save = self.pos;
576+
let maybe_name = self.parse_ident();
536577
self.skip_whitespace();
578+
if !maybe_name.is_empty() && self.peek_char(':') {
579+
self.expect(":")?;
580+
self.skip_whitespace();
581+
} else {
582+
self.pos = save;
583+
}
537584

538585
// Parse the type, which may include ownership qualifiers
539586
let (param_ty, _) = self.parse_param_type()?;
@@ -570,14 +617,27 @@ impl<'a> Parser<'a> {
570617

571618
self.skip_whitespace();
572619

573-
// Skip effects annotation if present: effects { ... }
574-
if self.peek_word("effects") {
575-
self.expect("effects")?;
620+
// Skip any annotation clauses before the body: `effects { ... }`,
621+
// `cost_bound { ... }`, `requires { ... }`, etc. The body is the
622+
// first `{` not preceded by an annotation keyword.
623+
loop {
576624
self.skip_whitespace();
577-
self.expect("{")?;
578-
self.skip_to_brace_close();
625+
if self.peek_char('{') {
626+
break; // function body
627+
}
628+
let at = self.pos;
629+
if at < self.src.len() && (self.src.as_bytes()[at] as char).is_ascii_alphabetic() {
630+
let _annotation = self.parse_ident();
631+
self.skip_whitespace();
632+
if self.peek_char('{') {
633+
self.expect("{")?;
634+
self.skip_to_brace_close();
635+
continue;
636+
}
637+
}
638+
break;
579639
}
580-
640+
581641
self.expect("{")?;
582642

583643
// Parse function body and emit placeholder ops
@@ -590,10 +650,15 @@ impl<'a> Parser<'a> {
590650
body.push(crate::Op::Drop);
591651
}
592652

593-
// If function returns a value, emit a constant 0
594-
if !results.is_empty() {
595-
// For now, assume i32 result
596-
body.push(crate::Op::I32Const(0));
653+
// If the function returns a value, emit a type-correct zero so the
654+
// representative body type-checks against the declared result type.
655+
if let Some(&rty) = results.first() {
656+
body.push(match rty {
657+
Wty::I32 => crate::Op::I32Const(0),
658+
Wty::I64 => crate::Op::I64Const(0),
659+
Wty::F32 => crate::Op::F32Const(0.0),
660+
Wty::F64 => crate::Op::F64Const(0.0),
661+
});
597662
}
598663

599664
let accesses = Vec::new();
@@ -773,13 +838,34 @@ impl<'a> Parser<'a> {
773838

774839
fn parse_import(&mut self) -> Result<(), String> {
775840
self.expect("import")?;
841+
self.skip_whitespace();
842+
// Optional `region` keyword: `import region Name from "module" ...`
843+
if self.peek_word("region") {
844+
self.expect("region")?;
845+
self.skip_whitespace();
846+
}
776847
let _name = self.parse_ident();
777848
self.skip_whitespace();
778849
self.expect("from")?;
779850
self.skip_whitespace();
780-
let _module = self.parse_ident();
851+
// Module source: a quoted string ("game_server") or a bare ident.
852+
if self.peek_char('"') {
853+
self.expect("\"")?;
854+
while self.pos < self.src.len() && self.src.as_bytes()[self.pos] != b'"' {
855+
self.pos += 1;
856+
}
857+
self.expect("\"")?;
858+
} else {
859+
let _module = self.parse_ident();
860+
}
781861
self.skip_whitespace();
782-
self.expect(";")?;
862+
// Either a re-declaration body `{ ... }` (multi-module) or a `;`.
863+
if self.peek_char('{') {
864+
self.expect("{")?;
865+
self.skip_to_brace_close();
866+
} else if self.peek_char(';') {
867+
self.expect(";")?;
868+
}
783869
Ok(())
784870
}
785871
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,29 @@ fn parsed_paint_type_schemas_round_trip() {
153153
assert_round_trips(&ex01_module);
154154
}
155155

156+
/// Round-trip the full example corpus: every `examples/NN-*.twasm` must parse,
157+
/// emit valid wasm, and pass the verifier. This is the v1.0 end-to-end
158+
/// soundness gate (#130) over the canonical six real `.twasm` sources — not
159+
/// just paint-type / example-01. Extending the front-end (ptr<T> fields,
160+
/// unnamed/borrow fn params, annotation clauses, import-region, invariant
161+
/// blocks) took this corpus from 2/6 to 6/6.
162+
#[test]
163+
fn parsed_example_corpus_round_trips() {
164+
let corpus: [(&str, &str); 6] = [
165+
("01-single-module", include_str!("../../../examples/01-single-module.twasm")),
166+
("02-multi-module", include_str!("../../../examples/02-multi-module.twasm")),
167+
("03-ownership-linearity", include_str!("../../../examples/03-ownership-linearity.twasm")),
168+
("04-ecs-game", include_str!("../../../examples/04-ecs-game.twasm")),
169+
("05-tropical-cost", include_str!("../../../examples/05-tropical-cost.twasm")),
170+
("06-epistemic-sync", include_str!("../../../examples/06-epistemic-sync.twasm")),
171+
];
172+
for (name, src) in corpus {
173+
let module =
174+
parser::parse_module(src).unwrap_or_else(|e| panic!("{name}.twasm must parse: {e}"));
175+
assert_round_trips(&module);
176+
}
177+
}
178+
156179
#[test]
157180
fn generated_corpus_round_trips() {
158181
for seed in 0..512u64 {

0 commit comments

Comments
 (0)