plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-wasm-codegen/tests/examples_test.rs
| 3d6f280 | 1 | #![allow(non_snake_case)] |
| 3d6f280 | 2 | use plum_wasm_codegen::compileSource; |
| 0e1801a | 3 | use plum_core::AstParser; |
| 0e1801a | 4 | |
| 3d6f280 | 5 | fn examplesDir() -> std::path::PathBuf { |
| 0e1801a | 6 | std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples") |
| 0e1801a | 7 | } |
| 0e1801a | 8 | |
| 3d6f280 | 9 | fn parseFile(name: &str) -> plum_core::ast::Source { |
| 3d6f280 | 10 | let path = examplesDir().join(name); |
| 0e1801a | 11 | let src = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {}: {}", path.display(), e)); |
| 0e1801a | 12 | let mut parser = tree_sitter::Parser::new(); |
| 0e1801a | 13 | parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap(); |
| 0e1801a | 14 | let tree = parser.parse(&src, None).unwrap_or_else(|| panic!("failed to parse {}", path.display())); |
| 0e1801a | 15 | assert!(!tree.root_node().has_error(), "{} has a parse error", path.display()); |
| 0e1801a | 16 | let ap = AstParser::new(&src); |
| 3d6f280 | 17 | ap.parseSource(tree.root_node()) |
| 0e1801a | 18 | } |
| 0e1801a | 19 | |
| 5ef4579 | 20 | /// See the identically-named helper in codegen_tests.rs for why both features |
| 5ef4579 | 21 | /// are needed (part of the wasm-gc migration, docs/superpowers/plans/2026-07-25-wasm-gc-migration.md). |
| 5ef4579 | 22 | fn gcEngine() -> wasmtime::Engine { |
| 5ef4579 | 23 | let mut config = wasmtime::Config::new(); |
| 5ef4579 | 24 | config.wasm_gc(true); |
| 5ef4579 | 25 | config.wasm_function_references(true); |
| 5ef4579 | 26 | wasmtime::Engine::new(&config).expect("engine with GC config should construct") |
| 5ef4579 | 27 | } |
| 5ef4579 | 28 | |
| 3d6f280 | 29 | fn assertCompiles(name: &str) -> Vec<u8> { |
| 3d6f280 | 30 | let source = parseFile(name); |
| 3d6f280 | 31 | let bytes = compileSource(&source).unwrap_or_else(|e| panic!("{} failed to compile: {}", name, e)); |
| 0e1801a | 32 | let result = wasmparser::validate(&bytes); |
| 0e1801a | 33 | assert!(result.is_ok(), "{} produced invalid wasm: {:?}", name, result.err()); |
| 0e1801a | 34 | bytes |
| 0e1801a | 35 | } |
| 0e1801a | 36 | |
| 0e1801a | 37 | /// Examples that stick to currently-supported codegen features (primitives, control |
| 0e1801a | 38 | /// flow, classes/methods) must actually compile to valid wasm — not just parse and |
| 0e1801a | 39 | /// type-check. |
| 0e1801a | 40 | #[test] |
| 3d6f280 | 41 | fn basicsCompiles() { |
| 3d6f280 | 42 | assertCompiles("basics.plum"); |
| 0e1801a | 43 | } |
| 0e1801a | 44 | |
| 0e1801a | 45 | #[test] |
| 3d6f280 | 46 | fn controlFlowCompiles() { |
| 3d6f280 | 47 | assertCompiles("control_flow.plum"); |
| 0e1801a | 48 | } |
| 0e1801a | 49 | |
| 0e1801a | 50 | #[test] |
| 3d6f280 | 51 | fn functionsCompiles() { |
| 3d6f280 | 52 | assertCompiles("functions.plum"); |
| 0e1801a | 53 | } |
| 0e1801a | 54 | |
| 0e1801a | 55 | #[test] |
| 3d6f280 | 56 | fn typesCompiles() { |
| 0e1801a | 57 | // Only class/trait/enum declarations, no function bodies to lower — should still |
| 0e1801a | 58 | // produce a valid (if unexciting) module. |
| 3d6f280 | 59 | assertCompiles("types.plum"); |
| 0e1801a | 60 | } |
| 0e1801a | 61 | |
| 0e1801a | 62 | #[test] |
| 3d6f280 | 63 | fn methodsCompilesAndRunsCorrectly() { |
| 3d6f280 | 64 | let bytes = assertCompiles("methods.plum"); |
| 0e1801a | 65 | |
| 5ef4579 | 66 | let engine = gcEngine(); |
| 0e1801a | 67 | let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable"); |
| 0e1801a | 68 | let mut store = wasmtime::Store::new(&engine, ()); |
| 0e1801a | 69 | let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate"); |
| 0e1801a | 70 | let main = instance |
| 0e1801a | 71 | .get_typed_func::<(), i64>(&mut store, "main") |
| 0e1801a | 72 | .expect("main should have signature () -> i64"); |
| 0e1801a | 73 | let result = main.call(&mut store, ()).expect("main should not trap"); |
| 0e1801a | 74 | // makeCat() -> Cat(age: 3); a = getAge() = 3; b = birthday() = 4; |
| 0e1801a | 75 | // w.innerAge() = inner.age = 3 => 3 + 4 + 3 = 10 |
| 0e1801a | 76 | assert_eq!(result, 10); |
| 0e1801a | 77 | } |
| 0e1801a | 78 | |
| fe36a46 | 79 | /// match.plum now exercises fully-supported syntax (general enum tag and |
| fe36a46 | 80 | /// constructor patterns) and must compile and run correctly end to end. |
| 0e1801a | 81 | #[test] |
| 3d6f280 | 82 | fn matchExampleCompilesAndRunsCorrectly() { |
| 3d6f280 | 83 | let bytes = assertCompiles("match.plum"); |
| 5ef4579 | 84 | let engine = gcEngine(); |
| fe36a46 | 85 | let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable"); |
| fe36a46 | 86 | let mut store = wasmtime::Store::new(&engine, ()); |
| fe36a46 | 87 | let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate"); |
| fe36a46 | 88 | let main = instance |
| fe36a46 | 89 | .get_typed_func::<(), i64>(&mut store, "main") |
| fe36a46 | 90 | .expect("main should have signature () -> i64"); |
| fe36a46 | 91 | let result = main.call(&mut store, ()).expect("main should not trap"); |
| fe36a46 | 92 | // describeOption(Some(5)) = 5 |
| fe36a46 | 93 | assert_eq!(result, 5); |
| 0e1801a | 94 | } |
| 0e1801a | 95 | |
| 6341c74 | 96 | /// closures.plum exercises non-capturing closures, capturing closures (snapshot-by-value), |
| 6341c74 | 97 | /// and closures passed directly as call arguments. |
| 6341c74 | 98 | #[test] |
| 3d6f280 | 99 | fn closuresExampleCompilesAndRunsCorrectly() { |
| 3d6f280 | 100 | let bytes = assertCompiles("closures.plum"); |
| 5ef4579 | 101 | let engine = gcEngine(); |
| 6341c74 | 102 | let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable"); |
| 6341c74 | 103 | let mut store = wasmtime::Store::new(&engine, ()); |
| 6341c74 | 104 | let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate"); |
| 6341c74 | 105 | let main = instance |
| 6341c74 | 106 | .get_typed_func::<(), i64>(&mut store, "main") |
| 6341c74 | 107 | .expect("main should have signature () -> i64"); |
| 6341c74 | 108 | // each(|v| v * 3) = 5 * 3 = 15; useCapturingClosure() = cb(5) with offset=100 snapshot |
| 6341c74 | 109 | // captured at creation = 5 + 100 = 105; total = 15 + 105 = 120. |
| 6341c74 | 110 | assert_eq!(main.call(&mut store, ()).expect("main should not trap"), 120); |
| 6341c74 | 111 | } |
| 6341c74 | 112 | |
| 35af6cf | 113 | /// strings.plum exercises string interpolation (plain literals, `Str`/`Int` |
| 35af6cf | 114 | /// interpolation, and escape sequences) — it has no `main` to execute, so (matching |
| 3d6f280 | 115 | /// `basicsCompiles`/`controlFlowCompiles`/etc. above for other main-less examples) |
| 35af6cf | 116 | /// this only checks that it compiles to a valid module. |
| 0e1801a | 117 | #[test] |
| 3d6f280 | 118 | fn stringsExampleCompiles() { |
| 3d6f280 | 119 | assertCompiles("strings.plum"); |
| 0e1801a | 120 | } |
| 0000000 | 121 | |
| 0000000 | 122 | /// io.plum exercises `extern fun` (see `Fn.is_extern`/`FnBody::Extern` in |
| 0000000 | 123 | /// plum-core/src/ast.rs and their handling in plum-wasm-codegen/src/lib.rs) — it |
| 0000000 | 124 | /// declares its own `extern fun printLn(s: Str)` (redeclared locally rather than a |
| 0000000 | 125 | /// real `import std/os`, to keep this fixture self-contained for the single-file |
| 0000000 | 126 | /// parse/check this test and plum-checker's examples test do), which codegen turns |
| 0000000 | 127 | /// into a genuine wasm `(import "plum" "printLn" ...)` instead of a compiled body. |
| 0000000 | 128 | /// Runs it for real, providing the import as a host function that records what |
| 0000000 | 129 | /// it's called with, to prove both the wasm-level import wiring AND the actual |
| 0000000 | 130 | /// bytes handed across the host boundary are correct. |
| 0000000 | 131 | #[test] |
| 0000000 | 132 | fn ioExampleCompilesAndRunsCorrectly() { |
| 0000000 | 133 | let bytes = assertCompiles("io.plum"); |
| 0000000 | 134 | let engine = gcEngine(); |
| 0000000 | 135 | let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable"); |
| 0000000 | 136 | let mut store = wasmtime::Store::new(&engine, ()); |
| 0000000 | 137 | |
| 0000000 | 138 | let printed: std::sync::Arc<std::sync::Mutex<Vec<String>>> = Default::default(); |
| 0000000 | 139 | let printed_for_closure = printed.clone(); |
| 0000000 | 140 | let import_ty = module |
| 0000000 | 141 | .imports() |
| 0000000 | 142 | .find(|imp| imp.module() == "plum" && imp.name() == "printLn") |
| 0000000 | 143 | .expect("io.plum should import plum::printLn") |
| 0000000 | 144 | .ty() |
| 0000000 | 145 | .func() |
| 0000000 | 146 | .cloned() |
| 0000000 | 147 | .expect("plum::printLn import should be a function"); |
| 0000000 | 148 | let print_ln = wasmtime::Func::new(&mut store, import_ty, move |mut caller, params, _results| { |
| 0000000 | 149 | let wasmtime::Val::AnyRef(Some(s)) = ¶ms[0] else { |
| 0000000 | 150 | return Err(wasmtime::Error::msg("printLn expects a Str argument")); |
| 0000000 | 151 | }; |
| 0000000 | 152 | let arr = s.unwrap_array(&caller)?; |
| 0000000 | 153 | let len = arr.len(&caller)?; |
| 0000000 | 154 | let mut buf = vec![0u8; len as usize]; |
| 0000000 | 155 | arr.copy_to_i8_slice(&mut caller, &mut buf)?; |
| 0000000 | 156 | printed_for_closure.lock().unwrap().push(String::from_utf8_lossy(&buf).into_owned()); |
| 0000000 | 157 | Ok(()) |
| 0000000 | 158 | }); |
| 0000000 | 159 | |
| 0000000 | 160 | let instance = wasmtime::Instance::new(&mut store, &module, &[wasmtime::Extern::Func(print_ln)]) |
| 0000000 | 161 | .expect("module should instantiate"); |
| 0000000 | 162 | let main = instance |
| 0000000 | 163 | .get_typed_func::<(), ()>(&mut store, "main") |
| 0000000 | 164 | .expect("main should have signature () -> ()"); |
| 0000000 | 165 | main.call(&mut store, ()).expect("main should not trap"); |
| 0000000 | 166 | |
| 0000000 | 167 | assert_eq!(*printed.lock().unwrap(), vec!["hello from plum".to_string(), "hello, world!".to_string()]); |
| 0000000 | 168 | } |