wasm: restore backend through shared SSA pipeline (#29742)

* wasm: restore WebAssembly backend through shared SSA

* wasm: fix SSA lowering and optimization review regressions

* wasm: preserve comparison literals with narrow integer operands

* ssa: fix numeric stores, full-width folding, and runtime symbol ownership

* ssa: keep backend-specific runtime and regression targets

Restore the native benchmark declaration and select the qualified WASM helper names for 32-bit targets. Point the WASM rune and runtime name controls at their intended target without changing their assertions or the public native runtime policy.

* ssa: preserve Darwin uname inline array ABI in native output
This commit is contained in:
Alexander Medvednikov authored and GitHub committed 2026-10-08 07:19:02 +03:00
1 parent 7d8484fe13
commit a35cd0a69f
22 files changed
+5630 -708

No files matched your search

+1 -1
View File
@@ -86,7 +86,7 @@ The main files are:
7. `vlib/v/gen/c` is the primary C backend. Other backends and lowerings live under
`vlib/v/gen/`, including FastC, WebAssembly, and ARM64 via SSA, MIR, and instruction
selection.
selection. WebAssembly also consumes SSA; see [the backend guide](doc/wasm.md).
The rest of the directories are vlib modules: `builtin/` (strings, arrays,
maps), `time/`, `os/`, etc. Their documentation is pretty clear.
+54
View File
@@ -0,0 +1,54 @@
# WebAssembly backend
V's WebAssembly backend lowers the checked and transformed program to `v.ssa`, then emits a
WebAssembly binary directly. It shares SSA construction and optimization with the ARM64 backend.
`-prod` runs the SSA optimizer, including scalar local promotion and phi elimination, before
WebAssembly generation. No C compiler, Emscripten, or Binaryen is needed to emit the module.
The standard compiler includes the backend. Rebuild it, then compile a program:
```sh
./v self
./v -b wasm -o hello.wasm examples/hello_world.v
./v -prod -b wasm -o hello.wasm examples/hello_world.v
```
A standalone compiler built from `vlib/v/v.v` needs `-compile-backend wasm` or `-all-backends`
when it is compiled. `-d skip_wasm` excludes WebAssembly from a compiler build.
The output path supplied with `-o` is used exactly. Programs with `main` export a WASI `_start`
entry point and linear `memory`. Module initialization functions run before `main`, in dependency
order. Modules without `main` run their initialization functions when instantiated.
The backend supports scalar integer, boolean, and floating-point functions, calls, recursion,
branches, loops, and numeric globals. `print` and `println` support integers, booleans, and string
literals. WASI output uses
`wasi_snapshot_preview1.fd_write`. Main-module scalar functions are exported using their V names;
imported functions use their qualified module name with dots replaced by double underscores.
An explicit `@[export: 'name']` attribute supplies the export name.
Rune literals retain their full Unicode code points in expressions and global initializers.
Integer literals retain their width until an operation or comparison selects its operand types,
including full-width constants in production optimization. Numeric assignments and arguments
convert to their declared types before optimization, for both direct calls and function values.
Floating-point unary negation preserves signed zero in both unoptimized and optimized output.
Constants and global initializers in moduleless scripts keep the script module scope after imports.
Implicit-main scripts retain imported functions called by their top-level statements. User
functions retain their bodies when their names overlap synthetic runtime helpers.
WebAssembly uses 32-bit pointers and target-specific SSA memory layouts. Control flow is emitted
from SSA basic blocks, with parallel copies on phi edges. Unsupported operations produce a compiler
error. Aggregate language features remain experimental; options, results, arrays, maps, and general
struct operations are not supported by this backend.
Runtime allocations use a zeroing bump allocator. Heap storage remains allocated for the module's
lifetime. Function calls use a separate 1 MiB stack in linear memory and trap if it is exhausted.
After a runtime trap, create a fresh instance before continuing execution.
The focused tests instantiate generated modules with Node.js and exercise both unoptimized and
optimized SSA:
```sh
./v test vlib/v/gen/wasm/ssa_gen_test.v
V3_TEST_WASM=1 ./v vlib/v/compiler_tests/wasm_codegen_test.v
```
+3
View File
@@ -34,6 +34,9 @@ backend; FastC is omitted by default because native linking does not include lib
The native runtime initializes globals, runtime constants, and module state before `main`.
Native macOS executables use a 64 MiB main stack, matching the C backend's linker setting
for recursive compiler passes.
Native macOS `C.utsname` uses Darwin's five inline `char[256]` fields. Field expressions
retain the declared `&char` value by returning each array's address. This ABI mapping is
specific to the 64-bit native target and leaves WebAssembly's 32-bit layout unchanged.
Runtime constant initializers run once, after their dependencies. The runtime also provides
process capture, pthread spawning and waiting, exit callbacks, and string-formatting helpers.
Native fixed-point interpolation follows the C backend's decimal rounding and padding policy.
+412 -27
View File
@@ -4,21 +4,24 @@ const vexe = @VEXE
const tests_dir = os.dir(@FILE)
const v3_dir = os.dir(tests_dir)
const v3_src = os.join_path(v3_dir, 'v.v')
const wasm_compiler = os.join_path(os.vtmp_dir(), 'ssa_wasm_codegen_compiler_${os.getpid()}')
fn testsuite_begin() {
if os.getenv('V3_TEST_WASM') != '1' {
eprintln('> skipping v3 wasm backend tests; set V3_TEST_WASM=1 to run')
exit(0)
}
build := os.exec([vexe, '-new-compiler', '-no-retry-compilation', '-cc', 'clang', '-gc', 'none',
'-compile-backend', 'wasm', '-o', wasm_compiler, v3_src])
assert build.exit_code == 0, build.output
}
fn testsuite_end() {
os.rm(wasm_compiler) or {}
}
fn v3_binary() string {
v3_bin := os.join_path(os.vtmp_dir(), 'v3_wasm_codegen_test')
build :=
os.exec([vexe, '-new-compiler', '-no-retry-compilation', '-cc', 'clang', '-gc', 'none',
'-compile-backend', 'wasm', '-o', v3_bin, '${v3_src}'])
assert build.exit_code == 0, build.output
return v3_bin
return wasm_compiler
}
fn compile_to_wasm(v3_bin string, src string, name string) string {
@@ -27,8 +30,8 @@ fn compile_to_wasm(v3_bin string, src string, name string) string {
os.write_file(src_path, src) or { panic(err) }
os.rm(out_path) or {}
res :=
os.exec([v3_bin, '-b', 'wasm', '-o', out_path, src_path])
assert res.exit_code == 0, res.output
os.exec([v3_bin, '-enable-globals', '-b', 'wasm', '-o', out_path, src_path])
assert res.exit_code == 0, '${name}: ${res.output}'
assert os.exists(out_path), 'missing wasm output for ${name}'
return out_path
}
@@ -74,15 +77,15 @@ fn run_wasi_expect(wasm string, expected []string) {
assert lines.len >= expected.len, res.output
for i, want in expected {
got := lines[lines.len - expected.len + i]
assert got == want, 'line ${i}: got ${got}, want ${want} (full: ${res.output})'
assert got == want, '${os.base(wasm)} line ${i}: got ${got}, want ${want} (full: ${res.output})'
}
}
fn test_wasm_block_scoping_preserves_outer_locals() {
v3_bin := v3_binary()
// A shadowing for-initializer and a loop-body declaration must not leak:
// after the loops the outer i (10) and x (1) are restored.
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'
// A for-initializer and a loop-body declaration must not alter outer locals:
// after the loops the outer i and x remain 10 and 1.
src := 'fn main() {\n\ti := 10\n\tfor inner_i := 0; inner_i < 1; inner_i++ {\n\t}\n\tprintln(i)\n\tx := 1\n\tfor j := 0; j < 2; j++ {\n\t\tinner_x := j + 2\n\t\tprintln(inner_x)\n\t}\n\tprintln(x)\n}\n'
wasm := compile_to_wasm(v3_bin, src, 'wasm_scope')
assert_valid_wasm(wasm)
run_wasi_expect(wasm, ['10', '2', '3', '1'])
@@ -90,7 +93,7 @@ fn test_wasm_block_scoping_preserves_outer_locals() {
fn test_wasm_narrow_integer_casts_and_arithmetic_wrap() {
v3_bin := v3_binary()
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'
src := 'fn main() {\n\tprintln(int(i8(i16(128))))\n\tprintln(int(u8(u16(256))))\n\tprintln(int(u16(u32(65536))))\n\tprintln(int(i16(i32(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'
wasm := compile_to_wasm(v3_bin, src, 'wasm_narrow')
assert_valid_wasm(wasm)
run_wasi_expect(wasm, ['-128', '0', '0', '-32768', '4', '-128', '44'])
@@ -122,6 +125,330 @@ fn test_wasm_hello_world() {
assert last_line(res.output) == 'hello world', res.output
}
fn test_wasm_implicit_main() {
wasm := compile_to_wasm(v3_binary(), "println('hello script')\n", 'wasm_implicit_main')
assert_valid_wasm(wasm)
run_wasi_expect(wasm, ['hello script'])
}
fn assert_wasm_source_before_and_after_optimization(name string, source string, checks string) {
dir := os.join_path(os.vtmp_dir(), 'wasm_review_${name}_${os.getpid()}')
os.mkdir_all(dir) or { panic(err) }
defer { os.rmdir_all(dir) or {} }
source_path := os.join_path(dir, 'main.v')
runner := os.join_path(dir, 'check.mjs')
os.write_file(source_path, source) or { panic(err) }
os.write_file(runner, "import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const bytes = readFileSync(process.argv[2]);
assert.ok(WebAssembly.validate(bytes));
const { instance } = await WebAssembly.instantiate(bytes, {});
const e = instance.exports;
${checks}
") or { panic(err) }
for production in [false, true] {
output := os.join_path(dir, 'main_${production}.wasm')
mut args := [v3_binary(), '-enable-globals', '-b', 'wasm', '-o', output, source_path]
if production {
args.insert(1, '-prod')
}
result := os.exec(args)
assert result.exit_code == 0, result.output
assert_valid_wasm(output)
if node := node_path() {
execution := run_node(node, runner, output)
assert execution.exit_code == 0, '${name}, production=${production}: ${execution.output}'
}
}
}
fn test_wasm_float_constant_comparisons() {
assert_wasm_source_before_and_after_optimization('float_comparisons', '
pub fn less() bool { return f64(1.25) < f64(1.5) }
pub fn less_equal() bool { return f64(1.25) <= f64(1.5) }
pub fn greater() bool { return f64(1.25) > f64(1.5) }
pub fn greater_equal() bool { return f64(1.25) >= f64(1.5) }
pub fn equal() bool { return f64(1.25) == f64(1.5) }
pub fn unequal() bool { return f64(1.25) != f64(1.5) }
pub fn f32_less() bool { return f32(-1.5) < f32(-1.25) }
pub fn f32_rounded_equal() bool { return f32(16777216) == f32(16777217) }
', '
assert.equal(e.less(), 1);
assert.equal(e.less_equal(), 1);
assert.equal(e.greater(), 0);
assert.equal(e.greater_equal(), 0);
assert.equal(e.equal(), 0);
assert.equal(e.unequal(), 1);
assert.equal(e.f32_less(), 1);
assert.equal(e.f32_rounded_equal(), 1);
')
}
fn test_wasm_assignments_keep_numeric_conversions() {
assert_wasm_source_before_and_after_optimization('numeric_stores', '
pub fn check(n int) bool {
mut x := f64(0)
x = n
return x < 1.5
}
pub fn promoted_float(n f32) bool {
mut x := f64(0)
x = n
return x < 1.5
}
pub fn unsigned_to_float(n u32) f64 {
mut x := f64(0)
x = f64(n)
return x + 0.5
}
pub fn branch_float(n int, flag bool) f64 {
mut x := f64(0)
if flag { x = n } else { x = 3 }
return x + 0.5
}
pub fn parallel_floats(n int) f64 {
mut x := f64(0)
mut y := f64(0)
x, y = n, n + 1
return x + y + 0.5
}
pub fn unsigned_to_signed(n u32) bool {
mut x := i64(0)
x = i64(n)
return x > 2147483647
}
', '
assert.equal(e.check(1), 1);
assert.equal(e.check(2), 0);
assert.equal(e.check(-1), 1);
assert.equal(e.promoted_float(1.25), 1);
assert.equal(e.promoted_float(1.75), 0);
assert.equal(e.unsigned_to_float(4000000000), 4000000000.5);
assert.equal(e.branch_float(1, 1), 1.5);
assert.equal(e.branch_float(1, 0), 3.5);
assert.equal(e.parallel_floats(3), 7.5);
assert.equal(e.unsigned_to_signed(4000000000), 1);
assert.equal(e.unsigned_to_signed(1), 0);
')
}
fn test_wasm_full_width_integer_constant_folding() {
assert_wasm_source_before_and_after_optimization('full_width_folding', '
pub fn check() bool { return 9223372036854775808 == 9223372036854775809 }
pub fn unequal() bool { return 9223372036854775808 != 9223372036854775809 }
pub fn hex_unequal() bool { return 0x8000000000000000 != 0x8000000000000001 }
pub fn unsigned_less() bool { return u64(9223372036854775808) < u64(9223372036854775809) }
pub fn unsigned_difference() u64 { return u64(9223372036854775809) - u64(9223372036854775808) }
pub fn unsigned_maximum() u64 { return 18446744073709551615 }
pub fn minimum_equal(n i64) bool { return n == -9223372036854775808 }
', '
assert.equal(e.check(), 0);
assert.equal(e.unequal(), 1);
assert.equal(e.hex_unequal(), 1);
assert.equal(e.unsigned_less(), 1);
assert.equal(e.unsigned_difference(), 1n);
assert.equal(e.unsigned_maximum(), -1n);
assert.equal(e.minimum_equal(-9223372036854775808n), 1);
assert.equal(e.minimum_equal(9223372036854775807n), 0);
')
}
fn test_wasm_wide_integer_literal_operands() {
assert_wasm_source_before_and_after_optimization('wide_literals', '
pub fn equal(n i64) bool { return n == 4294967296 }
pub fn reversed_equal(n i64) bool { return 4294967296 == n }
pub fn add(n i64) i64 { return n + 4294967296 }
pub fn reversed_add(n i64) i64 { return 4294967296 + n }
pub fn float_add(n f64) f64 { return n + 4294967296 }
pub fn float_equal(n f64) bool { return n == 4294967296 }
pub fn narrow_equal(n int) bool { return n == 4294967296 }
pub fn narrow_less(n int) bool { return n < 4294967296 }
pub fn narrow_negative_equal(n int) bool { return n == -4294967296 }
pub fn narrow_unsigned_equal(n u8) bool { return n == 256 }
pub fn unsigned_wide_equal(n u64) bool { return n == 18446744073709551615 }
', '
assert.equal(e.equal(4294967296n), 1);
assert.equal(e.equal(0n), 0);
assert.equal(e.reversed_equal(4294967296n), 1);
assert.equal(e.reversed_equal(0n), 0);
assert.equal(e.add(1n), 4294967297n);
assert.equal(e.reversed_add(1n), 4294967297n);
assert.equal(e.float_add(1), 4294967297);
assert.equal(e.float_equal(4294967296), 1);
assert.equal(e.float_equal(0), 0);
assert.equal(e.narrow_equal(0), 0);
assert.equal(e.narrow_less(0), 1);
assert.equal(e.narrow_less(2147483647), 1);
assert.equal(e.narrow_negative_equal(0), 0);
assert.equal(e.narrow_unsigned_equal(0), 0);
assert.equal(e.narrow_unsigned_equal(255), 0);
assert.equal(e.unsigned_wide_equal(-1n), 1);
assert.equal(e.unsigned_wide_equal(0n), 0);
')
}
fn test_wasm_indirect_call_numeric_conversions() {
assert_wasm_source_before_and_after_optimization('indirect_numeric', '
fn scale(x f64) f64 { return x * 2.0 }
fn scale32(x f32) f32 { return x * f32(2) }
pub fn integer_argument() f64 {
callback := scale
return callback(3)
}
pub fn f32_argument() f64 {
callback := scale
return callback(f32(3.5))
}
pub fn integer_to_f32_argument() f32 {
callback := scale32
return callback(3)
}
', '
assert.equal(e.integer_argument(), 6);
assert.equal(e.f32_argument(), 7);
assert.equal(e.integer_to_f32_argument(), 6);
')
}
fn test_wasm_unicode_rune_initializers_and_expressions() {
assert_wasm_source_before_and_after_optimization('runes', '
__global letter = `é`
__global ideograph = `界`
__global emoji = `😀`
pub fn global_letter() rune { return letter }
pub fn global_ideograph() rune { return ideograph }
pub fn global_emoji() rune { return emoji }
pub fn direct_letter() rune { return `é` }
pub fn direct_ideograph() rune { return `界` }
pub fn direct_emoji() rune { return `😀` }
pub fn newline() rune { return `\\n` }
', '
assert.equal(e.global_letter(), 233);
assert.equal(e.global_ideograph(), 30028);
assert.equal(e.global_emoji(), 128512);
assert.equal(e.direct_letter(), 233);
assert.equal(e.direct_ideograph(), 30028);
assert.equal(e.direct_emoji(), 128512);
assert.equal(e.newline(), 10);
')
}
fn test_wasm_user_functions_keep_runtime_stub_names() {
assert_wasm_source_before_and_after_optimization('user_runtime_names', '
fn current_rss_kb() i64 { return 123 }
fn macos_rss_kb() i64 { return 456 }
fn linux_rss_kb() i64 { return 789 }
pub fn sum() i64 { return current_rss_kb() + macos_rss_kb() + linux_rss_kb() }
', '
assert.equal(e.current_rss_kb(), 123n);
assert.equal(e.macos_rss_kb(), 456n);
assert.equal(e.linux_rss_kb(), 789n);
assert.equal(e.sum(), 1368n);
')
}
fn test_wasm_user_functions_keep_other_runtime_names() {
assert_wasm_source_before_and_after_optimization('other_runtime_names', '
import hash
fn wyhash64(a u64, b u64) u64 { return a + b }
fn wyhash(a u64, b u64) u64 { return a * 10 + b }
fn array_new(n int) int { return n + 11 }
fn new_map(n int) int { return n + 13 }
fn all_after_last(n int) int { return n + 17 }
fn prealloc_malloc(n int) int { return n + 19 }
fn join_path(n int) int { return n + 23 }
fn name_list(n int) int { return n + 29 }
pub fn call_hash() u64 { return wyhash64(3, 4) }
pub fn runtime_control() u64 {
return hash.wyhash64_c(3, 4)
}
', '
assert.equal(e.wyhash64(3n, 4n), 7n);
assert.equal(e.wyhash(3n, 4n), 34n);
assert.equal(e.array_new(5), 16);
assert.equal(e.new_map(5), 18);
assert.equal(e.all_after_last(5), 22);
assert.equal(e.prealloc_malloc(5), 24);
assert.equal(e.join_path(5), 28);
assert.equal(e.name_list(5), 34);
assert.equal(e.call_hash(), 7n);
assert.equal(e.runtime_control(), 725283166682890696n);
')
}
fn test_wasm_user_hash_runtime_name_in_main() {
source := os.join_path(os.vtmp_dir(), 'wasm_user_hash_${os.getpid()}.v')
output := os.join_path(os.vtmp_dir(), 'wasm_user_hash_${os.getpid()}.wasm')
defer {
os.rm(source) or {}
os.rm(output) or {}
}
os.write_file(source, 'fn wyhash64(a u64, b u64) u64 { return a + b }
fn main() { println(wyhash64(3, 4)) }
') or { panic(err) }
for production in [false, true] {
mut args := [v3_binary(), '-b', 'wasm', '-o', output, source]
if production {
args.insert(1, '-prod')
}
result := os.exec(args)
assert result.exit_code == 0, result.output
assert_valid_wasm(output)
run_wasi_expect(output, ['7'])
}
}
fn test_wasm_implicit_main_imported_scalar_calls() {
dir := os.join_path(os.vtmp_dir(), 'wasm_script_import_${os.getpid()}')
os.mkdir_all(os.join_path(dir, 'foo')) or { panic(err) }
defer { os.rmdir_all(dir) or {} }
main_v := os.join_path(dir, 'main.v')
os.write_file(main_v, 'import foo as helper
println(helper.answer())
callback := helper.answer
println(callback())
') or { panic(err) }
os.write_file(os.join_path(dir, 'foo', 'foo.v'), 'module foo
fn value() int { return 21 }
pub fn answer() int { return value() * 2 }
') or { panic(err) }
for production in [false, true] {
output := os.join_path(dir, 'main_${production}.wasm')
mut args := [vexe, '-b', 'wasm', '-o', output, main_v]
if production {
args.insert(1, '-prod')
}
result := os.exec(args)
assert result.exit_code == 0, result.output
assert_valid_wasm(output)
run_wasi_expect(output, ['42', '42'])
}
}
fn test_wasm_imported_function_value() {
dir := os.join_path(os.vtmp_dir(), 'wasm_imported_fn_value_${os.getpid()}')
defer { os.rmdir_all(dir) or {} }
os.mkdir_all(os.join_path(dir, 'foo')) or { panic(err) }
main_v := os.join_path(dir, 'main.v')
wasm := os.join_path(dir, 'main.wasm')
os.write_file(main_v, 'module main
import foo as helper
fn main() {
callback := helper.add
println(callback(35))
}
') or { panic(err) }
os.write_file(os.join_path(dir, 'foo', 'foo.v'), 'module foo
pub fn add(value int) int {
return value + 7
}
') or { panic(err) }
result := os.exec([v3_binary(), '-b', 'wasm', '-o', wasm, main_v])
assert result.exit_code == 0, result.output
assert_valid_wasm(wasm)
run_wasi_expect(wasm, ['42'])
}
fn test_wasm_control_flow_and_int_print() {
v3_bin := v3_binary()
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'
@@ -146,6 +473,31 @@ fn test_wasm_lowered_integer_print_formats() {
'4294967295', '18446744073709551615', '4294967295'])
}
fn test_wasm_production_control_flow_and_recursion() {
source := os.join_path(os.vtmp_dir(), 'wasm_ssa_prod_${os.getpid()}.v')
output := os.join_path(os.vtmp_dir(), 'wasm_ssa_prod_${os.getpid()}.wasm')
defer {
os.rm(source) or {}
os.rm(output) or {}
}
os.write_file(source, 'fn fib(n int) int {
if n < 2 { return n }
return fib(n - 1) + fib(n - 2)
}
fn main() {
mut a := 1
mut b := 2
for i := 0; i < 3; i++ { a, b = b, a }
println(a * 10 + b)
println(fib(10))
}
') or { panic(err) }
result := os.exec([v3_binary(), '-prod', '-b', 'wasm', '-o', output, source])
assert result.exit_code == 0, result.output
assert_valid_wasm(output)
run_wasi_expect(output, ['21', '55'])
}
fn test_wasm_exported_functions() {
v3_bin := v3_binary()
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'
@@ -311,7 +663,7 @@ fn test_wasm_module_scoped_globals() {
out_wasm := os.join_path(dir, 'main.wasm')
main_v := os.join_path(dir, 'main.v')
res :=
os.exec([v3_bin, '-b', 'wasm', '-o', '${out_wasm}', '${main_v}'])
os.exec([v3_bin, '-enable-globals', '-b', 'wasm', '-o', '${out_wasm}', '${main_v}'])
assert res.exit_code == 0, res.output
assert_valid_wasm(out_wasm)
@@ -329,12 +681,12 @@ fn test_wasm_module_scoped_globals() {
}
}
fn test_wasm_for_post_uses_loop_var_not_body_shadow() {
fn test_wasm_for_post_uses_loop_var_with_body_local() {
v3_bin := v3_binary()
// A body-local `i` must not rebind the name used by the post `i++`; the
// A body-local value must not replace the counter used by the post `i++`; the
// outer loop counter must still advance. The count/break bound keeps the
// test terminating even if the fix regresses (it would loop otherwise).
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'
src := 'fn main() {\n\tmut i := 0\n\tmut count := 0\n\tfor ; i < 3; i++ {\n\t\tvalue := 10\n\t\t_ = value\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'
wasm := compile_to_wasm(v3_bin, src, 'wasm_loopshadow')
assert_valid_wasm(wasm)
run_wasi_expect(wasm, ['3', '3'])
@@ -363,11 +715,11 @@ fn test_wasm_imported_module_alias_call() {
run_wasi_expect(out_wasm, ['42', '7'])
}
fn test_wasm_shadowing_initializer_reads_outer() {
fn test_wasm_inner_initializer_reads_outer() {
v3_bin := v3_binary()
// The inner `x := x + 1` initializer must read the outer x (5), giving 6,
// The inner `value := x + 1` initializer reads the outer x (5), giving 6,
// and the outer x is unchanged afterwards.
src := 'fn main() {\n\tx := 5\n\t{\n\t\tx := x + 1\n\t\tprintln(x)\n\t}\n\tprintln(x)\n}\n'
src := 'fn main() {\n\tx := 5\n\t{\n\t\tvalue := x + 1\n\t\tprintln(value)\n\t}\n\tprintln(x)\n}\n'
wasm := compile_to_wasm(v3_bin, src, 'wasm_shadow_init')
assert_valid_wasm(wasm)
run_wasi_expect(wasm, ['6', '5'])
@@ -430,8 +782,8 @@ fn test_wasm_import_aliases_are_file_scoped() {
fn test_wasm_numeric_type_aliases() {
v3_bin := v3_binary()
// Scalar aliases must classify as their base type: the alias-typed function
// is emitted, and Byte(300) wraps to u8 (44) rather than keeping 300.
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'
// is emitted, and Byte(u16(300)) wraps to u8 (44) rather than keeping 300.
src := 'type Byte = u8\ntype MyInt = int\n\nfn val() Byte {\n\treturn Byte(u16(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'
wasm := compile_to_wasm(v3_bin, src, 'wasm_alias_types')
assert_valid_wasm(wasm)
run_wasi_expect(wasm, ['44', '7', '4'])
@@ -560,6 +912,21 @@ fn test_wasm_if_expression_value() {
run_wasi_expect(wasm, ['10', '1', '-1', '0'])
}
fn test_wasm_moduleless_numeric_constants_before_and_after_optimization() {
assert_wasm_source_before_and_after_optimization('script_numeric_consts', '
const base = 10
const answer = base * 4 + 2
const big = u64(9223372036854775808)
__global result = answer - 1
fn answer_value() int { return answer }
fn big_value() u64 { return big }
fn result_value() int { return result }
', 'assert.equal(e.answer_value(), 42);
assert.equal(BigInt.asUintN(64, e.big_value()), 9223372036854775808n);
assert.equal(e.result_value(), 41);')
}
fn test_wasm_top_level_const_inlined() {
v3_bin := v3_binary()
// Numeric/bool consts are inlined at the use site instead of warning + 0.
@@ -822,10 +1189,10 @@ fn test_wasm_global_postfix() {
fn test_wasm_global_narrow_cast_initializer() {
v3_bin := v3_binary()
// An out-of-range cast initializer on an inferred narrow global must be
// A narrowing cast initializer on an inferred narrow global must be
// wrapped to the global's width at compile time, so the first read already
// sees the V value (u8(300)=44, i8(128)=-128, u16(70000)=4464).
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'
// sees the V value (u8(u16(300))=44, i8(i16(128))=-128, u16(u32(70000))=4464).
src := '__global b = u8(u16(300))\n__global s = i8(i16(128))\n__global w = u16(u32(70000))\n\nfn main() {\n\tprintln(int(b))\n\tprintln(int(s))\n\tprintln(int(w))\n}\n'
wasm := compile_to_wasm(v3_bin, src, 'wasm_global_narrow_init')
assert_valid_wasm(wasm)
run_wasi_expect(wasm, ['44', '-128', '4464'])
@@ -844,9 +1211,9 @@ fn test_wasm_global_const_expr_initializers() {
fn test_wasm_global_nested_cast_initializer() {
v3_bin := v3_binary()
// A nested cast initializer keeps each cast's width: the inner cast narrows
// before the wider outer cast, so `int(u8(300))` is 44, not 300, and a folded
// before the wider outer cast, so `int(u8(u16(300)))` is 44, not 300, and a folded
// float initializer rounds through an int cast (1.5 + 2.0 -> 3).
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'
src := '__global b = int(u8(u16(300)))\n__global g = int(i8(i16(128)))\n__global h = u8(u16(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'
wasm := compile_to_wasm(v3_bin, src, 'wasm_global_nested_cast')
assert_valid_wasm(wasm)
run_wasi_expect(wasm, ['44', '-128', '144', '3'])
@@ -899,3 +1266,21 @@ const { instance: i } = await WebAssembly.instantiate(readFileSync(process.argv[
const e = i.exports;
process.stdout.write(`\${e.add(3, 4)} \${e.fib(10)} \${e.gcd(48, 36)}`);
"
fn test_wasm_float_unary_minus_preserves_signed_zero() {
assert_wasm_source_before_and_after_optimization('signed_zero_negation', '
pub fn negative_zero64() f64 { return -0.0 }
pub fn negative_zero32() f32 { return -f32(0.0) }
pub fn negate64(value f64) f64 { return -value }
pub fn negate32(value f32) f32 { return -value }
', '
assert.ok(Object.is(e.negative_zero64(), -0));
assert.ok(Object.is(e.negative_zero32(), -0));
for (const negate of [e.negate64, e.negate32]) {
assert.ok(Object.is(negate(0), -0));
assert.ok(Object.is(negate(-0), 0));
assert.equal(negate(17), -17);
assert.equal(negate(-17), 17);
}
')
}
+62 -18
View File
@@ -40,6 +40,9 @@ $if !skip_eval ? {
$if !skip_arm64 ? {
import v.gen.arm64
}
$if !skip_ssa ? {
import v.ssa
import v.ssa.optimize
}
@@ -10284,23 +10287,20 @@ pub fn run(args []string) {
remove_binary_after_run := should_run && !is_crun && !is_direct_vsh && !explicit_output && !keep_c
&& !binary_existed_before
// Decide which backend modules to compile into the output. By default only the C
// backend is built; the fastc/arm64/wasm/eval backends (and the whole SSA pipeline that the
// arm64 backend pulls in: v.ssa + v.ssa.optimize) are skipped entirely. When compiling
// the V compiler itself this avoids parsing/checking/transforming/cgen-ing ~30k lines of
// unused backend code, which measurably speeds up the self-host build. The `skip_*`
// Decide which backend modules to compile into the output. Standalone compilers
// default to C; optional backends and their SSA dependencies can be skipped. This
// avoids parsing/checking/transforming/cgen-ing unused code when self-hosting. The `skip_*`
// defines drive two things in lock-step: `$if !skip_* ?` gates in main() make the parser
// drop the dispatch blocks (so the backend symbols are never referenced), and
// resolve_imports skips parsing the corresponding module directories.
// `-all-backends` keeps everything; `-compile-backend <name>` opts a specific backend back
// in; the active `-b` target backend is always force-included.
// The full `cmd/v` CLI (the executable that `make`, `makev.bat`, `v self`, and `v up`
// produce) keeps FastC when built through C; direct ARM64 builds omit its libtcc
// dependency. `-d skip_fastc` also removes it. Standalone `vlib/v/v.v` builds and
// portable `-cross` C keep pruning it.
// produce) keeps WebAssembly and, through C, FastC. Direct ARM64 builds
// omit FastC's libtcc dependency. Explicit skip defines remove optional backends.
mut include_fastc := all_backends || v3_compiles_fastc_by_default(input_file, output_cross_c, backend)
mut include_arm64 := all_backends
mut include_wasm := all_backends
mut include_wasm := all_backends || (cmd_v_build && !output_cross_c)
mut include_eval := all_backends
for cb in compile_backends {
for name in cb.split(',') {
@@ -10348,6 +10348,9 @@ pub fn run(args []string) {
if !include_wasm {
user_defines << 'skip_wasm'
}
if !include_arm64 && !include_wasm {
user_defines << 'skip_ssa'
}
if !include_eval {
user_defines << 'skip_eval'
}
@@ -13087,18 +13090,53 @@ pub fn run(args []string) {
// Generate only after monomorphization has pruned deferred generic comptime
// branches. output_file is the exact path requested via -o (or the
// <name>.wasm default).
mut g := wasmgen.Gen.new(a, pre_tc, used_fns)
g.gen()
mut metadata := wasmgen.Gen.new(a, pre_tc, used_fns)
config := metadata.ssa_configuration()
mut m := ssa.build_with_options(a, config.used_fns, pre_tc, ssa.BuildOptions{
target: ssa.TargetData{ ptr_size: 4 }
track_uses: is_prod
exact_used_fns: true
source_modules: config.source_modules
source_imports: config.source_imports
})
b.step('ssa build')
if is_prod {
optimize.optimize(mut m)
b.step('optimize')
}
m.release_codegen_analysis_metadata()
mut g := wasmgen.SSAGen.new(m)
mut main_fn := config.main_fn
mut exports := config.exports.clone()
if main_fn.len == 0 {
// SSA synthesizes main for scripts with top-level statements.
for f in m.funcs {
if f.name == 'main' && f.blocks.len > 0 && !f.is_c_extern {
main_fn = f.name
exports[main_fn] = 'main'
break
}
}
}
g.configure(exports, config.init_fns, main_fn)
g.gen() or {
eprintln(err.msg())
exit(1)
}
g.write(output_file) or {
eprintln('error writing ${output_file}')
exit(1)
}
for w in g.warnings_list() {
eprintln('wasm: ${w}')
for warning in g.warnings_list() {
eprintln('wasm: ${warning}')
}
b.step('wasm gen')
b.print_report()
return
} $else {
eprintln('WebAssembly support is not compiled into this executable')
eprintln('Rebuild with `v -compile-backend wasm self` or `-all-backends`.')
exit(1)
}
}
mut newly_cached_module_count := 0
@@ -18248,19 +18286,21 @@ fn real_path_is_in_dir(real_path string, real_dir string) bool {
// skipped_backend_module_groups lists the importable backend module groups that the current
// configuration excludes (driven by the same `skip_*` defines that gate the dispatch in
// main()). The arm64 backend is the only consumer of the SSA pipeline, so it shares a group
// with v.ssa and v.ssa.optimize.
// main()). SSA is shared by ARM64 and WebAssembly and is excluded only when both are disabled.
fn skipped_backend_module_groups(prefs &pref.Preferences) [][]string {
mut skipped := [][]string{}
if 'skip_fastc' in prefs.user_defines {
skipped << ['v.gen.fastc', 'v.fastcdriver']
}
if 'skip_arm64' in prefs.user_defines {
skipped << ['v.gen.arm64', 'v.ssa', 'v.ssa.optimize']
skipped << ['v.gen.arm64']
}
if 'skip_wasm' in prefs.user_defines {
skipped << ['v.gen.wasm']
}
if 'skip_arm64' in prefs.user_defines && 'skip_wasm' in prefs.user_defines {
skipped << ['v.ssa', 'v.ssa.optimize']
}
if 'skip_eval' in prefs.user_defines {
skipped << ['v.eval']
}
@@ -20750,10 +20790,14 @@ fn resolve_imports(mut a flat.FlatAst, mut p parser.Parser, prefs &pref.Preferen
// dispatch in main() is gated out by the matching `$if !skip_* ?`, so nothing
// references their symbols. Pre-seeding parsed_modules makes the loop below treat
// them as already handled, so neither v.v's top-level imports nor any transitive
// import pulls them in. Skipping the arm64 group (v.gen.arm64 + the v.ssa SSA
// pipeline) and the wasm/eval backends avoids ~30k lines of work when self-hosting.
// import pulls them in. The shared SSA pipeline is retained when either consumer is
// explicitly imported, even if neither backend is enabled in the compiler being built.
for skipped_group in skipped_backend_module_groups(prefs) {
mut group_requested := false
if 'v.ssa' in skipped_group
&& ('v.gen.arm64' in explicit_initial_imports || 'v.gen.wasm' in explicit_initial_imports) {
group_requested = true
}
for skipped in skipped_group {
if skipped in explicit_initial_imports {
group_requested = true
@@ -182,3 +182,48 @@ fn test_native_float_formatting_matches_c_rounding_and_shortest_decimal_expansio
}
}
}
fn test_native_float_unary_minus_preserves_zero_sign_bits() {
$if macos && arm64 {
path := os.join_path(os.vtmp_dir(), 'arm64_negative_zero_${os.getpid()}.v')
output := path.all_before_last('.')
defer {
os.rm(path) or {}
os.rm(output) or {}
}
os.write_file(path, 'module main
fn C.exit(int)
union Bits64 { f f64 u u64 }
union Bits32 { f f32 u u32 }
fn literal64() f64 { return -0.0 }
fn literal32() f32 { return -f32(0.0) }
fn negate64(value f64) f64 { return -value }
fn negate32(value f32) f32 { return -value }
fn main() {
literal_64 := Bits64{f: literal64()}
literal_32 := Bits32{f: literal32()}
negated_64 := Bits64{f: negate64(0.0)}
negated_32 := Bits32{f: negate32(f32(0.0))}
positive_64 := Bits64{f: negate64(literal64())}
positive_32 := Bits32{f: negate32(literal32())}
if unsafe { literal_64.u } != u64(0x8000000000000000) { C.exit(1) }
if unsafe { literal_32.u } != u32(0x80000000) { C.exit(2) }
if unsafe { negated_64.u } != u64(0x8000000000000000) { C.exit(3) }
if unsafe { negated_32.u } != u32(0x80000000) { C.exit(4) }
if unsafe { positive_64.u } != u64(0) { C.exit(5) }
if unsafe { positive_32.u } != u32(0) { C.exit(6) }
if negate64(17.0) != -17.0 || negate32(f32(-17.0)) != f32(17.0) { C.exit(7) }
}
')!
for production in [false, true] {
mut args := ['env', 'VFLAGS=', 'V_MACOS_V3_NO_FALLBACK=1', @VEXE, '-new-compiler',
'-no-retry-compilation', '-cc', 'clang', '-gc', 'none', '-nocache', '-b', 'arm64',
'-o', output, path]
if production { args.insert(5, '-prod') }
compiled := os.exec(args)
assert compiled.exit_code == 0, compiled.output
result := os.exec([output])
assert result.exit_code == 0, 'production=${production}, exit ${result.exit_code}: ${result.output}'
}
}
}
+66
View File
@@ -0,0 +1,66 @@
module arm64
import os
import v.parser
import v.pref
import v.ssa
import v.types
fn test_native_uname_uses_inline_arrays_with_pointer_field_values() {
$if !macos || !arm64 {
return
}
path := os.join_path(os.vtmp_dir(), 'arm64_uname_${os.getpid()}.v')
output := path.all_before_last('.')
defer {
os.rm(path) or {}
os.rm(output) or {}
}
os.write_file(path, 'module main
pub struct C.utsname {
sysname &char
nodename &char
release &char
version &char
machine &char
}
fn C.uname(&C.utsname) int
fn C.strlen(&char) usize
fn C.strcmp(&char, &char) int
fn C.exit(int)
struct UnameView { value &C.utsname }
struct PointerField { value &char }
fn copy_uname(value &C.utsname) C.utsname {
return unsafe { *value }
}
fn main() {
if sizeof(C.utsname) != 1280 { C.exit(1) }
mut value := C.utsname{}
if C.uname(&value) != 0 { C.exit(2) }
pointer := &value
if C.strcmp(value.sysname, c"Darwin") != 0 { C.exit(3) }
if C.strlen(pointer.nodename) == 0 { C.exit(4) }
if C.strlen(pointer.release) == 0 { C.exit(5) }
if C.strlen(pointer.version) == 0 { C.exit(6) }
if C.strcmp(pointer.machine, c"arm64") != 0 { C.exit(7) }
view := UnameView{value: pointer}
if C.strcmp(view.value.release, value.release) != 0 { C.exit(8) }
if C.strcmp(copy_uname(pointer).machine, value.machine) != 0 { C.exit(9) }
control := PointerField{value: c"pointer value"}
if C.strcmp(control.value, c"pointer value") != 0 { C.exit(10) }
}
')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
tc.annotate_types()
assert tc.errors.len == 0, tc.errors.str()
m := ssa.build_with_used(a, map[string]bool{}, tc)
mut g := Gen.new(m)
g.gen()
g.write_and_link(output)
result := os.exec([output])
assert result.exit_code == 0, result.output
}
+24 -1
View File
@@ -2,7 +2,7 @@ module wasm
// encode.v is the self-contained WebAssembly binary encoder used by the v3
// wasm backend. It mirrors the role of the arm64 backend's asm.v + macho.v:
// gen.v walks the flat AST and drives this module, which knows nothing about V
// ssa_gen.v lowers SSA and drives this module, which knows nothing about V
// and only emits raw WASM bytecode (LEB128 + sections). No external module is
// imported, so v3 stays self-contained and self-hostable.
@@ -67,6 +67,7 @@ mut:
mem_min int = 2
n_import int
start int = -1
table []int
}
pub fn Module.new() &Module {
@@ -147,6 +148,11 @@ pub fn (mut m Module) set_start(index int) {
m.start = index
}
// set_table installs the function references used by indirect calls.
pub fn (mut m Module) set_table(functions []int) {
m.table = functions.clone()
}
// ---- LEB128 ----
fn leb_u(mut out []u8, val_ u64) {
@@ -256,6 +262,13 @@ pub fn (m &Module) compile() []u8 {
leb_u(mut fsec, u64(f.type_idx))
}
section(mut out, 0x03, fsec)
if m.table.len > 0 {
mut tabsec := []u8{}
leb_u(mut tabsec, 1)
tabsec << [u8(0x70), 0x00]
leb_u(mut tabsec, u64(m.table.len + 1))
section(mut out, 0x04, tabsec)
}
// memory section (5)
mut msec := []u8{}
@@ -292,6 +305,16 @@ pub fn (m &Module) compile() []u8 {
leb_u(mut ssec, u64(m.start))
section(mut out, 0x08, ssec)
}
if m.table.len > 0 {
mut elemsec := []u8{}
leb_u(mut elemsec, 1)
elemsec << [u8(0x00), 0x41, 0x01, 0x0b]
leb_u(mut elemsec, u64(m.table.len))
for function in m.table {
leb_u(mut elemsec, u64(function))
}
section(mut out, 0x09, elemsec)
}
// code section (10)
mut csec := []u8{}
+119 -11
View File
@@ -4,11 +4,9 @@ import os
import v.flat
import v.types
// gen.v is the v3 WebAssembly backend. Like the C backend's FlatGen it walks
// the flat AST directly (WASM has structured control flow, so no SSA/relooping
// is needed), and like the arm64 backend it emits a binary module. Generics,
// strings-as-values, structs, arrays and maps are out of scope for now; the
// backend targets the integer/float core plus WASI `print`/`println`.
// gen.v retains the WebAssembly backend's source metadata and WASI helpers.
// The compiler uses ssa_gen.v for instruction emission from the shared SSA IR.
// Source metadata preserves export names and module initialization order.
// Fixed low-memory scratch layout (bytes):
// [0..3] nwritten result for fd_write
@@ -110,6 +108,81 @@ pub fn Gen.new(a &flat.FlatAst, tc &types.TypeChecker, used_fns map[string]bool)
}
}
// SSAConfiguration carries source-level entry points and export names into SSA code generation.
pub struct SSAConfiguration {
pub:
exports map[string]string
init_fns []string
main_fn string
used_fns map[string]bool
source_modules map[string]string
source_imports map[string]map[string]string
}
// ssa_configuration collects export and initialization metadata without emitting instructions.
pub fn (mut g Gen) ssa_configuration() SSAConfiguration {
g.collect_imports()
fns := g.collect_user_fns()
mut exports := map[string]string{}
// SSA supplies its own runtime helpers. AST reachability includes C runtime
// implementations that are not dependencies of the selected WASM functions.
mut used := map[string]bool{}
mut module_init := map[string]string{}
mut main_fn := ''
mut source_modules := map[string]string{}
mut source_imports := g.file_aliases.clone()
mut file := ''
for node in g.a.nodes {
if node.kind == .file {
file = node.value
// Flat ASTs include a trailing file node after each file's declarations.
// Preserve the module recorded between the opening and closing nodes.
if file !in source_modules {
source_modules[file] = ''
}
} else if node.kind == .module_decl && node.value !in ['', 'main', 'builtin'] {
source_modules[file] = g.module_path_for_file(file)
} else if node.kind == .import_decl && node.value.len > 0 {
alias := if node.typ.len > 0 { node.typ } else { node.value.all_after_last('.') }
source_imports[file][alias] = node.value
}
}
for f in fns {
key := qualified_fn_key(f.module, f.name)
exports[key] = g.explicit_export_fn_name(f) or { export_fn_name(f.module, f.name) }
used[key] = true
if f.name == 'main' && f.module in ['', 'main'] {
main_fn = key
} else if f.name == 'init' && f.params.len == 0 {
module_init[f.module] = key
}
}
mut init_fns := []string{}
mut visited := map[string]bool{}
g.ssa_init_order('', module_init, mut visited, mut init_fns)
return SSAConfiguration{
exports: exports
init_fns: init_fns
main_fn: main_fn
used_fns: used
source_modules: source_modules
source_imports: source_imports
}
}
fn (g &Gen) ssa_init_order(mod string, module_init map[string]string, mut visited map[string]bool, mut order []string) {
if visited[mod] {
return
}
visited[mod] = true
for dep in g.module_imports[mod] {
g.ssa_init_order(dep, module_init, mut visited, mut order)
}
if key := module_init[mod] {
order << key
}
}
// gen builds the whole module from the flat AST.
pub fn (mut g Gen) gen() {
g.collect_imports()
@@ -283,6 +356,33 @@ fn (mut g Gen) collect_user_fns() []FnInfo {
work << key
}
}
// SSA synthesizes main from executable file children. Treat its statements
// as roots too, using the file's imports to resolve calls and fn references.
mut script_keys := []string{}
for file_idx, file_node in g.a.nodes {
if file_idx < g.a.user_code_start || file_node.kind != .file
|| file_node.children_count == 0 {
continue
}
for i in 0 .. file_node.children_count {
child_id := g.a.child(&file_node, i)
if int(child_id) < g.a.user_code_start {
continue
}
child := g.a.nodes[int(child_id)]
// Keep the statement kinds aligned with SSA's top_level_stmt_ids.
if child.kind in [.expr_stmt, .assign, .decl_assign, .selector_assign, .index_assign,
.for_stmt, .for_in_stmt, .if_expr, .assert_stmt, .defer_stmt, .block] {
g.collect_call_keys(child_id, '', file_node.value, candidates, mut script_keys)
}
}
}
for key in script_keys {
if key !in reached {
reached[key] = true
work << key
}
}
// `init` functions are entry points (run before main), like the C path's
// _vinit. Every imported module runs its init regardless of whether any of
// its other functions are called, so seed the init of main and of every
@@ -319,16 +419,24 @@ fn (mut g Gen) collect_user_fns() []FnInfo {
return out
}
// called_candidate_keys returns the qualified keys of the candidate functions
// directly called inside f's body (calls to non-candidates such as intercepted
// builtins are ignored).
// called_candidate_keys returns the qualified keys of candidate functions called
// or referenced inside f's body. Runtime functions outside candidates are ignored.
fn (g &Gen) called_candidate_keys(f FnInfo, candidates map[string]FnInfo) []string {
mut keys := []string{}
g.collect_call_keys(g.a.nodes[int(f.node_id)], f.module, f.file, candidates, mut keys)
g.collect_call_keys(f.node_id, f.module, f.file, candidates, mut keys)
return keys
}
fn (g &Gen) collect_call_keys(node flat.Node, cur_mod string, cur_file string, candidates map[string]FnInfo, mut keys []string) {
fn (g &Gen) collect_call_keys(id flat.NodeId, cur_mod string, cur_file string, candidates map[string]FnInfo, mut keys []string) {
node := g.a.nodes[int(id)]
if name := g.tc.resolved_fn_value_name(id) {
for key in [name, name.replace('__', '.'), qualified_fn_key(cur_mod, name)] {
if key in candidates {
keys << key
break
}
}
}
if node.kind == .call && node.children_count > 0 {
for key in g.resolve_call_keys(g.a.child_node(&node, 0), cur_mod, cur_file) {
if key in candidates {
@@ -340,7 +448,7 @@ fn (g &Gen) collect_call_keys(node flat.Node, cur_mod string, cur_file string, c
for i in 0 .. node.children_count {
cid := g.a.child(&node, i)
if int(cid) >= 0 {
g.collect_call_keys(g.a.nodes[int(cid)], cur_mod, cur_file, candidates, mut keys)
g.collect_call_keys(cid, cur_mod, cur_file, candidates, mut keys)
}
}
}
+46
View File
@@ -0,0 +1,46 @@
module wasm
import os
import v.parser
import v.pref
import v.types
fn test_ssa_configuration_keeps_implicit_main_dependencies() {
dir := os.join_path(os.vtmp_dir(), 'wasm_script_reachability_${os.getpid()}')
os.mkdir_all(os.join_path(dir, 'foo')) or { panic(err) }
defer { os.rmdir_all(dir) or {} }
main_source := os.join_path(dir, 'main.v')
foo_source := os.join_path(dir, 'foo', 'foo.v')
os.write_file(main_source, '
import foo as helper
println(helper.answer())
callback := helper.callback_value
println(callback())
') or { panic(err) }
os.write_file(foo_source, '
module foo
fn value() int { return 21 }
pub fn answer() int { return value() * 2 }
pub fn callback_value() int { return value() * 3 }
fn ignored() int { return 0 }
fn prepare() {}
fn init() { prepare() }
') or { panic(err) }
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_files([foo_source, main_source])
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
_ = tc.check_semantics_opt(false)
assert tc.errors.len == 0, tc.errors.str()
tc.annotate_types()
mut metadata := Gen.new(a, &tc, map[string]bool{})
config := metadata.ssa_configuration()
assert config.main_fn == ''
assert config.used_fns['foo.answer'], config.used_fns.str()
assert config.used_fns['foo.callback_value'], config.used_fns.str()
assert config.used_fns['foo.value'], config.used_fns.str()
assert config.used_fns['foo.prepare'], config.used_fns.str()
assert !config.used_fns['foo.ignored'], config.used_fns.str()
assert config.init_fns == ['foo.init'], config.init_fns.str()
}
File diff suppressed because it is too large. Load diff
+664
View File
@@ -0,0 +1,664 @@
module wasm
import os
import v.parser
import v.pref
import v.ssa
import v.ssa.optimize
import v.types
fn ssa_wasm_test_dir(name string) string {
dir := os.join_path(os.vtmp_dir(), 'ssa_wasm_${name}_${os.getpid()}')
os.mkdir_all(dir) or { panic(err) }
return dir
}
fn assert_ssa_wasm_execution(path string, checks string) {
bytes := os.read_bytes(path) or { panic(err) }
assert bytes.len > 8
assert bytes[..8] == [u8(0), 97, 115, 109, 1, 0, 0, 0]
node := os.find_abs_path_of_executable('node') or {
eprintln('> node unavailable; skipping SSA WebAssembly execution')
return
}
runner := os.join_path(os.dir(path), 'check.mjs')
os.write_file(runner, "import assert from 'node:assert/strict';\nimport { readFileSync } from 'node:fs';\nconst bytes = readFileSync(process.argv[2]);\nassert.ok(WebAssembly.validate(bytes));\nconst { instance } = await WebAssembly.instantiate(bytes, {});\nconst e = instance.exports;\nassert.ok(e.memory instanceof WebAssembly.Memory);\n${checks}\n") or {
panic(err)
}
result := os.exec([node, runner, path])
assert result.exit_code == 0, result.output
}
fn test_ssa_wasm_numeric_source_before_and_after_optimization() {
dir := ssa_wasm_test_dir('numeric')
defer { os.rmdir_all(dir) or {} }
source := os.join_path(dir, 'numeric.v')
os.write_file(source, '
module main
pub fn sum(n int) int {
mut total := 0
for i := 0; i < n; i++ {
if i % 2 == 0 {
total += i
} else {
total -= i
}
}
return total
}
pub fn fib(n int) int {
if n < 2 {
return n
}
return fib(n - 1) + fib(n - 2)
}
pub fn signed(a i64, b i64) i64 {
return a / b + a % b
}
pub fn unsigned(a u64, b u64) u64 {
return a / b
}
pub fn unsigned_less(a u64, b u64) bool {
return a < b
}
pub fn f32_math(a f32, b f32) f32 {
return a * b + b
}
pub fn f64_math(a f64, b f64) f64 {
return a / b - b
}
pub fn signed_narrow(n int) int {
return int(i8(n))
}
pub fn unsigned_narrow(n int) int {
return int(u8(n))
}
pub fn write(value int) int {
return value + 7
}
pub fn call_write(value int) int {
return write(value)
}
pub fn narrow_logical_shift_assign() int {
mut x := i8(-5)
x >>>= 1
mut y := i16(-5)
y >>>= 1
return int(x) + int(y)
}
pub fn labeled_loops() int {
mut cb := 0
outer: for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
cb++
break outer
}
}
mut cc := 0
again: for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
cc++
continue again
}
}
mut cn := 0
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
cn++
break
}
}
return cb * 100 + cc * 10 + cn
}
') or { panic(err) }
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(source)
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
_ = tc.check_semantics_opt(false)
assert tc.errors.len == 0, tc.errors.str()
tc.annotate_types()
exports := {
'sum': 'sum'
'fib': 'fib'
'signed': 'signed'
'unsigned': 'unsigned'
'unsigned_less': 'unsigned_less'
'f32_math': 'f32_math'
'f64_math': 'f64_math'
'signed_narrow': 'signed_narrow'
'unsigned_narrow': 'unsigned_narrow'
'write': 'write'
'call_write': 'call_write'
'narrow_logical_shift_assign': 'narrow_logical_shift_assign'
'labeled_loops': 'labeled_loops'
}
checks := '
assert.equal(e.sum(6), -3);
assert.equal(e.sum(7), 3);
assert.equal(e.fib(10), 55);
assert.equal(e.signed(-11n, 3n), -5n);
assert.equal(e.unsigned(-1n, 3n), 6148914691236517205n);
assert.equal(e.unsigned_less(-1n, 1n), 0);
assert.equal(e.unsigned_less(1n, -1n), 1);
assert.equal(e.f32_math(1.5, 2), 5);
assert.equal(e.f64_math(12.5, 2), 4.25);
assert.equal(e.signed_narrow(128), -128);
assert.equal(e.signed_narrow(255), -1);
assert.equal(e.unsigned_narrow(256), 0);
assert.equal(e.unsigned_narrow(-1), 255);
assert.equal(e.write(3), 10);
assert.equal(e.call_write(4), 11);
assert.equal(e.narrow_logical_shift_assign(), 32890);
assert.equal(e.labeled_loops(), 133);
'
for production in [false, true] {
mut m := ssa.build_with_options(a, map[string]bool{}, &tc, ssa.BuildOptions{
target: ssa.TargetData{ ptr_size: 4 }
})
if production {
optimize.optimize(mut m)
}
mut g := SSAGen.new(m)
g.configure(exports, []string{}, '')
g.gen() or { panic(err) }
path := os.join_path(dir, 'numeric_${production}.wasm')
g.write(path) or { panic(err) }
assert_ssa_wasm_execution(path, checks)
}
}
fn ssa_wasm_swap_loop() &ssa.Module {
mut m := ssa.Module.new()
m.target = ssa.TargetData{ ptr_size: 4 }
i32_type := m.type_store.get_int(32)
bool_type := m.type_store.get_int(1)
func := m.new_function('swap_loop', i32_type)
n := m.add_value(.argument, i32_type, 'n', 0)
m.func_add_param(func, n)
entry := m.add_block(func, 'entry')
header := m.add_block(func, 'header')
body := m.add_block(func, 'body')
done := m.add_block(func, 'done')
zero := m.get_or_add_const(i32_type, '0')
one := m.get_or_add_const(i32_type, '1')
two := m.get_or_add_const(i32_type, '2')
ten := m.get_or_add_const(i32_type, '10')
m.add_instr(.jmp, entry, 0, [ssa.ValueID(header)])
a := m.add_instr(.phi, header, i32_type, [])
b := m.add_instr(.phi, header, i32_type, [])
i := m.add_instr(.phi, header, i32_type, [])
condition := m.add_instr(.lt, header, bool_type, [i, n])
m.add_instr(.br, header, 0, [condition, ssa.ValueID(body), ssa.ValueID(done)])
next_i := m.add_instr(.add, body, i32_type, [i, one])
m.add_instr(.jmp, body, 0, [ssa.ValueID(header)])
m.append_phi_operands(a, one, entry)
m.append_phi_operands(a, b, body)
m.append_phi_operands(b, two, entry)
m.append_phi_operands(b, a, body)
m.append_phi_operands(i, zero, entry)
m.append_phi_operands(i, next_i, body)
left := m.add_instr(.mul, done, i32_type, [a, ten])
result := m.add_instr(.add, done, i32_type, [left, b])
m.add_instr(.ret, done, 0, [result])
// Block order differs from control-flow order; the backedge swaps both phis.
mut function := m.funcs[func]
function.blocks = [entry, done, body, header]
m.funcs[func] = function
return m
}
fn test_ssa_wasm_loop_phi_parallel_copies() {
dir := ssa_wasm_test_dir('phi')
defer { os.rmdir_all(dir) or {} }
for production in [false, true] {
mut m := ssa_wasm_swap_loop()
if production {
optimize.optimize(mut m)
mut has_assign := false
for block in m.blocks {
for id in block.instrs {
if m.values[id].kind == .instruction {
instruction := m.instrs[m.values[id].index]
assert instruction.op != .phi
has_assign = has_assign || instruction.op == .assign
}
}
}
assert has_assign
}
mut g := SSAGen.new(m)
g.gen() or { panic(err) }
path := os.join_path(dir, 'phi_${production}.wasm')
g.write(path) or { panic(err) }
assert_ssa_wasm_execution(path, '
assert.equal(e.swap_loop(0), 12);
assert.equal(e.swap_loop(1), 21);
assert.equal(e.swap_loop(2), 12);
assert.equal(e.swap_loop(3), 21);
assert.equal(e.swap_loop(100), 12);
')
}
}
fn test_ssa_wasm_reports_unsupported_instruction() {
mut m := ssa.Module.new()
m.target = ssa.TargetData{ ptr_size: 4 }
func := m.new_function('unsupported', 0)
entry := m.add_block(func, 'entry')
m.add_instr(.go_call, entry, 0, [])
m.add_instr(.ret, entry, 0, [])
mut g := SSAGen.new(m)
g.gen() or {
assert err.msg().contains('unsupported'), err.msg()
assert err.msg().contains('go_call'), err.msg()
return
}
assert false, 'unsupported SSA instructions must fail WebAssembly generation'
}
fn ssa_wasm_aggregate_swap_loop() &ssa.Module {
mut m := ssa.Module.new()
m.target = ssa.TargetData{ ptr_size: 4 }
i32_type := m.type_store.get_int(32)
bool_type := m.type_store.get_int(1)
pair_type := m.type_store.register(ssa.Type{
kind: .struct_t
fields: [i32_type, i32_type]
field_names: ['first', 'second']
})
ptr_pair := m.type_store.get_ptr(pair_type)
ptr_i32 := m.type_store.get_ptr(i32_type)
func := m.new_function('aggregate_swap', i32_type)
n := m.add_value(.argument, i32_type, 'n', 0)
m.func_add_param(func, n)
entry := m.add_block(func, 'entry')
header := m.add_block(func, 'header')
body := m.add_block(func, 'body')
done := m.add_block(func, 'done')
zero := m.get_or_add_const(i32_type, '0')
one := m.get_or_add_const(i32_type, '1')
four := m.get_or_add_const(i32_type, '4')
hundred := m.get_or_add_const(i32_type, '100')
mut pairs := []ssa.ValueID{}
for scalar in [1, 2] {
slot := m.add_instr(.alloca, entry, ptr_pair, [])
first := m.add_instr(.get_element_ptr, entry, ptr_i32, [slot, zero])
second := m.add_instr(.get_element_ptr, entry, ptr_i32, [slot, four])
m.add_instr(.store, entry, 0, [m.get_or_add_const(i32_type, scalar.str()), first])
m.add_instr(.store, entry, 0, [
m.get_or_add_const(i32_type, (scalar * 10).str()),
second,
])
pairs << m.add_instr(.load, entry, pair_type, [slot])
}
m.add_instr(.jmp, entry, 0, [ssa.ValueID(header)])
a := m.add_instr(.phi, header, pair_type, [])
b := m.add_instr(.phi, header, pair_type, [])
i := m.add_instr(.phi, header, i32_type, [])
condition := m.add_instr(.lt, header, bool_type, [i, n])
m.add_instr(.br, header, 0, [condition, ssa.ValueID(body), ssa.ValueID(done)])
next_i := m.add_instr(.add, body, i32_type, [i, one])
m.add_instr(.jmp, body, 0, [ssa.ValueID(header)])
m.append_phi_operands(a, pairs[0], entry)
m.append_phi_operands(a, b, body)
m.append_phi_operands(b, pairs[1], entry)
m.append_phi_operands(b, a, body)
m.append_phi_operands(i, zero, entry)
m.append_phi_operands(i, next_i, body)
slot := m.add_instr(.alloca, done, ptr_pair, [])
m.add_instr(.store, done, 0, [a, slot])
first_ptr := m.add_instr(.get_element_ptr, done, ptr_i32, [slot, zero])
second_ptr := m.add_instr(.get_element_ptr, done, ptr_i32, [slot, four])
first := m.add_instr(.load, done, i32_type, [first_ptr])
second := m.add_instr(.load, done, i32_type, [second_ptr])
left := m.add_instr(.mul, done, i32_type, [first, hundred])
result := m.add_instr(.add, done, i32_type, [left, second])
m.add_instr(.ret, done, 0, [result])
return m
}
fn test_ssa_wasm_aggregate_phi_parallel_copies() {
dir := ssa_wasm_test_dir('aggregate_phi')
defer { os.rmdir_all(dir) or {} }
for production in [false, true] {
mut m := ssa_wasm_aggregate_swap_loop()
if production {
optimize.optimize(mut m)
}
mut g := SSAGen.new(m)
g.gen() or { panic(err) }
path := os.join_path(dir, 'aggregate_phi_${production}.wasm')
g.write(path) or { panic(err) }
assert_ssa_wasm_execution(path, '
assert.equal(e.aggregate_swap(0), 110);
assert.equal(e.aggregate_swap(1), 220);
assert.equal(e.aggregate_swap(2), 110);
assert.equal(e.aggregate_swap(3), 220);
assert.equal(e.aggregate_swap(100), 110);
')
}
}
fn test_ssa_wasm_function_references_and_indirect_calls() {
dir := ssa_wasm_test_dir('indirect')
defer { os.rmdir_all(dir) or {} }
for production in [false, true] {
mut m := ssa.Module.new()
m.target = ssa.TargetData{ ptr_size: 4 }
i32_type := m.type_store.get_int(32)
i64_type := m.type_store.get_int(64)
bool_type := m.type_store.get_int(1)
add := m.new_function('add', i32_type)
a := m.add_value(.argument, i32_type, 'a', 0)
b := m.add_value(.argument, i32_type, 'b', 1)
m.func_add_param(add, a)
m.func_add_param(add, b)
add_entry := m.add_block(add, 'entry')
result := m.add_instr(.add, add_entry, i32_type, [a, b])
m.add_instr(.ret, add_entry, 0, [result])
// The shared builder represents stored function references as i64 values.
ref := m.add_value(.func_ref, i64_type, 'add', add)
caller := m.new_function('indirect_add', i32_type)
x := m.add_value(.argument, i32_type, 'x', 0)
m.func_add_param(caller, x)
entry := m.add_block(caller, 'entry')
ptr_ref := m.type_store.get_ptr(i64_type)
slot := m.add_instr(.alloca, entry, ptr_ref, [])
m.add_instr(.store, entry, 0, [ref, slot])
loaded := m.add_instr(.load, entry, i64_type, [slot])
three := m.get_or_add_const(i32_type, '3')
called := m.add_instr(.call_indirect, entry, i32_type, [loaded, x, three])
m.add_instr(.ret, entry, 0, [called])
check_ref := m.new_function('reference_is_nonzero', bool_type)
check_entry := m.add_block(check_ref, 'entry')
zero := m.get_or_add_const(i64_type, '0')
nonzero := m.add_instr(.ne, check_entry, bool_type, [ref, zero])
m.add_instr(.ret, check_entry, 0, [nonzero])
null_call := m.new_function('null_indirect_call', i32_type)
null_entry := m.add_block(null_call, 'entry')
null_result := m.add_instr(.call_indirect, null_entry, i32_type, [zero, three, three])
m.add_instr(.ret, null_entry, 0, [null_result])
if production {
optimize.optimize(mut m)
}
mut g := SSAGen.new(m)
g.configure({
'indirect_add': 'indirect_add'
'reference_is_nonzero': 'reference_is_nonzero'
'null_indirect_call': 'null_indirect_call'
}, []string{}, '')
g.gen() or { panic(err) }
path := os.join_path(dir, 'indirect_${production}.wasm')
g.write(path) or { panic(err) }
assert_ssa_wasm_execution(path, '
assert.equal(e.indirect_add(7), 10);
assert.equal(e.indirect_add(-4), -1);
assert.equal(e.reference_is_nonzero(), 1);
assert.throws(() => e.null_indirect_call(), WebAssembly.RuntimeError);
')
}
}
fn test_ssa_wasm_internal_shadowing_scope_lowering() {
dir := ssa_wasm_test_dir('shadowing')
defer { os.rmdir_all(dir) or {} }
source := os.join_path(dir, 'shadowing.v')
os.write_file(source, '
fn scope() int {
i := 10
for i := 0; i < 1; i++ {}
x := 1
mut total := 0
for j := 0; j < 2; j++ {
x := j + 2
total = total * 10 + x
}
return i * 1000 + total * 10 + x
}
fn post_scope() int {
mut i := 0
mut count := 0
for ; i < 3; i++ {
i := 10
_ = i
count++
if count > 100 {
break
}
}
return i * 1000 + count
}
fn initializer_scope() int {
x := 5
mut total := 0
{
x := x + 1
total = x
}
return total * 10 + x
}
') or { panic(err) }
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(source)
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
// Source checking rejects shadowing; retain these internal lowering regressions
// by passing the parsed, annotated tree directly to the shared SSA builder.
tc.annotate_types()
for production in [false, true] {
mut m := ssa.build_with_options(a, map[string]bool{}, &tc, ssa.BuildOptions{
target: ssa.TargetData{ ptr_size: 4 }
})
if production {
optimize.optimize(mut m)
}
mut g := SSAGen.new(m)
g.configure({
'scope': 'scope'
'post_scope': 'post_scope'
'initializer_scope': 'initializer_scope'
}, []string{}, '')
g.gen() or { panic(err) }
path := os.join_path(dir, 'shadowing_${production}.wasm')
g.write(path) or { panic(err) }
assert_ssa_wasm_execution(path, '
assert.equal(e.scope(), 10231);
assert.equal(e.post_scope(), 3003);
assert.equal(e.initializer_scope(), 65);
')
}
}
fn test_ssa_wasm_module_scoped_globals_and_void_calls() {
dir := ssa_wasm_test_dir('module_globals')
defer { os.rmdir_all(dir) or {} }
os.mkdir_all(os.join_path(dir, 'foo')) or { panic(err) }
main_source := os.join_path(dir, 'main.v')
foo_source := os.join_path(dir, 'foo', 'foo.v')
os.write_file(main_source, '
module main
import foo
__global counter int
pub fn counters() int {
counter = 100
foo.bump()
foo.bump()
foo.bump()
return counter * 10 + foo.get()
}
') or { panic(err) }
os.write_file(foo_source, '
module foo
__global counter int
pub fn bump() {
counter++
}
pub fn get() int {
return counter
}
') or { panic(err) }
mut prefs := pref.new_preferences()
prefs.enable_globals = true
mut p := parser.Parser.new(prefs)
a := p.parse_files([foo_source, main_source])
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.enable_globals = true
tc.collect(a)
_ = tc.check_semantics_opt(false)
assert tc.errors.len == 0, tc.errors.str()
tc.annotate_types()
mut metadata := Gen.new(a, &tc, map[string]bool{})
config := metadata.ssa_configuration()
assert metadata.import_paths == ['foo'], metadata.import_paths.str()
assert config.used_fns['foo.bump'], config.used_fns.str()
assert config.used_fns['foo.get'], config.used_fns.str()
assert config.source_modules[foo_source] == 'foo', config.source_modules.str()
assert config.source_imports[main_source]['foo'] == 'foo', config.source_imports.str()
for production in [false, true] {
mut m := ssa.build_with_options(a, config.used_fns, &tc, ssa.BuildOptions{
target: ssa.TargetData{ ptr_size: 4 }
exact_used_fns: true
source_modules: config.source_modules
source_imports: config.source_imports
})
if production {
optimize.optimize(mut m)
}
mut g := SSAGen.new(m)
g.configure(config.exports, config.init_fns, config.main_fn)
g.gen() or { panic(err) }
path := os.join_path(dir, 'globals_${production}.wasm')
g.write(path) or { panic(err) }
assert_ssa_wasm_execution(path, 'assert.equal(e.counters(), 1003);')
}
}
fn test_ssa_wasm_rejects_intrinsic_function_references() {
mut m := ssa.Module.new()
m.target = ssa.TargetData{ ptr_size: 4 }
i64_type := m.type_store.get_int(64)
ptr_type := m.type_store.get_ptr(m.type_store.get_int(8))
external := m.new_function('C.malloc', ptr_type)
m.funcs[external].is_c_extern = true
func := m.new_function('intrinsic_reference', i64_type)
entry := m.add_block(func, 'entry')
ref := m.add_value(.func_ref, i64_type, 'C.malloc', external)
m.add_instr(.ret, entry, 0, [ref])
mut g := SSAGen.new(m)
g.gen() or {
assert err.msg().contains('unknown function reference'), err.msg()
assert err.msg().contains('C.malloc'), err.msg()
return
}
assert false, 'external intrinsic references require an explicit WebAssembly implementation'
}
fn test_ssa_wasm_pointer_integer_casts_preserve_wasm32_bits() {
dir := ssa_wasm_test_dir('pointer_casts')
defer { os.rmdir_all(dir) or {} }
for production in [false, true] {
mut m := ssa.Module.new()
m.target = ssa.TargetData{ ptr_size: 4 }
u64_type := m.type_store.get_uint(64)
ptr_type := m.type_store.get_ptr(m.type_store.get_int(8))
func := m.new_function('pointer_number', u64_type)
ptr := m.add_value(.argument, ptr_type, 'ptr', 0)
m.func_add_param(func, ptr)
entry := m.add_block(func, 'entry')
number := m.add_instr(.bitcast, entry, u64_type, [ptr])
m.add_instr(.ret, entry, 0, [number])
round_trip := m.new_function('pointer_round_trip', u64_type)
input := m.add_value(.argument, u64_type, 'input', 0)
m.func_add_param(round_trip, input)
round_entry := m.add_block(round_trip, 'entry')
narrowed := m.add_instr(.bitcast, round_entry, ptr_type, [input])
widened := m.add_instr(.bitcast, round_entry, u64_type, [narrowed])
m.add_instr(.ret, round_entry, 0, [widened])
if production {
optimize.optimize(mut m)
}
mut g := SSAGen.new(m)
g.gen() or { panic(err) }
path := os.join_path(dir, 'pointer_${production}.wasm')
g.write(path) or { panic(err) }
assert_ssa_wasm_execution(path, '
assert.equal(e.pointer_number(0x80000000), 2147483648n);
assert.equal(e.pointer_number(0xffffffff), 4294967295n);
assert.equal(e.pointer_round_trip(-1n), 4294967295n);
assert.equal(e.pointer_round_trip(4294967296n), 0n);
')
}
}
fn test_ssa_wasm_imported_function_value_keeps_its_definition() {
dir := ssa_wasm_test_dir('imported_fn_ref')
defer { os.rmdir_all(dir) or {} }
os.mkdir_all(os.join_path(dir, 'foo')) or { panic(err) }
main_source := os.join_path(dir, 'main.v')
foo_source := os.join_path(dir, 'foo', 'foo.v')
os.write_file(main_source, '
module main
import foo as helper
pub fn invoke(value int) int {
callback := helper.add
return callback(value)
}
') or { panic(err) }
os.write_file(foo_source, '
module foo
pub fn add(value int) int {
return value + 7
}
') or { panic(err) }
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_files([foo_source, main_source])
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
_ = tc.check_semantics_opt(false)
assert tc.errors.len == 0, tc.errors.str()
tc.annotate_types()
mut metadata := Gen.new(a, &tc, map[string]bool{})
config := metadata.ssa_configuration()
assert config.used_fns['foo.add'], config.used_fns.str()
for production in [false, true] {
mut m := ssa.build_with_options(a, config.used_fns, &tc, ssa.BuildOptions{
target: ssa.TargetData{ ptr_size: 4 }
exact_used_fns: true
source_modules: config.source_modules
source_imports: config.source_imports
})
if production {
optimize.optimize(mut m)
}
mut g := SSAGen.new(m)
g.configure(config.exports, config.init_fns, config.main_fn)
g.gen() or { panic(err) }
path := os.join_path(dir, 'imported_fn_ref_${production}.wasm')
g.write(path) or { panic(err) }
assert_ssa_wasm_execution(path, '
assert.equal(e.invoke(3), 10);
assert.equal(e.invoke(-7), 0);
')
}
}
+5
View File
@@ -1,5 +1,10 @@
This command compiles the given target, along with their dependencies,
into an executable using the WebAssembly code generation backend.
The backend emits a binary from V's SSA intermediate representation. `-prod`
optimizes SSA before code generation. See doc/wasm.md for supported features.
The standard compiler includes this backend. A standalone compiler build needs
`-compile-backend wasm` or `-all-backends`.
Usage:
v -b wasm [-options] ['run'] <target.v|target_directory> [run options]
+1
View File
@@ -15,3 +15,4 @@ tccbin_automation.md
translated_fixed_array_c_abi.md
upcoming.md
vscode.md
wasm.md
+1872 -631
View File
File diff suppressed because it is too large. Load diff
+224
View File
@@ -0,0 +1,224 @@
module ssa
import os
import v.parser
import v.pref
import v.types
fn assert_runtime_symbol_source_body(m &Module, name string, expected string) {
mut found := 0
for f in m.funcs {
if f.name != name {
continue
}
assert !f.is_c_extern, name
assert f.params.len == 0, name
assert f.blocks.len == 1, name
last := m.blocks[f.blocks[0]].instrs.last()
instruction := m.instrs[m.values[last].index]
assert instruction.op == .ret, name
assert m.values[instruction.operands[0]].name == expected, name
found++
}
assert found == 1, name
}
fn test_runtime_replacements_preserve_selected_main_declarations() {
names := ['wyhash64', 'array_new', 'new_map', 'prealloc_malloc', 'check_fwrite', 'is_dir',
'bytestr', 'f32_to_str_l', 'normalize_path_in_builder', 'cpu_relax', 'tos2', 'int_str',
'v3_pthread_create', 'join_path', 'name_list']
path := os.join_path(os.vtmp_dir(), 'ssa_runtime_user_symbols_${os.getpid()}.v')
defer { os.rm(path) or {} }
mut source := 'module main\n'
mut used := map[string]bool{}
for i, name in names {
source += 'fn ${name}() int { return ${i + 17} }\n'
used[name] = true
}
source += "
fn runtime_control() int {
values := [1, 2]
mut lookup := map[string]int{}
lookup['a'] = 3
lookup['b'] = 4
lookup['c'] = values[1]
return lookup['a'] + lookup['b'] + lookup['c']
}
fn source_control() int { return new_map() + array_new() + join_path() + name_list() }
"
used['runtime_control'] = true
used['source_control'] = true
os.write_file(path, source)!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
_ = tc.check_semantics_opt(false)
assert tc.errors.len == 0, tc.errors.str()
tc.annotate_types()
m := build_with_options(a, used, &tc, BuildOptions{
target: TargetData{ ptr_size: 4 }
exact_used_fns: true
})
for i, name in names {
assert_runtime_symbol_source_body(m, name, '${i + 17}')
}
// C helpers retain their own implementations and signatures alongside V names.
mut c_helpers := 0
for f in m.funcs {
if f.name == 'C.wyhash64' {
assert !f.is_c_extern
assert f.params.len == 2
assert f.blocks.len > 0
c_helpers++
} else if f.name == 'C.cpu_relax' {
assert !f.is_c_extern
assert f.params.len == 0
assert f.typ == TypeID(0)
c_helpers++
} else if f.name == 'C.v3_pthread_create' {
assert !f.is_c_extern
assert f.params.len == 4
assert f.blocks.len > 0
c_helpers++
}
}
assert c_helpers == 3
mut runtime_targets := map[string]bool{}
mut source_targets := map[string]bool{}
for f in m.funcs {
if f.name !in ['runtime_control', 'source_control'] {
continue
}
for block_id in f.blocks {
for value_id in m.blocks[block_id].instrs {
instruction := m.instrs[m.values[value_id].index]
if instruction.op != .call {
continue
}
callee := m.funcs[m.values[instruction.operands[0]].index]
assert m.values[instruction.operands[0]].name == callee.name
if f.name == 'runtime_control' {
runtime_targets[callee.name] = true
} else {
assert instruction.operands.len == 1
source_targets[callee.name] = true
}
}
}
}
assert runtime_targets['__ssa_runtime_new_map'], runtime_targets.str()
assert runtime_targets['__ssa_runtime_array_new'], runtime_targets.str()
assert source_targets == {
'new_map': true
'array_new': true
'join_path': true
'name_list': true
}
}
fn test_runtime_replacement_filters_respect_module_ownership() {
b := Builder{}
for name in ['wyhash64', 'panic', 'new_map', 'prealloc_malloc', 'bytestr', 'array_eq_raw'] {
assert b.skip_source_fn_in_module(name, 'builtin'), name
for module_name in ['', 'main', 'other', 'example.builtin'] {
assert !b.skip_source_fn_in_module(name, module_name), '${module_name}.${name}'
}
}
for name in ['new_builder', 'Builder.write_string', 'Builder.push_many'] {
assert b.skip_source_fn_in_module(name, 'strings'), name
assert !b.skip_source_fn_in_module(name, 'example.strings'), name
}
for name in ['is_dir', 'check_fwrite', 'join_path_single'] {
assert b.skip_source_fn_in_module(name, 'os'), name
assert !b.skip_source_fn_in_module(name, 'example.os'), name
}
assert b.skip_source_fn_in_module('fxx_to_str_l_parse', 'strconv')
assert !b.skip_source_fn_in_module('fxx_to_str_l_parse', 'other')
assert b.skip_source_fn_in_module('PRNG.u64', 'rand')
assert !b.skip_source_fn_in_module('PRNG.u64', 'other')
assert b.skip_source_fn_in_module('Decoder.decompress', 'embed_file')
assert !b.skip_source_fn_in_module('Decoder.decompress', 'other')
}
fn test_runtime_replacements_preserve_nested_module_declarations() {
dir := os.join_path(os.vtmp_dir(), 'ssa_runtime_nested_symbols_${os.getpid()}')
os.mkdir_all(dir)!
defer { os.rmdir_all(dir) or {} }
path := os.join_path(dir, 'strings.v')
os.write_file(path, 'module strings
pub fn new_builder() int { return 41 }
pub fn wyhash64() int { return 43 }
pub fn check_fread() int { return 47 }
')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
m := build_with_options(a, map[string]bool{}, unsafe { nil }, BuildOptions{
target: TargetData{ ptr_size: 4 }
source_modules: {
path: 'example.strings'
}
})
assert_runtime_symbol_source_body(m, 'example.strings.new_builder', '41')
assert_runtime_symbol_source_body(m, 'example.strings.wyhash64', '43')
assert_runtime_symbol_source_body(m, 'example.strings.check_fread', '47')
mut runtime_builder := false
for f in m.funcs {
if f.name == 'strings.new_builder' {
assert f.params.len == 1
assert f.blocks.len > 0
runtime_builder = true
}
}
assert runtime_builder
}
fn test_empty_source_module_override_preserves_builtin_ownership() {
dir := os.join_path(os.vtmp_dir(), 'ssa_runtime_builtin_owner_${os.getpid()}')
os.mkdir_all(dir)!
defer { os.rmdir_all(dir) or {} }
builtin_source := os.join_path(dir, 'builtin.v')
main_source := os.join_path(dir, 'main.v')
os.write_file(builtin_source, 'module builtin
fn new_map_data() int { return 99 }
fn new_map() int { return new_map_data() }
')!
os.write_file(main_source, 'fn new_map() int { return 31 }
')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_files([builtin_source, main_source])
assert p.diagnostics.len == 0, p.diagnostics.str()
source_modules := {
builtin_source: ''
main_source: ''
}
b := Builder{
a: a
source_modules: source_modules
}
for node in a.nodes {
if node.kind == .module_decl {
assert b.source_module_name(node) == node.value
}
}
m := build_with_options(a, {
'new_map': true
}, unsafe { nil }, BuildOptions{
target: TargetData{ ptr_size: 4 }
exact_used_fns: true
source_modules: source_modules
})
assert_runtime_symbol_source_body(m, 'new_map', '31')
mut runtime_map := false
for f in m.funcs {
assert f.name != 'new_map_data'
if f.name == '__ssa_runtime_new_map' {
assert f.params.len == 6
assert f.blocks.len > 0
runtime_map = true
}
}
assert runtime_map
}
+83
View File
@@ -0,0 +1,83 @@
module ssa
import v.ssa.optimize
struct NumericStoreCase {
from string
to string
op OpCode
}
fn test_numeric_store_conversions_survive_scalar_promotion() {
cases := [
NumericStoreCase{'int', 'f64', .sitofp},
NumericStoreCase{'u32', 'f64', .uitofp},
NumericStoreCase{'f32', 'f64', .bitcast},
NumericStoreCase{'f64', 'f32', .bitcast},
NumericStoreCase{'i8', 'i64', .sext},
NumericStoreCase{'u8', 'i64', .zext},
NumericStoreCase{'i64', 'u8', .trunc},
NumericStoreCase{'i8', 'u8', .bitcast},
NumericStoreCase{'f64', 'int', .fptosi},
NumericStoreCase{'f64', 'u32', .fptoui},
]
for path in ['user', 'synthetic', 'field'] {
for scenario in cases {
mut m := Module.new()
m.target = TargetData{ ptr_size: 4 }
mut b := Builder{
m: m
i64_type: m.type_store.get_int(64)
i32_type: m.type_store.get_int(32)
i8_type: m.type_store.get_int(8)
u64_type: m.type_store.get_uint(64)
u32_type: m.type_store.get_uint(32)
u8_type: m.type_store.get_uint(8)
f32_type: m.type_store.get_float(32)
f64_type: m.type_store.get_float(64)
}
from := b.resolve_type(scenario.from)
to := b.resolve_type(scenario.to)
func := m.new_function('conversion', to)
entry := m.add_block(func, 'entry')
input := m.add_value(.argument, from, 'input', 0)
m.func_add_param(func, input)
b.cur_block = entry
address := b.emit0(.alloca, m.type_store.get_ptr(to))
store := if path == 'synthetic' {
// Synthetic emitters must put conversions in their explicit block.
b.cur_block = -1
b.block_instr2(.store, entry, b.void_type, input, address)
} else if path == 'field' {
value := b.coerce_store_value(input, to)
b.emit2(.store, b.void_type, value, address)
} else {
b.emit2(.store, b.void_type, input, address)
}
stored := m.instrs[m.values[store].index].operands[0]
assert m.values[stored].typ == to, '${path}: ${scenario}'
assert m.values[stored].kind == .instruction
conversion := m.instrs[m.values[stored].index]
assert conversion.op == scenario.op, '${path}: ${scenario}'
assert conversion.block == entry
b.cur_block = entry
loaded := b.emit1(.load, to, address)
b.emit1(.ret, b.void_type, loaded)
optimize.optimize(mut m)
mut returns := 0
for block in m.funcs[func].blocks {
for value in m.blocks[block].instrs {
instruction := m.instrs[m.values[value].index]
assert instruction.op !in [.alloca, .store, .load]
if instruction.op == .ret {
result := m.values[instruction.operands[0]]
assert result.typ == to, '${path}: ${scenario}'
assert m.instrs[result.index].op == scenario.op
returns++
}
}
}
assert returns == 1
}
}
}
+465 -7
View File
@@ -80,13 +80,105 @@ fn test_native_field_type_lookup_preserves_containers_through_pointer_receivers(
// test_bench_runtime_stubs_include_macos_rss_helper validates this v3 regression case.
fn test_bench_runtime_stubs_include_macos_rss_helper() {
assert 'macos_rss_kb' in bench_runtime_stub_names
assert 'bench.macos_rss_kb' in bench_runtime_stub_names
assert 'v.bench.macos_rss_kb' in bench_runtime_stub_names
assert 'macos_rss_kb' in bench_runtime_stub_names
assert 'macos_rss_kb' !in wasm_bench_runtime_stub_names
assert wasm_bench_runtime_stub_names.all(it.contains('.'))
assert 'macos_peak_rss_kb' !in bench_runtime_stub_names
assert 'bench.macos_peak_rss_kb' !in bench_runtime_stub_names
b := Builder{}
assert b.skip_source_fn('macos_rss_kb')
assert b.skip_source_fn('bench.macos_rss_kb')
for name in ['current_rss_kb', 'macos_rss_kb', 'linux_rss_kb'] {
assert b.skip_source_fn_in_module(name, 'bench')
assert b.skip_source_fn_in_module(name, 'v.bench')
assert !b.skip_source_fn_in_module(name, 'main')
assert !b.skip_source_fn_in_module(name, 'other')
}
}
fn test_bench_runtime_stubs_preserve_user_functions() {
path := os.join_path(os.vtmp_dir(), 'ssa_user_rss_helpers_${os.getpid()}.v')
defer { os.rm(path) or {} }
os.write_file(path, 'module main\nfn current_rss_kb() i64 { return 17 }\nfn macos_rss_kb() i64 { return 23 }\nfn linux_rss_kb() i64 { return 31 }\n')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
m := build_with_options(a, map[string]bool{}, unsafe { nil }, BuildOptions{
target: TargetData{ ptr_size: 4 }
})
expected := {
'current_rss_kb': '17'
'macos_rss_kb': '23'
'linux_rss_kb': '31'
}
mut found := 0
for f in m.funcs {
if want := expected[f.name] {
assert f.blocks.len == 1
last := m.blocks[f.blocks[0]].instrs.last()
instruction := m.instrs[m.values[last].index]
assert instruction.op == .ret
assert m.values[instruction.operands[0]].name == want
found++
}
}
assert found == expected.len
}
fn test_char_literal_value_decodes_unicode_and_escapes() {
expected := {
'A': 65
'é': 233
'★': 9733
'😀': 128512
r'\x41': 65
r'\u0041': 65
r'\U0001F600': 128512
r'\101': 65
r'\0': 0
r'\a': 7
r'\b': 8
r'\e': 27
r'\f': 12
r'\n': 10
r'\r': 13
r'\t': 9
r'\v': 11
r'\\': 92
}
for value, want in expected {
assert char_literal_value(value) == want, value
}
}
fn test_rune_literals_use_rune_width() {
path := os.join_path(os.vtmp_dir(), 'ssa_rune_literals_${os.getpid()}.v')
defer { os.rm(path) or {} }
os.write_file(path, 'fn unicode() rune { return `😀` }\nfn escaped_ascii() rune { return `\\u0041` }\nfn escaped_unicode() rune { return `\\xe2\\x98\\x85` }\n')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
m := build_with_options(a, map[string]bool{}, unsafe { nil }, BuildOptions{
target: TargetData{ ptr_size: 4 }
})
expected := {
'unicode': '128512'
'escaped_ascii': '65'
'escaped_unicode': '9733'
}
mut found := 0
for f in m.funcs {
if want := expected[f.name] {
last := m.blocks[f.blocks[0]].instrs.last()
instruction := m.instrs[m.values[last].index]
assert instruction.op == .ret
value := m.values[instruction.operands[0]]
assert value.name == want
assert m.type_store.types[value.typ].width == 32
found++
}
}
assert found == expected.len
}
// test_runtime_helpers_remain_used_when_module_qualified validates this v3 regression case.
@@ -108,15 +200,15 @@ fn test_native_c_tm_uses_the_platform_abi_layout() {
// test_native_task_basic_info_uses_the_platform_abi_layout validates this v3 regression case.
fn test_native_task_basic_info_uses_the_platform_abi_layout() {
abi := native_c_struct_abi('C.task_basic_info') or { panic('missing task info ABI') }
abi := native_c_struct_abi('C.task_basic_info', 8) or { panic('missing task info ABI') }
assert abi.field_names[1] == 'resident_size'
assert abi.field_types == ['u64', 'u64', 'u64', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32']
assert native_c_struct_abi('C.other') == none
assert native_c_struct_abi('C.other', 8) == none
}
// test_native_rusage_uses_the_platform_abi_layout validates this v3 regression case.
fn test_native_rusage_uses_the_platform_abi_layout() {
abi := native_c_struct_abi('C.rusage') or { panic('missing rusage ABI') }
abi := native_c_struct_abi('C.rusage', 8) or { panic('missing rusage ABI') }
assert abi.field_names[4] == 'ru_maxrss'
assert abi.field_types.len == 18
assert abi.field_types.all(it == 'i64')
@@ -132,7 +224,7 @@ fn test_native_darwin_pthread_types_use_the_platform_abi_layout() {
'C.pthread_condattr_t': '[2]u64'
}
for name, field_type in expected {
abi := native_c_struct_abi(name) or { panic('missing ${name} ABI') }
abi := native_c_struct_abi(name, 8) or { panic('missing ${name} ABI') }
assert abi.field_names == ['opaque']
assert abi.field_types == [field_type]
}
@@ -298,6 +390,250 @@ fn test_build_can_skip_optimizer_use_lists() {
assert m.values.all(it.uses.len == 0)
}
fn test_wasm32_and_native_string_layouts() {
a := &flat.FlatAst{}
for pointer_size in [4, 8] {
m := build_with_options(a, map[string]bool{}, unsafe { nil }, BuildOptions{
target: TargetData{ ptr_size: pointer_size }
})
mut found := false
for f in m.funcs {
if f.name != 'println' {
continue
}
string_type := m.values[f.params[0]].typ
pointer_type := m.type_store.types[string_type].fields[0]
assert m.type_size(pointer_type) == pointer_size
assert m.type_align(pointer_type) == pointer_size
assert m.struct_field_offset(string_type, 1) == pointer_size
assert m.struct_field_offset(string_type, 2) == pointer_size + 4
assert m.type_size(string_type) == pointer_size + 8
found = true
}
assert found
}
}
fn test_wasm32_global_constant_initializers() {
path := os.join_path(os.vtmp_dir(), 'ssa_wasm_global_initializers_${os.getpid()}.v')
defer { os.rm(path) or {} }
os.write_file(path, 'const base = 10\n__global nested = int(u8(u16(300)))\n__global signed = int(i8(128))\n__global wrapped = int(u8(250) + u8(10))\n__global folded = base * 4 + 1\n__global half = u64(18446744073709551615) / u64(2)\n__global fractional = 1.5 + 2.0\n__global oversized64 = u64(1) << u64(64)\n__global oversized32 = u32(1) << u64(32)\n__global narrow_signed = int(i8(-1) >> u64(8))\n__global narrow_logical = int(i8(-5) >>> 1)\n')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.enable_globals = true
tc.collect(a)
_ = tc.check_semantics_opt(false)
assert tc.errors.len == 0, tc.errors.str()
tc.annotate_types()
m := build_with_options(a, map[string]bool{}, &tc, BuildOptions{
target: TargetData{ ptr_size: 4 }
})
expected := {
'nested': i64(44)
'signed': i64(-128)
'wrapped': i64(4)
'folded': i64(41)
'half': i64(9223372036854775807)
'oversized64': i64(0)
'oversized32': i64(0)
'narrow_signed': i64(-1)
'narrow_logical': i64(125)
}
mut found := 0
for global in m.globals {
if value := expected[global.name] {
assert global.initial_value == value, global.name
found++
} else if global.name == 'fractional' {
assert global.initial_data == [u8(0), 0, 0, 0, 0, 0, 12, 64]
found++
}
}
assert found == expected.len + 1
}
fn test_wasm32_wide_literals_compared_with_narrow_integers() {
path := os.join_path(os.vtmp_dir(), 'ssa_narrow_comparison_${os.getpid()}.v')
defer { os.rm(path) or {} }
os.write_file(path, 'fn equal(value int) bool { return value == 4294967296 }\nfn lower(value int) bool { return value < 4294967296 }\nfn unsigned_byte(value u8) bool { return value == 256 }\nfn unsigned_max(value u64) bool { return value == 18446744073709551615 }\n')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
_ = tc.check_semantics_opt(false)
assert tc.errors.len == 0, tc.errors.str()
tc.annotate_types()
m := build_with_options(a, map[string]bool{}, &tc, BuildOptions{
target: TargetData{ ptr_size: 4 }
})
expected_unsigned := {
'equal': false
'lower': false
'unsigned_byte': true
'unsigned_max': true
}
mut found := 0
for f in m.funcs {
if unsigned := expected_unsigned[f.name] {
for block in f.blocks {
for value in m.blocks[block].instrs {
instruction := m.instrs[m.values[value].index]
if instruction.op !in [.eq, .lt] {
continue
}
for operand in instruction.operands {
typ := m.type_store.types[m.values[operand].typ]
assert typ.kind == .int_t, f.name
assert typ.width == 64, f.name
assert typ.is_unsigned == unsigned, f.name
}
found++
}
}
}
}
assert found == expected_unsigned.len
}
fn test_wasm32_indirect_call_numeric_parameter_types() {
path := os.join_path(os.vtmp_dir(), 'ssa_indirect_numeric_${os.getpid()}.v')
defer { os.rm(path) or {} }
os.write_file(path, 'fn scale(value f64) f64 { return value * 2.0 }\nfn scale32(value f32) f32 { return value * f32(2) }\nfn integer_argument() f64 { callback := scale\nreturn callback(3) }\nfn f32_argument() f64 { callback := scale\nreturn callback(f32(3.5)) }\nfn integer_to_f32_argument() f32 { callback := scale32\nreturn callback(3) }\n')!
mut p := parser.Parser.new(pref.new_preferences())
mut a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
_ = tc.check_semantics_opt(false)
assert tc.errors.len == 0, tc.errors.str()
tc.annotate_types()
// The CLI's non-generic path lowers after the local checker scopes are gone.
tc.trust_checked_expr_types = false
for node in a.nodes {
if node.kind != .call || node.children_count == 0 {
continue
}
callee_id := a.child(&node, 0)
if a.nodes[int(callee_id)].value == 'callback' {
// Transformed local callees recover their signature from the binding.
a.nodes[int(callee_id)].typ = ''
tc.expr_type_set[int(callee_id)] = false
}
}
m := build_with_options(a, map[string]bool{}, &tc, BuildOptions{
target: TargetData{ ptr_size: 4 }
})
expected := {
'integer_argument': 64
'f32_argument': 64
'integer_to_f32_argument': 32
}
mut found := 0
for f in m.funcs {
if width := expected[f.name] {
for block in f.blocks {
for value in m.blocks[block].instrs {
instruction := m.instrs[m.values[value].index]
if instruction.op != .call_indirect {
continue
}
argument := m.values[instruction.operands[1]]
argument_type := m.type_store.types[argument.typ]
assert argument_type.kind == .float_t, f.name
assert argument_type.width == width, f.name
found++
}
}
}
}
assert found == expected.len
}
fn test_wasm32_scalar_str_methods_without_native_runtime_bodies() {
dir := os.join_path(os.vtmp_dir(), 'ssa_wasm_scalar_str_${os.getpid()}')
os.mkdir_all(dir)!
defer { os.rmdir_all(dir) or {} }
builtin_source := os.join_path(dir, 'builtin.v')
main_source := os.join_path(dir, 'main.v')
os.write_file(builtin_source, 'module builtin\nfn (value int) str() string { return "native signed" }\nfn (value u64) str() string { return "native unsigned" }\nfn (value bool) str() string { return "native boolean" }\n')!
os.write_file(main_source, 'module main\nstruct Named {}\nfn (value Named) str() string { return "user method" }\nfn main() {\n signed := int(-1).str()\n unsigned := u64(18446744073709551615).str()\n boolean := true.str()\n user := Named{}.str()\n wide := (-9223372036854775807).str()\n}\n')!
mut p := parser.Parser.new(pref.new_preferences())
mut a := p.parse_files([builtin_source, main_source])
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
_ = tc.check_semantics_opt(false)
assert tc.errors.len == 0, tc.errors.str()
tc.annotate_types()
for transformed in [false, true] {
if transformed {
// The transformer moves method receivers into explicit call arguments.
for id in 0 .. a.nodes.len {
node := a.nodes[id]
if node.kind != .call || node.children_count != 1 {
continue
}
callee_id := a.child(&node, 0)
callee := a.nodes[int(callee_id)]
if callee.kind != .selector || callee.value != 'str' {
continue
}
base_id := a.child(&callee, 0)
base := a.nodes[int(base_id)]
primitive := if base.kind == .bool_literal {
'bool'
} else if base.kind == .paren {
'int'
} else {
base.value
}
if primitive !in ['int', 'u64', 'bool'] {
continue
}
a.nodes[int(callee_id)].kind = .ident
a.nodes[int(callee_id)].value = '${primitive}.str'
a.nodes[int(callee_id)].children_count = 0
a.nodes[id].children_start = i32(a.begin_children())
a.add_child(callee_id)
a.add_child(base_id)
a.nodes[id].children_count = 2
}
}
m := build_with_options(a, {
'main': true
'Named.str': true
}, &tc, BuildOptions{
target: TargetData{ ptr_size: 4 }
exact_used_fns: true
})
mut targets := []string{}
mut signed_widths := []int{}
for f in m.funcs {
assert f.name !in ['int.str', 'u64.str', 'bool.str']
if f.name != 'main' {
continue
}
for block in f.blocks {
for value in m.blocks[block].instrs {
instruction := m.instrs[m.values[value].index]
if instruction.op == .call {
target := m.values[instruction.operands[0]].name
targets << target
if target == 'int_str' {
signed_widths << m.type_store.types[m.values[instruction.operands[1]].typ].width
}
}
}
}
}
assert targets == ['int_str', 'strconv__format_uint', 'bool_str', 'Named.str', 'int_str']
assert signed_widths == [32, 64]
}
}
fn test_type_size_reuses_module_layout_cache() {
mut m := Module.new()
i32_type := m.type_store.get_int(32)
@@ -460,6 +796,42 @@ fn test_native_enum_registration_evaluates_escaped_initializer_references() {
}
}
fn test_wasm32_script_constants_reset_module_scope_after_imported_file() {
root := os.join_path(os.vtmp_dir(), 'ssa_script_consts_${os.getpid()}')
os.mkdir_all(root)!
defer { os.rmdir_all(root) or {} }
library := os.join_path(root, 'dependency.v')
script := os.join_path(root, 'main.v')
os.write_file(library, 'module dependency\nconst base = 99\n__global imported_result = base\n')!
os.write_file(script, 'const base = 10\nconst answer = base * 4 + 2\n__global result = answer - 1\n')!
mut p := parser.Parser.new(pref.new_preferences())
p.parse_file(library)
p.parse_file(script)
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(p.a)
tc.enable_globals = true
tc.collect(p.a)
tc.annotate_types()
assert tc.errors.len == 0, tc.errors.str()
m := build_with_options(p.a, map[string]bool{}, &tc, BuildOptions{
target: TargetData{ ptr_size: 4 }
source_modules: {
library: 'dependency'
script: 'main'
}
})
imported := m.globals.filter(it.name == 'dependency.imported_result')
assert imported.len == 1
assert imported[0].initial_value == 99
for global in m.globals {
if global.name == 'result' {
assert global.initial_value == 41
return
}
}
assert false, 'missing script result global'
}
fn native_thread_test_module(name string, source string, detached bool) &Module {
path := os.join_path(os.vtmp_dir(), 'v3_ssa_thread_${name}_${os.getpid()}.v')
defer { os.rm(path) or {} }
@@ -749,3 +1121,89 @@ fn main() {}
}
assert found
}
fn test_float_unary_minus_preserves_signed_zero_for_both_targets() ! {
path := os.join_path(os.vtmp_dir(), 'ssa_float_negation_${os.getpid()}.v')
defer { os.rm(path) or {} }
os.write_file(path, 'module main
fn negative_zero64() f64 { return -0.0 }
fn negative_zero32() f32 { return -f32(0.0) }
fn negate64(value f64) f64 { return -value }
fn negate32(value f32) f32 { return -value }
')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
tc.annotate_types()
assert tc.errors.len == 0, tc.errors.str()
for pointer_size in [4, 8] {
m := build_with_options(a, map[string]bool{}, &tc, BuildOptions{
target: TargetData{ ptr_size: pointer_size }
})
mut found := 0
for f in m.funcs {
if f.name !in ['negative_zero64', 'negative_zero32', 'negate64', 'negate32'] {
continue
}
mut negation_found := false
for block_id in f.blocks {
for value in m.blocks[block_id].instrs {
instr := m.instrs[m.values[value].index]
if instr.op != .fsub { continue }
zero := m.values[instr.operands[0]]
assert zero.name == '-0.0', f.name
assert m.type_store.types[zero.typ].kind == .float_t, f.name
assert m.type_store.types[zero.typ].width == if f.name.ends_with('32') {
32
} else {
64
}, f.name
assert zero.typ == m.values[instr.operands[1]].typ, f.name
negation_found = true
}
}
assert negation_found, f.name
found++
}
assert found == 4
}
}
fn test_wasm32_maps_keep_source_and_runtime_helpers_separate() ! {
path := os.join_path(os.vtmp_dir(), 'ssa_wasm_map_names_${os.getpid()}.v')
defer { os.rm(path) or {} }
os.write_file(path, 'module main
fn map__get(value int) int { return value + 13 }
fn lookup() int { values := {1: 7}
return values[1] }
')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
mut tc := types.TypeChecker.new(a)
tc.collect(a)
tc.annotate_types()
assert tc.errors.len == 0, tc.errors.str()
m := build_with_options(a, map[string]bool{}, &tc, BuildOptions{
target: TargetData{ ptr_size: 4 }
})
assert !m.funcs.any(it.name in ['v3_native_map_hash_key', 'v3_native_map_index'])
source := m.funcs.filter(it.name == 'map__get')[0]
assert source.params.len == 1
lookup := m.funcs.filter(it.name == 'lookup')[0]
mut runtime_call_found := false
for block_id in lookup.blocks {
for value in m.blocks[block_id].instrs {
instr := m.instrs[m.values[value].index]
if instr.op != .call { continue }
callee := m.values[instr.operands[0]]
if !callee.name.contains('map__get') { continue }
assert callee.name != 'map__get'
assert m.funcs[callee.index].params.len == 3
runtime_call_found = true
}
}
assert runtime_call_found
}
+40
View File
@@ -0,0 +1,40 @@
module ssa
import os
import v.parser
import v.pref
fn test_native_uname_layout_preserves_darwin_fields_without_changing_wasm32() {
path := os.join_path(os.vtmp_dir(), 'ssa_utsname_${os.getpid()}.v')
defer { os.rm(path) or {} }
os.write_file(path, 'pub struct C.utsname {
sysname &char
nodename &char
release &char
version &char
machine &char
}
')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(path)
assert p.diagnostics.len == 0, p.diagnostics.str()
for pointer_size in [4, 8] {
m := build_with_options(a, map[string]bool{}, unsafe { nil }, BuildOptions{
target: TargetData{ ptr_size: pointer_size }
})
mut found := false
for id, typ in m.type_store.types {
if typ.kind != .struct_t
|| typ.field_names != ['sysname', 'nodename', 'release', 'version', 'machine'] {
continue
}
stride := if pointer_size == 8 { 256 } else { 4 }
assert m.type_size(TypeID(id)) == stride * 5
for i in 0 .. 5 {
assert m.struct_field_offset(TypeID(id), i) == stride * i
}
found = true
}
assert found
}
}
+72 -9
View File
@@ -25,6 +25,13 @@ fn constant_fold(mut m ssa.Module) bool {
if rhs.kind == .constant && rhs.name == 'undef' {
continue
}
// These folds use integer identities and parse constants as i64.
// Comparisons share opcodes with floats, which must retain their
// precision and IEEE semantics when evaluated by the backend.
if m.type_store.types[lhs.typ].kind != .int_t
|| m.type_store.types[rhs.typ].kind != .int_t {
continue
}
// Algebraic simplifications first (work even with non-constant operands):
// x+0, x*1, x*0, x-x, x^x, x&x, x|x, x<<0, x*2 -> x<<1, etc.
@@ -57,23 +64,36 @@ fn constant_fold(mut m ssa.Module) bool {
if lhs.kind != .constant || rhs.kind != .constant {
continue
}
l_int := lhs.name.i64()
r_int := rhs.name.i64()
l_int := integer_constant_value(m, lhs) or { continue }
r_int := integer_constant_value(m, rhs) or { continue }
if instr.op in [.shl, .ashr, .lshr]
&& r_int >= m.type_store.types[lhs.typ].width {
// Oversized shifts have target-specific behavior; preserve runtime evaluation.
continue
}
if instr.op in [.sdiv, .srem] && r_int == -1 {
width := m.type_store.types[lhs.typ].width
minimum := if width == 64 { min_i64 } else { -i64(u64(1) << (width - 1)) }
// Preserve the target's overflow behavior and avoid trapping the compiler.
if l_int == minimum {
continue
}
}
mut result := i64(0)
mut folded := false
match instr.op {
.add {
result = l_int + r_int
result = i64(u64(l_int) + u64(r_int))
folded = true
}
.sub {
result = l_int - r_int
result = i64(u64(l_int) - u64(r_int))
folded = true
}
.mul {
result = l_int * r_int
result = i64(u64(l_int) * u64(r_int))
folded = true
}
.sdiv {
@@ -126,7 +146,13 @@ fn constant_fold(mut m ssa.Module) bool {
}
.lshr {
if r_int >= 0 && r_int < 64 {
result = i64(u64(l_int) >> u64(r_int))
width := m.type_store.types[lhs.typ].width
bits := if width == 64 {
u64(l_int)
} else {
u64(l_int) & ((u64(1) << width) - 1)
}
result = i64(bits >> u64(r_int))
folded = true
}
}
@@ -185,6 +211,43 @@ fn constant_fold(mut m ssa.Module) bool {
return changed
}
// integer_constant_value preserves the full integer bit pattern, including
// literals above max_i64, then interprets it using the operand's SSA type.
fn integer_constant_value(m &ssa.Module, value ssa.Value) ?i64 {
typ := m.type_store.types[value.typ]
if typ.kind != .int_t || typ.width <= 0 || typ.width > 64 {
return none
}
mut literal := value.name.replace('_', '')
mut negative := false
if literal.starts_with('-') || literal.starts_with('+') {
negative = literal[0] == `-`
literal = literal[1..]
}
mut base := 10
if literal.starts_with('0x') || literal.starts_with('0X') {
base = 16
literal = literal[2..]
} else if literal.starts_with('0o') || literal.starts_with('0O') {
base = 8
literal = literal[2..]
} else if literal.starts_with('0b') || literal.starts_with('0B') {
base = 2
literal = literal[2..]
}
parsed := literal.parse_uint(base, 64) or { return none }
mut bits := if negative { u64(0) - parsed } else { parsed }
if typ.width < 64 {
mask := (u64(1) << typ.width) - 1
bits &= mask
// i1 represents a boolean, rather than a signed one-bit integer.
if !typ.is_unsigned && typ.width > 1 && bits & (u64(1) << (typ.width - 1)) != 0 {
bits |= ~mask
}
}
return i64(bits)
}
// try_algebraic_simplify implements identity/strength-reduction simplifications:
// x+0=x, x*1=x, x*0=0, x-x=0, x^x=0, x&x=x, x|x=x, x<<0=x, x*2=x<<1, x/1=x.
// Returns (replacement_id, needs_zero):
@@ -192,7 +255,7 @@ fn constant_fold(mut m ssa.Module) bool {
// repl == -2 -> rewrite as `x << 1` (caller builds the shift)
// needs_zero == true -> replace all uses with a fresh zero constant
// repl == -1 -> no simplification
fn try_algebraic_simplify(_m &ssa.Module, val_id int, instr ssa.Instruction, lhs ssa.Value, rhs ssa.Value) (int, bool) {
fn try_algebraic_simplify(m &ssa.Module, val_id int, instr ssa.Instruction, lhs ssa.Value, rhs ssa.Value) (int, bool) {
lhs_id := instr.operands[0]
rhs_id := instr.operands[1]
@@ -213,11 +276,11 @@ fn try_algebraic_simplify(_m &ssa.Module, val_id int, instr ssa.Instruction, lhs
mut const_is_rhs := false
mut other_id := 0
if lhs.kind == .constant && lhs.name != 'undef' {
const_val = lhs.name.i64()
const_val = integer_constant_value(m, lhs) or { return -1, false }
const_is_rhs = false
other_id = instr.operands[1]
} else if rhs.kind == .constant && rhs.name != 'undef' {
const_val = rhs.name.i64()
const_val = integer_constant_value(m, rhs) or { return -1, false }
const_is_rhs = true
other_id = instr.operands[0]
} else {
+315
View File
@@ -0,0 +1,315 @@
module optimize
import v.ssa
fn assert_comparison_preserved(mut m ssa.Module, op ssa.OpCode, lhs ssa.ValueID, rhs ssa.ValueID) {
bool_type := m.type_store.get_int(1)
func_id := m.new_function('compare', bool_type)
entry := m.add_block(func_id, 'entry')
comparison := m.add_instr(op, entry, bool_type, [lhs, rhs])
ret := m.add_instr(.ret, entry, ssa.TypeID(0), [comparison])
optimize_with_options(mut m, OptimizeOptions{
mem2reg: true
eliminate_phis: true
strict_verify: true
})
assert m.blocks[entry].instrs == [comparison, ret]
assert m.instrs[m.values[comparison].index].op == op
assert m.instrs[m.values[comparison].index].operands == [lhs, rhs]
assert m.instrs[m.values[ret].index].operands == [comparison]
}
fn test_constant_fold_preserves_float_comparisons() {
for width in [32, 64] {
for op in [ssa.OpCode.eq, .ne, .lt, .gt, .le, .ge] {
mut m := ssa.Module.new()
float_type := m.type_store.get_float(width)
lhs := m.get_or_add_const(float_type, '1.25')
rhs := m.get_or_add_const(float_type, '1.5')
assert_comparison_preserved(mut m, op, lhs, rhs)
}
}
}
fn test_constant_fold_preserves_float_precision() {
// These differ as f64 values but round to the same f32 value.
for width in [32, 64] {
mut m := ssa.Module.new()
float_type := m.type_store.get_float(width)
lhs := m.get_or_add_const(float_type, '16777216')
rhs := m.get_or_add_const(float_type, '16777217')
assert_comparison_preserved(mut m, .eq, lhs, rhs)
}
}
fn test_constant_fold_preserves_mixed_float_integer_comparisons() {
for float_on_left in [false, true] {
mut m := ssa.Module.new()
float_type := m.type_store.get_float(64)
int_type := m.type_store.get_int(64)
float_value := m.get_or_add_const(float_type, '1.5')
int_value := m.get_or_add_const(int_type, '1')
lhs := if float_on_left { float_value } else { int_value }
rhs := if float_on_left { int_value } else { float_value }
assert_comparison_preserved(mut m, .eq, lhs, rhs)
}
}
fn test_constant_fold_still_folds_integer_comparisons() {
ops := [ssa.OpCode.eq, .ne, .lt, .gt, .le, .ge, .ult, .ugt, .ule, .uge]
expected := ['0', '1', '1', '0', '1', '0', '0', '1', '0', '1']
for i, op in ops {
mut m := ssa.Module.new()
bool_type := m.type_store.get_int(1)
int_type := m.type_store.get_int(64)
func_id := m.new_function('compare', bool_type)
entry := m.add_block(func_id, 'entry')
lhs := m.get_or_add_const(int_type, '-1')
rhs := m.get_or_add_const(int_type, '0')
comparison := m.add_instr(op, entry, bool_type, [lhs, rhs])
ret := m.add_instr(.ret, entry, ssa.TypeID(0), [comparison])
optimize(mut m)
assert m.blocks[entry].instrs == [ret]
ret_operand := m.values[m.instrs[m.values[ret].index].operands[0]]
assert ret_operand.kind == .constant
assert ret_operand.typ == bool_type
assert ret_operand.name == expected[i]
}
}
fn assert_integer_fold(op ssa.OpCode, lhs_name string, rhs_name string, expected string, unsigned bool) {
mut m := ssa.Module.new()
int_type := if unsigned { m.type_store.get_uint(64) } else { m.type_store.get_int(64) }
result_type := if op in [.eq, .ne, .lt, .gt, .le, .ge, .ult, .ugt, .ule, .uge] {
m.type_store.get_int(1)
} else {
int_type
}
func_id := m.new_function('fold', result_type)
entry := m.add_block(func_id, 'entry')
lhs := m.get_or_add_const(int_type, lhs_name)
rhs := m.get_or_add_const(int_type, rhs_name)
operation := m.add_instr(op, entry, result_type, [lhs, rhs])
ret := m.add_instr(.ret, entry, ssa.TypeID(0), [operation])
optimize_with_options(mut m, OptimizeOptions{
strict_verify: true
})
assert m.blocks[entry].instrs == [ret]
result := m.values[m.instrs[m.values[ret].index].operands[0]]
assert result.kind == .constant
assert result.typ == result_type
assert result.name == expected, '${op}: ${lhs_name}, ${rhs_name}'
}
fn test_constant_fold_preserves_full_width_integer_bits() {
for unsigned in [false, true] {
assert_integer_fold(.eq, '9223372036854775808', '9223372036854775809', '0', unsigned)
assert_integer_fold(.ne, '9223372036854775808', '9223372036854775809', '1', unsigned)
assert_integer_fold(.eq, '18446744073709551615', '-1', '1', unsigned)
assert_integer_fold(.add, '18446744073709551615', '3', '2', unsigned)
assert_integer_fold(.sub, '18446744073709551615', '2', '-3', unsigned)
assert_integer_fold(.mul, '18446744073709551615', '3', '-3', unsigned)
assert_integer_fold(.and_, '18446744073709551615', '0xff', '255', unsigned)
}
assert_integer_fold(.lt, '9223372036854775807', '9223372036854775808', '0', false)
assert_integer_fold(.lt, '9223372036854775808', '9223372036854775809', '1', false)
assert_integer_fold(.ult, '9223372036854775807', '9223372036854775808', '1', true)
assert_integer_fold(.ugt, '18446744073709551615', '9223372036854775808', '1', true)
assert_integer_fold(.udiv, '18446744073709551615', '3', '6148914691236517205', true)
assert_integer_fold(.urem, '18446744073709551615', '2', '1', true)
assert_integer_fold(.sdiv, '-9223372036854775808', '2', '-4611686018427387904', false)
}
fn test_constant_fold_parses_integer_radices_and_separators() {
for name in ['0xffffffffffffffff', '0xFFFF_FFFF_FFFF_FFFF', '0o1777777777777777777777',
'0b' + '1'.repeat(64), '18_446_744_073_709_551_615'] {
assert_integer_fold(.eq, name, '18446744073709551615', '1', true)
assert_integer_fold(.add, name, '3', '2', true)
}
assert_integer_fold(.ne, '0x8000000000000000', '0x8000000000000001', '1', false)
assert_integer_fold(.eq, '+9223372036854775808', '-9223372036854775808', '1', false)
assert_integer_fold(.add, '010', '5', '15', false)
}
fn test_integer_constant_values_use_operand_width_and_boolean_representation() {
mut m := ssa.Module.new()
i8_type := m.type_store.get_int(8)
u8_type := m.type_store.get_uint(8)
bool_type := m.type_store.get_int(1)
for name in ['128', '0x80', '-128'] {
value := m.get_or_add_const(i8_type, name)
assert integer_constant_value(m, m.values[value])? == -128
}
value := m.get_or_add_const(u8_type, '256')
assert integer_constant_value(m, m.values[value])? == 0
true_value := m.get_or_add_const(bool_type, '1')
assert integer_constant_value(m, m.values[true_value])? == 1
false_value := m.get_or_add_const(bool_type, '0')
assert integer_constant_value(m, m.values[false_value])? == 0
}
fn test_constant_fold_observes_narrow_result_width() {
for unsigned in [false, true] {
mut m := ssa.Module.new()
int_type := if unsigned { m.type_store.get_uint(8) } else { m.type_store.get_int(8) }
bool_type := m.type_store.get_int(1)
func_id := m.new_function('overflow', bool_type)
entry := m.add_block(func_id, 'entry')
lhs := m.get_or_add_const(int_type, if unsigned { '255' } else { '127' })
one := m.get_or_add_const(int_type, '1')
zero := m.get_or_add_const(int_type, '0')
sum := m.add_instr(.add, entry, int_type, [lhs, one])
comparison := m.add_instr(if unsigned { ssa.OpCode.eq } else { ssa.OpCode.lt },
entry, bool_type, [sum, zero])
ret := m.add_instr(.ret, entry, ssa.TypeID(0), [comparison])
optimize_with_options(mut m, OptimizeOptions{
strict_verify: true
})
assert m.blocks[entry].instrs == [ret]
result := m.values[m.instrs[m.values[ret].index].operands[0]]
assert result.kind == .constant
assert result.name == '1'
}
}
fn test_constant_fold_logical_right_shift_uses_operand_width() {
expected := ['127', '32767', '2147483647', '9223372036854775807']
for i, width in [8, 16, 32, 64] {
mut m := ssa.Module.new()
int_type := m.type_store.get_int(width)
func_id := m.new_function('logical_shift', int_type)
entry := m.add_block(func_id, 'entry')
lhs := m.get_or_add_const(int_type, '-1')
one := m.get_or_add_const(int_type, '1')
shift := m.add_instr(.lshr, entry, int_type, [lhs, one])
ret := m.add_instr(.ret, entry, ssa.TypeID(0), [shift])
optimize_with_options(mut m, OptimizeOptions{
strict_verify: true
})
assert m.blocks[entry].instrs == [ret]
result := m.values[m.instrs[m.values[ret].index].operands[0]]
assert result.kind == .constant
assert result.name == expected[i]
}
}
fn test_constant_fold_logical_shift_after_narrow_overflow() {
maximums := ['127', '32767', '2147483647']
expected := ['64', '16384', '1073741824']
for i, width in [8, 16, 32] {
mut m := ssa.Module.new()
int_type := m.type_store.get_int(width)
func_id := m.new_function('logical_shift', int_type)
entry := m.add_block(func_id, 'entry')
lhs := m.get_or_add_const(int_type, maximums[i])
one := m.get_or_add_const(int_type, '1')
sum := m.add_instr(.add, entry, int_type, [lhs, one])
shift := m.add_instr(.lshr, entry, int_type, [sum, one])
ret := m.add_instr(.ret, entry, ssa.TypeID(0), [shift])
optimize_with_options(mut m, OptimizeOptions{
strict_verify: true
})
assert m.blocks[entry].instrs == [ret]
result := m.values[m.instrs[m.values[ret].index].operands[0]]
assert result.kind == .constant
assert result.name == expected[i]
}
}
fn test_constant_fold_preserves_oversized_shifts() {
for width in [8, 16, 32, 64] {
for count in [width, width + 1] {
for op in [ssa.OpCode.shl, .ashr, .lshr] {
mut m := ssa.Module.new()
int_type := m.type_store.get_int(width)
count_type := m.type_store.get_int(64)
func_id := m.new_function('oversized_shift', int_type)
entry := m.add_block(func_id, 'entry')
lhs := m.get_or_add_const(int_type, '-1')
rhs := m.get_or_add_const(count_type, count.str())
shift := m.add_instr(op, entry, int_type, [lhs, rhs])
ret := m.add_instr(.ret, entry, ssa.TypeID(0), [shift])
optimize_with_options(mut m, OptimizeOptions{
strict_verify: true
})
assert m.blocks[entry].instrs == [shift, ret]
assert m.instrs[m.values[ret].index].operands == [shift]
}
}
}
}
fn test_constant_fold_declines_invalid_and_wider_constants() {
for name in ['18446744073709551616', '0x10000000000000000', 'not_an_integer', 'undef'] {
mut m := ssa.Module.new()
int_type := m.type_store.get_int(64)
lhs := m.get_or_add_const(int_type, name)
rhs := m.get_or_add_const(int_type, '1')
assert_comparison_preserved(mut m, .eq, lhs, rhs)
}
}
fn test_constant_fold_declines_signed_division_overflow() {
minimums := ['-128', '-32768', '-2147483648', '-9223372036854775808']
for i, width in [8, 16, 32, 64] {
for op in [ssa.OpCode.sdiv, .srem] {
mut m := ssa.Module.new()
int_type := m.type_store.get_int(width)
func_id := m.new_function('overflow', int_type)
entry := m.add_block(func_id, 'entry')
lhs := m.get_or_add_const(int_type, minimums[i])
rhs := m.get_or_add_const(int_type, '-1')
operation := m.add_instr(op, entry, int_type, [lhs, rhs])
ret := m.add_instr(.ret, entry, ssa.TypeID(0), [operation])
optimize_with_options(mut m, OptimizeOptions{
strict_verify: true
})
assert m.blocks[entry].instrs == [operation, ret]
assert m.instrs[m.values[ret].index].operands == [operation]
}
}
}
fn test_algebraic_simplification_parses_full_width_constants() {
for name in ['18446744073709551615', '0xffffffffffffffff'] {
mut m := ssa.Module.new()
int_type := m.type_store.get_uint(64)
value := m.get_or_add_const(int_type, name)
other := m.add_value(.argument, int_type, 'value', 0)
instr := ssa.Instruction{
op: .mul
operands: [other, value]
}
replacement, needs_zero := try_algebraic_simplify(m, 0, instr, m.values[other],
m.values[value])
assert replacement == -1
assert !needs_zero
}
mut m := ssa.Module.new()
int_type := m.type_store.get_int(64)
one := m.get_or_add_const(int_type, '0x1')
other := m.add_value(.argument, int_type, 'value', 0)
instr := ssa.Instruction{
op: .mul
operands: [other, one]
}
replacement, needs_zero := try_algebraic_simplify(m, 0, instr, m.values[other], m.values[one])
assert replacement == other
assert !needs_zero
}
+3 -3
View File
@@ -884,8 +884,8 @@ fn (m &Module) type_size_inner(typ_id TypeID, depth int, mut visiting []bool, mu
}
typ := m.type_store.types[typ_id]
if typ.kind in [.ptr_t, .func_t] {
cache[typ_id] = 8
return 8
cache[typ_id] = m.target.ptr_size
return m.target.ptr_size
}
if typ.width > 0 {
size := (typ.width + 7) / 8
@@ -1006,7 +1006,7 @@ fn (m &Module) type_align_for_layout_inner(typ_id TypeID, depth int) int {
}
typ := m.type_store.types[typ_id]
if typ.kind in [.ptr_t, .func_t] {
return 8
return m.target.ptr_size
}
if typ.alignment > 0 {
return typ.alignment