plum

#treesitter#compiler#wasm

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

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


265296b — Peter John 2026-11-03T16:54:24+05:30
codegen: regression tests for break/continue inside match arms in loops
plum-wasm-codegen/tests/loop_break_e2e_test.rs ADDED
@@ -0,0 +1,194 @@
1
+ #![allow(non_snake_case)]
2
+ use plum_wasm_codegen::compileSource;
3
+
4
+ fn tailEngine() -> wasmtime::Engine {
5
+ let mut config = wasmtime::Config::new();
6
+ config.wasm_gc(true);
7
+ config.wasm_function_references(true);
8
+ config.wasm_tail_call(true);
9
+ wasmtime::Engine::new(&config).expect("engine with GC and tail calls should construct")
10
+ }
11
+
12
+ fn repoRoot() -> std::path::PathBuf {
13
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
14
+ }
15
+
16
+ fn compile(name: &str, src: &str) -> Vec<u8> {
17
+ let dir = std::env::temp_dir().join(format!("plum_tail_calls_{}_{}", name, std::process::id()));
18
+ std::fs::create_dir_all(&dir).unwrap();
19
+ let path = dir.join("main.plum");
20
+ std::fs::write(&path, src).unwrap();
21
+ let r = plum_core::loadAndMerge(&path, &repoRoot());
22
+ let _ = std::fs::remove_dir_all(&dir);
23
+ let source = r.unwrap_or_else(|e| panic!("load: {e}"));
24
+ let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
25
+ let mut v = wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all());
26
+ v.validate_all(&bytes).expect("valid wasm");
27
+ bytes
28
+ }
29
+
30
+ /// Compiles `src` through the real pipeline and runs its `main() -> Int`.
31
+ fn runInt(name: &str, src: &str) -> i64 {
32
+ let bytes = compile(name, src);
33
+ let engine = tailEngine();
34
+ let module = wasmtime::Module::new(&engine, &bytes).unwrap();
35
+ let mut store = wasmtime::Store::new(&engine, ());
36
+ let mut linker = wasmtime::Linker::new(&engine);
37
+ linker
38
+ .define_unknown_imports_as_default_values(&mut store, &module)
39
+ .unwrap();
40
+ let instance = linker.instantiate(&mut store, &module).unwrap();
41
+ let main = instance
42
+ .get_typed_func::<(), i64>(&mut store, "main")
43
+ .unwrap();
44
+ main.call(&mut store, ()).unwrap()
45
+ }
46
+
47
+ const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/List\n\n";
48
+
49
+ fn prog(body: &str) -> String {
50
+ format!("{PRE}{body}")
51
+ }
52
+
53
+ // Every loop is fuel-bounded (`i < 50`) so a wrong branch depth fails an
54
+ // assertion instead of hanging the test.
55
+ const LST: &str = "
56
+ enum Lst = Nil | Cons(head: Int, tail: Lst)
57
+
58
+ fun build(n: Int, acc: Lst) -> Lst =
59
+ if n == 0 then acc else build(n - 1, Cons(head: n, tail: acc))
60
+ ";
61
+
62
+ #[test]
63
+ fn break_in_if_in_match_arm_in_while() {
64
+ let src = prog(&format!(
65
+ "{LST}
66
+ fun find(xs: Lst, target: Int) -> Int =
67
+ i := 0
68
+ cur := xs
69
+ while i < 50
70
+ match cur
71
+ Cons(h, t) =>
72
+ if h == target
73
+ break
74
+ cur = t
75
+ Nil =>
76
+ break
77
+ i = i + 1
78
+ return i
79
+
80
+ fun main() -> Int = find(build(10, Nil), 4) * 100 + find(build(10, Nil), 99)
81
+ "
82
+ ));
83
+ assert_eq!(runInt("break_if_match_while", &src), 3 * 100 + 10);
84
+ }
85
+
86
+ #[test]
87
+ fn continue_in_if_in_match_arm_in_while() {
88
+ let src = prog(&format!(
89
+ "{LST}
90
+ fun sumOdd(xs: Lst) -> Int =
91
+ total := 0
92
+ i := 0
93
+ cur := xs
94
+ while i < 50
95
+ i = i + 1
96
+ match cur
97
+ Cons(h, t) =>
98
+ cur = t
99
+ if h % 2 == 0
100
+ continue
101
+ total = total + h
102
+ Nil =>
103
+ break
104
+ return total * 100 + i
105
+
106
+ fun main() -> Int = sumOdd(build(6, Nil))
107
+ "
108
+ ));
109
+ // 1 + 3 + 5 = 9; seven iterations (six cons cells then Nil).
110
+ assert_eq!(runInt("continue_if_match_while", &src), 900 + 7);
111
+ }
112
+
113
+ #[test]
114
+ fn break_in_match_arm_direct() {
115
+ let src = prog(&format!(
116
+ "{LST}
117
+ fun count(xs: Lst) -> Int =
118
+ i := 0
119
+ cur := xs
120
+ while i < 50
121
+ match cur
122
+ Nil =>
123
+ break
124
+ Cons(h, t) =>
125
+ cur = t
126
+ i = i + 1
127
+ return i
128
+
129
+ fun wild(n: Int) -> Int =
130
+ i := 0
131
+ while i < 50
132
+ match i
133
+ _ =>
134
+ break
135
+ i = i + 1
136
+ return i + n
137
+
138
+ fun main() -> Int = count(build(5, Nil)) * 100 + wild(7)
139
+ "
140
+ ));
141
+ assert_eq!(runInt("break_match_arm_direct", &src), 5 * 100 + 7);
142
+ }
143
+
144
+ #[test]
145
+ fn break_in_else_if_chain_in_while() {
146
+ let src = prog(
147
+ "fun main() -> Int =
148
+ i := 0
149
+ while i < 50
150
+ if i == 100
151
+ i = i + 1000
152
+ else if i == 100
153
+ i = i + 2000
154
+ else if i == 7
155
+ break
156
+ i = i + 1
157
+ return i
158
+ ",
159
+ );
160
+ assert_eq!(runInt("break_else_if_chain", &src), 7);
161
+ }
162
+
163
+ #[test]
164
+ fn nested_loops_with_match_in_inner() {
165
+ let src = prog(&format!(
166
+ "{LST}
167
+ fun main() -> Int =
168
+ outer := 0
169
+ hits := 0
170
+ while outer < 50
171
+ if outer == 3
172
+ break
173
+ cur := build(5, Nil)
174
+ j := 0
175
+ while j < 50
176
+ match cur
177
+ Cons(h, t) =>
178
+ cur = t
179
+ if h == 2
180
+ j = j + 1
181
+ continue
182
+ if h == 4
183
+ break
184
+ hits = hits + 1
185
+ Nil =>
186
+ break
187
+ j = j + 1
188
+ outer = outer + 1
189
+ return outer * 100 + hits
190
+ "
191
+ ));
192
+ // inner: h=1 hit, h=2 skipped, h=3 hit, h=4 break => 2 hits per outer pass, 3 passes.
193
+ assert_eq!(runInt("nested_loops_match", &src), 3 * 100 + 6);
194
+ }