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:
1 parent
7d8484fe13
commit
a35cd0a69f
22 files changed
+5630
-708
No files matched your search
+1
-1
@@ -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
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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}'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
')
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -15,3 +15,4 @@ tccbin_automation.md
|
||||
translated_fixed_array_c_abi.md
|
||||
upcoming.md
|
||||
vscode.md
|
||||
wasm.md
|
||||
+1872
-631
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user