Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/typed-wasm-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ impl Wty {
pub enum Op {
LocalGet(u32),
I32Const(i32),
I64Const(i64),
F32Const(f32),
F64Const(f64),
I32Load {
offset: u64,
},
Expand Down Expand Up @@ -330,6 +333,9 @@ fn op_to_instruction(op: Op) -> Instruction<'static> {
match op {
Op::LocalGet(i) => Instruction::LocalGet(i),
Op::I32Const(c) => Instruction::I32Const(c),
Op::I64Const(c) => Instruction::I64Const(c),
Op::F32Const(c) => Instruction::F32Const(c.into()),
Op::F64Const(c) => Instruction::F64Const(c.into()),
Op::I32Load { offset } => Instruction::I32Load(memarg(offset, 2)),
Op::I32Store { offset } => Instruction::I32Store(memarg(offset, 2)),
Op::F32Load { offset } => Instruction::F32Load(memarg(offset, 2)),
Expand Down
135 changes: 113 additions & 22 deletions crates/typed-wasm-codegen/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ impl<'a> Parser<'a> {

fn peek_word(&mut self, word: &str) -> bool {
let start = self.pos;
if self.src[start..].starts_with(word) {
// Panic-safe: `get` returns None on an out-of-range / non-char-boundary
// index instead of panicking on malformed or truncated input.
if self.src.get(start..).map_or(false, |rest| rest.starts_with(word)) {
let next_char = self.src.as_bytes().get(start + word.len());
if next_char.is_none() || !next_char.unwrap().is_ascii_alphabetic() {
return true;
Expand Down Expand Up @@ -132,15 +134,18 @@ impl<'a> Parser<'a> {

fn expect(&mut self, s: &str) -> Result<(), String> {
self.skip_whitespace();
if self.src[self.pos..].starts_with(s) {
if self.src.get(self.pos..).map_or(false, |rest| rest.starts_with(s)) {
self.pos += s.len();
Ok(())
} else {
// Panic-safe error context: clamp to the end of input and fall back
// if the window straddles a UTF-8 boundary, so malformed/truncated
// input yields an Err, never an out-of-bounds slice panic.
let end = (self.pos + 20).min(self.src.len());
let found = self.src.get(self.pos..end).unwrap_or("<end-of-input>");
Err(format!(
"Expected '{}' at position {}, found '{}'",
s,
self.pos,
&self.src[self.pos..self.pos + 20]
s, self.pos, found
))
}
}
Expand Down Expand Up @@ -208,6 +213,21 @@ impl<'a> Parser<'a> {
continue;
}

// Check for a constraint block: `invariant { ... }` (and similar
// region-body annotation blocks). Skipped — the verifier checks
// the schema/carriers, not these source-level constraints.
if self.peek_word("invariant") {
self.expect("invariant")?;
self.skip_whitespace();
self.expect("{")?;
self.skip_to_brace_close();
self.skip_whitespace();
if self.peek_char(';') {
self.expect(";")?;
}
continue;
}

let field_name = self.parse_ident();
self.skip_whitespace();
self.expect(":")?;
Expand Down Expand Up @@ -275,7 +295,31 @@ impl<'a> Parser<'a> {
self.expect("<")?;
nullable = true;
}


// Check for ptr<Region> / unique<Region> / ref<Region> field pointer
// (e.g. `next: opt<ptr<FreeSlot>>`). Field-level region pointers, the
// `@Region` form's keyword-with-angle-brackets sibling.
if self.peek_word("ptr") || self.peek_word("unique") || self.peek_word("ref") {
let kw = self.parse_ident();
self.skip_whitespace();
self.expect("<")?;
self.skip_whitespace();
let region_name = self.parse_ident();
self.skip_whitespace();
self.expect(">")?;
if nullable {
self.skip_whitespace();
self.expect(">")?;
}
let kind = match kw.as_str() {
"unique" => PtrKind::Exclusive,
"ref" => PtrKind::Borrow,
_ => PtrKind::Owning,
};
let target = self.region_map.get(&region_name).copied().unwrap_or(0);
return Ok((FieldTy::Ptr { kind, target, nullable }, 1));
}

// Check for @Region reference
if self.peek_char('@') {
self.expect("@")?;
Expand Down Expand Up @@ -529,11 +573,19 @@ impl<'a> Parser<'a> {
break;
}

// Parse parameter: name: type (skip name, just get type)
let _param_name = self.parse_ident();
self.skip_whitespace();
self.expect(":")?;
// Parse optional parameter name. Params may be named (`p: &mut
// region<T>`, `dt: f32`) or unnamed (`&mut region<Particles>`,
// `i32`). Detect a `name:` prefix; if absent, rewind and parse the
// type directly.
let save = self.pos;
let maybe_name = self.parse_ident();
self.skip_whitespace();
if !maybe_name.is_empty() && self.peek_char(':') {
self.expect(":")?;
self.skip_whitespace();
} else {
self.pos = save;
}

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

self.skip_whitespace();

// Skip effects annotation if present: effects { ... }
if self.peek_word("effects") {
self.expect("effects")?;
// Skip any annotation clauses before the body: `effects { ... }`,
// `cost_bound { ... }`, `requires { ... }`, etc. The body is the
// first `{` not preceded by an annotation keyword.
loop {
self.skip_whitespace();
self.expect("{")?;
self.skip_to_brace_close();
if self.peek_char('{') {
break; // function body
}
let at = self.pos;
if at < self.src.len() && (self.src.as_bytes()[at] as char).is_ascii_alphabetic() {
let _annotation = self.parse_ident();
self.skip_whitespace();
if self.peek_char('{') {
self.expect("{")?;
self.skip_to_brace_close();
continue;
}
}
break;
}

self.expect("{")?;

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

// If function returns a value, emit a constant 0
if !results.is_empty() {
// For now, assume i32 result
body.push(crate::Op::I32Const(0));
// If the function returns a value, emit a type-correct zero so the
// representative body type-checks against the declared result type.
if let Some(&rty) = results.first() {
body.push(match rty {
Wty::I32 => crate::Op::I32Const(0),
Wty::I64 => crate::Op::I64Const(0),
Wty::F32 => crate::Op::F32Const(0.0),
Wty::F64 => crate::Op::F64Const(0.0),
});
}

let accesses = Vec::new();
Expand Down Expand Up @@ -773,13 +843,34 @@ impl<'a> Parser<'a> {

fn parse_import(&mut self) -> Result<(), String> {
self.expect("import")?;
self.skip_whitespace();
// Optional `region` keyword: `import region Name from "module" ...`
if self.peek_word("region") {
self.expect("region")?;
self.skip_whitespace();
}
let _name = self.parse_ident();
self.skip_whitespace();
self.expect("from")?;
self.skip_whitespace();
let _module = self.parse_ident();
// Module source: a quoted string ("game_server") or a bare ident.
if self.peek_char('"') {
self.expect("\"")?;
while self.pos < self.src.len() && self.src.as_bytes()[self.pos] != b'"' {
self.pos += 1;
}
self.expect("\"")?;
} else {
let _module = self.parse_ident();
}
self.skip_whitespace();
self.expect(";")?;
// Either a re-declaration body `{ ... }` (multi-module) or a `;`.
if self.peek_char('{') {
self.expect("{")?;
self.skip_to_brace_close();
} else if self.peek_char(';') {
self.expect(";")?;
}
Ok(())
}
}
Expand Down
54 changes: 54 additions & 0 deletions crates/typed-wasm-codegen/tests/corpus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,60 @@ fn parsed_paint_type_schemas_round_trip() {
assert_round_trips(&ex01_module);
}

/// Round-trip the full example corpus: every `examples/NN-*.twasm` must parse,
/// emit valid wasm, and pass the verifier. This is the v1.0 end-to-end
/// soundness gate (#130) over the canonical six real `.twasm` sources — not
/// just paint-type / example-01. Extending the front-end (ptr<T> fields,
/// unnamed/borrow fn params, annotation clauses, import-region, invariant
/// blocks) took this corpus from 2/6 to 6/6.
#[test]
fn parsed_example_corpus_round_trips() {
let corpus: [(&str, &str); 6] = [
("01-single-module", include_str!("../../../examples/01-single-module.twasm")),
("02-multi-module", include_str!("../../../examples/02-multi-module.twasm")),
("03-ownership-linearity", include_str!("../../../examples/03-ownership-linearity.twasm")),
("04-ecs-game", include_str!("../../../examples/04-ecs-game.twasm")),
("05-tropical-cost", include_str!("../../../examples/05-tropical-cost.twasm")),
("06-epistemic-sync", include_str!("../../../examples/06-epistemic-sync.twasm")),
];
for (name, src) in corpus {
let module =
parser::parse_module(src).unwrap_or_else(|e| panic!("{name}.twasm must parse: {e}"));
assert_round_trips(&module);
}
}

/// Reinforce: a malformed or truncated `.twasm` must yield `Ok`/`Err`, never a
/// panic. Feeds char-boundary truncations of every example plus adversarial
/// fragments (unbalanced delimiters, UTF-8, partial type forms) through the
/// parser; the test fails if any input panics (out-of-bounds slice, etc.).
#[test]
fn parser_never_panics_on_malformed_input() {
let examples: [&str; 6] = [
include_str!("../../../examples/01-single-module.twasm"),
include_str!("../../../examples/02-multi-module.twasm"),
include_str!("../../../examples/03-ownership-linearity.twasm"),
include_str!("../../../examples/04-ecs-game.twasm"),
include_str!("../../../examples/05-tropical-cost.twasm"),
include_str!("../../../examples/06-epistemic-sync.twasm"),
];
for src in examples {
for cut in (0..src.len()).step_by(7) {
if src.is_char_boundary(cut) {
let _ = parser::parse_module(&src[..cut]); // must not panic
}
}
}
for frag in [
"", "region", "region X", "region X {", "region X { f: ",
"region X { f: i32", "fn", "fn f(", "fn f(&mut region<",
"fn f() -> ", "import region", "import region X from \"",
"opt<ptr<", "区域 region", "{{{{{", "<<<<<", "region X { invariant {",
] {
let _ = parser::parse_module(frag); // must not panic
}
}

#[test]
fn generated_corpus_round_trips() {
for seed in 0..512u64 {
Expand Down
Loading