From a35cd0a69fa2154d7caecf09671b04459f90cded Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 8 Oct 2026 07:19:02 +0300 Subject: [PATCH] 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 --- CONTRIBUTING.md | 2 +- doc/wasm.md | 54 + vlib/v/README.md | 3 + vlib/v/compiler_tests/wasm_codegen_test.v | 439 ++- vlib/v/driver/driver.v | 80 +- vlib/v/gen/arm64/native_float_literal_test.v | 45 + vlib/v/gen/arm64/native_uname_test.v | 66 + vlib/v/gen/wasm/encode.v | 25 +- vlib/v/gen/wasm/gen.v | 130 +- vlib/v/gen/wasm/gen_test.v | 46 + vlib/v/gen/wasm/ssa_gen.v | 1054 ++++++++ vlib/v/gen/wasm/ssa_gen_test.v | 664 +++++ vlib/v/help/build/build-wasm.txt | 5 + vlib/v/slow_tests/inout/os.out | 1 + vlib/v/ssa/builder.v | 2503 +++++++++++++----- vlib/v/ssa/builder_runtime_symbols_test.v | 224 ++ vlib/v/ssa/builder_store_test.v | 83 + vlib/v/ssa/builder_test.v | 472 +++- vlib/v/ssa/native_c_struct_test.v | 40 + vlib/v/ssa/optimize/fold.v | 81 +- vlib/v/ssa/optimize/fold_test.v | 315 +++ vlib/v/ssa/ssa.v | 6 +- 22 files changed, 5630 insertions(+), 708 deletions(-) create mode 100644 doc/wasm.md create mode 100644 vlib/v/gen/arm64/native_uname_test.v create mode 100644 vlib/v/gen/wasm/gen_test.v create mode 100644 vlib/v/gen/wasm/ssa_gen.v create mode 100644 vlib/v/gen/wasm/ssa_gen_test.v create mode 100644 vlib/v/ssa/builder_runtime_symbols_test.v create mode 100644 vlib/v/ssa/builder_store_test.v create mode 100644 vlib/v/ssa/native_c_struct_test.v create mode 100644 vlib/v/ssa/optimize/fold_test.v diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 147d7cb558..ce1c0f72ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/doc/wasm.md b/doc/wasm.md new file mode 100644 index 0000000000..648391a9d6 --- /dev/null +++ b/doc/wasm.md @@ -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 +``` diff --git a/vlib/v/README.md b/vlib/v/README.md index 1013d28253..3c30c8adf1 100644 --- a/vlib/v/README.md +++ b/vlib/v/README.md @@ -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. diff --git a/vlib/v/compiler_tests/wasm_codegen_test.v b/vlib/v/compiler_tests/wasm_codegen_test.v index 6680934619..aec8a1c0e8 100644 --- a/vlib/v/compiler_tests/wasm_codegen_test.v +++ b/vlib/v/compiler_tests/wasm_codegen_test.v @@ -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); +} +') +} diff --git a/vlib/v/driver/driver.v b/vlib/v/driver/driver.v index 61a5d740f9..788b76889b 100644 --- a/vlib/v/driver/driver.v +++ b/vlib/v/driver/driver.v @@ -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 ` 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 // .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 diff --git a/vlib/v/gen/arm64/native_float_literal_test.v b/vlib/v/gen/arm64/native_float_literal_test.v index a617a5fe61..de6d15759e 100644 --- a/vlib/v/gen/arm64/native_float_literal_test.v +++ b/vlib/v/gen/arm64/native_float_literal_test.v @@ -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}' + } + } +} diff --git a/vlib/v/gen/arm64/native_uname_test.v b/vlib/v/gen/arm64/native_uname_test.v new file mode 100644 index 0000000000..ea16a45b70 --- /dev/null +++ b/vlib/v/gen/arm64/native_uname_test.v @@ -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 +} diff --git a/vlib/v/gen/wasm/encode.v b/vlib/v/gen/wasm/encode.v index 641904eaa6..dd8e39d9f3 100644 --- a/vlib/v/gen/wasm/encode.v +++ b/vlib/v/gen/wasm/encode.v @@ -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{} diff --git a/vlib/v/gen/wasm/gen.v b/vlib/v/gen/wasm/gen.v index 9252db9e32..7411b922fa 100644 --- a/vlib/v/gen/wasm/gen.v +++ b/vlib/v/gen/wasm/gen.v @@ -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) } } } diff --git a/vlib/v/gen/wasm/gen_test.v b/vlib/v/gen/wasm/gen_test.v new file mode 100644 index 0000000000..d94809baab --- /dev/null +++ b/vlib/v/gen/wasm/gen_test.v @@ -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() +} diff --git a/vlib/v/gen/wasm/ssa_gen.v b/vlib/v/gen/wasm/ssa_gen.v new file mode 100644 index 0000000000..15ef75ee38 --- /dev/null +++ b/vlib/v/gen/wasm/ssa_gen.v @@ -0,0 +1,1054 @@ +module wasm + +import os +import v.flat +import v.ssa + +// SSAGen lowers SSA to a WebAssembly binary with WASI output support. +// A dispatch loop represents arbitrary SSA control flow without requiring a +// reducible graph. Each invocation owns its locals and linear-memory frame. +@[heap] +pub struct SSAGen { +mut: + m &ssa.Module = unsafe { nil } + mod &Module = unsafe { nil } + configured bool + exports map[string]string + init_fns []string + main_fn string + fn_index map[string]int + fn_table map[string]int + global_addr map[string]int + strings map[int]int + data []u8 + stack_global int + heap_global int + stack_bottom int + write_index int + exit_index int + alloc_index int + cur Code + locals []u8 + value_local map[int]int + slots map[int]int + phi_snapshots map[int]int + frame_size int + frame_local int + pc_local int + sret_local int = -1 + nparams int + cur_ret ssa.TypeID + warnings []string +} + +// new creates a WebAssembly generator for an SSA module. +pub fn SSAGen.new(m &ssa.Module) &SSAGen { + return &SSAGen{ m: m, mod: Module.new() } +} + +// configure supplies entry points and export names collected by the frontend. +pub fn (mut g SSAGen) configure(exports map[string]string, init_fns []string, main_fn string) { + g.configured = true + g.exports = exports.clone() + g.init_fns = init_fns.clone() + g.main_fn = main_fn +} + +// gen emits only functions reachable from the selected SSA entry points. +pub fn (mut g SSAGen) gen() ! { + functions := g.reachable_functions()! + g.collect_data() + mut init := Code{} + g.stack_bottom = (data_base + g.data.len + 15) & ~15 + stack_top := g.stack_bottom + 1024 * 1024 + init.i32_const(stack_top) + init.end() + g.stack_global = g.mod.add_global(valtype_i32, init.bytes) + g.heap_global = g.mod.add_global(valtype_i32, init.bytes) + g.mod.set_mem_min((stack_top + 65535) / 65536 + 1) + mut uses_write := false + mut uses_exit := false + for fid in functions { + for bid in g.m.funcs[fid].blocks { + for vid in g.m.blocks[bid].instrs { + instr := g.m.instrs[g.m.values[vid].index] + if instr.op == .call && instr.operands.len > 0 { + if name := g.intrinsic_name(g.m.values[instr.operands[0]]) { + uses_write = uses_write || name == 'write' + uses_exit = uses_exit || name in ['exit', 'abort'] + } + } + } + } + } + mut helper_count := 1 + mut fd_write := -1 + if uses_write { + write_type := g.mod.add_type([valtype_i32, valtype_i32, valtype_i32, valtype_i32], [valtype_i32]) + fd_write = g.mod.add_import_func('wasi_snapshot_preview1', 'fd_write', write_type) + } + if uses_exit { + exit_type := g.mod.add_type([valtype_i32], []) + g.exit_index = g.mod.add_import_func('wasi_snapshot_preview1', 'proc_exit', exit_type) + } + if uses_write { + g.write_index = g.mod.reserve_func_index(0) + mut runtime := &Gen{ mod: g.mod } + runtime.emit_write_helper(fd_write) + helper_count++ + } + g.alloc_index = g.mod.reserve_func_index(helper_count - 1) + g.emit_allocator() + mut table := []int{} + for i, fid in functions { + f := g.m.funcs[fid] + g.fn_index[f.name] = g.mod.reserve_func_index(helper_count + i) + g.fn_table[f.name] = i + 1 + table << g.fn_index[f.name] + } + g.mod.set_table(table) + for fid in functions { + g.emit_function(fid)! + } + g.mod.add_export('memory', export_mem, 0) + mut used_exports := map[string]bool{} + used_exports['memory'] = true + used_exports['_start'] = true + for fid in functions { + f := g.m.funcs[fid] + if g.configured && f.name !in g.exports { + continue + } + name := if g.configured { + g.exports[f.name] + } else { + f.name.trim_string_left('main__').replace('.', '__') + } + if name in used_exports { + g.warnings << 'not exporting ${f.name.all_after_last('.')}: export name `${name}` is already in use' + continue + } + used_exports[name] = true + g.mod.add_export(name, export_func, g.fn_index[f.name]) + } + mut entry := Code{} + for name in g.init_fns { + idx := g.fn_index[name] or { return error('wasm: missing init function `${name}`') } + entry.call(idx) + } + if g.main_fn.len > 0 { + idx := g.fn_index[g.main_fn] or { return error('wasm: missing main function `${g.main_fn}`') } + entry.call(idx) + } + entry.end() + entry_type := g.mod.add_type([], []) + entry_index := g.mod.add_func(entry_type, [], entry.bytes) + if g.main_fn.len > 0 { + g.mod.add_export('_start', export_func, entry_index) + } else if g.init_fns.len > 0 { + g.mod.set_start(entry_index) + } + if g.data.len > 0 { + g.mod.add_data(data_base, g.data) + } + g.mod.add_data(nl_ptr, [u8(10)]) +} + +// write writes the generated module to path. +pub fn (g &SSAGen) write(path string) ! { + os.write_file_array(path, g.mod.compile())! +} + +// warnings_list returns non-fatal export diagnostics. +pub fn (g &SSAGen) warnings_list() []string { + return g.warnings +} + +fn (mut g SSAGen) reachable_functions() ![]int { + mut names := map[string]int{} + for i, f in g.m.funcs { + names[f.name] = i + } + mut roots := []string{} + if g.configured { + for name, _ in g.exports { + roots << name + } + roots << g.init_fns + if g.main_fn.len > 0 { + roots << g.main_fn + } + } else { + for f in g.m.funcs { + if !f.is_c_extern && f.blocks.len > 0 { + roots << f.name + } + if f.name in ['main', 'main__main'] { + g.main_fn = f.name + } + } + } + mut reached := map[int]bool{} + for roots.len > 0 { + name := roots.pop() + fid := names[name] or { return error('wasm: unknown SSA function `${name}`') } + if fid in reached { + continue + } + f := g.m.funcs[fid] + if f.is_c_extern || f.blocks.len == 0 { + return error('wasm: unsupported external function `${name}`') + } + reached[fid] = true + for bid in f.blocks { + for vid in g.m.blocks[bid].instrs { + instr := g.m.instrs[g.m.values[vid].index] + for oi, operand in instr.operands { + if instr.is_value_operand(oi) && operand > 0 && g.m.values[operand].kind == .func_ref { + value := g.m.values[operand] + if _ := g.intrinsic_name(value) { continue } + roots << g.function_name(value) + } + } + } + } + } + mut functions := []int{} + for i in 0 .. g.m.funcs.len { + if i in reached { + functions << i + } + } + return functions +} + +fn (mut g SSAGen) reserve_data(size int, alignment int) int { + for (data_base + g.data.len) % alignment != 0 { + g.data << 0 + } + addr := data_base + g.data.len + g.data << []u8{len: size} + return addr +} + +fn (mut g SSAGen) collect_data() { + for global in g.m.globals { + size := g.m.type_size(global.typ) + addr := g.reserve_data(if size > 0 { size } else { 8 }, 8) + g.global_addr[global.name] = addr + for i in 0 .. size { + g.data[addr - data_base + i] = if i < global.initial_data.len { + global.initial_data[i] + } else if i < 8 { + u8(u64(global.initial_value) >> (i * 8)) + } else { + u8(0) + } + } + } + for i, value in g.m.values { + if value.kind !in [.string_literal, .c_string_literal] { + continue + } + text := g.reserve_data(value.name.len + 1, 1) + for j, byte in value.name.bytes() { + g.data[text - data_base + j] = byte + } + if value.kind == .c_string_literal { + g.strings[i] = text + continue + } + size := g.m.type_size(value.typ) + addr := g.reserve_data(size, 8) + g.strings[i] = addr + for j in 0 .. g.m.target.ptr_size { + g.data[addr - data_base + j] = u8(u64(text) >> (j * 8)) + } + len_offset := g.m.struct_field_offset(value.typ, 1) + lit_offset := g.m.struct_field_offset(value.typ, 2) + for j in 0 .. 4 { + g.data[addr - data_base + len_offset + j] = u8(u32(value.name.len) >> (j * 8)) + } + if lit_offset > 0 && lit_offset < size { + g.data[addr - data_base + lit_offset] = 1 + } + } +} + +fn (g &SSAGen) aggregate(typ ssa.TypeID) bool { + return typ > 0 && g.m.type_store.types[typ].kind in [.struct_t, .array_t] +} + +fn (g &SSAGen) wtype(typ ssa.TypeID) WType { + if typ <= 0 { return .void } + t := g.m.type_store.types[typ] + return match t.kind { + .void_t { .void } + .float_t { + if t.width == 32 { WType.f32 } else { WType.f64 } + } + .int_t { + if t.width > 32 { WType.i64 } else { WType.i32 } + } + else { .i32 } + } +} + +fn (mut g SSAGen) temp(w WType) int { + idx := g.nparams + g.locals.len + g.locals << wt_valtype(w) + return idx +} + +fn (mut g SSAGen) reserve_value(id int) { + if id <= 0 || id in g.value_local { return } + value := g.m.values[id] + w := g.wtype(value.typ) + if w == .void { return } + g.value_local[id] = g.temp(w) + if g.aggregate(value.typ) { + g.frame_size = (g.frame_size + 7) & ~7 + g.slots[id] = g.frame_size + g.frame_size += g.m.type_size(value.typ) + } +} + +fn (mut g SSAGen) frame_addr(offset int) { + g.cur.local_get(g.frame_local) + if offset > 0 { + g.cur.i32_const(offset) + g.cur.raw(0x6a) + } +} + +fn (mut g SSAGen) emit_function(fid int) ! { + f := g.m.funcs[fid] + g.cur = Code{} + g.locals = []u8{} + g.value_local = map[int]int{} + g.slots = map[int]int{} + g.phi_snapshots = map[int]int{} + g.frame_size = 0 + g.cur_ret = f.typ + g.nparams = f.params.len + if g.aggregate(f.typ) { 1 } else { 0 } + g.sret_local = if g.aggregate(f.typ) { f.params.len } else { -1 } + mut params := []u8{} + for i, param in f.params { + params << wt_valtype(g.wtype(g.m.values[param].typ)) + g.value_local[param] = i + if g.aggregate(g.m.values[param].typ) { + g.frame_size = (g.frame_size + 7) & ~7 + g.slots[param] = g.frame_size + g.frame_size += g.m.type_size(g.m.values[param].typ) + } + } + if g.sret_local >= 0 { params << valtype_i32 } + for bid in f.blocks { + for vid in g.m.blocks[bid].instrs { + instr := g.m.instrs[g.m.values[vid].index] + g.reserve_value(vid) + if instr.op == .phi && g.aggregate(instr.typ) { + g.frame_size = (g.frame_size + 7) & ~7 + g.phi_snapshots[vid] = g.frame_size + g.frame_size += g.m.type_size(instr.typ) + } + if instr.op == .assign && instr.operands.len > 0 { + g.reserve_value(instr.operands[0]) + } + if instr.op == .alloca { + elem := g.m.type_store.types[instr.typ].elem_type + mut count := 1 + if instr.operands.len > 0 { + value := g.m.values[instr.operands[0]] + if value.kind != .constant { + return error('wasm: dynamic alloca in `${f.name}`') + } + count = int(parse_int_literal(value.name)) + } + align := g.m.type_align(elem) + g.frame_size = (g.frame_size + align - 1) / align * align + g.slots[vid] = g.frame_size + g.frame_size += g.m.type_size(elem) * if count > 0 { count } else { 1 } + } + } + } + g.frame_size = (g.frame_size + 15) & ~15 + g.frame_local = g.temp(.i32) + g.pc_local = g.temp(.i32) + g.cur.global_get(g.stack_global) + g.cur.i32_const(g.frame_size) + g.cur.raw(0x6b) + g.cur.local_tee(g.frame_local) + g.cur.i32_const(g.stack_bottom) + g.cur.raw(0x49) + g.cur.if_void() + g.cur.raw(0x00) + g.cur.end() + g.cur.local_get(g.frame_local) + g.cur.global_set(g.stack_global) + for id, offset in g.slots { + if g.aggregate(g.m.values[id].typ) { + g.frame_addr(offset) + if id in f.params { + g.cur.local_get(g.value_local[id]) + g.cur.i32_const(g.m.type_size(g.m.values[id].typ)) + g.memory_copy() + g.frame_addr(offset) + } + g.cur.local_set(g.value_local[id]) + } + } + g.cur.i32_const(f.blocks[0]) + g.cur.local_set(g.pc_local) + g.cur.loop_void() + for bid in f.blocks { + g.cur.local_get(g.pc_local) + g.cur.i32_const(bid) + g.cur.raw(0x46) + g.cur.if_void() + for vid in g.m.blocks[bid].instrs { + g.emit_instruction(vid)! + } + g.cur.raw(0x00) // A block must end in an SSA terminator. + g.cur.end() + } + g.cur.raw(0x00) // Invalid dispatch target. + g.cur.end() + g.cur.raw(0x00) + g.cur.end() + mut results := []u8{} + if g.wtype(f.typ) != .void && !g.aggregate(f.typ) { results << wt_valtype(g.wtype(f.typ)) } + ti := g.mod.add_type(params, results) + g.mod.add_func(ti, g.locals, g.cur.bytes) +} + +fn (mut g SSAGen) value(id int) ! { + if id <= 0 || id >= g.m.values.len { return error('wasm: invalid SSA value ${id}') } + value := g.m.values[id] + match value.kind { + .constant { + match g.wtype(value.typ) { + .i64 { g.cur.i64_const(parse_int_literal(value.name)) } + .f32 { g.cur.f32_const(f32(value.name.f64())) } + .f64 { g.cur.f64_const(value.name.f64()) } + else { g.cur.i32_const(parse_int_literal(value.name)) } + } + g.narrow(value.typ) + } + .string_literal, .c_string_literal { g.cur.i32_const(g.strings[id]) } + .global { + addr := g.global_addr[value.name] or { return error('wasm: unknown global `${value.name}`') } + g.cur.i32_const(addr) + } + .func_ref { + index := g.fn_table[g.function_name(value)] or { return error('wasm: unknown function reference `${value.name}`') } + if g.wtype(value.typ) == .i64 { + g.cur.i64_const(index) + } else { + g.cur.i32_const(index) + } + } + else { + idx := g.value_local[id] or { return error('wasm: missing local for SSA value ${id}') } + g.cur.local_get(idx) + } + } +} + +fn (mut g SSAGen) value_as(id int, typ ssa.TypeID) ! { + g.value(id)! + from := g.wtype(g.m.values[id].typ) + to := g.wtype(typ) + signed := !g.m.type_store.types[g.m.values[id].typ].is_unsigned + g.convert(from, to, signed) +} + +fn (mut g SSAGen) convert(from WType, to WType, signed bool) { + mut runtime := &Gen{ cur: g.cur } + runtime.coerce(from, to, signed) + g.cur = runtime.cur +} + +fn (mut g SSAGen) narrow(typ ssa.TypeID) { + if typ <= 0 { return } + t := g.m.type_store.types[typ] + if t.kind != .int_t || t.width >= 32 { return } + if t.width == 1 { + g.cur.i32_const(1) + g.cur.raw(0x71) + } else { + mut runtime := &Gen{ cur: g.cur } + runtime.emit_narrow(t.width, t.is_unsigned) + g.cur = runtime.cur + } +} + +fn (mut g SSAGen) result(id int) { + g.narrow(g.m.values[id].typ) + g.cur.local_set(g.value_local[id]) +} + +fn (mut g SSAGen) memory_copy() { + g.cur.raw(0xfc) + g.cur.raw(0x0a) + g.cur.raw(0x00) + g.cur.raw(0x00) +} + +fn (mut g SSAGen) memory_fill() { + g.cur.raw(0xfc) + g.cur.raw(0x0b) + g.cur.raw(0x00) +} + +fn (mut g SSAGen) copy_value(dest int, src int) ! { + if g.aggregate(g.m.values[dest].typ) { + g.value(dest)! + g.copy_aggregate_source(src, g.m.values[dest].typ)! + } else { + g.value_as(src, g.m.values[dest].typ)! + g.result(dest) + } +} + +fn (mut g SSAGen) copy_aggregate_source(src int, typ ssa.TypeID) ! { + if g.m.values[src].kind == .constant { + g.cur.i32_const(0) + g.cur.i32_const(g.m.type_size(typ)) + g.memory_fill() + } else { + g.value(src)! + g.cur.i32_const(g.m.type_size(typ)) + g.memory_copy() + } +} + +fn (mut g SSAGen) edge(from int, to int, depth int) ! { + mut destinations := []int{} + mut temporaries := []int{} + for vid in g.m.blocks[to].instrs { + instr := g.m.instrs[g.m.values[vid].index] + if instr.op != .phi { continue } + for oi := 0; oi + 1 < instr.operands.len; oi += 2 { + if int(instr.operands[oi + 1]) != from { continue } + tmp := g.temp(g.wtype(instr.typ)) + if g.aggregate(instr.typ) { + g.frame_addr(g.phi_snapshots[vid]) + g.copy_aggregate_source(instr.operands[oi], instr.typ)! + g.frame_addr(g.phi_snapshots[vid]) + } else { + g.value_as(instr.operands[oi], instr.typ)! + } + g.cur.local_set(tmp) + destinations << vid + temporaries << tmp + } + } + for i, dest in destinations { + if g.aggregate(g.m.values[dest].typ) { + g.value(dest)! + g.cur.local_get(temporaries[i]) + g.cur.i32_const(g.m.type_size(g.m.values[dest].typ)) + g.memory_copy() + } else { + g.cur.local_get(temporaries[i]) + g.result(dest) + } + } + g.cur.i32_const(to) + g.cur.local_set(g.pc_local) + g.cur.br(depth) +} + +fn (mut g SSAGen) load_scalar(typ ssa.TypeID, offset int) { + t := g.m.type_store.types[typ] + if t.kind in [.ptr_t, .func_t] && g.m.target.ptr_size == 8 { + g.cur.load(0x29, 0, offset) + g.cur.raw(0xa7) + return + } + op := match g.wtype(typ) { + .i64 { u8(0x29) } + .f32 { u8(0x2a) } + .f64 { u8(0x2b) } + else { + if t.kind == .int_t && t.width <= 8 { + if t.is_unsigned || t.width == 1 { u8(0x2d) } else { u8(0x2c) } + } else if t.kind == .int_t && t.width <= 16 { + if t.is_unsigned { u8(0x2f) } else { u8(0x2e) } + } else { + u8(0x28) + } + } + } + g.cur.load(op, 0, offset) +} + +fn (mut g SSAGen) store_scalar(typ ssa.TypeID, offset int) { + t := g.m.type_store.types[typ] + if t.kind in [.ptr_t, .func_t] && g.m.target.ptr_size == 8 { + g.cur.raw(0xad) + g.cur.store(0x37, 0, offset) + return + } + op := match g.wtype(typ) { + .i64 { u8(0x37) } + .f32 { u8(0x38) } + .f64 { u8(0x39) } + else { + if t.kind == .int_t && t.width <= 8 { + u8(0x3a) + } else if t.kind == .int_t && t.width <= 16 { + u8(0x3b) + } else { + u8(0x36) + } + } + } + g.cur.store(op, 0, offset) +} + +fn (mut g SSAGen) emit_instruction(id int) ! { + instr := g.m.instrs[g.m.values[id].index] + ops := instr.operands + match instr.op { + .alloca { + g.frame_addr(g.slots[id]) + g.result(id) + // V aggregate and local storage starts zeroed. + g.value(id)! + g.cur.i32_const(0) + elem := g.m.type_store.types[instr.typ].elem_type + mut count := 1 + if ops.len > 0 { count = int(parse_int_literal(g.m.values[ops[0]].name)) } + g.cur.i32_const(g.m.type_size(elem) * if count > 0 { count } else { 1 }) + g.memory_fill() + } + .heap_alloc { + elem := g.m.type_store.types[instr.typ].elem_type + g.cur.i32_const(g.m.type_size(elem)) + g.cur.call(g.alloc_index) + g.result(id) + } + .get_element_ptr { + g.value(ops[0])! + g.value(ops[1])! + g.convert(g.wtype(g.m.values[ops[1]].typ), .i32, false) + g.cur.raw(0x6a) + g.result(id) + } + .load { + if g.aggregate(instr.typ) { + g.value(id)! + g.value(ops[0])! + g.cur.i32_const(g.m.type_size(instr.typ)) + g.memory_copy() + } else { + g.value(ops[0])! + g.load_scalar(instr.typ, 0) + g.result(id) + } + } + .store { + ptr_type := g.m.type_store.types[g.m.values[ops[1]].typ] + typ := if ptr_type.kind == .ptr_t { ptr_type.elem_type } else { g.m.values[ops[0]].typ } + g.value(ops[1])! + if g.aggregate(typ) { + if g.m.values[ops[0]].kind == .constant { + g.cur.i32_const(0) + g.cur.i32_const(g.m.type_size(typ)) + g.memory_fill() + } else { + g.value(ops[0])! + g.cur.i32_const(g.m.type_size(typ)) + g.memory_copy() + } + } else { + g.value_as(ops[0], typ)! + g.store_scalar(typ, 0) + } + } + .shl, .ashr, .lshr { + w := g.wtype(instr.typ) + count_w := g.wtype(g.m.values[ops[1]].typ) + count := g.temp(count_w) + g.value_as(ops[0], instr.typ)! + if instr.op == .lshr && w == .i32 { + lhs := g.m.type_store.types[g.m.values[ops[0]].typ] + if lhs.kind == .int_t && lhs.width > 0 && lhs.width < 32 { + g.cur.i32_const((i64(1) << lhs.width) - 1) + g.cur.raw(0x71) + } + } + g.value(ops[1])! + g.cur.local_tee(count) + g.convert(count_w, w, false) + g.cur.raw(ssa_wasm_binary(instr.op, w)!) + init_push_zero(w, mut g.cur) + g.cur.local_get(count) + width := if w == .i64 { 64 } else { 32 } + if count_w == .i64 { + g.cur.i64_const(width) + g.cur.raw(0x54) + } else { + g.cur.i32_const(width) + g.cur.raw(0x49) + } + g.cur.raw(0x1b) + g.result(id) + } + .add, .sub, .mul, .sdiv, .srem, .udiv, .urem, .and_, .or_, .xor, + .fadd, .fsub, .fmul, .fdiv { + g.value_as(ops[0], instr.typ)! + g.value_as(ops[1], instr.typ)! + g.cur.raw(ssa_wasm_binary(instr.op, g.wtype(instr.typ))!) + g.result(id) + } + .lt, .gt, .le, .ge, .ult, .ugt, .ule, .uge, .eq, .ne { + typ := g.m.values[ops[0]].typ + g.value(ops[0])! + g.value_as(ops[1], typ)! + g.cur.raw(ssa_wasm_compare(instr.op, g.wtype(typ))) + g.result(id) + } + .neg { + w := g.wtype(instr.typ) + if w in [.f32, .f64] { + g.value(ops[0])! + g.cur.raw(if w == .f32 { u8(0x8c) } else { u8(0x9a) }) + } else { + init_push_zero(w, mut g.cur) + g.value(ops[0])! + g.cur.raw(if w == .i64 { u8(0x7d) } else { u8(0x6b) }) + } + g.result(id) + } + .trunc, .sext, .zext, .fptoui, .fptosi, .uitofp, .sitofp, .bitcast { + if g.aggregate(instr.typ) { + g.copy_value(id, ops[0])! + return + } + g.value(ops[0])! + from := g.wtype(g.m.values[ops[0]].typ) + to := g.wtype(instr.typ) + if instr.op in [.zext, .sext] && from == .i32 { + source_width := g.m.type_store.types[g.m.values[ops[0]].typ].width + if source_width > 0 && source_width < 32 { + if instr.op == .zext || source_width == 1 { + g.cur.i32_const((i64(1) << source_width) - 1) + g.cur.raw(0x71) + } else if source_width == 8 { + g.cur.raw(0xc0) + } else if source_width == 16 { + g.cur.raw(0xc1) + } + } + } + float_width_cast := from in [.f32, .f64] && to in [.f32, .f64] + if instr.op == .bitcast && from != to && !float_width_cast && ((from in [ + .f32, + .f64, + ]) || (to in [ + .f32, + .f64, + ])) { + op := if from == .f32 && to == .i32 { + u8(0xbc) + } else if from == .f64 && to == .i64 { + u8(0xbd) + } else if from == .i32 && to == .f32 { + u8(0xbe) + } else if from == .i64 && to == .f64 { + u8(0xbf) + } else { + return error('wasm: incompatible bitcast ${from} to ${to}') + } + g.cur.raw(op) + } else { + source := g.m.type_store.types[g.m.values[ops[0]].typ] + signed := if instr.op == .bitcast { + source.kind !in [.ptr_t, .func_t] && !source.is_unsigned + } else { + instr.op !in [.zext, .fptoui, .uitofp] + } + g.convert(from, to, signed) + } + g.result(id) + } + .select { + if g.aggregate(instr.typ) { + g.value(ops[0])! + g.cur.if_void() + g.copy_value(id, ops[1])! + g.cur.else_() + g.copy_value(id, ops[2])! + g.cur.end() + return + } + g.value_as(ops[1], instr.typ)! + g.value_as(ops[2], instr.typ)! + g.value(ops[0])! + g.cur.raw(0x1b) + g.result(id) + } + .assign { g.copy_value(ops[0], ops[1])! } + .phi {} + .call, .call_indirect, .call_sret { g.emit_call(id, instr)! } + .ret { + if g.sret_local >= 0 { + g.cur.local_get(g.sret_local) + g.copy_aggregate_source(ops[0], g.cur_ret)! + } else if g.wtype(g.cur_ret) != .void { + g.value_as(ops[0], g.cur_ret)! + g.narrow(g.cur_ret) + } + g.cur.local_get(g.frame_local) + g.cur.i32_const(g.frame_size) + g.cur.raw(0x6a) + g.cur.global_set(g.stack_global) + g.cur.ret() + } + .jmp { g.edge(instr.block, ops[0], 1)! } + .br { + g.value(ops[0])! + g.cur.if_void() + g.edge(instr.block, ops[1], 2)! + g.cur.else_() + g.edge(instr.block, ops[2], 2)! + g.cur.end() + } + .switch_ { + for oi := 2; oi + 1 < ops.len; oi += 2 { + g.value(ops[0])! + g.value_as(ops[oi], g.m.values[ops[0]].typ)! + g.cur.raw(if g.wtype(g.m.values[ops[0]].typ) == .i64 { u8(0x51) } else { u8(0x46) }) + g.cur.if_void() + g.edge(instr.block, ops[oi + 1], 2)! + g.cur.end() + } + g.edge(instr.block, ops[1], 1)! + } + .unreachable { g.cur.raw(0x00) } + .fence {} + else { return error('wasm: unsupported SSA opcode `${instr.op}`') } + } +} + +fn (g &SSAGen) function_name(value ssa.Value) string { + if value.index >= 0 && value.index < g.m.funcs.len { + return g.m.funcs[value.index].name + } + return value.name +} + +fn (g &SSAGen) intrinsic_name(value ssa.Value) ?string { + if value.kind != .func_ref || value.index < 0 || value.index >= g.m.funcs.len || !g.m.funcs[value.index].is_c_extern { + return none + } + name := g.function_name(value).trim_string_left('C.') + if name in ['write', 'malloc', 'calloc', 'free', 'memcpy', 'memmove', 'memset', 'exit', 'abort'] { + return name + } + return none +} + +fn (mut g SSAGen) emit_call(id int, instr ssa.Instruction) ! { + ops := instr.operands + callee := g.m.values[ops[0]] + if instr.op != .call_indirect { + if name := g.intrinsic_name(callee) { + g.emit_intrinsic(id, instr, name)! + return + } + } + mut params := []u8{} + for oi in 1 .. ops.len { + mut typ := g.m.values[ops[oi]].typ + if instr.op != .call_indirect && callee.index >= 0 && callee.index < g.m.funcs.len { + f := g.m.funcs[callee.index] + if oi - 1 < f.params.len { typ = g.m.values[f.params[oi - 1]].typ } + } + g.value_as(ops[oi], typ)! + params << wt_valtype(g.wtype(typ)) + } + if g.aggregate(instr.typ) { + g.value(id)! + params << valtype_i32 + } + if instr.op == .call_indirect { + mut results := []u8{} + if g.wtype(instr.typ) != .void && !g.aggregate(instr.typ) { + results << wt_valtype(g.wtype(instr.typ)) + } + ti := g.mod.add_type(params, results) + g.value(ops[0])! + g.convert(g.wtype(callee.typ), .i32, false) + g.cur.raw(0x11) + leb_u(mut g.cur.bytes, u64(ti)) + g.cur.raw(0x00) + } else { + index := g.fn_index[g.function_name(callee)] or { return error('wasm: missing callee `${callee.name}`') } + g.cur.call(index) + } + if g.wtype(instr.typ) != .void && !g.aggregate(instr.typ) { g.result(id) } +} + +fn (mut g SSAGen) intrinsic_arg(id int) ! { + g.value(id)! + g.convert(g.wtype(g.m.values[id].typ), .i32, false) +} + +fn (mut g SSAGen) emit_intrinsic(id int, instr ssa.Instruction, name string) ! { + ops := instr.operands + match name { + 'write' { + g.intrinsic_arg(ops[2])! + g.intrinsic_arg(ops[3])! + g.intrinsic_arg(ops[1])! + g.cur.call(g.write_index) + if g.wtype(instr.typ) != .void { + g.value_as(ops[3], instr.typ)! + g.result(id) + } + } + 'malloc', 'calloc' { + g.intrinsic_arg(ops[1])! + if name == 'calloc' { + g.intrinsic_arg(ops[2])! + g.cur.raw(0x6c) + } + g.cur.call(g.alloc_index) + g.result(id) + } + 'free' {} + 'memcpy', 'memmove' { + g.intrinsic_arg(ops[1])! + g.intrinsic_arg(ops[2])! + g.intrinsic_arg(ops[3])! + g.memory_copy() + if g.wtype(instr.typ) != .void { + g.value_as(ops[1], instr.typ)! + g.result(id) + } + } + 'memset' { + g.intrinsic_arg(ops[1])! + g.intrinsic_arg(ops[2])! + g.intrinsic_arg(ops[3])! + g.memory_fill() + if g.wtype(instr.typ) != .void { + g.value_as(ops[1], instr.typ)! + g.result(id) + } + } + 'exit', 'abort' { + if name == 'exit' { + g.intrinsic_arg(ops[1])! + } else { + g.cur.i32_const(1) + } + g.cur.call(g.exit_index) + g.cur.raw(0x00) + } + else { return error('wasm: unsupported intrinsic `${name}`') } + } +} + +fn (mut g SSAGen) emit_allocator() { + mut c := Code{} + // malloc(size): align the bump pointer, grow memory, and zero the allocation. + c.global_get(g.heap_global) + c.local_set(1) + c.local_get(1) + c.local_get(0) + c.raw(0x6a) + c.i32_const(15) + c.raw(0x6a) + c.i32_const(-16) + c.raw(0x71) + c.local_tee(2) + c.local_get(1) + c.raw(0x49) + c.if_void() + c.raw(0x00) + c.end() + c.local_get(2) + c.i32_const(65535) + c.raw(0x6a) + c.i32_const(16) + c.raw(0x76) + c.raw(0x3f) + c.raw(0x00) + c.local_tee(3) + c.raw(0x4b) + c.if_void() + c.local_get(2) + c.i32_const(65535) + c.raw(0x6a) + c.i32_const(16) + c.raw(0x76) + c.local_get(3) + c.raw(0x6b) + c.raw(0x40) + c.raw(0x00) + c.i32_const(-1) + c.raw(0x46) + c.if_void() + c.raw(0x00) + c.end() + c.end() + c.local_get(2) + c.global_set(g.heap_global) + c.local_get(1) + c.i32_const(0) + c.local_get(0) + c.raw(0xfc) + c.raw(0x0b) + c.raw(0x00) + c.local_get(1) + c.end() + ti := g.mod.add_type([valtype_i32], [valtype_i32]) + g.mod.add_func(ti, [valtype_i32, valtype_i32, valtype_i32], c.bytes) +} + +fn ssa_wasm_binary(op ssa.OpCode, w WType) !u8 { + if w in [.f32, .f64] { + base := if w == .f32 { u8(0x92) } else { u8(0xa0) } + return match op { + .add, .fadd { base } + .sub, .fsub { base + 1 } + .mul, .fmul { base + 2 } + .sdiv, .udiv, .fdiv { base + 3 } + else { return error('wasm: unsupported floating operation `${op}`') } + } + } + base := if w == .i64 { u8(0x7c) } else { u8(0x6a) } + return match op { + .add { base } + .sub { base + 1 } + .mul { base + 2 } + .sdiv { base + 3 } + .udiv { base + 4 } + .srem { base + 5 } + .urem { base + 6 } + .and_ { base + 7 } + .or_ { base + 8 } + .xor { base + 9 } + .shl { base + 10 } + .ashr { base + 11 } + .lshr { base + 12 } + else { return error('wasm: unsupported integer operation `${op}`') } + } +} + +fn ssa_wasm_compare(op ssa.OpCode, w WType) u8 { + flat_op := match op { + .eq { flat.Op.eq } + .ne { flat.Op.ne } + .lt, .ult { flat.Op.lt } + .gt, .ugt { flat.Op.gt } + .le, .ule { flat.Op.le } + else { flat.Op.ge } + } + return cmp_op(flat_op, w, op !in [.ult, .ugt, .ule, .uge]) +} diff --git a/vlib/v/gen/wasm/ssa_gen_test.v b/vlib/v/gen/wasm/ssa_gen_test.v new file mode 100644 index 0000000000..d65ffa47c5 --- /dev/null +++ b/vlib/v/gen/wasm/ssa_gen_test.v @@ -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); +') + } +} diff --git a/vlib/v/help/build/build-wasm.txt b/vlib/v/help/build/build-wasm.txt index d7431b3ac9..dc120c968f 100644 --- a/vlib/v/help/build/build-wasm.txt +++ b/vlib/v/help/build/build-wasm.txt @@ -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'] [run options] diff --git a/vlib/v/slow_tests/inout/os.out b/vlib/v/slow_tests/inout/os.out index 8393a66779..3fecd081b8 100644 --- a/vlib/v/slow_tests/inout/os.out +++ b/vlib/v/slow_tests/inout/os.out @@ -15,3 +15,4 @@ tccbin_automation.md translated_fixed_array_c_abi.md upcoming.md vscode.md +wasm.md diff --git a/vlib/v/ssa/builder.v b/vlib/v/ssa/builder.v index 8dfd915d83..eedb61dc07 100644 --- a/vlib/v/ssa/builder.v +++ b/vlib/v/ssa/builder.v @@ -1,5 +1,6 @@ module ssa +import math import v.flat import v.token import v.types @@ -44,6 +45,9 @@ const arm64_force_external_syms = ['_malloc', '_free', '_calloc', '_realloc', '_ '_objc_getClass', '_sel_registerName', '_objc_alloc_init', '_objc_autoreleasePoolPush', '_objc_autoreleasePoolPop', '_MTLCreateSystemDefaultDevice', '_dlopen', '_dlsym'] +const wasm_bench_runtime_stub_names = ['bench.current_rss_kb', 'bench.macos_rss_kb', + 'bench.linux_rss_kb', 'v.bench.current_rss_kb', 'v.bench.macos_rss_kb', 'v.bench.linux_rss_kb'] + const bench_runtime_stub_names = ['current_rss_kb', 'macos_rss_kb', 'linux_rss_kb', 'bench.current_rss_kb', 'bench.macos_rss_kb', 'bench.linux_rss_kb', 'v.bench.current_rss_kb', 'v.bench.macos_rss_kb', 'v.bench.linux_rss_kb'] @@ -111,6 +115,13 @@ mut: loop_targets []LoopTargets top_level_main bool thread_stack_size int + source_fn_names map[string]bool + runtime_fn_ids map[string]int + runtime_fn_types map[string]TypeID + break_targets []BlockID + continue_targets []BlockID + break_labels map[string]BlockID + continue_labels map[string]BlockID // --- Build-control machinery (ported from v2 build_all) --- // When set, build_functions only materializes this one function body (hot reload). hot_fn string @@ -122,6 +133,10 @@ mut: // SSA-level type alias name -> resolved base TypeID. Supplements the checker's // alias map (used when building without a checker). type_aliases map[string]TypeID + // File-scoped names preserve distinct nested modules that share a leaf name. + source_modules map[string]string + source_imports map[string]map[string]string + exact_used_fns bool } // VarBinding represents var binding data used by ssa. @@ -131,6 +146,12 @@ struct VarBinding { typ string } +struct BuilderScope { + active bool + vars map[string]ValueID + type_names map[string]string +} + // IfGuardState stores if guard state state used by ssa. struct IfGuardState { active bool @@ -151,8 +172,12 @@ pub: skip_modules []string // skip all functions declared in these modules track_uses bool = true // build optimizer/verifier use lists thread_stack_size int = 8 * 1024 * 1024 // zero uses the pthread default - test_files []string // driver-selected test file paths; enables a test entrypoint - test_run_only []string // bare or qualified function-name globs + test_files []string // driver-selected test file paths; enables a test entrypoint + test_run_only []string // bare or qualified function-name globs + target TargetData // target memory layout, defaults to 64-bit pointers + source_modules map[string]string // source filename -> full module path + source_imports map[string]map[string]string // source filename -> import alias -> full module path + exact_used_fns bool // materialize only source functions listed in used_fns } // build supports build handling for ssa. @@ -201,8 +226,12 @@ pub fn build_with_options(a_ &flat.FlatAst, used_fns map[string]bool, tc &types. loop_targets: []LoopTargets{} skip_modules: map[string]bool{} type_aliases: map[string]TypeID{} + source_modules: opts.source_modules + source_imports: opts.source_imports + exact_used_fns: opts.exact_used_fns } b.m.track_uses = opts.track_uses + b.m.target = opts.target b.void_type = TypeID(0) b.i64_type = b.m.type_store.get_int(64) b.i32_type = b.m.type_store.get_int(32) @@ -217,9 +246,7 @@ pub fn build_with_options(a_ &flat.FlatAst, used_fns map[string]bool, tc &types. mut str_fields := []TypeID{} str_fields << b.m.type_store.get_ptr(b.i8_type) str_fields << b.i32_type - // `is_lit` occupies the trailing 4-byte slot (offset 12) that the struct is padded - // to anyway. It must be a named field so `s.is_lit` (e.g. in `string.free`) resolves - // to offset 12 instead of defaulting to offset 0 (which would read `str`). + // Keep `is_lit` named so string ownership checks resolve its target-specific offset. str_fields << b.i32_type mut str_field_names := []string{} str_field_names << 'str' @@ -257,9 +284,11 @@ pub fn build_with_options(a_ &flat.FlatAst, used_fns map[string]bool, tc &types. map_state_fields << b.i64_type map_state_fields << b.i64_type map_state_fields << b.i64_type - map_state_fields << ptr_i8 - map_state_fields << b.i64_type - map_state_fields << b.i64_type + if b.m.target.ptr_size == 8 { + map_state_fields << ptr_i8 + map_state_fields << b.i64_type + map_state_fields << b.i64_type + } mut map_state_field_names := []string{} map_state_field_names << 'keys' map_state_field_names << 'vals' @@ -267,9 +296,11 @@ pub fn build_with_options(a_ &flat.FlatAst, used_fns map[string]bool, tc &types. map_state_field_names << 'len' map_state_field_names << 'key_size' map_state_field_names << 'val_size' - map_state_field_names << 'hash_slots' - map_state_field_names << 'hash_cap' - map_state_field_names << 'indexed_len' + if b.m.target.ptr_size == 8 { + map_state_field_names << 'hash_slots' + map_state_field_names << 'hash_cap' + map_state_field_names << 'indexed_len' + } b.map_state_type = b.m.type_store.register(Type{ kind: .struct_t fields: map_state_fields @@ -299,19 +330,39 @@ pub fn build_with_options(a_ &flat.FlatAst, used_fns map[string]bool, tc &types. b.register_globals() b.global_vars = b.vars.clone() b.register_functions() - b.register_native_initializers() + if b.m.target.ptr_size == 8 { b.register_native_initializers() } b.build_functions() - b.build_native_test_main(opts.test_files, opts.test_run_only) - b.build_native_initialization() + if b.m.target.ptr_size == 8 { + b.build_native_test_main(opts.test_files, opts.test_run_only) + b.build_native_initialization() + } + if b.m.target.ptr_size == 4 { b.canonicalize_internal_function_refs() } return b.m } +// canonicalize_internal_function_refs keeps backend symbol lookup consistent with +// the selected SSA function, including runtime helpers renamed around source functions. +fn (mut b Builder) canonicalize_internal_function_refs() { + for i in 0 .. b.m.values.len { + value := b.m.values[i] + if value.kind != .func_ref || value.index < 0 || value.index >= b.m.funcs.len { + continue + } + f := b.m.funcs[value.index] + if !f.is_c_extern && value.name != f.name { + mut reference := value + reference.name = f.name + b.m.values[i] = reference + } + } +} + // register_types updates register types state for ssa. fn (mut b Builder) register_types() { mut cur_module := '' for node in b.a.nodes { if node.kind == .module_decl { - cur_module = node.value + cur_module = b.source_module_name(node) continue } if node.kind == .struct_decl { @@ -332,7 +383,7 @@ fn (mut b Builder) register_types() { cur_module = '' for node in b.a.nodes { if node.kind == .module_decl { - cur_module = node.value + cur_module = b.source_module_name(node) continue } if node.kind == .struct_decl { @@ -353,7 +404,7 @@ fn (mut b Builder) register_types() { b.struct_field_types[cur_module + '.' + short_name + '.' + f.value] = field_type_name } } - if abi := native_c_struct_abi(node.value) { + if abi := native_c_struct_abi(node.value, b.m.target.ptr_size) { field_names = abi.field_names.clone() field_types = []TypeID{cap: abi.field_types.len} for field_type in abi.field_types { @@ -405,7 +456,7 @@ fn (mut b Builder) register_types() { cur_module = '' for node in b.a.nodes { if node.kind == .module_decl { - cur_module = node.value + cur_module = b.source_module_name(node) continue } if node.kind != .type_decl || node.children_count == 0 { @@ -456,7 +507,15 @@ struct NativeCStructAbi { field_types []string } -fn native_c_struct_abi(struct_name string) ?NativeCStructAbi { +fn native_c_struct_abi(struct_name string, pointer_size int) ?NativeCStructAbi { + if struct_name == 'C.utsname' && pointer_size == 8 { + // Darwin stores five inline char[256] fields. The V declaration uses + // &char so selectors have C array-to-pointer semantics. + return NativeCStructAbi{ + field_names: ['sysname', 'nodename', 'release', 'version', 'machine'] + field_types: ['[256]char', '[256]char', '[256]char', '[256]char', '[256]char'] + } + } if struct_name == 'C.pthread_mutex_t' { return NativeCStructAbi{ field_names: ['opaque'] @@ -622,6 +681,9 @@ fn (mut b Builder) ssa_type_from_checker_type(typ types.Type) TypeID { return b.i32_type } if typ is types.ISize || typ is types.USize { + if b.m.target.ptr_size == 4 { + return if typ is types.USize { b.u32_type } else { b.i32_type } + } return b.i64_type } if typ is types.Primitive { @@ -853,10 +915,15 @@ fn (b &Builder) struct_type_id_for_decl(name string, module_name string) TypeID // register_consts updates register consts state for ssa. fn (mut b Builder) register_consts() { - mut cur_module := '' + mut cur_module := 'main' for node in b.a.nodes { + if node.kind == .file { + // A script can omit its module declaration after imported module files. + cur_module = 'main' + continue + } if node.kind == .module_decl { - cur_module = node.value + cur_module = b.source_module_name(node) continue } if node.kind == .const_decl { @@ -889,59 +956,82 @@ fn (mut b Builder) register_globals() { for node in b.a.nodes { if node.kind == .file { cur_module = 'main' - } else if node.kind == .module_decl { - cur_module = node.value - } else if node.kind == .import_decl { + continue + } + if node.kind == .module_decl { + cur_module = b.source_module_name(node) + continue + } + if node.kind == .import_decl { alias := if node.typ.len > 0 { node.typ } else { node.value.all_after_last('.') } b.global_imports[cur_module + '#' + alias] = node.value + continue } if node.kind == .global_decl { for i in 0 .. node.children_count { f := b.a.child_node(&node, i) - type_name := if f.typ.len > 0 { - f.typ - } else if f.children_count > 0 { - expr_id := b.a.child(f, 0) - checked := b.checked_expr_type_name(expr_id) - expr := b.a.node(expr_id) - if checked.len > 0 { - checked - } else if expr.kind == .struct_init || expr.kind == .array_init { - expr.value - } else if expr.kind == .array_literal { - '[]int' - } else if expr.kind == .map_init { - 'map[string]int' + if b.m.target.ptr_size == 4 { + mut typ := b.resolve_type(f.typ) + if b.m.target.ptr_size == 4 && f.typ.len == 0 && f.children_count > 0 { + typ = b.global_constant_type(b.a.child(f, 0)) + } + name := if b.source_modules.len > 0 { + ssa_fn_name_in_module(cur_module, f.value) } else { - b.infer_v_type(expr_id) + f.value + } + addr := b.m.add_global(name, typ) + b.vars[name] = addr + if b.m.target.ptr_size == 4 && f.children_count > 0 { + b.cur_module = cur_module + b.initialize_global(b.m.values[addr].index, b.a.child(f, 0)) } } else { - 'int' - } - b.cur_module = cur_module - name := native_global_name(cur_module, f.value) - typ := match name { - 'g_main_argc' { b.i64_type } - 'g_main_argv' { b.m.type_store.get_ptr(b.m.type_store.get_ptr(b.i8_type)) } - else { b.resolve_struct_storage_type(type_name, cur_module) } - } - if f.value.starts_with('C.') { - if f.value == 'C.errno' { + type_name := if f.typ.len > 0 { + f.typ + } else if f.children_count > 0 { + expr_id := b.a.child(f, 0) + checked := b.checked_expr_type_name(expr_id) + expr := b.a.node(expr_id) + if checked.len > 0 { + checked + } else if expr.kind == .struct_init || expr.kind == .array_init { + expr.value + } else if expr.kind == .array_literal { + '[]int' + } else if expr.kind == .map_init { + 'map[string]int' + } else { + b.infer_v_type(expr_id) + } + } else { + 'int' + } + b.cur_module = cur_module + name := native_global_name(cur_module, f.value) + typ := match name { + 'g_main_argc' { b.i64_type } + 'g_main_argv' { b.m.type_store.get_ptr(b.m.type_store.get_ptr(b.i8_type)) } + else { b.resolve_struct_storage_type(type_name, cur_module) } + } + if f.value.starts_with('C.') { + if f.value == 'C.errno' { + continue + } + symbol := match f.value { + 'C.stdin' { '__stdinp' } + 'C.stdout' { '__stdoutp' } + 'C.stderr' { '__stderrp' } + else { f.value.all_after('C.') } + } + b.vars[f.value] = b.m.add_external_global(symbol, typ) + b.global_type_names[f.value] = type_name continue } - symbol := match f.value { - 'C.stdin' { '__stdinp' } - 'C.stdout' { '__stdoutp' } - 'C.stderr' { '__stderrp' } - else { f.value.all_after('C.') } - } - b.vars[f.value] = b.m.add_external_global(symbol, typ) - b.global_type_names[f.value] = type_name - continue + b.vars[name] = b.m.add_global(ssa_c_name(name), typ) + b.global_type_names[name] = type_name + b.global_aliases[cur_module + '.' + f.value] = name } - b.vars[name] = b.m.add_global(ssa_c_name(name), typ) - b.global_type_names[name] = type_name - b.global_aliases[cur_module + '.' + f.value] = name } } } @@ -950,6 +1040,248 @@ fn (mut b Builder) register_globals() { b.m.type_store.get_ptr(b.m.type_store.get_ptr(b.i8_type))) } +fn (mut b Builder) initialize_global(index int, id flat.NodeId) { + typ_id := b.m.globals[index].typ + if typ_id <= 0 || typ_id >= b.m.type_store.types.len { + return + } + typ := b.m.type_store.types[typ_id] + if typ.kind == .float_t { + value := b.global_constant_float(id, 0) + bits := if typ.width == 32 { u64(math.f32_bits(f32(value))) } else { math.f64_bits(value) } + mut data := []u8{len: b.m.type_size(typ_id)} + for i in 0 .. data.len { + data[i] = u8(bits >> (i * 8)) + } + b.m.globals[index].initial_data = data + } else if typ.kind == .int_t { + b.m.globals[index].initial_value = b.narrow_global_constant(b.global_constant_int(id, + 0), typ_id) + } +} + +fn (mut b Builder) global_constant_type(id flat.NodeId) TypeID { + if b.tc != unsafe { nil } { + return b.ssa_type_from_checker_type(b.tc.resolve_type(id)) + } + node := b.a.nodes[int(id)] + if node.kind == .cast_expr { + return b.resolve_type(node.value) + } + if node.kind == .float_literal { + return b.f64_type + } + if node.kind == .bool_literal { + return b.i1_type + } + return b.i32_type +} + +fn (b &Builder) narrow_global_constant(value i64, typ_id TypeID) i64 { + if typ_id <= 0 || typ_id >= b.m.type_store.types.len { + return value + } + typ := b.m.type_store.types[typ_id] + if typ.kind != .int_t || typ.width <= 1 || typ.width >= 64 { + return value + } + mask := (u64(1) << typ.width) - 1 + bits := u64(value) & mask + if !typ.is_unsigned && bits & (u64(1) << (typ.width - 1)) != 0 { + return i64(bits | ~mask) + } + return i64(bits) +} + +fn (b &Builder) global_constant_expr(node flat.Node) ?flat.NodeId { + if node.kind == .ident { + return b.lookup_const_expr(node.value) + } + if name := b.source_selector_name(node) { + return b.lookup_const_expr(name) + } + return none +} + +fn (mut b Builder) global_constant_int(id flat.NodeId, depth int) i64 { + if !b.valid_node_id(id) || depth > 64 { + return 0 + } + node := b.a.nodes[int(id)] + if expr := b.global_constant_expr(node) { + return b.global_constant_int(expr, depth + 1) + } + match node.kind { + .int_literal { + return global_integer_literal(node.value) + } + .float_literal { + return i64(node.value.f64()) + } + .bool_literal { + return i64(int(node.value == 'true')) + } + .char_literal { + return i64(char_literal_value(node.value)) + } + .paren { + return b.global_constant_int(b.a.child(&node, 0), depth + 1) + } + .cast_expr { + target := b.resolve_type(node.value) + if b.m.type_store.types[target].kind == .float_t { + return i64(b.global_constant_float(id, depth + 1)) + } + value := b.global_constant_int(b.a.child(&node, 0), depth + 1) + return b.narrow_global_constant(value, target) + } + .prefix { + value := b.global_constant_int(b.a.child(&node, 0), depth + 1) + return match node.op { + .minus { -value } + .bit_not { ~value } + .not { i64(int(value == 0)) } + else { value } + } + } + .infix { + expr_type := b.global_constant_type(id) + if b.m.type_store.types[expr_type].kind == .float_t { + return i64(b.global_constant_float(id, depth + 1)) + } + left_id := b.a.child(&node, 0) + right_id := b.a.child(&node, 1) + left := b.global_constant_int(left_id, depth + 1) + right := b.global_constant_int(right_id, depth + 1) + left_type := b.global_constant_type(left_id) + right_type := b.global_constant_type(right_id) + unsigned := b.is_unsigned_type(left_type) || b.is_unsigned_type(right_type) + if node.op in [.left_shift, .right_shift, .right_shift_unsigned] { + width := int_max(32, b.scalar_type_width(left_type)) + if u64(right) >= u64(width) { + return 0 + } + mut bits := u64(left) + if node.op == .right_shift_unsigned && b.scalar_type_width(left_type) < 32 { + bits &= (u64(1) << b.scalar_type_width(left_type)) - 1 + } + shifted := if node.op == .left_shift { + i64(bits << u64(right)) + } else if node.op == .right_shift_unsigned || b.is_unsigned_type(left_type) { + i64(bits >> u64(right)) + } else { + left >> u64(right) + } + return b.narrow_global_constant(shifted, expr_type) + } + value := match node.op { + .plus { left + right } + .minus { left - right } + .mul { left * right } + .div { + if right == 0 { + i64(0) + } else if unsigned { + i64(u64(left) / u64(right)) + } else { + left / right + } + } + .mod { + if right == 0 { + i64(0) + } else if unsigned { + i64(u64(left) % u64(right)) + } else { + left % right + } + } + .amp { left & right } + .pipe { left | right } + .xor { left ^ right } + .eq { i64(int(left == right)) } + .ne { i64(int(left != right)) } + .lt { i64(int(if unsigned { u64(left) < u64(right) } else { left < right })) } + .le { i64(int(if unsigned { u64(left) <= u64(right) } else { left <= right })) } + .gt { i64(int(if unsigned { u64(left) > u64(right) } else { left > right })) } + .ge { i64(int(if unsigned { u64(left) >= u64(right) } else { left >= right })) } + else { i64(0) } + } + return b.narrow_global_constant(value, expr_type) + } + else { return 0 } + } +} + +fn (mut b Builder) global_constant_float(id flat.NodeId, depth int) f64 { + if !b.valid_node_id(id) || depth > 64 { + return 0 + } + node := b.a.nodes[int(id)] + if expr := b.global_constant_expr(node) { + return b.global_constant_float(expr, depth + 1) + } + match node.kind { + .float_literal { return node.value.f64() } + .int_literal { return f64(u64(global_integer_literal(node.value))) } + .paren { return b.global_constant_float(b.a.child(&node, 0), depth + 1) } + .prefix { + value := b.global_constant_float(b.a.child(&node, 0), depth + 1) + return if node.op == .minus { -value } else { value } + } + .cast_expr { + target := b.resolve_type(node.value) + if b.m.type_store.types[target].kind != .float_t { + return f64(b.global_constant_int(id, depth + 1)) + } + value := b.global_constant_float(b.a.child(&node, 0), depth + 1) + return if target == b.f32_type { f64(f32(value)) } else { value } + } + .infix { + left := b.global_constant_float(b.a.child(&node, 0), depth + 1) + right := b.global_constant_float(b.a.child(&node, 1), depth + 1) + return match node.op { + .plus { left + right } + .minus { left - right } + .mul { left * right } + .div { + if right != 0 { left / right } else { f64(0) } + } + else { f64(0) } + } + } + else { return f64(b.global_constant_int(id, depth + 1)) } + } +} + +fn global_integer_literal(value string) i64 { + clean := value.replace('_', '') + mut digits := clean + mut base := u64(10) + if clean.starts_with('0x') || clean.starts_with('0X') { + digits = clean[2..] + base = 16 + } else if clean.starts_with('0b') || clean.starts_with('0B') { + digits = clean[2..] + base = 2 + } else if clean.starts_with('0o') || clean.starts_with('0O') { + digits = clean[2..] + base = 8 + } + mut result := u64(0) + for ch in digits { + digit := if ch >= `0` && ch <= `9` { + u64(ch - `0`) + } else if ch >= `a` && ch <= `f` { + u64(ch - `a` + 10) + } else { + u64(ch - `A` + 10) + } + result = result * base + digit + } + return i64(result) +} + // ensure_runtime_global supports ensure runtime global handling for Builder. fn (mut b Builder) ensure_runtime_global(name string, typ TypeID) { if name in b.vars { @@ -1202,10 +1534,12 @@ fn (mut b Builder) register_functions() { p2 << b.i64_type p2 << ptr_i8 b.register_extern('clock_gettime', b.i64_type, p2) - b.register_basic_format_stubs() - b.register_string_plus_stubs() - b.register_native_string_format_helpers() - b.register_bench_runtime_stubs() + if b.m.target.ptr_size == 8 { + b.register_basic_format_stubs() + b.register_string_plus_stubs() + b.register_native_string_format_helpers() + b.register_bench_runtime_stubs() + } p1 = []TypeID{} p1 << ptr_i8 b.register_extern('free', b.void_type, p1) @@ -1245,8 +1579,12 @@ fn (mut b Builder) register_functions() { mut cur_module := '' for node in b.a.nodes { + if node.kind == .file { + cur_module = '' + continue + } if node.kind == .module_decl { - cur_module = node.value + cur_module = b.source_module_name(node) continue } if node.kind == .fn_decl { @@ -1254,7 +1592,7 @@ fn (mut b Builder) register_functions() { continue } fn_name := ssa_fn_name_in_module(cur_module, node.value) - if b.used_fns.len > 0 && !b.source_fn_is_used(node.value, cur_module) { + if (b.exact_used_fns || b.used_fns.len > 0) && !b.source_fn_is_used(node.value, cur_module) { continue } ret_type := b.checker_return_type(node.value, cur_module) or { @@ -1278,9 +1616,19 @@ fn (mut b Builder) register_functions() { b.fn_types[fn_name] = fn_type func_id := b.m.new_function(fn_name, ret_type) b.fn_ids[fn_name] = func_id + b.source_fn_names[fn_name] = true + if cur_module == 'builtin' { + b.runtime_fn_ids[fn_name] = func_id + b.runtime_fn_types[fn_name] = fn_type + } } } b.register_top_level_main() + if b.m.target.ptr_size == 4 { + b.register_basic_format_stubs() + b.register_string_plus_stubs() + b.register_bench_runtime_stubs() + } b.register_wyhash_stubs() b.register_string_eq_stub() b.register_fast_string_eq_stub() @@ -1295,10 +1643,10 @@ fn (mut b Builder) register_functions() { b.register_string_builder_stubs() b.register_path_runtime_stubs() b.register_map_runtime_stubs() - b.register_native_map_format_helpers() + if b.m.target.ptr_size == 8 { b.register_native_map_format_helpers() } b.register_u8_runtime_stubs() b.register_heap_tracking_stubs() - b.register_at_exit_runtime() + if b.m.target.ptr_size == 4 { b.register_at_exit_stub() } else { b.register_at_exit_runtime() } b.register_rand_prng_interface_stubs() b.register_embed_file_interface_stubs() b.register_pthread_compat_stubs() @@ -1315,11 +1663,11 @@ fn (mut b Builder) register_functions() { b.register_fixed_array_contains_stubs() b.register_array_contains_stubs() b.register_enum_autostr_fns() - b.register_native_runtime_helpers() - b.register_native_allocation_helpers() - b.register_native_header_helpers() - b.register_native_signal_helpers() - b.register_native_worker_helpers() + if b.m.target.ptr_size == 8 { b.register_native_runtime_helpers() } + if b.m.target.ptr_size == 8 { b.register_native_allocation_helpers() } + if b.m.target.ptr_size == 8 { b.register_native_header_helpers() } + if b.m.target.ptr_size == 8 { b.register_native_signal_helpers() } + if b.m.target.ptr_size == 8 { b.register_native_worker_helpers() } } // register_enum_autostr_fns synthesizes a `__autostr` helper per enum decl, @@ -1335,7 +1683,7 @@ fn (mut b Builder) register_enum_autostr_fns() { cur_module = '' } .module_decl { - cur_module = node.value + cur_module = b.source_module_name(node) } .enum_decl { qualified := if cur_module.len > 0 && cur_module != 'main' @@ -1367,7 +1715,7 @@ fn (mut b Builder) register_enum_autostr_fns() { } mut p1 := []TypeID{} p1 << b.i64_type - fid := b.register_synthetic_function(fn_name, b.str_type, p1) + fid := b.register_runtime_function(fn_name, b.str_type, p1) or { continue } if is_flag { // `@[flag]` enums stringify as `Name{.a | .b}` (and `Name{}` for 0), // matching the C backend, rather than a single field name. @@ -1507,7 +1855,7 @@ fn (mut b Builder) generate_enum_autostr_body(func_id int, field_names []string, b.block_instr1(.ret, match_block, b.void_type, name_val) cur_block = next_block } - int_str_ref := b.m.add_value(.func_ref, b.str_type, 'int_str', b.fn_ids['int_str']) + int_str_ref := b.m.add_value(.func_ref, b.str_type, 'int_str', b.runtime_fn_id('int_str')) default_val := b.block_instr2(.call, cur_block, b.str_type, int_str_ref, it) b.block_instr1(.ret, cur_block, b.void_type, default_val) } @@ -1531,7 +1879,7 @@ fn (mut b Builder) generate_flag_enum_autostr_body(func_id int, short_name strin true_const := b.m.get_or_add_const(b.i1_type, '1') b.block_instr2(.store, entry, b.void_type, true_const, first_slot) false_const := b.m.get_or_add_const(b.i1_type, '0') - plus_ref := b.m.add_value(.func_ref, b.str_type, 'string__plus', b.fn_ids['string__plus']) + plus_ref := b.m.add_value(.func_ref, b.str_type, 'string__plus', b.runtime_fn_id('string__plus')) mut cur_block := entry mut seen := map[int]bool{} @@ -1587,6 +1935,34 @@ fn ssa_fn_name_in_module(module_name string, name string) string { return name } +fn (b &Builder) source_module_name(node flat.Node) string { + if file := b.a.source_files[node.pos.id] { + if name := b.source_modules[file.name] { + if name.len > 0 { + return name + } + } + } + return node.value +} + +fn (b &Builder) source_import_name(node flat.Node, alias string) ?string { + file := b.a.source_files[node.pos.id] or { return none } + return b.source_imports[file.name][alias] or { return none } +} + +fn (b &Builder) source_selector_name(node flat.Node) ?string { + if node.kind != .selector || node.children_count == 0 { + return none + } + base := b.a.child_node(&node, 0) + if base.kind != .ident || base.value in b.vars { + return none + } + module_name := b.source_import_name(node, base.value) or { return none } + return ssa_fn_name_in_module(module_name, node.value) +} + // register_extern updates register extern state for ssa. fn (mut b Builder) register_extern(name string, ret TypeID, params []TypeID) { fn_type := b.m.type_store.register(Type{ @@ -1601,6 +1977,8 @@ fn (mut b Builder) register_extern(name string, ret TypeID, params []TypeID) { b.fn_types[extern_name] = fn_type b.c_fn_types[name] = fn_type b.c_fn_ids[name] = func_id + b.runtime_fn_types[name] = fn_type + b.runtime_fn_ids[name] = func_id mut f := b.m.funcs[func_id] f.is_c_extern = true f.is_prototype = true @@ -1636,8 +2014,17 @@ fn (mut b Builder) register_runtime_extern(name string, ret TypeID, params []Typ // has_fn_decl reports whether has fn decl applies in ssa. fn (b &Builder) has_fn_decl(name string) bool { + mut module_name := '' for node in b.a.nodes { - if node.kind == .fn_decl && node.value == name { + if node.kind == .file { + module_name = '' + } else if node.kind == .module_decl { + module_name = b.source_module_name(node) + } else if node.kind == .fn_decl + && ssa_fn_name_in_module(module_name, node.value) == name + && !b.skip_source_fn_in_module(node.value, module_name) + && (!(b.exact_used_fns || b.used_fns.len > 0) + || b.source_fn_is_used(node.value, module_name)) { return true } } @@ -1646,9 +2033,6 @@ fn (b &Builder) has_fn_decl(name string) bool { // skip_source_fn supports skip source fn handling for Builder. fn (b &Builder) skip_source_fn(name string) bool { - if name in bench_runtime_stub_names { - return true - } if name in ['_wymix', 'wyhash', 'wyhash64', 'string__eq', 'string__lt', 'array_new', 'array_get', 'string__plus', 'string_plus_many', 'string.trim_right', 'array_push', 'array_push_many', 'array.push_many', 'array_clone', 'panic', 'panic_debug', 'fast_string_eq', @@ -1690,23 +2074,51 @@ fn (b &Builder) skip_source_fn(name string) bool { // skip_source_fn_in_module supports skip source fn in module handling for Builder. fn (b &Builder) skip_source_fn_in_module(name string, module_name string) bool { - if module_name == 'builtin' && name in b.c_fn_ids { + if module_name in ['bench', 'v.bench'] + && ssa_fn_name_in_module(module_name, name) in bench_runtime_stub_names { return true } if module_name == 'builtin' && name.starts_with('VMapData.') { // The native map stubs replace the C runtime's copy-on-write implementation. return true } - if module_name == 'ast' && name in ['Expr.name', 'SelectorExpr.name', '[]Expr.name_list'] { + if module_name == 'builtin' { + return name in b.c_fn_ids || b.skip_source_fn(name) + || name in ['int_str', 'bool_str', 'string.int', 'string__int', 'array_slice', + 'array__clone', 'array_push_many_ptr', 'IError.msg', 'IError.code', 'Array_str', + 'array_index_string', 'array_index_int', 'array_contains_string', 'array_contains_int', + 'fixed_array_contains_string', 'fixed_array_contains_int', 'v3_string_pad', + 'v3_char_string', 'v3_f64_fixed', 'v3_int_zpad', 'v3_i64_zpad', 'v3_u64_zpad'] + || name.starts_with('v3_array_sort_') + } + if module_name in ['ast', 'v.ast'] + && name in ['Expr.name', 'SelectorExpr.name', '[]Expr.name_list'] { return true } - if module_name == 'c' && name.starts_with('Gen.') { + if module_name in ['c', 'v.gen.c'] && name.starts_with('Gen.') { return true } - if module_name == 'os' && name in ['is_dir', 'is_link', 'ls'] { - return true + if module_name == 'strings' { + return b.skip_source_fn(ssa_fn_name_in_module(module_name, name)) + || name.starts_with('Builder.') && b.skip_source_fn(name) + || name == 'Builder.push_many' } - return b.skip_source_fn(name) + if module_name == 'os' { + return name in ['is_dir', 'is_link', 'ls', 'normalize_path_in_builder', 'check_fwrite', + 'check_fread', 'join_path_single'] + } + if module_name == 'strconv' { + return name in ['f32_to_str_l', 'f32_to_str_l_with_dot', 'f64_to_str_l', 'f64_to_str_l_with_dot', + 'fxx_to_str_l_parse', 'fxx_to_str_l_parse_with_dot'] + } + if module_name == 'rand' { + return name == 'new_default' || name in ['PRNG.seed', 'PRNG.free', 'PRNG.u8', 'PRNG.u16', + 'PRNG.u32', 'PRNG.u64', 'PRNG.block_size'] + } + if module_name == 'embed_file' { + return name == 'Decoder.decompress' + } + return false } // register_top_level_main updates register top level main state for ssa. @@ -1768,10 +2180,12 @@ fn (mut b Builder) register_wyhash_stubs() { mut p2 := []TypeID{} p2 << b.i64_type p2 << b.i64_type - wymix_id := b.register_synthetic_function('_wymix', b.i64_type, p2) - b.generate_wymix_body(wymix_id) - wyhash64_id := b.register_synthetic_function('wyhash64', b.i64_type, p2) - b.generate_wyhash64_body(wyhash64_id) + if wymix_id := b.register_runtime_c_function('_wymix', b.i64_type, p2) { + b.generate_wymix_body(wymix_id) + } + if wyhash64_id := b.register_runtime_c_function('wyhash64', b.i64_type, p2) { + b.generate_wyhash64_body(wyhash64_id) + } ptr_u8 := b.m.type_store.get_ptr(b.i8_type) ptr_u64 := b.m.type_store.get_ptr(b.u64_type) @@ -1780,20 +2194,38 @@ fn (mut b Builder) register_wyhash_stubs() { p4 << b.i64_type p4 << b.i64_type p4 << ptr_u64 - wyhash_id := b.register_synthetic_function('wyhash', b.i64_type, p4) - b.generate_wyhash_body(wyhash_id) + if wyhash_id := b.register_runtime_c_function('wyhash', b.i64_type, p4) { + b.generate_wyhash_body(wyhash_id) + } } -// register_synthetic_function updates register synthetic function state for ssa. -fn (mut b Builder) register_synthetic_function(name string, ret TypeID, params []TypeID) int { +// register_runtime_function keeps Wasm source and runtime names independent. +fn (mut b Builder) register_runtime_function(name string, ret TypeID, params []TypeID) ?int { + if b.m.target.ptr_size == 8 { return b.register_synthetic_function(name, ret, params) } + if id := b.runtime_fn_ids[name] { + if !b.m.funcs[id].is_c_extern && b.m.funcs[id].blocks.len > 0 { + return none + } + } + // Preserve selected source signatures and bodies while keeping an independent + // implementation for compiler-generated runtime calls with the same spelling. + mut synthetic_name := name + if b.source_fn_names[name] { + synthetic_name = '__ssa_runtime_${name}' + for (synthetic_name in b.fn_ids) { + synthetic_name += '_' + } + } fn_type := b.m.type_store.register(Type{ kind: .func_t ret_type: ret params: params }) - b.fn_types[name] = fn_type - func_id := b.m.new_function(name, ret) - b.fn_ids[name] = func_id + b.fn_types[synthetic_name] = fn_type + func_id := b.m.new_function(synthetic_name, ret) + b.fn_ids[synthetic_name] = func_id + b.runtime_fn_ids[name] = func_id + b.runtime_fn_types[name] = fn_type mut f := b.m.funcs[func_id] f.typ = ret f.blocks = []BlockID{} @@ -1803,17 +2235,38 @@ fn (mut b Builder) register_synthetic_function(name string, ret TypeID, params [ return func_id } -fn (mut b Builder) register_synthetic_c_function(name string, ret TypeID, params []TypeID) int { - func_id := b.register_synthetic_function(name, ret, params) - fn_type := b.fn_types[name] +fn (mut b Builder) register_runtime_c_function(name string, ret TypeID, params []TypeID) ?int { + if b.m.target.ptr_size == 8 { return b.register_synthetic_c_function(name, ret, params) } + // Keep the C implementation separate when a V function owns the bare name. + synthetic_name := if b.source_fn_names[name] { 'C.${name}' } else { name } + func_id := b.register_runtime_function(synthetic_name, ret, params)? + fn_type := b.runtime_fn_types[synthetic_name] extern_name := 'C.${name}' b.fn_ids[extern_name] = func_id b.fn_types[extern_name] = fn_type b.c_fn_ids[name] = func_id b.c_fn_types[name] = fn_type + b.runtime_fn_ids[name] = func_id + b.runtime_fn_types[name] = fn_type return func_id } +// runtime_fn_lookup resolves compiler-generated calls independently of source names. +fn (b &Builder) runtime_fn_lookup(name string) ?int { + if b.m.target.ptr_size == 8 { return b.fn_ids[name] or { none } } + if id := b.runtime_fn_ids[name] { + return id + } + if id := b.c_fn_ids[name] { + return id + } + return b.fn_ids[name] or { none } +} + +fn (b &Builder) runtime_fn_id(name string) int { + return b.runtime_fn_lookup(name) or { 0 } +} + // func_add_argument supports func add argument handling for Builder. fn (mut b Builder) func_add_argument(func_id int, typ TypeID, name string) ValueID { mut f := b.m.funcs[func_id] @@ -1839,7 +2292,7 @@ fn (mut b Builder) block_instr1(op OpCode, block_id BlockID, typ TypeID, a Value // block_instr2 supports block instr2 handling for Builder. fn (mut b Builder) block_instr2(op OpCode, block_id BlockID, typ TypeID, a ValueID, c ValueID) ValueID { mut ops := []ValueID{} - ops << a + ops << if op == .store { b.coerce_scalar_store_value(a, c, block_id) } else { a } ops << c return b.m.add_instr(op, block_id, typ, ops) } @@ -1865,48 +2318,50 @@ fn (mut b Builder) block_instr4(op OpCode, block_id BlockID, typ TypeID, a Value // block_struct_field_ptr supports block struct field ptr handling for Builder. fn (mut b Builder) block_struct_field_ptr(block_id BlockID, base_addr ValueID, typ_id TypeID, field_idx int) ValueID { - mut builtin_field_type := TypeID(0) - mut builtin_offset := 0 - mut has_builtin_layout := false - if typ_id == b.str_type { - has_builtin_layout = true - if field_idx == 0 { - builtin_field_type = b.m.type_store.get_ptr(b.i8_type) - builtin_offset = 0 - } else if field_idx == 1 { - builtin_field_type = b.i32_type - builtin_offset = 8 - } else if field_idx == 2 { - // is_lit (i32) shares the trailing word with len; it sits at offset 12, - // matching `struct_field_offset` used by the field readers. - builtin_field_type = b.i32_type - builtin_offset = 12 - } else { - builtin_field_type = b.i64_type + if b.m.target.ptr_size == 8 { + mut builtin_field_type := TypeID(0) + mut builtin_offset := 0 + mut has_builtin_layout := false + if typ_id == b.str_type { + has_builtin_layout = true + if field_idx == 0 { + builtin_field_type = b.m.type_store.get_ptr(b.i8_type) + builtin_offset = 0 + } else if field_idx == 1 { + builtin_field_type = b.i32_type + builtin_offset = 8 + } else if field_idx == 2 { + // is_lit (i32) shares the trailing word with len; it sits at offset 12, + // matching `struct_field_offset` used by the field readers. + builtin_field_type = b.i32_type + builtin_offset = 12 + } else { + builtin_field_type = b.i64_type + builtin_offset = field_idx * 8 + } + } else if typ_id == b.array_type { + has_builtin_layout = true + if field_idx == 0 { + builtin_field_type = b.m.type_store.get_ptr(b.i8_type) + builtin_offset = 0 + } else { + builtin_field_type = b.i32_type + builtin_offset = 8 + (field_idx - 1) * 4 + } + } else if typ_id == b.map_state_type { + has_builtin_layout = true + builtin_field_type = b.m.type_store.types[typ_id].fields[field_idx] builtin_offset = field_idx * 8 - } - } else if typ_id == b.array_type { - has_builtin_layout = true - if field_idx == 0 { - builtin_field_type = b.m.type_store.get_ptr(b.i8_type) + } else if typ_id == b.map_type { + has_builtin_layout = true + builtin_field_type = b.m.type_store.get_ptr(b.map_state_type) builtin_offset = 0 - } else { - builtin_field_type = b.i32_type - builtin_offset = 8 + (field_idx - 1) * 4 } - } else if typ_id == b.map_state_type { - has_builtin_layout = true - builtin_field_type = b.m.type_store.types[typ_id].fields[field_idx] - builtin_offset = field_idx * 8 - } else if typ_id == b.map_type { - has_builtin_layout = true - builtin_field_type = b.m.type_store.get_ptr(b.map_state_type) - builtin_offset = 0 - } - if has_builtin_layout { - off_const := b.m.get_or_add_const(b.i64_type, '${builtin_offset}') - return b.block_instr2(.get_element_ptr, block_id, - b.m.type_store.get_ptr(builtin_field_type), base_addr, off_const) + if has_builtin_layout { + off_const := b.m.get_or_add_const(b.i64_type, '${builtin_offset}') + return b.block_instr2(.get_element_ptr, block_id, + b.m.type_store.get_ptr(builtin_field_type), base_addr, off_const) + } } typ := b.m.type_store.types[typ_id] field_type := if field_idx < typ.fields.len { typ.fields[field_idx] } else { b.i64_type } @@ -1928,40 +2383,58 @@ fn (mut b Builder) register_basic_format_stubs() { ptr_i8 := b.m.type_store.get_ptr(b.i8_type) mut p1_ptr := []TypeID{} p1_ptr << ptr_i8 - tos2_id := b.register_synthetic_function('tos2', b.str_type, p1_ptr) - b.generate_tos2_body(tos2_id, 0) - tos3_id := b.register_synthetic_function('tos3', b.str_type, p1_ptr) - b.generate_tos2_body(tos3_id, 0) - tos_clone_id := b.register_synthetic_function('tos_clone', b.str_type, p1_ptr) - b.generate_tos_clone_body(tos_clone_id) + if tos2_id := b.register_runtime_function('tos2', b.str_type, p1_ptr) { + b.generate_tos2_body(tos2_id, 0) + } + if tos3_id := b.register_runtime_function('tos3', b.str_type, p1_ptr) { + b.generate_tos2_body(tos3_id, 0) + } + if tos_clone_id := b.register_runtime_function('tos_clone', b.str_type, p1_ptr) { + b.generate_tos_clone_body(tos_clone_id) + } b.register_pointer_string_stubs() mut p1_i64 := []TypeID{} p1_i64 << b.i64_type - int_str_id := b.register_synthetic_function('int_str', b.str_type, p1_i64) - b.generate_int_format_body(int_str_id, true, false) + if int_str_id := b.register_runtime_function('int_str', b.str_type, p1_i64) { + b.generate_int_format_body(int_str_id, true, false) + } mut p1_i1 := []TypeID{} p1_i1 << b.i1_type - bool_str_id := b.register_synthetic_function('bool_str', b.str_type, p1_i1) - b.generate_bool_str_body(bool_str_id) + if bool_str_id := b.register_runtime_function('bool_str', b.str_type, p1_i1) { + b.generate_bool_str_body(bool_str_id) + } mut p1_string := []TypeID{} p1_string << b.str_type - string_int_id := b.register_synthetic_function('string.int', b.i64_type, p1_string) - b.generate_string_int_body(string_int_id) - string_int_c_id := b.register_synthetic_function('string__int', b.i64_type, p1_string) - b.generate_string_int_body(string_int_c_id) + if string_int_id := b.register_runtime_function('string.int', b.i64_type, p1_string) { + b.generate_string_int_body(string_int_id) + } + if string_int_c_id := b.register_runtime_function('string__int', b.i64_type, p1_string) { + b.generate_string_int_body(string_int_c_id) + } mut p2_i64 := []TypeID{} p2_i64 << b.i64_type p2_i64 << b.i64_type - format_int_id := b.register_synthetic_function('strconv__format_int', b.str_type, p2_i64) - b.generate_int_format_body(format_int_id, true, true) - format_uint_id := b.register_synthetic_function('strconv__format_uint', b.str_type, p2_i64) - b.generate_int_format_body(format_uint_id, false, true) + if format_int_id := b.register_runtime_function('strconv__format_int', b.str_type, p2_i64) { + b.generate_int_format_body(format_int_id, true, true) + } + if format_uint_id := b.register_runtime_function('strconv__format_uint', b.str_type, p2_i64) { + b.generate_int_format_body(format_uint_id, false, true) + } b.register_v3_string_format_stubs() + if b.m.target.ptr_size == 4 { + for name in ['strconv__f32_to_str_l', 'strconv__f32_to_str_l_with_dot', 'f32_to_str_l', + 'f32_to_str_l_with_dot', 'strconv__f64_to_str_l', 'strconv__f64_to_str_l_with_dot', + 'f64_to_str_l', 'f64_to_str_l_with_dot'] { + if float_str_id := b.register_runtime_function(name, b.str_type, p1_i64) { + b.generate_const_string_body(float_str_id, '0.0') + } + } + } } // register_v3_string_format_stubs registers helper calls emitted by the string-interpolation @@ -1972,8 +2445,48 @@ fn (mut b Builder) register_v3_string_format_stubs() { p_pad << b.str_type p_pad << b.i32_type p_pad << b.i32_type - pad_id := b.register_synthetic_function('v3_string_pad', b.str_type, p_pad) - b.generate_string_pad_body(pad_id) + if b.m.target.ptr_size == 4 { + if pad_id := b.register_runtime_function('v3_string_pad', b.str_type, p_pad) { + b.generate_string_pad_body(pad_id) + } + + mut p_char := []TypeID{} + p_char << b.i32_type + if char_id := b.register_runtime_function('v3_char_string', b.str_type, p_char) { + b.generate_const_string_body(char_id, '?') + } + + mut p_f64 := []TypeID{} + p_f64 << b.f64_type + p_f64 << b.i32_type + if fixed_id := b.register_runtime_function('v3_f64_fixed', b.str_type, p_f64) { + b.generate_const_string_body(fixed_id, '0.0') + } + + mut p_int := []TypeID{} + p_int << b.i32_type + p_int << b.i32_type + if int_zpad_id := b.register_runtime_function('v3_int_zpad', b.str_type, p_int) { + b.generate_int_zpad_passthrough_body(int_zpad_id, b.i32_type) + } + + mut p_i64 := []TypeID{} + p_i64 << b.i64_type + p_i64 << b.i32_type + if i64_zpad_id := b.register_runtime_function('v3_i64_zpad', b.str_type, p_i64) { + b.generate_int_zpad_passthrough_body(i64_zpad_id, b.i64_type) + } + + mut p_u64 := []TypeID{} + p_u64 << b.u64_type + p_u64 << b.i32_type + if u64_zpad_id := b.register_runtime_function('v3_u64_zpad', b.str_type, p_u64) { + b.generate_const_string_body(u64_zpad_id, '0') + } + } else { + pad_id := b.register_synthetic_function('v3_string_pad', b.str_type, p_pad) + b.generate_string_pad_body(pad_id) + } } // register_pointer_string_stubs updates register pointer string stubs state for ssa. @@ -1984,10 +2497,11 @@ fn (mut b Builder) register_pointer_string_stubs() { for name in ['u8.vstring', 'char.vstring', 'byteptr.vstring', 'charptr.vstring', 'u8.vstring_literal', 'char.vstring_literal', 'byteptr.vstring_literal', 'charptr.vstring_literal'] { - func_id := b.register_synthetic_function(name, b.str_type, p1_ptr) - // The `_literal` wrappers borrow static C data (is_lit=1, never freed); the plain - // ones return a non-copying view of a runtime buffer (is_lit=0), matching V. - b.generate_tos2_body(func_id, if name.contains('_literal') { 1 } else { 0 }) + if func_id := b.register_runtime_function(name, b.str_type, p1_ptr) { + // The `_literal` wrappers borrow static C data (is_lit=1, never freed); the plain + // ones return a non-copying view of a runtime buffer (is_lit=0), matching V. + b.generate_tos2_body(func_id, if name.contains('_literal') { 1 } else { 0 }) + } } mut p2_ptr_len := []TypeID{} @@ -1996,8 +2510,9 @@ fn (mut b Builder) register_pointer_string_stubs() { for name in ['u8.vstring_with_len', 'char.vstring_with_len', 'byteptr.vstring_with_len', 'charptr.vstring_with_len', 'u8.vstring_literal_with_len', 'char.vstring_literal_with_len', 'byteptr.vstring_literal_with_len', 'charptr.vstring_literal_with_len'] { - func_id := b.register_synthetic_function(name, b.str_type, p2_ptr_len) - b.generate_vstring_with_len_body(func_id, if name.contains('_literal') { 1 } else { 0 }) + if func_id := b.register_runtime_function(name, b.str_type, p2_ptr_len) { + b.generate_vstring_with_len_body(func_id, if name.contains('_literal') { 1 } else { 0 }) + } } } @@ -2049,7 +2564,7 @@ fn (mut b Builder) generate_int_format_body(func_id int, is_signed bool, has_rad false_val := b.m.get_or_add_const(b.i1_type, '0') true_val := b.m.get_or_add_const(b.i1_type, '1') - malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.fn_ids['malloc']) + malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.runtime_fn_id('malloc')) buf := b.block_instr2(.call, entry, ptr_i8, malloc_ref, buf_size) end := b.block_instr2(.add, entry, ptr_i8, buf, last_off) nul := b.m.get_or_add_const(b.i8_type, '0') @@ -2094,7 +2609,7 @@ fn (mut b Builder) generate_int_format_body(func_id int, is_signed bool, has_rad b.block_instr1(.jmp, zero_digit_block, b.void_type, ValueID(sign_block)) n_cur := b.block_instr1(.load, loop_block, b.i64_type, alloca_n) - // The stored magnitude is unsigned, including abs(i64.min) and u64.max. + // The magnitude of the minimum signed integer occupies the unsigned half of i64. rem := b.block_instr2(.urem, loop_block, b.i64_type, n_cur, ten) digit := b.block_instr2(.add, loop_block, b.i64_type, rem, ascii_zero) pos := b.block_instr1(.load, loop_block, ptr_i8, alloca_pos) @@ -2106,7 +2621,7 @@ fn (mut b Builder) generate_int_format_body(func_id int, is_signed bool, has_rad b.block_instr2(.store, loop_block, b.void_type, new_len, alloca_len) quot := b.block_instr2(.udiv, loop_block, b.i64_type, n_cur, ten) b.block_instr2(.store, loop_block, b.void_type, quot, alloca_n) - has_more := b.block_instr2(.gt, loop_block, b.i1_type, quot, zero) + has_more := b.block_instr2(.ne, loop_block, b.i1_type, quot, zero) b.block_instr3(.br, loop_block, b.void_type, has_more, ValueID(loop_block), ValueID(sign_block)) neg_flag := b.block_instr1(.load, sign_block, b.i1_type, alloca_neg) @@ -2127,7 +2642,7 @@ fn (mut b Builder) generate_int_format_body(func_id int, is_signed bool, has_rad // The digits were written backwards, so `data` points into the middle of `buf`. // Shift them to the allocation start (memmove handles the overlap) and NUL-terminate, // so the returned owned string's `str` is `buf` itself and free() gets a valid pointer. - memmove_ref := b.m.add_value(.func_ref, b.void_type, 'memmove', b.fn_ids['memmove']) + memmove_ref := b.m.add_value(.func_ref, b.void_type, 'memmove', b.runtime_fn_id('memmove')) b.block_instr4(.call, done_block, ptr_i8, memmove_ref, buf, data, len) buf_term := b.block_instr2(.add, done_block, ptr_i8, buf, len) b.block_instr2(.store, done_block, b.void_type, nul, buf_term) @@ -2251,7 +2766,7 @@ fn (mut b Builder) generate_string_pad_body(func_id int) { pad_len32 := b.block_instr2(.sub, pad_block, b.i32_type, width, len32) pad_len64 := b.block_instr1(.zext, pad_block, b.i64_type, pad_len32) alloc_len := b.block_instr2(.add, pad_block, b.i64_type, width64, one64) - malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.fn_ids['malloc']) + malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.runtime_fn_id('malloc')) out_data := b.block_instr2(.call, pad_block, ptr_i8, malloc_ref, alloc_len) left := b.block_instr1(.load, pad_block, b.i32_type, left_slot) left_aligned := b.block_instr2(.ne, pad_block, b.i1_type, left, zero32) @@ -2261,18 +2776,18 @@ fn (mut b Builder) generate_string_pad_body(func_id int) { b.block_instr3(.br, pad_block, b.void_type, left_aligned, ValueID(copy_left), ValueID(copy_right)) - memcpy_ref_left := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref_left := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, copy_left, ptr_i8, memcpy_ref_left, out_data, data, len64) left_pad_dest := b.block_instr2(.add, copy_left, ptr_i8, out_data, len64) - memset_ref_left := b.m.add_value(.func_ref, b.void_type, 'memset', b.fn_ids['memset']) + memset_ref_left := b.m.add_value(.func_ref, b.void_type, 'memset', b.runtime_fn_id('memset')) space64 := b.m.get_or_add_const(b.i64_type, '32') b.block_instr4(.call, copy_left, ptr_i8, memset_ref_left, left_pad_dest, space64, pad_len64) b.block_instr1(.jmp, copy_left, b.void_type, ValueID(done)) - memset_ref_right := b.m.add_value(.func_ref, b.void_type, 'memset', b.fn_ids['memset']) + memset_ref_right := b.m.add_value(.func_ref, b.void_type, 'memset', b.runtime_fn_id('memset')) b.block_instr4(.call, copy_right, ptr_i8, memset_ref_right, out_data, space64, pad_len64) right_text_dest := b.block_instr2(.add, copy_right, ptr_i8, out_data, pad_len64) - memcpy_ref_right := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref_right := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, copy_right, ptr_i8, memcpy_ref_right, right_text_dest, data, len64) b.block_instr1(.jmp, copy_right, b.void_type, ValueID(done)) @@ -2283,11 +2798,32 @@ fn (mut b Builder) generate_string_pad_body(func_id int) { b.block_instr1(.ret, done, b.void_type, result) } +// generate_int_zpad_passthrough_body keeps zero-padded helper calls buildable on the +// SSA/native path. C output still uses the full helper implementation. +fn (mut b Builder) generate_int_zpad_passthrough_body(func_id int, value_type TypeID) { + entry := b.m.add_block(func_id, 'int_zpad_entry') + value := b.func_add_argument(func_id, value_type, 'value') + _ := b.func_add_argument(func_id, b.i32_type, 'width') + mut widened := value + if value_type != b.i64_type { + widened = b.block_instr1(.sext, entry, b.i64_type, value) + } + int_str_ref := b.m.add_value(.func_ref, b.str_type, 'int_str', b.runtime_fn_id('int_str')) + result := b.block_instr2(.call, entry, b.str_type, int_str_ref, widened) + b.block_instr1(.ret, entry, b.void_type, result) +} + // register_bench_runtime_stubs updates register bench runtime stubs state for ssa. fn (mut b Builder) register_bench_runtime_stubs() { - for name in bench_runtime_stub_names { - id := b.register_synthetic_function(name, b.i64_type, []TypeID{}) - b.generate_const_i64_body(id, '0') + names := if b.m.target.ptr_size == 4 { + wasm_bench_runtime_stub_names + } else { + bench_runtime_stub_names + } + for name in names { + if id := b.register_runtime_function(name, b.i64_type, []TypeID{}) { + b.generate_const_i64_body(id, '0') + } } } @@ -2298,7 +2834,7 @@ fn (mut b Builder) generate_tos2_body(func_id int, is_lit int) { ptr_i8 := b.m.type_store.get_ptr(b.i8_type) entry := b.m.add_block(func_id, 'entry') s := b.func_add_argument(func_id, ptr_i8, 's') - strlen_ref := b.m.add_value(.func_ref, b.i64_type, 'strlen', b.fn_ids['strlen']) + strlen_ref := b.m.add_value(.func_ref, b.i64_type, 'strlen', b.runtime_fn_id('strlen')) len := b.block_instr2(.call, entry, b.i64_type, strlen_ref, s) result := b.emit_make_string(entry, s, len, is_lit) b.block_instr1(.ret, entry, b.void_type, result) @@ -2309,7 +2845,7 @@ fn (mut b Builder) generate_tos_clone_body(func_id int) { ptr_i8 := b.m.type_store.get_ptr(b.i8_type) entry := b.m.add_block(func_id, 'entry') s := b.func_add_argument(func_id, ptr_i8, 's') - strlen_ref := b.m.add_value(.func_ref, b.i64_type, 'strlen', b.fn_ids['strlen']) + strlen_ref := b.m.add_value(.func_ref, b.i64_type, 'strlen', b.runtime_fn_id('strlen')) len := b.block_instr2(.call, entry, b.i64_type, strlen_ref, s) result := b.emit_make_owned_string(entry, s, len) b.block_instr1(.ret, entry, b.void_type, result) @@ -2331,14 +2867,16 @@ fn (mut b Builder) register_string_plus_stubs() { mut p2 := []TypeID{} p2 << b.str_type p2 << b.str_type - plus_id := b.register_synthetic_function('string__plus', b.str_type, p2) - b.generate_string_plus_body(plus_id) + if plus_id := b.register_runtime_function('string__plus', b.str_type, p2) { + b.generate_string_plus_body(plus_id) + } mut p_many := []TypeID{} p_many << b.i64_type p_many << b.m.type_store.get_ptr(b.str_type) - many_id := b.register_synthetic_function('string_plus_many', b.str_type, p_many) - b.generate_string_plus_many_body(many_id) + if many_id := b.register_runtime_function('string_plus_many', b.str_type, p_many) { + b.generate_string_plus_many_body(many_id) + } } // emit_make_string emits emit make string output for ssa. @@ -2371,9 +2909,9 @@ fn (mut b Builder) emit_make_owned_string(block_id BlockID, src ValueID, len Val ptr_i8 := b.m.type_store.get_ptr(b.i8_type) one := b.m.get_or_add_const(b.i64_type, '1') alloc_len := b.block_instr2(.add, block_id, b.i64_type, len, one) - malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.fn_ids['malloc']) + malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.runtime_fn_id('malloc')) out_data := b.block_instr2(.call, block_id, ptr_i8, malloc_ref, alloc_len) - memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, block_id, ptr_i8, memcpy_ref, out_data, src, len) zero8 := b.m.get_or_add_const(b.i8_type, '0') term_ptr := b.block_instr2(.add, block_id, ptr_i8, out_data, len) @@ -2408,12 +2946,12 @@ fn (mut b Builder) generate_string_plus_body(func_id int) { one := b.m.get_or_add_const(b.i64_type, '1') alloc_len := b.block_instr2(.add, entry, b.i64_type, total_len, one) - malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.fn_ids['malloc']) + malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.runtime_fn_id('malloc')) out_data := b.block_instr2(.call, entry, ptr_i8, malloc_ref, alloc_len) - memcpy_ref_left := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref_left := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, entry, ptr_i8, memcpy_ref_left, out_data, left_data, left_len) right_dest := b.block_instr2(.add, entry, ptr_i8, out_data, left_len) - memcpy_ref_right := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref_right := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, entry, ptr_i8, memcpy_ref_right, right_dest, right_data, right_len) zero8 := b.m.get_or_add_const(b.i8_type, '0') term_ptr := b.block_instr2(.add, entry, ptr_i8, out_data, total_len) @@ -2437,7 +2975,7 @@ fn (mut b Builder) generate_string_plus_many_body(func_id int) { alloca_written := b.block_instr0(.alloca, entry, ptr_i64) zero64 := b.m.get_or_add_const(b.i64_type, '0') one64 := b.m.get_or_add_const(b.i64_type, '1') - stride := b.m.get_or_add_const(b.i64_type, '16') + stride := b.m.get_or_add_const(b.i64_type, '${b.m.type_size(b.str_type)}') b.block_instr2(.store, entry, b.void_type, zero64, alloca_i) b.block_instr2(.store, entry, b.void_type, zero64, alloca_total) @@ -2464,7 +3002,7 @@ fn (mut b Builder) generate_string_plus_many_body(func_id int) { total_len := b.block_instr1(.load, alloc_block, b.i64_type, alloca_total) alloc_len := b.block_instr2(.add, alloc_block, b.i64_type, total_len, one64) - malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.fn_ids['malloc']) + malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.runtime_fn_id('malloc')) out_data := b.block_instr2(.call, alloc_block, ptr_i8, malloc_ref, alloc_len) b.block_instr2(.store, alloc_block, b.void_type, zero64, alloca_i) b.block_instr2(.store, alloc_block, b.void_type, zero64, alloca_written) @@ -2488,7 +3026,7 @@ fn (mut b Builder) generate_string_plus_many_body(func_id int) { copy_len := b.block_instr1(.zext, copy_body, b.i64_type, copy_len32) written := b.block_instr1(.load, copy_body, b.i64_type, alloca_written) dest := b.block_instr2(.add, copy_body, ptr_i8, out_data, written) - memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, copy_body, ptr_i8, memcpy_ref, dest, copy_data, copy_len) new_written := b.block_instr2(.add, copy_body, b.i64_type, written, copy_len) next_i_copy := b.block_instr2(.add, copy_body, b.i64_type, i_copy, one64) @@ -2513,89 +3051,102 @@ fn (mut b Builder) register_array_runtime_stubs() { p3 << b.i64_type p3 << b.i64_type p3 << b.i64_type - array_new_id := b.register_synthetic_function('array_new', b.array_type, p3) - b.generate_array_new_body(array_new_id) + if array_new_id := b.register_runtime_function('array_new', b.array_type, p3) { + b.generate_array_new_body(array_new_id) + } mut p2 := []TypeID{} p2 << b.array_type p2 << b.i64_type - array_get_id := b.register_synthetic_function('array_get', ptr_i8, p2) - b.generate_array_get_body(array_get_id) + if array_get_id := b.register_runtime_function('array_get', ptr_i8, p2) { + b.generate_array_get_body(array_get_id) + } p3 = []TypeID{} p3 << b.array_type p3 << b.i64_type p3 << b.i64_type - array_slice_id := b.register_synthetic_function('array_slice', b.array_type, p3) - b.generate_array_slice_body(array_slice_id) + if array_slice_id := b.register_runtime_function('array_slice', b.array_type, p3) { + b.generate_array_slice_body(array_slice_id) + } mut p2_array := []TypeID{} p2_array << b.array_type p2_array << b.array_type - array_eq_string_id := b.register_synthetic_function('array_eq_string', b.i1_type, p2_array) - b.generate_array_eq_string_body(array_eq_string_id) + if array_eq_string_id := b.register_runtime_function('array_eq_string', b.i1_type, p2_array) { + b.generate_array_eq_string_body(array_eq_string_id) + } mut p3_array := []TypeID{} p3_array << b.array_type p3_array << b.array_type p3_array << b.i64_type - array_eq_raw_id := b.register_synthetic_function('array_eq_raw', b.i1_type, p3_array) - b.generate_array_eq_raw_body(array_eq_raw_id) + if array_eq_raw_id := b.register_runtime_function('array_eq_raw', b.i1_type, p3_array) { + b.generate_array_eq_raw_body(array_eq_raw_id) + } mut p3_array_depth := []TypeID{} p3_array_depth << b.array_type p3_array_depth << b.array_type p3_array_depth << b.i32_type - array_eq_array_id := b.register_synthetic_function('array_eq_array', b.i1_type, p3_array_depth) - b.generate_array_eq_array_body(array_eq_array_id) + if array_eq_array_id := b.register_runtime_function('array_eq_array', b.i1_type, p3_array_depth) { + b.generate_array_eq_array_body(array_eq_array_id) + } p2 = []TypeID{} p2 << ptr_array p2 << ptr_i8 - array_push_id := b.register_synthetic_function('array_push', b.void_type, p2) - b.generate_array_push_body(array_push_id) + if array_push_id := b.register_runtime_function('array_push', b.void_type, p2) { + b.generate_array_push_body(array_push_id) + } mut p3_push_many := []TypeID{} p3_push_many << ptr_array p3_push_many << ptr_i8 p3_push_many << b.i64_type - array_push_many_id := b.register_synthetic_function('array.push_many', b.void_type, - p3_push_many) - b.generate_array_push_many_body(array_push_many_id) + if array_push_many_id := b.register_runtime_function('array.push_many', b.void_type, + p3_push_many) { + b.generate_array_push_many_body(array_push_many_id) + } // `array_push_many_ptr` is the C-macro name the transformer emits for // `arr << ptr_value` / fixed-array appends; it has the same // (ptr_array, ptr_value, count) signature as `array.push_many`. It needs its // own definition so the native linker resolves the `_array_push_many_ptr` symbol. - array_push_many_ptr_id := b.register_synthetic_function('array_push_many_ptr', b.void_type, - p3_push_many) - b.generate_array_push_many_body(array_push_many_ptr_id) + if array_push_many_ptr_id := b.register_runtime_function('array_push_many_ptr', b.void_type, + p3_push_many) { + b.generate_array_push_many_body(array_push_many_ptr_id) + } mut p2_push_many := []TypeID{} p2_push_many << ptr_array p2_push_many << b.array_type - array_push_many_wrapper_id := b.register_synthetic_function('array_push_many', b.void_type, - p2_push_many) - b.generate_array_push_many_array_body(array_push_many_wrapper_id) + if array_push_many_wrapper_id := b.register_runtime_function('array_push_many', b.void_type, + p2_push_many) { + b.generate_array_push_many_array_body(array_push_many_wrapper_id) + } mut p1 := []TypeID{} p1 << b.array_type - array_clone_id := b.register_synthetic_function('array_clone', b.array_type, p1) - b.generate_array_clone_body(array_clone_id) + if array_clone_id := b.register_runtime_function('array_clone', b.array_type, p1) { + b.generate_array_clone_body(array_clone_id) + } // The transformer lowers `arr.clone()` to `array__clone(&arr)`. Provide the // pointer-taking ABI alongside the value-taking synthetic clone. mut p1_ptr_arr := []TypeID{} p1_ptr_arr << b.m.type_store.get_ptr(b.array_type) - array_clone_ptr_id := b.register_synthetic_function('array__clone', b.array_type, p1_ptr_arr) - b.generate_array_clone_ptr_body(array_clone_ptr_id) + if array_clone_ptr_id := b.register_runtime_function('array__clone', b.array_type, p1_ptr_arr) { + b.generate_array_clone_ptr_body(array_clone_ptr_id) + } mut p3_repeat := []TypeID{} p3_repeat << b.array_type p3_repeat << b.i64_type p3_repeat << b.i64_type - array_repeat_id := b.register_synthetic_function('array.repeat_to_depth', b.array_type, - p3_repeat) - b.generate_array_repeat_to_depth_body(array_repeat_id) + if array_repeat_id := b.register_runtime_function('array.repeat_to_depth', b.array_type, + p3_repeat) { + b.generate_array_repeat_to_depth_body(array_repeat_id) + } for sort_type in ['int', 'i8', 'i16', 'i64', 'u8', 'u16', 'u32', 'u64', 'isize', 'usize', 'f32', 'f64', 'rune', 'char'] { @@ -2613,9 +3164,10 @@ fn (mut b Builder) register_array_runtime_stubs() { } mut sort_params := []TypeID{} sort_params << ptr_array - sort_id := b.register_synthetic_function('v3_array_sort_${sort_type}', b.void_type, - sort_params) - b.generate_scalar_array_sort_body(sort_id, elem_type) + if sort_id := b.register_runtime_function('v3_array_sort_${sort_type}', b.void_type, + sort_params) { + b.generate_scalar_array_sort_body(sort_id, elem_type) + } } } @@ -2694,22 +3246,27 @@ fn (mut b Builder) generate_scalar_array_sort_body(func_id int, elem_type TypeID fn (mut b Builder) register_panic_stub() { mut p1 := []TypeID{} p1 << b.str_type - panic_id := b.register_synthetic_function('panic', b.void_type, p1) - b.generate_panic_body(panic_id) + if panic_id := b.register_runtime_function('panic', b.void_type, p1) { + b.generate_panic_body(panic_id) + } } // register_printing_stubs updates register printing stubs state for ssa. fn (mut b Builder) register_printing_stubs() { mut p1 := []TypeID{} p1 << b.str_type - print_id := b.register_synthetic_function('print', b.void_type, p1) - b.generate_print_body(print_id, '1', false) - println_id := b.register_synthetic_function('println', b.void_type, p1) - b.generate_print_body(println_id, '1', true) - eprint_id := b.register_synthetic_function('eprint', b.void_type, p1) - b.generate_print_body(eprint_id, '2', false) - eprintln_id := b.register_synthetic_function('eprintln', b.void_type, p1) - b.generate_print_body(eprintln_id, '2', true) + if print_id := b.register_runtime_function('print', b.void_type, p1) { + b.generate_print_body(print_id, '1', false) + } + if println_id := b.register_runtime_function('println', b.void_type, p1) { + b.generate_print_body(println_id, '1', true) + } + if eprint_id := b.register_runtime_function('eprint', b.void_type, p1) { + b.generate_print_body(eprint_id, '2', false) + } + if eprintln_id := b.register_runtime_function('eprintln', b.void_type, p1) { + b.generate_print_body(eprintln_id, '2', true) + } } // generate_print_body supports generate print body handling for Builder. @@ -2726,7 +3283,7 @@ fn (mut b Builder) generate_print_body(func_id int, fd_value string, newline boo len32 := b.block_instr1(.load, entry, b.i32_type, len_ptr) len := b.block_instr1(.zext, entry, b.i64_type, len32) fd := b.m.get_or_add_const(b.i64_type, fd_value) - write_ref := b.m.add_value(.func_ref, b.void_type, 'write', b.fn_ids['write']) + write_ref := b.m.add_value(.func_ref, b.void_type, 'write', b.runtime_fn_id('write')) b.block_instr4(.call, entry, b.i64_type, write_ref, fd, data, len) if newline { newline_str := b.m.add_value(.string_literal, b.str_type, '\n', 0) @@ -2744,11 +3301,11 @@ fn (mut b Builder) generate_print_body(func_id int, fd_value string, newline boo fn (mut b Builder) generate_panic_body(func_id int) { entry := b.m.add_block(func_id, 'entry') message := b.func_add_argument(func_id, b.str_type, 'message') - if eprintln_id := b.fn_ids['eprintln'] { + if eprintln_id := b.runtime_fn_lookup('eprintln') { fn_ref := b.m.add_value(.func_ref, b.void_type, 'eprintln', eprintln_id) b.block_instr2(.call, entry, b.void_type, fn_ref, message) } - if exit_id := b.fn_ids['exit'] { + if exit_id := b.runtime_fn_lookup('exit') { one := b.m.get_or_add_const(b.i64_type, '1') fn_ref := b.m.add_value(.func_ref, b.void_type, 'exit', exit_id) b.block_instr2(.call, entry, b.void_type, fn_ref, one) @@ -2763,53 +3320,67 @@ fn (mut b Builder) register_string_builder_stubs() { mut p1 := []TypeID{} p1 << b.i64_type - new_id := b.register_synthetic_function('strings.new_builder', b.array_type, p1) - b.generate_builder_new_body(new_id) + if new_id := b.register_runtime_function('strings.new_builder', b.array_type, p1) { + b.generate_builder_new_body(new_id) + } mut p2 := []TypeID{} p2 << ptr_builder p2 << b.str_type - write_string_id := - b.register_synthetic_function('strings.Builder.write_string', b.void_type, p2) - b.generate_builder_write_string_body(write_string_id, false) - writeln_id := b.register_synthetic_function('strings.Builder.writeln', b.void_type, p2) - b.generate_builder_write_string_body(writeln_id, true) + if write_string_id := b.register_runtime_function('strings.Builder.write_string', b.void_type, p2) { + b.generate_builder_write_string_body(write_string_id, false) + } + if writeln_id := b.register_runtime_function('strings.Builder.writeln', b.void_type, p2) { + b.generate_builder_write_string_body(writeln_id, true) + } p1 = []TypeID{} p1 << ptr_builder - str_id := b.register_synthetic_function('strings.Builder.str', b.str_type, p1) - b.generate_builder_str_body(str_id) - free_id := b.register_synthetic_function('strings.Builder.free', b.void_type, p1) - b.generate_builder_free_body(free_id) + if str_id := b.register_runtime_function('strings.Builder.str', b.str_type, p1) { + b.generate_builder_str_body(str_id) + } + if free_id := b.register_runtime_function('strings.Builder.free', b.void_type, p1) { + b.generate_builder_free_body(free_id) + } p2 = []TypeID{} p2 << ptr_builder p2 << b.i8_type - write_u8_id := b.register_synthetic_function('strings.Builder.write_u8', b.void_type, p2) - b.generate_builder_write_u8_body(write_u8_id) - write_byte_id := b.register_synthetic_function('strings.Builder.write_byte', b.void_type, p2) - b.generate_builder_write_u8_body(write_byte_id) + if b.m.target.ptr_size == 4 { + if write_u8_id := b.register_runtime_function('strings.Builder.write_u8', b.void_type, p2) { + b.generate_builder_write_u8_body(write_u8_id) + } + } else { + write_u8_id := b.register_synthetic_function('strings.Builder.write_u8', b.void_type, p2) + b.generate_builder_write_u8_body(write_u8_id) + write_byte_id := b.register_synthetic_function('strings.Builder.write_byte', b.void_type, p2) + b.generate_builder_write_u8_body(write_byte_id) + } mut p3 := []TypeID{} p3 << ptr_builder p3 << ptr_i8 p3 << b.i64_type - write_ptr_id := b.register_synthetic_function('strings.Builder.write_ptr', b.void_type, p3) - b.generate_builder_write_ptr_body(write_ptr_id) - push_many_id := b.register_synthetic_function('strings.Builder.push_many', b.void_type, p3) - b.generate_builder_write_ptr_body(push_many_id) + if write_ptr_id := b.register_runtime_function('strings.Builder.write_ptr', b.void_type, p3) { + b.generate_builder_write_ptr_body(write_ptr_id) + } + if push_many_id := b.register_runtime_function('strings.Builder.push_many', b.void_type, p3) { + b.generate_builder_write_ptr_body(push_many_id) + } p2 = []TypeID{} p2 << ptr_builder p2 << b.array_type - write_runes_id := b.register_synthetic_function('strings.Builder.write_runes', b.void_type, p2) - b.generate_builder_free_body(write_runes_id) + if write_runes_id := b.register_runtime_function('strings.Builder.write_runes', b.void_type, p2) { + b.generate_builder_free_body(write_runes_id) + } p2 = []TypeID{} p2 << ptr_builder p2 << b.i64_type - last_n_id := b.register_synthetic_function('strings.Builder.last_n', b.str_type, p2) - b.generate_builder_last_n_body(last_n_id) + if last_n_id := b.register_runtime_function('strings.Builder.last_n', b.str_type, p2) { + b.generate_builder_last_n_body(last_n_id) + } } // generate_builder_new_body supports generate builder new body handling for Builder. @@ -2818,7 +3389,7 @@ fn (mut b Builder) generate_builder_new_body(func_id int) { initial_size := b.func_add_argument(func_id, b.i64_type, 'initial_size') one := b.m.get_or_add_const(b.i64_type, '1') zero := b.m.get_or_add_const(b.i64_type, '0') - fn_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.fn_ids['array_new']) + fn_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.runtime_fn_id('array_new')) builder := b.block_instr4(.call, entry, b.array_type, fn_ref, one, zero, initial_size) b.block_instr1(.ret, entry, b.void_type, builder) } @@ -2843,13 +3414,21 @@ fn (mut b Builder) emit_builder_append(func_id int, entry BlockID, builder_ptr V two := b.m.get_or_add_const(b.i64_type, '2') double_needed := b.block_instr2(.mul, blk_grow, b.i64_type, needed, two) new_cap := b.block_instr2(.add, blk_grow, b.i64_type, double_needed, two) - grow_ref := b.m.add_value(.func_ref, b.void_type, '__ssa_array_grow', b.fn_ids['__ssa_array_grow']) - b.block_instr3(.call, blk_grow, b.void_type, grow_ref, builder_ptr, new_cap) + if b.m.target.ptr_size == 4 { + old_data := b.block_instr1(.load, blk_grow, ptr_i8, data_ptr) + realloc_ref := b.m.add_value(.func_ref, b.void_type, 'realloc', b.runtime_fn_id('realloc')) + new_data := b.block_instr3(.call, blk_grow, ptr_i8, realloc_ref, old_data, new_cap) + b.block_instr2(.store, blk_grow, b.void_type, new_data, data_ptr) + b.block_instr2(.store, blk_grow, b.void_type, new_cap, cap_ptr) + } else { + grow_ref := b.m.add_value(.func_ref, b.void_type, '__ssa_array_grow', b.fn_ids['__ssa_array_grow']) + b.block_instr3(.call, blk_grow, b.void_type, grow_ref, builder_ptr, new_cap) + } b.block_instr1(.jmp, blk_grow, b.void_type, ValueID(blk_copy)) data := b.block_instr1(.load, blk_copy, ptr_i8, data_ptr) dest := b.block_instr2(.add, blk_copy, ptr_i8, data, old_len) - memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, blk_copy, ptr_i8, memcpy_ref, dest, src_ptr, add_len) b.block_instr2(.store, blk_copy, b.void_type, needed, len_ptr) return blk_copy @@ -2966,16 +3545,18 @@ fn (mut b Builder) register_path_runtime_stubs() { mut p1 := []TypeID{} p1 << ptr_builder for name in ['normalize_path_in_builder', 'os.normalize_path_in_builder'] { - normalize_id := b.register_synthetic_function(name, b.void_type, p1) - b.generate_builder_free_body(normalize_id) + if normalize_id := b.register_runtime_function(name, b.void_type, p1) { + b.generate_builder_free_body(normalize_id) + } } mut p2 := []TypeID{} p2 << b.str_type p2 << b.str_type for name in ['join_path_single', 'os.join_path_single'] { - join_id := b.register_synthetic_function(name, b.str_type, p2) - b.generate_join_path_single_body(join_id) + if join_id := b.register_runtime_function(name, b.str_type, p2) { + b.generate_join_path_single_body(join_id) + } } } @@ -2985,7 +3566,7 @@ fn (mut b Builder) generate_join_path_single_body(func_id int) { base := b.func_add_argument(func_id, b.str_type, 'base') elem := b.func_add_argument(func_id, b.str_type, 'elem') slash := b.m.add_value(.string_literal, b.str_type, '/', 0) - plus_ref := b.m.add_value(.func_ref, b.str_type, 'string__plus', b.fn_ids['string__plus']) + plus_ref := b.m.add_value(.func_ref, b.str_type, 'string__plus', b.runtime_fn_id('string__plus')) with_sep := b.block_instr3(.call, entry, b.str_type, plus_ref, base, slash) result := b.block_instr3(.call, entry, b.str_type, plus_ref, with_sep, elem) b.block_instr1(.ret, entry, b.void_type, result) @@ -2995,20 +3576,24 @@ fn (mut b Builder) generate_join_path_single_body(func_id int) { fn (mut b Builder) register_map_runtime_stubs() { ptr_i8 := b.m.type_store.get_ptr(b.i8_type) ptr_map := b.m.type_store.get_ptr(b.map_type) - b.register_native_map_index_helpers() + if b.m.target.ptr_size == 8 { + b.register_native_map_index_helpers() + } mut p2 := []TypeID{} p2 << ptr_map p2 << ptr_i8 - find_id := b.register_synthetic_function('v3_map_find', b.i64_type, p2) - b.generate_map_find_body(find_id) + if find_id := b.register_runtime_function('v3_map_find', b.i64_type, p2) { + b.generate_map_find_body(find_id) + } mut p6 := []TypeID{} for _ in 0 .. 6 { p6 << b.i64_type } - new_id := b.register_synthetic_function('new_map', b.map_type, p6) - b.generate_new_map_body(new_id) + if new_id := b.register_runtime_function('new_map', b.map_type, p6) { + b.generate_new_map_body(new_id) + } mut p3 := []TypeID{} p3 << ptr_map @@ -3021,70 +3606,94 @@ fn (mut b Builder) register_map_runtime_stubs() { p5 << ptr_i8 p5 << b.i64_type p5 << b.i64_type - sized_set_id := b.register_synthetic_function('v3_map_set_sized', b.void_type, p5) - b.generate_map_set_sized_body(sized_set_id) - set_id := b.register_synthetic_function('map__set', b.void_type, p3) - b.generate_map_set_default_body(set_id) + if sized_set_id := b.register_runtime_function('v3_map_set_sized', b.void_type, p5) { + b.generate_map_set_sized_body(sized_set_id) + } + if set_id := b.register_runtime_function('map__set', b.void_type, p3) { + b.generate_map_set_default_body(set_id) + } p3 = []TypeID{} p3 << ptr_map p3 << ptr_i8 p3 << ptr_i8 - get_id := b.register_synthetic_function('map__get', ptr_i8, p3) - b.generate_map_get_body(get_id) + if get_id := b.register_runtime_function('map__get', ptr_i8, p3) { + b.generate_map_get_body(get_id) + } mut p2_map_key := []TypeID{} p2_map_key << ptr_map p2_map_key << ptr_i8 - get_check_id := b.register_synthetic_function('map__get_check', ptr_i8, p2_map_key) - b.generate_map_get_check_body(get_check_id) - get_key_check_id := b.register_synthetic_function('map__get_key_check', ptr_i8, p2_map_key) - b.generate_map_get_key_check_body(get_key_check_id) - get_or_set_id := b.register_synthetic_function('map__get_or_set', ptr_i8, p3) - b.generate_native_map_get_and_set_body(get_or_set_id) - get_and_set_id := b.register_synthetic_function('map__get_and_set', ptr_i8, p3) - b.generate_native_map_get_and_set_body(get_and_set_id) + if b.m.target.ptr_size == 4 { + if get_check_id := b.register_runtime_function('map__get_check', ptr_i8, p2_map_key) { + b.generate_map_get_check_body(get_check_id) + } + if get_key_check_id := b.register_runtime_function('map__get_key_check', ptr_i8, p2_map_key) { + b.generate_map_get_key_check_body(get_key_check_id) + } + if get_or_set_id := b.register_runtime_function('map__get_or_set', ptr_i8, p3) { + b.generate_map_get_body(get_or_set_id) + } + } else { + get_check_id := b.register_synthetic_function('map__get_check', ptr_i8, p2_map_key) + b.generate_map_get_check_body(get_check_id) + get_key_check_id := b.register_synthetic_function('map__get_key_check', ptr_i8, p2_map_key) + b.generate_map_get_key_check_body(get_key_check_id) + get_or_set_id := b.register_synthetic_function('map__get_or_set', ptr_i8, p3) + b.generate_native_map_get_and_set_body(get_or_set_id) + get_and_set_id := b.register_synthetic_function('map__get_and_set', ptr_i8, p3) + b.generate_native_map_get_and_set_body(get_and_set_id) + } p2 = []TypeID{} p2 << ptr_map p2 << ptr_i8 - exists_id := b.register_synthetic_function('map__exists', b.i1_type, p2) - b.generate_map_exists_body(exists_id) + if exists_id := b.register_runtime_function('map__exists', b.i1_type, p2) { + b.generate_map_exists_body(exists_id) + } mut p1_ptr := []TypeID{} p1_ptr << ptr_map - clear_id := b.register_synthetic_function('map__clear', b.void_type, p1_ptr) - b.generate_map_clear_body(clear_id) - free_id := b.register_synthetic_function('map__free', b.void_type, p1_ptr) - b.generate_map_free_body(free_id) + if clear_id := b.register_runtime_function('map__clear', b.void_type, p1_ptr) { + b.generate_map_clear_body(clear_id) + } + if free_id := b.register_runtime_function('map__free', b.void_type, p1_ptr) { + b.generate_map_free_body(free_id) + } mut p2_arr := []TypeID{} p2_arr << ptr_map p2_arr << b.i64_type - keys_id := b.register_synthetic_function('map__keys', b.array_type, p2_arr) - b.generate_map_keys_values_body(keys_id, 0, 4) - values_id := b.register_synthetic_function('map__values', b.array_type, p2_arr) - b.generate_map_keys_values_body(values_id, 1, 5) + if keys_id := b.register_runtime_function('map__keys', b.array_type, p2_arr) { + b.generate_map_keys_values_body(keys_id, 0, 4) + } + if values_id := b.register_runtime_function('map__values', b.array_type, p2_arr) { + b.generate_map_keys_values_body(values_id, 1, 5) + } mut p2_reserve := []TypeID{} p2_reserve << ptr_map p2_reserve << b.u32_type - reserve_id := b.register_synthetic_function('map__reserve', b.void_type, p2_reserve) - b.generate_map_reserve_body(reserve_id) + if reserve_id := b.register_runtime_function('map__reserve', b.void_type, p2_reserve) { + b.generate_map_reserve_body(reserve_id) + } p2 = []TypeID{} p2 << ptr_map p2 << ptr_i8 - delete_id := b.register_synthetic_function('map__delete', b.void_type, p2) - b.generate_map_delete_body(delete_id) + if delete_id := b.register_runtime_function('map__delete', b.void_type, p2) { + b.generate_map_delete_body(delete_id) + } mut p1_map := []TypeID{} p1_map << ptr_map - clone_id := b.register_synthetic_function('map__clone', b.map_type, p1_map) - b.generate_map_clone_body(clone_id) + if clone_id := b.register_runtime_function('map__clone', b.map_type, p1_map) { + b.generate_map_clone_body(clone_id) + } mut p1_ptr_map := []TypeID{} p1_ptr_map << ptr_map - move_id := b.register_synthetic_function('map__move', b.map_type, p1_ptr_map) - b.generate_map_move_body(move_id) + if move_id := b.register_runtime_function('map__move', b.map_type, p1_ptr_map) { + b.generate_map_move_body(move_id) + } } // generate_map_reserve_body emits a capacity no-op for the simplified SSA map runtime. @@ -3112,7 +3721,7 @@ fn (mut b Builder) generate_map_clear_body(func_id int) { len_ptr := b.map_state_field_ptr(blk_clear, state, 3) zero := b.m.get_or_add_const(b.i64_type, '0') b.block_instr2(.store, blk_clear, b.void_type, zero, len_ptr) - b.invalidate_native_map_index(blk_clear, state) + if b.m.target.ptr_size == 8 { b.invalidate_native_map_index(blk_clear, state) } b.block_instr1(.jmp, blk_clear, b.void_type, ValueID(blk_done)) b.block_instr0(.ret, blk_done, b.void_type) @@ -3137,7 +3746,7 @@ fn (mut b Builder) generate_map_delete_body(func_id int) { entry := b.m.add_block(func_id, 'entry') map_ptr := b.func_add_argument(func_id, ptr_map, 'm') key_ptr := b.func_add_argument(func_id, ptr_i8, 'key') - find_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_find', b.fn_ids['v3_map_find']) + find_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_find', b.runtime_fn_id('v3_map_find')) idx := b.block_instr3(.call, entry, b.i64_type, find_ref, map_ptr, key_ptr) zero := b.m.get_or_add_const(b.i64_type, '0') found := b.block_instr2(.ge, entry, b.i1_type, idx, zero) @@ -3172,14 +3781,14 @@ fn (mut b Builder) generate_map_delete_body(func_id int) { val_dst := b.block_instr2(.add, blk_compact, ptr_i8, vals, val_dst_off) key_src := b.block_instr2(.add, blk_compact, ptr_i8, keys, key_src_off) val_src := b.block_instr2(.add, blk_compact, ptr_i8, vals, val_src_off) - memcpy_ref_key := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) - memcpy_ref_val := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref_key := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) + memcpy_ref_val := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, blk_compact, ptr_i8, memcpy_ref_key, key_dst, key_src, key_size) b.block_instr4(.call, blk_compact, ptr_i8, memcpy_ref_val, val_dst, val_src, val_size) b.block_instr1(.jmp, blk_compact, b.void_type, ValueID(blk_store_len)) b.block_instr2(.store, blk_store_len, b.void_type, new_len, len_ptr) - b.invalidate_native_map_index(blk_store_len, state) + if b.m.target.ptr_size == 8 { b.invalidate_native_map_index(blk_store_len, state) } b.block_instr1(.jmp, blk_store_len, b.void_type, ValueID(blk_done)) b.block_instr0(.ret, blk_done, b.void_type) @@ -3201,7 +3810,7 @@ fn (mut b Builder) generate_map_clone_body(func_id int) { one := b.m.get_or_add_const(b.i64_type, '1') state_size := b.m.get_or_add_const(b.i64_type, b.m.type_size(b.map_state_type).str()) - calloc_ref := b.m.add_value(.func_ref, b.void_type, 'calloc', b.fn_ids['calloc']) + calloc_ref := b.m.add_value(.func_ref, b.void_type, 'calloc', b.runtime_fn_id('calloc')) new_state_raw := b.block_instr3(.call, blk_clone, ptr_i8, calloc_ref, one, state_size) new_state := b.block_instr1(.bitcast, blk_clone, ptr_state, new_state_raw) @@ -3221,8 +3830,8 @@ fn (mut b Builder) generate_map_clone_body(func_id int) { new_vals := b.block_instr3(.call, blk_clone, ptr_i8, calloc_ref, cap, val_size) key_bytes := b.block_instr2(.mul, blk_clone, b.i64_type, len, key_size) val_bytes := b.block_instr2(.mul, blk_clone, b.i64_type, len, val_size) - memcpy_ref_keys := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) - memcpy_ref_vals := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref_keys := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) + memcpy_ref_vals := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, blk_clone, ptr_i8, memcpy_ref_keys, new_keys, old_keys, key_bytes) b.block_instr4(.call, blk_clone, ptr_i8, memcpy_ref_vals, new_vals, old_vals, val_bytes) @@ -3290,20 +3899,20 @@ fn (mut b Builder) generate_map_keys_values_body(func_id int, data_field int, si src := b.block_instr1(.load, blk_copy, ptr_i8, src_ptr) len := b.block_instr1(.load, blk_copy, b.i64_type, len_ptr) elem_size := b.block_instr1(.load, blk_copy, b.i64_type, elem_size_ptr) - array_new_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.fn_ids['array_new']) + array_new_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.runtime_fn_id('array_new')) arr := b.block_instr4(.call, blk_copy, b.array_type, array_new_ref, elem_size, len, len) arr_slot := b.block_instr0(.alloca, blk_copy, ptr_array) b.block_instr2(.store, blk_copy, b.void_type, arr, arr_slot) data_ptr := b.block_struct_field_ptr(blk_copy, arr_slot, b.array_type, 0) data := b.block_instr1(.load, blk_copy, ptr_i8, data_ptr) byte_len := b.block_instr2(.mul, blk_copy, b.i64_type, len, elem_size) - memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, blk_copy, ptr_i8, memcpy_ref, data, src, byte_len) result := b.block_instr1(.load, blk_copy, b.array_type, arr_slot) b.block_instr1(.ret, blk_copy, b.void_type, result) zero := b.m.get_or_add_const(b.i64_type, '0') - empty_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.fn_ids['array_new']) + empty_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.runtime_fn_id('array_new')) empty := b.block_instr4(.call, blk_empty, b.array_type, empty_ref, elem_size_arg, zero, zero) b.block_instr1(.ret, blk_empty, b.void_type, empty) } @@ -3313,8 +3922,9 @@ fn (mut b Builder) register_u8_runtime_stubs() { mut p1 := []TypeID{} p1 << b.i8_type for name in ['u8.is_digit', 'u8.is_letter', 'u8.is_alnum', 'u8.is_capital'] { - func_id := b.register_synthetic_function(name, b.i1_type, p1) - b.generate_u8_predicate_body(func_id, name) + if func_id := b.register_runtime_function(name, b.i1_type, p1) { + b.generate_u8_predicate_body(func_id, name) + } } } @@ -3359,12 +3969,14 @@ fn (mut b Builder) register_heap_tracking_stubs() { mut p2 := []TypeID{} p2 << ptr_i8 p2 << b.i64_type - alloc_id := b.register_synthetic_function('_ht_alloc', b.void_type, p2) - b.generate_void_noop_body(alloc_id) + if alloc_id := b.register_runtime_function('_ht_alloc', b.void_type, p2) { + b.generate_void_noop_body(alloc_id) + } mut p1 := []TypeID{} p1 << ptr_i8 - free_id := b.register_synthetic_function('_ht_free', b.void_type, p1) - b.generate_void_noop_body(free_id) + if free_id := b.register_runtime_function('_ht_free', b.void_type, p1) { + b.generate_void_noop_body(free_id) + } } // generate_void_noop_body supports generate void noop body handling for Builder. @@ -3376,15 +3988,27 @@ fn (mut b Builder) generate_void_noop_body(func_id int) { // register_process_capture_helpers supplies the C header's process capture helpers for native output. fn (mut b Builder) register_process_capture_helpers() { ptr_i8 := b.m.type_store.get_ptr(b.i8_type) - ptr_i64 := b.m.type_store.get_ptr(b.i64_type) - ptr_argv := b.m.type_store.get_ptr(ptr_i8) - b.register_native_process_externs() - execute_id := b.register_synthetic_c_function('v_os_execute_capture_start', b.i64_type, - [ptr_i8, ptr_i64, ptr_i64]) - b.generate_native_process_capture_body(execute_id, true) - exec_id := b.register_synthetic_c_function('v_os_exec_capture_start', b.i64_type, - [ptr_argv, ptr_i64, ptr_i64]) - b.generate_native_process_capture_body(exec_id, false) + if b.m.target.ptr_size == 4 { + mut p3 := []TypeID{} + p3 << ptr_i8 + p3 << ptr_i8 + p3 << ptr_i8 + for name in ['v_os_execute_capture_start', 'v_os_exec_capture_start'] { + if func_id := b.register_runtime_function(name, b.i64_type, p3) { + b.generate_const_i64_body(func_id, '1') + } + } + } else { + ptr_i64 := b.m.type_store.get_ptr(b.i64_type) + ptr_argv := b.m.type_store.get_ptr(ptr_i8) + b.register_native_process_externs() + execute_id := b.register_synthetic_c_function('v_os_execute_capture_start', b.i64_type, + [ptr_i8, ptr_i64, ptr_i64]) + b.generate_native_process_capture_body(execute_id, true) + exec_id := b.register_synthetic_c_function('v_os_exec_capture_start', b.i64_type, + [ptr_argv, ptr_i64, ptr_i64]) + b.generate_native_process_capture_body(exec_id, false) + } } // register_file_check_stubs updates register file check stubs state for ssa. @@ -3392,8 +4016,9 @@ fn (mut b Builder) register_file_check_stubs() { mut p1 := []TypeID{} p1 << b.i64_type for name in ['check_fwrite', 'os.check_fwrite', 'check_fread', 'os.check_fread'] { - func_id := b.register_synthetic_function(name, b.i64_type, p1) - b.generate_identity_i64_body(func_id) + if func_id := b.register_runtime_function(name, b.i64_type, p1) { + b.generate_identity_i64_body(func_id) + } } } @@ -3407,8 +4032,9 @@ fn (mut b Builder) register_modulecache_stubs() { for _ in 0 .. 7 { params << ptr_u64 } - func_id := b.register_synthetic_c_function('v3_modulecache_file_metadata', b.i64_type, params) - b.generate_const_i64_with_params_body(func_id, params, '0') + if func_id := b.register_runtime_c_function('v3_modulecache_file_metadata', b.i64_type, params) { + b.generate_const_i64_with_params_body(func_id, params, '0') + } } // generate_identity_i64_body supports generate identity i64 body handling for Builder. @@ -3423,16 +4049,19 @@ fn (mut b Builder) register_fd_macro_stubs() { ptr_i8 := b.m.type_store.get_ptr(b.i8_type) mut fd_set_params := []TypeID{} fd_set_params << ptr_i8 - zero_id := b.register_synthetic_function('FD_ZERO', b.void_type, fd_set_params) - b.generate_void_noop_with_params_body(zero_id, fd_set_params) + if zero_id := b.register_runtime_function('FD_ZERO', b.void_type, fd_set_params) { + b.generate_void_noop_with_params_body(zero_id, fd_set_params) + } mut fd_check_params := []TypeID{} fd_check_params << b.i64_type fd_check_params << ptr_i8 - set_id := b.register_synthetic_function('FD_SET', b.void_type, fd_check_params) - b.generate_void_noop_with_params_body(set_id, fd_check_params) - isset_id := b.register_synthetic_function('FD_ISSET', b.i64_type, fd_check_params) - b.generate_const_i64_with_params_body(isset_id, fd_check_params, '0') + if set_id := b.register_runtime_function('FD_SET', b.void_type, fd_check_params) { + b.generate_void_noop_with_params_body(set_id, fd_check_params) + } + if isset_id := b.register_runtime_function('FD_ISSET', b.i64_type, fd_check_params) { + b.generate_const_i64_with_params_body(isset_id, fd_check_params, '0') + } } // register_signal_macro_stubs updates register signal macro stubs state for ssa. @@ -3442,11 +4071,7 @@ fn (mut b Builder) register_signal_macro_stubs() { params << b.i64_type params << ptr_i8 name := 'v_signal_with_handler_cast' - func_id := b.register_synthetic_function(name, ptr_i8, params) - if fn_type := b.fn_types[name] { - b.c_fn_types[name] = fn_type - } - b.c_fn_ids[name] = func_id + func_id := b.register_runtime_c_function(name, ptr_i8, params) or { return } b.generate_const_ptr_with_params_body(func_id, params, ptr_i8, '0') } @@ -3484,17 +4109,20 @@ fn (mut b Builder) register_os_stat_stubs() { mut p1 := []TypeID{} p1 << b.str_type for name in ['is_dir', 'os.is_dir'] { - is_dir_id := b.register_synthetic_function(name, b.i1_type, p1) - b.generate_os_stat_kind_body(is_dir_id, 'stat', '16384') + if is_dir_id := b.register_runtime_function(name, b.i1_type, p1) { + b.generate_os_stat_kind_body(is_dir_id, 'stat', '16384') + } } for name in ['is_link', 'os.is_link'] { - is_link_id := b.register_synthetic_function(name, b.i1_type, p1) - b.generate_os_stat_kind_body(is_link_id, 'lstat', '40960') + if is_link_id := b.register_runtime_function(name, b.i1_type, p1) { + b.generate_os_stat_kind_body(is_link_id, 'lstat', '40960') + } } ls_result_type := b.option_type_id('[]string', true) for name in ['ls', 'os.ls'] { - ls_id := b.register_synthetic_function(name, ls_result_type, p1) - b.generate_os_ls_body(ls_id, ls_result_type) + if ls_id := b.register_runtime_function(name, ls_result_type, p1) { + b.generate_os_ls_body(ls_id, ls_result_type) + } } } @@ -3507,7 +4135,7 @@ fn (mut b Builder) generate_os_stat_kind_body(func_id int, stat_fn string, expec path_data := b.emit_cstring_from_string(entry, path) stat_size := b.m.get_or_add_const(b.i64_type, '256') - malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.fn_ids['malloc']) + malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.runtime_fn_id('malloc')) stat_buf := b.block_instr2(.call, entry, ptr_i8, malloc_ref, stat_size) stat_idx := b.c_fn_ids[stat_fn] or { false_val := b.m.get_or_add_const(b.i1_type, '0') @@ -3545,10 +4173,10 @@ fn (mut b Builder) generate_os_ls_body(func_id int, result_type TypeID) { path := b.func_add_argument(func_id, b.str_type, 'path') path_data := b.emit_cstring_from_string(entry, path) - str_size := b.m.get_or_add_const(b.i64_type, '16') + str_size := b.m.get_or_add_const(b.i64_type, '${b.m.type_size(b.str_type)}') zero64 := b.m.get_or_add_const(b.i64_type, '0') cap := b.m.get_or_add_const(b.i64_type, '50') - array_new_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.fn_ids['array_new']) + array_new_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.runtime_fn_id('array_new')) arr := b.block_instr4(.call, entry, b.array_type, array_new_ref, str_size, zero64, cap) arr_alloca := b.block_instr0(.alloca, entry, ptr_array) b.block_instr2(.store, entry, b.void_type, arr, arr_alloca) @@ -3607,12 +4235,12 @@ fn (mut b Builder) generate_os_ls_body(func_id int, result_type TypeID) { b.block_instr3(.br, blk_third, b.void_type, is_parent, ValueID(blk_next), ValueID(blk_push)) - tos_clone_ref := b.m.add_value(.func_ref, b.str_type, 'tos_clone', b.fn_ids['tos_clone']) + tos_clone_ref := b.m.add_value(.func_ref, b.str_type, 'tos_clone', b.runtime_fn_id('tos_clone')) name_str := b.block_instr2(.call, blk_push, b.str_type, tos_clone_ref, name_ptr) name_alloca := b.block_instr0(.alloca, blk_push, ptr_string) b.block_instr2(.store, blk_push, b.void_type, name_str, name_alloca) name_elem := b.block_instr1(.bitcast, blk_push, ptr_i8, name_alloca) - array_push_ref := b.m.add_value(.func_ref, b.void_type, 'array_push', b.fn_ids['array_push']) + array_push_ref := b.m.add_value(.func_ref, b.void_type, 'array_push', b.runtime_fn_id('array_push')) b.block_instr3(.call, blk_push, b.void_type, array_push_ref, arr_alloca, name_elem) b.block_instr1(.jmp, blk_push, b.void_type, ValueID(blk_loop)) @@ -3664,9 +4292,9 @@ fn (mut b Builder) emit_cstring_from_string(block_id BlockID, value ValueID) Val len := b.block_instr1(.zext, block_id, b.i64_type, len32) one := b.m.get_or_add_const(b.i64_type, '1') alloc_len := b.block_instr2(.add, block_id, b.i64_type, len, one) - malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.fn_ids['malloc']) + malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.runtime_fn_id('malloc')) out_data := b.block_instr2(.call, block_id, ptr_i8, malloc_ref, alloc_len) - memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, block_id, ptr_i8, memcpy_ref, out_data, data, len) zero8 := b.m.get_or_add_const(b.i8_type, '0') term_ptr := b.block_instr2(.add, block_id, ptr_i8, out_data, len) @@ -3688,38 +4316,44 @@ fn (mut b Builder) register_prealloc_allocator_stubs() { ptr_i8 := b.m.type_store.get_ptr(b.i8_type) mut p1 := []TypeID{} p1 << b.i64_type - malloc_id := b.register_synthetic_function('prealloc_malloc', ptr_i8, p1) - b.generate_prealloc_malloc_body(malloc_id) - calloc_id := b.register_synthetic_function('prealloc_calloc', ptr_i8, p1) - b.generate_prealloc_calloc_body(calloc_id) + if malloc_id := b.register_runtime_function('prealloc_malloc', ptr_i8, p1) { + b.generate_prealloc_malloc_body(malloc_id) + } + if calloc_id := b.register_runtime_function('prealloc_calloc', ptr_i8, p1) { + b.generate_prealloc_calloc_body(calloc_id) + } mut p2 := []TypeID{} p2 << b.i64_type p2 << b.i64_type - malloc_align_id := b.register_synthetic_function('prealloc_malloc_align', ptr_i8, p2) - b.generate_prealloc_malloc_align_body(malloc_align_id) + if malloc_align_id := b.register_runtime_function('prealloc_malloc_align', ptr_i8, p2) { + b.generate_prealloc_malloc_align_body(malloc_align_id) + } mut p3 := []TypeID{} p3 << ptr_i8 p3 << b.i64_type p3 << b.i64_type - realloc_id := b.register_synthetic_function('prealloc_realloc', ptr_i8, p3) - b.generate_prealloc_realloc_body(realloc_id) + if realloc_id := b.register_runtime_function('prealloc_realloc', ptr_i8, p3) { + b.generate_prealloc_realloc_body(realloc_id) + } mut realloc_params := []TypeID{} realloc_params << ptr_i8 realloc_params << b.i64_type - v_realloc_id := b.register_synthetic_function('v_realloc', ptr_i8, realloc_params) - b.generate_libc_realloc_body(v_realloc_id) + if v_realloc_id := b.register_runtime_function('v_realloc', ptr_i8, realloc_params) { + b.generate_libc_realloc_body(v_realloc_id) + } - scope_begin_id := b.register_synthetic_function('prealloc_scope_begin', ptr_i8, []TypeID{}) - b.generate_const_ptr_with_params_body(scope_begin_id, []TypeID{}, ptr_i8, '0') + if scope_begin_id := b.register_runtime_function('prealloc_scope_begin', ptr_i8, []TypeID{}) { + b.generate_const_ptr_with_params_body(scope_begin_id, []TypeID{}, ptr_i8, '0') + } } fn (mut b Builder) generate_prealloc_malloc_body(func_id int) { ptr_i8 := b.m.type_store.get_ptr(b.i8_type) entry := b.m.add_block(func_id, 'entry') n := b.func_add_argument(func_id, b.i64_type, 'n') - malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.fn_ids['malloc']) + malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.runtime_fn_id('malloc')) result := b.block_instr2(.call, entry, ptr_i8, malloc_ref, n) b.block_instr1(.ret, entry, b.void_type, result) } @@ -3729,7 +4363,7 @@ fn (mut b Builder) generate_prealloc_calloc_body(func_id int) { entry := b.m.add_block(func_id, 'entry') n := b.func_add_argument(func_id, b.i64_type, 'n') one := b.m.get_or_add_const(b.i64_type, '1') - calloc_ref := b.m.add_value(.func_ref, b.void_type, 'calloc', b.fn_ids['calloc']) + calloc_ref := b.m.add_value(.func_ref, b.void_type, 'calloc', b.runtime_fn_id('calloc')) result := b.block_instr3(.call, entry, ptr_i8, calloc_ref, one, n) b.block_instr1(.ret, entry, b.void_type, result) } @@ -3739,7 +4373,7 @@ fn (mut b Builder) generate_prealloc_malloc_align_body(func_id int) { entry := b.m.add_block(func_id, 'entry') n := b.func_add_argument(func_id, b.i64_type, 'n') _ := b.func_add_argument(func_id, b.i64_type, 'align') - malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.fn_ids['malloc']) + malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.runtime_fn_id('malloc')) result := b.block_instr2(.call, entry, ptr_i8, malloc_ref, n) b.block_instr1(.ret, entry, b.void_type, result) } @@ -3750,7 +4384,7 @@ fn (mut b Builder) generate_prealloc_realloc_body(func_id int) { old_data := b.func_add_argument(func_id, ptr_i8, 'old_data') _ := b.func_add_argument(func_id, b.i64_type, 'old_size') new_size := b.func_add_argument(func_id, b.i64_type, 'new_size') - realloc_ref := b.m.add_value(.func_ref, b.void_type, 'realloc', b.fn_ids['realloc']) + realloc_ref := b.m.add_value(.func_ref, b.void_type, 'realloc', b.runtime_fn_id('realloc')) result := b.block_instr3(.call, entry, ptr_i8, realloc_ref, old_data, new_size) b.block_instr1(.ret, entry, b.void_type, result) } @@ -3760,7 +4394,7 @@ fn (mut b Builder) generate_libc_realloc_body(func_id int) { entry := b.m.add_block(func_id, 'entry') old_data := b.func_add_argument(func_id, ptr_i8, 'old_data') new_size := b.func_add_argument(func_id, b.i64_type, 'new_size') - realloc_ref := b.m.add_value(.func_ref, b.void_type, 'realloc', b.fn_ids['realloc']) + realloc_ref := b.m.add_value(.func_ref, b.void_type, 'realloc', b.runtime_fn_id('realloc')) result := b.block_instr3(.call, entry, ptr_i8, realloc_ref, old_data, new_size) b.block_instr1(.ret, entry, b.void_type, result) } @@ -3770,23 +4404,27 @@ fn (mut b Builder) register_prealloc_atomic_stubs() { ptr_i64 := b.m.type_store.get_ptr(b.i64_type) mut p1_ptr := []TypeID{} p1_ptr << ptr_i64 - load_id := b.register_synthetic_function('v_prealloc_atomic_load_i32', b.i64_type, p1_ptr) - b.generate_atomic_load_i64_body(load_id) + if load_id := b.register_runtime_function('v_prealloc_atomic_load_i32', b.i64_type, p1_ptr) { + b.generate_atomic_load_i64_body(load_id) + } mut p2 := []TypeID{} p2 << ptr_i64 p2 << b.i64_type - add_id := b.register_synthetic_function('v_prealloc_atomic_add_i32', b.i64_type, p2) - b.generate_atomic_add_i64_body(add_id) - store_id := b.register_synthetic_function('v_prealloc_atomic_store_i32', b.i64_type, p2) - b.generate_atomic_store_i64_body(store_id) + if add_id := b.register_runtime_function('v_prealloc_atomic_add_i32', b.i64_type, p2) { + b.generate_atomic_add_i64_body(add_id) + } + if store_id := b.register_runtime_function('v_prealloc_atomic_store_i32', b.i64_type, p2) { + b.generate_atomic_store_i64_body(store_id) + } mut p3 := []TypeID{} p3 << ptr_i64 p3 << b.i64_type p3 << b.i64_type - cas_id := b.register_synthetic_function('v_prealloc_atomic_cas_i32', b.i64_type, p3) - b.generate_atomic_cas_i64_body(cas_id) + if cas_id := b.register_runtime_function('v_prealloc_atomic_cas_i32', b.i64_type, p3) { + b.generate_atomic_cas_i64_body(cas_id) + } } fn (mut b Builder) register_atomic_builtin_stubs() { @@ -3798,71 +4436,85 @@ fn (mut b Builder) register_atomic_builtin_stubs() { mut fence_params := []TypeID{} fence_params << b.i64_type - fence_id := b.register_synthetic_c_function('atomic_thread_fence', b.void_type, fence_params) - b.generate_void_return_body(fence_id) - cpu_relax_id := b.register_synthetic_c_function('cpu_relax', b.void_type, []TypeID{}) - b.generate_void_return_body(cpu_relax_id) + if fence_id := b.register_runtime_c_function('atomic_thread_fence', b.void_type, fence_params) { + b.generate_void_return_body(fence_id) + } + if cpu_relax_id := b.register_runtime_c_function('cpu_relax', b.void_type, []TypeID{}) { + b.generate_void_return_body(cpu_relax_id) + } } fn (mut b Builder) register_atomic_scalar_stubs(suffix string, typ TypeID) { ptr_i8 := b.m.type_store.get_ptr(b.i8_type) mut p1 := []TypeID{} p1 << ptr_i8 - load_id := b.register_synthetic_c_function('atomic_load_${suffix}', typ, p1) - b.generate_atomic_load_body(load_id, typ) + if load_id := b.register_runtime_c_function('atomic_load_${suffix}', typ, p1) { + b.generate_atomic_load_body(load_id, typ) + } mut p2 := []TypeID{} p2 << ptr_i8 p2 << typ - store_id := b.register_synthetic_c_function('atomic_store_${suffix}', b.void_type, p2) - b.generate_atomic_store_body(store_id, typ) - exchange_id := b.register_synthetic_c_function('atomic_exchange_${suffix}', typ, p2) - b.generate_atomic_exchange_body(exchange_id, typ) - fetch_add_id := b.register_synthetic_c_function('atomic_fetch_add_${suffix}', typ, p2) - b.generate_atomic_fetch_add_body(fetch_add_id, typ) - fetch_sub_id := b.register_synthetic_c_function('atomic_fetch_sub_${suffix}', typ, p2) - b.generate_atomic_fetch_sub_body(fetch_sub_id, typ) + if store_id := b.register_runtime_c_function('atomic_store_${suffix}', b.void_type, p2) { + b.generate_atomic_store_body(store_id, typ) + } + if exchange_id := b.register_runtime_c_function('atomic_exchange_${suffix}', typ, p2) { + b.generate_atomic_exchange_body(exchange_id, typ) + } + if fetch_add_id := b.register_runtime_c_function('atomic_fetch_add_${suffix}', typ, p2) { + b.generate_atomic_fetch_add_body(fetch_add_id, typ) + } + if fetch_sub_id := b.register_runtime_c_function('atomic_fetch_sub_${suffix}', typ, p2) { + b.generate_atomic_fetch_sub_body(fetch_sub_id, typ) + } mut p3 := []TypeID{} p3 << ptr_i8 p3 << ptr_i8 p3 << typ - strong_id := b.register_synthetic_c_function('atomic_compare_exchange_strong_${suffix}', - b.i1_type, p3) - b.generate_atomic_compare_exchange_body(strong_id, typ) - weak_id := - b.register_synthetic_c_function('atomic_compare_exchange_weak_${suffix}', b.i1_type, p3) - b.generate_atomic_compare_exchange_body(weak_id, typ) + if strong_id := b.register_runtime_c_function('atomic_compare_exchange_strong_${suffix}', + b.i1_type, p3) { + b.generate_atomic_compare_exchange_body(strong_id, typ) + } + if weak_id := b.register_runtime_c_function('atomic_compare_exchange_weak_${suffix}', b.i1_type, p3) { + b.generate_atomic_compare_exchange_body(weak_id, typ) + } } fn (mut b Builder) register_atomic_ptr_stubs() { ptr_i8 := b.m.type_store.get_ptr(b.i8_type) mut p1 := []TypeID{} p1 << ptr_i8 - load_id := b.register_synthetic_c_function('atomic_load_ptr', ptr_i8, p1) - b.generate_atomic_load_ptr_body(load_id) + if load_id := b.register_runtime_c_function('atomic_load_ptr', ptr_i8, p1) { + b.generate_atomic_load_ptr_body(load_id) + } mut p2 := []TypeID{} p2 << ptr_i8 p2 << ptr_i8 - store_id := b.register_synthetic_c_function('atomic_store_ptr', b.void_type, p2) - b.generate_atomic_store_ptr_body(store_id) - exchange_id := b.register_synthetic_c_function('atomic_exchange_ptr', ptr_i8, p2) - b.generate_atomic_exchange_ptr_body(exchange_id) - fetch_add_id := b.register_synthetic_c_function('atomic_fetch_add_ptr', ptr_i8, p2) - b.generate_atomic_fetch_add_ptr_body(fetch_add_id) - fetch_sub_id := b.register_synthetic_c_function('atomic_fetch_sub_ptr', ptr_i8, p2) - b.generate_atomic_fetch_sub_ptr_body(fetch_sub_id) + if store_id := b.register_runtime_c_function('atomic_store_ptr', b.void_type, p2) { + b.generate_atomic_store_ptr_body(store_id) + } + if exchange_id := b.register_runtime_c_function('atomic_exchange_ptr', ptr_i8, p2) { + b.generate_atomic_exchange_ptr_body(exchange_id) + } + if fetch_add_id := b.register_runtime_c_function('atomic_fetch_add_ptr', ptr_i8, p2) { + b.generate_atomic_fetch_add_ptr_body(fetch_add_id) + } + if fetch_sub_id := b.register_runtime_c_function('atomic_fetch_sub_ptr', ptr_i8, p2) { + b.generate_atomic_fetch_sub_ptr_body(fetch_sub_id) + } mut p3 := []TypeID{} p3 << ptr_i8 p3 << ptr_i8 p3 << b.i64_type - strong_id := - b.register_synthetic_c_function('atomic_compare_exchange_strong_ptr', b.i1_type, p3) - b.generate_atomic_compare_exchange_ptr_body(strong_id) - weak_id := b.register_synthetic_c_function('atomic_compare_exchange_weak_ptr', b.i1_type, p3) - b.generate_atomic_compare_exchange_ptr_body(weak_id) + if strong_id := b.register_runtime_c_function('atomic_compare_exchange_strong_ptr', b.i1_type, p3) { + b.generate_atomic_compare_exchange_ptr_body(strong_id) + } + if weak_id := b.register_runtime_c_function('atomic_compare_exchange_weak_ptr', b.i1_type, p3) { + b.generate_atomic_compare_exchange_ptr_body(weak_id) + } } fn (mut b Builder) generate_void_return_body(func_id int) { @@ -4089,27 +4741,37 @@ fn (mut b Builder) generate_atomic_cas_i64_body(func_id int) { fn (mut b Builder) register_array_string_stubs() { mut p1 := []TypeID{} p1 << b.array_type - array_str_id := b.register_synthetic_function('Array_str', b.str_type, p1) - b.generate_const_string_body(array_str_id, '[]') + if array_str_id := b.register_runtime_function('Array_str', b.str_type, p1) { + b.generate_const_string_body(array_str_id, '[]') + } for name in ['bytestr', 'Array_u8__bytestr', '[]u8.bytestr'] { - bytestr_id := b.register_synthetic_function(name, b.str_type, p1) - b.generate_array_bytestr_body(bytestr_id) + if bytestr_id := b.register_runtime_function(name, b.str_type, p1) { + b.generate_array_bytestr_body(bytestr_id) + } } for name in ['Array_u8__hex', '[]u8.hex'] { - array_hex_id := b.register_synthetic_function(name, b.str_type, p1) - b.generate_array_byte_hex_body(array_hex_id) + if b.m.target.ptr_size == 4 { + if array_hex_id := b.register_runtime_function(name, b.str_type, p1) { + b.generate_const_string_body(array_hex_id, '') + } + } else { + array_hex_id := b.register_synthetic_function(name, b.str_type, p1) + b.generate_array_byte_hex_body(array_hex_id) + } } for name in ['Array_rune__string', '[]rune.string'] { - rune_string_id := b.register_synthetic_function(name, b.str_type, p1) - b.generate_const_string_body(rune_string_id, '') + if rune_string_id := b.register_runtime_function(name, b.str_type, p1) { + b.generate_const_string_body(rune_string_id, '') + } } mut p2 := []TypeID{} p2 << b.array_type p2 << b.str_type for name in ['array_string_join', 'Array_string__join'] { - func_id := b.register_synthetic_function(name, b.str_type, p2) - b.generate_array_string_join_body(func_id) + if func_id := b.register_runtime_function(name, b.str_type, p2) { + b.generate_array_string_join_body(func_id) + } } } @@ -4130,7 +4792,7 @@ fn (mut b Builder) generate_array_string_join_body(func_id int) { b.block_instr2(.store, entry, b.void_type, zero, i_alloca) new_ref := b.m.add_value(.func_ref, b.array_type, 'strings.new_builder', - b.fn_ids['strings.new_builder']) + b.runtime_fn_id('strings.new_builder')) initial_cap := b.m.get_or_add_const(b.i64_type, '16') builder := b.block_instr2(.call, entry, b.array_type, new_ref, initial_cap) b.block_instr2(.store, entry, b.void_type, builder, builder_alloca) @@ -4156,7 +4818,7 @@ fn (mut b Builder) generate_array_string_join_body(func_id int) { b.block_instr3(.br, body, b.void_type, needs_sep, ValueID(write_sep), ValueID(write_elem)) write_ref := b.m.add_value(.func_ref, b.void_type, 'strings.Builder.write_string', - b.fn_ids['strings.Builder.write_string']) + b.runtime_fn_id('strings.Builder.write_string')) b.block_instr3(.call, write_sep, b.void_type, write_ref, builder_alloca, sep) b.block_instr1(.jmp, write_sep, b.void_type, ValueID(write_elem)) @@ -4167,14 +4829,14 @@ fn (mut b Builder) generate_array_string_join_body(func_id int) { elem_ptr_raw) elem := b.block_instr1(.load, write_elem, b.str_type, elem_ptr) write_ref2 := b.m.add_value(.func_ref, b.void_type, 'strings.Builder.write_string', - b.fn_ids['strings.Builder.write_string']) + b.runtime_fn_id('strings.Builder.write_string')) b.block_instr3(.call, write_elem, b.void_type, write_ref2, builder_alloca, elem) next_i := b.block_instr2(.add, write_elem, b.i64_type, i_val, one) b.block_instr2(.store, write_elem, b.void_type, next_i, i_alloca) b.block_instr1(.jmp, write_elem, b.void_type, ValueID(loop)) str_ref := b.m.add_value(.func_ref, b.str_type, 'strings.Builder.str', - b.fn_ids['strings.Builder.str']) + b.runtime_fn_id('strings.Builder.str')) result := b.block_instr2(.call, done, b.str_type, str_ref, builder_alloca) b.block_instr1(.ret, done, b.void_type, result) } @@ -4196,9 +4858,9 @@ fn (mut b Builder) generate_array_bytestr_body(func_id int) { one := b.m.get_or_add_const(b.i64_type, '1') alloc_len := b.block_instr2(.add, entry, b.i64_type, len, one) - malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.fn_ids['malloc']) + malloc_ref := b.m.add_value(.func_ref, b.void_type, 'malloc', b.runtime_fn_id('malloc')) out_data := b.block_instr2(.call, entry, ptr_i8, malloc_ref, alloc_len) - memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, entry, ptr_i8, memcpy_ref, out_data, data, len) zero8 := b.m.get_or_add_const(b.i8_type, '0') term_ptr := b.block_instr2(.add, entry, ptr_i8, out_data, len) @@ -4208,23 +4870,44 @@ fn (mut b Builder) generate_array_bytestr_body(func_id int) { b.block_instr1(.ret, entry, b.void_type, result) } +fn (mut b Builder) register_at_exit_stub() { + mut p1 := []TypeID{} + p1 << b.resolve_type('FnExitCb') + result_type := b.option_type_id('void', true) + if func_id := b.register_runtime_function('at_exit', result_type, p1) { + b.generate_at_exit_stub_body(func_id, result_type, p1) + } +} + +fn (mut b Builder) generate_at_exit_stub_body(func_id int, result_type TypeID, params []TypeID) { + entry := b.m.add_block(func_id, 'at_exit_entry') + for i, param_type in params { + _ := b.func_add_argument(func_id, param_type, 'arg${i}') + } + result := b.block_option_value(entry, result_type, true, ValueID(0)) + b.block_instr1(.ret, entry, b.void_type, result) +} + fn (mut b Builder) register_pthread_compat_stubs() { ptr_i8 := b.m.type_store.get_ptr(b.i8_type) - zero_id := b.register_synthetic_c_function('v3_pthread_zero', b.u64_type, []TypeID{}) - b.generate_const_body_with_params(zero_id, b.u64_type, '0', []TypeID{}) + if zero_id := b.register_runtime_c_function('v3_pthread_zero', b.u64_type, []TypeID{}) { + b.generate_const_body_with_params(zero_id, b.u64_type, '0', []TypeID{}) + } mut p_create := []TypeID{} p_create << ptr_i8 p_create << b.i64_type p_create << ptr_i8 p_create << ptr_i8 - create_id := b.register_synthetic_c_function('v3_pthread_create', b.i32_type, p_create) - b.generate_const_body_with_params(create_id, b.i32_type, '11', p_create) + if create_id := b.register_runtime_c_function('v3_pthread_create', b.i32_type, p_create) { + b.generate_const_body_with_params(create_id, b.i32_type, '11', p_create) + } mut p_setkind := []TypeID{} p_setkind << ptr_i8 p_setkind << b.i32_type - setkind_id := b.register_synthetic_c_function('pthread_rwlockattr_setkind_np', b.i32_type, - p_setkind) - b.generate_const_body_with_params(setkind_id, b.i32_type, '0', p_setkind) + if setkind_id := b.register_runtime_c_function('pthread_rwlockattr_setkind_np', b.i32_type, + p_setkind) { + b.generate_const_body_with_params(setkind_id, b.i32_type, '0', p_setkind) + } } fn (mut b Builder) register_rand_prng_interface_stubs() { @@ -4236,39 +4919,47 @@ fn (mut b Builder) register_rand_prng_interface_stubs() { mut p_new_default := []TypeID{} p_new_default << config_type for name in ['rand.new_default', 'rand__new_default'] { - func_id := b.register_synthetic_function(name, prng_ptr, p_new_default) - b.generate_const_body_with_params(func_id, prng_ptr, '0', p_new_default) + if func_id := b.register_runtime_function(name, prng_ptr, p_new_default) { + b.generate_const_body_with_params(func_id, prng_ptr, '0', p_new_default) + } } mut p_seed := []TypeID{} p_seed << ptr_i8 p_seed << b.array_type for name in ['rand.PRNG.seed', 'rand__PRNG__seed'] { - func_id := b.register_synthetic_function(name, b.void_type, p_seed) - b.generate_noop_body(func_id, p_seed) + if func_id := b.register_runtime_function(name, b.void_type, p_seed) { + b.generate_noop_body(func_id, p_seed) + } } for name in ['rand.PRNG.free', 'rand__PRNG__free'] { - func_id := b.register_synthetic_function(name, b.void_type, p_recv) - b.generate_noop_body(func_id, p_recv) + if func_id := b.register_runtime_function(name, b.void_type, p_recv) { + b.generate_noop_body(func_id, p_recv) + } } for name in ['rand.PRNG.u8', 'rand__PRNG__u8'] { - func_id := b.register_synthetic_function(name, b.u8_type, p_recv) - b.generate_const_body_with_params(func_id, b.u8_type, '0', p_recv) + if func_id := b.register_runtime_function(name, b.u8_type, p_recv) { + b.generate_const_body_with_params(func_id, b.u8_type, '0', p_recv) + } } for name in ['rand.PRNG.u16', 'rand__PRNG__u16'] { - func_id := b.register_synthetic_function(name, b.u16_type, p_recv) - b.generate_const_body_with_params(func_id, b.u16_type, '0', p_recv) + if func_id := b.register_runtime_function(name, b.u16_type, p_recv) { + b.generate_const_body_with_params(func_id, b.u16_type, '0', p_recv) + } } for name in ['rand.PRNG.u32', 'rand__PRNG__u32'] { - func_id := b.register_synthetic_function(name, b.u32_type, p_recv) - b.generate_const_body_with_params(func_id, b.u32_type, '0', p_recv) + if func_id := b.register_runtime_function(name, b.u32_type, p_recv) { + b.generate_const_body_with_params(func_id, b.u32_type, '0', p_recv) + } } for name in ['rand.PRNG.u64', 'rand__PRNG__u64'] { - func_id := b.register_synthetic_function(name, b.u64_type, p_recv) - b.generate_const_body_with_params(func_id, b.u64_type, '0', p_recv) + if func_id := b.register_runtime_function(name, b.u64_type, p_recv) { + b.generate_const_body_with_params(func_id, b.u64_type, '0', p_recv) + } } for name in ['rand.PRNG.block_size', 'rand__PRNG__block_size'] { - func_id := b.register_synthetic_function(name, b.i32_type, p_recv) - b.generate_const_body_with_params(func_id, b.i32_type, '8', p_recv) + if func_id := b.register_runtime_function(name, b.i32_type, p_recv) { + b.generate_const_body_with_params(func_id, b.i32_type, '8', p_recv) + } } } @@ -4277,7 +4968,9 @@ fn (mut b Builder) register_embed_file_interface_stubs() { mut params := []TypeID{} params << b.resolve_type('embed_file.Decoder') params << b.array_type - func_id := b.register_synthetic_function('embed_file.Decoder.decompress', result_type, params) + func_id := b.register_runtime_function('embed_file.Decoder.decompress', result_type, params) or { + return + } entry := b.m.add_block(func_id, 'decompress_entry') for i, param_type in params { _ := b.func_add_argument(func_id, param_type, 'arg${i}') @@ -4308,10 +5001,12 @@ fn (mut b Builder) register_ierror_stubs() { ptr_ierror := b.m.type_store.get_ptr(b.i64_type) mut p1 := []TypeID{} p1 << ptr_ierror - msg_id := b.register_synthetic_function('IError.msg', b.str_type, p1) - b.generate_ierror_msg_body(msg_id, ptr_ierror) - code_id := b.register_synthetic_function('IError.code', b.i64_type, p1) - b.generate_ierror_code_body(code_id, ptr_ierror) + if msg_id := b.register_runtime_function('IError.msg', b.str_type, p1) { + b.generate_ierror_msg_body(msg_id, ptr_ierror) + } + if code_id := b.register_runtime_function('IError.code', b.i64_type, p1) { + b.generate_ierror_code_body(code_id, ptr_ierror) + } } // generate_ierror_msg_body supports generate ierror msg body handling for Builder. @@ -4337,16 +5032,18 @@ fn (mut b Builder) register_fixed_array_contains_stubs() { p3_string << ptr_i8 p3_string << b.i64_type p3_string << b.str_type - contains_string_id := b.register_synthetic_function('fixed_array_contains_string', b.i1_type, - p3_string) - b.generate_fixed_array_contains_string_body(contains_string_id) + if contains_string_id := b.register_runtime_function('fixed_array_contains_string', b.i1_type, + p3_string) { + b.generate_fixed_array_contains_string_body(contains_string_id) + } mut p3_int := []TypeID{} p3_int << ptr_i8 p3_int << b.i64_type p3_int << b.i64_type - contains_int_id := b.register_synthetic_function('fixed_array_contains_int', b.i1_type, p3_int) - b.generate_const_bool_body(contains_int_id, false) + if contains_int_id := b.register_runtime_function('fixed_array_contains_int', b.i1_type, p3_int) { + b.generate_const_bool_body(contains_int_id, false) + } } // generate_fixed_array_contains_string_body @@ -4375,12 +5072,12 @@ fn (mut b Builder) generate_fixed_array_contains_string_body(func_id int) { in_range := b.block_instr2(.lt, blk_loop, b.i1_type, i, len) b.block_instr3(.br, blk_loop, b.void_type, in_range, ValueID(blk_body), ValueID(blk_not_found)) - stride := b.m.get_or_add_const(b.i64_type, '16') + stride := b.m.get_or_add_const(b.i64_type, '${b.m.type_size(b.str_type)}') offset := b.block_instr2(.mul, blk_body, b.i64_type, i, stride) slot := b.block_instr2(.add, blk_body, ptr_i8, arr, offset) slot_string_ptr := b.block_instr1(.bitcast, blk_body, ptr_string, slot) slot_string := b.block_instr1(.load, blk_body, b.str_type, slot_string_ptr) - eq_ref := b.m.add_value(.func_ref, b.void_type, 'string__eq', b.fn_ids['string__eq']) + eq_ref := b.m.add_value(.func_ref, b.void_type, 'string__eq', b.runtime_fn_id('string__eq')) is_eq := b.block_instr3(.call, blk_body, b.i1_type, eq_ref, slot_string, needle) b.block_instr3(.br, blk_body, b.void_type, is_eq, ValueID(blk_found), ValueID(blk_next)) @@ -4406,19 +5103,23 @@ fn (mut b Builder) register_array_contains_stubs() { mut p2_string := []TypeID{} p2_string << b.array_type p2_string << b.str_type - index_string_id := b.register_synthetic_function('array_index_string', b.i64_type, p2_string) - b.generate_array_index_string_body(index_string_id) - contains_string_id := b.register_synthetic_function('array_contains_string', b.i1_type, - p2_string) - b.generate_array_contains_from_index_body(contains_string_id, 'array_index_string', b.str_type) + if index_string_id := b.register_runtime_function('array_index_string', b.i64_type, p2_string) { + b.generate_array_index_string_body(index_string_id) + } + if contains_string_id := b.register_runtime_function('array_contains_string', b.i1_type, + p2_string) { + b.generate_array_contains_from_index_body(contains_string_id, 'array_index_string', b.str_type) + } mut p2_int := []TypeID{} p2_int << b.array_type p2_int << b.i64_type - index_int_id := b.register_synthetic_function('array_index_int', b.i64_type, p2_int) - b.generate_array_index_int_body(index_int_id) - contains_int_id := b.register_synthetic_function('array_contains_int', b.i1_type, p2_int) - b.generate_array_contains_from_index_body(contains_int_id, 'array_index_int', b.i64_type) + if index_int_id := b.register_runtime_function('array_index_int', b.i64_type, p2_int) { + b.generate_array_index_int_body(index_int_id) + } + if contains_int_id := b.register_runtime_function('array_contains_int', b.i1_type, p2_int) { + b.generate_array_contains_from_index_body(contains_int_id, 'array_index_int', b.i64_type) + } } // generate_array_contains_from_index_body @@ -4427,7 +5128,7 @@ fn (mut b Builder) generate_array_contains_from_index_body(func_id int, index_na entry := b.m.add_block(func_id, 'entry') arr := b.func_add_argument(func_id, b.array_type, 'arr') needle := b.func_add_argument(func_id, needle_type, 'needle') - index_ref := b.m.add_value(.func_ref, b.void_type, index_name, b.fn_ids[index_name]) + index_ref := b.m.add_value(.func_ref, b.void_type, index_name, b.runtime_fn_id(index_name)) idx := b.block_instr3(.call, entry, b.i64_type, index_ref, arr, needle) zero := b.m.get_or_add_const(b.i64_type, '0') found := b.block_instr2(.ge, entry, b.i1_type, idx, zero) @@ -4473,7 +5174,7 @@ fn (mut b Builder) generate_array_index_string_body(func_id int) { slot := b.block_instr2(.add, blk_body, ptr_i8, data, offset) slot_string_ptr := b.block_instr1(.bitcast, blk_body, ptr_string, slot) slot_string := b.block_instr1(.load, blk_body, b.str_type, slot_string_ptr) - eq_ref := b.m.add_value(.func_ref, b.void_type, 'string__eq', b.fn_ids['string__eq']) + eq_ref := b.m.add_value(.func_ref, b.void_type, 'string__eq', b.runtime_fn_id('string__eq')) is_eq := b.block_instr3(.call, blk_body, b.i1_type, eq_ref, slot_string, needle) b.block_instr3(.br, blk_body, b.void_type, is_eq, ValueID(blk_found), ValueID(blk_next)) @@ -4546,7 +5247,7 @@ fn (mut b Builder) emit_map_state_alloc(block_id BlockID, key_size ValueID, val_ one := b.m.get_or_add_const(b.i64_type, '1') zero := b.m.get_or_add_const(b.i64_type, '0') state_size := b.m.get_or_add_const(b.i64_type, b.m.type_size(b.map_state_type).str()) - calloc_ref := b.m.add_value(.func_ref, b.void_type, 'calloc', b.fn_ids['calloc']) + calloc_ref := b.m.add_value(.func_ref, b.void_type, 'calloc', b.runtime_fn_id('calloc')) state_raw := b.block_instr3(.call, block_id, ptr_i8, calloc_ref, one, state_size) state := b.block_instr1(.bitcast, block_id, ptr_state, state_raw) keys := b.block_instr3(.call, block_id, ptr_i8, calloc_ref, cap, key_size) @@ -4604,7 +5305,81 @@ fn (mut b Builder) generate_new_map_body(func_id int) { // generate_map_find_body supports generate map find body handling for Builder. fn (mut b Builder) generate_map_find_body(func_id int) { - b.generate_native_map_find_body(func_id) + if b.m.target.ptr_size == 4 { + ptr_i8 := b.m.type_store.get_ptr(b.i8_type) + ptr_map := b.m.type_store.get_ptr(b.map_type) + ptr_i64 := b.m.type_store.get_ptr(b.i64_type) + ptr_string := b.m.type_store.get_ptr(b.str_type) + ptr_state := b.m.type_store.get_ptr(b.map_state_type) + entry := b.m.add_block(func_id, 'entry') + map_ptr := b.func_add_argument(func_id, ptr_map, 'map') + key_ptr := b.func_add_argument(func_id, ptr_i8, 'key') + + alloca_i := b.block_instr0(.alloca, entry, ptr_i64) + zero := b.m.get_or_add_const(b.i64_type, '0') + b.block_instr2(.store, entry, b.void_type, zero, alloca_i) + + blk_has_map := b.m.add_block(func_id, 'map_find_has_map') + blk_loop := b.m.add_block(func_id, 'map_find_loop') + blk_body := b.m.add_block(func_id, 'map_find_body') + blk_string_cmp := b.m.add_block(func_id, 'map_find_string_cmp') + blk_mem_cmp := b.m.add_block(func_id, 'map_find_mem_cmp') + blk_found := b.m.add_block(func_id, 'map_find_found') + blk_next := b.m.add_block(func_id, 'map_find_next') + blk_not_found := b.m.add_block(func_id, 'map_find_not_found') + zero_map := b.m.get_or_add_const(ptr_map, '0') + has_map := b.block_instr2(.ne, entry, b.i1_type, map_ptr, zero_map) + b.block_instr3(.br, entry, b.void_type, has_map, ValueID(blk_has_map), ValueID(blk_not_found)) + + state := b.map_state_ptr(blk_has_map, map_ptr) + zero_state := b.m.get_or_add_const(ptr_state, '0') + has_state := b.block_instr2(.ne, blk_has_map, b.i1_type, state, zero_state) + keys_ptr := b.map_state_field_ptr(blk_has_map, state, 0) + len_ptr := b.map_state_field_ptr(blk_has_map, state, 3) + key_size_ptr := b.map_state_field_ptr(blk_has_map, state, 4) + b.block_instr3(.br, blk_has_map, b.void_type, has_state, ValueID(blk_loop), + ValueID(blk_not_found)) + + i := b.block_instr1(.load, blk_loop, b.i64_type, alloca_i) + len := b.block_instr1(.load, blk_loop, b.i64_type, len_ptr) + in_range := b.block_instr2(.lt, blk_loop, b.i1_type, i, len) + b.block_instr3(.br, blk_loop, b.void_type, in_range, ValueID(blk_body), ValueID(blk_not_found)) + + keys := b.block_instr1(.load, blk_body, ptr_i8, keys_ptr) + key_size := b.block_instr1(.load, blk_body, b.i64_type, key_size_ptr) + offset := b.block_instr2(.mul, blk_body, b.i64_type, i, key_size) + slot_key := b.block_instr2(.add, blk_body, ptr_i8, keys, offset) + string_key_size := b.m.get_or_add_const(b.i64_type, '${b.m.type_size(b.str_type)}') + is_string_key := b.block_instr2(.eq, blk_body, b.i1_type, key_size, string_key_size) + b.block_instr3(.br, blk_body, b.void_type, is_string_key, ValueID(blk_string_cmp), + ValueID(blk_mem_cmp)) + + slot_string_ptr := b.block_instr1(.bitcast, blk_string_cmp, ptr_string, slot_key) + key_string_ptr := b.block_instr1(.bitcast, blk_string_cmp, ptr_string, key_ptr) + slot_string := b.block_instr1(.load, blk_string_cmp, b.str_type, slot_string_ptr) + key_string := b.block_instr1(.load, blk_string_cmp, b.str_type, key_string_ptr) + eq_ref := b.m.add_value(.func_ref, b.void_type, 'fast_string_eq', b.runtime_fn_id('fast_string_eq')) + string_eq := b.block_instr3(.call, blk_string_cmp, b.i1_type, eq_ref, slot_string, key_string) + b.block_instr3(.br, blk_string_cmp, b.void_type, string_eq, ValueID(blk_found), + ValueID(blk_next)) + + memcmp_ref := b.m.add_value(.func_ref, b.void_type, 'memcmp', b.runtime_fn_id('memcmp')) + cmp := b.block_instr4(.call, blk_mem_cmp, b.i64_type, memcmp_ref, slot_key, key_ptr, key_size) + mem_eq := b.block_instr2(.eq, blk_mem_cmp, b.i1_type, cmp, zero) + b.block_instr3(.br, blk_mem_cmp, b.void_type, mem_eq, ValueID(blk_found), ValueID(blk_next)) + + b.block_instr1(.ret, blk_found, b.void_type, i) + + one := b.m.get_or_add_const(b.i64_type, '1') + next_i := b.block_instr2(.add, blk_next, b.i64_type, i, one) + b.block_instr2(.store, blk_next, b.void_type, next_i, alloca_i) + b.block_instr1(.jmp, blk_next, b.void_type, ValueID(blk_loop)) + + not_found := b.m.get_or_add_const(b.i64_type, '-1') + b.block_instr1(.ret, blk_not_found, b.void_type, not_found) + } else { + b.generate_native_map_find_body(func_id) + } } // generate_map_exists_body supports generate map exists body handling for Builder. @@ -4614,7 +5389,7 @@ fn (mut b Builder) generate_map_exists_body(func_id int) { entry := b.m.add_block(func_id, 'entry') map_ptr := b.func_add_argument(func_id, ptr_map, 'map') key_ptr := b.func_add_argument(func_id, ptr_i8, 'key') - find_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_find', b.fn_ids['v3_map_find']) + find_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_find', b.runtime_fn_id('v3_map_find')) idx := b.block_instr3(.call, entry, b.i64_type, find_ref, map_ptr, key_ptr) zero := b.m.get_or_add_const(b.i64_type, '0') found := b.block_instr2(.ge, entry, b.i1_type, idx, zero) @@ -4629,7 +5404,7 @@ fn (mut b Builder) generate_map_get_body(func_id int) { map_ptr := b.func_add_argument(func_id, ptr_map, 'map') key_ptr := b.func_add_argument(func_id, ptr_i8, 'key') zero_ptr := b.func_add_argument(func_id, ptr_i8, 'zero') - find_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_find', b.fn_ids['v3_map_find']) + find_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_find', b.runtime_fn_id('v3_map_find')) idx := b.block_instr3(.call, entry, b.i64_type, find_ref, map_ptr, key_ptr) zero := b.m.get_or_add_const(b.i64_type, '0') found := b.block_instr2(.ge, entry, b.i1_type, idx, zero) @@ -4657,7 +5432,7 @@ fn (mut b Builder) generate_map_get_check_body(func_id int) { entry := b.m.add_block(func_id, 'entry') map_ptr := b.func_add_argument(func_id, ptr_map, 'map') key_ptr := b.func_add_argument(func_id, ptr_i8, 'key') - find_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_find', b.fn_ids['v3_map_find']) + find_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_find', b.runtime_fn_id('v3_map_find')) idx := b.block_instr3(.call, entry, b.i64_type, find_ref, map_ptr, key_ptr) zero := b.m.get_or_add_const(b.i64_type, '0') found := b.block_instr2(.ge, entry, b.i1_type, idx, zero) @@ -4685,7 +5460,7 @@ fn (mut b Builder) generate_map_get_key_check_body(func_id int) { entry := b.m.add_block(func_id, 'entry') map_ptr := b.func_add_argument(func_id, ptr_map, 'map') key_ptr := b.func_add_argument(func_id, ptr_i8, 'key') - find_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_find', b.fn_ids['v3_map_find']) + find_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_find', b.runtime_fn_id('v3_map_find')) idx := b.block_instr3(.call, entry, b.i64_type, find_ref, map_ptr, key_ptr) zero := b.m.get_or_add_const(b.i64_type, '0') found := b.block_instr2(.ge, entry, b.i1_type, idx, zero) @@ -4715,10 +5490,10 @@ fn (mut b Builder) generate_map_set_default_body(func_id int) { map_ptr := b.func_add_argument(func_id, ptr_map, 'map') key_ptr := b.func_add_argument(func_id, ptr_i8, 'key') val_ptr := b.func_add_argument(func_id, ptr_i8, 'val') - key_size := b.m.get_or_add_const(b.i64_type, '16') + key_size := b.m.get_or_add_const(b.i64_type, '${b.m.type_size(b.str_type)}') val_size := b.m.get_or_add_const(b.i64_type, '8') fn_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_set_sized', - b.fn_ids['v3_map_set_sized']) + b.runtime_fn_id('v3_map_set_sized')) mut args := []ValueID{} args << fn_ref args << map_ptr @@ -4769,7 +5544,7 @@ fn (mut b Builder) generate_map_set_sized_body(func_id int) { key_size_ptr := b.map_state_field_ptr(blk_ready, state, 4) val_size_ptr := b.map_state_field_ptr(blk_ready, state, 5) - find_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_find', b.fn_ids['v3_map_find']) + find_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_find', b.runtime_fn_id('v3_map_find')) idx := b.block_instr3(.call, blk_ready, b.i64_type, find_ref, map_ptr, key_ptr) found := b.block_instr2(.ge, blk_ready, b.i1_type, idx, zero) b.block_instr3(.br, blk_ready, b.void_type, found, ValueID(blk_update), ValueID(blk_insert)) @@ -4778,7 +5553,7 @@ fn (mut b Builder) generate_map_set_sized_body(func_id int) { val_size_update := b.block_instr1(.load, blk_update, b.i64_type, val_size_ptr) update_off := b.block_instr2(.mul, blk_update, b.i64_type, idx, val_size_update) update_dest := b.block_instr2(.add, blk_update, ptr_i8, vals_update, update_off) - memcpy_ref_update := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref_update := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, blk_update, ptr_i8, memcpy_ref_update, update_dest, val_ptr, val_size_update) b.block_instr0(.ret, blk_update, b.void_type) @@ -4800,8 +5575,8 @@ fn (mut b Builder) generate_map_set_sized_body(func_id int) { new_cap := b.block_instr2(.add, blk_grow, b.i64_type, double_cap, eight) key_bytes := b.block_instr2(.mul, blk_grow, b.i64_type, new_cap, key_size_grow) val_bytes := b.block_instr2(.mul, blk_grow, b.i64_type, new_cap, val_size_grow) - realloc_ref_keys := b.m.add_value(.func_ref, b.void_type, 'realloc', b.fn_ids['realloc']) - realloc_ref_vals := b.m.add_value(.func_ref, b.void_type, 'realloc', b.fn_ids['realloc']) + realloc_ref_keys := b.m.add_value(.func_ref, b.void_type, 'realloc', b.runtime_fn_id('realloc')) + realloc_ref_vals := b.m.add_value(.func_ref, b.void_type, 'realloc', b.runtime_fn_id('realloc')) keys_new := b.block_instr3(.call, blk_grow, ptr_i8, realloc_ref_keys, keys_old, key_bytes) vals_new := b.block_instr3(.call, blk_grow, ptr_i8, realloc_ref_vals, vals_old, val_bytes) b.block_instr2(.store, blk_grow, b.void_type, keys_new, keys_ptr) @@ -4817,8 +5592,8 @@ fn (mut b Builder) generate_map_set_sized_body(func_id int) { val_off := b.block_instr2(.mul, blk_store, b.i64_type, len, val_size) key_dest := b.block_instr2(.add, blk_store, ptr_i8, keys, key_off) val_dest := b.block_instr2(.add, blk_store, ptr_i8, vals, val_off) - memcpy_ref_key := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) - memcpy_ref_val := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref_key := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) + memcpy_ref_val := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, blk_store, ptr_i8, memcpy_ref_key, key_dest, key_ptr, key_size) b.block_instr4(.call, blk_store, ptr_i8, memcpy_ref_val, val_dest, val_ptr, val_size) one := b.m.get_or_add_const(b.i64_type, '1') @@ -4862,7 +5637,7 @@ fn (mut b Builder) generate_array_new_body(func_id int) { blk_fields := b.m.add_block(func_id, 'array_new_fields') b.block_instr3(.br, blk_init, b.void_type, has_cap, ValueID(blk_alloc), ValueID(blk_empty)) - fn_ref := b.m.add_value(.func_ref, b.void_type, 'calloc', b.fn_ids['calloc']) + fn_ref := b.m.add_value(.func_ref, b.void_type, 'calloc', b.runtime_fn_id('calloc')) allocated_data := b.block_instr3(.call, blk_alloc, ptr_i8, fn_ref, final_cap, elem_size) b.block_instr2(.store, blk_alloc, b.void_type, allocated_data, alloca_data) b.block_instr1(.jmp, blk_alloc, b.void_type, ValueID(blk_fields)) @@ -5025,7 +5800,7 @@ fn (mut b Builder) generate_array_eq_string_body(func_id int) { right_elem_ptr := b.block_instr1(.bitcast, body, ptr_string, right_elem_raw) left_elem := b.block_instr1(.load, body, b.str_type, left_elem_ptr) right_elem := b.block_instr1(.load, body, b.str_type, right_elem_ptr) - string_eq_ref := b.m.add_value(.func_ref, b.i1_type, 'string__eq', b.fn_ids['string__eq']) + string_eq_ref := b.m.add_value(.func_ref, b.i1_type, 'string__eq', b.runtime_fn_id('string__eq')) elem_eq := b.block_instr3(.call, body, b.i1_type, string_eq_ref, left_elem, right_elem) next := b.m.add_block(func_id, 'array_eq_string_next') b.block_instr3(.br, body, b.void_type, elem_eq, ValueID(next), ValueID(return_false)) @@ -5074,7 +5849,7 @@ fn (mut b Builder) generate_array_eq_raw_body(func_id int) { left_data := b.block_instr1(.load, compare, ptr_i8, left_data_ptr) right_data := b.block_instr1(.load, compare, ptr_i8, right_data_ptr) byte_count := b.block_instr2(.mul, compare, b.i64_type, left_len, elem_size) - memcmp_ref := b.m.add_value(.func_ref, b.i64_type, 'memcmp', b.fn_ids['memcmp']) + memcmp_ref := b.m.add_value(.func_ref, b.i64_type, 'memcmp', b.runtime_fn_id('memcmp')) cmp := b.block_instr4(.call, compare, b.i64_type, memcmp_ref, left_data, right_data, byte_count) is_same := b.block_instr2(.eq, compare, b.i1_type, cmp, zero64) b.block_instr3(.br, compare, b.void_type, is_same, ValueID(return_true), ValueID(return_false)) @@ -5113,8 +5888,9 @@ fn (mut b Builder) generate_array_eq_array_body(func_id int) { // register_arguments_stub updates register arguments stub state for ssa. fn (mut b Builder) register_arguments_stub() { - arguments_id := b.register_synthetic_function('arguments', b.array_type, []TypeID{}) - b.generate_arguments_body(arguments_id) + if arguments_id := b.register_runtime_function('arguments', b.array_type, []TypeID{}) { + b.generate_arguments_body(arguments_id) + } } // generate_arguments_body supports generate arguments body handling for Builder. @@ -5131,11 +5907,11 @@ fn (mut b Builder) generate_arguments_body(func_id int) { argc := b.block_instr1(.load, entry, b.i64_type, argc_global) argv := b.block_instr1(.load, entry, ptr_ptr_i8, argv_global) - elem_size := b.m.get_or_add_const(b.i64_type, '16') + elem_size := b.m.get_or_add_const(b.i64_type, '${b.m.type_size(b.str_type)}') zero := b.m.get_or_add_const(b.i64_type, '0') one := b.m.get_or_add_const(b.i64_type, '1') - ptr_size := b.m.get_or_add_const(b.i64_type, '8') - new_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.fn_ids['array_new']) + ptr_size := b.m.get_or_add_const(b.i64_type, '${b.m.target.ptr_size}') + new_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.runtime_fn_id('array_new')) arr := b.block_instr4(.call, entry, b.array_type, new_ref, elem_size, zero, argc) alloca_arr := b.block_instr0(.alloca, entry, ptr_array) @@ -5155,7 +5931,7 @@ fn (mut b Builder) generate_arguments_body(func_id int) { argv_off := b.block_instr2(.mul, body, b.i64_type, i, ptr_size) argv_slot := b.block_instr2(.add, body, ptr_ptr_i8, argv, argv_off) cstr := b.block_instr1(.load, body, ptr_i8, argv_slot) - tos_clone_ref := b.m.add_value(.func_ref, b.str_type, 'tos_clone', b.fn_ids['tos_clone']) + tos_clone_ref := b.m.add_value(.func_ref, b.str_type, 'tos_clone', b.runtime_fn_id('tos_clone')) arg_string := b.block_instr2(.call, body, b.str_type, tos_clone_ref, cstr) data_ptr := b.block_struct_field_ptr(body, alloca_arr, b.array_type, 0) @@ -5197,14 +5973,14 @@ fn (mut b Builder) generate_array_clone_body(func_id int) { elem_size32 := b.block_instr1(.load, entry, b.i32_type, elem_size_ptr) elem_size := b.block_instr1(.zext, entry, b.i64_type, elem_size32) - new_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.fn_ids['array_new']) + new_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.runtime_fn_id('array_new')) clone := b.block_instr4(.call, entry, b.array_type, new_ref, elem_size, len, cap) b.block_instr2(.store, entry, b.void_type, clone, alloca_clone) clone_data_ptr := b.block_struct_field_ptr(entry, alloca_clone, b.array_type, 0) clone_data := b.block_instr1(.load, entry, ptr_i8, clone_data_ptr) copy_size := b.block_instr2(.mul, entry, b.i64_type, len, elem_size) - memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, entry, ptr_i8, memcpy_ref, clone_data, data, copy_size) result := b.block_instr1(.load, entry, b.array_type, alloca_clone) b.block_instr1(.ret, entry, b.void_type, result) @@ -5233,14 +6009,14 @@ fn (mut b Builder) generate_array_clone_ptr_body(func_id int) { elem_size32 := b.block_instr1(.load, entry, b.i32_type, elem_size_ptr) elem_size := b.block_instr1(.zext, entry, b.i64_type, elem_size32) - new_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.fn_ids['array_new']) + new_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.runtime_fn_id('array_new')) clone := b.block_instr4(.call, entry, b.array_type, new_ref, elem_size, len, cap) b.block_instr2(.store, entry, b.void_type, clone, alloca_clone) clone_data_ptr := b.block_struct_field_ptr(entry, alloca_clone, b.array_type, 0) clone_data := b.block_instr1(.load, entry, ptr_i8, clone_data_ptr) copy_size := b.block_instr2(.mul, entry, b.i64_type, len, elem_size) - memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, entry, ptr_i8, memcpy_ref, clone_data, data, copy_size) result := b.block_instr1(.load, entry, b.array_type, alloca_clone) b.block_instr1(.ret, entry, b.void_type, result) @@ -5271,7 +6047,7 @@ fn (mut b Builder) generate_array_repeat_to_depth_body(func_id int) { elem_size := b.block_instr1(.zext, entry, b.i64_type, elem_size32) total_len := b.block_instr2(.mul, entry, b.i64_type, len, count) - new_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.fn_ids['array_new']) + new_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', b.runtime_fn_id('array_new')) out := b.block_instr4(.call, entry, b.array_type, new_ref, elem_size, total_len, total_len) b.block_instr2(.store, entry, b.void_type, out, alloca_out) @@ -5293,7 +6069,7 @@ fn (mut b Builder) generate_array_repeat_to_depth_body(func_id int) { dest_off := b.block_instr2(.mul, body, b.i64_type, i, chunk_size) dest := b.block_instr2(.add, body, ptr_i8, out_data, dest_off) - memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, body, ptr_i8, memcpy_ref, dest, data, chunk_size) next_i := b.block_instr2(.add, body, b.i64_type, i, one) b.block_instr2(.store, body, b.void_type, next_i, alloca_i) @@ -5328,8 +6104,19 @@ fn (mut b Builder) generate_array_push_body(func_id int) { two := b.m.get_or_add_const(b.i64_type, '2') double_cap := b.block_instr2(.mul, blk_grow, b.i64_type, cap, two) new_cap := b.block_instr2(.add, blk_grow, b.i64_type, double_cap, two) - grow_ref := b.m.add_value(.func_ref, b.void_type, '__ssa_array_grow', b.fn_ids['__ssa_array_grow']) - b.block_instr3(.call, blk_grow, b.void_type, grow_ref, arr_ptr, new_cap) + if b.m.target.ptr_size == 4 { + old_data := b.block_instr1(.load, blk_grow, ptr_i8, data_ptr) + elem_size_grow32 := b.block_instr1(.load, blk_grow, b.i32_type, elem_size_ptr) + elem_size_grow := b.block_instr1(.zext, blk_grow, b.i64_type, elem_size_grow32) + new_size := b.block_instr2(.mul, blk_grow, b.i64_type, new_cap, elem_size_grow) + realloc_ref := b.m.add_value(.func_ref, b.void_type, 'realloc', b.runtime_fn_id('realloc')) + new_data := b.block_instr3(.call, blk_grow, ptr_i8, realloc_ref, old_data, new_size) + b.block_instr2(.store, blk_grow, b.void_type, new_data, data_ptr) + b.block_instr2(.store, blk_grow, b.void_type, new_cap, cap_ptr) + } else { + grow_ref := b.m.add_value(.func_ref, b.void_type, '__ssa_array_grow', b.fn_ids['__ssa_array_grow']) + b.block_instr3(.call, blk_grow, b.void_type, grow_ref, arr_ptr, new_cap) + } b.block_instr1(.jmp, blk_grow, b.void_type, ValueID(blk_store)) data := b.block_instr1(.load, blk_store, ptr_i8, data_ptr) @@ -5337,7 +6124,7 @@ fn (mut b Builder) generate_array_push_body(func_id int) { elem_size := b.block_instr1(.zext, blk_store, b.i64_type, elem_size32) offset := b.block_instr2(.mul, blk_store, b.i64_type, len, elem_size) dest := b.block_instr2(.add, blk_store, ptr_i8, data, offset) - memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, blk_store, ptr_i8, memcpy_ref, dest, elem_ptr, elem_size) one := b.m.get_or_add_const(b.i64_type, '1') new_len := b.block_instr2(.add, blk_store, b.i64_type, len, one) @@ -5379,8 +6166,19 @@ fn (mut b Builder) generate_array_push_many_body(func_id int) { two := b.m.get_or_add_const(b.i64_type, '2') new_cap_base := b.block_instr2(.mul, blk_grow, b.i64_type, new_len, two) new_cap := b.block_instr2(.add, blk_grow, b.i64_type, new_cap_base, two) - grow_ref := b.m.add_value(.func_ref, b.void_type, '__ssa_array_grow', b.fn_ids['__ssa_array_grow']) - b.block_instr3(.call, blk_grow, b.void_type, grow_ref, arr_ptr, new_cap) + if b.m.target.ptr_size == 4 { + old_data := b.block_instr1(.load, blk_grow, ptr_i8, data_ptr) + elem_size_grow32 := b.block_instr1(.load, blk_grow, b.i32_type, elem_size_ptr) + elem_size_grow := b.block_instr1(.zext, blk_grow, b.i64_type, elem_size_grow32) + new_size := b.block_instr2(.mul, blk_grow, b.i64_type, new_cap, elem_size_grow) + realloc_ref := b.m.add_value(.func_ref, b.void_type, 'realloc', b.runtime_fn_id('realloc')) + new_data := b.block_instr3(.call, blk_grow, ptr_i8, realloc_ref, old_data, new_size) + b.block_instr2(.store, blk_grow, b.void_type, new_data, data_ptr) + b.block_instr2(.store, blk_grow, b.void_type, new_cap, cap_ptr) + } else { + grow_ref := b.m.add_value(.func_ref, b.void_type, '__ssa_array_grow', b.fn_ids['__ssa_array_grow']) + b.block_instr3(.call, blk_grow, b.void_type, grow_ref, arr_ptr, new_cap) + } b.block_instr1(.jmp, blk_grow, b.void_type, ValueID(blk_copy)) data := b.block_instr1(.load, blk_copy, ptr_i8, data_ptr) @@ -5389,7 +6187,7 @@ fn (mut b Builder) generate_array_push_many_body(func_id int) { offset := b.block_instr2(.mul, blk_copy, b.i64_type, len, elem_size) dest := b.block_instr2(.add, blk_copy, ptr_i8, data, offset) copy_size := b.block_instr2(.mul, blk_copy, b.i64_type, count, elem_size) - memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.fn_ids['memcpy']) + memcpy_ref := b.m.add_value(.func_ref, b.void_type, 'memcpy', b.runtime_fn_id('memcpy')) b.block_instr4(.call, blk_copy, ptr_i8, memcpy_ref, dest, src_ptr, copy_size) b.block_instr2(.store, blk_copy, b.void_type, new_len, len_ptr) b.block_instr0(.ret, blk_copy, b.void_type) @@ -5413,7 +6211,7 @@ fn (mut b Builder) generate_array_push_many_array_body(func_id int) { len32 := b.block_instr1(.load, entry, b.i32_type, len_ptr) len := b.block_instr1(.zext, entry, b.i64_type, len32) push_many_ref := b.m.add_value(.func_ref, b.void_type, 'array.push_many', - b.fn_ids['array.push_many']) + b.runtime_fn_id('array.push_many')) b.block_instr4(.call, entry, b.void_type, push_many_ref, arr_ptr, data, len) b.block_instr0(.ret, entry, b.void_type) } @@ -5423,8 +6221,9 @@ fn (mut b Builder) register_string_eq_stub() { mut p2 := []TypeID{} p2 << b.str_type p2 << b.str_type - func_id := b.register_synthetic_function('string__eq', b.i1_type, p2) - b.generate_string_eq_body(func_id) + if func_id := b.register_runtime_function('string__eq', b.i1_type, p2) { + b.generate_string_eq_body(func_id) + } } // register_fast_string_eq_stub updates register fast string eq stub state for ssa. @@ -5432,8 +6231,9 @@ fn (mut b Builder) register_fast_string_eq_stub() { mut p2 := []TypeID{} p2 << b.str_type p2 << b.str_type - func_id := b.register_synthetic_function('fast_string_eq', b.i1_type, p2) - b.generate_string_eq_body(func_id) + if func_id := b.register_runtime_function('fast_string_eq', b.i1_type, p2) { + b.generate_string_eq_body(func_id) + } } // register_string_lt_stub updates register string lt stub state for ssa. @@ -5441,8 +6241,9 @@ fn (mut b Builder) register_string_lt_stub() { mut p2 := []TypeID{} p2 << b.str_type p2 << b.str_type - func_id := b.register_synthetic_function('string__lt', b.i1_type, p2) - b.generate_string_lt_body(func_id) + if func_id := b.register_runtime_function('string__lt', b.i1_type, p2) { + b.generate_string_lt_body(func_id) + } } // register_string_trim_stubs updates register string trim stubs state for ssa. @@ -5450,8 +6251,9 @@ fn (mut b Builder) register_string_trim_stubs() { mut p2 := []TypeID{} p2 << b.str_type p2 << b.str_type - trim_right_id := b.register_synthetic_function('string.trim_right', b.str_type, p2) - b.generate_string_trim_right_body(trim_right_id) + if trim_right_id := b.register_runtime_function('string.trim_right', b.str_type, p2) { + b.generate_string_trim_right_body(trim_right_id) + } } // generate_string_trim_right_body supports generate string trim right body handling for Builder. @@ -5537,7 +6339,7 @@ fn (mut b Builder) generate_string_trim_right_body(func_id int) { final_len := b.block_instr1(.load, done, b.i64_type, alloca_end) mut result := ValueID(0) for substr_name in ['string.substr', 'string__substr', 'substr'] { - if substr_id := b.fn_ids[substr_name] { + if substr_id := b.runtime_fn_lookup(substr_name) { substr_ref := b.m.add_value(.func_ref, b.str_type, substr_name, substr_id) result = b.block_instr4(.call, done, b.str_type, substr_ref, s, zero64, final_len) break @@ -5555,12 +6357,14 @@ fn (mut b Builder) register_string_last_part_stubs() { p2 << b.str_type p2 << b.str_type for name in ['string.all_before_last', 'string__all_before_last', 'all_before_last'] { - func_id := b.register_synthetic_function(name, b.str_type, p2) - b.generate_string_all_last_body(func_id, true) + if func_id := b.register_runtime_function(name, b.str_type, p2) { + b.generate_string_all_last_body(func_id, true) + } } for name in ['string.all_after_last', 'string__all_after_last', 'all_after_last'] { - func_id := b.register_synthetic_function(name, b.str_type, p2) - b.generate_string_all_last_body(func_id, false) + if func_id := b.register_runtime_function(name, b.str_type, p2) { + b.generate_string_all_last_body(func_id, false) + } } } @@ -5615,7 +6419,7 @@ fn (mut b Builder) generate_string_all_last_body(func_id int, before bool) { b.block_instr3(.br, loop, b.void_type, in_range, ValueID(body), ValueID(done)) slot := b.block_instr2(.add, body, ptr_i8, s_data, i) - memcmp_ref := b.m.add_value(.func_ref, b.void_type, 'memcmp', b.fn_ids['memcmp']) + memcmp_ref := b.m.add_value(.func_ref, b.void_type, 'memcmp', b.runtime_fn_id('memcmp')) cmp := b.block_instr4(.call, body, b.i64_type, memcmp_ref, slot, sub_data, sub_len) is_match := b.block_instr2(.eq, body, b.i1_type, cmp, zero) b.block_instr3(.br, body, b.void_type, is_match, ValueID(match_block), ValueID(next)) @@ -5677,7 +6481,7 @@ fn (mut b Builder) generate_string_eq_body(func_id int) { b.block_instr1(.ret, blk_false, b.void_type, false_val) a_len64 := b.block_instr1(.zext, blk_cmp, b.i64_type, a_len) - fn_ref := b.m.add_value(.func_ref, b.void_type, 'memcmp', b.fn_ids['memcmp']) + fn_ref := b.m.add_value(.func_ref, b.void_type, 'memcmp', b.runtime_fn_id('memcmp')) cmp := b.block_instr4(.call, blk_cmp, b.i64_type, fn_ref, a_str, b_str, a_len64) is_eq := b.block_instr2(.eq, blk_cmp, b.i1_type, cmp, zero_64) b.block_instr1(.ret, blk_cmp, b.void_type, is_eq) @@ -5727,7 +6531,7 @@ fn (mut b Builder) generate_string_lt_body(func_id int) { min_len := b.block_instr1(.load, blk_cmp, b.i32_type, alloca_min) min_len64 := b.block_instr1(.zext, blk_cmp, b.i64_type, min_len) - fn_ref := b.m.add_value(.func_ref, b.void_type, 'memcmp', b.fn_ids['memcmp']) + fn_ref := b.m.add_value(.func_ref, b.void_type, 'memcmp', b.runtime_fn_id('memcmp')) // memcmp returns C int; its upper register bits are outside the ABI result. cmp32 := b.block_instr4(.call, blk_cmp, b.i32_type, fn_ref, a_str, b_str, min_len64) cmp := b.block_instr1(.sext, blk_cmp, b.i64_type, cmp32) @@ -5960,8 +6764,12 @@ fn (mut b Builder) generate_wyhash_body(func_id int) { fn (mut b Builder) build_functions() { mut cur_module := '' for node in b.a.nodes { + if node.kind == .file { + cur_module = '' + continue + } if node.kind == .module_decl { - cur_module = node.value + cur_module = b.source_module_name(node) continue } if node.kind == .fn_decl { @@ -5974,7 +6782,7 @@ fn (mut b Builder) build_functions() { b.mark_fn_prototype(fn_name) continue } - if b.used_fns.len > 0 && !b.source_fn_is_used(node.value, cur_module) { + if (b.exact_used_fns || b.used_fns.len > 0) && !b.source_fn_is_used(node.value, cur_module) { continue } // Hot reload: only the named function's body is materialized. @@ -6020,7 +6828,7 @@ fn (mut b Builder) register_type_aliases() { mut cur_module := '' for node in b.a.nodes { if node.kind == .module_decl { - cur_module = node.value + cur_module = b.source_module_name(node) continue } // A type_decl with no children and a non-empty `typ` is an alias @@ -6191,6 +6999,9 @@ fn (b &Builder) fn_is_used_for_decl(name string, qualified_method bool) bool { fn (b &Builder) source_fn_is_used(name string, module_name string) bool { fn_name := ssa_fn_name_in_module(module_name, name) + if b.exact_used_fns { + return b.used_fns[fn_name] + } // Method declarations include their receiver; a bare libc name is unrelated. qualified_method := name.replace('__', '.').contains('.') if module_name.len > 0 && module_name != 'main' && module_name != 'builtin' { @@ -6318,6 +7129,9 @@ fn (mut b Builder) reset_function_state() { b.mut_param_names.clear() b.sum_guard_variants.clear() b.label_blocks.clear() + b.break_labels.clear() + b.continue_labels.clear() + b.pending_loop_label = '' b.defer_body_ids.clear() b.defer_active_slots.clear() b.pending_loop_label = '' @@ -6350,6 +7164,13 @@ fn (mut b Builder) reset_function_state() { } } } + if b.source_modules.len > 0 && b.cur_module !in ['', 'main', 'builtin'] { + for name, val_id in b.global_vars { + if name.starts_with(b.cur_module + '.') { + b.vars[name[b.cur_module.len + 1..]] = val_id + } + } + } } // fn_body_ids supports fn body ids handling for Builder. @@ -6614,9 +7435,7 @@ fn (mut b Builder) build_stmt(id flat.NodeId) { b.build_match_stmt(node) } .block { - for i in 0 .. node.children_count { - b.build_stmt(b.a.child(&node, i)) - } + b.build_scoped_stmts(node, 0) } .assert_stmt { b.build_assert(node) @@ -6632,6 +7451,35 @@ fn (mut b Builder) build_stmt(id flat.NodeId) { } } +fn (b &Builder) save_scope() BuilderScope { + if b.m.target.ptr_size != 4 { + return BuilderScope{} + } + return BuilderScope{ + active: true + vars: b.vars.clone() + type_names: b.var_type_names.clone() + } +} + +fn (mut b Builder) restore_scope(scope BuilderScope) { + if scope.active { + b.vars = scope.vars.clone() + b.var_type_names = scope.type_names.clone() + } +} + +fn (mut b Builder) build_scoped_stmts(node flat.Node, start int) { + scope := b.save_scope() + for i in start .. node.children_count { + b.build_stmt(b.a.child(&node, i)) + if scope.active && b.current_block_terminated() { + break + } + } + b.restore_scope(scope) +} + // build_multi_return_value builds multi return value data for ssa. fn (mut b Builder) build_multi_return_value(ret_type TypeID, node flat.Node) ValueID { alloca := b.emit0(.alloca, b.m.type_store.get_ptr(ret_type)) @@ -6717,6 +7565,13 @@ fn (mut b Builder) build_assign_rhs(lhs flat.Node, rhs_id flat.NodeId) ValueID { } fn (mut b Builder) build_assign(node flat.Node) { + mut parallel_values := []ValueID{} + if b.m.target.ptr_size == 4 && node.op == .assign && node.children_count > 2 { + for index := 0; index < node.children_count; index += 2 { + lhs := b.a.child_node(&node, index) + parallel_values << b.build_assign_rhs(*lhs, b.a.child(&node, index + 1)) + } + } mut i := 0 for i < node.children_count { lhs_id := b.a.child(&node, i) @@ -6724,14 +7579,20 @@ fn (mut b Builder) build_assign(node flat.Node) { lhs := b.a.nodes[int(lhs_id)] if lhs.kind == .ident && lhs.value == '_' { - b.build_expr(rhs_id) + if parallel_values.len == 0 { + b.build_expr(rhs_id) + } i += 2 continue } if lhs.kind == .ident { if addr := b.vars[lhs.value] { if node.op == .assign { - rhs_val := b.build_assign_rhs(lhs, rhs_id) + rhs_val := if parallel_values.len > 0 { + parallel_values[i / 2] + } else { + b.build_assign_rhs(lhs, rhs_id) + } b.emit2(.store, b.void_type, b.coerce_value_for_param(rhs_val, b.deref_type(addr)), addr) } else { @@ -6745,7 +7606,7 @@ fn (mut b Builder) build_assign(node flat.Node) { cur := b.emit1(.load, b.deref_type(target_addr), target_addr) rhs_val := b.coerce_int_value(b.build_assign_rhs(lhs, rhs_id), b.value_type(cur)) - op := b.compound_to_op(node.op) + op := b.compound_to_op(node.op, b.value_type(cur)) result := b.emit2(op, b.value_type(cur), cur, rhs_val) b.emit2(.store, b.void_type, result, target_addr) } @@ -6761,14 +7622,18 @@ fn (mut b Builder) build_assign(node flat.Node) { b.build_lvalue_addr(lhs_id) } if node.op == .assign { - rhs_val := b.build_assign_rhs(lhs, rhs_id) + rhs_val := if parallel_values.len > 0 { + parallel_values[i / 2] + } else { + b.build_assign_rhs(lhs, rhs_id) + } b.emit2(.store, b.void_type, b.coerce_value_for_param(rhs_val, b.deref_type(addr)), addr) } else { field_type := b.deref_type(addr) cur := b.emit1(.load, field_type, addr) rhs_val := b.coerce_int_value(b.build_assign_rhs(lhs, rhs_id), field_type) - op := b.compound_to_op(node.op) + op := b.compound_to_op(node.op, b.value_type(cur)) result := b.emit2(op, field_type, cur, rhs_val) b.emit2(.store, b.void_type, result, addr) } @@ -6795,7 +7660,7 @@ fn (mut b Builder) build_selector_assign(node flat.Node) { field_type := b.deref_type(field_ptr) cur := b.emit1(.load, field_type, field_ptr) rhs_val := b.build_expr(rhs_id) - op := b.compound_to_op(node.op) + op := b.compound_to_op(node.op, b.value_type(cur)) result := b.emit2(op, field_type, cur, rhs_val) b.emit2(.store, b.void_type, result, field_ptr) } @@ -6819,7 +7684,7 @@ fn (mut b Builder) build_index_assign(node flat.Node) { field_type := b.deref_type(addr) cur := b.emit1(.load, field_type, addr) rhs_val := b.build_expr(rhs_id) - op := b.compound_to_op(node.op) + op := b.compound_to_op(node.op, b.value_type(cur)) result := b.emit2(op, field_type, cur, rhs_val) b.emit2(.store, b.void_type, result, addr) } @@ -6829,6 +7694,7 @@ fn (mut b Builder) build_index_assign(node flat.Node) { // build_for builds for data for ssa. fn (mut b Builder) build_for(node flat.Node) { + scope := b.save_scope() label := b.take_pending_loop_label() init_id := b.a.child(&node, 0) cond_id := b.a.child(&node, 1) @@ -6863,9 +7729,7 @@ fn (mut b Builder) build_for(node flat.Node) { cond_block }) - for i in 3 .. node.children_count { - b.build_stmt(b.a.child(&node, i)) - } + b.build_scoped_stmts(node, 3) b.loop_targets.delete_last() blk := b.m.blocks[b.cur_block] @@ -6887,6 +7751,7 @@ fn (mut b Builder) build_for(node flat.Node) { } b.cur_block = exit_block + b.restore_scope(scope) } // build_for_in builds for in data for ssa. @@ -7198,9 +8063,7 @@ fn (mut b Builder) build_map_for_in(node flat.Node, key_id flat.NodeId, val_id f // build_for_in_body builds for in body data for ssa. fn (mut b Builder) build_for_in_body(node flat.Node, body_start int) { - for i in body_start .. node.children_count { - b.build_stmt(b.a.child(&node, i)) - } + b.build_scoped_stmts(node, body_start) } // build_if builds if data for ssa. @@ -7234,9 +8097,7 @@ fn (mut b Builder) build_if(node flat.Node) { if else_node.kind == .if_expr { b.build_if(*else_node) } else if else_node.kind == .block { - for i in 0 .. else_node.children_count { - b.build_stmt(b.a.child(else_node, i)) - } + b.build_scoped_stmts(*else_node, 0) } eblk := b.m.blocks[b.cur_block] else_terminated = b.current_block_terminated() @@ -7256,9 +8117,7 @@ fn (mut b Builder) build_if(node flat.Node) { b.activate_if_guard(guard) } then_node := b.a.child_node(&node, 1) - for i in 0 .. then_node.children_count { - b.build_stmt(b.a.child(then_node, i)) - } + b.build_scoped_stmts(*then_node, 0) tblk := b.m.blocks[b.cur_block] then_terminated := b.current_block_terminated() then_sum_guards := b.sum_guard_variants.clone() @@ -7357,7 +8216,7 @@ fn (mut b Builder) build_map_index_if_guard(name string, index_node flat.Node) ( } else { b.emit1(.bitcast, ptr_i8, key_alloca) } - get_ref := b.m.add_value(.func_ref, ptr_i8, 'map__get_check', b.fn_ids['map__get_check']) + get_ref := b.m.add_value(.func_ref, ptr_i8, 'map__get_check', b.runtime_fn_id('map__get_check')) value_ptr := b.emit3(.call, ptr_i8, get_ref, map_ptr, key_ptr) zero_ptr := b.m.get_or_add_const(ptr_i8, '0') found := b.emit2(.ne, b.i1_type, value_ptr, zero_ptr) @@ -7682,7 +8541,7 @@ fn (mut b Builder) build_assert(node flat.Node) { b.emit3(.br, b.void_type, cond_val, ValueID(ok_block), ValueID(fail_block)) b.cur_block = fail_block - if exit_fn := b.fn_ids['exit'] { + if exit_fn := b.runtime_fn_lookup('exit') { one := b.m.get_or_add_const(b.i64_type, '1') fn_ref := b.m.add_value(.func_ref, b.void_type, 'exit', exit_fn) b.emit2(.call, b.void_type, fn_ref, one) @@ -7727,7 +8586,8 @@ fn (mut b Builder) build_expr(id flat.NodeId) ValueID { return b.m.add_value(.string_literal, b.m.type_store.get_ptr(b.u8_type), decode_native_c_string(node.value[2..]), 0) } - return b.m.get_or_add_const(b.i8_type, '${char_literal_value(node.value)}') + typ := if b.m.target.ptr_size == 4 { b.i32_type } else { b.i8_type } + return b.m.get_or_add_const(typ, '${char_literal_value(node.value)}') } .string_interp { return b.build_string_interp(node) @@ -7834,7 +8694,7 @@ fn (mut b Builder) build_expr(id flat.NodeId) ValueID { return b.m.get_or_add_const(b.i64_type, '0') } .infix { - return b.build_infix(node) + return b.build_infix(id, node) } .prefix { return b.build_prefix(node, id) @@ -7976,7 +8836,7 @@ fn (mut b Builder) build_in_expr(node flat.Node) ValueID { } else { b.emit1(.bitcast, ptr_i8, key_alloca) } - exists_ref := b.m.add_value(.func_ref, b.i1_type, 'map__exists', b.fn_ids['map__exists']) + exists_ref := b.m.add_value(.func_ref, b.i1_type, 'map__exists', b.runtime_fn_id('map__exists')) return b.emit3(.call, b.i1_type, exists_ref, map_ptr, key_ptr) } @@ -8124,17 +8984,28 @@ fn char_literal_value(value string) int { return 0 } if value[0] == `\\` && value.len > 1 { + if value[1] in [`x`, `u`, `U`] { + return int(global_integer_literal('0x' + value[2..])) + } + if value.len == 4 && value[1] >= `0` && value[1] <= `7` { + return int(global_integer_literal('0o' + value[1..])) + } return match value[1] { + `a` { 7 } + `b` { 8 } + `e` { 27 } + `f` { 12 } `n` { 10 } `r` { 13 } `t` { 9 } + `v` { 11 } `0` { 0 } `\\` { 92 } `'` { 39 } else { int(value[1]) } } } - return int(value[0]) + return int(value.runes()[0]) } fn parse_int_literal(value string) int { @@ -8195,11 +9066,11 @@ fn (mut b Builder) build_array_literal(node flat.Node) ValueID { elem_size_const := b.m.get_or_add_const(b.i64_type, '${actual_elem_size}') zero := b.m.get_or_add_const(b.i64_type, '0') cap_const := b.m.get_or_add_const(b.i64_type, '${node.children_count}') - new_ref := b.m.add_value(.func_ref, b.array_type, 'array_new', b.fn_ids['array_new']) + new_ref := b.m.add_value(.func_ref, b.array_type, 'array_new', b.runtime_fn_id('array_new')) arr := b.emit4(.call, b.array_type, new_ref, elem_size_const, zero, cap_const) arr_alloca := b.emit0(.alloca, ptr_array) b.emit2(.store, b.void_type, arr, arr_alloca) - push_ref := b.m.add_value(.func_ref, b.void_type, 'array_push', b.fn_ids['array_push']) + push_ref := b.m.add_value(.func_ref, b.void_type, 'array_push', b.runtime_fn_id('array_push')) for value in values { value_type := b.value_type(value) value_alloca := b.emit0(.alloca, b.m.type_store.get_ptr(value_type)) @@ -8252,7 +9123,7 @@ fn (mut b Builder) build_array_init(node flat.Node) ValueID { elem_size := b.m.type_size(elem_type) actual_elem_size := if elem_size > 0 { elem_size } else { 8 } elem_size_const := b.m.get_or_add_const(b.i64_type, '${actual_elem_size}') - new_ref := b.m.add_value(.func_ref, b.array_type, 'array_new', b.fn_ids['array_new']) + new_ref := b.m.add_value(.func_ref, b.array_type, 'array_new', b.runtime_fn_id('array_new')) arr := b.emit4(.call, b.array_type, new_ref, elem_size_const, len_val, cap_val) if init_val == ValueID(0) { return arr @@ -8357,7 +9228,7 @@ fn (mut b Builder) build_map_init(node flat.Node) ValueID { val_size := b.m.get_or_add_const(b.i64_type, '${b.m.type_size(val_type)}') zero := b.m.get_or_add_const(b.i64_type, '0') mut args := []ValueID{} - if fn_id := b.fn_ids['new_map'] { + if fn_id := b.runtime_fn_lookup('new_map') { fn_ref := b.m.add_value(.func_ref, b.void_type, 'new_map', fn_id) args << fn_ref args << key_size @@ -8397,7 +9268,7 @@ fn (mut b Builder) build_map_init(node flat.Node) ValueID { } else { b.emit1(.bitcast, ptr_i8, val_alloca) } - if set_id := b.fn_ids['v3_map_set_sized'] { + if set_id := b.runtime_fn_lookup('v3_map_set_sized') { set_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_set_sized', set_id) mut set_args := []ValueID{} set_args << set_ref @@ -8506,7 +9377,7 @@ fn (mut b Builder) build_fixed_array_init(node flat.Node) ValueID { actual_elem_size := if elem_size > 0 { elem_size } else { 8 } byte_count := b.m.get_or_add_const(b.i64_type, '${len_text.int() * actual_elem_size}') zero := b.m.get_or_add_const(b.i64_type, '0') - memset_ref := b.m.add_value(.func_ref, b.void_type, 'memset', b.fn_ids['memset']) + memset_ref := b.m.add_value(.func_ref, b.void_type, 'memset', b.runtime_fn_id('memset')) b.emit4(.call, ptr_i8, memset_ref, data, zero, byte_count) mut elem_index := 0 @@ -8616,7 +9487,7 @@ fn (mut b Builder) build_map_index_or_expr(id flat.NodeId, index_node flat.Node, } else { b.emit1(.bitcast, ptr_i8, key_alloca) } - get_ref := b.m.add_value(.func_ref, ptr_i8, 'map__get_check', b.fn_ids['map__get_check']) + get_ref := b.m.add_value(.func_ref, ptr_i8, 'map__get_check', b.runtime_fn_id('map__get_check')) value_ptr := b.emit3(.call, ptr_i8, get_ref, map_ptr, key_ptr) zero_ptr := b.m.get_or_add_const(ptr_i8, '0') found := b.emit2(.ne, b.i1_type, value_ptr, zero_ptr) @@ -8716,7 +9587,7 @@ fn (mut b Builder) default_value_for_type(typ TypeID) ValueID { return b.m.add_value(.string_literal, b.str_type, '', 0) } if typ == b.array_type { - fn_ref := b.m.add_value(.func_ref, b.array_type, 'array_new', b.fn_ids['array_new']) + fn_ref := b.m.add_value(.func_ref, b.array_type, 'array_new', b.runtime_fn_id('array_new')) zero := b.m.get_or_add_const(b.i64_type, '0') one := b.m.get_or_add_const(b.i64_type, '1') return b.emit4(.call, b.array_type, fn_ref, one, zero, zero) @@ -8777,7 +9648,7 @@ fn (b &Builder) current_block_terminated() bool { return blk.instrs.len > 0 && b.is_terminator(blk.instrs.last()) } -fn (mut b Builder) build_infix(node flat.Node) ValueID { +fn (mut b Builder) build_infix(id flat.NodeId, node flat.Node) ValueID { if node.op == .power { panic('power expression reached the V3 SSA backend') } @@ -8817,6 +9688,51 @@ fn (mut b Builder) build_infix(node flat.Node) ValueID { if b.value_type(lhs) == b.str_type || b.value_type(rhs) == b.str_type { return b.build_string_infix(node.op, lhs, rhs) } + if b.m.target.ptr_size == 4 && b.tc != unsafe { nil } { + lhs_literal := b.is_integer_literal_expr(lhs_id) + rhs_literal := b.is_integer_literal_expr(rhs_id) + // Untyped literals must keep their bits until the operation's type is known. + if !lhs_literal { + lhs = b.coerce_numeric_value(lhs, b.global_constant_type(lhs_id)) + } + if !rhs_literal { + rhs = b.coerce_numeric_value(rhs, b.global_constant_type(rhs_id)) + } + if node.op in [.eq, .ne, .lt, .gt, .le, .ge] { + if lhs_literal && !rhs_literal { + lhs = b.coerce_comparison_literal(lhs_id, lhs, b.value_type(rhs)) + } else if rhs_literal && !lhs_literal { + rhs = b.coerce_comparison_literal(rhs_id, rhs, b.value_type(lhs)) + } + if b.is_int_type(b.value_type(lhs)) && b.is_int_type(b.value_type(rhs)) { + if b.is_unsigned_type(b.value_type(lhs)) != b.is_unsigned_type(b.value_type(rhs)) { + return b.build_mixed_integer_comparison(node.op, lhs, rhs) + } + width := int_max(b.scalar_type_width(b.value_type(lhs)), b.scalar_type_width(b.value_type(rhs))) + common := if b.is_unsigned_type(b.value_type(lhs)) { + b.m.type_store.get_uint(width) + } else { + b.m.type_store.get_int(width) + } + lhs = b.coerce_int_value(lhs, common) + rhs = b.coerce_int_value(rhs, common) + } else if b.is_float_type(b.value_type(lhs)) || b.is_float_type(b.value_type(rhs)) { + common := if b.value_type(lhs) == b.f64_type || b.value_type(rhs) == b.f64_type { + b.f64_type + } else { + b.f32_type + } + lhs = b.coerce_numeric_value(lhs, common) + rhs = b.coerce_numeric_value(rhs, common) + } + } else { + result := b.global_constant_type(id) + lhs = b.coerce_numeric_value(lhs, result) + if node.op !in [.left_shift, .right_shift, .right_shift_unsigned] { + rhs = b.coerce_numeric_value(rhs, result) + } + } + } lhs_type := b.value_type(lhs) mut op := OpCode.add if b.is_float_type(lhs_type) { @@ -8868,6 +9784,9 @@ fn (mut b Builder) build_infix(node flat.Node) ValueID { .right_shift { op = if b.is_unsigned_type(lhs_type) { OpCode.lshr } else { OpCode.ashr } } + .right_shift_unsigned { + op = OpCode.lshr + } .eq { op = OpCode.eq } @@ -8895,6 +9814,110 @@ fn (mut b Builder) build_infix(node flat.Node) ValueID { return b.emit2(op, result_type, lhs, rhs) } +fn (mut b Builder) build_mixed_integer_comparison(op flat.Op, lhs ValueID, rhs ValueID) ValueID { + lhs_signed := !b.is_unsigned_type(b.value_type(lhs)) + signed_value := if lhs_signed { lhs } else { rhs } + zero := b.m.get_or_add_const(b.value_type(signed_value), '0') + negative := b.emit2(.lt, b.i1_type, signed_value, zero) + width := int_max(b.scalar_type_width(b.value_type(lhs)), b.scalar_type_width(b.value_type(rhs))) + common := b.m.type_store.get_uint(width) + left := b.coerce_int_value(lhs, common) + right := b.coerce_int_value(rhs, common) + comparison_op := match op { + .eq { OpCode.eq } + .ne { OpCode.ne } + .lt { OpCode.ult } + .le { OpCode.ule } + .gt { OpCode.ugt } + .ge { OpCode.uge } + else { OpCode.eq } + } + nonnegative_result := b.emit2(comparison_op, b.i1_type, left, right) + negative_result := match op { + .lt, .le { lhs_signed } + .gt, .ge { !lhs_signed } + .ne { true } + else { false } + } + constant := b.m.get_or_add_const(b.i1_type, if negative_result { '1' } else { '0' }) + return b.emit3(.select, b.i1_type, negative, constant, nonnegative_result) +} + +fn int_max(left int, right int) int { + return if left > right { left } else { right } +} + +fn (b &Builder) is_integer_literal_expr(id flat.NodeId) bool { + if !b.valid_node_id(id) { + return false + } + node := b.a.nodes[int(id)] + if node.kind == .int_literal { + return true + } + if node.children_count > 0 && (node.kind == .paren + || (node.kind == .prefix && node.op in [.plus, .minus])) { + return b.is_integer_literal_expr(b.a.child(&node, 0)) + } + return false +} + +fn (b &Builder) is_negative_integer_literal_expr(id flat.NodeId) bool { + node := b.a.nodes[int(id)] + if node.kind == .int_literal { + return node.value.starts_with('-') + } + if node.kind == .prefix && node.op == .minus { + return !b.is_negative_integer_literal_expr(b.a.child(&node, 0)) + } + if node.children_count > 0 { + return b.is_negative_integer_literal_expr(b.a.child(&node, 0)) + } + return false +} + +fn (mut b Builder) coerce_comparison_literal(id flat.NodeId, value ValueID, peer_type TypeID) ValueID { + mut target := peer_type + value_type := b.value_type(value) + if b.is_int_type(value_type) && b.is_int_type(peer_type) { + width := int_max(b.scalar_type_width(value_type), b.scalar_type_width(peer_type)) + target = if b.is_unsigned_type(peer_type) && !b.is_negative_integer_literal_expr(id) { + b.m.type_store.get_uint(width) + } else { + b.m.type_store.get_int(width) + } + } + return b.coerce_numeric_value(value, target) +} + +fn (mut b Builder) coerce_numeric_value(value ValueID, target TypeID) ValueID { + return b.coerce_numeric_value_in_block(value, target, b.cur_block) +} + +fn (mut b Builder) coerce_numeric_value_in_block(value ValueID, target TypeID, block_id BlockID) ValueID { + from := b.value_type(value) + if from == target { + return value + } + mut op := OpCode.bitcast + if b.is_int_type(from) && b.is_int_type(target) { + from_width := b.scalar_type_width(from) + to_width := b.scalar_type_width(target) + if to_width > 0 && from_width > 0 && to_width < from_width { + op = .trunc + } else if to_width > from_width { + op = if b.is_unsigned_type(from) { OpCode.zext } else { OpCode.sext } + } + } else if b.is_int_type(from) && b.is_float_type(target) { + op = if b.is_unsigned_type(from) { OpCode.uitofp } else { OpCode.sitofp } + } else if b.is_float_type(from) && b.is_int_type(target) { + op = if b.is_unsigned_type(target) { OpCode.fptoui } else { OpCode.fptosi } + } else if !b.is_float_type(from) || !b.is_float_type(target) { + return value + } + return b.block_instr1(op, block_id, target, value) +} + fn (mut b Builder) build_qualified_infix_call(node flat.Node) ?ValueID { if node.children_count < 2 || node.op !in [.dot, .amp] { return none @@ -9054,7 +10077,7 @@ fn (mut b Builder) build_string_infix(op flat.Op, lhs ValueID, rhs ValueID) Valu } fn (mut b Builder) emit_runtime_call(name string, ret_type TypeID, values []ValueID) ValueID { - if fn_idx := b.fn_ids[name] { + if fn_idx := b.runtime_fn_lookup(name) { fn_ref := b.m.add_value(.func_ref, ret_type, name, fn_idx) mut args := []ValueID{} args << fn_ref @@ -9098,6 +10121,10 @@ fn (mut b Builder) build_cast_expr(node flat.Node) ValueID { } } target_type := b.resolve_type(target_name) + // A positive literal can occupy the unsigned half of i64 before a float cast. + if b.m.target.ptr_size == 4 && b.is_float_type(target_type) && child.kind == .int_literal { + value = b.m.get_or_add_const(b.u64_type, child.value) + } from_type := b.value_type(value) if target_type == from_type { return value @@ -9172,7 +10199,7 @@ fn (mut b Builder) heap_copy_value(value ValueID, value_type TypeID) ValueID { size_const := b.m.get_or_add_const(b.i64_type, '${size}') ptr_i8 := b.m.type_store.get_ptr(b.i8_type) src := b.emit1(.bitcast, ptr_i8, alloca) - memdup_ref := b.m.add_value(.func_ref, ptr_i8, 'memdup', b.fn_ids['memdup']) + memdup_ref := b.m.add_value(.func_ref, ptr_i8, 'memdup', b.runtime_fn_id('memdup')) copied := b.emit3(.call, ptr_i8, memdup_ref, src, size_const) return b.emit1(.bitcast, ptr_type, copied) } @@ -9378,7 +10405,7 @@ fn (mut b Builder) build_array_push_expr(node flat.Node) ValueID { arr_ptr := b.build_lvalue_addr(lhs_id) rhs_val := b.build_expr(rhs_id) if node.value == 'push_many' || b.value_type(rhs_val) == b.array_type { - if fn_id := b.fn_ids['array_push_many'] { + if fn_id := b.runtime_fn_lookup('array_push_many') { fn_ref := b.m.add_value(.func_ref, b.void_type, 'array_push_many', fn_id) return b.emit3(.call, b.void_type, fn_ref, arr_ptr, rhs_val) } @@ -9392,7 +10419,7 @@ fn (mut b Builder) build_array_push_expr(node flat.Node) ValueID { } else { b.emit1(.bitcast, ptr_i8, elem_ptr) } - if fn_id := b.fn_ids['array_push'] { + if fn_id := b.runtime_fn_lookup('array_push') { fn_ref := b.m.add_value(.func_ref, b.void_type, 'array_push', fn_id) return b.emit3(.call, b.void_type, fn_ref, arr_ptr, elem_arg) } @@ -9465,8 +10492,13 @@ fn (mut b Builder) build_prefix(node flat.Node, _id flat.NodeId) ValueID { val := b.build_expr(child_id) match node.op { .minus { + typ := b.value_type(val) + if b.m.type_store.types[typ].kind == .float_t { + negative_zero := b.m.get_or_add_const(typ, '-0.0') + return b.emit2(.fsub, typ, negative_zero, val) + } zero := b.m.get_or_add_const(b.i64_type, '0') - return b.emit2(.sub, b.value_type(val), zero, val) + return b.emit2(.sub, typ, zero, val) } .not { zero := b.m.get_or_add_const(b.i1_type, '0') @@ -9584,6 +10616,11 @@ fn (mut b Builder) build_lvalue_addr(id flat.NodeId) ValueID { } fn (mut b Builder) build_selector_addr(node flat.Node) ValueID { + if name := b.source_selector_name(node) { + if addr := b.vars[name] { + return addr + } + } if addr := b.native_global_selector_addr(node) { return addr } @@ -9929,12 +10966,28 @@ fn (mut b Builder) build_call(id flat.NodeId, node flat.Node) ValueID { if fn_node.kind == .selector { base_id = b.a.child(&fn_node, 0) base := b.a.nodes[int(base_id)] + if b.m.target.ptr_size == 4 && fn_node.value == 'str' { + receiver_name := if base.kind == .bool_literal { + 'bool' + } else { + b.receiver_type_name(base_id) + } + if result := b.build_scalar_str_call(node, '${receiver_name}.str', base_id) { + return result + } + } if base.kind == .ident && base.value == 'C' { actual_name = fn_node.value is_c_call = true } else { mut found_name := fn_node.value mut resolved_handled := false + if name := b.source_selector_name(fn_node) { + if name in b.fn_ids { + found_name = name + resolved_handled = true + } + } if builder_name := b.builder_method_name_for_base(base, fn_node.value) { found_name = builder_name is_method = true @@ -10000,8 +11053,15 @@ fn (mut b Builder) build_call(id flat.NodeId, node flat.Node) ValueID { } actual_name = found_name } + } else if node.pos == token.Pos{} && fn_name in b.runtime_fn_ids { + // Lowered runtime calls have no source span; keep their original helper + // owner even when the current module declares a function with that name. + actual_name = fn_name } else { - if resolved := b.resolved_call_name(id) { + qualified_name := ssa_fn_name_in_module(b.cur_module, fn_name) + if b.source_modules.len > 0 && qualified_name in b.fn_ids { + actual_name = qualified_name + } else if resolved := b.resolved_call_name(id) { actual_name = resolved } } @@ -10015,6 +11075,12 @@ fn (mut b Builder) build_call(id flat.NodeId, node flat.Node) ValueID { actual_name = actual_name[2..] is_c_call = true } + if b.m.target.ptr_size == 4 && !is_c_call { + scalar_base := if fn_node.kind == .selector { base_id } else { flat.empty_node } + if result := b.build_scalar_str_call(node, actual_name, scalar_base) { + return result + } + } if !is_c_call && b.is_disabled_fn_name(actual_name) { // The transformer normally elides disabled `@[if ...]` calls. Minimal // programs can skip transforming an otherwise reachable builtin body, so @@ -10074,10 +11140,12 @@ fn (mut b Builder) build_call(id flat.NodeId, node flat.Node) ValueID { return b.m.get_or_add_const(b.m.type_store.get_ptr(b.i8_type), '0') } if actual_name in ['ast.Expr.name', 'Expr.name', 'ast.SelectorExpr.name', 'SelectorExpr.name', - 'ast.[]Expr.name_list', '[]Expr.name_list', 'name_list', 'types__Type__str'] { + 'ast.[]Expr.name_list', '[]Expr.name_list', 'name_list', 'types__Type__str'] + && !b.source_fn_names[actual_name] { return b.m.add_value(.string_literal, b.str_type, '', 0) } - if actual_name == 'join_path' || actual_name == 'os.join_path' { + if actual_name == 'os.join_path' + || (actual_name == 'join_path' && !b.source_fn_names[actual_name]) { return b.build_join_path_call(node) } if fn_node.kind == .selector && node.children_count == 2 { @@ -10189,6 +11257,7 @@ fn (mut b Builder) build_call(id flat.NodeId, node flat.Node) ValueID { } mut fn_idx := 0 + runtime_call := !is_c_call && node.pos == token.Pos{} && resolved_name in b.runtime_fn_ids if is_c_call { if idx := b.c_fn_ids[resolved_name] { fn_idx = idx @@ -10197,6 +11266,8 @@ fn (mut b Builder) build_call(id flat.NodeId, node flat.Node) ValueID { } else { panic('ssa: unknown function `${actual_name}`') } + } else if runtime_call { + fn_idx = b.runtime_fn_ids[resolved_name] } else if idx := b.fn_ids[resolved_name] { fn_idx = idx } else { @@ -10223,12 +11294,16 @@ fn (mut b Builder) build_call(id flat.NodeId, node flat.Node) ValueID { ft := b.m.type_store.types[ft_id] param_types = ft.params.clone() } + } else if runtime_call { + if ft_id := b.runtime_fn_types[resolved_name] { + param_types = b.m.type_store.types[ft_id].params.clone() + } } else if ft_id := b.fn_types[resolved_name] { ft := b.m.type_store.types[ft_id] param_types = ft.params.clone() } - if resolved_name == 'map__set' { + if resolved_name == 'map__set' && (runtime_call || !b.source_fn_names[resolved_name]) { return b.build_map_set_call(node) } @@ -10236,6 +11311,10 @@ fn (mut b Builder) build_call(id flat.NodeId, node flat.Node) ValueID { allow_pointer_slot := !is_c_call || resolved_name !in b.c_fn_types mut args := []ValueID{} args << fn_ref + if b.m.target.ptr_size == 4 && resolved_name in ['print', 'println', 'eprint', 'eprintln'] + && node.children_count == 1 { + args << b.m.add_value(.string_literal, b.str_type, '', 0) + } if resolved_name == 'error_posix' && node.children_count == 1 && param_types.len > 0 { args << b.default_system_error_value(param_types[0]) return b.m.add_instr(.call, b.cur_block, ret_type, args) @@ -10338,11 +11417,15 @@ fn (mut b Builder) build_call(id flat.NodeId, node flat.Node) ValueID { } } } - args << if param_idx < param_types.len { + mut arg := if param_idx < param_types.len { b.build_typed_call_arg(arg_id, param_types[param_idx], allow_pointer_slot) } else { b.build_expr(arg_id) } + if b.m.target.ptr_size == 4 && resolved_name in ['print', 'println', 'eprint', 'eprintln'] { + arg = b.stringify_scalar(arg) + } + args << arg } if resolved_name in ['map__keys', 'map__values'] && args.len == 2 && param_types.len == 2 { if elem_size := b.map_array_elem_size_arg(id, node) { @@ -10368,6 +11451,54 @@ fn (b &Builder) call_never_returns(id flat.NodeId, name string, is_c_call bool) return is_c_call && b.a.noreturn_fns['C.${name}'] } +fn (mut b Builder) stringify_scalar(value ValueID) ValueID { + typ := b.value_type(value) + if typ == b.str_type { + return value + } + if typ == b.i1_type { + return b.emit_runtime_call('bool_str', b.str_type, [value]) + } + if b.is_unsigned_type(typ) { + ten := b.m.get_or_add_const(b.i64_type, '10') + return b.emit_runtime_call('strconv__format_uint', b.str_type, [value, ten]) + } + if b.is_int_type(typ) { + return b.emit_runtime_call('int_str', b.str_type, [value]) + } + return value +} + +fn (mut b Builder) build_scalar_str_call(node flat.Node, resolved_name string, base_id flat.NodeId) ?ValueID { + name := resolved_name.replace('__', '.').trim_string_left('builtin.') + primitive_name := name.all_before_last('.') + if name.all_after_last('.') != 'str' + || primitive_name !in ['int', 'i8', 'i16', 'i32', 'i64', 'u8', 'u16', 'u32', 'u64', 'bool', + 'rune', 'isize', 'usize', 'string'] { + return none + } + mut value_id := base_id + if int(base_id) < 0 { + if node.children_count != 2 { + return none + } + value_id = b.a.child(&node, 1) + } else if node.children_count != 1 { + base := b.a.nodes[int(base_id)] + if node.children_count != 2 || base.kind != .ident || base.value != primitive_name { + return none + } + value_id = b.a.child(&node, 1) + } + // Default integer literals can exceed int's width before their print conversion. + value := if primitive_name == 'int' { + b.build_expr(value_id) + } else { + b.coerce_numeric_value(b.build_expr(value_id), b.resolve_type(primitive_name)) + } + return b.stringify_scalar(value) +} + fn (b &Builder) is_disabled_fn_name(name string) bool { if name in b.a.disabled_fns || name.replace('__', '.') in b.a.disabled_fns { return true @@ -10572,21 +11703,10 @@ fn (b &Builder) flag_enum_expr_type_name(id flat.NodeId) string { fn (mut b Builder) coerce_int_value(value ValueID, to_type TypeID) ValueID { from_type := b.value_type(value) - if from_type == to_type || !b.is_int_type(from_type) || !b.is_int_type(to_type) { + if !b.is_int_type(from_type) || !b.is_int_type(to_type) { return value } - from_width := b.scalar_type_width(from_type) - to_width := b.scalar_type_width(to_type) - if to_width > 0 && from_width > 0 && to_width < from_width { - return b.emit1(.trunc, to_type, value) - } - if to_width > from_width { - if b.is_unsigned_type(from_type) { - return b.emit1(.zext, to_type, value) - } - return b.emit1(.sext, to_type, value) - } - return b.emit1(.bitcast, to_type, value) + return b.coerce_numeric_value(value, to_type) } fn (mut b Builder) build_array_len_mutator_call(base_id flat.NodeId, method string) ValueID { @@ -10617,7 +11737,7 @@ fn (mut b Builder) build_array_pop_call(id flat.NodeId, base_id flat.NodeId) Val one := b.m.get_or_add_const(b.i64_type, '1') index := b.emit2(.sub, b.i64_type, len64, one) ptr_i8 := b.m.type_store.get_ptr(b.i8_type) - fn_ref := b.m.add_value(.func_ref, ptr_i8, 'array_get', b.fn_ids['array_get']) + fn_ref := b.m.add_value(.func_ref, ptr_i8, 'array_get', b.runtime_fn_id('array_get')) elem_ptr := b.emit3(.call, ptr_i8, fn_ref, base, index) typed_ptr := b.emit1(.bitcast, b.m.type_store.get_ptr(elem_type), elem_ptr) value := b.emit1(.load, elem_type, typed_ptr) @@ -10637,7 +11757,7 @@ fn (mut b Builder) build_array_first_last_call(id flat.NodeId, base_id flat.Node b.m.get_or_add_const(b.i64_type, '0') } ptr_i8 := b.m.type_store.get_ptr(b.i8_type) - fn_ref := b.m.add_value(.func_ref, ptr_i8, 'array_get', b.fn_ids['array_get']) + fn_ref := b.m.add_value(.func_ref, ptr_i8, 'array_get', b.runtime_fn_id('array_get')) elem_ptr := b.emit3(.call, ptr_i8, fn_ref, base, index) typed_ptr := b.emit1(.bitcast, b.m.type_store.get_ptr(elem_type), elem_ptr) return b.emit1(.load, elem_type, typed_ptr) @@ -10650,7 +11770,7 @@ fn (mut b Builder) build_array_index_call(base_id flat.NodeId, needle_id flat.No needle_type := if elem_type == b.str_type { b.str_type } else { b.i64_type } mut needle := b.build_expr(needle_id) needle = b.coerce_int_value(needle, needle_type) - fn_ref := b.m.add_value(.func_ref, b.i64_type, fn_name, b.fn_ids[fn_name]) + fn_ref := b.m.add_value(.func_ref, b.i64_type, fn_name, b.runtime_fn_id(fn_name)) return b.emit3(.call, b.i64_type, fn_ref, base, needle) } @@ -10669,7 +11789,7 @@ fn (mut b Builder) build_array_join_call(base_id flat.NodeId, sep_id flat.NodeId b.emit2(.store, b.void_type, zero, i_alloca) new_ref := b.m.add_value(.func_ref, b.array_type, 'strings.new_builder', - b.fn_ids['strings.new_builder']) + b.runtime_fn_id('strings.new_builder')) initial_cap := b.m.get_or_add_const(b.i64_type, '16') builder := b.emit2(.call, b.array_type, new_ref, initial_cap) b.emit2(.store, b.void_type, builder, builder_alloca) @@ -10698,7 +11818,7 @@ fn (mut b Builder) build_array_join_call(base_id flat.NodeId, sep_id flat.NodeId b.cur_block = write_sep write_ref := b.m.add_value(.func_ref, b.void_type, 'strings.Builder.write_string', - b.fn_ids['strings.Builder.write_string']) + b.runtime_fn_id('strings.Builder.write_string')) b.emit3(.call, b.void_type, write_ref, builder_alloca, sep) b.emit1(.jmp, b.void_type, ValueID(write_elem)) @@ -10709,7 +11829,7 @@ fn (mut b Builder) build_array_join_call(base_id flat.NodeId, sep_id flat.NodeId elem_ptr := b.emit1(.bitcast, b.m.type_store.get_ptr(b.str_type), elem_ptr_raw) elem := b.emit1(.load, b.str_type, elem_ptr) write_ref2 := b.m.add_value(.func_ref, b.void_type, 'strings.Builder.write_string', - b.fn_ids['strings.Builder.write_string']) + b.runtime_fn_id('strings.Builder.write_string')) b.emit3(.call, b.void_type, write_ref2, builder_alloca, elem) next_i := b.emit2(.add, b.i64_type, i_val, one) b.emit2(.store, b.void_type, next_i, i_alloca) @@ -10717,7 +11837,7 @@ fn (mut b Builder) build_array_join_call(base_id flat.NodeId, sep_id flat.NodeId b.cur_block = done str_ref := b.m.add_value(.func_ref, b.str_type, 'strings.Builder.str', - b.fn_ids['strings.Builder.str']) + b.runtime_fn_id('strings.Builder.str')) return b.emit2(.call, b.str_type, str_ref, builder_alloca) } @@ -10733,7 +11853,7 @@ fn (mut b Builder) build_array_repeat_call(base_id flat.NodeId, count_id flat.No b.m.get_or_add_const(b.i64_type, '0') } fn_ref := b.m.add_value(.func_ref, b.array_type, 'array.repeat_to_depth', - b.fn_ids['array.repeat_to_depth']) + b.runtime_fn_id('array.repeat_to_depth')) return b.emit4(.call, b.array_type, fn_ref, base, count, depth) } @@ -10841,10 +11961,29 @@ fn (b &Builder) typed_receiver_method_name(base_id flat.NodeId, method string) ? fn (mut b Builder) build_indirect_call(id flat.NodeId, node flat.Node, callee_id flat.NodeId) ValueID { callee := b.build_expr(callee_id) + mut param_types := []TypeID{} + if b.m.target.ptr_size == 4 && b.tc != unsafe { nil } { + mut callee_type := types.unalias_type(b.tc.expr_type(callee_id) or { + b.tc.resolve_type(callee_id) + }) + if callee_type !is types.FnType { + // Transformed local callees can retain their type only on the binding. + callee_type = types.unalias_type(b.tc.parse_type(b.checked_expr_type_name(callee_id))) + } + if callee_type is types.FnType { + for param_type in callee_type.params { + param_types << b.ssa_type_from_checker_type(param_type) + } + } + } mut args := []ValueID{} args << callee for i in 1 .. node.children_count { - args << b.build_expr(b.a.child(&node, i)) + mut arg := b.build_expr(b.a.child(&node, i)) + if i - 1 < param_types.len { + arg = b.coerce_numeric_value(arg, param_types[i - 1]) + } + args << arg } return b.m.add_instr(.call_indirect, b.cur_block, b.call_expr_result_type(id, node), args) } @@ -10910,7 +12049,7 @@ fn (mut b Builder) build_const_string_array_arg(id flat.NodeId) ?ValueID { ptr_i8 := b.m.type_store.get_ptr(b.i8_type) count := b.m.get_or_add_const(b.i64_type, '${expr.children_count}') alloca := b.emit1(.alloca, ptr_string, count) - stride := 16 + stride := b.m.type_size(b.str_type) for i in 0 .. expr.children_count { child_id := b.a.child(&expr, i) value := b.build_expr(child_id) @@ -11129,7 +12268,7 @@ fn (mut b Builder) float_literal_type(id flat.NodeId, node flat.Node) TypeID { } fn (mut b Builder) build_map_set_call(node flat.Node) ValueID { - fn_idx := b.fn_ids['v3_map_set_sized'] + fn_idx := b.runtime_fn_id('v3_map_set_sized') fn_ref := b.m.add_value(.func_ref, b.void_type, 'v3_map_set_sized', fn_idx) mut args := []ValueID{} args << fn_ref @@ -11156,12 +12295,12 @@ fn (mut b Builder) build_map_delete_call(base_id flat.NodeId, key_id flat.NodeId } else { b.emit1(.bitcast, ptr_i8, key_alloca) } - fn_ref := b.m.add_value(.func_ref, b.void_type, 'map__delete', b.fn_ids['map__delete']) + fn_ref := b.m.add_value(.func_ref, b.void_type, 'map__delete', b.runtime_fn_id('map__delete')) return b.emit3(.call, b.void_type, fn_ref, map_ptr, key_ptr) } fn (mut b Builder) build_map_clone_call(base_id flat.NodeId) ValueID { - fn_ref := b.m.add_value(.func_ref, b.void_type, 'map__clone', b.fn_ids['map__clone']) + fn_ref := b.m.add_value(.func_ref, b.void_type, 'map__clone', b.runtime_fn_id('map__clone')) base := b.map_expr_ptr(base_id) return b.emit2(.call, b.map_type, fn_ref, base) } @@ -11284,12 +12423,12 @@ fn (mut b Builder) build_join_path_call(node flat.Node) ValueID { } mut fn_idx := 0 mut found_fn := false - if idx := b.fn_ids['join_path_single'] { + if idx := b.runtime_fn_lookup('join_path_single') { fn_idx = idx found_fn = true } if !found_fn { - if idx := b.fn_ids['os.join_path_single'] { + if idx := b.runtime_fn_lookup('os.join_path_single') { fn_idx = idx found_fn = true } @@ -11525,6 +12664,17 @@ fn (b &Builder) should_pass_ident_addr_for_ptr_param(addr ValueID, param_type Ty } fn (mut b Builder) build_selector(node flat.Node) ValueID { + if name := b.source_selector_name(node) { + if addr := b.vars[name] { + return b.emit1(.load, b.deref_type(addr), addr) + } + if expr_id := b.lookup_const_expr(name) { + return b.build_expr(expr_id) + } + if fn_idx := b.fn_ids[name] { + return b.m.add_value(.func_ref, b.i64_type, name, fn_idx) + } + } base_id := b.a.child(&node, 0) base := b.a.nodes[int(base_id)] field_name := node.value @@ -11555,7 +12705,7 @@ fn (mut b Builder) build_selector(node flat.Node) ValueID { if base.kind == .ident && base.value == 'os' { if field_name == 'args' { - if fn_id := b.fn_ids['arguments'] { + if fn_id := b.runtime_fn_lookup('arguments') { fn_ref := b.m.add_value(.func_ref, b.array_type, 'arguments', fn_id) return b.emit1(.call, b.array_type, fn_ref) } @@ -11616,6 +12766,12 @@ fn (mut b Builder) build_selector(node flat.Node) ValueID { } } field_ptr := b.build_selector_addr(node) + if b.m.target.ptr_size == 8 { + base_type := b.resolve_type(b.checked_expr_type_name(base_id)) + if field := b.native_utsname_field_value(base_type, field_ptr) { + return field + } + } return b.emit1(.load, b.deref_type(field_ptr), field_ptr) } base_val := b.build_expr(base_id) @@ -11806,7 +12962,31 @@ fn (mut b Builder) load_map_len(map_ptr ValueID) ValueID { return b.emit1(.load, b.i64_type, result_slot) } +fn (mut b Builder) native_utsname_field_value(struct_typ_id TypeID, field_ptr ValueID) ?ValueID { + if b.m.target.ptr_size == 8 { + mut base_type := struct_typ_id + if base_type > 0 && base_type < b.m.type_store.types.len + && b.m.type_store.types[base_type].kind == .ptr_t { + base_type = b.m.type_store.types[base_type].elem_type + } + if uname_type := b.struct_types['C.utsname'] { + field_type := b.deref_type(field_ptr) + if base_type == uname_type && field_type > 0 + && field_type < b.m.type_store.types.len + && b.m.type_store.types[field_type].kind == .array_t { + // Preserve the logical &char field value: it is the inline array's + // address, rather than a pointer loaded from its first eight bytes. + return b.emit1(.bitcast, b.m.type_store.get_ptr(b.i8_type), field_ptr) + } + } + } + return none +} + fn (mut b Builder) load_selector_field(node flat.Node, struct_typ_id TypeID, field_ptr ValueID) ValueID { + if field := b.native_utsname_field_value(struct_typ_id, field_ptr) { + return field + } field := b.emit1(.load, b.deref_type(field_ptr), field_ptr) if node.typ.len == 0 || !b.sum_type_has_field(struct_typ_id, node.value) { return field @@ -11900,7 +13080,7 @@ fn (mut b Builder) build_index(id flat.NodeId, node flat.Node) ValueID { } if base_typ == b.str_type { for substr_name in ['string.substr', 'string__substr', 'substr'] { - if substr_id := b.fn_ids[substr_name] { + if substr_id := b.runtime_fn_lookup(substr_name) { substr_ref := b.m.add_value(.func_ref, b.str_type, substr_name, substr_id) return b.emit4(.call, b.str_type, substr_ref, base, start, end) } @@ -11910,7 +13090,7 @@ fn (mut b Builder) build_index(id flat.NodeId, node flat.Node) ValueID { slice_len := b.emit2(.sub, b.i64_type, end, start) return b.emit_make_owned_string(b.cur_block, slice_data, slice_len) } - fn_ref := b.m.add_value(.func_ref, b.array_type, 'array_slice', b.fn_ids['array_slice']) + fn_ref := b.m.add_value(.func_ref, b.array_type, 'array_slice', b.runtime_fn_id('array_slice')) return b.emit4(.call, b.array_type, fn_ref, base, start, end) } if node.children_count < 2 { @@ -11952,7 +13132,7 @@ fn (mut b Builder) build_index(id flat.NodeId, node flat.Node) ValueID { return b.emit1(.load, elem_type, elem_ptr) } } - fn_ref := b.m.add_value(.func_ref, ptr_i8, 'array_get', b.fn_ids['array_get']) + fn_ref := b.m.add_value(.func_ref, ptr_i8, 'array_get', b.runtime_fn_id('array_get')) elem_ptr := b.emit3(.call, ptr_i8, fn_ref, base, index) elem_type := b.index_elem_type(id, node) typed_ptr := b.emit1(.bitcast, b.m.type_store.get_ptr(elem_type), elem_ptr) @@ -11978,7 +13158,7 @@ fn (mut b Builder) build_map_index_addr(base_id flat.NodeId, index ValueID, key_ b.emit1(.bitcast, ptr_i8, default_alloca) } map_ptr := b.map_expr_ptr(base_id) - get_ref := b.m.add_value(.func_ref, ptr_i8, 'map__get', b.fn_ids['map__get']) + get_ref := b.m.add_value(.func_ref, ptr_i8, 'map__get', b.runtime_fn_id('map__get')) value_ptr := b.emit4(.call, ptr_i8, get_ref, map_ptr, key_ptr, default_ptr) if b.m.type_store.get_ptr(val_type) == ptr_i8 { return value_ptr @@ -12046,7 +13226,7 @@ fn (mut b Builder) build_index_addr(id flat.NodeId, node flat.Node) ValueID { } } ptr_i8 := b.m.type_store.get_ptr(b.i8_type) - fn_ref := b.m.add_value(.func_ref, ptr_i8, 'array_get', b.fn_ids['array_get']) + fn_ref := b.m.add_value(.func_ref, ptr_i8, 'array_get', b.runtime_fn_id('array_get')) elem_ptr := b.emit3(.call, ptr_i8, fn_ref, base, index) elem_type := b.index_elem_type(id, node) return b.emit1(.bitcast, b.m.type_store.get_ptr(elem_type), elem_ptr) @@ -12191,6 +13371,9 @@ fn (mut b Builder) build_struct_init(node flat.Node) ValueID { fn (mut b Builder) coerce_store_value(value ValueID, target_type TypeID) ValueID { mut result := value value_type := b.value_type(value) + if b.m.target.ptr_size == 4 { + result = b.coerce_numeric_value(value, target_type) + } if target_type > 0 && target_type < b.m.type_store.types.len && b.m.type_store.types[target_type].kind == .array_t && value_type > 0 && value_type < b.m.type_store.types.len { @@ -12289,7 +13472,7 @@ fn (mut b Builder) default_field_value(struct_name string, field_name string, ze elem_type := b.resolve_type(elem_name) elem_size := b.m.type_size(elem_type) elem_size_const := b.m.get_or_add_const(b.i64_type, '${elem_size}') - if fn_id := b.fn_ids['array_new'] { + if fn_id := b.runtime_fn_lookup('array_new') { fn_ref := b.m.add_value(.func_ref, b.void_type, 'array_new', fn_id) return b.emit4(.call, b.array_type, fn_ref, elem_size_const, zero, zero) } @@ -12302,7 +13485,7 @@ fn (mut b Builder) default_field_value(struct_name string, field_name string, ze actual_val_size := if val_size > 0 { val_size } else { 8 } key_size_const := b.m.get_or_add_const(b.i64_type, '${actual_key_size}') val_size_const := b.m.get_or_add_const(b.i64_type, '${actual_val_size}') - if fn_id := b.fn_ids['new_map'] { + if fn_id := b.runtime_fn_lookup('new_map') { fn_ref := b.m.add_value(.func_ref, b.void_type, 'new_map', fn_id) mut args := []ValueID{} args << fn_ref @@ -12398,7 +13581,7 @@ fn (mut b Builder) build_heap_struct_init(node flat.Node) ValueID { size_const := b.m.get_or_add_const(b.i64_type, '${size}') ptr_i8 := b.m.type_store.get_ptr(b.i8_type) src_cast := b.emit1(.bitcast, ptr_i8, alloca) - memdup_ref := b.m.add_value(.func_ref, ptr_i8, 'memdup', b.fn_ids['memdup']) + memdup_ref := b.m.add_value(.func_ref, ptr_i8, 'memdup', b.runtime_fn_id('memdup')) result := b.emit3(.call, ptr_i8, memdup_ref, src_cast, size_const) return b.emit1(.bitcast, b.m.type_store.get_ptr(typ_id), result) } @@ -12411,7 +13594,7 @@ fn (mut b Builder) build_string_interp(node flat.Node) ValueID { return b.m.add_value(.string_literal, b.str_type, '', 0) } mut result := ValueID(0) - plus_ref := b.m.add_value(.func_ref, b.str_type, 'string__plus', b.fn_ids['string__plus']) + plus_ref := b.m.add_value(.func_ref, b.str_type, 'string__plus', b.runtime_fn_id('string__plus')) for i in 0 .. n { child_id := b.a.child(&node, i) child := b.a.nodes[int(child_id)] @@ -12424,7 +13607,7 @@ fn (mut b Builder) build_string_interp(node flat.Node) ValueID { if value_type == b.str_type { part = val } else if value_type == b.i1_type { - bool_ref := b.m.add_value(.func_ref, b.str_type, 'bool_str', b.fn_ids['bool_str']) + bool_ref := b.m.add_value(.func_ref, b.str_type, 'bool_str', b.runtime_fn_id('bool_str')) part = b.emit2(.call, b.str_type, bool_ref, val) } else if value_type in [b.f32_type, b.f64_type] { name := if value_type == b.f32_type { @@ -12432,10 +13615,10 @@ fn (mut b Builder) build_string_interp(node flat.Node) ValueID { } else { 'strconv__f64_to_str_l' } - float_ref := b.m.add_value(.func_ref, b.str_type, name, b.fn_ids[name]) + float_ref := b.m.add_value(.func_ref, b.str_type, name, b.runtime_fn_id(name)) part = b.emit2(.call, b.str_type, float_ref, val) } else { - int_str_ref := b.m.add_value(.func_ref, b.str_type, 'int_str', b.fn_ids['int_str']) + int_str_ref := b.m.add_value(.func_ref, b.str_type, 'int_str', b.runtime_fn_id('int_str')) part = b.emit2(.call, b.str_type, int_str_ref, val) } } @@ -12568,6 +13751,12 @@ fn (mut b Builder) primitive_type_id(name string) ?TypeID { 'int' { b.i32_type } + 'isize' { + if b.m.target.ptr_size == 4 { b.i32_type } else { b.i64_type } + } + 'usize' { + if b.m.target.ptr_size == 4 { b.u32_type } else { b.u64_type } + } 'i8', 'char' { b.i8_type } @@ -12943,11 +14132,21 @@ fn (mut b Builder) emit1(op OpCode, typ TypeID, a ValueID) ValueID { fn (mut b Builder) emit2(op OpCode, typ TypeID, a ValueID, c ValueID) ValueID { mut ops := []ValueID{} - ops << a + ops << if op == .store { b.coerce_scalar_store_value(a, c, b.cur_block) } else { a } ops << c return b.m.add_instr(op, b.cur_block, typ, ops) } +fn (mut b Builder) coerce_scalar_store_value(value ValueID, address ValueID, block_id BlockID) ValueID { + address_type := b.value_type(address) + if b.m.target.ptr_size != 4 || !b.is_pointer_type(address_type) { + return value + } + // Store conversions must survive scalar alloca promotion in production builds. + target := b.m.type_store.types[address_type].elem_type + return b.coerce_numeric_value_in_block(value, target, block_id) +} + fn (mut b Builder) emit3(op OpCode, typ TypeID, a ValueID, c ValueID, d ValueID) ValueID { mut ops := []ValueID{} ops << a @@ -12965,7 +14164,18 @@ fn (mut b Builder) emit4(op OpCode, typ TypeID, a ValueID, c ValueID, d ValueID, return b.m.add_instr(op, b.cur_block, typ, ops) } -fn (b &Builder) compound_to_op(op flat.Op) OpCode { +fn (b &Builder) compound_to_op(op flat.Op, typ TypeID) OpCode { + if b.m.target.ptr_size == 4 && op == .right_shift_unsigned_assign { + return .lshr + } + if b.m.target.ptr_size == 4 && b.is_unsigned_type(typ) { + match op { + .div_assign { return .udiv } + .mod_assign { return .urem } + .right_shift_assign { return .lshr } + else {} + } + } return match op { .plus_assign { .add } .minus_assign { .sub } @@ -12981,3 +14191,34 @@ fn (b &Builder) compound_to_op(op flat.Op) OpCode { else { .add } } } + +fn (mut b Builder) register_synthetic_function(name string, ret TypeID, params []TypeID) int { + fn_type := b.m.type_store.register(Type{ + kind: .func_t + ret_type: ret + params: params + }) + b.fn_types[name] = fn_type + func_id := b.m.new_function(name, ret) + b.fn_ids[name] = func_id + mut f := b.m.funcs[func_id] + f.typ = ret + f.blocks = []BlockID{} + f.params = []ValueID{} + f.is_c_extern = false + b.m.funcs[func_id] = f + return func_id +} + +fn (mut b Builder) register_synthetic_c_function(name string, ret TypeID, params []TypeID) int { + func_id := b.register_synthetic_function(name, ret, params) + fn_type := b.fn_types[name] + extern_name := 'C.${name}' + b.fn_ids[extern_name] = func_id + b.fn_types[extern_name] = fn_type + b.c_fn_ids[name] = func_id + b.c_fn_types[name] = fn_type + return func_id +} + +// func_add_argument supports func add argument handling for Builder. diff --git a/vlib/v/ssa/builder_runtime_symbols_test.v b/vlib/v/ssa/builder_runtime_symbols_test.v new file mode 100644 index 0000000000..9e095ddc3f --- /dev/null +++ b/vlib/v/ssa/builder_runtime_symbols_test.v @@ -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 +} diff --git a/vlib/v/ssa/builder_store_test.v b/vlib/v/ssa/builder_store_test.v new file mode 100644 index 0000000000..eb98596260 --- /dev/null +++ b/vlib/v/ssa/builder_store_test.v @@ -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 + } + } +} diff --git a/vlib/v/ssa/builder_test.v b/vlib/v/ssa/builder_test.v index e480904578..f6714aefe1 100644 --- a/vlib/v/ssa/builder_test.v +++ b/vlib/v/ssa/builder_test.v @@ -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 +} diff --git a/vlib/v/ssa/native_c_struct_test.v b/vlib/v/ssa/native_c_struct_test.v new file mode 100644 index 0000000000..2d17ab8a94 --- /dev/null +++ b/vlib/v/ssa/native_c_struct_test.v @@ -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 + } +} diff --git a/vlib/v/ssa/optimize/fold.v b/vlib/v/ssa/optimize/fold.v index a0ffffb70d..abf83a0dce 100644 --- a/vlib/v/ssa/optimize/fold.v +++ b/vlib/v/ssa/optimize/fold.v @@ -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 { diff --git a/vlib/v/ssa/optimize/fold_test.v b/vlib/v/ssa/optimize/fold_test.v new file mode 100644 index 0000000000..0dcd131718 --- /dev/null +++ b/vlib/v/ssa/optimize/fold_test.v @@ -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 +} diff --git a/vlib/v/ssa/ssa.v b/vlib/v/ssa/ssa.v index 9d8c0543db..931e216b3c 100644 --- a/vlib/v/ssa/ssa.v +++ b/vlib/v/ssa/ssa.v @@ -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