| 1 | import os |
| 2 | |
| 3 | const vexe = @VEXE |
| 4 | const tests_dir = os.dir(@FILE) |
| 5 | const v3_dir = os.dir(tests_dir) |
| 6 | const v3_src = os.join_path(v3_dir, 'v3.v') |
| 7 | |
| 8 | fn 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 | |
| 16 | fn 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. |
| 29 | fn 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 | |
| 36 | fn 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. |
| 43 | fn 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 | |
| 53 | fn 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. |
| 59 | fn 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 | |
| 73 | fn 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 | |
| 83 | fn 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 | |
| 91 | fn 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 | |
| 101 | fn 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 | |
| 117 | fn 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 | |
| 131 | fn 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 | |
| 146 | fn 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 | |
| 166 | fn 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 | |
| 187 | fn 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 | |
| 208 | fn 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 | |
| 239 | fn 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 | |
| 277 | fn 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 | |
| 314 | fn 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 | |
| 325 | fn 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 | |
| 348 | fn 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 | |
| 358 | fn 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 | |
| 366 | fn 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 | |
| 377 | fn 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 | |
| 412 | fn 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 | |
| 422 | fn 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 | |
| 432 | fn 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 | |
| 442 | fn 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 | |
| 453 | fn 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 | |
| 463 | fn 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 | |
| 473 | fn 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 | |
| 490 | fn 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 | |
| 500 | fn 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 | |
| 525 | fn 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 | |
| 535 | fn 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 | |
| 545 | fn 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 | |
| 554 | fn 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 | |
| 564 | fn 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 | |
| 592 | fn 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 | |
| 612 | fn 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 | |
| 622 | fn 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 | |
| 632 | fn 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 | |
| 642 | fn 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 | |
| 652 | fn 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 | |
| 663 | fn 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 | |
| 672 | fn 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 | |
| 693 | fn 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 | |
| 701 | fn 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 | |
| 721 | fn 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 | |
| 746 | fn 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 | |
| 769 | fn 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 | |
| 780 | fn 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 | |
| 789 | fn 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 | |
| 799 | fn 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 | |
| 809 | fn 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 | |
| 820 | fn 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 | |
| 830 | fn 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 | |
| 841 | fn 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 | |
| 852 | fn 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 | |
| 863 | fn 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 | |
| 874 | const wasi_runner_js = "import { WASI } from 'node:wasi'; |
| 875 | import { readFile } from 'node:fs/promises'; |
| 876 | const wasi = new WASI({ version: 'preview1', args: [], env: {} }); |
| 877 | const bytes = await readFile(process.argv[2]); |
| 878 | const mod = await WebAssembly.compile(bytes); |
| 879 | const inst = await WebAssembly.instantiate(mod, wasi.getImportObject()); |
| 880 | wasi.start(inst); |
| 881 | " |
| 882 | |
| 883 | const exports_runner_js = "import { readFileSync } from 'node:fs'; |
| 884 | const { instance: i } = await WebAssembly.instantiate(readFileSync(process.argv[2])); |
| 885 | const e = i.exports; |
| 886 | process.stdout.write(`\${e.add(3, 4)} \${e.fib(10)} \${e.gcd(48, 36)}`); |
| 887 | " |
| 888 | |