plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
12537c4
— Peter John
2026-07-19T21:52:11+05:30
fix(plum-wasm-codegen): assert/todo trap, tail-position If also diverges
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -618,6 +618,28 @@ fn compile_block(block: &ast::Block, body: &mut Vec<u8>, ctx: &LocalCtx, state:
|
|
|
618
618
|
Ok(())
|
|
619
619
|
}
|
|
620
620
|
|
|
621
|
+
/// True if control can never fall through past this statement — every reachable path
|
|
622
|
+
/// ends in a `return`. Used to decide whether a tail-position If/Match needs a
|
|
623
|
+
/// trailing `unreachable` to satisfy wasm's per-block (not whole-function) validation
|
|
624
|
+
/// when the function declares a non-Unit return type.
|
|
625
|
+
fn stmt_always_diverges(stmt: &ast::Stmt) -> bool {
|
|
626
|
+
match stmt {
|
|
627
|
+
ast::Stmt::Return(_) | ast::Stmt::Todo => true,
|
|
628
|
+
ast::Stmt::If(if_) => {
|
|
629
|
+
if_.else_.is_some()
|
|
630
|
+
&& block_always_diverges(&if_.body)
|
|
631
|
+
&& if_.else_ifs.iter().all(|ei| block_always_diverges(&ei.body))
|
|
632
|
+
&& if_.else_.as_ref().is_some_and(block_always_diverges)
|
|
633
|
+
}
|
|
634
|
+
ast::Stmt::Match(m) => !m.cases.is_empty() && m.cases.iter().all(|c| block_always_diverges(&c.body)),
|
|
635
|
+
_ => false,
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
fn block_always_diverges(block: &ast::Block) -> bool {
|
|
640
|
+
block.stmts.last().map(stmt_always_diverges).unwrap_or(false)
|
|
641
|
+
}
|
|
642
|
+
|
|
621
643
|
/// Compiles a block that is the body of a function. If the function returns a value
|
|
622
644
|
/// and the last statement is an expression, that expression's value is left on the
|
|
623
645
|
/// stack instead of being dropped.
|
|
@@ -639,14 +661,14 @@ fn compile_block_as_fn_body(
|
|
|
639
661
|
compile_expr(e, body, ctx, state)?;
|
|
640
662
|
// do NOT drop — this is the return value
|
|
641
663
|
}
|
|
642
|
-
|
|
664
|
+
_ if stmt_always_diverges(last) => {
|
|
643
|
-
|
|
665
|
+
compile_stmt(last, body, ctx, state)?;
|
|
644
|
-
// Every
|
|
666
|
+
// Every branch of this If/Match already `return`s; if it somehow
|
|
645
|
-
//
|
|
667
|
+
// falls through anyway (a bug, or a non-exhaustive match), trap
|
|
646
|
-
// with the required
|
|
668
|
+
// rather than continue with the required result value missing.
|
|
647
669
|
// This also satisfies wasm validation, which requires a value at
|
|
648
670
|
// the function's `end` regardless of whether every branch inside
|
|
649
|
-
//
|
|
671
|
+
// an (BlockType::Empty) if/else chain already returned.
|
|
650
672
|
Instruction::Unreachable.encode(body);
|
|
651
673
|
}
|
|
652
674
|
_ => {
|
|
@@ -764,7 +786,17 @@ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
|
|
|
764
786
|
ast::Stmt::Match(m) => {
|
|
765
787
|
compile_match(m, body, ctx, state)?;
|
|
766
788
|
}
|
|
767
|
-
ast::Stmt::Assert(
|
|
789
|
+
ast::Stmt::Assert(e) => {
|
|
790
|
+
compile_expr(e, body, ctx, state)?;
|
|
791
|
+
Instruction::I32Eqz.encode(body);
|
|
792
|
+
Instruction::If(BlockType::Empty).encode(body);
|
|
793
|
+
Instruction::Unreachable.encode(body);
|
|
794
|
+
Instruction::End.encode(body);
|
|
795
|
+
}
|
|
796
|
+
ast::Stmt::Todo => {
|
|
797
|
+
// Marks an unimplemented body — trap rather than silently continuing.
|
|
798
|
+
Instruction::Unreachable.encode(body);
|
|
799
|
+
}
|
|
768
800
|
}
|
|
769
801
|
Ok(())
|
|
770
802
|
}
|
plum-wasm-codegen/tests/codegen_tests.rs
CHANGED
|
@@ -305,3 +305,57 @@ main() -> Int =
|
|
|
305
305
|
let bytes = compile_source(&source).expect("compile failed");
|
|
306
306
|
assert_eq!(run_main(&bytes), 42);
|
|
307
307
|
}
|
|
308
|
+
|
|
309
|
+
#[test]
|
|
310
|
+
fn assert_traps_on_false_and_passes_through_on_true() {
|
|
311
|
+
let src_ok = "\
|
|
312
|
+
check(n: Int) -> Int =
|
|
313
|
+
assert n > 0
|
|
314
|
+
n
|
|
315
|
+
|
|
316
|
+
main() -> Int =
|
|
317
|
+
check(5)
|
|
318
|
+
";
|
|
319
|
+
let source = parse(src_ok);
|
|
320
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
321
|
+
assert_eq!(run_main(&bytes), 5);
|
|
322
|
+
|
|
323
|
+
let src_trap = "\
|
|
324
|
+
check(n: Int) -> Int =
|
|
325
|
+
assert n > 0
|
|
326
|
+
n
|
|
327
|
+
|
|
328
|
+
main() -> Int =
|
|
329
|
+
check(-1)
|
|
330
|
+
";
|
|
331
|
+
let source = parse(src_trap);
|
|
332
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
333
|
+
let engine = wasmtime::Engine::default();
|
|
334
|
+
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
335
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
336
|
+
let instance = wasmtime::Instance::new(&mut store, &module, &[]).unwrap();
|
|
337
|
+
let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
|
|
338
|
+
let err = main.call(&mut store, ()).expect_err("a false assert should trap, not silently continue");
|
|
339
|
+
assert_eq!(err.downcast_ref::<wasmtime::Trap>(), Some(&wasmtime::Trap::UnreachableCodeReached), "got: {}", err);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
#[test]
|
|
343
|
+
fn todo_traps_at_runtime() {
|
|
344
|
+
// `todo` marks an unimplemented body — it must trap, not silently do nothing.
|
|
345
|
+
let src = "\
|
|
346
|
+
notDoneYet() -> Int =
|
|
347
|
+
todo
|
|
348
|
+
|
|
349
|
+
main() -> Int =
|
|
350
|
+
notDoneYet()
|
|
351
|
+
";
|
|
352
|
+
let source = parse(src);
|
|
353
|
+
let bytes = compile_source(&source).expect("compile failed");
|
|
354
|
+
let engine = wasmtime::Engine::default();
|
|
355
|
+
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
356
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
357
|
+
let instance = wasmtime::Instance::new(&mut store, &module, &[]).unwrap();
|
|
358
|
+
let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
|
|
359
|
+
let err = main.call(&mut store, ()).expect_err("todo should trap");
|
|
360
|
+
assert_eq!(err.downcast_ref::<wasmtime::Trap>(), Some(&wasmtime::Trap::UnreachableCodeReached), "got: {}", err);
|
|
361
|
+
}
|