plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


12537c4Peter 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
- ast::Stmt::Match(m) => {
664
+ _ if stmt_always_diverges(last) => {
643
- compile_match(m, body, ctx, state)?;
665
+ compile_stmt(last, body, ctx, state)?;
644
- // Every arm of a tail-position match must itself `return`; if a
666
+ // Every branch of this If/Match already `return`s; if it somehow
645
- // non-exhaustive match falls through, trap rather than continue
667
+ // falls through anyway (a bug, or a non-exhaustive match), trap
646
- // with the required-but-never-produced result value missing.
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
- // the match's (BlockType::Empty) if/else chain already returned.
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(_) | ast::Stmt::Todo => {}
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
+ }