plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-wasm-codegen/tests/codegen_tests.rs
#![allow(non_snake_case)]
use plum_wasm_codegen::compileSource;
use plum_core::AstParser;
use wasm_encoder::Encode;
fn parse(src: &str) -> plum_core::ast::Source {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
let tree = parser.parse(src, None).unwrap();
let ap = AstParser::new(src);
ap.parseSource(tree.root_node())
}
/// The wasm-gc migration (docs/superpowers/plans/2026-07-25-wasm-gc-migration.md)
/// needs both `wasm_gc` and `wasm_function_references` enabled — confirmed
/// empirically (see the `wasmtimeGcConfig*` tests below) rather than assumed from
/// docs, since wasmtime's own doc comment on `Config::wasm_gc` warns its GC support
/// is still in progress. Every test that instantiates/runs compiled output uses
/// this shared engine so the whole harness stays on one config.
fn gcEngine() -> wasmtime::Engine {
let mut config = wasmtime::Config::new();
config.wasm_gc(true);
config.wasm_function_references(true);
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
}
#[test]
fn compilesToValidWasm() {
let src = "fun add(a: Int, b: Int) -> Int =\n a + b\n";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
// Valid WASM starts with the magic number
assert_eq!(&bytes[0..4], b"\0asm");
assert_eq!(&bytes[4..8], &[1, 0, 0, 0]); // version 1
}
#[test]
fn outputValidates() {
let src = "fun add(a: Int, b: Int) -> Int =\n a + b\n";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
// wasmparser should accept the output
let result = wasmparser::validate(&bytes);
assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
}
#[test]
fn factorialCompiles() {
let src = "\
fun factorial(x: Int) -> Int =
if x < 2
return 1
return x * factorial(x - 1)
";
let source = parse(src);
let bytes = compileSource(&source).expect("factorial should compile");
assert_eq!(&bytes[0..4], b"\0asm");
let result = wasmparser::validate(&bytes);
assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
}
#[test]
fn give42CompilesAndExports() {
let src = "fun give42() -> Int =\n 42\n";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(&bytes[0..4], b"\0asm");
let result = wasmparser::validate(&bytes);
assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
}
fn assertValid(src: &str) -> Vec<u8> {
let source = parse(src);
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed for {:?}: {}", src, e));
let result = wasmparser::validate(&bytes);
assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
bytes
}
#[test]
fn boolLiteralsCompile() {
assertValid("fun main() -> Bool =\n True\n");
assertValid("fun main() -> Bool =\n False\n");
}
#[test]
fn stringLiteralCompiles() {
assertValid("fun main() -> Str =\n \"hello\"\n");
}
#[test]
fn emptyStringLiteralCompiles() {
assertValid("fun main() -> Str =\n \"\"\n");
}
#[test]
fn stringInterpolationOfAnIntRunsCorrectly() {
let src = "fun main() -> Str =\n x = 42\n \"{x}\"\n";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMainStr(&bytes), "42");
}
#[test]
fn stringInterpolationOfANegativeIntRunsCorrectly() {
let src = "fun main() -> Str =\n x = -7\n \"{x}\"\n";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMainStr(&bytes), "-7");
}
#[test]
fn stringInterpolationOfZeroRunsCorrectly() {
let src = "fun main() -> Str =\n x = 0\n \"{x}\"\n";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMainStr(&bytes), "0");
}
#[test]
fn stringInterpolationWithSurroundingTextAndMultipleInterpsRunsCorrectly() {
let src = "fun main() -> Str =\n count = 3\n total = 10\n \"{count} of {total} complete\"\n";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMainStr(&bytes), "3 of 10 complete");
}
#[test]
fn stringInterpolationOfAStrRunsCorrectly() {
let src = "fun greet(name: Str) -> Str =\n \"Hello, {name}!\"\n\nfun main() -> Str =\n greet(\"World\")\n";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMainStr(&bytes), "Hello, World!");
}
#[test]
fn stringInterpolationOfABoolRunsCorrectly() {
let src = "fun main() -> Str =\n b = True\n \"is {b}\"\n";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMainStr(&bytes), "is True");
}
#[test]
fn stringInterpolationOfAFloatIsAClearError() {
// Float-to-decimal-string formatting is a substantial separate undertaking
// (correct rounding needs something like Grisu/Ryu); scoped out for now with
// an explicit error rather than emitting an incorrect conversion.
let src = "fun main() -> Str =\n x = 1.5\n \"{x}\"\n";
let source = parse(src);
let err = compileSource(&source).expect_err("float interpolation should not silently compile");
assert!(err.contains("Float"), "got: {}", err);
}
#[test]
fn floatArithmeticAndNegationCompile() {
assertValid("fun main(x: Float) -> Float =\n y = -x\n y + 1.5\n");
}
#[test]
fn classFieldAndMethodCompile() {
let src = "\
type Cat =
name: Str
age: Int
fun getAge() -> Int =
self.age
fun makeCat() -> Int =
c = Cat(name: \"x\", age: 3)
c.getAge()
";
assertValid(src);
}
#[test]
fn nestedClassCallCompiles() {
let src = "\
type Pair =
a: Int
b: Int
type Wrapper =
inner: Pair
tag: Int
fun make() -> Int =
w = Wrapper(inner: Pair(a: 1, b: 2), tag: 9)
w.tag
";
assertValid(src);
}
#[test]
fn matchWithIntAndWildcardCompiles() {
let src = "fun main(a: Int) -> Int =\n match a\n 1 =>\n return 10\n _ =>\n return 0\n";
assertValid(src);
}
#[test]
fn matchBindingPatternCompiles() {
let src = "fun main(a: Int) -> Int =\n match a\n x =>\n return x\n";
assertValid(src);
}
#[test]
fn matchInlineCaseBodyCompiles() {
// Case bodies can be a single inline expression, not just an indented block.
let src = "fun main(a: Int) =\n match a\n 1 => 10\n _ => 0\n";
assertValid(src);
}
#[test]
fn matchBoolVariantPatternCompiles() {
let src = "fun main(a: Bool) -> Int =\n match a\n True =>\n return 1\n False =>\n return 0\n";
assertValid(src);
}
#[test]
fn matchStringPatternIsAClearError() {
let src = "fun main(a: Str) -> Int =\n match a\n \"x\" =>\n 1\n _ =>\n 0\n";
let source = parse(src);
let err = compileSource(&source).expect_err("string match patterns are not yet supported");
assert!(err.contains("string match"), "got: {}", err);
}
#[test]
fn nestedConstructorPatternMatchesAndBindsRunsCorrectly() {
let src = "\
enum Option =
| Some[Int]
| None
enum Nested =
| Wrap[Option]
| Empty
fun f(n: Nested) -> Int =
match n
Wrap(Some(v)) =>
return v
Wrap(None) =>
return -1
Empty =>
return 0
fun main() -> Int =
f(Wrap(Some(5)))
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 5);
}
#[test]
fn nestedConstructorPatternMismatchFallsThroughToNextCaseRunsCorrectly() {
// `Wrap(Some(v))` shouldn't match a runtime `Wrap(None)` — codegen must fall
// through to the next *top-level* case, not just fail to bind `v`.
let src = "\
enum Option =
| Some[Int]
| None
enum Nested =
| Wrap[Option]
| Empty
fun f(n: Nested) -> Int =
match n
Wrap(Some(v)) =>
return v
Wrap(None) =>
return -1
Empty =>
return 0
fun main() -> Int =
f(Wrap(None))
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), -1);
}
#[test]
fn nestedConstructorPatternAgainstASpecializedGenericEnumRunsCorrectly() {
// Exercises monomorphize.rs's recursive mangling: the outer `Full(...)` pattern
// matches against `Box`'s own specialization, but the *inner* `Some(v)`/`None`
// sub-pattern matches against `Box`'s generic field type (`Option`, itself
// specialized to `Option$Int`) — each level needs its own mangling table, not
// just the outermost one.
let src = "\
enum Option =
| Some[T]
| None
enum Box =
| Full[T]
| Empty
fun unwrap(b: Box) -> Int =
match b
Full(Some(v)) => v
Full(None) => -1
Empty => 0
fun main() -> Int =
unwrap(Full(Some(7)))
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 7);
}
#[test]
fn doublyNestedConstructorPatternRunsCorrectly() {
// `Some(Some(v))` — two levels of nesting, proving the recursion isn't just
// one level deep.
let src = "\
enum Option =
| Some[Option]
| None
fun unwrapTwice(o: Option) -> Int =
match o
Some(Some(None)) => 1
Some(None) => 2
None => 3
_ => 0
fun main() -> Int =
unwrapTwice(Some(Some(None)))
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 1);
}
/// Runs `main`'s wasm bytes and returns its i64 result. wasm's own validator
/// (via wasmparser, above) only proves the module is well-formed — it can't catch
/// wrong *values*, so these tests actually execute the compiled output.
fn runMain(bytes: &[u8]) -> i64 {
let engine = gcEngine();
let module = wasmtime::Module::new(&engine, bytes).expect("module should be loadable");
let mut store = wasmtime::Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
let main = instance
.get_typed_func::<(), i64>(&mut store, "main")
.expect("main should have signature () -> i64");
main.call(&mut store, ()).expect("main should not trap")
}
fn runMainF64(bytes: &[u8]) -> f64 {
let engine = gcEngine();
let module = wasmtime::Module::new(&engine, bytes).expect("module should be loadable");
let mut store = wasmtime::Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
let main = instance
.get_typed_func::<(), f64>(&mut store, "main")
.expect("main should have signature () -> f64");
main.call(&mut store, ()).expect("main should not trap")
}
/// Runs a `() -> Str`-returning `main`, reading the returned `array<i8>` GC value
/// back out byte-by-byte via wasmtime's host-side GC ref API (`Str` has no length
/// prefix of its own now — `array.len` is native, see Decision 4 of the wasm-gc
/// migration plan) — untyped `Func::call` is used because `main`'s wasm return type
/// is a concrete `(ref $Str)`, not one `get_typed_func` can name directly.
fn runMainStr(bytes: &[u8]) -> String {
let engine = gcEngine();
let module = wasmtime::Module::new(&engine, bytes).expect("module should be loadable");
let mut store = wasmtime::Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
let main = instance.get_func(&mut store, "main").expect("module should export main");
let mut results = [wasmtime::Val::null_any_ref()];
main.call(&mut store, &[], &mut results).expect("main should not trap");
let any_ref = match &results[0] {
wasmtime::Val::AnyRef(Some(r)) => *r,
other => panic!("main should return a non-null anyref (Str), got {:?}", other),
};
let array = any_ref.unwrap_array(&store).expect("Str's returned anyref should be a GC array");
let len = array.len(&store).expect("array.len should succeed");
let mut bytes_out = Vec::with_capacity(len as usize);
for i in 0..len {
let byte = match array.get(&mut store, i).expect("array.get should succeed") {
wasmtime::Val::I32(b) => b as u8,
other => panic!("Str array element should be i32, got {:?}", other),
};
bytes_out.push(byte);
}
String::from_utf8(bytes_out).expect("string bytes should be valid utf8")
}
#[test]
fn factorialRunsCorrectly() {
let src = "\
fun factorial(x: Int) -> Int =
if x < 2
return 1
return x * factorial(x - 1)
fun main() -> Int =
factorial(5)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 120);
}
#[test]
fn classFieldAndMethodRunCorrectly() {
let src = "\
type Cat =
name: Str
age: Int
fun getAge() -> Int =
self.age
fun main() -> Int =
c = Cat(name: \"x\", age: 7)
c.getAge()
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 7);
}
#[test]
fn nestedMethodDeclarationRunsCorrectly() {
let src = "\
type Cat =
name: Str
age: Int
fun getAge(self) -> Int =
self.age
fun main() -> Int =
c = Cat(name: \"x\", age: 7)
c.getAge()
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 7);
}
#[test]
fn enumDiscriminantValueFieldAccessRunsCorrectlyForEachVariant() {
let src = "\
enum Step(n: Int) =
| ReadMin(10)
| ReadMax(20)
fun toNumber(self) -> Int =
self.n
fun main() -> Int =
ReadMin.toNumber() * 100 + ReadMax.toNumber()
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 1020);
}
#[test]
fn enumDiscriminantValueMatchesByVariantNameCorrectly() {
let src = "\
enum Step(n: Int) =
| ReadMin(10)
| ReadMax(20)
fun toNumber(s: Step) -> Int =
match s
ReadMin => 1
ReadMax => 2
fun main() -> Int =
toNumber(ReadMin) * 10 + toNumber(ReadMax)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 12);
}
#[test]
fn nestedClassCallRunsCorrectly() {
let src = "\
type Pair =
a: Int
b: Int
type Wrapper =
inner: Pair
tag: Int
fun main() -> Int =
w = Wrapper(inner: Pair(a: 11, b: 22), tag: 99)
w.inner.b
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 22);
}
#[test]
fn repeatedClassCallInALoopDoesNotAlias() {
// Regression test: class instances are bump-allocated at *runtime* (via a
// mutable wasm global), not at a compile-time-fixed address — otherwise every
// iteration's `Box(...)` would alias the same memory and this would sum to 5*4=20
// instead of 0+1+2+3+4=10.
let src = "\
type Box =
v: Int
fun sumBoxes() -> Int =
total = 0
for i := range 5
b = Box(v: i)
total = total + b.v
return total
fun main() -> Int =
sumBoxes()
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 10);
}
#[test]
fn matchIntAndWildcardRunCorrectly() {
let src = "\
fun classify(a: Int) -> Int =
match a
1 =>
return 100
2 =>
return 200
_ =>
return 0
fun main() -> Int =
classify(2)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 200);
}
#[test]
fn matchBoolVariantPatternRunsCorrectly() {
// Regression test: True/False are built-in Bool variants and must be treated as
// tag comparisons, not bindings, even without an explicit `enum Bool` in this
// source file — otherwise the first arm always "matches" (as a rebinding) and
// `pick(False)` would wrongly return 1.
let src = "\
fun pick(a: Bool) -> Int =
match a
True =>
return 1
False =>
return 0
fun main() -> Int =
pick(False)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 0);
}
#[test]
fn lowercaseSingleWordFunctionCallRunsCorrectly() {
// Regression test: `factorial(...)` (an all-lowercase, no-uppercase, no-underscore
// callee) used to fail to parse at all — `var_identifier` and `fn_identifier` both
// matched its text and the grammar's lexer would nondeterministically commit to
// `var_identifier`, breaking every such call site.
let src = "\
fun double(n: Int) -> Int =
n * 2
fun main() -> Int =
double(21)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 42);
}
#[test]
fn assertTrapsOnFalseAndPassesThroughOnTrue() {
let src_ok = "\
fun check(n: Int) -> Int =
assert n > 0
n
fun main() -> Int =
check(5)
";
let source = parse(src_ok);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 5);
let src_trap = "\
fun check(n: Int) -> Int =
assert n > 0
n
fun main() -> Int =
check(-1)
";
let source = parse(src_trap);
let bytes = compileSource(&source).expect("compile failed");
let engine = gcEngine();
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
let mut store = wasmtime::Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &module, &[]).unwrap();
let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
let err = main.call(&mut store, ()).expect_err("a false assert should trap, not silently continue");
assert_eq!(err.downcast_ref::<wasmtime::Trap>(), Some(&wasmtime::Trap::UnreachableCodeReached), "got: {}", err);
}
#[test]
fn todoTrapsAtRuntime() {
// `todo` marks an unimplemented body — it must trap, not silently do nothing.
let src = "\
fun notDoneYet() -> Int =
todo
fun main() -> Int =
notDoneYet()
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
let engine = gcEngine();
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
let mut store = wasmtime::Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &module, &[]).unwrap();
let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
let err = main.call(&mut store, ()).expect_err("todo should trap");
assert_eq!(err.downcast_ref::<wasmtime::Trap>(), Some(&wasmtime::Trap::UnreachableCodeReached), "got: {}", err);
}
#[test]
fn payloadFreeVariantConstructionCompiles() {
let src = "\
enum Color =
| Red
| Green
| Blue
fun main() -> Int =\n x = Green\n 0\n";
assertValid(src);
}
#[test]
fn payloadVariantConstructionCompilesAndRuns() {
let src = "\
enum Option =
| Some[Int]
| None
fun unwrapOr(o: Option, default: Int) -> Int =
match o
Some(v) =>
return v
None =>
return default
fun main() -> Int =
unwrapOr(Some(7), 0)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 7);
}
#[test]
fn multiFieldVariantConstructionCompilesAndRuns() {
let src = "\
enum Shape =
| Rect[Int, Int]
| Circle[Int]
fun area(s: Shape) -> Int =
match s
Rect(w, h) =>
return w * h
Circle(r) =>
return r * r
fun main() -> Int =
area(Rect(3, 4))
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 12);
}
#[test]
fn nonBoolBareTagPatternRunsCorrectly() {
let src = "\
enum Color =
| Red
| Green
| Blue
fun code(c: Color) -> Int =
match c
Red =>
return 1
Green =>
return 2
Blue =>
return 3
fun main() -> Int =
code(Green)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 2);
}
#[test]
fn constructorPatternWildcardFieldRunsCorrectly() {
let src = "\
enum Option =
| Some[Int]
| None
fun isSome(o: Option) -> Int =
match o
Some(_) =>
return 1
None =>
return 0
fun main() -> Int =
isSome(Some(99))
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 1);
}
#[test]
fn constructorPatternDoesNotMisfireOnPayloadFreeSibling() {
// Regression test: `None` is a small-int tag, not a heap pointer. The
// constructor-pattern arm for `Some(v)` must not treat a payload-free
// sibling value as if it were a pointer to a `Some` payload.
let src = "\
enum Option =
| Some[Int]
| None
fun unwrapOr(o: Option, default: Int) -> Int =
match o
Some(v) =>
return v
None =>
return default
fun main() -> Int =
unwrapOr(None, 5)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 5);
}
#[test]
fn enumClassFieldConstructAndDestructureRunsCorrectly() {
// Interop check: a class with a field of an enum type, constructed with a
// payload variant, then matched via the class field.
let src = "\
enum Option =
| Some[Int]
| None
type Box =
value: Option
fun unwrap(default: Int) -> Int =
match self.value
Some(v) =>
return v
None =>
return default
fun main() -> Int =
b = Box(value: Some(42))
b.unwrap(0)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 42);
}
#[test]
fn tailMatchWithoutReturnRunsCorrectly() {
let src = "\
fun bindExample(n: Int) -> Int =
match n
x =>
x
fun main() -> Int =
bindExample(5)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 5);
}
#[test]
fn tailIfWithoutReturnRunsCorrectly() {
let src = "\
fun abs(n: Int) -> Int =
if n < 0
-n
else
n
fun main() -> Int =
abs(-7)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 7);
}
#[test]
fn tailIfNestedInsideMatchArmWithoutReturnRunsCorrectly() {
let src = "\
fun classify(n: Int) -> Int =
match n
0 =>
1
x =>
if x < 0
-1
else
2
fun main() -> Int =
classify(-5)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), -1);
}
#[test]
fn tailMatchMixingReturnAndBareExprArmsRunsCorrectly() {
let src = "\
fun describe(n: Int) -> Int =
match n
0 =>
return 100
x =>
x * 2
fun main() -> Int =
describe(21)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 42);
}
#[test]
fn tailEnumMatchWithoutReturnRunsCorrectly() {
let src = "\
enum Option =
| Some[Int]
| None
fun unwrapOr(o: Option, default: Int) -> Int =
match o
Some(v) =>
v
None =>
default
fun main() -> Int =
unwrapOr(Some(9), 0)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 9);
}
#[test]
fn tailIfWithoutElseIsAClearError() {
let src = "\
fun bad(n: Int) -> Int =
if n < 0
return 1
";
let source = parse(src);
let err = compileSource(&source).expect_err("if without else in value position must be a clear error, not invalid wasm");
assert!(err.contains("doesn't produce a return value"), "got: {}", err);
}
#[test]
fn tailMatchNonExhaustiveIsAClearError() {
let src = "\
fun bad(n: Int) -> Int =
match n
0 =>
1
";
let source = parse(src);
let err = compileSource(&source).expect_err("non-exhaustive match in value position must be a clear error, not invalid wasm");
assert!(err.contains("doesn't produce a return value"), "got: {}", err);
}
#[test]
fn tailMatchArmEndingInNonValueStatementIsAClearError() {
let src = "\
fun bad(n: Int) -> Int =
match n
x =>
y = x
";
let source = parse(src);
let err = compileSource(&source).expect_err("a match arm ending in a non-value statement must be a clear error, not invalid wasm");
assert!(err.contains("doesn't produce a return value"), "got: {}", err);
}
#[test]
fn genericClassSpecializedAtTwoTypesDoesNotAlias() {
let src = "\
type Box[T] =
value: T
fun getIntValue() -> Int =
self.value
fun useInt() -> Int =
b = Box(value: 7)
b.getIntValue()
fun main() -> Int =
useInt()
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 7);
}
#[test]
fn genericFunctionCalledAtMultipleConcreteTypesRunsCorrectly() {
let src = "\
fun identity(value: T) -> T =
value
fun main() -> Int =
identity(5) + identity(37)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 42);
}
#[test]
fn genericMethodOnGenericClassRunsCorrectly() {
let src = "\
type Box[T] =
value: T
fun getValue() -> Int =
self.value
fun main() -> Int =
b = Box(value: 9)
b.getValue()
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 9);
}
#[test]
fn transitivelyGenericCallChainRunsCorrectly() {
let src = "\
fun identity(value: T) -> T =
value
fun doubled(value: T) -> Int =
identity(value) + identity(value)
fun main() -> Int =
doubled(21)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 42);
}
#[test]
fn genericEnumSpecializedAndMatchedRunsCorrectly() {
let src = "\
enum Option =
| Some[T]
| None
fun unwrapOr(o: Option, default: Int) -> Int =
match o
Some(v) =>
v
None =>
default
fun main() -> Int =
unwrapOr(Some(13), 0)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 13);
}
#[test]
fn genericEnumMultipleInstantiationsCoexistAndRunCorrectly() {
// `Str.length()` is not a real, working method in this codebase (no built-in
// Str methods exist in codegen, and string-literal match patterns are an
// explicit, documented "not yet supported" error — see
// `matchStringPatternIsAClearError` above). So the `Some(v) => ...` arm
// for the Str instantiation returns a fixed literal instead of deriving
// anything from `v`'s content; the point of this test is that `Option$Str`
// coexists with `Option$Int` and both run correctly, not string processing.
let src = "\
enum Option =
| Some[T]
| None
fun unwrapIntOr(o: Option, default: Int) -> Int =
match o
Some(v) =>
v
None =>
default
fun unwrapStrOr(o: Option, default: Int) -> Int =
match o
Some(v) =>
4
None =>
default
fun main() -> Int =
unwrapIntOr(Some(13), 0) + unwrapStrOr(Some(\"abcd\"), 0)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 17);
}
#[test]
fn sameBareGenericEnumParamFunctionCalledMultipleTimesRunsCorrectly() {
// Regression test: the same generic function taking a bare generic-enum-typed param,
// called at the same concrete instantiation multiple times, must correctly specialize
// and reuse that specialization. This tests that the mangling logic for `unwrapOr`
// produces identical specialized code on both call sites, not aliased/incorrect code.
let src = "\
enum Option =
| Some[T]
| None
fun unwrapOr(o: Option, default: Int) -> Int =
match o
Some(v) =>
v
None =>
default
fun main() -> Int =
unwrapOr(Some(5), 0) + unwrapOr(Some(37), 0)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 42);
}
#[test]
fn ordinaryFunctionWithBareGenericClassParamRunsCorrectly() {
let src = "\
type Box[T] =
value: T
fun getBoxValue() -> Int =
self.value
fun sumBox(b: Box) -> Int =
b.getBoxValue()
fun main() -> Int =
sumBox(Box(value: 11))
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 11);
}
#[test]
fn wasmModuleWithATableElementValidatesAndCallIndirectWorks() {
// Exercises WasmModule's new table/element support directly, independent of any
// closure-compiling logic (which doesn't exist yet) — builds a tiny module by
// hand: one function that returns 42, registered as table element 0, called via
// `call_indirect` from `main` using a runtime-computed (not compile-time-constant)
// table index, to prove the table/element wiring is real, not coincidentally
// skipped by validation.
let mut module = plum_wasm_codegen::WasmModule::new();
let ret42_type = module.addType(&[], &[wasm_encoder::ValType::I64]);
let ret42_idx = module.addFunction(ret42_type, &{
let mut body = vec![0u8]; // 0 local-decl groups
wasm_encoder::Instruction::I64Const(42).encode(&mut body);
wasm_encoder::Instruction::End.encode(&mut body);
body
});
let table_idx = module.addTableElement(ret42_idx);
assert_eq!(table_idx, 0);
let main_type = module.addType(&[], &[wasm_encoder::ValType::I64]);
let main_idx = module.addFunction(main_type, &{
let mut body = vec![0u8]; // 0 local-decl groups
wasm_encoder::Instruction::I32Const(0).encode(&mut body); // table index operand
wasm_encoder::Instruction::CallIndirect { type_index: ret42_type, table_index: 0 }.encode(&mut body);
wasm_encoder::Instruction::End.encode(&mut body);
body
});
module.addExport("main", wasm_encoder::ExportKind::Func, main_idx);
let bytes = module.finish();
let result = wasmparser::validate(&bytes);
assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
let engine = gcEngine();
let wasm_module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable");
let mut store = wasmtime::Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate");
let main = instance.get_typed_func::<(), i64>(&mut store, "main").expect("main should have signature () -> i64");
assert_eq!(main.call(&mut store, ()).expect("main should not trap"), 42);
}
#[test]
fn nonCapturingClosurePassedAndCalledRunsCorrectly() {
let src = "\
fun each(cb: fn(Int) -> Int) -> Int =
cb(5)
fun main() -> Int =
each(|v| v)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 5);
}
#[test]
fn multiLineClosureCallArgumentWithClosingParenOnLastStatementLineRunsCorrectly() {
// Was a documented gap: the external scanner never emitted a dedent for a
// multi-line closure body immediately followed by `)` on the same line as the
// body's last statement, so this shape didn't parse at all before.
let src = "\
fun each(cb: fn(Int) -> Int) -> Int =
cb(5)
fun main() -> Int =
each(|v|
x = v + 1
x * 2)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 12);
}
#[test]
fn capturingClosureSnapshotsValueAtCreationTimeRunsCorrectly() {
let src = "\
fun each(cb: fn(Int) -> Int) -> Int =
cb(0)
fun useClosure() -> Int =
x = 10
cb = |v|
x + v
x = 999
each(cb)
fun main() -> Int =
useClosure()
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
// The closure must see x==10 (its value when the closure was created), not 999
// (its value when `each(cb)` is actually called) - proving snapshot-by-value
// capture, not a live/shared reference.
assert_eq!(runMain(&bytes), 10);
}
#[test]
fn closureAssignedThenCalledAtFloatTypeRunsCorrectly() {
// Was a documented gap: a closure created via assignment (not passed directly as
// a call argument) and later called at a concrete non-Int type could hit a wasm
// runtime trap — the checker's own closure inference gives every param a fresh
// TVar and never unifies it against how it's used in the body, so a genuinely
// Float param silently defaulted to Int, producing a `call_indirect` signature
// mismatch between the compiled closure body and its call site.
let src = "\
fun eachF(cb: fn(Float) -> Float) -> Float =
cb(0.0)
fun useClosure() -> Float =
offset = 2.5
cb = |v|
offset + v
eachF(cb)
fun main() -> Float =
useClosure()
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMainF64(&bytes), 2.5);
}
#[test]
fn closureAssignedThenCalledAtClassTypeRunsCorrectly() {
let src = "\
type Cat =
age: Int
fun eachCat(cb: fn(Cat) -> Int) -> Int =
cb(Cat(age: 7))
fun useClosure() -> Int =
cb = |c|
c.age
eachCat(cb)
fun main() -> Int =
useClosure()
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 7);
}
#[test]
fn closurePassedThroughAlreadyGenericHigherOrderFunctionRunsCorrectly() {
let src = "\
fun identity(value: T) -> T =
value
fun each(cb: fn(Int) -> Int) -> Int =
cb(identity(7))
fun main() -> Int =
each(|v| v * 2)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 14);
}
#[test]
fn nestedClosureLiteralRunsCorrectly() {
// Was a documented gap: a closure literal nested inside another closure's body
// produced a clear compile error (the discovery pre-pass never recursed into a
// closure's own body to find closures nested inside it). Here `inner` (nested
// inside `outer`'s body) needs `offset` — a name from `useNested`'s scope, two
// levels up from `inner` itself, and not referenced by `outer` directly — which
// exercises the multi-level capture chain: `outer` must itself capture `offset`
// purely because `inner` needs it, not because `outer` uses it.
let src = "\
fun each(cb: fn(Int) -> Int) -> Int =
cb(5)
fun useNested() -> Int =
offset = 100
outer = |v|
inner = |w|
w + offset
inner(v)
each(outer)
fun main() -> Int =
useNested()
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 105);
}
#[test]
fn nestedClosureLiteralPassedDirectlyAsCallArgumentRunsCorrectly() {
let src = "\
fun each(cb: fn(Int) -> Int) -> Int =
cb(5)
fun main() -> Int =
each(|v|
each(|w| w + v))
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 10);
}
#[test]
fn namedFunctionUsedAsAClosureTypedValueRunsCorrectly() {
// A plain top-level named function (not a `|params| body` closure literal) used
// wherever a `fn(...)`-typed value is expected — no closure literal involved at
// the call site at all.
let src = "\
fun double(x: Int) -> Int =
x * 2
fun each(cb: fn(Int) -> Int) -> Int =
cb(21)
fun main() -> Int =
each(double)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 42);
}
#[test]
fn namedFunctionUsedAsAValueAssignedThenCalledRunsCorrectly() {
let src = "\
fun double(x: Int) -> Int =
x * 2
fun main() -> Int =
f = double
f(21)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 42);
}
#[test]
fn namedFunctionUsedAsAValueAlongsideAClosureAtTheSameCallTypeRunsCorrectly() {
// Proves the trampoline shares the same call_indirect type as an ordinary
// closure of the same signature (both must resolve to the same wasm function
// type, since both flow through the exact same `cb(...)` call site).
let src = "\
fun double(x: Int) -> Int =
x * 2
fun each(cb: fn(Int) -> Int) -> Int =
cb(10)
fun main() -> Int =
each(double) + each(|v| v + 1)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 31);
}
#[test]
fn multiSubjectMatchWithEnumTagsRunsCorrectly() {
// Mirrors libs/std/bool.plum's `and`/`or`: `match self, o` against two Bool
// subjects, each case naming a tag pattern per position.
let src = "\
fun and(a: Bool, b: Bool) -> Bool =
match a, b
True, True => True
True, False => False
False, True => False
False, False => False
fun main() -> Int =
x = and(True, True)
y = and(True, False)
match x, y
True, False => 1
_, _ => 0
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 1);
}
#[test]
fn multiSubjectMatchFallsThroughToNextCaseWhenOnlyFirstPositionMatches() {
// The first case's position-0 pattern (`1`) matches, but position-1 (`1`)
// doesn't (b is 2) — codegen must fall through to the *next case* (trying its
// own position 0 again), not just "move on" within the first case.
let src = "\
fun classify(a: Int, b: Int) -> Int =
match a, b
1, 1 => 100
1, 2 => 200
_, _ => 0
fun main() -> Int =
classify(1, 2)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 200);
}
#[test]
fn multiSubjectMatchWithBindingAndWildcardRunsCorrectly() {
let src = "\
fun combine(a: Int, b: Int) -> Int =
match a, b
0, y => y
x, 0 => x
x, y => x + y
fun main() -> Int =
combine(3, 4)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 7);
}
#[test]
fn multiSubjectMatchWithGenericEnumVariantRunsCorrectly() {
// Exercises the monomorphize.rs fix: each subject's own generic-enum
// specialization (`Some$Int`) must be mangled independently per position.
let src = "\
enum Option =
| Some[T]
| None
fun both(a: Option, b: Option) -> Int =
match a, b
Some(x), Some(y) => x + y
_, _ => 0
fun main() -> Int =
both(Some(3), Some(4))
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 7);
}
#[test]
fn fieldAssignmentTargetRunsCorrectly() {
let src = "\
type Counter =
value: Int
fun bump() =
self.value = self.value + 1
fun main() -> Int =
c = Counter(value: 41)
c.bump()
c.value
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 42);
}
#[test]
fn chainedFieldAssignmentTargetRunsCorrectly() {
let src = "\
type Inner =
value: Int
type Outer =
inner: Inner
fun bump() =
self.inner.value = self.inner.value + 1
fun main() -> Int =
o = Outer(inner: Inner(value: 9))
o.bump()
o.inner.value
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 10);
}
#[test]
fn mixedMultiAssignWithFieldTargetRunsCorrectly() {
let src = "\
type Counter =
value: Int
fun main() -> Int =
c = Counter(value: 5)
a, c.value = 100, 7
a + c.value
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 107);
}
#[test]
fn variadicCallWithVaryingTrailingArgCountsRunsCorrectly() {
let src = "\
fun combine(prefix: Int, rest: ...Int) -> Int =
prefix
fun main() -> Int =
a = combine(10)
b = combine(20, 1)
c = combine(30, 1, 2, 3)
a + b + c
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 60);
}
#[test]
fn sumAllVariadicIntRunsCorrectly() {
let src = "\
fun sumAll(nums: ...Int) -> Int =
total = 0
for v := range nums
total = total + v
total
fun main() -> Int =
sumAll(1, 2, 3, 4)
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 10);
}
#[test]
fn sumAllVariadicIntWithZeroArgsRunsCorrectly() {
let src = "\
fun sumAll(nums: ...Int) -> Int =
total = 0
for v := range nums
total = total + v
total
fun main() -> Int =
sumAll()
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
assert_eq!(runMain(&bytes), 0);
}
#[test]
fn wasmtimeGcConfigCanRunAHandEncodedGcModule() {
// Throwaway empirical check (plan Task 1, Step 1): confirm wasmtime 28's GC
// support actually works end to end before building a type-emitter on top of
// it. Hand-encodes the smallest possible module with one GC struct type and
// one function that does struct.new_default and returns it, bypassing plum
// entirely, so a failure here is unambiguously about wasmtime/wasm-encoder,
// not about anything plum-specific.
use wasm_encoder::*;
let mut module = Module::new();
let mut types = TypeSection::new();
// type 0: struct { i32 }
types.ty().struct_(vec![FieldType { element_type: StorageType::Val(ValType::I32), mutable: true }]);
// type 1: () -> (ref null 0)
let struct_ref = ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(0) });
types.ty().function(vec![], vec![struct_ref]);
module.section(&types);
let mut funcs = FunctionSection::new();
funcs.function(1);
module.section(&funcs);
let mut exports = ExportSection::new();
exports.export("main", ExportKind::Func, 0);
module.section(&exports);
let mut code = CodeSection::new();
let mut f = Function::new(vec![]);
f.instruction(&Instruction::StructNewDefault(0));
f.instruction(&Instruction::End);
code.function(&f);
module.section(&code);
let bytes = module.finish();
let mut config = wasmtime::Config::new();
config.wasm_gc(true);
config.wasm_function_references(true);
let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
let wasm_module = wasmtime::Module::new(&engine, &bytes).expect("hand-encoded GC module should be loadable");
let mut store = wasmtime::Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate");
let main = instance
.get_func(&mut store, "main")
.expect("main should be exported");
let mut results = [wasmtime::Val::I32(0)];
main.call(&mut store, &[], &mut results).expect("main should not trap");
}
#[test]
fn wasmtimeGcConfigSupportsSubtypingRefTestAndNullableSelfReferentialFields() {
// Deeper empirical check: enum-variant subtyping (abstract supertype + concrete
// subtypes in one `rec` group), ref.test-based dispatch, ref.cast to narrow to a
// subtype, and a nullable field that references the struct's OWN type (the
// Node.next: Option[Node] shape List needs) — all in one hand-encoded module,
// bypassing plum entirely.
use wasm_encoder::*;
let mut module = Module::new();
let mut types = TypeSection::new();
// rec group: type 0 = abstract enum supertype (empty struct, non-final so it can
// be subtyped); type 1 = concrete "Some"-like subtype with one i32 payload field;
// type 2 = concrete "None"-like subtype (empty, no payload).
types.ty().rec(vec![
SubType {
is_final: false,
supertype_idx: None,
composite_type: CompositeType { inner: CompositeInnerType::Struct(StructType { fields: vec![].into() }), shared: false },
},
SubType {
is_final: true,
supertype_idx: Some(0),
composite_type: CompositeType {
inner: CompositeInnerType::Struct(StructType { fields: vec![
FieldType { element_type: StorageType::Val(ValType::I32), mutable: false },
].into() }),
shared: false,
},
},
SubType {
is_final: true,
supertype_idx: Some(0),
composite_type: CompositeType { inner: CompositeInnerType::Struct(StructType { fields: vec![].into() }), shared: false },
},
]);
// type 3: a self-referential Node struct — { value: i32, next: ref null $Node }.
// Must be declared in its own rec group (or alone) referencing its own index (3)
// for the nullable self-reference to resolve.
types.ty().rec(vec![
SubType {
is_final: true,
supertype_idx: None,
composite_type: CompositeType {
inner: CompositeInnerType::Struct(StructType { fields: vec![
FieldType { element_type: StorageType::Val(ValType::I32), mutable: false },
FieldType { element_type: StorageType::Val(ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(3) })), mutable: true },
].into() }),
shared: false,
},
},
]);
// type 4: () -> i32 — constructs a "Some"-like subtype (type 1) holding 42,
// stores it as the supertype (type 0), ref.tests it against type 1, then
// ref.casts and struct.gets the payload back out. Also builds a 2-node linked
// list (type 3) and confirms unlinking (overwriting `next` with ref.null) and
// reading back the remaining node's value both work.
let super_ref = ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(0) });
types.ty().function(vec![], vec![ValType::I32]);
module.section(&types);
let mut funcs = FunctionSection::new();
funcs.function(4);
module.section(&funcs);
let mut exports = ExportSection::new();
exports.export("main", ExportKind::Func, 0);
module.section(&exports);
let mut code = CodeSection::new();
let mut f = Function::new(vec![(1, super_ref.clone()), (1, ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(3) }))]);
let locals_super = 0u32;
let locals_node = 1u32;
// local_super = Some(42) (as the supertype)
f.instruction(&Instruction::I32Const(42));
f.instruction(&Instruction::StructNew(1));
f.instruction(&Instruction::LocalSet(locals_super));
// local_node = Node { value: 1, next: null }
f.instruction(&Instruction::I32Const(1));
f.instruction(&Instruction::RefNull(HeapType::Concrete(3)));
f.instruction(&Instruction::StructNew(3));
f.instruction(&Instruction::LocalSet(locals_node));
// if ref.test(local_super, type 1) { result = ref.cast(local_super, type1).field0 } else { result = -1 }
f.instruction(&Instruction::LocalGet(locals_super));
f.instruction(&Instruction::RefTestNonNull(HeapType::Concrete(1)));
f.instruction(&Instruction::If(BlockType::Result(ValType::I32)));
f.instruction(&Instruction::LocalGet(locals_super));
f.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(1)));
f.instruction(&Instruction::StructGet { struct_type_index: 1, field_index: 0 });
f.instruction(&Instruction::Else);
f.instruction(&Instruction::I32Const(-1));
f.instruction(&Instruction::End);
// unlink: local_node.next = ref.null (already null, but exercise the store path)
f.instruction(&Instruction::LocalGet(locals_node));
f.instruction(&Instruction::RefNull(HeapType::Concrete(3)));
f.instruction(&Instruction::StructSet { struct_type_index: 3, field_index: 1 });
// add local_node.value to the ref.test result and return
f.instruction(&Instruction::LocalGet(locals_node));
f.instruction(&Instruction::StructGet { struct_type_index: 3, field_index: 0 });
f.instruction(&Instruction::I32Add);
f.instruction(&Instruction::End);
code.function(&f);
module.section(&code);
let bytes = module.finish();
let mut config = wasmtime::Config::new();
config.wasm_gc(true);
config.wasm_function_references(true);
let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
let wasm_module = wasmtime::Module::new(&engine, &bytes).unwrap_or_else(|e| panic!("module should be loadable: {e}"));
let mut store = wasmtime::Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate");
let main = instance
.get_typed_func::<(), i32>(&mut store, "main")
.expect("main should have signature () -> i32");
let result = main.call(&mut store, ()).expect("main should not trap");
assert_eq!(result, 43, "expected ref.test/ref.cast payload (42) + node.value (1) = 43");
}
#[test]
fn wasmtimeGcConfigAllowsStructNewInGlobalConstExpr() {
// Decision 2 of the wasm-gc migration plan pre-allocates payload-free enum
// variants (True/False/None/...) once as globals. Confirm a global's
// initializer expression can directly use struct.new (not just i32.const/
// ref.null), or the plan needs a `start` function fallback instead.
use wasm_encoder::*;
let mut module = Module::new();
let mut types = TypeSection::new();
types.ty().struct_(vec![FieldType { element_type: StorageType::Val(ValType::I32), mutable: false }]);
types.ty().function(vec![], vec![ValType::I32]);
module.section(&types);
let mut funcs = FunctionSection::new();
funcs.function(1);
module.section(&funcs);
let mut globals = GlobalSection::new();
let struct_ref_ty = ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(0) });
let mut init = Vec::new();
Instruction::I32Const(7).encode(&mut init);
Instruction::StructNew(0).encode(&mut init);
Instruction::End.encode(&mut init);
globals.global(
GlobalType { val_type: struct_ref_ty, mutable: false, shared: false },
&ConstExpr::raw(init),
);
module.section(&globals);
let mut exports = ExportSection::new();
exports.export("main", ExportKind::Func, 0);
module.section(&exports);
let mut code = CodeSection::new();
let mut f = Function::new(vec![]);
f.instruction(&Instruction::GlobalGet(0));
f.instruction(&Instruction::StructGet { struct_type_index: 0, field_index: 0 });
f.instruction(&Instruction::End);
code.function(&f);
module.section(&code);
let bytes = module.finish();
let mut config = wasmtime::Config::new();
config.wasm_gc(true);
config.wasm_function_references(true);
let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
let wasm_module = match wasmtime::Module::new(&engine, &bytes) {
Ok(m) => m,
Err(e) => {
println!("struct.new in a global const-expr is NOT supported by this wasmtime/config: {e}");
println!("plan implication: Task 2 Step 2 (2a) must use a `start` function instead of a const global initializer.");
return;
}
};
let mut store = wasmtime::Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate");
let main = instance.get_typed_func::<(), i32>(&mut store, "main").expect("main should have signature () -> i32");
let result = main.call(&mut store, ()).expect("main should not trap");
assert_eq!(result, 7);
println!("struct.new IS supported directly in a global const-expr initializer.");
}
#[test]
fn wasmtimeGcConfigSupportsStartFunctionInitializingGcGlobals() {
// Follow-up to the previous test: struct.new isn't allowed in a global
// const-expr, so confirm the `start` function fallback works instead —
// a mutable global initialized to ref.null, populated by struct.new inside
// a `start` function that runs once at instantiation before any export.
use wasm_encoder::*;
let mut module = Module::new();
let mut types = TypeSection::new();
types.ty().struct_(vec![FieldType { element_type: StorageType::Val(ValType::I32), mutable: false }]);
types.ty().function(vec![], vec![]); // start fn: () -> ()
types.ty().function(vec![], vec![ValType::I32]); // main: () -> i32
module.section(&types);
let mut funcs = FunctionSection::new();
funcs.function(1); // func 0: start
funcs.function(2); // func 1: main
module.section(&funcs);
let mut globals = GlobalSection::new();
let struct_ref_ty = ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(0) });
globals.global(
GlobalType { val_type: struct_ref_ty.clone(), mutable: true, shared: false },
&ConstExpr::ref_null(HeapType::Concrete(0)),
);
module.section(&globals);
let mut exports = ExportSection::new();
exports.export("main", ExportKind::Func, 1);
module.section(&exports);
let start = StartSection { function_index: 0 };
module.section(&start);
let mut code = CodeSection::new();
let mut start_fn = Function::new(vec![]);
start_fn.instruction(&Instruction::I32Const(99));
start_fn.instruction(&Instruction::StructNew(0));
start_fn.instruction(&Instruction::GlobalSet(0));
start_fn.instruction(&Instruction::End);
code.function(&start_fn);
let mut main_fn = Function::new(vec![]);
main_fn.instruction(&Instruction::GlobalGet(0));
main_fn.instruction(&Instruction::StructGet { struct_type_index: 0, field_index: 0 });
main_fn.instruction(&Instruction::End);
code.function(&main_fn);
module.section(&code);
let bytes = module.finish();
let mut config = wasmtime::Config::new();
config.wasm_gc(true);
config.wasm_function_references(true);
let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
let wasm_module = wasmtime::Module::new(&engine, &bytes).unwrap_or_else(|e| panic!("module should be loadable: {e}"));
let mut store = wasmtime::Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate (start fn should run automatically)");
let main = instance.get_typed_func::<(), i32>(&mut store, "main").expect("main should have signature () -> i32");
let result = main.call(&mut store, ()).expect("main should not trap");
assert_eq!(result, 99, "start fn should have populated the global before main ran");
}
#[test]
fn wasmtimeGcConfigSupportsArrayNewDataFromPassiveSegmentWithNoMemorySection() {
// Decision 4 of the wasm-gc migration plan: static string data lives in a
// PASSIVE data segment (no active memory offset), consumed via array.new_data
// — confirming this needs no `memory` section in the module at all, which is
// what lets the whole memory section disappear once bump allocation is retired.
use wasm_encoder::*;
let mut module = Module::new();
let mut types = TypeSection::new();
types.ty().array(&StorageType::I8, false);
let arr_ref = ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(0) });
types.ty().function(vec![], vec![ValType::I32]);
module.section(&types);
let mut funcs = FunctionSection::new();
funcs.function(1);
module.section(&funcs);
let mut exports = ExportSection::new();
exports.export("main", ExportKind::Func, 0);
module.section(&exports);
// Required whenever the module uses array.new_data/memory.init/data.drop —
// the validator needs the passive-segment count before the code section.
module.section(&DataCountSection { count: 1 });
let mut code = CodeSection::new();
let mut f = Function::new(vec![(1, arr_ref)]);
// local 0 = array.new_data(type 0, data segment 0) with offset=0, len=5 ("hello")
f.instruction(&Instruction::I32Const(0)); // data offset
f.instruction(&Instruction::I32Const(5)); // length
f.instruction(&Instruction::ArrayNewData { array_type_index: 0, array_data_index: 0 });
f.instruction(&Instruction::LocalSet(0));
// return array.get(local0, 0) — the byte 'h' = 104
f.instruction(&Instruction::LocalGet(0));
f.instruction(&Instruction::I32Const(0));
f.instruction(&Instruction::ArrayGetU(0));
f.instruction(&Instruction::End);
code.function(&f);
module.section(&code);
// NOTE: deliberately no MemorySection at all.
let mut data = DataSection::new();
data.passive(b"hello".iter().copied());
module.section(&data);
let bytes = module.finish();
let mut config = wasmtime::Config::new();
config.wasm_gc(true);
config.wasm_function_references(true);
let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
let wasm_module = wasmtime::Module::new(&engine, &bytes).unwrap_or_else(|e| panic!("module with no memory section + passive data should be loadable: {e}"));
let mut store = wasmtime::Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate");
let main = instance.get_typed_func::<(), i32>(&mut store, "main").expect("main should have signature () -> i32");
let result = main.call(&mut store, ()).expect("main should not trap");
assert_eq!(result, b'h' as i32);
}
#[test]
fn wasmtimeGcConfigSupportsWideningConcreteStructRefToAnyrefAndCastingBack() {
// Decision 3 of the wasm-gc migration plan: closure env pointers are `anyref`
// in the shared call_indirect signature, with each closure's body ref.cast-ing
// back to its own concrete env struct type. Confirm a concrete struct ref can
// be stored where anyref is expected (implicit widening, no instruction needed)
// and RefCastNonNull(Concrete(_)) recovers the concrete type correctly.
use wasm_encoder::*;
let mut module = Module::new();
let mut types = TypeSection::new();
types.ty().struct_(vec![FieldType { element_type: StorageType::Val(ValType::I32), mutable: false }]);
// "identity-ish" function: (anyref) -> i32, casts back to concrete type 0 and reads field 0.
types.ty().function(vec![ValType::Ref(RefType::ANYREF)], vec![ValType::I32]);
// main: () -> i32, builds a concrete struct, passes it (widened) to func 1.
types.ty().function(vec![], vec![ValType::I32]);
module.section(&types);
let mut funcs = FunctionSection::new();
funcs.function(1); // func 0: the anyref-accepting fn
funcs.function(2); // func 1: main
module.section(&funcs);
let mut exports = ExportSection::new();
exports.export("main", ExportKind::Func, 1);
module.section(&exports);
let mut code = CodeSection::new();
let mut cast_fn = Function::new(vec![]);
cast_fn.instruction(&Instruction::LocalGet(0));
cast_fn.instruction(&Instruction::RefCastNonNull(HeapType::Concrete(0)));
cast_fn.instruction(&Instruction::StructGet { struct_type_index: 0, field_index: 0 });
cast_fn.instruction(&Instruction::End);
code.function(&cast_fn);
let mut main_fn = Function::new(vec![]);
main_fn.instruction(&Instruction::I32Const(55));
main_fn.instruction(&Instruction::StructNew(0)); // pushes (ref 0) — implicitly a subtype of anyref
main_fn.instruction(&Instruction::Call(0)); // call expects anyref param — implicit widening at the call site
main_fn.instruction(&Instruction::End);
code.function(&main_fn);
module.section(&code);
let bytes = module.finish();
let mut config = wasmtime::Config::new();
config.wasm_gc(true);
config.wasm_function_references(true);
let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
let wasm_module = wasmtime::Module::new(&engine, &bytes).unwrap_or_else(|e| panic!("module should be loadable: {e}"));
let mut store = wasmtime::Store::new(&engine, ());
let instance = wasmtime::Instance::new(&mut store, &wasm_module, &[]).expect("module should instantiate");
let main = instance.get_typed_func::<(), i32>(&mut store, "main").expect("main should have signature () -> i32");
let result = main.call(&mut store, ()).expect("main should not trap");
assert_eq!(result, 55, "concrete struct ref should widen to anyref implicitly and cast back correctly");
}
#[test]
fn gcTypeRegistryProducesAWellFormedTypeSectionAlongsideBumpAllocatorCodegen() {
// Task 1 Step 5 of the wasm-gc migration plan: the new (currently unconsumed)
// wasm-gc type registry declares a well-formed type section — a struct type per
// class, a supertype+subtypes set per enum (including the built-in Bool), and a
// shared Str array type — even though every OTHER part of this compiled module
// still uses the old bump-allocator representation. Exercises a class, an enum
// with both a payload and a payload-free variant, and Str, so all three GC type
// shapes actually get emitted.
let src = "\
type Cat =
name: Str
age: Int
enum Option =
| Some[Int]
| None
fun main() -> Int =
c = Cat(name: \"x\", age: 7)
c.age
";
let source = parse(src);
let bytes = compileSource(&source).expect("compile failed");
let result = wasmparser::validate(&bytes);
assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
// And the still-untouched bump-allocator codegen must still actually run correctly —
// this task is purely additive, nothing behavioral should have changed.
assert_eq!(runMain(&bytes), 7);
}
/// Task 3 of the wasm-gc migration plan: `libs/std/list.plum`'s `add`/`unlink`/`set`/
/// `removeAt`/`remove`/`clear`/`reverse`, ported field-for-field/statement-for-statement
/// from the real file (see that file's identical method bodies), against the
/// struct.new/struct.get/struct.set representation.
///
/// This uses `NodeLink`/`Int` in place of the real file's `Option[Node]`/generic `T`:
/// `plum-checker`'s monomorphizer mangles a generic type's name once it's specialized
/// (`Option` -> `Option$Int`) but does NOT rewrite `ClassEnv`/`EnumVariants`' OWN
/// declared field types to match (`plumTypeFromAst` drops type arguments entirely,
/// recording a class field typed `Option[Node]` as the bare, now-dangling `TNamed("Option")`)
/// — a real, pre-existing gap in the checker's generics support, unrelated to and
/// discovered while working on this migration, that currently blocks the REAL
/// `libs/std/list.plum` (and its already-existing, unrelated `get`/`each`/`map`
/// methods) from compiling at all. `NodeLink`/`Node` here are deliberately NOT
/// generic, sidestepping that gap, so this test still exercises the exact wasm-gc
/// struct/array mechanics (self-referential nullable-via-enum fields, `struct.set`
/// mutation through an aliased reference, `ref.test` dispatch) Task 2 built.
const LIST_SOURCE_PREFIX: &str = "\
enum NodeLink =
| HasNode[Node]
| NoNode
enum Option =
| Some[Int]
| None
type Node =
value: Int
prev: NodeLink
next: NodeLink
type List =
head: NodeLink
tail: NodeLink
size: Int
fun get(self, i: Int) -> Option =
current = self.head
index = 0
while current != NoNode
match current
HasNode(node) =>
if index == i
return Some(node.value)
current = node.next
index = index + 1
NoNode =>
break
None
fun length(self) -> Int =
self.size
fun add(self, values: ...Int) =
for v := range values
node = Node(value: v, prev: self.tail, next: NoNode)
match self.tail
HasNode(t) =>
t.next = HasNode(node)
NoNode =>
self.head = HasNode(node)
self.tail = HasNode(node)
self.size = self.size + 1
fun unlink(self, node: Node) =
match node.prev
HasNode(p) =>
p.next = node.next
NoNode =>
self.head = node.next
match node.next
HasNode(n) =>
n.prev = node.prev
NoNode =>
self.tail = node.prev
self.size = self.size - 1
fun set(self, i: Int, v: Int) -> Option =
current = self.head
index = 0
while current != NoNode
match current
HasNode(node) =>
if index == i
old = node.value
node.value = v
return Some(old)
current = node.next
index = index + 1
NoNode =>
break
None
fun removeAt(self, i: Int) =
current = self.head
index = 0
while current != NoNode
match current
HasNode(node) =>
if index == i
self.unlink(node)
return
current = node.next
index = index + 1
NoNode =>
break
fun remove(self, v: Int) =
current = self.head
while current != NoNode
match current
HasNode(node) =>
if node.value == v
self.unlink(node)
return
current = node.next
NoNode =>
break
fun clear(self) =
self.head = NoNode
self.tail = NoNode
self.size = 0
fun reverse(self) -> List =
current = self.head
while current != NoNode
match current
HasNode(node) =>
next = node.next
node.next = node.prev
node.prev = next
current = next
NoNode =>
break
oldHead = self.head
self.head = self.tail
self.tail = oldHead
self
fun optSum(o: Option) -> Int =
match o
Some(v) =>
v
None =>
-1000
";
#[test]
fn listAddSetRemoveAtRemoveClearReverseAllWorkCorrectly() {
let src = format!("{LIST_SOURCE_PREFIX}\
fun main() -> Int =
l = List(head: NoNode, tail: NoNode, size: 0)
l.add(1, 2, 3, 4, 5)
a = l.length()
b = optSum(l.get(0))
c = optSum(l.get(4))
oldVal = optSum(l.set(2, 30))
d = optSum(l.get(2))
l.removeAt(0)
e = l.length()
f = optSum(l.get(0))
l.remove(30)
g = l.length()
l.reverse()
h = optSum(l.get(0))
l.clear()
i = l.length()
a + b + c + oldVal + d + e + f + g + h + i
");
let source = parse(&src);
let bytes = compileSource(&source).expect("compile failed");
let result = wasmparser::validate(&bytes);
assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
// add(1,2,3,4,5): a=length=5, b=get(0)=1, c=get(4)=5
// set(2,30): oldVal=3, d=get(2)=30 -> list [1,2,30,4,5]
// removeAt(0): e=length=4, f=get(0)=2 -> list [2,30,4,5]
// remove(30): g=length=3 -> list [2,4,5]
// reverse(): h=get(0)=5 -> list [5,4,2]
// clear(): i=length=0
// 5+1+5+3+30+4+2+3+5+0 = 58
assert_eq!(runMain(&bytes), 58);
}
/// Proves `removeAt`/`clear` actually detach nodes from the list (not just decrement
/// `size`) by removing every node one at a time via repeated `removeAt(0)` and
/// confirming the list ends up correctly empty and reports zero length — the removed
/// `Node`s (and their `NodeLink` links to each other) become unreachable and eligible
/// for collection once nothing in the list still points to them, since there's no
/// direct "assert this was garbage collected" hook available from a compiled
/// program's own execution.
#[test]
fn removingEveryNodeInALoopLeavesAnEmptyCorrectlyFunctioningList() {
let src = format!("{LIST_SOURCE_PREFIX}\
fun main() -> Int =
l = List(head: NoNode, tail: NoNode, size: 0)
l.add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
i = 0
while i < 10
l.removeAt(0)
i = i + 1
afterLoopLength = l.length()
isEmpty = optSum(l.get(0))
l.add(42)
afterReAdd = optSum(l.get(0))
afterLoopLength + isEmpty + afterReAdd
");
let source = parse(&src);
let bytes = compileSource(&source).expect("compile failed");
let result = wasmparser::validate(&bytes);
assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
// afterLoopLength=0, isEmpty(get(0) on empty list)=-1000, afterReAdd=42
// 0 + -1000 + 42 = -958
assert_eq!(runMain(&bytes), -958);
}