v4 / vlib / v3 / tests / wasm_codegen_test.v
887 lines · 814 sloc · 40.5 KB · 7e64778441b0a076e75a61df7eb5cfcef6c75f39
Raw
1import os
2
3const vexe = @VEXE
4const tests_dir = os.dir(@FILE)
5const v3_dir = os.dir(tests_dir)
6const v3_src = os.join_path(v3_dir, 'v3.v')
7
8fn v3_binary() string {
9 v3_bin := os.join_path(os.vtmp_dir(), 'v3_wasm_codegen_test')
10 build :=
11 os.execute('${os.quoted_path(vexe)} -o ${os.quoted_path(v3_bin)} ${os.quoted_path(v3_src)}')
12 assert build.exit_code == 0, build.output
13 return v3_bin
14}
15
16fn compile_to_wasm(v3_bin string, src string, name string) string {
17 src_path := os.join_path(os.vtmp_dir(), '${name}.v')
18 out_path := os.join_path(os.vtmp_dir(), '${name}.wasm')
19 os.write_file(src_path, src) or { panic(err) }
20 os.rm(out_path) or {}
21 res :=
22 os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_path)} ${os.quoted_path(src_path)}')
23 assert res.exit_code == 0, res.output
24 assert os.exists(out_path), 'missing wasm output for ${name}'
25 return out_path
26}
27
28// assert_valid_wasm checks the module header and that it is non-trivial.
29fn assert_valid_wasm(path string) {
30 bytes := os.read_bytes(path) or { panic(err) }
31 assert bytes.len > 8, 'wasm too small'
32 assert bytes[0] == 0x00 && bytes[1] == 0x61 && bytes[2] == 0x73 && bytes[3] == 0x6d, 'bad wasm magic'
33 assert bytes[4] == 0x01 && bytes[5] == 0x00 && bytes[6] == 0x00 && bytes[7] == 0x00, 'bad wasm version'
34}
35
36fn node_path() ?string {
37 path := os.find_abs_path_of_executable('node') or { return none }
38 return path
39}
40
41// last_line returns the last non-empty line, ignoring any stderr noise such as
42// node's WASI ExperimentalWarning that os.execute folds into the output.
43fn last_line(out string) string {
44 mut result := ''
45 for line in out.split_into_lines() {
46 if line.trim_space().len > 0 {
47 result = line.trim_space()
48 }
49 }
50 return result
51}
52
53fn run_node(node string, runner string, wasm string) os.Result {
54 return os.execute('${os.quoted_path(node)} --no-warnings ${os.quoted_path(runner)} ${os.quoted_path(wasm)}')
55}
56
57// run_wasi_expect runs a WASI module and asserts its trailing output lines.
58// Skips execution (compile/validate already happened) when node is absent.
59fn run_wasi_expect(wasm string, expected []string) {
60 node := node_path() or { return }
61 runner := os.join_path(os.vtmp_dir(), 'wasm_run_wasi.mjs')
62 os.write_file(runner, wasi_runner_js) or { panic(err) }
63 res := run_node(node, runner, wasm)
64 assert res.exit_code == 0, res.output
65 lines := res.output.split_into_lines().map(it.trim_space()).filter(it.len > 0)
66 assert lines.len >= expected.len, res.output
67 for i, want in expected {
68 got := lines[lines.len - expected.len + i]
69 assert got == want, 'line ${i}: got ${got}, want ${want} (full: ${res.output})'
70 }
71}
72
73fn test_wasm_block_scoping_preserves_outer_locals() {
74 v3_bin := v3_binary()
75 // A shadowing for-initializer and a loop-body declaration must not leak:
76 // after the loops the outer i (10) and x (1) are restored.
77 src := 'fn main() {\n\ti := 10\n\tfor i := 0; i < 1; i++ {\n\t}\n\tprintln(i)\n\tx := 1\n\tfor j := 0; j < 2; j++ {\n\t\tx := j + 2\n\t\tprintln(x)\n\t}\n\tprintln(x)\n}\n'
78 wasm := compile_to_wasm(v3_bin, src, 'wasm_scope')
79 assert_valid_wasm(wasm)
80 run_wasi_expect(wasm, ['10', '2', '3', '1'])
81}
82
83fn test_wasm_narrow_integer_casts_and_arithmetic_wrap() {
84 v3_bin := v3_binary()
85 src := 'fn main() {\n\tprintln(int(i8(128)))\n\tprintln(int(u8(256)))\n\tprintln(int(u16(65536)))\n\tprintln(int(i16(32768)))\n\tmut a := u8(250)\n\ta += u8(10)\n\tprintln(int(a))\n\tmut b := i8(127)\n\tb++\n\tprintln(int(b))\n\tprintln(int(u8(200) + u8(100)))\n}\n'
86 wasm := compile_to_wasm(v3_bin, src, 'wasm_narrow')
87 assert_valid_wasm(wasm)
88 run_wasi_expect(wasm, ['-128', '0', '0', '-32768', '4', '-128', '44'])
89}
90
91fn test_wasm_runtime_oversized_shift_is_zero() {
92 v3_bin := v3_binary()
93 // V yields 0 when a runtime shift count is >= the operand width, whereas
94 // raw WASM masks the count modulo the width; in-range shifts are unchanged.
95 src := 'fn osc() u64 {\n\treturn u64(64)\n}\n\nfn main() {\n\tshift := osc()\n\tbits := u64(9221120237041090561)\n\tprintln(bits >> shift)\n\tprintln(bits << shift)\n\tmut left := u64(1)\n\tleft <<= shift\n\tprintln(left)\n\tprintln(u64(1) << u64(40))\n}\n'
96 wasm := compile_to_wasm(v3_bin, src, 'wasm_shiftguard')
97 assert_valid_wasm(wasm)
98 run_wasi_expect(wasm, ['0', '0', '0', '1099511627776'])
99}
100
101fn test_wasm_hello_world() {
102 v3_bin := v3_binary()
103 wasm := compile_to_wasm(v3_bin, "fn main() {\n\tprintln('hello world')\n}\n", 'wasm_hello')
104 assert_valid_wasm(wasm)
105
106 node := node_path() or {
107 eprintln('> node not found, skipping wasm execution check')
108 return
109 }
110 runner := os.join_path(os.vtmp_dir(), 'wasm_run_wasi.mjs')
111 os.write_file(runner, wasi_runner_js) or { panic(err) }
112 res := run_node(node, runner, wasm)
113 assert res.exit_code == 0, res.output
114 assert last_line(res.output) == 'hello world', res.output
115}
116
117fn test_wasm_control_flow_and_int_print() {
118 v3_bin := v3_binary()
119 src := 'fn main() {\n\tmut sum := 0\n\tfor i := 0; i < 5; i++ {\n\t\tsum = sum + i\n\t}\n\tif sum > 5 {\n\t\tprintln(sum)\n\t} else {\n\t\tprintln(-1)\n\t}\n}\n'
120 wasm := compile_to_wasm(v3_bin, src, 'wasm_flow')
121 assert_valid_wasm(wasm)
122
123 node := node_path() or { return }
124 runner := os.join_path(os.vtmp_dir(), 'wasm_run_wasi.mjs')
125 os.write_file(runner, wasi_runner_js) or { panic(err) }
126 res := run_node(node, runner, wasm)
127 assert res.exit_code == 0, res.output
128 assert last_line(res.output) == '10', res.output
129}
130
131fn test_wasm_exported_functions() {
132 v3_bin := v3_binary()
133 src := 'fn add(a int, b int) int {\n\treturn a + b\n}\n\nfn fib(n int) int {\n\tif n < 2 {\n\t\treturn n\n\t}\n\treturn fib(n - 1) + fib(n - 2)\n}\n\nfn gcd(a int, b int) int {\n\tmut x := a\n\tmut y := b\n\tfor y != 0 {\n\t\tt := y\n\t\ty = x % y\n\t\tx = t\n\t}\n\treturn x\n}\n'
134 wasm := compile_to_wasm(v3_bin, src, 'wasm_exports')
135 assert_valid_wasm(wasm)
136
137 node := node_path() or { return }
138 runner := os.join_path(os.vtmp_dir(), 'wasm_call_exports.mjs')
139 os.write_file(runner, exports_runner_js) or { panic(err) }
140 res := run_node(node, runner, wasm)
141 assert res.exit_code == 0, res.output
142 // runner prints "add fib gcd" results space-separated
143 assert last_line(res.output) == '7 55 12', res.output
144}
145
146fn test_wasm_unsigned_i64_and_bool() {
147 v3_bin := v3_binary()
148 src := 'fn main() {\n\ta := u32(4000000000)\n\tprintln(a / u32(3))\n\tc := u64(10000000000)\n\tprintln(c * 2)\n\td := u64(1)\n\tprintln(d << 40)\n\tprintln(a > u32(3))\n}\n'
149 wasm := compile_to_wasm(v3_bin, src, 'wasm_unsigned')
150 assert_valid_wasm(wasm)
151
152 node := node_path() or { return }
153 runner := os.join_path(os.vtmp_dir(), 'wasm_run_wasi.mjs')
154 os.write_file(runner, wasi_runner_js) or { panic(err) }
155 res := run_node(node, runner, wasm)
156 assert res.exit_code == 0, res.output
157 lines := res.output.split_into_lines().map(it.trim_space()).filter(it.len > 0)
158 expected := ['1333333333', '20000000000', '1099511627776', 'true']
159 assert lines.len >= expected.len, res.output
160 for i, want in expected {
161 got := lines[lines.len - expected.len + i]
162 assert got == want, 'line ${i}: got ${got}, want ${want} (full: ${res.output})'
163 }
164}
165
166fn test_wasm_f32_cast_and_unsigned_u64_print() {
167 v3_bin := v3_binary()
168 // f32->int/i64 truncation casts and full-range unsigned u64 printing.
169 src := 'fn main() {\n\tf := f32(3.9)\n\tprintln(int(f))\n\tprintln(i64(f))\n\tg := f32(-2.7)\n\tprintln(int(g))\n\tprintln(u64(0) - u64(1))\n}\n'
170 wasm := compile_to_wasm(v3_bin, src, 'wasm_f32_u64')
171 assert_valid_wasm(wasm)
172
173 node := node_path() or { return }
174 runner := os.join_path(os.vtmp_dir(), 'wasm_run_wasi.mjs')
175 os.write_file(runner, wasi_runner_js) or { panic(err) }
176 res := run_node(node, runner, wasm)
177 assert res.exit_code == 0, res.output
178 lines := res.output.split_into_lines().map(it.trim_space()).filter(it.len > 0)
179 expected := ['3', '3', '-2', '18446744073709551615']
180 assert lines.len >= expected.len, res.output
181 for i, want in expected {
182 got := lines[lines.len - expected.len + i]
183 assert got == want, 'line ${i}: got ${got}, want ${want} (full: ${res.output})'
184 }
185}
186
187fn test_wasm_signed_compound_float_postfix_and_u64_literal() {
188 v3_bin := v3_binary()
189 // signed /= %= >>=, unsigned /=, float ++/--, full-range u64 decimal literal.
190 src := 'fn main() {\n\tmut x := -6\n\tx /= 2\n\tprintln(x)\n\tmut y := -7\n\ty %= 3\n\tprintln(y)\n\tmut z := -8\n\tz >>= 1\n\tprintln(z)\n\tmut u := u32(4000000000)\n\tu /= u32(2)\n\tprintln(u)\n\tmut f := 1.5\n\tf++\n\tprintln(int(f))\n\tmut g := f32(5.5)\n\tg--\n\tprintln(int(g))\n\tprintln(u64(18446744073709551615))\n}\n'
191 wasm := compile_to_wasm(v3_bin, src, 'wasm_signed_float_u64')
192 assert_valid_wasm(wasm)
193
194 node := node_path() or { return }
195 runner := os.join_path(os.vtmp_dir(), 'wasm_run_wasi.mjs')
196 os.write_file(runner, wasi_runner_js) or { panic(err) }
197 res := run_node(node, runner, wasm)
198 assert res.exit_code == 0, res.output
199 lines := res.output.split_into_lines().map(it.trim_space()).filter(it.len > 0)
200 expected := ['-3', '-1', '-4', '2000000000', '2', '4', '18446744073709551615']
201 assert lines.len >= expected.len, res.output
202 for i, want in expected {
203 got := lines[lines.len - expected.len + i]
204 assert got == want, 'line ${i}: got ${got}, want ${want} (full: ${res.output})'
205 }
206}
207
208fn test_wasm_string_literal_escapes_not_double_decoded() {
209 v3_bin := v3_binary()
210 // The parser already unescapes literals, so `'a\\nb'` is the four bytes
211 // a, backslash, n, b and must print verbatim (not as a newline). Compare
212 // against the C backend to avoid escape miscounts.
213 src := "fn main() {\n\tprintln('a\\\\nb')\n\tprintln('c\\nd')\n}\n"
214 src_path := os.join_path(os.vtmp_dir(), 'wasm_esc.v')
215 os.write_file(src_path, src) or { panic(err) }
216 c_bin := os.join_path(os.vtmp_dir(), 'wasm_esc_c')
217 cres :=
218 os.execute('${os.quoted_path(v3_bin)} -b c -o ${os.quoted_path(c_bin)} ${os.quoted_path(src_path)}')
219 assert cres.exit_code == 0, cres.output
220 cout := os.execute(os.quoted_path(c_bin))
221 assert cout.exit_code == 0, cout.output
222
223 wasm := compile_to_wasm(v3_bin, src, 'wasm_esc')
224 assert_valid_wasm(wasm)
225 node := node_path() or { return }
226 runner := os.join_path(os.vtmp_dir(), 'wasm_run_wasi.mjs')
227 os.write_file(runner, wasi_runner_js) or { panic(err) }
228 wres := run_node(node, runner, wasm)
229 assert wres.exit_code == 0, wres.output
230 wlines := wres.output.split_into_lines().map(it.trim_space()).filter(it.len > 0)
231 clines := cout.output.split_into_lines().map(it.trim_space()).filter(it.len > 0)
232 assert wlines.len >= clines.len, wres.output
233 for i, want in clines {
234 got := wlines[wlines.len - clines.len + i]
235 assert got == want, 'line ${i}: wasm ${got} != c ${want}'
236 }
237}
238
239fn test_wasm_imported_module_numeric_call() {
240 v3_bin := v3_binary()
241 dir := os.join_path(os.vtmp_dir(), 'wasm_modtest')
242 os.rmdir_all(dir) or {}
243 os.mkdir_all(os.join_path(dir, 'moda')) or { panic(err) }
244 os.write_file(os.join_path(dir, 'main.v'),
245 'import moda\n\nfn main() {\n\tprintln(moda.answer())\n\tprintln(moda.add(3, 4))\n}\n') or {
246 panic(err)
247 }
248 // answer() calls a bare helper() in the same module, which must resolve to
249 // moda.helper (not main-module helper).
250 os.write_file(os.join_path(dir, 'moda', 'moda.v'),
251 'module moda\n\nfn helper() int {\n\treturn 21\n}\n\npub fn answer() int {\n\treturn helper() * 2\n}\n\npub fn add(a int, b int) int {\n\treturn a + b\n}\n') or {
252 panic(err)
253 }
254 out_wasm := os.join_path(dir, 'main.wasm')
255 main_v := os.join_path(dir, 'main.v')
256 res :=
257 os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_wasm)} ${os.quoted_path(main_v)}')
258 assert res.exit_code == 0, res.output
259 assert_valid_wasm(out_wasm)
260 // No fallback warnings: the imported module calls must resolve.
261 assert !res.output.contains('unsupported call'), res.output
262
263 node := node_path() or { return }
264 runner := os.join_path(os.vtmp_dir(), 'wasm_run_wasi.mjs')
265 os.write_file(runner, wasi_runner_js) or { panic(err) }
266 rres := run_node(node, runner, out_wasm)
267 assert rres.exit_code == 0, rres.output
268 lines := rres.output.split_into_lines().map(it.trim_space()).filter(it.len > 0)
269 expected := ['42', '7']
270 assert lines.len >= expected.len, rres.output
271 for i, want in expected {
272 got := lines[lines.len - expected.len + i]
273 assert got == want, 'line ${i}: got ${got}, want ${want} (full: ${rres.output})'
274 }
275}
276
277fn test_wasm_module_scoped_globals() {
278 v3_bin := v3_binary()
279 // main and an imported module both declare `__global counter`. They must map
280 // to two distinct wasm globals keyed by module, so bumping the module's copy
281 // leaves main's untouched (main=100, foo=3).
282 dir := os.join_path(os.vtmp_dir(), 'wasm_modglobals')
283 os.rmdir_all(dir) or {}
284 os.mkdir_all(os.join_path(dir, 'foo')) or { panic(err) }
285 os.write_file(os.join_path(dir, 'main.v'),
286 'module main\n\nimport foo\n\n__global counter int\n\nfn main() {\n\tcounter = 100\n\tfoo.bump()\n\tfoo.bump()\n\tfoo.bump()\n\tprintln(counter)\n\tprintln(foo.get())\n}\n') or {
287 panic(err)
288 }
289 os.write_file(os.join_path(dir, 'foo', 'foo.v'),
290 'module foo\n\n__global counter int\n\npub fn bump() {\n\tcounter++\n}\n\npub fn get() int {\n\treturn counter\n}\n') or {
291 panic(err)
292 }
293 out_wasm := os.join_path(dir, 'main.wasm')
294 main_v := os.join_path(dir, 'main.v')
295 res :=
296 os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_wasm)} ${os.quoted_path(main_v)}')
297 assert res.exit_code == 0, res.output
298 assert_valid_wasm(out_wasm)
299
300 node := node_path() or { return }
301 runner := os.join_path(os.vtmp_dir(), 'wasm_run_wasi.mjs')
302 os.write_file(runner, wasi_runner_js) or { panic(err) }
303 rres := run_node(node, runner, out_wasm)
304 assert rres.exit_code == 0, rres.output
305 lines := rres.output.split_into_lines().map(it.trim_space()).filter(it.len > 0)
306 expected := ['100', '3']
307 assert lines.len >= expected.len, rres.output
308 for i, want in expected {
309 got := lines[lines.len - expected.len + i]
310 assert got == want, 'line ${i}: got ${got}, want ${want} (full: ${rres.output})'
311 }
312}
313
314fn test_wasm_for_post_uses_loop_var_not_body_shadow() {
315 v3_bin := v3_binary()
316 // A body-local `i` must not rebind the name used by the post `i++`; the
317 // outer loop counter must still advance. The count/break bound keeps the
318 // test terminating even if the fix regresses (it would loop otherwise).
319 src := 'fn main() {\n\tmut i := 0\n\tmut count := 0\n\tfor ; i < 3; i++ {\n\t\ti := 10\n\t\t_ = i\n\t\tcount++\n\t\tif count > 100 {\n\t\t\tbreak\n\t\t}\n\t}\n\tprintln(i)\n\tprintln(count)\n}\n'
320 wasm := compile_to_wasm(v3_bin, src, 'wasm_loopshadow')
321 assert_valid_wasm(wasm)
322 run_wasi_expect(wasm, ['3', '3'])
323}
324
325fn test_wasm_imported_module_alias_call() {
326 v3_bin := v3_binary()
327 dir := os.join_path(os.vtmp_dir(), 'wasm_modalias')
328 os.rmdir_all(dir) or {}
329 os.mkdir_all(os.join_path(dir, 'moda')) or { panic(err) }
330 os.write_file(os.join_path(dir, 'main.v'),
331 'import moda as m\n\nfn main() {\n\tprintln(m.answer())\n\tprintln(m.add(3, 4))\n}\n') or {
332 panic(err)
333 }
334 os.write_file(os.join_path(dir, 'moda', 'moda.v'),
335 'module moda\n\npub fn answer() int {\n\treturn 42\n}\n\npub fn add(a int, b int) int {\n\treturn a + b\n}\n') or {
336 panic(err)
337 }
338 out_wasm := os.join_path(dir, 'main.wasm')
339 main_v := os.join_path(dir, 'main.v')
340 res :=
341 os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_wasm)} ${os.quoted_path(main_v)}')
342 assert res.exit_code == 0, res.output
343 assert_valid_wasm(out_wasm)
344 assert !res.output.contains('unsupported call'), res.output
345 run_wasi_expect(out_wasm, ['42', '7'])
346}
347
348fn test_wasm_shadowing_initializer_reads_outer() {
349 v3_bin := v3_binary()
350 // The inner `x := x + 1` initializer must read the outer x (5), giving 6,
351 // and the outer x is unchanged afterwards.
352 src := 'fn main() {\n\tx := 5\n\t{\n\t\tx := x + 1\n\t\tprintln(x)\n\t}\n\tprintln(x)\n}\n'
353 wasm := compile_to_wasm(v3_bin, src, 'wasm_shadow_init')
354 assert_valid_wasm(wasm)
355 run_wasi_expect(wasm, ['6', '5'])
356}
357
358fn test_wasm_parallel_multi_assign_swaps() {
359 v3_bin := v3_binary()
360 src := 'fn main() {\n\tmut a := 1\n\tmut b := 2\n\ta, b = b, a\n\tprintln(a)\n\tprintln(b)\n}\n'
361 wasm := compile_to_wasm(v3_bin, src, 'wasm_swap')
362 assert_valid_wasm(wasm)
363 run_wasi_expect(wasm, ['2', '1'])
364}
365
366fn test_wasm_parallel_multi_assign_swaps_globals() {
367 v3_bin := v3_binary()
368 // Parallel assignment must buffer __global targets too and write them back
369 // with global_set + narrowing, so `a, b = b, a` swaps the globals (incl.
370 // sub-32-bit ones) instead of leaving them unchanged.
371 src := '__global a int\n__global b int\n__global p = u8(250)\n__global q = u8(10)\n\nfn main() {\n\ta = 1\n\tb = 2\n\ta, b = b, a\n\tprintln(a)\n\tprintln(b)\n\tp, q = q, p\n\tprintln(int(p))\n\tprintln(int(q))\n}\n'
372 wasm := compile_to_wasm(v3_bin, src, 'wasm_global_swap')
373 assert_valid_wasm(wasm)
374 run_wasi_expect(wasm, ['2', '1', '10', '250'])
375}
376
377fn test_wasm_import_aliases_are_file_scoped() {
378 v3_bin := v3_binary()
379 // Two modules each `import ... as m` for different real modules; the alias
380 // must resolve per file, not via a single global map.
381 dir := os.join_path(os.vtmp_dir(), 'wasm_aliascol')
382 os.rmdir_all(dir) or {}
383 for sub in ['mc', 'md', 'usea', 'useb'] {
384 os.mkdir_all(os.join_path(dir, sub)) or { panic(err) }
385 }
386 os.write_file(os.join_path(dir, 'main.v'),
387 'import usea\nimport useb\n\nfn main() {\n\tprintln(usea.go())\n\tprintln(useb.go())\n}\n') or {
388 panic(err)
389 }
390 os.write_file(os.join_path(dir, 'mc', 'mc.v'),
391 'module mc\n\npub fn val() int {\n\treturn 100\n}\n') or { panic(err) }
392 os.write_file(os.join_path(dir, 'md', 'md.v'),
393 'module md\n\npub fn val() int {\n\treturn 200\n}\n') or { panic(err) }
394 os.write_file(os.join_path(dir, 'usea', 'usea.v'),
395 'module usea\n\nimport mc as m\n\npub fn go() int {\n\treturn m.val()\n}\n') or {
396 panic(err)
397 }
398 os.write_file(os.join_path(dir, 'useb', 'useb.v'),
399 'module useb\n\nimport md as m\n\npub fn go() int {\n\treturn m.val()\n}\n') or {
400 panic(err)
401 }
402 out_wasm := os.join_path(dir, 'main.wasm')
403 main_v := os.join_path(dir, 'main.v')
404 res :=
405 os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_wasm)} ${os.quoted_path(main_v)}')
406 assert res.exit_code == 0, res.output
407 assert_valid_wasm(out_wasm)
408 assert !res.output.contains('unsupported call'), res.output
409 run_wasi_expect(out_wasm, ['100', '200'])
410}
411
412fn test_wasm_numeric_type_aliases() {
413 v3_bin := v3_binary()
414 // Scalar aliases must classify as their base type: the alias-typed function
415 // is emitted, and Byte(300) wraps to u8 (44) rather than keeping 300.
416 src := 'type Byte = u8\ntype MyInt = int\n\nfn val() Byte {\n\treturn Byte(300)\n}\n\nfn add(a MyInt, b MyInt) MyInt {\n\treturn a + b\n}\n\nfn main() {\n\tprintln(int(val()))\n\tprintln(int(add(3, 4)))\n\tmut x := Byte(250)\n\tx += Byte(10)\n\tprintln(int(x))\n}\n'
417 wasm := compile_to_wasm(v3_bin, src, 'wasm_alias_types')
418 assert_valid_wasm(wasm)
419 run_wasi_expect(wasm, ['44', '7', '4'])
420}
421
422fn test_wasm_float_to_unsigned_cast_saturates() {
423 v3_bin := v3_binary()
424 // V's float->int casts saturate: u64(-1.0) -> 0, 2^63 keeps the high bit,
425 // 2^64 saturates to max. Signedness comes from the target type.
426 src := 'fn main() {\n\tprintln(u64(-1.0))\n\tprintln(u64(9223372036854775808.0))\n\tprintln(u64(18446744073709551616.0))\n\tprintln(int(f32(3.9)))\n\tprintln(int(f32(-2.7)))\n}\n'
427 wasm := compile_to_wasm(v3_bin, src, 'wasm_fcast')
428 assert_valid_wasm(wasm)
429 run_wasi_expect(wasm, ['0', '9223372036854775808', '18446744073709551615', '3', '-2'])
430}
431
432fn test_wasm_shift_keeps_lhs_width() {
433 v3_bin := v3_binary()
434 // The shift result keeps the lhs width even with a wider count: a u32 shift
435 // by 40 is over-width (>= 32) -> 0, while a u64 shift by 40 is valid.
436 src := 'fn w32() u32 {\n\treturn u32(40)\n}\n\nfn w64() u64 {\n\treturn u64(40)\n}\n\nfn main() {\n\ta := u32(1)\n\tprintln(a << w64())\n\tb := u64(1)\n\tprintln(b << w32())\n\tmut e := u32(1)\n\te <<= w64()\n\tprintln(e)\n}\n'
437 wasm := compile_to_wasm(v3_bin, src, 'wasm_shiftwidth')
438 assert_valid_wasm(wasm)
439 run_wasi_expect(wasm, ['0', '1099511627776', '0'])
440}
441
442fn test_wasm_multi_decl_buffers_rhs() {
443 v3_bin := v3_binary()
444 // Both initializers of a parallel declaration must be evaluated against the
445 // enclosing scope, so `a, b := x + 1, x + 2` reads the outer x for both,
446 // giving a == 6 and b == 7.
447 src := 'fn main() {\n\tx := 5\n\t{\n\t\ta, b := x + 1, x + 2\n\t\tprintln(a)\n\t\tprintln(b)\n\t}\n}\n'
448 wasm := compile_to_wasm(v3_bin, src, 'wasm_multidecl')
449 assert_valid_wasm(wasm)
450 run_wasi_expect(wasm, ['6', '7'])
451}
452
453fn test_wasm_unsigned_right_shift_masks_narrow() {
454 v3_bin := v3_binary()
455 // `>>>` on a signed narrow operand works on the 8/16-bit pattern, so the
456 // sign-extension bits must be masked off first.
457 src := 'fn main() {\n\tprintln(int(i8(-5) >>> 1))\n\tprintln(int(i16(-5) >>> 1))\n\tprintln(int(u8(250) >>> 1))\n}\n'
458 wasm := compile_to_wasm(v3_bin, src, 'wasm_urshift')
459 assert_valid_wasm(wasm)
460 run_wasi_expect(wasm, ['125', '32765', '125'])
461}
462
463fn test_wasm_bit_not_narrows_result() {
464 v3_bin := v3_binary()
465 // ~ keeps the operand width: ~u8(0) is 255, ~u16(0) is 65535, and a u8
466 // return carries the narrowed value.
467 src := 'fn allset() u8 {\n\treturn ~u8(0)\n}\n\nfn main() {\n\tprintln(~u8(0))\n\tprintln(~u16(0))\n\tprintln(allset())\n}\n'
468 wasm := compile_to_wasm(v3_bin, src, 'wasm_bitnot')
469 assert_valid_wasm(wasm)
470 run_wasi_expect(wasm, ['255', '65535', '255'])
471}
472
473fn test_wasm_output_path_is_exact() {
474 v3_bin := v3_binary()
475 src_path := os.join_path(os.vtmp_dir(), 'wasm_exactpath.v')
476 os.write_file(src_path, "fn main() {\n\tprintln('hi')\n}\n") or { panic(err) }
477 // -o with a path that does not end in .wasm must write that exact file,
478 // not <path>.wasm.
479 out_path := os.join_path(os.vtmp_dir(), 'wasm_exact_out')
480 os.rm(out_path) or {}
481 os.rm(out_path + '.wasm') or {}
482 res :=
483 os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_path)} ${os.quoted_path(src_path)}')
484 assert res.exit_code == 0, res.output
485 assert os.exists(out_path), 'expected exact output ${out_path}'
486 assert !os.exists(out_path + '.wasm'), 'unexpected ${out_path}.wasm'
487 assert_valid_wasm(out_path)
488}
489
490fn test_wasm_mixed_numeric_println_promotes() {
491 v3_bin := v3_binary()
492 // `1 + u64(x)` promotes to u64, so it must print unsigned, not as a signed
493 // int. Also a u32 sum with the high bit set must zero-extend.
494 src := 'fn main() {\n\tprintln(1 + u64(9223372036854775808))\n\tprintln(u64(9223372036854775808) + 1)\n\ta := u32(4000000000)\n\tprintln(1 + a)\n}\n'
495 wasm := compile_to_wasm(v3_bin, src, 'wasm_promote')
496 assert_valid_wasm(wasm)
497 run_wasi_expect(wasm, ['9223372036854775809', '9223372036854775809', '4000000001'])
498}
499
500fn test_wasm_nested_module_import_call() {
501 v3_bin := v3_binary()
502 // `import foo.bar` selects `bar`; calls must resolve to the `module bar`
503 // declaration name, not the full import path.
504 dir := os.join_path(os.vtmp_dir(), 'wasm_nestmod')
505 os.rmdir_all(dir) or {}
506 os.mkdir_all(os.join_path(dir, 'foo', 'bar')) or { panic(err) }
507 os.write_file(os.join_path(dir, 'main.v'),
508 'import foo.bar\n\nfn main() {\n\tprintln(bar.answer())\n\tprintln(bar.add(3, 4))\n}\n') or {
509 panic(err)
510 }
511 os.write_file(os.join_path(dir, 'foo', 'bar', 'bar.v'),
512 'module bar\n\npub fn answer() int {\n\treturn 42\n}\n\npub fn add(a int, b int) int {\n\treturn a + b\n}\n') or {
513 panic(err)
514 }
515 out_wasm := os.join_path(dir, 'main.wasm')
516 main_v := os.join_path(dir, 'main.v')
517 res :=
518 os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_wasm)} ${os.quoted_path(main_v)}')
519 assert res.exit_code == 0, res.output
520 assert_valid_wasm(out_wasm)
521 assert !res.output.contains('unsupported call'), res.output
522 run_wasi_expect(out_wasm, ['42', '7'])
523}
524
525fn test_wasm_unsigned_shift_assign_masks_narrow() {
526 v3_bin := v3_binary()
527 // `>>>=` on a signed narrow local must shift the 8/16-bit pattern, not the
528 // sign-extended i32.
529 src := 'fn main() {\n\tmut x := i8(-5)\n\tx >>>= 1\n\tprintln(int(x))\n\tmut y := i16(-5)\n\ty >>>= 1\n\tprintln(int(y))\n}\n'
530 wasm := compile_to_wasm(v3_bin, src, 'wasm_urshift_assign')
531 assert_valid_wasm(wasm)
532 run_wasi_expect(wasm, ['125', '32765'])
533}
534
535fn test_wasm_if_expression_value() {
536 v3_bin := v3_binary()
537 // `if` used as a value (decl rhs, return, else-if chain) selects the branch
538 // value rather than producing zero.
539 src := 'fn classify(n int) int {\n\treturn if n > 0 { 1 } else if n < 0 { -1 } else { 0 }\n}\n\nfn main() {\n\tx := if 3 > 2 { 10 } else { 20 }\n\tprintln(x)\n\tprintln(classify(5))\n\tprintln(classify(-5))\n\tprintln(classify(0))\n}\n'
540 wasm := compile_to_wasm(v3_bin, src, 'wasm_ifexpr')
541 assert_valid_wasm(wasm)
542 run_wasi_expect(wasm, ['10', '1', '-1', '0'])
543}
544
545fn test_wasm_top_level_const_inlined() {
546 v3_bin := v3_binary()
547 // Numeric/bool consts are inlined at the use site instead of warning + 0.
548 src := 'const answer = 42\nconst big = u64(9223372036854775808)\n\nfn main() {\n\tprintln(answer)\n\tprintln(big)\n\tprintln(answer + 1)\n}\n'
549 wasm := compile_to_wasm(v3_bin, src, 'wasm_const')
550 assert_valid_wasm(wasm)
551 run_wasi_expect(wasm, ['42', '9223372036854775808', '43'])
552}
553
554fn test_wasm_unsigned_division_promotes() {
555 v3_bin := v3_binary()
556 // An unsigned operand makes / and % unsigned even when the lhs is an
557 // untyped/signed literal; signed div/rem still works for signed operands.
558 src := 'fn main() {\n\tprintln(4000000000 / u32(2))\n\tprintln(4000000001 % u32(3))\n\tprintln(-10 / 3)\n\tprintln(-10 % 3)\n}\n'
559 wasm := compile_to_wasm(v3_bin, src, 'wasm_udiv')
560 assert_valid_wasm(wasm)
561 run_wasi_expect(wasm, ['2000000000', '2', '-3', '-1'])
562}
563
564fn test_wasm_nested_imported_modules() {
565 v3_bin := v3_binary()
566 // Nested modules imported under different aliases must resolve to their own
567 // functions. (Two nested modules that share the same leaf name collapse to a
568 // single identity in the shared import-resolution layer — the C backend emits
569 // only one such function too — so distinct leaves are used here.)
570 dir := os.join_path(os.vtmp_dir(), 'wasm_nestedmods')
571 os.rmdir_all(dir) or {}
572 os.mkdir_all(os.join_path(dir, 'foo', 'alpha')) or { panic(err) }
573 os.mkdir_all(os.join_path(dir, 'bar', 'beta')) or { panic(err) }
574 os.write_file(os.join_path(dir, 'main.v'),
575 'import foo.alpha as fu\nimport bar.beta as bu\n\nfn main() {\n\tprintln(fu.val())\n\tprintln(bu.val())\n}\n') or {
576 panic(err)
577 }
578 os.write_file(os.join_path(dir, 'foo', 'alpha', 'alpha.v'),
579 'module alpha\n\npub fn val() int {\n\treturn 111\n}\n') or { panic(err) }
580 os.write_file(os.join_path(dir, 'bar', 'beta', 'beta.v'),
581 'module beta\n\npub fn val() int {\n\treturn 222\n}\n') or { panic(err) }
582 out_wasm := os.join_path(dir, 'main.wasm')
583 main_v := os.join_path(dir, 'main.v')
584 res :=
585 os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_wasm)} ${os.quoted_path(main_v)}')
586 assert res.exit_code == 0, res.output
587 assert_valid_wasm(out_wasm)
588 assert !res.output.contains('unsupported call'), res.output
589 run_wasi_expect(out_wasm, ['111', '222'])
590}
591
592fn test_wasm_main_dir_matching_import_name() {
593 v3_bin := v3_binary()
594 // The main file lives in a directory whose name matches an imported module;
595 // `fn main` must still be treated as main-module (export main/_start).
596 dir := os.join_path(os.vtmp_dir(), 'wasm_dircollide', 'moda')
597 os.rmdir_all(os.dir(dir)) or {}
598 os.mkdir_all(os.join_path(dir, 'moda')) or { panic(err) }
599 os.write_file(os.join_path(dir, 'main.v'),
600 'import moda\n\nfn main() {\n\tprintln(moda.answer())\n}\n') or { panic(err) }
601 os.write_file(os.join_path(dir, 'moda', 'moda.v'),
602 'module moda\n\npub fn answer() int {\n\treturn 42\n}\n') or { panic(err) }
603 out_wasm := os.join_path(dir, 'main.wasm')
604 main_v := os.join_path(dir, 'main.v')
605 res :=
606 os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_wasm)} ${os.quoted_path(main_v)}')
607 assert res.exit_code == 0, res.output
608 assert_valid_wasm(out_wasm)
609 run_wasi_expect(out_wasm, ['42'])
610}
611
612fn test_wasm_user_function_named_memory() {
613 v3_bin := v3_binary()
614 // A user `fn memory()` must not collide with the exported linear memory;
615 // the module stays valid (the conflicting export is skipped).
616 src := 'fn memory() int {\n\treturn 7\n}\n\nfn main() {\n\tprintln(memory())\n}\n'
617 wasm := compile_to_wasm(v3_bin, src, 'wasm_memname')
618 assert_valid_wasm(wasm)
619 run_wasi_expect(wasm, ['7'])
620}
621
622fn test_wasm_float_cast_of_large_literal() {
623 v3_bin := v3_binary()
624 // A non-negative literal beyond i64 max must keep its magnitude/sign when
625 // cast to a float (the integer path would wrap it negative first).
626 src := 'fn main() {\n\tprintln(int(f64(9223372036854775808) > 0.0))\n\tprintln(int(f64(18446744073709551615) > 0.0))\n\tprintln(int(f64(9223372036854775808) == 9223372036854775808.0))\n}\n'
627 wasm := compile_to_wasm(v3_bin, src, 'wasm_fcastlit')
628 assert_valid_wasm(wasm)
629 run_wasi_expect(wasm, ['1', '1', '1'])
630}
631
632fn test_wasm_shift_over_width_semantics() {
633 v3_bin := v3_binary()
634 // V promotes narrow types to int for shifts, so i8(-1) >> 8 stays -1 (the
635 // computation width is 32), while full-width over-width shifts zero out.
636 src := 'fn rc(n int) int {\n\treturn n\n}\n\nfn main() {\n\tprintln(int(i8(-1) >> rc(8)))\n\tprintln(u32(1) << rc(32))\n\ta := u64(1)\n\tprintln(a << rc(64))\n\tprintln(int(i8(-5) >>> rc(1)))\n}\n'
637 wasm := compile_to_wasm(v3_bin, src, 'wasm_shiftsem')
638 assert_valid_wasm(wasm)
639 run_wasi_expect(wasm, ['-1', '0', '0', '125'])
640}
641
642fn test_wasm_mixed_signed_unsigned_comparison() {
643 v3_bin := v3_binary()
644 // Mixed signed/unsigned comparisons compare by mathematical value, so a
645 // negative signed operand is smaller than any unsigned operand.
646 src := 'fn main() {\n\tprintln(int(i64(-89) <= u64(567)))\n\tprintln(int(int(-1) != u32(0xffffffff)))\n\tprintln(int(u64(0xfffffffffffffffe) == i64(-2)))\n\tprintln(int(u32(8543) > int(-7523)))\n\tprintln(int(i16(-27) < u32(65463356)))\n}\n'
647 wasm := compile_to_wasm(v3_bin, src, 'wasm_mixedcmp')
648 assert_valid_wasm(wasm)
649 run_wasi_expect(wasm, ['1', '1', '0', '1', '1'])
650}
651
652fn test_wasm_unary_minus_large_literal() {
653 v3_bin := v3_binary()
654 // Negating a literal outside i32 range must keep i64 width (and wrap on
655 // overflow like V's cast), not truncate to 32 bits.
656 src := 'fn main() {\n\tprintln(i64(-9223372036854775808))\n\tprintln(i64(-9223372036854775809))\n\tprintln(-9223372036854775807)\n\tprintln(i64(-5))\n}\n'
657 wasm := compile_to_wasm(v3_bin, src, 'wasm_negbig')
658 assert_valid_wasm(wasm)
659 run_wasi_expect(wasm, ['-9223372036854775808', '9223372036854775807', '-9223372036854775807',
660 '-5'])
661}
662
663fn test_wasm_init_runs_before_main() {
664 v3_bin := v3_binary()
665 // fn init() is an entry point: it (and imported-module inits) run before main.
666 src := "fn init() {\n\tprintln('init')\n}\n\nfn main() {\n\tprintln('main')\n}\n"
667 wasm := compile_to_wasm(v3_bin, src, 'wasm_init')
668 assert_valid_wasm(wasm)
669 run_wasi_expect(wasm, ['init', 'main'])
670}
671
672fn test_wasm_imported_module_init() {
673 v3_bin := v3_binary()
674 dir := os.join_path(os.vtmp_dir(), 'wasm_initmod')
675 os.rmdir_all(dir) or {}
676 os.mkdir_all(os.join_path(dir, 'moda')) or { panic(err) }
677 os.write_file(os.join_path(dir, 'main.v'),
678 "import moda\n\nfn main() {\n\tprintln('main')\n\tprintln(moda.answer())\n}\n") or {
679 panic(err)
680 }
681 os.write_file(os.join_path(dir, 'moda', 'moda.v'),
682 "module moda\n\nfn init() {\n\tprintln('moda init')\n}\n\npub fn answer() int {\n\treturn 42\n}\n") or {
683 panic(err)
684 }
685 out_wasm := os.join_path(dir, 'main.wasm')
686 res := os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_wasm)} ${os.quoted_path(os.join_path(dir,
687 'main.v'))}')
688 assert res.exit_code == 0, res.output
689 assert_valid_wasm(out_wasm)
690 run_wasi_expect(out_wasm, ['moda init', 'main', '42'])
691}
692
693fn test_wasm_char_literals() {
694 v3_bin := v3_binary()
695 src := 'fn code() u8 {\n\treturn `A`\n}\n\nfn main() {\n\tprintln(int(code()))\n\tprintln(int(`A`))\n\tprintln(int(`0`))\n\tprintln(int(`\\n`))\n}\n'
696 wasm := compile_to_wasm(v3_bin, src, 'wasm_char')
697 assert_valid_wasm(wasm)
698 run_wasi_expect(wasm, ['65', '65', '48', '10'])
699}
700
701fn test_wasm_init_only_imported_module() {
702 v3_bin := v3_binary()
703 // An imported module with only an init() (no called function) must still
704 // run its init before main.
705 dir := os.join_path(os.vtmp_dir(), 'wasm_initonly')
706 os.rmdir_all(dir) or {}
707 os.mkdir_all(os.join_path(dir, 'moda')) or { panic(err) }
708 os.write_file(os.join_path(dir, 'main.v'), "import moda\n\nfn main() {\n\tprintln('main')\n}\n") or {
709 panic(err)
710 }
711 os.write_file(os.join_path(dir, 'moda', 'moda.v'),
712 "module moda\n\nfn init() {\n\tprintln('moda init')\n}\n") or { panic(err) }
713 out_wasm := os.join_path(dir, 'main.wasm')
714 res := os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_wasm)} ${os.quoted_path(os.join_path(dir,
715 'main.v'))}')
716 assert res.exit_code == 0, res.output
717 assert_valid_wasm(out_wasm)
718 run_wasi_expect(out_wasm, ['moda init', 'main'])
719}
720
721fn test_wasm_init_dependency_order() {
722 v3_bin := v3_binary()
723 // main -> a -> b: inits run dependency-first (b, then a), then main.
724 dir := os.join_path(os.vtmp_dir(), 'wasm_initdep')
725 os.rmdir_all(dir) or {}
726 os.mkdir_all(os.join_path(dir, 'a')) or { panic(err) }
727 os.mkdir_all(os.join_path(dir, 'b')) or { panic(err) }
728 os.write_file(os.join_path(dir, 'main.v'),
729 "import a\n\nfn main() {\n\tprintln('main')\n\tprintln(a.av())\n}\n") or { panic(err) }
730 os.write_file(os.join_path(dir, 'a', 'a.v'),
731 "module a\n\nimport b\n\nfn init() {\n\tprintln('a init')\n}\n\npub fn av() int {\n\treturn b.bv()\n}\n") or {
732 panic(err)
733 }
734 os.write_file(os.join_path(dir, 'b', 'b.v'),
735 "module b\n\nfn init() {\n\tprintln('b init')\n}\n\npub fn bv() int {\n\treturn 5\n}\n") or {
736 panic(err)
737 }
738 out_wasm := os.join_path(dir, 'main.wasm')
739 res := os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_wasm)} ${os.quoted_path(os.join_path(dir,
740 'main.v'))}')
741 assert res.exit_code == 0, res.output
742 assert_valid_wasm(out_wasm)
743 run_wasi_expect(out_wasm, ['b init', 'a init', 'main', '5'])
744}
745
746fn test_wasm_imported_module_const() {
747 v3_bin := v3_binary()
748 // A numeric const accessed through an imported-module selector (mod.name)
749 // must be inlined, not emitted as 0.
750 dir := os.join_path(os.vtmp_dir(), 'wasm_constmod')
751 os.rmdir_all(dir) or {}
752 os.mkdir_all(os.join_path(dir, 'moda')) or { panic(err) }
753 os.write_file(os.join_path(dir, 'main.v'),
754 'import moda\n\nfn main() {\n\tprintln(moda.answer)\n\tprintln(moda.answer + 1)\n}\n') or {
755 panic(err)
756 }
757 os.write_file(os.join_path(dir, 'moda', 'moda.v'), 'module moda\n\npub const answer = 42\n') or {
758 panic(err)
759 }
760 out_wasm := os.join_path(dir, 'main.wasm')
761 res := os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out_wasm)} ${os.quoted_path(os.join_path(dir,
762 'main.v'))}')
763 assert res.exit_code == 0, res.output
764 assert_valid_wasm(out_wasm)
765 assert !res.output.contains('unsupported'), res.output
766 run_wasi_expect(out_wasm, ['42', '43'])
767}
768
769fn test_wasm_labeled_break_continue() {
770 v3_bin := v3_binary()
771 // `break label` exits the labeled outer loop (count 1, not 3); `continue
772 // label` advances the outer loop (count 3, not 9); plain inner break works.
773 src := 'fn main() {\n\tmut cb := 0\n\touter: for i := 0; i < 3; i++ {\n\t\tfor j := 0; j < 3; j++ {\n\t\t\tcb++\n\t\t\tif j == 0 {\n\t\t\t\tbreak outer\n\t\t\t}\n\t\t}\n\t}\n\tprintln(cb)\n\tmut cc := 0\n\to2: for i := 0; i < 3; i++ {\n\t\tfor j := 0; j < 3; j++ {\n\t\t\tcc++\n\t\t\tcontinue o2\n\t\t}\n\t}\n\tprintln(cc)\n\tmut cn := 0\n\tfor i := 0; i < 3; i++ {\n\t\tfor j := 0; j < 3; j++ {\n\t\t\tcn++\n\t\t\tbreak\n\t\t}\n\t}\n\tprintln(cn)\n}\n'
774 wasm := compile_to_wasm(v3_bin, src, 'wasm_labeled')
775 assert_valid_wasm(wasm)
776 assert !res_contains_unsupported(v3_bin, src), 'labeled loop emitted a warning'
777 run_wasi_expect(wasm, ['1', '3', '3'])
778}
779
780fn res_contains_unsupported(v3_bin string, src string) bool {
781 src_path := os.join_path(os.vtmp_dir(), 'wasm_labeled_warn.v')
782 out := os.join_path(os.vtmp_dir(), 'wasm_labeled_warn.wasm')
783 os.write_file(src_path, src) or { panic(err) }
784 res :=
785 os.execute('${os.quoted_path(v3_bin)} -b wasm -o ${os.quoted_path(out)} ${os.quoted_path(src_path)}')
786 return res.output.contains('unsupported statement: label_stmt')
787}
788
789fn test_wasm_numeric_globals() {
790 v3_bin := v3_binary()
791 // __global is lowered to a wasm mutable global: reads/writes (incl. across
792 // functions), initializers, compound assigns and narrow wrapping all work.
793 src := '__global counter int\n__global x = 10\n__global b u8\n\nfn inc() {\n\tcounter = counter + 1\n}\n\nfn main() {\n\tcounter = 3\n\tinc()\n\tinc()\n\tprintln(counter)\n\tx += 5\n\tprintln(x)\n\tb = u8(250)\n\tb += u8(10)\n\tprintln(int(b))\n}\n'
794 wasm := compile_to_wasm(v3_bin, src, 'wasm_globals')
795 assert_valid_wasm(wasm)
796 run_wasi_expect(wasm, ['5', '15', '4'])
797}
798
799fn test_wasm_global_postfix() {
800 v3_bin := v3_binary()
801 // `counter++`/`counter--` on a numeric global must read-modify-write the wasm
802 // global (incl. across functions) and narrow-wrap for sub-32-bit globals.
803 src := '__global counter int\n__global b = u8(250)\n\nfn bump() {\n\tcounter++\n}\n\nfn main() {\n\tcounter = 3\n\tcounter++\n\tbump()\n\tcounter--\n\tprintln(counter)\n\tb++\n\tprintln(int(b))\n}\n'
804 wasm := compile_to_wasm(v3_bin, src, 'wasm_global_postfix')
805 assert_valid_wasm(wasm)
806 run_wasi_expect(wasm, ['4', '251'])
807}
808
809fn test_wasm_global_narrow_cast_initializer() {
810 v3_bin := v3_binary()
811 // An out-of-range cast initializer on an inferred narrow global must be
812 // wrapped to the global's width at compile time, so the first read already
813 // sees the V value (u8(300)=44, i8(128)=-128, u16(70000)=4464).
814 src := '__global b = u8(300)\n__global s = i8(128)\n__global w = u16(70000)\n\nfn main() {\n\tprintln(int(b))\n\tprintln(int(s))\n\tprintln(int(w))\n}\n'
815 wasm := compile_to_wasm(v3_bin, src, 'wasm_global_narrow_init')
816 assert_valid_wasm(wasm)
817 run_wasi_expect(wasm, ['44', '-128', '4464'])
818}
819
820fn test_wasm_global_const_expr_initializers() {
821 v3_bin := v3_binary()
822 // A const-expression initializer (binary ops, const-identifier references,
823 // shifts) is folded into the wasm global's constant init, not zeroed.
824 src := 'const base = 10\n\n__global a = 1 + 2\n__global c = base * 4 + 1\n__global e = 1 << 4\n__global f = (7 - 2) * 3\n__global neg = -5 + 2\n__global big = i64(1) << 40\n\nfn main() {\n\tprintln(a)\n\tprintln(c)\n\tprintln(e)\n\tprintln(f)\n\tprintln(neg)\n\tprintln(big)\n}\n'
825 wasm := compile_to_wasm(v3_bin, src, 'wasm_global_const_expr')
826 assert_valid_wasm(wasm)
827 run_wasi_expect(wasm, ['3', '41', '16', '15', '-3', '1099511627776'])
828}
829
830fn test_wasm_global_nested_cast_initializer() {
831 v3_bin := v3_binary()
832 // A nested cast initializer keeps each cast's width: the inner cast narrows
833 // before the wider outer cast, so `int(u8(300))` is 44, not 300, and a folded
834 // float initializer rounds through an int cast (1.5 + 2.0 -> 3).
835 src := '__global b = int(u8(300))\n__global g = int(i8(128))\n__global h = u8(300) + u8(100)\n__global fl = 1.5 + 2.0\n\nfn main() {\n\tprintln(b)\n\tprintln(g)\n\tprintln(int(h))\n\tprintln(int(fl))\n}\n'
836 wasm := compile_to_wasm(v3_bin, src, 'wasm_global_nested_cast')
837 assert_valid_wasm(wasm)
838 run_wasi_expect(wasm, ['44', '-128', '144', '3'])
839}
840
841fn test_wasm_global_infix_narrows_before_outer_cast() {
842 v3_bin := v3_binary()
843 // A folded infix initializer must wrap to its own resolved type before a
844 // wider outer cast observes it: `u8(250)+u8(10)` narrows to 4 (not 260)
845 // before the surrounding `int`, matching normal codegen.
846 src := '__global x = int(u8(250) + u8(10))\n__global y = int(u8(200) + u8(100))\n\nfn main() {\n\tprintln(x)\n\tprintln(y)\n}\n'
847 wasm := compile_to_wasm(v3_bin, src, 'wasm_global_infix_cast')
848 assert_valid_wasm(wasm)
849 run_wasi_expect(wasm, ['4', '44'])
850}
851
852fn test_wasm_global_float_cast_of_large_literal() {
853 v3_bin := v3_binary()
854 // A float cast of a large non-negative integer literal must stay positive,
855 // like gen_cast: f64(9223372036854775808) (2^63) must not flip sign through
856 // parse_int_literal's wrapped i64, so `x > 0.0` holds.
857 src := '__global x = f64(9223372036854775808)\n\nfn main() {\n\tprintln(x > 0.0)\n}\n'
858 wasm := compile_to_wasm(v3_bin, src, 'wasm_global_float_cast')
859 assert_valid_wasm(wasm)
860 run_wasi_expect(wasm, ['true'])
861}
862
863fn test_wasm_global_unsigned_const_fold() {
864 v3_bin := v3_binary()
865 // Unsigned constant initializers must fold with unsigned div/rem/compare:
866 // folded operands are i64 bit patterns (u64 max -> -1), so signed division
867 // would mis-fold `u64(max) / u64(2)` to 0 instead of 9223372036854775807.
868 src := '__global half = u64(18446744073709551615) / u64(2)\n__global rem = u64(18446744073709551615) % u64(10)\n__global cmp = u64(18446744073709551615) > u64(1)\n\nfn main() {\n\tprintln(half)\n\tprintln(rem)\n\tprintln(cmp)\n}\n'
869 wasm := compile_to_wasm(v3_bin, src, 'wasm_global_unsigned_fold')
870 assert_valid_wasm(wasm)
871 run_wasi_expect(wasm, ['9223372036854775807', '5', 'true'])
872}
873
874const wasi_runner_js = "import { WASI } from 'node:wasi';
875import { readFile } from 'node:fs/promises';
876const wasi = new WASI({ version: 'preview1', args: [], env: {} });
877const bytes = await readFile(process.argv[2]);
878const mod = await WebAssembly.compile(bytes);
879const inst = await WebAssembly.instantiate(mod, wasi.getImportObject());
880wasi.start(inst);
881"
882
883const exports_runner_js = "import { readFileSync } from 'node:fs';
884const { instance: i } = await WebAssembly.instantiate(readFileSync(process.argv[2]));
885const e = i.exports;
886process.stdout.write(`\${e.add(3, 4)} \${e.fib(10)} \${e.gcd(48, 36)}`);
887"
888