From 722553b6b8fbbc1a3954a36283c17937ced9f74f Mon Sep 17 00:00:00 2001 From: Reuben Dunnington Date: Tue, 30 Apr 2024 20:14:48 -0700 Subject: [PATCH 1/3] bytebox wasm backend * These changes add support for the bytebox runtime to the wasm layer as an optional backend. The wasm backend still defaults to wasm3. To enable support, you must pass "--wasm-backend bytebox" when invoking "orcadev build" 1. When building the SDK and tools, invoke orcadev build --wasm-backend bytebox. 2. When bundling the app, pass --wasm-backend bytebox * This change also includes some misc fixes: * Update build scripts to preserve the runtime orca.dll's corresponding pdb, which was being overwritten by the orca.exe (tool) pdb. * orca bundle: avoid printing error on Windows when attempting to remove temporary dir that didn't exist (would happen if you didn't supply an icon) * orca bundle: instead of linking dynamically with runtime.obj, use a prebuilt orca_runtime.exe and directly embed the supplied icon file into it. This avoids requiring MSVC be in the path when running bundle commands. --- .gitignore | 2 + scripts/dev.py | 98 +- src/ext/bytebox/LICENSE | 21 + src/ext/bytebox/bench/main.zig | 65 + src/ext/bytebox/bench/samples/add-one.zig | 3 + src/ext/bytebox/bench/samples/fibonacci.zig | 9 + src/ext/bytebox/bench/samples/mandelbrot.zig | 44 + src/ext/bytebox/build.zig | 147 + src/ext/bytebox/run/main.zig | 417 ++ src/ext/bytebox/src/.clang-format | 70 + src/ext/bytebox/src/bytebox.h | 171 + src/ext/bytebox/src/cffi.zig | 845 +++ src/ext/bytebox/src/common.zig | 97 + src/ext/bytebox/src/core.zig | 72 + src/ext/bytebox/src/definition.zig | 3556 +++++++++++ src/ext/bytebox/src/instance.zig | 1234 ++++ src/ext/bytebox/src/opcode.zig | 1436 +++++ src/ext/bytebox/src/stringpool.zig | 120 + src/ext/bytebox/src/tests.zig | 118 + src/ext/bytebox/src/vm_register.zig | 1503 +++++ src/ext/bytebox/src/vm_stack.zig | 5458 +++++++++++++++++ src/ext/bytebox/src/wasi.zig | 2616 ++++++++ src/ext/bytebox/src/zig-stable-array/LICENSE | 54 + .../bytebox/src/zig-stable-array/README.md | 19 + .../src/zig-stable-array/stable_array.zig | 424 ++ src/ext/bytebox/test/main.zig | 1538 +++++ src/ext/bytebox/test/wasi/bytebox_adapter.py | 35 + src/ext/bytebox/test/wasi/runtests.sh | 2 + src/runtime.c | 8 +- src/runtime.h | 16 +- src/tool/bundle.c | 66 +- src/tool/win32_icon.c | 145 +- src/wasm/backend_bytebox.c | 411 ++ src/wasm/backend_wasm3.c | 17 +- src/wasm/wasm.h | 7 +- 35 files changed, 20752 insertions(+), 92 deletions(-) create mode 100644 src/ext/bytebox/LICENSE create mode 100644 src/ext/bytebox/bench/main.zig create mode 100644 src/ext/bytebox/bench/samples/add-one.zig create mode 100644 src/ext/bytebox/bench/samples/fibonacci.zig create mode 100644 src/ext/bytebox/bench/samples/mandelbrot.zig create mode 100644 src/ext/bytebox/build.zig create mode 100644 src/ext/bytebox/run/main.zig create mode 100644 src/ext/bytebox/src/.clang-format create mode 100644 src/ext/bytebox/src/bytebox.h create mode 100644 src/ext/bytebox/src/cffi.zig create mode 100644 src/ext/bytebox/src/common.zig create mode 100644 src/ext/bytebox/src/core.zig create mode 100644 src/ext/bytebox/src/definition.zig create mode 100644 src/ext/bytebox/src/instance.zig create mode 100644 src/ext/bytebox/src/opcode.zig create mode 100644 src/ext/bytebox/src/stringpool.zig create mode 100644 src/ext/bytebox/src/tests.zig create mode 100644 src/ext/bytebox/src/vm_register.zig create mode 100644 src/ext/bytebox/src/vm_stack.zig create mode 100644 src/ext/bytebox/src/wasi.zig create mode 100644 src/ext/bytebox/src/zig-stable-array/LICENSE create mode 100644 src/ext/bytebox/src/zig-stable-array/README.md create mode 100644 src/ext/bytebox/src/zig-stable-array/stable_array.zig create mode 100644 src/ext/bytebox/test/main.zig create mode 100644 src/ext/bytebox/test/wasi/bytebox_adapter.py create mode 100644 src/ext/bytebox/test/wasi/runtests.sh create mode 100644 src/wasm/backend_bytebox.c diff --git a/.gitignore b/.gitignore index 6e170ed9..b694184d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,8 @@ src/orca_surface.c src/graphics/orca_gl31.h *bind_gen.c *_stubs.c +src/ext/bytebox/zig-cache +src/ext/bytebox/zig-out .vscode/launch.json .vscode/settings.json diff --git a/scripts/dev.py b/scripts/dev.py index 6de92812..8080a65c 100644 --- a/scripts/dev.py +++ b/scripts/dev.py @@ -26,6 +26,7 @@ def attach_dev_commands(subparsers): build_cmd = subparsers.add_parser("build", help="Build Orca from source.") build_cmd.add_argument("--version", help="embed a version string in the Orca CLI tool (default is git commit hash)") build_cmd.add_argument("--release", action="store_true", help="compile in release mode (default is debug)") + build_cmd.add_argument("--wasm-backend", help="specify a wasm backend. Options: wasm3 (default), bytebox") build_cmd.set_defaults(func=dev_shellish(build_all)) platform_layer_cmd = subparsers.add_parser("build-platform-layer", help="Build the Orca platform layer from source.") @@ -45,6 +46,7 @@ def attach_dev_commands(subparsers): runtime_cmd = subparsers.add_parser("build-runtime", help="Build the Orca runtime from source.") runtime_cmd.add_argument("--release", action="store_true", help="compile in release mode (default is debug)") + runtime_cmd.add_argument("--wasm-backend", help="specify a wasm backend. Options: wasm3 (default), bytebox") runtime_cmd.set_defaults(func=dev_shellish(build_runtime)) libc_cmd = subparsers.add_parser("build-orca-libc", help="Build the Orca libC from source.") @@ -84,7 +86,7 @@ def func_from_source(args): def build_all(args): ensure_programs() - build_runtime_internal(args.release) # this also builds the platform layer + build_runtime_internal(args.release, args.wasm_backend) # this also builds the platform layer build_libc_internal(args.release) build_sdk_internal(args.release) build_tool(args) @@ -563,13 +565,33 @@ def build_wasm3_lib_mac(release): subprocess.run(["libtool", "-static", "-o", "build/lib/libwasm3.a", "-no_warning_for_no_symbols", *glob.glob("build/obj/*.o")], check=True) subprocess.run(["rm", "-rf", "build/obj"], check=True) +def build_bytebox(release): + print("Building bytebox...") + args = ["zig", "build"] + if release: + args += ["-Doptimize=ReleaseSafe"] + subprocess.run(args, cwd="src/ext/bytebox/") + + if platform.system() == "Windows": + shutil.copy("src/ext/bytebox/zig-out/lib/bytebox.lib", "build/bin") + elif platform.system() == "Darwin": + shutil.copy("src/ext/bytebox/zig-out/lib/libbytebox.a", "build/bin") + else: + log_error(f"can't build bytebox for unknown platform '{platform.system()}'") + exit(1) + + def build_runtime(args): ensure_programs() - build_runtime_internal(args.release) + build_runtime_internal(args.release, args.wasm_backend) -def build_runtime_internal(release): +def build_runtime_internal(release, wasm_backend): build_platform_layer_internal(release) - build_wasm3(release) + + if wasm_backend == "bytebox": + build_bytebox(release) + else: + build_wasm3(release) print("Building Orca runtime...") @@ -577,14 +599,14 @@ def build_runtime_internal(release): os.makedirs("build/lib", exist_ok=True) if platform.system() == "Windows": - build_runtime_win(release) + build_runtime_win(release, wasm_backend) elif platform.system() == "Darwin": - build_runtime_mac(release) + build_runtime_mac(release, wasm_backend) else: log_error(f"can't build Orca for unknown platform '{platform.system()}'") exit(1) -def build_runtime_win(release): +def build_runtime_win(release, wasm_backend): gen_all_bindings() @@ -593,28 +615,61 @@ def build_runtime_win(release): "/I", "src", "/I", "src/ext", "/I", "src/ext/angle/include", - "/I", "src/ext/wasm3/source", ] - subprocess.run([ + defines = [] + link_commands = ["build/bin/orca.dll.lib"] + + if wasm_backend == "bytebox": + includes += ["/I", "src/ext/bytebox/zig-out/include"] + defines += ["/DOC_WASM_BACKEND_WASM3=0", "/DOC_WASM_BACKEND_BYTEBOX=1"] + + # Bytebox uses zig libraries that depend on NTDLL on Windows. Additionally, some zig APIs expect there to be + # a large stack to use for scratch space, as zig stacks are 8MB by default. When linking bytebox, we must + # ensure we provide the same amount of stack space, else risk stack overflows. + link_commands += ["build/bin/bytebox.lib", "ntdll.lib", "/STACK:8388608,8388608"] + else: + includes += ["/I", "src/ext/wasm3/source"] + defines += ["/DOC_WASM_BACKEND_WASM3=1", "/DOC_WASM_BACKEND_BYTEBOX=0"] + link_commands += ["build/bin/wasm3.lib"] + + compile_args=[ "cl", - "/c", "/Zi", "/Zc:preprocessor", "/std:c11", "/experimental:c11atomics", + *defines, *includes, "src/runtime.c", - "/Fo:build/bin/runtime.obj", - ], check=True) + "/Fe:build/bin/orca_runtime.exe", + "/link", + *link_commands + ] + + backend_deps = "ntdll.lib "; -def build_runtime_mac(release): + subprocess.run(compile_args, check=True) + +def build_runtime_mac(release, wasm_backend): includes = [ "-Isrc", "-Isrc/ext", "-Isrc/ext/angle/include", - "-Isrc/ext/wasm3/source" ] - libs = ["-Lbuild/bin", "-Lbuild/lib", "-lorca", "-lwasm3"] + + defines = [] + + libs = ["-Lbuild/bin", "-Lbuild/lib", "-lorca"] + + if wasm_backend == "bytebox": + includes += ["-Isrc/ext/bytebox/zig-out/include"] + defines += ["-DOC_WASM_BACKEND_WASM3=0", "-DOC_WASM_BACKEND_BYTEBOX=1"] + libs += ["-lbytebox"] + else: + includes += ["-Isrc/ext/wasm3/source"] + defines += ["-DOC_WASM_BACKEND_WASM3=1", "-DOC_WASM_BACKEND_BYTEBOX=0"] + libs += ["-lwasm3"] + debug_flags = ["-O2"] if release else ["-g", "-DOC_DEBUG -DOC_LOG_COMPILE_DEBUG"] flags = [ *debug_flags, @@ -625,7 +680,7 @@ def build_runtime_mac(release): # compile orca subprocess.run([ - "clang", *flags, *includes, *libs, + "clang", *flags, *defines, *includes, *libs, "-o", "build/bin/orca_runtime", "src/runtime.c", ], check=True) @@ -775,6 +830,10 @@ def build_platform_layer_lib_win(release): "/IMPLIB:build/bin/orca.dll.lib", ], check=True) + build_dir = os.path.join("build", "bin") + # ensure orca.exe debug pdb doesn't override orca.dll pdb + shutil.copy(os.path.join(build_dir, "orca.pdb"), os.path.join(build_dir, "orca_dll.pdb")) + def build_platform_layer_lib_mac(release): embed_text_files("src/graphics/wgpu_renderer_shaders.h", "oc_wgsl_", [ @@ -1117,6 +1176,7 @@ def build_tool_win(release, version): "shlwapi.lib", "shell32.lib", "ole32.lib", + "Kernel32.lib", # libs needed by curl "advapi32.lib", @@ -1173,6 +1233,7 @@ def build_tool_mac(release, version): "-Lsrc/ext/zlib/build", "-lz", ] + subprocess.run([ "clang", f"-mmacos-version-min={MACOS_VERSION_MIN}", @@ -1237,9 +1298,7 @@ def package_sdk_internal(dest, target): if target == "Windows": shutil.copy(os.path.join("build", "bin", "orca.exe"), bin_dir) shutil.copy(os.path.join("build", "bin", "orca.dll"), bin_dir) - shutil.copy(os.path.join("build", "bin", "orca.dll.lib"), bin_dir) - shutil.copy(os.path.join("build", "bin", "wasm3.lib"), bin_dir) - shutil.copy(os.path.join("build", "bin", "runtime.obj"), bin_dir) + shutil.copy(os.path.join("build", "bin", "orca_runtime.exe"), bin_dir) shutil.copy(os.path.join("build", "bin", "liborca_wasm.a"), bin_dir) shutil.copy(os.path.join("build", "bin", "libEGL.dll"), bin_dir) shutil.copy(os.path.join("build", "bin", "libGLESv2.dll"), bin_dir) @@ -1253,6 +1312,7 @@ def package_sdk_internal(dest, target): shutil.copy(os.path.join("build", "bin", "libGLESv2.dylib"), bin_dir) shutil.copy(os.path.join("build", "bin", "libwebgpu.dylib"), bin_dir) + shutil.copytree(os.path.join("build", "orca-libc"), libc_dir, dirs_exist_ok=True) shutil.copytree("resources", res_dir, dirs_exist_ok=True) diff --git a/src/ext/bytebox/LICENSE b/src/ext/bytebox/LICENSE new file mode 100644 index 00000000..6df97b08 --- /dev/null +++ b/src/ext/bytebox/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Reuben Dunnington + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/ext/bytebox/bench/main.zig b/src/ext/bytebox/bench/main.zig new file mode 100644 index 00000000..533f3543 --- /dev/null +++ b/src/ext/bytebox/bench/main.zig @@ -0,0 +1,65 @@ +const std = @import("std"); +const bytebox = @import("bytebox"); +const Val = bytebox.Val; +const Timer = std.time.Timer; + +const Benchmark = struct { + name: []const u8, + filename: []const u8, + param: i32, +}; + +fn elapsedMilliseconds(timer: *std.time.Timer) f64 { + var ns_elapsed: f64 = @as(f64, @floatFromInt(timer.read())); + const ms_elapsed = ns_elapsed / 1000000.0; + return ms_elapsed; +} + +fn run(allocator: std.mem.Allocator, benchmark: Benchmark) !void { + var cwd = std.fs.cwd(); + var wasm_data: []u8 = try cwd.readFileAlloc(allocator, benchmark.filename, 1024 * 64); // Our wasm programs aren't very large + + var timer = try Timer.start(); + + var module_def = try bytebox.createModuleDefinition(allocator, .{}); + defer module_def.destroy(); + try module_def.decode(wasm_data); + + var module_instance = try bytebox.createModuleInstance(.Stack, module_def, allocator); + defer module_instance.destroy(); + try module_instance.instantiate(.{}); + + const handle = try module_instance.getFunctionHandle("run"); + var input = [1]Val{.{ .I32 = benchmark.param }}; + var output = [1]Val{.{ .I32 = 0 }}; + try module_instance.invoke(handle, &input, &output, .{}); + + const ms_elapsed: f64 = elapsedMilliseconds(&timer); + std.log.info("{s} decode+instantiate+run took {d}ms\n", .{ benchmark.name, ms_elapsed }); +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var allocator: std.mem.Allocator = gpa.allocator(); + + const benchmarks = [_]Benchmark{ .{ + .name = "add-one", + .filename = "zig-out/lib/add-one.wasm", + .param = 123456789, + }, .{ + .name = "fibonacci", + .filename = "zig-out/lib/fibonacci.wasm", + .param = 20, + }, .{ + .name = "mandelbrot", + .filename = "zig-out/lib/mandelbrot.wasm", + .param = 20, + } }; + + for (benchmarks) |benchmark| { + run(allocator, benchmark) catch |e| { + std.log.err("{s} 'run' invocation failed with error: {}\n", .{ benchmark.name, e }); + return e; + }; + } +} diff --git a/src/ext/bytebox/bench/samples/add-one.zig b/src/ext/bytebox/bench/samples/add-one.zig new file mode 100644 index 00000000..1d30a02f --- /dev/null +++ b/src/ext/bytebox/bench/samples/add-one.zig @@ -0,0 +1,3 @@ +export fn run(n: i32) i32 { + return n + 1; +} diff --git a/src/ext/bytebox/bench/samples/fibonacci.zig b/src/ext/bytebox/bench/samples/fibonacci.zig new file mode 100644 index 00000000..e0c2a3cd --- /dev/null +++ b/src/ext/bytebox/bench/samples/fibonacci.zig @@ -0,0 +1,9 @@ +export fn run(n: i32) i32 { + if (n < 2) { + return 1; + } else { + var a = run(n - 1); + var b = run(n - 2); + return a + b; + } +} diff --git a/src/ext/bytebox/bench/samples/mandelbrot.zig b/src/ext/bytebox/bench/samples/mandelbrot.zig new file mode 100644 index 00000000..0697d4f4 --- /dev/null +++ b/src/ext/bytebox/bench/samples/mandelbrot.zig @@ -0,0 +1,44 @@ +const std = @import("std"); +const complex = std.math.complex; +const Complex = complex.Complex(f32); + +const Color = struct { + R: u8, + G: u8, + B: u8, +}; + +const COLOR_BLACK = Color{ .R = 0, .G = 0, .B = 0 }; +const COLOR_WHITE = Color{ .R = 255, .G = 255, .B = 255 }; + +const WIDTH = 256; +const HEIGHT = 256; + +var pixels = [_]Color{.{ .R = 0, .G = 0, .B = 0 }} ** (WIDTH * HEIGHT); + +fn mandelbrot(c: Complex, max_counter: i32) Color { + var counter: u32 = 0; + var z = Complex.init(0, 0); + while (counter < max_counter) : (counter += 1) { + z = z.mul(z).add(c); + if (2.0 <= complex.abs(z)) { + return COLOR_WHITE; + } + } + + return COLOR_BLACK; +} + +export fn run(max_counter: i32) i32 { + var y: u32 = 0; + while (y < HEIGHT) : (y += 1) { + var x: u32 = 0; + while (x < WIDTH) : (x += 1) { + const c = Complex.init(@as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y))); + const color: Color = mandelbrot(c, max_counter); + pixels[y * HEIGHT + x] = color; + } + } + + return 0; +} diff --git a/src/ext/bytebox/build.zig b/src/ext/bytebox/build.zig new file mode 100644 index 00000000..e91302e7 --- /dev/null +++ b/src/ext/bytebox/build.zig @@ -0,0 +1,147 @@ +const std = @import("std"); + +const Build = std.Build; +const CrossTarget = std.zig.CrossTarget; +const Builder = std.build.Builder; +const CompileStep = std.build.CompileStep; +const InstallFileStep = std.build.InstallFileStep; + +const ExeOpts = struct { + exe_name: []const u8, + root_src: []const u8, + step_name: []const u8, + description: []const u8, + step_dependencies: ?[]*Build.Step = null, + should_emit_asm: bool = false, +}; + +pub fn build(b: *Build) void { + const should_emit_asm = b.option(bool, "asm", "Emit asm for the bytebox binaries") orelse false; + + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + var bench_add_one_step: *CompileStep = buildWasmLib(b, "bench/samples/add-one.zig", optimize); + var bench_fibonacci_step: *CompileStep = buildWasmLib(b, "bench/samples/fibonacci.zig", optimize); + var bench_mandelbrot_step: *CompileStep = buildWasmLib(b, "bench/samples/mandelbrot.zig", optimize); + + const bytebox_module: *Build.Module = b.createModule(.{ + .source_file = Build.LazyPath.relative("src/core.zig"), + }); + + _ = buildExeWithRunStep(b, target, optimize, bytebox_module, .{ + .exe_name = "bytebox", + .root_src = "run/main.zig", + .step_name = "run", + .description = "Run a wasm program", + .should_emit_asm = should_emit_asm, + }); + + var bench_steps = [_]*Build.Step{ + &bench_add_one_step.step, + &bench_fibonacci_step.step, + &bench_mandelbrot_step.step, + }; + _ = buildExeWithRunStep(b, target, optimize, bytebox_module, .{ + .exe_name = "benchmark", + .root_src = "bench/main.zig", + .step_name = "bench", + .description = "Run the benchmark suite", + .step_dependencies = &bench_steps, + }); + + const lib_bytebox = b.addStaticLibrary(.{ + .name = "bytebox", + .root_source_file = .{ .path = "src/cffi.zig" }, + .target = target, + .optimize = optimize, + }); + lib_bytebox.installHeader("src/bytebox.h", "bytebox.h"); + b.installArtifact(lib_bytebox); + + // Unit tests + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/tests.zig" }, + .target = target, + .optimize = optimize, + }); + const run_unit_tests = b.addRunArtifact(unit_tests); + const unit_test_step = b.step("test-unit", "Run unit tests"); + unit_test_step.dependOn(&run_unit_tests.step); + + // wasm tests + var wasm_testsuite_step = buildExeWithRunStep(b, target, optimize, bytebox_module, .{ + .exe_name = "testsuite", + .root_src = "test/main.zig", + .step_name = "test-wasm", + .description = "Run the wasm testsuite", + }); + + // wasi tests + const wasi_testsuite = b.addSystemCommand(&.{"python3"}); + wasi_testsuite.addArg("test/wasi/run.py"); + const wasi_testsuite_step = b.step("test-wasi", "Run wasi testsuite"); + wasi_testsuite_step.dependOn(&wasi_testsuite.step); + + // All tests + const all_tests_step = b.step("test", "Run unit, wasm, and wasi tests"); + all_tests_step.dependOn(unit_test_step); + all_tests_step.dependOn(wasm_testsuite_step); + all_tests_step.dependOn(wasi_testsuite_step); +} + +fn buildExeWithRunStep(b: *Build, target: CrossTarget, optimize: std.builtin.Mode, bytebox_module: *Build.Module, opts: ExeOpts) *Build.Step { + const exe = b.addExecutable(.{ + .name = opts.exe_name, + .root_source_file = Build.LazyPath.relative(opts.root_src), + .target = target, + .optimize = optimize, + }); + + exe.addModule("bytebox", bytebox_module); + + // exe.addModule("bytebox", .{ + // .source_file = Build.LazyPath.relative("src/core.zig"), + // }); + + // exe.emit_asm = if (opts.should_emit_asm) .emit else .default; + b.installArtifact(exe); + + if (opts.step_dependencies) |steps| { + for (steps) |step| { + exe.step.dependOn(step); + } + } + + const run = b.addRunArtifact(exe); + run.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run.addArgs(args); + } + + const step: *Build.Step = b.step(opts.step_name, opts.description); + step.dependOn(&run.step); + + return step; +} + +fn buildWasmLib(b: *Build, filepath: []const u8, optimize: std.builtin.Mode) *CompileStep { + var filename: []const u8 = std.fs.path.basename(filepath); + var filename_no_extension: []const u8 = filename[0 .. filename.len - 4]; + + const lib = b.addSharedLibrary(.{ + .name = filename_no_extension, + .root_source_file = Build.LazyPath.relative(filepath), + .target = CrossTarget{ + .cpu_arch = .wasm32, + .os_tag = .freestanding, + }, + .optimize = optimize, + }); + + // const mode = b.standardOptimizeOption(); + // lib.setBuildMode(mode); + b.installArtifact(lib); + + return lib; +} diff --git a/src/ext/bytebox/run/main.zig b/src/ext/bytebox/run/main.zig new file mode 100644 index 00000000..7d9159e7 --- /dev/null +++ b/src/ext/bytebox/run/main.zig @@ -0,0 +1,417 @@ +const std = @import("std"); +const bytebox = @import("bytebox"); +const wasi = bytebox.wasi; + +const Val = bytebox.Val; +const ValType = bytebox.ValType; +const TraceMode = bytebox.DebugTrace.Mode; + +const RunErrors = error{ + IoError, + MissingFunction, + FunctionParamMismatch, + BadFunctionParam, +}; + +const CmdOpts = struct { + print_help: bool = false, + print_version: bool = false, + print_dump: bool = false, + trace: TraceMode = .None, + + filename: ?[]const u8 = null, + invoke: ?InvokeArgs = null, + invalid_arg: ?[]const u8 = null, + missing_options: ?[]const u8 = null, + + wasm_argv: ?[][]const u8 = null, + wasm_env: ?[][]const u8 = null, + wasm_dirs: ?[][]const u8 = null, +}; + +const InvokeArgs = struct { + funcname: []const u8, + args: [][]const u8, +}; + +fn isArgvOption(arg: []const u8) bool { + return arg.len > 0 and arg[0] == '-'; +} + +fn getArgSafe(index: usize, args: [][]const u8) ?[]const u8 { + return if (index < args.len) args[index] else null; +} + +fn parseCmdOpts(args: [][]const u8, env_buffer: *std.ArrayList([]const u8), dir_buffer: *std.ArrayList([]const u8)) CmdOpts { + var opts = CmdOpts{}; + + if (args.len < 2) { + opts.print_help = true; + } + + var arg_index: usize = 1; + while (arg_index < args.len) { + var arg = args[arg_index]; + + if (arg_index == 1 and !isArgvOption(arg)) { + opts.filename = arg; + opts.wasm_argv = args[1..2]; + } else if (arg_index == 2 and !isArgvOption(arg)) { + var wasm_argv_begin: usize = arg_index - 1; // include wasm filename + var wasm_argv_end: usize = arg_index; + while (wasm_argv_end + 1 < args.len and !isArgvOption(args[wasm_argv_end + 1])) { + wasm_argv_end += 1; + } + opts.wasm_argv = args[wasm_argv_begin .. wasm_argv_end + 1]; + arg_index = wasm_argv_end; + } else if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) { + opts.print_help = true; + } else if (std.mem.eql(u8, arg, "-v") or std.mem.eql(u8, arg, "--version")) { + opts.print_version = true; + } else if (std.mem.eql(u8, arg, "--dump")) { + if (opts.filename != null) { + opts.print_dump = true; + } else { + opts.missing_options = arg; + } + } else if (std.mem.eql(u8, arg, "-i") or std.mem.eql(u8, arg, "--invoke")) { + arg_index += 1; + if (arg_index < args.len) { + opts.invoke = InvokeArgs{ + .funcname = args[arg_index], + .args = args[arg_index + 1 ..], + }; + } else { + opts.missing_options = arg; + } + arg_index = args.len; + } else if (std.mem.eql(u8, arg, "-e") or std.mem.eql(u8, arg, "--env")) { + arg_index += 1; + if (getArgSafe(arg_index, args)) |env| { + env_buffer.appendAssumeCapacity(env); + } else { + opts.missing_options = arg; + } + } else if (std.mem.eql(u8, arg, "-d") or std.mem.eql(u8, arg, "--dir")) { + arg_index += 1; + if (getArgSafe(arg_index, args)) |dir| { + dir_buffer.appendAssumeCapacity(dir); + } else { + opts.missing_options = arg; + } + } else if (std.mem.eql(u8, arg, "-t") or std.mem.eql(u8, arg, "--trace")) { + arg_index += 1; + if (getArgSafe(arg_index, args)) |mode_str| { + if (std.ascii.eqlIgnoreCase(mode_str, "function") or std.ascii.eqlIgnoreCase(mode_str, "func")) { + opts.trace = TraceMode.Function; + } else if (std.ascii.eqlIgnoreCase(mode_str, "instruction") or std.ascii.eqlIgnoreCase(mode_str, "instr")) { + opts.trace = TraceMode.Instruction; + } else if (std.ascii.eqlIgnoreCase(mode_str, "none")) { + opts.trace = TraceMode.None; + } else { + opts.invalid_arg = mode_str; + } + } else { + opts.missing_options = arg; + } + } else { + opts.invalid_arg = arg; + break; + } + + arg_index += 1; + } + + if (env_buffer.items.len > 0) { + opts.wasm_env = env_buffer.items; + } + + if (dir_buffer.items.len > 0) { + opts.wasm_dirs = dir_buffer.items; + } + + return opts; +} + +const version_string = "bytebox v0.0.1"; + +fn printHelp(args: [][]const u8) !void { + const usage_string: []const u8 = + \\Usage: {s} [WASM_ARGS]... [OPTION]... + \\ + \\ Options: + \\ + \\ -h, --help + \\ Print help information. + \\ + \\ -v, --version + \\ Print version information. + \\ + \\ --dump + \\ Prints the given module definition's imports and exports. Imports are qualified + \\ with the import module name. + \\ + \\ -i, --invoke [ARGS]... + \\ Call an exported, named function with arguments. The arguments are automatically + \\ translated from string inputs to the function's native types. If the conversion + \\ is not possible, an error is printed and execution aborts. + \\ + \\ -e, --env + \\ Set an environment variable for the execution environment. Typically retrieved + \\ via the WASI API environ_sizes_get() and environ_get(). Multiple instances of + \\ this flag is needed to pass multiple variables. + \\ + \\ -d, --dir + \\ Allow WASI programs to access this directory and paths within it. Can be relative + \\ to the current working directory or absolute. Multiple instances of this flag can + \\ be used to pass multiple dirs. + \\ + \\ -t, --trace + \\ Print a trace of the wasm program as it executes. MODE can be: + \\ * none (default) + \\ * function + \\ * instruction + \\ + \\ + ; + + const stdout = std.io.getStdOut().writer(); + try stdout.print(usage_string, .{args[0]}); +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var allocator: std.mem.Allocator = gpa.allocator(); + + var args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + var env_buffer = std.ArrayList([]const u8).init(allocator); + defer env_buffer.deinit(); + try env_buffer.ensureTotalCapacity(4096); // 4096 vars should be enough for most insane script file scenarios. + + var dir_buffer = std.ArrayList([]const u8).init(allocator); + defer dir_buffer.deinit(); + try dir_buffer.ensureTotalCapacity(4096); + + const opts: CmdOpts = parseCmdOpts(args, &env_buffer, &dir_buffer); + + const stdout = std.io.getStdOut().writer(); + const stderr = std.io.getStdErr().writer(); + + if (opts.print_help) { + try printHelp(args); + return; + } else if (opts.print_version) { + try stdout.print("{s}", .{version_string}); + return; + } else if (opts.invalid_arg) |invalid_arg| { + try stderr.print("Invalid argument '{s}'.\n", .{invalid_arg}); + try printHelp(args); + return; + } else if (opts.missing_options) |missing_options| { + try stderr.print("Argument {s} is missing required options.\n", .{missing_options}); + try printHelp(args); + return; + } else if (opts.invoke != null and opts.filename == null) { + try stderr.print("Cannot invoke {s} without a file to load.", .{opts.invoke.?.funcname}); + try printHelp(args); + return; + } + + if (opts.trace != .None) { + if (bytebox.DebugTrace.setMode(opts.trace) == false) { + try stderr.print("Failed to set trace mode to {}. Option unavailable in non-debug builds.\n", .{opts.trace}); + } + } + + std.debug.assert(opts.filename != null); + + var cwd = std.fs.cwd(); + var wasm_data: []u8 = cwd.readFileAlloc(allocator, opts.filename.?, 1024 * 1024 * 128) catch |e| { + std.log.err("Failed to read file '{s}' into memory: {}", .{ opts.filename.?, e }); + return RunErrors.IoError; + }; + defer allocator.free(wasm_data); + + const module_def_opts = bytebox.ModuleDefinitionOpts{ + .debug_name = std.fs.path.basename(opts.filename.?), + }; + var module_def = try bytebox.createModuleDefinition(allocator, module_def_opts); + defer module_def.destroy(); + + module_def.decode(wasm_data) catch |e| { + std.log.err("Caught error decoding module: {}", .{e}); + return e; + }; + + if (opts.print_dump) { + var strbuf = std.ArrayList(u8).init(allocator); + try strbuf.ensureTotalCapacity(1024 * 16); + try module_def.dump(strbuf.writer()); + try stdout.print("{s}", .{strbuf.items}); + return; + } + + var module_instance = try bytebox.createModuleInstance(.Stack, module_def, allocator); + defer module_instance.destroy(); + + var imports_wasi: bytebox.ModuleImportPackage = try wasi.initImports(.{ + .argv = opts.wasm_argv, + .env = opts.wasm_env, + .dirs = opts.wasm_dirs, + }, allocator); + defer wasi.deinitImports(&imports_wasi); + + var instantiate_opts = bytebox.ModuleInstantiateOpts{ + .imports = &[_]bytebox.ModuleImportPackage{imports_wasi}, + }; + + module_instance.instantiate(instantiate_opts) catch |e| { + std.log.err("Caught {} instantiating module.", .{e}); + return e; + }; + + const invoke_funcname: []const u8 = if (opts.invoke) |invoke| invoke.funcname else "_start"; + const invoke_args: [][]const u8 = if (opts.invoke) |invoke| invoke.args else &[_][]u8{}; + + const func_handle: bytebox.FunctionHandle = module_instance.getFunctionHandle(invoke_funcname) catch { + // don't log an error if the user didn't explicitly try to invoke a function + if (opts.invoke != null) { + std.log.err("Failed to find function '{s}' - either it doesn't exist or is not a public export.", .{invoke_funcname}); + } + return RunErrors.MissingFunction; + }; + + const func_export: bytebox.FunctionExport = module_def.getFunctionExport(func_handle); + + const num_params: usize = invoke_args.len; + if (func_export.params.len != num_params) { + var strbuf = std.ArrayList(u8).init(allocator); + defer strbuf.deinit(); + try writeSignature(&strbuf, &func_export); + std.log.err("Specified {} params but expected {}. The signature of '{s}' is:\n{s}", .{ + num_params, + func_export.params.len, + invoke_funcname, + strbuf.items, + }); + return RunErrors.FunctionParamMismatch; + } + + std.debug.assert(invoke_args.len == num_params); + + var params = std.ArrayList(bytebox.Val).init(allocator); + defer params.deinit(); + try params.resize(invoke_args.len); + for (func_export.params, 0..) |valtype, i| { + const arg: []const u8 = invoke_args[i]; + switch (valtype) { + .I32 => { + var parsed: i32 = std.fmt.parseInt(i32, arg, 0) catch |e| { + std.log.err("Failed to parse arg at index {} ('{s}') as an i32: {}", .{ i, arg, e }); + return RunErrors.BadFunctionParam; + }; + params.items[i] = Val{ .I32 = parsed }; + }, + .I64 => { + var parsed: i64 = std.fmt.parseInt(i64, arg, 0) catch |e| { + std.log.err("Failed to parse arg at index {} ('{s}') as an i64: {}", .{ i, arg, e }); + return RunErrors.BadFunctionParam; + }; + params.items[i] = Val{ .I64 = parsed }; + }, + .F32 => { + var parsed: f32 = std.fmt.parseFloat(f32, arg) catch |e| { + std.log.err("Failed to parse arg at index {} ('{s}') as a f32: {}", .{ i, arg, e }); + return RunErrors.BadFunctionParam; + }; + params.items[i] = Val{ .F32 = parsed }; + }, + .F64 => { + var parsed: f64 = std.fmt.parseFloat(f64, arg) catch |e| { + std.log.err("Failed to parse arg at index {} ('{s}') as a f64: {}", .{ i, arg, e }); + return RunErrors.BadFunctionParam; + }; + params.items[i] = Val{ .F64 = parsed }; + }, + .V128 => { + std.log.err("Param at index {} is a v128, which is currently only invokeable from code.", .{i}); + return RunErrors.BadFunctionParam; + }, + .FuncRef => { + std.log.err("Param at index {} is a v128, making this function only invokeable from code.", .{i}); + return RunErrors.BadFunctionParam; + }, + .ExternRef => { + std.log.err("Param at index {} is an externref, making this function only invokeable from code.", .{i}); + return RunErrors.BadFunctionParam; + }, + } + } + + var returns = std.ArrayList(bytebox.Val).init(allocator); + try returns.resize(func_export.returns.len); + + module_instance.invoke(func_handle, params.items.ptr, returns.items.ptr, .{}) catch |e| { + var backtrace = module_instance.formatBacktrace(1, allocator) catch unreachable; + std.log.err("Caught {} during function invoke. Backtrace:\n{s}\n", .{ e, backtrace.items }); + backtrace.deinit(); + return e; + }; + + { + var strbuf = std.ArrayList(u8).init(allocator); + defer strbuf.deinit(); + var writer = strbuf.writer(); + + if (returns.items.len > 0) { + const return_types = func_export.returns; + try std.fmt.format(writer, "return:\n", .{}); + for (returns.items, 0..) |_, i| { + switch (return_types[i]) { + .I32 => try std.fmt.format(writer, " {} (i32)\n", .{returns.items[i].I32}), + .I64 => try std.fmt.format(writer, " {} (i64)\n", .{returns.items[i].I64}), + .F32 => try std.fmt.format(writer, " {} (f32)\n", .{returns.items[i].F32}), + .F64 => try std.fmt.format(writer, " {} (f64)\n", .{returns.items[i].F64}), + .V128 => unreachable, // TODO support + .FuncRef => try std.fmt.format(writer, " (funcref)\n", .{}), + .ExternRef => try std.fmt.format(writer, " (externref)\n", .{}), + } + } + try std.fmt.format(writer, "\n", .{}); + } + if (strbuf.items.len > 0) { + try stdout.print("{s}\n", .{strbuf.items}); + } + } +} + +fn writeSignature(strbuf: *std.ArrayList(u8), info: *const bytebox.FunctionExport) !void { + var writer = strbuf.writer(); + if (info.params.len == 0) { + try std.fmt.format(writer, " params: none\n", .{}); + } else { + try std.fmt.format(writer, " params:\n", .{}); + for (info.params) |valtype| { + var name: []const u8 = valtypeToString(valtype); + try std.fmt.format(writer, " {s}\n", .{name}); + } + } + + if (info.returns.len == 0) { + try std.fmt.format(writer, " returns: none\n", .{}); + } else { + try std.fmt.format(writer, " returns:\n", .{}); + for (info.returns) |valtype| { + var name: []const u8 = valtypeToString(valtype); + try std.fmt.format(writer, " {s}\n", .{name}); + } + } +} + +fn valtypeToString(valtype: ValType) []const u8 { + return switch (valtype) { + inline else => |v| @typeInfo(ValType).Enum.fields[@intFromEnum(v)].name, + }; +} diff --git a/src/ext/bytebox/src/.clang-format b/src/ext/bytebox/src/.clang-format new file mode 100644 index 00000000..f789fb11 --- /dev/null +++ b/src/ext/bytebox/src/.clang-format @@ -0,0 +1,70 @@ +--- +Language: Cpp +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLambdasOnASingleLine: Empty +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Allman +BreakBeforeInheritanceComma: true +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeComma +BreakStringLiterals: true +ColumnLimit: 0 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +DerivePointerAlignment: false +DisableFormat: false +FixNamespaceComments: true +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +PackConstructorInitializers: Never +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +ReflowComments: false +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 4 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 4 +UseTab: Always diff --git a/src/ext/bytebox/src/bytebox.h b/src/ext/bytebox/src/bytebox.h new file mode 100644 index 00000000..c0333a65 --- /dev/null +++ b/src/ext/bytebox/src/bytebox.h @@ -0,0 +1,171 @@ +// C interface for bytebox wasm runtime. + +#include +#include +#include + +struct bb_slice +{ + char* data; + size_t length; +}; +typedef struct bb_slice bb_slice; + +enum bb_error +{ + BB_ERROR_OK, + BB_ERROR_FAILED, + BB_ERROR_OUTOFMEMORY, + BB_ERROR_INVALIDPARAM, + BB_ERROR_UNKNOWNEXPORT, + BB_ERROR_UNKNOWNIMPORT, + BB_ERROR_INCOMPATIBLEIMPORT, + BB_ERROR_TRAP_DEBUG, + BB_ERROR_TRAP_UNREACHABLE, + BB_ERROR_TRAP_INTEGERDIVISIONBYZERO, + BB_ERROR_TRAP_INTEGEROVERFLOW, + BB_ERROR_TRAP_INDIRECTCALLTYPEMISMATCH, + BB_ERROR_TRAP_INVALIDINTEGERCONVERSION, + BB_ERROR_TRAP_OUTOFBOUNDSMEMORYACCESS, + BB_ERROR_TRAP_UNDEFINEDELEMENT, + BB_ERROR_TRAP_UNINITIALIZEDELEMENT, + BB_ERROR_TRAP_OUTOFBOUNDSTABLEACCESS, + BB_ERROR_TRAP_STACKEXHAUSTED, +}; +typedef enum bb_error bb_error; + +enum bb_valtype +{ + BB_VALTYPE_I32, + BB_VALTYPE_I64, + BB_VALTYPE_F32, + BB_VALTYPE_F64, +}; +typedef enum bb_valtype bb_valtype; + +typedef float bb_v128[4]; +union bb_val +{ + int32_t i32_val; + int64_t i64_val; + float f32_val; + double f64_val; + bb_v128 v128_val; + uint32_t externref_val; +}; +typedef union bb_val bb_val; + +struct bb_module_definition_init_opts +{ + const char* debug_name; +}; +typedef struct bb_module_definition_init_opts bb_module_definition_init_opts; + +typedef struct bb_module_definition bb_module_definition; +typedef struct bb_module_instance bb_module_instance; +typedef struct bb_import_package bb_import_package; + +typedef void bb_host_function(void* userdata, bb_module_instance* module, const bb_val* params, bb_val* returns); +typedef void* bb_wasm_memory_resize(void* mem, size_t new_size_bytes, size_t old_size_bytes, void* userdata); +typedef void bb_wasm_memory_free(void* mem, size_t size_bytes, void* userdata); + +struct bb_wasm_memory_config +{ + bb_wasm_memory_resize* resize_callback; + bb_wasm_memory_free* free_callback; + void* userdata; +}; +typedef struct bb_wasm_memory_config bb_wasm_memory_config; + +struct bb_module_instance_instantiate_opts +{ + bb_import_package** packages; + size_t num_packages; + bb_wasm_memory_config wasm_memory_config; + size_t stack_size; + bool enable_debug; +}; +typedef struct bb_module_instance_instantiate_opts bb_module_instance_instantiate_opts; + +struct bb_module_instance_invoke_opts +{ + bool trap_on_start; +}; +typedef struct bb_module_instance_invoke_opts bb_module_instance_invoke_opts; + +struct bb_func_handle +{ + uint32_t index; + uint32_t type; +}; +typedef struct bb_func_handle bb_func_handle; + +struct bb_func_info +{ + bb_valtype* params; + size_t num_params; + bb_valtype* returns; + size_t num_returns; +}; +typedef struct bb_func_info bb_func_info; + +enum bb_global_mut +{ + BB_GLOBAL_MUT_IMMUTABLE, + BB_GLOBAL_MUT_MUTABLE, +}; +typedef enum bb_global_mut bb_global_mut; + +struct bb_global +{ + bb_val* value; + bb_valtype type; + bb_global_mut mut; +}; +typedef struct bb_global bb_global; + +enum bb_debug_trace_mode +{ + BB_DEBUG_TRACE_NONE, + BB_DEBUG_TRACE_FUNCTION, + BB_DEBUG_TRACE_INSTRUCTION, +}; +typedef enum bb_debug_trace_mode bb_debug_trace_mode; + +enum bb_debug_trap_mode +{ + BB_DEBUG_TRAP_MODE_DISABLED, + BB_DEBUG_TRAP_MODE_ENABLED, +}; +typedef enum bb_debug_trap_mode bb_debug_trap_mode; + +const char* bb_error_str(bb_error err); + +bb_module_definition* bb_module_definition_create(bb_module_definition_init_opts opts); +void bb_module_definition_destroy(bb_module_definition* definition); +bb_error bb_module_definition_decode(bb_module_definition* definition, const char* data, size_t length); +bb_slice bb_module_definition_get_custom_section(const bb_module_definition* definition, const char* name); + +bb_import_package* bb_import_package_init(const char* name); +void bb_import_package_deinit(bb_import_package* package); // only deinit when all module_instances using the package have been destroyed +bb_error bb_import_package_add_function(bb_import_package* package, bb_host_function* func, const char* export_name, const bb_valtype* params, size_t num_params, const bb_valtype* returns, size_t num_returns, void* userdata); +bb_error bb_import_package_add_memory(bb_import_package* package, const bb_wasm_memory_config* config, const char* export_name, uint32_t min_pages, uint32_t max_pages); + +void bb_set_debug_trace_mode(bb_debug_trace_mode mode); + +bb_module_instance* bb_module_instance_create(bb_module_definition* definition); +void bb_module_instance_destroy(bb_module_instance* instance); +bb_error bb_module_instance_instantiate(bb_module_instance* instance, bb_module_instance_instantiate_opts opts); +bb_error bb_module_instance_find_func(bb_module_instance* instance, const char* func_name, bb_func_handle* out_handle); +bb_func_info bb_module_instance_func_info(bb_module_instance* instance, bb_func_handle handle); +bb_error bb_module_instance_invoke(bb_module_instance* instance, bb_func_handle, const bb_val* params, size_t num_params, bb_val* returns, size_t num_returns, bb_module_instance_invoke_opts opts); +bb_error bb_module_instance_resume(bb_module_instance* instance, bb_val* returns, size_t num_returns); +bb_error bb_module_instance_step(bb_module_instance* instance, bb_val* returns, size_t num_returns); +bb_error bb_module_instance_debug_set_trap(bb_module_instance* instance, uint32_t address, bb_debug_trap_mode trap_mode); +void* bb_module_instance_mem(bb_module_instance* instance, size_t offset, size_t length); +bb_slice bb_module_instance_mem_all(bb_module_instance* instance); +bb_error bb_module_instance_mem_grow(bb_module_instance* instance, size_t num_pages); +bb_error bb_module_instance_mem_grow_absolute(bb_module_instance* instance, size_t total_pages); +bb_global bb_module_instance_find_global(bb_module_instance* instance, const char* global_name); + +bool bb_func_handle_isvalid(bb_func_handle handle); diff --git a/src/ext/bytebox/src/cffi.zig b/src/ext/bytebox/src/cffi.zig new file mode 100644 index 00000000..7b717ff4 --- /dev/null +++ b/src/ext/bytebox/src/cffi.zig @@ -0,0 +1,845 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const AllocError = std.mem.Allocator.Error; + +const core = @import("core.zig"); +const ValType = core.ValType; +const Val = core.Val; +const ModuleDefinition = core.ModuleDefinition; +const ModuleInstance = core.ModuleInstance; +const ModuleImportPackage = core.ModuleImportPackage; + +const StableArray = @import("zig-stable-array/stable_array.zig").StableArray; + +// C interface +const CSlice = extern struct { + data: ?[*]u8, + length: usize, +}; + +const CError = enum(c_int) { + Ok, + Failed, + OutOfMemory, + InvalidParameter, + UnknownExport, + UnknownImport, + IncompatibleImport, + TrapDebug, + TrapUnreachable, + TrapIntegerDivisionByZero, + TrapIntegerOverflow, + TrapIndirectCallTypeMismatch, + TrapInvalidIntegerConversion, + TrapOutOfBoundsMemoryAccess, + TrapUndefinedElement, + TrapUninitializedElement, + TrapOutOfBoundsTableAccess, + TrapStackExhausted, +}; + +const CModuleDefinitionInitOpts = extern struct { + debug_name: ?[*:0]u8, +}; + +const CHostFunction = *const fn (userdata: ?*anyopaque, module: *core.ModuleInstance, params: [*]const Val, returns: [*]Val) void; + +const CWasmMemoryConfig = extern struct { + resize: ?core.WasmMemoryResizeFunction, + free: ?core.WasmMemoryFreeFunction, + userdata: ?*anyopaque, +}; + +const CModuleInstanceInstantiateOpts = extern struct { + packages: ?[*]?*const ModuleImportPackage, + num_packages: usize, + wasm_memory_config: CWasmMemoryConfig, + stack_size: usize, + enable_debug: bool, +}; + +const CModuleInstanceInvokeOpts = extern struct { + trap_on_start: bool, +}; + +const CFuncHandle = extern struct { + index: u32, + type: u32, +}; + +const CFuncInfo = extern struct { + params: ?[*]const ValType, + num_params: usize, + returns: ?[*]const ValType, + num_returns: usize, +}; + +const CDebugTraceMode = enum(c_int) { + None, + Function, + Instruction, +}; + +const CDebugTrapMode = enum(c_int) { + Disabled, + Enabled, +}; + +const CGlobalMut = enum(c_int) { + Immutable = 0, + Mutable = 1, +}; + +const CGlobalExport = extern struct { + value: ?*Val, + type: ValType, + mut: CGlobalMut, +}; + +// TODO logging callback as well? +// TODO allocator hooks +// const CAllocFunc = *const fn (size: usize, userdata: ?*anyopaque) ?*anyopaque; +// const CReallocFunc = *const fn (mem: ?*anyopaque, size: usize, userdata: ?*anyopaque) ?*anyopaque; +// const CFreeFunc = *const fn (mem: ?*anyopaque, userdata: ?*anyopaque) void; + +const INVALID_FUNC_INDEX = std.math.maxInt(u32); + +var cffi_gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +// const CAllocator = struct { +// const AllocError = std.mem.Allocator.Error; + +// fallback: FallbackAllocator, +// alloc_func: ?CAllocFunc = null, +// realloc_func: ?CReallocFunc = null, +// free_func: ?CFreeFunc = null, +// userdata: ?*anyopaque = null, + +// fn allocator(self: *CAllocator) std.mem.Allocator() { +// if (alloc_func != null and realloc_func != null and free_func != null) { +// return std.mem.Allocator.init( +// self, +// alloc, +// resize, +// free +// ); +// } else { +// return fallback.allocator(); +// } +// } + +// fn alloc(ptr: *anyopaque, len: usize, ptr_align: u29, len_align: u29, ret_addr: usize) AllocError![]u8 { +// _ = ret_addr; + +// var allocator = @ptrCast(*CAllocator, @alignCast(@alignOf(CAllocator), ptr)); +// const size = +// const mem_or_null: ?[*]anyopaque = allocator.alloc_func(size, allocator.userdata); +// if (mem_or_null) |mem| { +// var bytes = @ptrCast([*]u8, @alignCast(1, mem)); +// return bytes[0..size]; +// } else { +// return AllocError.OutOfMemory; +// } +// } + +// fn resize(ptr: *anyopaque, buf: []u8, buf_align: u29, new_len: usize, len_align: u29, ret_addr: usize) ?usize { + +// } + +// fn free(ptr: *anyopaque, buf: []u8, buf_align: u29, ret_addr: usize) void { + +// } +// }; + +// var cffi_allocator = CAllocator{ .fallback = FallbackAllocator{} }; + +// export fn bb_set_memory_hooks(alloc_func: CAllocFunc, realloc_func: CReallocFunc, free_func: CFreeFunc, userdata: ?*anyopaque) void { +// cffi_allocator.alloc_func = alloc_func; +// cffi_allocator.realloc_func = realloc_func; +// cffi_allocator.free_func = free_func; +// cffi_allocator.userdata = userdata; +// } + +export fn bb_error_str(c_error: CError) [*:0]const u8 { + return switch (c_error) { + .Ok => "BB_ERROR_OK", + .Failed => "BB_ERROR_FAILED", + .OutOfMemory => "BB_ERROR_OUTOFMEMORY", + .InvalidParameter => "BB_ERROR_INVALIDPARAMETER", + .UnknownExport => "BB_ERROR_UNKNOWNEXPORT", + .UnknownImport => "BB_ERROR_UNKNOWNIMPORT", + .IncompatibleImport => "BB_ERROR_INCOMPATIBLEIMPORT", + .TrapDebug => "BB_ERROR_TRAP_DEBUG", + .TrapUnreachable => "BB_ERROR_TRAP_UNREACHABLE", + .TrapIntegerDivisionByZero => "BB_ERROR_TRAP_INTEGERDIVISIONBYZERO", + .TrapIntegerOverflow => "BB_ERROR_TRAP_INTEGEROVERFLOW", + .TrapIndirectCallTypeMismatch => "BB_ERROR_TRAP_INDIRECTCALLTYPEMISMATCH", + .TrapInvalidIntegerConversion => "BB_ERROR_TRAP_INVALIDINTEGERCONVERSION", + .TrapOutOfBoundsMemoryAccess => "BB_ERROR_TRAP_OUTOFBOUNDSMEMORYACCESS", + .TrapUndefinedElement => "BB_ERROR_TRAP_UNDEFINEDELEMENT", + .TrapUninitializedElement => "BB_ERROR_TRAP_UNINITIALIZEDELEMENT", + .TrapOutOfBoundsTableAccess => "BB_ERROR_TRAP_OUTOFBOUNDSTABLEACCESS", + .TrapStackExhausted => "BB_ERROR_TRAP_STACKEXHAUSTED", + }; +} + +export fn bb_module_definition_create(c_opts: CModuleDefinitionInitOpts) ?*core.ModuleDefinition { + var allocator = cffi_gpa.allocator(); + + const debug_name: []const u8 = if (c_opts.debug_name == null) "" else std.mem.sliceTo(c_opts.debug_name.?, 0); + const opts_translated = core.ModuleDefinitionOpts{ + .debug_name = debug_name, + }; + return core.createModuleDefinition(allocator, opts_translated) catch null; +} + +export fn bb_module_definition_destroy(module: ?*core.ModuleDefinition) void { + if (module) |m| { + m.destroy(); + } +} + +export fn bb_module_definition_decode(module: ?*core.ModuleDefinition, data: ?[*]u8, length: usize) CError { + if (module != null and data != null) { + const data_slice = data.?[0..length]; + if (module.?.decode(data_slice)) { + return .Ok; + } else |_| { + return CError.Failed; + } + } + + return CError.InvalidParameter; +} + +export fn bb_module_definition_get_custom_section(module: ?*core.ModuleDefinition, name: ?[*:0]const u8) CSlice { + if (module != null and name != null) { + const name_slice: []const u8 = std.mem.sliceTo(name.?, 0); + if (module.?.getCustomSection(name_slice)) |section_data| { + return CSlice{ + .data = section_data.ptr, + .length = section_data.len, + }; + } + } + + return CSlice{ + .data = null, + .length = 0, + }; +} + +export fn bb_import_package_init(c_name: ?[*:0]const u8) ?*ModuleImportPackage { + var package: ?*ModuleImportPackage = null; + var allocator = cffi_gpa.allocator(); + + if (c_name != null) { + package = allocator.create(ModuleImportPackage) catch null; + + if (package) |p| { + const name: []const u8 = std.mem.sliceTo(c_name.?, 0); + p.* = ModuleImportPackage.init(name, null, null, allocator) catch { + allocator.destroy(p); + return null; + }; + } + } + + return package; +} + +export fn bb_import_package_deinit(package: ?*ModuleImportPackage) void { + if (package) |p| { + p.deinit(); + } +} + +export fn bb_import_package_add_function(package: ?*ModuleImportPackage, func: ?CHostFunction, c_name: ?[*:0]const u8, c_params: ?[*]ValType, num_params: usize, c_returns: ?[*]ValType, num_returns: usize, userdata: ?*anyopaque) CError { + if (package != null and c_name != null and func != null) { + if (num_params > 0 and c_params == null) { + return CError.InvalidParameter; + } + if (num_returns > 0 and c_returns == null) { + return CError.InvalidParameter; + } + + const name: []const u8 = std.mem.sliceTo(c_name.?, 0); + const param_types: []ValType = if (c_params) |params| params[0..num_params] else &[_]ValType{}; + const return_types: []ValType = if (c_returns) |returns| returns[0..num_returns] else &[_]ValType{}; + + package.?.addHostFunction(name, param_types, return_types, func.?, userdata) catch { + return CError.OutOfMemory; + }; + + return CError.Ok; + } + + return CError.InvalidParameter; +} + +export fn bb_import_package_add_memory(package: ?*ModuleImportPackage, config: ?*CWasmMemoryConfig, c_name: ?[*:0]const u8, min_pages: u32, max_pages: u32) CError { + if (package != null and config != null and c_name != null) { + if ((package.?.memories.items.len > 0)) { + return CError.InvalidParameter; + } + if (config.?.resize == null) { + return CError.InvalidParameter; + } + if (config.?.free == null) { + return CError.InvalidParameter; + } + + const name: []const u8 = std.mem.sliceTo(c_name.?, 0); + const limits = core.Limits{ + .min = min_pages, + .max = max_pages, + }; + + var allocator: *std.mem.Allocator = &package.?.allocator; + + var mem_instance = allocator.create(core.MemoryInstance) catch return CError.OutOfMemory; + + const wasm_memory_config = core.WasmMemoryExternal{ + .resize_callback = config.?.resize.?, + .free_callback = config.?.free.?, + .userdata = config.?.userdata, + }; + + mem_instance.* = core.MemoryInstance.init(limits, wasm_memory_config); + if (mem_instance.grow(limits.min) == false) { + unreachable; + } + + var mem_import = core.MemoryImport{ + .name = name, + .data = .{ .Host = mem_instance }, + }; + + package.?.memories.append(mem_import) catch { + mem_instance.deinit(); + allocator.destroy(mem_instance); + return CError.OutOfMemory; + }; + } + + return CError.InvalidParameter; +} + +export fn bb_set_debug_trace_mode(c_mode: CDebugTraceMode) void { + const mode = switch (c_mode) { + .None => core.DebugTrace.Mode.None, + .Function => core.DebugTrace.Mode.Function, + .Instruction => core.DebugTrace.Mode.Instruction, + }; + _ = core.DebugTrace.setMode(mode); +} + +export fn bb_module_instance_create(module_definition: ?*ModuleDefinition) ?*ModuleInstance { + var allocator = cffi_gpa.allocator(); + + var module: ?*core.ModuleInstance = null; + + if (module_definition) |def| { + module = core.createModuleInstance(.Stack, def, allocator) catch null; + } + + return module; +} + +export fn bb_module_instance_destroy(module: ?*ModuleInstance) void { + if (module) |m| { + m.destroy(); + } +} + +export fn bb_module_instance_instantiate(module: ?*ModuleInstance, c_opts: CModuleInstanceInstantiateOpts) CError { + // Both wasm memory config callbacks must be set or null - partially overriding the behavior isn't valid + var num_wasm_memory_callbacks: u32 = 0; + num_wasm_memory_callbacks += if (c_opts.wasm_memory_config.resize != null) 1 else 0; + num_wasm_memory_callbacks += if (c_opts.wasm_memory_config.free != null) 1 else 0; + + if (module != null and c_opts.packages != null and num_wasm_memory_callbacks != 1) { + const packages: []?*const ModuleImportPackage = c_opts.packages.?[0..c_opts.num_packages]; + + var allocator = cffi_gpa.allocator(); + var flat_packages = std.ArrayList(ModuleImportPackage).init(allocator); + defer flat_packages.deinit(); + + flat_packages.ensureTotalCapacityPrecise(packages.len) catch return CError.OutOfMemory; + for (packages) |p| { + if (p != null) { + flat_packages.appendAssumeCapacity(p.?.*); + } + } + + var opts = core.ModuleInstantiateOpts{ + .imports = flat_packages.items, + .stack_size = c_opts.stack_size, + .enable_debug = c_opts.enable_debug, + }; + + if (num_wasm_memory_callbacks > 0) { + opts.wasm_memory_external = core.WasmMemoryExternal{ + .resize_callback = c_opts.wasm_memory_config.resize.?, + .free_callback = c_opts.wasm_memory_config.free.?, + .userdata = c_opts.wasm_memory_config.userdata, + }; + } + + if (module.?.instantiate(opts)) { + return CError.Ok; + } else |err| { + return translateError(err); + } + } + + return CError.InvalidParameter; +} + +export fn bb_module_instance_find_func(module: ?*ModuleInstance, c_func_name: ?[*:0]const u8, out_handle: ?*CFuncHandle) CError { + if (module != null and c_func_name != null and out_handle != null) { + const func_name = std.mem.sliceTo(c_func_name.?, 0); + + out_handle.?.index = INVALID_FUNC_INDEX; + + if (module.?.getFunctionHandle(func_name)) |handle| { + out_handle.?.index = handle.index; + out_handle.?.type = @intFromEnum(handle.type); + return CError.Ok; + } else |err| { + std.debug.assert(err == error.ExportUnknownFunction); + return CError.UnknownExport; + } + } + + return CError.InvalidParameter; +} + +export fn bb_module_instance_func_info(module: ?*ModuleInstance, c_func_handle: CFuncHandle) CFuncInfo { + if (module != null and c_func_handle.index != INVALID_FUNC_INDEX) { + if (std.meta.intToEnum(core.FunctionHandleType, c_func_handle.type)) |handle_type| { + const func_handle = core.FunctionHandle{ + .index = c_func_handle.index, + .type = handle_type, + }; + + const maybe_info: ?core.FunctionExport = module.?.getFunctionInfo(func_handle); + if (maybe_info) |info| { + return CFuncInfo{ + .params = if (info.params.len > 0) info.params.ptr else null, + .num_params = info.params.len, + .returns = if (info.returns.len > 0) info.returns.ptr else null, + .num_returns = info.returns.len, + }; + } + } else |_| {} // intToEnum failed, user must have passed invalid data + } + + return CFuncInfo{ + .params = null, + .num_params = 0, + .returns = null, + .num_returns = 0, + }; +} + +export fn bb_module_instance_invoke(module: ?*ModuleInstance, c_handle: CFuncHandle, params: ?[*]const Val, num_params: usize, returns: ?[*]Val, num_returns: usize, opts: CModuleInstanceInvokeOpts) CError { + if (module != null and c_handle.index != INVALID_FUNC_INDEX) { + const handle = core.FunctionHandle{ + .index = c_handle.index, + .type = @as(core.FunctionHandleType, @enumFromInt(c_handle.type)), + }; + + const invoke_opts = core.InvokeOpts{ + .trap_on_start = opts.trap_on_start, + }; + + var params_slice: []const Val = if (params != null) params.?[0..num_params] else &[_]Val{}; + var returns_slice: []Val = if (returns != null) returns.?[0..num_returns] else &[_]Val{}; + + if (module.?.invoke(handle, params_slice.ptr, returns_slice.ptr, invoke_opts)) { + return CError.Ok; + } else |err| { + return translateError(err); + } + } + + return CError.InvalidParameter; +} + +export fn bb_module_instance_resume(module: ?*ModuleInstance, returns: ?[*]Val, num_returns: usize) CError { + _ = module; + _ = returns; + _ = num_returns; + return CError.Failed; +} + +export fn bb_module_instance_step(module: ?*ModuleInstance, returns: ?[*]Val, num_returns: usize) CError { + _ = module; + _ = returns; + _ = num_returns; + return CError.Failed; +} + +export fn bb_module_instance_debug_set_trap(module: ?*ModuleInstance, address: u32, trap_mode: CDebugTrapMode) CError { + _ = module; + _ = address; + _ = trap_mode; + return CError.Failed; +} + +export fn bb_module_instance_mem(module: ?*ModuleInstance, offset: usize, length: usize) ?*anyopaque { + if (module != null and length > 0) { + var mem = module.?.memorySlice(offset, length); + return if (mem.len > 0) mem.ptr else null; + } + + return null; +} + +export fn bb_module_instance_mem_all(module: ?*ModuleInstance) CSlice { + if (module != null) { + var mem = module.?.memoryAll(); + return CSlice{ + .data = mem.ptr, + .length = mem.len, + }; + } + + return CSlice{ + .data = null, + .length = 0, + }; +} + +export fn bb_module_instance_mem_grow(module: ?*ModuleInstance, num_pages: usize) CError { + if (module != null) { + if (module.?.memoryGrow(num_pages)) { + return CError.Ok; + } else { + return CError.Failed; + } + } + return CError.InvalidParameter; +} + +export fn bb_module_instance_mem_grow_absolute(module: ?*ModuleInstance, total_pages: usize) CError { + if (module != null) { + if (module.?.memoryGrowAbsolute(total_pages)) { + return CError.Ok; + } else { + return CError.Failed; + } + } + return CError.InvalidParameter; +} + +export fn bb_module_instance_find_global(module: ?*ModuleInstance, c_global_name: ?[*:0]const u8) CGlobalExport { + comptime { + std.debug.assert(@intFromEnum(CGlobalMut.Immutable) == @intFromEnum(core.GlobalMut.Immutable)); + std.debug.assert(@intFromEnum(CGlobalMut.Mutable) == @intFromEnum(core.GlobalMut.Mutable)); + } + + if (module != null and c_global_name != null) { + const global_name = std.mem.sliceTo(c_global_name.?, 0); + if (module.?.getGlobalExport(global_name)) |global| { + return CGlobalExport{ + .value = global.val, + .type = global.valtype, + .mut = @as(CGlobalMut, @enumFromInt(@intFromEnum(global.mut))), + }; + } else |_| {} + } + + return CGlobalExport{ + .value = null, + .type = .I32, + .mut = .Immutable, + }; +} + +export fn bb_func_handle_isvalid(c_handle: CFuncHandle) bool { + return c_handle.index != INVALID_FUNC_INDEX; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Local helpers + +fn translateError(err: anyerror) CError { + switch (err) { + error.OutOfMemory => return CError.OutOfMemory, + error.UnlinkableUnknownImport => return CError.UnknownImport, + error.UnlinkableIncompatibleImportType => return CError.IncompatibleImport, + error.TrapDebug => return CError.TrapDebug, + error.TrapUnreachable => return CError.TrapUnreachable, + error.TrapIntegerDivisionByZero => return CError.TrapIntegerDivisionByZero, + error.TrapIntegerOverflow => return CError.TrapIntegerOverflow, + error.TrapIndirectCallTypeMismatch => return CError.TrapIndirectCallTypeMismatch, + error.TrapInvalidIntegerConversion => return CError.TrapInvalidIntegerConversion, + error.TrapOutOfBoundsMemoryAccess => return CError.TrapOutOfBoundsMemoryAccess, + error.TrapUndefinedElement => return CError.TrapUndefinedElement, + error.TrapUninitializedElement => return CError.TrapUninitializedElement, + error.TrapOutOfBoundsTableAccess => return CError.TrapOutOfBoundsTableAccess, + error.TrapStackExhausted => return CError.TrapStackExhausted, + else => return CError.Failed, + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// MSVC linking compat + +// NOTE: Zig expects various chkstk functions to be present during linking, which would be fine if +// zig or clang linked this code, but when linking a static lib with the MSVC compiler, the compiler +// runtime has different names for these functions. Here we borrow the compiler_rt stack_probe.zig +// file and adapt it for our uses to ensure we can link with both clang and msvc runtimes. + +comptime { + if (builtin.os.tag == .windows) { + const is_mingw = builtin.os.tag == .windows and builtin.abi.isGnu(); + + // Default stack-probe functions emitted by LLVM + if (is_mingw) { + @export(_chkstk, .{ .name = "_alloca", .linkage = .Weak }); + @export(___chkstk_ms, .{ .name = "___chkstk_ms", .linkage = .Weak }); + + if (builtin.cpu.arch.isAARCH64()) { + @export(__chkstk, .{ .name = "__chkstk", .linkage = .Weak }); + } + } else if (!builtin.link_libc) { + // This symbols are otherwise exported by MSVCRT.lib + @export(_chkstk, .{ .name = "_chkstk", .linkage = .Weak }); + @export(__chkstk, .{ .name = "__chkstk", .linkage = .Weak }); + } + } + + switch (builtin.cpu.arch) { + .x86, + .x86_64, + => { + @export(zig_probe_stack, .{ .name = "__zig_probe_stack", .linkage = .Weak }); + }, + else => {}, + } +} + +// Zig's own stack-probe routine (available only on x86 and x86_64) +fn zig_probe_stack() callconv(.Naked) void { + @setRuntimeSafety(false); + + // Versions of the Linux kernel before 5.1 treat any access below SP as + // invalid so let's update it on the go, otherwise we'll get a segfault + // instead of triggering the stack growth. + + switch (builtin.cpu.arch) { + .x86_64 => { + // %rax = probe length, %rsp = stack pointer + asm volatile ( + \\ push %%rcx + \\ mov %%rax, %%rcx + \\ cmp $0x1000,%%rcx + \\ jb 2f + \\ 1: + \\ sub $0x1000,%%rsp + \\ orl $0,16(%%rsp) + \\ sub $0x1000,%%rcx + \\ cmp $0x1000,%%rcx + \\ ja 1b + \\ 2: + \\ sub %%rcx, %%rsp + \\ orl $0,16(%%rsp) + \\ add %%rax,%%rsp + \\ pop %%rcx + \\ ret + ); + }, + .x86 => { + // %eax = probe length, %esp = stack pointer + asm volatile ( + \\ push %%ecx + \\ mov %%eax, %%ecx + \\ cmp $0x1000,%%ecx + \\ jb 2f + \\ 1: + \\ sub $0x1000,%%esp + \\ orl $0,8(%%esp) + \\ sub $0x1000,%%ecx + \\ cmp $0x1000,%%ecx + \\ ja 1b + \\ 2: + \\ sub %%ecx, %%esp + \\ orl $0,8(%%esp) + \\ add %%eax,%%esp + \\ pop %%ecx + \\ ret + ); + }, + else => {}, + } + + unreachable; +} + +fn win_probe_stack_only() void { + @setRuntimeSafety(false); + + switch (builtin.cpu.arch) { + .x86_64 => { + asm volatile ( + \\ push %%rcx + \\ push %%rax + \\ cmp $0x1000,%%rax + \\ lea 24(%%rsp),%%rcx + \\ jb 1f + \\ 2: + \\ sub $0x1000,%%rcx + \\ test %%rcx,(%%rcx) + \\ sub $0x1000,%%rax + \\ cmp $0x1000,%%rax + \\ ja 2b + \\ 1: + \\ sub %%rax,%%rcx + \\ test %%rcx,(%%rcx) + \\ pop %%rax + \\ pop %%rcx + \\ ret + ); + }, + .x86 => { + asm volatile ( + \\ push %%ecx + \\ push %%eax + \\ cmp $0x1000,%%eax + \\ lea 12(%%esp),%%ecx + \\ jb 1f + \\ 2: + \\ sub $0x1000,%%ecx + \\ test %%ecx,(%%ecx) + \\ sub $0x1000,%%eax + \\ cmp $0x1000,%%eax + \\ ja 2b + \\ 1: + \\ sub %%eax,%%ecx + \\ test %%ecx,(%%ecx) + \\ pop %%eax + \\ pop %%ecx + \\ ret + ); + }, + else => {}, + } + if (comptime builtin.cpu.arch.isAARCH64()) { + // NOTE: page size hardcoded to 4096 for now + asm volatile ( + \\ lsl x16, x15, #4 + \\ mov x17, sp + \\1: + \\ + \\ sub x17, x17, 4096 + \\ subs x16, x16, 4096 + \\ ldr xzr, [x17] + \\ b.gt 1b + \\ + \\ ret + ); + } + + unreachable; +} + +fn win_probe_stack_adjust_sp() void { + @setRuntimeSafety(false); + + switch (builtin.cpu.arch) { + .x86_64 => { + asm volatile ( + \\ push %%rcx + \\ cmp $0x1000,%%rax + \\ lea 16(%%rsp),%%rcx + \\ jb 1f + \\ 2: + \\ sub $0x1000,%%rcx + \\ test %%rcx,(%%rcx) + \\ sub $0x1000,%%rax + \\ cmp $0x1000,%%rax + \\ ja 2b + \\ 1: + \\ sub %%rax,%%rcx + \\ test %%rcx,(%%rcx) + \\ + \\ lea 8(%%rsp),%%rax + \\ mov %%rcx,%%rsp + \\ mov -8(%%rax),%%rcx + \\ push (%%rax) + \\ sub %%rsp,%%rax + \\ ret + ); + }, + .x86 => { + asm volatile ( + \\ push %%ecx + \\ cmp $0x1000,%%eax + \\ lea 8(%%esp),%%ecx + \\ jb 1f + \\ 2: + \\ sub $0x1000,%%ecx + \\ test %%ecx,(%%ecx) + \\ sub $0x1000,%%eax + \\ cmp $0x1000,%%eax + \\ ja 2b + \\ 1: + \\ sub %%eax,%%ecx + \\ test %%ecx,(%%ecx) + \\ + \\ lea 4(%%esp),%%eax + \\ mov %%ecx,%%esp + \\ mov -4(%%eax),%%ecx + \\ push (%%eax) + \\ sub %%esp,%%eax + \\ ret + ); + }, + else => {}, + } + + unreachable; +} + +// Windows has a multitude of stack-probing functions with similar names and +// slightly different behaviours: some behave as alloca() and update the stack +// pointer after probing the stack, other do not. +// +// Function name | Adjusts the SP? | +// | x86 | x86_64 | +// ---------------------------------------- +// _chkstk (_alloca) | yes | yes | +// __chkstk | yes | no | +// __chkstk_ms | no | no | +// ___chkstk (__alloca) | yes | yes | +// ___chkstk_ms | no | no | + +fn _chkstk() callconv(.Naked) void { + @setRuntimeSafety(false); + @call(.always_inline, win_probe_stack_adjust_sp, .{}); +} +fn __chkstk() callconv(.Naked) void { + @setRuntimeSafety(false); + if (comptime builtin.cpu.arch.isAARCH64()) { + @call(.always_inline, win_probe_stack_only, .{}); + } else switch (builtin.cpu.arch) { + .x86 => @call(.always_inline, win_probe_stack_adjust_sp, .{}), + .x86_64 => @call(.always_inline, win_probe_stack_only, .{}), + else => unreachable, + } +} +fn ___chkstk() callconv(.Naked) void { + @setRuntimeSafety(false); + @call(.always_inline, win_probe_stack_adjust_sp, .{}); +} +fn __chkstk_ms() callconv(.Naked) void { + @setRuntimeSafety(false); + @call(.always_inline, win_probe_stack_only, .{}); +} +fn ___chkstk_ms() callconv(.Naked) void { + @setRuntimeSafety(false); + @call(.always_inline, win_probe_stack_only, .{}); +} diff --git a/src/ext/bytebox/src/common.zig b/src/ext/bytebox/src/common.zig new file mode 100644 index 00000000..cd6410f3 --- /dev/null +++ b/src/ext/bytebox/src/common.zig @@ -0,0 +1,97 @@ +// Lowest layer of the codebase, that contains types and code used in higher layers + +const std = @import("std"); + +pub const StableArray = @import("zig-stable-array/stable_array.zig").StableArray; + +pub fn decodeLEB128(comptime T: type, reader: anytype) !T { + if (@typeInfo(T).Int.signedness == .signed) { + return std.leb.readILEB128(T, reader) catch |e| { + if (e == error.Overflow) { + return error.MalformedLEB128; + } else { + return e; + } + }; + } else { + return std.leb.readULEB128(T, reader) catch |e| { + if (e == error.Overflow) { + return error.MalformedLEB128; + } else { + return e; + } + }; + } +} + +pub const ScratchAllocator = struct { + buffer: StableArray(u8), + + const InitOpts = struct { + max_size: usize, + }; + + fn init(opts: InitOpts) ScratchAllocator { + return ScratchAllocator{ + .buffer = StableArray(u8).init(opts.max_size), + }; + } + + pub fn allocator(self: *ScratchAllocator) std.mem.Allocator { + return std.mem.Allocator.init(self, alloc, resize, free); + } + + pub fn reset(self: *ScratchAllocator) void { + self.buffer.resize(0) catch unreachable; + } + + fn alloc( + self: *ScratchAllocator, + len: usize, + ptr_align: u29, + len_align: u29, + ret_addr: usize, + ) std.mem.Allocator.Error![]u8 { + _ = ret_addr; + _ = len_align; + + const alloc_size = len; + const offset_begin = std.mem.alignForward(self.buffer.items.len, ptr_align); + const offset_end = offset_begin + alloc_size; + self.buffer.resize(offset_end) catch { + return std.mem.Allocator.Error.OutOfMemory; + }; + return self.buffer.items[offset_begin..offset_end]; + } + + fn resize( + self: *ScratchAllocator, + old_mem: []u8, + old_align: u29, + new_size: usize, + len_align: u29, + ret_addr: usize, + ) ?usize { + _ = self; + _ = old_align; + _ = ret_addr; + + if (new_size > old_mem.len) { + return null; + } + const aligned_size: usize = if (len_align == 0) new_size else std.mem.alignForward(new_size, len_align); + return aligned_size; + } + + fn free( + self: *ScratchAllocator, + old_mem: []u8, + old_align: u29, + ret_addr: usize, + ) void { + _ = self; + _ = old_mem; + _ = old_align; + _ = ret_addr; + } +}; diff --git a/src/ext/bytebox/src/core.zig b/src/ext/bytebox/src/core.zig new file mode 100644 index 00000000..14b19e48 --- /dev/null +++ b/src/ext/bytebox/src/core.zig @@ -0,0 +1,72 @@ +const std = @import("std"); +const def = @import("definition.zig"); +const inst = @import("instance.zig"); +const vm_stack = @import("vm_stack.zig"); +const vm_register = @import("vm_register.zig"); +pub const wasi = @import("wasi.zig"); + +pub const i8x16 = def.i8x16; +pub const u8x16 = def.u8x16; +pub const i16x8 = def.i16x8; +pub const u16x8 = def.u16x8; +pub const i32x4 = def.i32x4; +pub const u32x4 = def.u32x4; +pub const i64x2 = def.i64x2; +pub const u64x2 = def.u64x2; +pub const f32x4 = def.f32x4; +pub const f64x2 = def.f64x2; +pub const v128 = def.v128; + +pub const MalformedError = def.MalformedError; +pub const ValidationError = def.ValidationError; + +pub const FunctionExport = def.FunctionExport; +pub const FunctionHandle = def.FunctionHandle; +pub const FunctionHandleType = def.FunctionHandleType; +pub const GlobalDefinition = def.GlobalDefinition; +pub const GlobalMut = def.GlobalMut; +pub const Limits = def.Limits; +pub const ModuleDefinition = def.ModuleDefinition; +pub const ModuleDefinitionOpts = def.ModuleDefinitionOpts; +pub const TaggedVal = def.TaggedVal; +pub const Val = def.Val; +pub const ValType = def.ValType; + +pub const UnlinkableError = inst.UnlinkableError; +pub const UninstantiableError = inst.UninstantiableError; +pub const ExportError = inst.ExportError; +pub const TrapError = inst.TrapError; + +pub const DebugTrace = inst.DebugTrace; +pub const GlobalImport = inst.GlobalImport; +pub const GlobalInstance = inst.GlobalInstance; +pub const MemoryImport = inst.MemoryImport; +pub const MemoryInstance = inst.MemoryInstance; +pub const ModuleImportPackage = inst.ModuleImportPackage; +pub const ModuleInstance = inst.ModuleInstance; +pub const ModuleInstantiateOpts = inst.ModuleInstantiateOpts; +pub const TableImport = inst.TableImport; +pub const TableInstance = inst.TableInstance; +pub const WasmMemoryExternal = inst.WasmMemoryExternal; +pub const WasmMemoryFreeFunction = inst.WasmMemoryFreeFunction; +pub const WasmMemoryResizeFunction = inst.WasmMemoryResizeFunction; +pub const InvokeOpts = inst.InvokeOpts; + +const AllocError = std.mem.Allocator.Error; + +pub fn createModuleDefinition(allocator: std.mem.Allocator, opts: ModuleDefinitionOpts) AllocError!*ModuleDefinition { + return try ModuleDefinition.create(allocator, opts); +} + +pub const VmType = enum { + Stack, + Register, +}; + +pub fn createModuleInstance(vm_type: VmType, module_def: *const ModuleDefinition, allocator: std.mem.Allocator) AllocError!*ModuleInstance { + var vm: *inst.VM = switch (vm_type) { + .Stack => try inst.VM.create(vm_stack.StackVM, allocator), + .Register => try inst.VM.create(vm_register.RegisterVM, allocator), + }; + return try ModuleInstance.create(module_def, vm, allocator); +} diff --git a/src/ext/bytebox/src/definition.zig b/src/ext/bytebox/src/definition.zig new file mode 100644 index 00000000..3270cd87 --- /dev/null +++ b/src/ext/bytebox/src/definition.zig @@ -0,0 +1,3556 @@ +// This file contains types and code shared between both the ModuleDefinition and VMs + +const std = @import("std"); +const AllocError = std.mem.Allocator.Error; + +const common = @import("common.zig"); +const StableArray = common.StableArray; + +const opcodes = @import("opcode.zig"); +const Opcode = opcodes.Opcode; +const WasmOpcode = opcodes.WasmOpcode; + +// HACK: just get the code working, will need to resolve anything dependent on this eventually +const inst = @import("instance.zig"); +const ModuleInstance = inst.ModuleInstance; +const Store = inst.Store; +const GlobalInstance = inst.GlobalInstance; + +pub const MalformedError = error{ + MalformedMagicSignature, + MalformedUnexpectedEnd, + MalformedUnsupportedWasmVersion, + MalformedSectionId, + MalformedTypeSentinel, + MalformedLEB128, + MalformedMissingZeroByte, + MalformedTooManyLocals, + MalformedFunctionCodeSectionMismatch, + MalformedMissingDataCountSection, + MalformedDataCountMismatch, + MalformedDataType, + MalformedIllegalOpcode, + MalformedReferenceType, + MalformedSectionSizeMismatch, + MalformedInvalidImport, + MalformedLimits, + MalformedMultipleStartSections, + MalformedElementType, + MalformedUTF8Encoding, + MalformedMutability, + MalformedCustomSection, + + MalformedValType, + MalformedBytecode, +}; + +pub const ValidationError = error{ + ValidationTypeMismatch, + ValidationTypeMustBeNumeric, + ValidationUnknownType, + ValidationUnknownFunction, + ValidationUnknownGlobal, + ValidationUnknownLocal, + ValidationUnknownTable, + ValidationUnknownMemory, + ValidationUnknownElement, + ValidationUnknownData, + ValidationOutOfBounds, + ValidationTypeStackHeightMismatch, + ValidationBadAlignment, + ValidationUnknownLabel, + ValidationImmutableGlobal, + ValidationBadConstantExpression, + ValidationGlobalReferencingMutableGlobal, + ValidationUnknownBlockTypeIndex, + ValidationSelectArity, + ValidationMultipleMemories, + ValidationMemoryInvalidMaxLimit, + ValidationMemoryMaxPagesExceeded, + ValidationConstantExpressionGlobalMustBeImport, + ValidationConstantExpressionGlobalMustBeImmutable, + ValidationStartFunctionType, + ValidationLimitsMinMustNotBeLargerThanMax, + ValidationConstantExpressionTypeMismatch, + ValidationDuplicateExportName, + ValidationFuncRefUndeclared, + ValidationIfElseMismatch, + ValidationInvalidLaneIndex, +}; + +pub const i8x16 = @Vector(16, i8); +pub const u8x16 = @Vector(16, u8); +pub const i16x8 = @Vector(8, i16); +pub const u16x8 = @Vector(8, u16); +pub const i32x4 = @Vector(4, i32); +pub const u32x4 = @Vector(4, u32); +pub const i64x2 = @Vector(2, i64); +pub const u64x2 = @Vector(2, u64); +pub const f32x4 = @Vector(4, f32); +pub const f64x2 = @Vector(2, f64); +pub const v128 = f32x4; + +const Section = enum(u8) { Custom, FunctionType, Import, Function, Table, Memory, Global, Export, Start, Element, Code, Data, DataCount }; + +const k_function_type_sentinel_byte: u8 = 0x60; +const k_block_type_void_sentinel_byte: u8 = 0x40; + +fn decodeFloat(comptime T: type, reader: anytype) !T { + return switch (T) { + f32 => @as(f32, @bitCast(try reader.readIntLittle(u32))), + f64 => @as(f64, @bitCast(try reader.readIntLittle(u64))), + else => unreachable, + }; +} + +fn decodeVec(reader: anytype) !v128 { + var bytes: [16]u8 = undefined; + _ = try reader.read(&bytes); + return std.mem.bytesToValue(v128, &bytes); +} + +pub const ValType = enum(c_int) { + I32, + I64, + F32, + F64, + V128, + FuncRef, + ExternRef, + + fn bytecodeToValtype(byte: u8) !ValType { + return switch (byte) { + 0x7B => .V128, + 0x7F => .I32, + 0x7E => .I64, + 0x7D => .F32, + 0x7C => .F64, + 0x70 => .FuncRef, + 0x6F => .ExternRef, + else => { + return error.MalformedValType; + }, + }; + } + + fn decode(reader: anytype) !ValType { + return try bytecodeToValtype(try reader.readByte()); + } + + fn decodeReftype(reader: anytype) !ValType { + var valtype = try decode(reader); + if (isRefType(valtype) == false) { + return error.MalformedReferenceType; + } + return valtype; + } + + pub fn isRefType(valtype: ValType) bool { + return switch (valtype) { + .FuncRef => true, + .ExternRef => true, + else => false, + }; + } + + pub fn count() comptime_int { + return @typeInfo(ValType).Enum.fields.len; + } +}; + +pub const Val = extern union { + I32: i32, + I64: i64, + F32: f32, + F64: f64, + V128: v128, + FuncRef: extern struct { + index: u32, // index into functions + module_instance: ?*ModuleInstance, // TODO make this an index as well + + // fn getModule(self: @This()) *ModuleInstance { + // return @as(*ModuleInstance, @alignCast(@ptrCast(self.module_instance))); + // } + }, + ExternRef: u32, // TODO figure out what this indexes + + const k_null_funcref: u32 = std.math.maxInt(u32); + + pub fn default(valtype: ValType) Val { + return switch (valtype) { + .I32 => Val{ .I32 = 0 }, + .I64 => Val{ .I64 = 0 }, + .F32 => Val{ .F32 = 0.0 }, + .F64 => Val{ .F64 = 0.0 }, + .V128 => Val{ .V128 = f32x4{ 0, 0, 0, 0 } }, + .FuncRef => nullRef(.FuncRef) catch unreachable, + .ExternRef => nullRef(.ExternRef) catch unreachable, + }; + } + + pub fn nullRef(valtype: ValType) !Val { + return switch (valtype) { + .FuncRef => funcrefFromIndex(Val.k_null_funcref), + .ExternRef => Val{ .ExternRef = Val.k_null_funcref }, + else => error.MalformedBytecode, + }; + } + + pub fn funcrefFromIndex(index: u32) Val { + return Val{ .FuncRef = .{ .index = index, .module_instance = null } }; + } + + pub fn isNull(v: Val) bool { + // Because FuncRef.index is located at the same memory location as ExternRef, this passes for both types + return v.FuncRef.index == k_null_funcref; + } + + pub fn eql(valtype: ValType, v1: Val, v2: Val) bool { + return switch (valtype) { + .I32 => v1.I32 == v2.I32, + .I64 => v1.I64 == v2.I64, + .F32 => v1.F32 == v2.F32, + .F64 => v1.F64 == v2.F64, + .V128 => @reduce(.And, v1.V128 == v2.V128), + .FuncRef => std.meta.eql(v1.FuncRef, v2.FuncRef), + .ExternRef => v1.ExternRef == v2.ExternRef, + }; + } +}; + +test "Val.isNull" { + const v1: Val = try Val.nullRef(.FuncRef); + const v2: Val = try Val.nullRef(.ExternRef); + + try std.testing.expect(v1.isNull() == true); + try std.testing.expect(v2.isNull() == true); + + const v3 = Val.funcrefFromIndex(12); + const v4 = Val{ .ExternRef = 234 }; + + try std.testing.expect(v3.isNull() == false); + try std.testing.expect(v4.isNull() == false); +} + +pub const TaggedVal = struct { + val: Val, + type: ValType, + + pub fn nullRef(valtype: ValType) !TaggedVal { + return TaggedVal{ + .val = try Val.nullRef(valtype), + .type = valtype, + }; + } + + pub fn funcrefFromIndex(index: u32) TaggedVal { + return TaggedVal{ + .val = Val.funcrefFromIndex(index), + .type = .FuncRef, + }; + } + + pub const HashMapContext = struct { + pub fn hash(self: @This(), v: TaggedVal) u64 { + _ = self; + + var hasher = std.hash.Wyhash.init(0); + hasher.update(std.mem.asBytes(&v.val)); + hasher.update(std.mem.asBytes(&v.type)); + return hasher.final(); + } + + pub fn eql(self: @This(), a: TaggedVal, b: TaggedVal) bool { + _ = self; + + return a.type == b.type and Val.eql(a.type, a.val, b.val); + } + }; +}; + +pub const Limits = struct { + min: u32, + max: ?u32, + + fn decode(reader: anytype) !Limits { + const has_max = try reader.readByte(); + if (has_max > 1) { + return error.MalformedLimits; + } + const min = try common.decodeLEB128(u32, reader); + var max: ?u32 = null; + + switch (has_max) { + 0 => {}, + 1 => { + max = try common.decodeLEB128(u32, reader); + if (max.? < min) { + return error.ValidationLimitsMinMustNotBeLargerThanMax; + } + }, + else => unreachable, + } + + return Limits{ + .min = min, + .max = max, + }; + } +}; + +const BlockType = enum { + Void, + ValType, + TypeIndex, +}; + +pub const BlockTypeValue = union(BlockType) { + Void: void, + ValType: ValType, + TypeIndex: u32, + + fn getBlocktypeParamTypes(blocktype: BlockTypeValue, module_def: *const ModuleDefinition) []const ValType { + switch (blocktype) { + else => return &BlockTypeStatics.empty, + .TypeIndex => |index| return module_def.types.items[index].getParams(), + } + } + + fn getBlocktypeReturnTypes(blocktype: BlockTypeValue, module_def: *const ModuleDefinition) []const ValType { + switch (blocktype) { + .Void => return &BlockTypeStatics.empty, + .ValType => |v| return switch (v) { + .I32 => &BlockTypeStatics.valtype_i32, + .I64 => &BlockTypeStatics.valtype_i64, + .F32 => &BlockTypeStatics.valtype_f32, + .F64 => &BlockTypeStatics.valtype_f64, + .V128 => &BlockTypeStatics.valtype_v128, + .FuncRef => &BlockTypeStatics.reftype_funcref, + .ExternRef => &BlockTypeStatics.reftype_externref, + }, + .TypeIndex => |index| return module_def.types.items[index].getReturns(), + } + } + + fn toU64(value: BlockTypeValue) u64 { + comptime { + std.debug.assert(@sizeOf(BlockTypeValue) == @sizeOf(u64)); + } + const value_ptr: *const BlockTypeValue = &value; + return @as(*const u64, @ptrCast(value_ptr)).*; + } + + fn fromU64(value: u64) BlockTypeValue { + const value_ptr: *const u64 = &value; + return @as(*const BlockTypeValue, @ptrCast(value_ptr)).*; + } +}; + +pub const BlockTypeStatics = struct { + const empty = [_]ValType{}; + const valtype_i32 = [_]ValType{.I32}; + const valtype_i64 = [_]ValType{.I64}; + const valtype_f32 = [_]ValType{.F32}; + const valtype_f64 = [_]ValType{.F64}; + const valtype_v128 = [_]ValType{.V128}; + const reftype_funcref = [_]ValType{.FuncRef}; + const reftype_externref = [_]ValType{.ExternRef}; +}; + +const ConstantExpressionType = enum { + Value, + Global, +}; + +pub const ConstantExpression = union(ConstantExpressionType) { + Value: TaggedVal, + Global: u32, // global index + + const ExpectedGlobalMut = enum { + Any, + Immutable, + }; + + fn decode(reader: anytype, module_def: *const ModuleDefinition, comptime expected_global_mut: ExpectedGlobalMut, expected_valtype: ValType) !ConstantExpression { + const opcode = try WasmOpcode.decode(reader); + + const expr = switch (opcode) { + .I32_Const => ConstantExpression{ .Value = TaggedVal{ .type = .I32, .val = .{ .I32 = try common.decodeLEB128(i32, reader) } } }, + .I64_Const => ConstantExpression{ .Value = TaggedVal{ .type = .I64, .val = .{ .I64 = try common.decodeLEB128(i64, reader) } } }, + .F32_Const => ConstantExpression{ .Value = TaggedVal{ .type = .F32, .val = .{ .F32 = try decodeFloat(f32, reader) } } }, + .F64_Const => ConstantExpression{ .Value = TaggedVal{ .type = .F64, .val = .{ .F64 = try decodeFloat(f64, reader) } } }, + .V128_Const => ConstantExpression{ .Value = TaggedVal{ .type = .V128, .val = .{ .V128 = try decodeVec(reader) } } }, + .Ref_Null => ConstantExpression{ .Value = try TaggedVal.nullRef(try ValType.decode(reader)) }, + .Ref_Func => ConstantExpression{ .Value = TaggedVal.funcrefFromIndex(try common.decodeLEB128(u32, reader)) }, + .Global_Get => ConstantExpression{ .Global = try common.decodeLEB128(u32, reader) }, + else => return error.ValidationBadConstantExpression, + }; + + if (opcode == .Global_Get) { + try ModuleValidator.validateGlobalIndex(expr.Global, module_def); + + if (module_def.imports.globals.items.len <= expr.Global) { + return error.ValidationConstantExpressionGlobalMustBeImport; + } + + if (expected_global_mut == .Immutable) { + if (expr.Global < module_def.imports.globals.items.len) { + if (module_def.imports.globals.items[expr.Global].mut != .Immutable) { + return error.ValidationConstantExpressionGlobalMustBeImmutable; + } + } else { + const local_index: usize = module_def.imports.globals.items.len - expr.Global; + if (module_def.globals.items[local_index].mut != .Immutable) { + return error.ValidationConstantExpressionGlobalMustBeImmutable; + } + } + } + + var global_valtype: ValType = undefined; + if (expr.Global < module_def.imports.globals.items.len) { + const global_import_def: *const GlobalImportDefinition = &module_def.imports.globals.items[expr.Global]; + global_valtype = global_import_def.valtype; + } else { + const local_index: usize = module_def.imports.globals.items.len - expr.Global; + const global_def: *const GlobalDefinition = &module_def.globals.items[local_index]; + global_valtype = global_def.valtype; + } + + if (global_valtype != expected_valtype) { + return error.ValidationConstantExpressionTypeMismatch; + } + } else { + if (expr.Value.type != expected_valtype) { + return error.ValidationConstantExpressionTypeMismatch; + } + } + + const end = @as(WasmOpcode, @enumFromInt(try reader.readByte())); + if (end != .End) { + return error.ValidationBadConstantExpression; + } + + return expr; + } + + pub fn resolve(self: *const ConstantExpression, store: *Store) Val { + switch (self.*) { + .Value => |val| { + return val.val; + }, + .Global => |global_index| { + std.debug.assert(global_index < store.imports.globals.items.len + store.globals.items.len); + const global: *GlobalInstance = store.getGlobal(global_index); + return global.value; + }, + } + } + + pub fn resolveTo(self: *const ConstantExpression, store: *Store, comptime T: type) T { + const val: Val = self.resolve(store); + switch (T) { + i32 => return val.I32, + u32 => return @as(u32, @bitCast(val.I32)), + i64 => return val.I64, + u64 => return @as(u64, @bitCast(val.I64)), + f32 => return val.F64, + f64 => return val.F64, + else => unreachable, + } + } + + pub fn resolveType(self: *const ConstantExpression, module_def: *const ModuleDefinition) ValType { + switch (self.*) { + .Value => |val| return val.type, + .Global => |index| { + if (index < module_def.imports.globals.items.len) { + const global_import_def: *const GlobalImportDefinition = &module_def.imports.globals.items[index]; + return global_import_def.valtype; + } else { + const local_index: usize = module_def.imports.globals.items.len - index; + const global_def: *const GlobalDefinition = &module_def.globals.items[local_index]; + return global_def.valtype; + } + unreachable; + }, + } + } +}; + +pub const FunctionTypeDefinition = struct { + types: std.ArrayList(ValType), + num_params: u32, + + pub fn getParams(self: *const FunctionTypeDefinition) []const ValType { + return self.types.items[0..self.num_params]; + } + pub fn getReturns(self: *const FunctionTypeDefinition) []const ValType { + return self.types.items[self.num_params..]; + } + pub fn calcNumReturns(self: *const FunctionTypeDefinition) u32 { + const total: u32 = @as(u32, @intCast(self.types.items.len)); + return total - self.num_params; + } + + pub const SortContext = struct { + const Self = @This(); + + pub fn hash(_: Self, f: *FunctionTypeDefinition) u64 { + var seed: u64 = 0; + if (f.types.items.len > 0) { + seed = std.hash.Murmur2_64.hash(std.mem.sliceAsBytes(f.types.items)); + } + return std.hash.Murmur2_64.hashWithSeed(std.mem.asBytes(&f.num_params), seed); + } + + pub fn eql(_: Self, a: *const FunctionTypeDefinition, b: *const FunctionTypeDefinition) bool { + if (a.num_params != b.num_params or a.types.items.len != b.types.items.len) { + return false; + } + + for (a.types.items, 0..) |typeA, i| { + var typeB = b.types.items[i]; + if (typeA != typeB) { + return false; + } + } + + return true; + } + + fn less(context: Self, a: *FunctionTypeDefinition, b: *FunctionTypeDefinition) bool { + var ord = Self.order(context, a, b); + return ord == std.math.Order.lt; + } + + fn order(context: Self, a: *FunctionTypeDefinition, b: *FunctionTypeDefinition) std.math.Order { + var hashA = Self.hash(context, a); + var hashB = Self.hash(context, b); + + if (hashA < hashB) { + return std.math.Order.lt; + } else if (hashA > hashB) { + return std.math.Order.gt; + } else { + return std.math.Order.eq; + } + } + }; +}; + +pub const FunctionDefinition = struct { + type_index: usize, + instructions_begin: usize, + instructions_end: usize, + continuation: usize, + locals: std.ArrayList(ValType), // TODO use a slice of a large contiguous array instead + + pub fn instructions(func: FunctionDefinition, module_def: ModuleDefinition) []Instruction { + return module_def.code.instructions.items[func.instructions_begin..func.instructions_end]; + } + + pub fn numParamsAndLocals(func: FunctionDefinition, module_def: ModuleDefinition) usize { + const func_type: *const FunctionTypeDefinition = func.typeDefinition(module_def); + const param_types: []const ValType = func_type.getParams(); + return param_types.len + func.locals.items.len; + } + + pub fn typeDefinition(func: FunctionDefinition, module_def: ModuleDefinition) *const FunctionTypeDefinition { + return &module_def.types.items[func.type_index]; + } +}; + +const ExportType = enum(u8) { + Function = 0x00, + Table = 0x01, + Memory = 0x02, + Global = 0x03, +}; + +pub const ExportDefinition = struct { + name: []const u8, + index: u32, +}; + +pub const FunctionExport = struct { + params: []const ValType, + returns: []const ValType, +}; + +pub const FunctionHandleType = enum(u8) { + Export, + Import, +}; + +pub const FunctionHandle = struct { + index: u32, + type: FunctionHandleType, +}; + +pub const GlobalMut = enum(u8) { + Immutable = 0, + Mutable = 1, + + fn decode(reader: anytype) !GlobalMut { + const byte = try reader.readByte(); + const value = std.meta.intToEnum(GlobalMut, byte) catch { + return error.MalformedMutability; + }; + return value; + } +}; + +pub const GlobalDefinition = struct { + valtype: ValType, + mut: GlobalMut, + expr: ConstantExpression, +}; + +pub const GlobalExport = struct { + val: *Val, + valtype: ValType, + mut: GlobalMut, +}; + +pub const TableDefinition = struct { + reftype: ValType, + limits: Limits, +}; + +pub const MemoryDefinition = struct { + limits: Limits, + + pub const k_page_size: usize = 64 * 1024; + pub const k_max_pages: usize = std.math.powi(usize, 2, 16) catch unreachable; +}; + +pub const ElementMode = enum { + Active, + Passive, + Declarative, +}; + +pub const ElementDefinition = struct { + table_index: u32, + mode: ElementMode, + reftype: ValType, + offset: ?ConstantExpression, + elems_value: std.ArrayList(Val), + elems_expr: std.ArrayList(ConstantExpression), +}; + +pub const DataMode = enum { + Active, + Passive, +}; + +pub const DataDefinition = struct { + bytes: std.ArrayList(u8), + memory_index: ?u32, + offset: ?ConstantExpression, + mode: DataMode, + + fn decode(reader: anytype, module_def: *const ModuleDefinition, allocator: std.mem.Allocator) !DataDefinition { + var data_type: u32 = try common.decodeLEB128(u32, reader); + if (data_type > 2) { + return error.MalformedDataType; + } + + var memory_index: ?u32 = null; + if (data_type == 0x00) { + memory_index = 0; + } else if (data_type == 0x02) { + memory_index = try common.decodeLEB128(u32, reader); + } + + var mode = DataMode.Passive; + var offset: ?ConstantExpression = null; + if (data_type == 0x00 or data_type == 0x02) { + mode = DataMode.Active; + offset = try ConstantExpression.decode(reader, module_def, .Immutable, .I32); + } + + var num_bytes = try common.decodeLEB128(u32, reader); + var bytes = std.ArrayList(u8).init(allocator); + try bytes.resize(num_bytes); + var num_read = try reader.read(bytes.items); + if (num_read != num_bytes) { + return error.MalformedUnexpectedEnd; + } + + if (memory_index) |index| { + if (module_def.imports.memories.items.len + module_def.memories.items.len <= index) { + return error.ValidationUnknownMemory; + } + } + + return DataDefinition{ + .bytes = bytes, + .memory_index = memory_index, + .offset = offset, + .mode = mode, + }; + } +}; + +pub const ImportNames = struct { + module_name: []const u8, + import_name: []const u8, +}; + +const FunctionImportDefinition = struct { + names: ImportNames, + type_index: u32, +}; + +const TableImportDefinition = struct { + names: ImportNames, + reftype: ValType, + limits: Limits, +}; + +const MemoryImportDefinition = struct { + names: ImportNames, + limits: Limits, +}; + +const GlobalImportDefinition = struct { + names: ImportNames, + valtype: ValType, + mut: GlobalMut, +}; + +const MemArg = struct { + alignment: u32, + offset: u32, + + fn decode(reader: anytype, comptime bitwidth: u32) !MemArg { + std.debug.assert(bitwidth % 8 == 0); + var memarg = MemArg{ + .alignment = try common.decodeLEB128(u32, reader), + .offset = try common.decodeLEB128(u32, reader), + }; + const bit_alignment = std.math.powi(u32, 2, memarg.alignment) catch return error.ValidationBadAlignment; + if (bit_alignment > bitwidth / 8) { + return error.ValidationBadAlignment; + } + return memarg; + } +}; + +pub const MemoryOffsetAndLaneImmediates = struct { + offset: u32, + laneidx: u8, +}; + +pub const CallIndirectImmediates = struct { + type_index: u32, + table_index: u32, +}; + +pub const BranchTableImmediates = struct { + label_ids: std.ArrayList(u32), // TODO optimize to make less allocations + fallback_id: u32, +}; + +pub const TablePairImmediates = struct { + index_x: u32, + index_y: u32, +}; + +pub const BlockImmediates = struct { + blocktype: BlockTypeValue, + num_returns: u32, + continuation: u32, +}; + +pub const IfImmediates = struct { + blocktype: BlockTypeValue, + num_returns: u32, + else_continuation: u32, + end_continuation: u32, +}; + +const InstructionImmediatesTypes = enum(u8) { + Void, + ValType, + ValueI32, + ValueF32, + ValueI64, + ValueF64, + ValueVec, + Index, + LabelId, + MemoryOffset, + MemoryOffsetAndLane, + Block, + CallIndirect, + TablePair, + If, + VecShuffle16, +}; + +pub const InstructionImmediates = union(InstructionImmediatesTypes) { + Void: void, + ValType: ValType, + ValueI32: i32, + ValueF32: f32, + ValueI64: i64, + ValueF64: f64, + ValueVec: v128, + Index: u32, + LabelId: u32, + MemoryOffset: u32, + MemoryOffsetAndLane: MemoryOffsetAndLaneImmediates, + Block: BlockImmediates, + CallIndirect: CallIndirectImmediates, + TablePair: TablePairImmediates, + If: IfImmediates, + VecShuffle16: [16]u8, +}; + +pub const Instruction = struct { + opcode: Opcode, + immediate: InstructionImmediates, + + fn decode(reader: anytype, module: *ModuleDefinition) !Instruction { + const Helpers = struct { + fn decodeBlockType(_reader: anytype, _module: *const ModuleDefinition) !InstructionImmediates { + var blocktype: BlockTypeValue = undefined; + + const blocktype_raw = try _reader.readByte(); + const valtype_or_err = ValType.bytecodeToValtype(blocktype_raw); + if (std.meta.isError(valtype_or_err)) { + if (blocktype_raw == k_block_type_void_sentinel_byte) { + blocktype = BlockTypeValue{ .Void = {} }; + } else { + _reader.context.pos -= 1; // move the stream backwards 1 byte to reconstruct the integer + var index_33bit = try common.decodeLEB128(i33, _reader); + if (index_33bit < 0) { + return error.MalformedBytecode; + } + var index: u32 = @as(u32, @intCast(index_33bit)); + if (index < _module.types.items.len) { + blocktype = BlockTypeValue{ .TypeIndex = index }; + } else { + return error.ValidationUnknownBlockTypeIndex; + } + } + } else { + var valtype: ValType = valtype_or_err catch unreachable; + blocktype = BlockTypeValue{ .ValType = valtype }; + } + + const num_returns: u32 = @as(u32, @intCast(blocktype.getBlocktypeReturnTypes(_module).len)); + + return InstructionImmediates{ + .Block = BlockImmediates{ + .blocktype = blocktype, + .num_returns = num_returns, + .continuation = std.math.maxInt(u32), // will be set later in the code decode + }, + }; + } + + fn decodeTablePair(_reader: anytype) !InstructionImmediates { + const elem_index = try common.decodeLEB128(u32, _reader); + const table_index = try common.decodeLEB128(u32, _reader); + + return InstructionImmediates{ + .TablePair = TablePairImmediates{ + .index_x = elem_index, + .index_y = table_index, + }, + }; + } + + fn decodeMemoryOffsetAndLane(_reader: anytype, comptime bitwidth: u32) !InstructionImmediates { + const memarg = try MemArg.decode(_reader, bitwidth); + const laneidx = try _reader.readByte(); + return InstructionImmediates{ .MemoryOffsetAndLane = MemoryOffsetAndLaneImmediates{ + .offset = memarg.offset, + .laneidx = laneidx, + } }; + } + }; + + const wasm_op: WasmOpcode = try WasmOpcode.decode(reader); + var opcode: Opcode = wasm_op.toOpcode(); + var immediate = InstructionImmediates{ .Void = {} }; + + switch (opcode) { + .Select_T => { + const num_types = try common.decodeLEB128(u32, reader); + if (num_types != 1) { + return error.ValidationSelectArity; + } + immediate = InstructionImmediates{ .ValType = try ValType.decode(reader) }; + }, + .Local_Get => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; + }, + .Local_Set => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; + }, + .Local_Tee => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; + }, + .Global_Get => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; + }, + .Global_Set => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; + }, + .Table_Get => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; + }, + .Table_Set => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; + }, + .I32_Const => { + immediate = InstructionImmediates{ .ValueI32 = try common.decodeLEB128(i32, reader) }; + }, + .I64_Const => { + immediate = InstructionImmediates{ .ValueI64 = try common.decodeLEB128(i64, reader) }; + }, + .F32_Const => { + immediate = InstructionImmediates{ .ValueF32 = try decodeFloat(f32, reader) }; + }, + .F64_Const => { + immediate = InstructionImmediates{ .ValueF64 = try decodeFloat(f64, reader) }; + }, + .Block => { + immediate = try Helpers.decodeBlockType(reader, module); + }, + .Loop => { + immediate = try Helpers.decodeBlockType(reader, module); + }, + .If => { + const block_immediates: InstructionImmediates = try Helpers.decodeBlockType(reader, module); + immediate = InstructionImmediates{ + .If = IfImmediates{ + .blocktype = block_immediates.Block.blocktype, + .num_returns = block_immediates.Block.num_returns, + .else_continuation = block_immediates.Block.continuation, + .end_continuation = block_immediates.Block.continuation, + }, + }; + }, + .IfNoElse => unreachable, // we convert the If opcode to IfNoElse only after reaching the end of the block, not when decoding the opcode and immediates + .Branch => { + immediate = InstructionImmediates{ .LabelId = try common.decodeLEB128(u32, reader) }; + }, + .Branch_If => { + immediate = InstructionImmediates{ .LabelId = try common.decodeLEB128(u32, reader) }; + }, + .Branch_Table => { + const table_length = try common.decodeLEB128(u32, reader); + + var label_ids = std.ArrayList(u32).init(module.allocator); + try label_ids.ensureTotalCapacity(table_length); + + var index: u32 = 0; + while (index < table_length) : (index += 1) { + var id = try common.decodeLEB128(u32, reader); + label_ids.addOneAssumeCapacity().* = id; + } + var fallback_id = try common.decodeLEB128(u32, reader); + + var branch_table = BranchTableImmediates{ + .label_ids = label_ids, + .fallback_id = fallback_id, + }; + + for (module.code.branch_table.items, 0..) |*item, i| { + if (item.fallback_id == branch_table.fallback_id) { + if (std.mem.eql(u32, item.label_ids.items, branch_table.label_ids.items)) { + immediate = InstructionImmediates{ .Index = @as(u32, @intCast(i)) }; + break; + } + } + } + + if (std.meta.activeTag(immediate) == .Void) { + immediate = InstructionImmediates{ .Index = @as(u32, @intCast(module.code.branch_table.items.len)) }; + try module.code.branch_table.append(branch_table); + } else { + // don't need this anymore since we're reusing the existing one + branch_table.label_ids.deinit(); + } + }, + .Call => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; // function index + }, + .Call_Indirect => { + immediate = InstructionImmediates{ .CallIndirect = .{ + .type_index = try common.decodeLEB128(u32, reader), + .table_index = try common.decodeLEB128(u32, reader), + } }; + }, + .I32_Load => { + var memarg = try MemArg.decode(reader, 32); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I64_Load => { + var memarg = try MemArg.decode(reader, 64); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .F32_Load => { + var memarg = try MemArg.decode(reader, 32); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .F64_Load => { + var memarg = try MemArg.decode(reader, 64); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I32_Load8_S => { + var memarg = try MemArg.decode(reader, 8); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I32_Load8_U => { + var memarg = try MemArg.decode(reader, 8); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I32_Load16_S => { + var memarg = try MemArg.decode(reader, 16); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I32_Load16_U => { + var memarg = try MemArg.decode(reader, 16); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I64_Load8_S => { + var memarg = try MemArg.decode(reader, 8); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I64_Load8_U => { + var memarg = try MemArg.decode(reader, 8); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I64_Load16_S => { + var memarg = try MemArg.decode(reader, 16); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I64_Load16_U => { + var memarg = try MemArg.decode(reader, 16); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I64_Load32_S => { + var memarg = try MemArg.decode(reader, 32); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I64_Load32_U => { + var memarg = try MemArg.decode(reader, 32); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I32_Store => { + var memarg = try MemArg.decode(reader, 32); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I64_Store => { + var memarg = try MemArg.decode(reader, 64); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .F32_Store => { + var memarg = try MemArg.decode(reader, 32); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .F64_Store => { + var memarg = try MemArg.decode(reader, 64); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I32_Store8 => { + var memarg = try MemArg.decode(reader, 8); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I32_Store16 => { + var memarg = try MemArg.decode(reader, 16); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I64_Store8 => { + var memarg = try MemArg.decode(reader, 8); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I64_Store16 => { + var memarg = try MemArg.decode(reader, 16); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I64_Store32 => { + var memarg = try MemArg.decode(reader, 32); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .Memory_Size => { + var reserved = try reader.readByte(); + if (reserved != 0x00) { + return error.MalformedMissingZeroByte; + } + }, + .Memory_Grow => { + var reserved = try reader.readByte(); + if (reserved != 0x00) { + return error.MalformedMissingZeroByte; + } + }, + .Memory_Init => { + try ModuleValidator.validateMemoryIndex(module); + + if (module.data_count == null) { + return error.MalformedMissingDataCountSection; + } + + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; // dataidx + + var reserved = try reader.readByte(); + if (reserved != 0x00) { + return error.MalformedMissingZeroByte; + } + }, + .Ref_Null => { + var valtype = try ValType.decode(reader); + if (valtype.isRefType() == false) { + return error.MalformedBytecode; + } + + immediate = InstructionImmediates{ .ValType = valtype }; + }, + .Ref_Func => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; // funcidx + }, + .Data_Drop => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; // dataidx + }, + .Memory_Copy => { + var reserved = try reader.readByte(); + if (reserved != 0x00) { + return error.MalformedMissingZeroByte; + } + reserved = try reader.readByte(); + if (reserved != 0x00) { + return error.MalformedMissingZeroByte; + } + }, + .Memory_Fill => { + var reserved = try reader.readByte(); + if (reserved != 0x00) { + return error.MalformedMissingZeroByte; + } + }, + .Table_Init => { + immediate = try Helpers.decodeTablePair(reader); + }, + .Elem_Drop => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; // elemidx + }, + .Table_Copy => { + immediate = try Helpers.decodeTablePair(reader); + }, + .Table_Grow => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; // elemidx + }, + .Table_Size => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; // elemidx + }, + .Table_Fill => { + immediate = InstructionImmediates{ .Index = try common.decodeLEB128(u32, reader) }; // elemidx + }, + .V128_Load => { + var memarg = try MemArg.decode(reader, 128); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .V128_Load8x8_S, .V128_Load8x8_U => { + var memarg = try MemArg.decode(reader, 8 * 8); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .V128_Load16x4_S, .V128_Load16x4_U => { + var memarg = try MemArg.decode(reader, 16 * 4); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .V128_Load32x2_S, .V128_Load32x2_U => { + var memarg = try MemArg.decode(reader, 32 * 2); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .V128_Load8_Splat => { + var memarg = try MemArg.decode(reader, 8); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .V128_Load16_Splat => { + var memarg = try MemArg.decode(reader, 16); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .V128_Load32_Splat => { + var memarg = try MemArg.decode(reader, 32); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .V128_Load64_Splat => { + var memarg = try MemArg.decode(reader, 64); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .I8x16_Extract_Lane_S, .I8x16_Extract_Lane_U, .I8x16_Replace_Lane, .I16x8_Extract_Lane_S, .I16x8_Extract_Lane_U, .I16x8_Replace_Lane, .I32x4_Extract_Lane, .I32x4_Replace_Lane, .I64x2_Extract_Lane, .I64x2_Replace_Lane, .F32x4_Extract_Lane, .F32x4_Replace_Lane, .F64x2_Extract_Lane, .F64x2_Replace_Lane => { + immediate = InstructionImmediates{ .Index = try reader.readByte() }; // laneidx + }, + .V128_Store => { + var memarg = try MemArg.decode(reader, 128); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .V128_Const => { + immediate = InstructionImmediates{ .ValueVec = try decodeVec(reader) }; + }, + .I8x16_Shuffle => { + var lane_indices: [16]u8 = undefined; + for (&lane_indices) |*v| { + const laneidx: u8 = try reader.readByte(); + v.* = laneidx; + } + immediate = InstructionImmediates{ .VecShuffle16 = lane_indices }; + }, + .V128_Load8_Lane => { + immediate = try Helpers.decodeMemoryOffsetAndLane(reader, 8); + }, + .V128_Load16_Lane => { + immediate = try Helpers.decodeMemoryOffsetAndLane(reader, 16); + }, + .V128_Load32_Lane => { + immediate = try Helpers.decodeMemoryOffsetAndLane(reader, 32); + }, + .V128_Load64_Lane => { + immediate = try Helpers.decodeMemoryOffsetAndLane(reader, 64); + }, + .V128_Store8_Lane => { + immediate = try Helpers.decodeMemoryOffsetAndLane(reader, 8); + }, + .V128_Store16_Lane => { + immediate = try Helpers.decodeMemoryOffsetAndLane(reader, 16); + }, + .V128_Store32_Lane => { + immediate = try Helpers.decodeMemoryOffsetAndLane(reader, 32); + }, + .V128_Store64_Lane => { + immediate = try Helpers.decodeMemoryOffsetAndLane(reader, 64); + }, + .V128_Load32_Zero => { + var memarg = try MemArg.decode(reader, 128); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + .V128_Load64_Zero => { + var memarg = try MemArg.decode(reader, 128); + immediate = InstructionImmediates{ .MemoryOffset = memarg.offset }; + }, + else => {}, + } + + return .{ + .opcode = opcode, + .immediate = immediate, + }; + } +}; + +const CustomSection = struct { + name: []const u8, + data: std.ArrayList(u8), +}; + +pub const NameCustomSection = struct { + const NameAssoc = struct { + name: []const u8, + func_index: usize, + + fn cmp(_: void, a: NameAssoc, b: NameAssoc) bool { + return a.func_index < b.func_index; + } + + fn order(_: void, a: NameAssoc, b: NameAssoc) std.math.Order { + if (a.func_index < b.func_index) { + return .lt; + } else if (a.func_index > b.func_index) { + return .gt; + } else { + return .eq; + } + } + }; + + // all string slices here are static strings or point into CustomSection.data - no need to free + module_name: []const u8, + function_names: std.ArrayList(NameAssoc), + + // function_index_to_local_names_begin: std.hash_map.AutoHashMap(u32, u32), + // local_names: std.ArrayList([]const u8), + + fn init(allocator: std.mem.Allocator) NameCustomSection { + return NameCustomSection{ + .module_name = "", + .function_names = std.ArrayList(NameAssoc).init(allocator), + }; + } + + fn deinit(self: *NameCustomSection) void { + self.function_names.deinit(); + } + + fn decode(self: *NameCustomSection, module_definition: *const ModuleDefinition, bytes: []const u8) MalformedError!void { + self.decodeInternal(module_definition, bytes) catch |err| { + std.debug.print("NameCustomSection.decode: caught error from internal: {}", .{err}); + return MalformedError.MalformedCustomSection; + }; + } + + fn decodeInternal(self: *NameCustomSection, module_definition: *const ModuleDefinition, bytes: []const u8) !void { + const DecodeHelpers = struct { + fn readName(stream: anytype) ![]const u8 { + var reader = stream.reader(); + const name_length = try common.decodeLEB128(u32, reader); + const name: []const u8 = stream.buffer[stream.pos .. stream.pos + name_length]; + try stream.seekBy(name_length); + return name; + } + }; + + var fixed_buffer_stream = std.io.fixedBufferStream(bytes); + var reader = fixed_buffer_stream.reader(); + + while (try fixed_buffer_stream.getPos() != try fixed_buffer_stream.getEndPos()) { + const section_code = try reader.readByte(); + const section_size = try common.decodeLEB128(u32, reader); + + switch (section_code) { + 0 => { + self.module_name = try DecodeHelpers.readName(&fixed_buffer_stream); + }, + 1 => { + const num_func_names = try common.decodeLEB128(u32, reader); + try self.function_names.ensureTotalCapacity(num_func_names); + + var index: u32 = 0; + while (index < num_func_names) : (index += 1) { + const func_index = try common.decodeLEB128(u32, reader); + const func_name: []const u8 = try DecodeHelpers.readName(&fixed_buffer_stream); + self.function_names.appendAssumeCapacity(NameAssoc{ + .name = func_name, + .func_index = func_index, + }); + } + + std.sort.heap(NameAssoc, self.function_names.items, {}, NameAssoc.cmp); + }, + 2 => { // TODO locals + try fixed_buffer_stream.seekBy(section_size); + }, + else => { + try fixed_buffer_stream.seekBy(section_size); + }, + } + } + + if (self.module_name.len == 0) { + if (module_definition.debug_name.len > 0) { + self.module_name = module_definition.debug_name; + } else { + self.module_name = ""; + } + } + } + + pub fn getModuleName(self: *const NameCustomSection) []const u8 { + return self.module_name; + } + + pub fn findFunctionName(self: *const NameCustomSection, func_index: usize) []const u8 { + if (func_index < self.function_names.items.len) { + if (self.function_names.items[func_index].func_index == func_index) { + return self.function_names.items[func_index].name; + } else { + const temp_nameassoc = NameAssoc{ + .name = "", + .func_index = func_index, + }; + + if (std.sort.binarySearch(NameAssoc, temp_nameassoc, self.function_names.items, {}, NameAssoc.order)) |found_index| { + return self.function_names.items[found_index].name; + } + } + } + return ""; + } + + // fn findFunctionLocalName(self: *NameCustomSection, func_index: u32, local_index: u32) []const u8 { + // return ""; + // } +}; + +const ModuleValidator = struct { + const ControlFrame = struct { + opcode: Opcode, + start_types: []const ValType, + end_types: []const ValType, + types_stack_height: usize, + is_function: bool, + is_unreachable: bool, + }; + + // Note that we use a nullable ValType here to map to the "Unknown" value type as described in the wasm spec + // validation algorithm: https://webassembly.github.io/spec/core/appendix/algorithm.html + type_stack: std.ArrayList(?ValType), + control_stack: std.ArrayList(ControlFrame), + control_types: StableArray(ValType), + + fn init(allocator: std.mem.Allocator) ModuleValidator { + return ModuleValidator{ + .type_stack = std.ArrayList(?ValType).init(allocator), + .control_stack = std.ArrayList(ControlFrame).init(allocator), + .control_types = StableArray(ValType).init(1 * 1024 * 1024), + }; + } + + fn deinit(self: *ModuleValidator) void { + self.type_stack.deinit(); + self.control_stack.deinit(); + self.control_types.deinit(); + } + + fn validateTypeIndex(index: u64, module: *const ModuleDefinition) !void { + if (module.types.items.len <= index) { + return error.ValidationUnknownType; + } + } + + fn validateGlobalIndex(index: u64, module: *const ModuleDefinition) !void { + if (module.imports.globals.items.len + module.globals.items.len <= index) { + return error.ValidationUnknownGlobal; + } + } + + fn validateTableIndex(index: u64, module: *const ModuleDefinition) !void { + if (module.imports.tables.items.len + module.tables.items.len <= index) { + return error.ValidationUnknownTable; + } + } + + fn getTableReftype(module: *const ModuleDefinition, index: u64) !ValType { + if (index < module.imports.tables.items.len) { + return module.imports.tables.items[index].reftype; + } + + const local_index = index - module.imports.tables.items.len; + if (local_index < module.tables.items.len) { + return module.tables.items[local_index].reftype; + } + + return error.ValidationUnknownTable; + } + + fn validateFunctionIndex(index: u64, module: *const ModuleDefinition) !void { + if (module.imports.functions.items.len + module.functions.items.len <= index) { + return error.ValidationUnknownFunction; + } + } + + fn validateMemoryIndex(module: *const ModuleDefinition) !void { + if (module.imports.memories.items.len + module.memories.items.len < 1) { + return error.ValidationUnknownMemory; + } + } + + fn validateElementIndex(index: u64, module: *const ModuleDefinition) !void { + if (module.elements.items.len <= index) { + return error.ValidationUnknownElement; + } + } + + fn validateDataIndex(index: u64, module: *const ModuleDefinition) !void { + if (module.data_count.? <= index) { + return error.ValidationUnknownData; + } + } + + fn beginValidateCode(self: *ModuleValidator, module: *const ModuleDefinition, func: *const FunctionDefinition) !void { + try validateTypeIndex(func.type_index, module); + + const func_type_def: *const FunctionTypeDefinition = &module.types.items[func.type_index]; + + try self.pushControl(Opcode.Call, func_type_def.getParams(), func_type_def.getReturns()); + } + + fn validateCode(self: *ModuleValidator, module: *const ModuleDefinition, func: *const FunctionDefinition, instruction: Instruction) !void { + const Helpers = struct { + fn popReturnTypes(validator: *ModuleValidator, types: []const ValType) !void { + var i = types.len; + while (i > 0) { + i -= 1; + try validator.popType(types[i]); + } + } + + fn enterBlock(validator: *ModuleValidator, module_: *const ModuleDefinition, instruction_: Instruction) !void { + const blocktype: BlockTypeValue = switch (instruction_.immediate) { + .Block => |v| v.blocktype, + .If => |v| v.blocktype, + else => unreachable, + }; + + var start_types: []const ValType = blocktype.getBlocktypeParamTypes(module_); + var end_types: []const ValType = blocktype.getBlocktypeReturnTypes(module_); + try popReturnTypes(validator, start_types); + + try validator.pushControl(instruction_.opcode, start_types, end_types); + } + + fn getLocalValtype(validator: *const ModuleValidator, module_: *const ModuleDefinition, func_: *const FunctionDefinition, locals_index: u64) !ValType { + var i = validator.control_stack.items.len - 1; + while (i >= 0) : (i -= 1) { + const frame: *const ControlFrame = &validator.control_stack.items[i]; + if (frame.is_function) { + const func_type: *const FunctionTypeDefinition = &module_.types.items[func_.type_index]; + if (locals_index < func_type.num_params) { + return func_type.getParams()[locals_index]; + } else { + if (func_.locals.items.len <= locals_index - func_type.num_params) { + return error.ValidationUnknownLocal; + } + return func_.locals.items[locals_index - func_type.num_params]; + } + } + } + unreachable; + } + + const GlobalMutablilityRequirement = enum { + None, + Mutable, + }; + + fn getGlobalValtype(module_: *const ModuleDefinition, global_index: u64, required_mutability: GlobalMutablilityRequirement) !ValType { + if (global_index < module_.imports.globals.items.len) { + const global: *const GlobalImportDefinition = &module_.imports.globals.items[global_index]; + if (required_mutability == .Mutable and global.mut == .Immutable) { + return error.ValidationImmutableGlobal; + } + return global.valtype; + } + + const module_global_index = global_index - module_.imports.globals.items.len; + if (module_global_index < module_.globals.items.len) { + const global: *const GlobalDefinition = &module_.globals.items[module_global_index]; + if (required_mutability == .Mutable and global.mut == .Immutable) { + return error.ValidationImmutableGlobal; + } + return global.valtype; + } + + return error.ValidationUnknownGlobal; + } + + fn vecLaneTypeToValtype(comptime T: type) ValType { + return switch (T) { + i8 => .I32, + u8 => .I32, + i16 => .I32, + u16 => .I32, + i32 => .I32, + i64 => .I64, + f32 => .F32, + f64 => .F64, + else => @compileError("unsupported lane type"), + }; + } + + fn validateNumericUnaryOp(validator: *ModuleValidator, pop_type: ValType, push_type: ValType) !void { + try validator.popType(pop_type); + try validator.pushType(push_type); + } + + fn validateNumericBinaryOp(validator: *ModuleValidator, pop_type: ValType, push_type: ValType) !void { + try validator.popType(pop_type); + try validator.popType(pop_type); + try validator.pushType(push_type); + } + + fn validateLoadOp(validator: *ModuleValidator, module_: *const ModuleDefinition, load_type: ValType) !void { + try validator.popType(.I32); + try validateMemoryIndex(module_); + try validator.pushType(load_type); + } + + fn validateStoreOp(validator: *ModuleValidator, module_: *const ModuleDefinition, store_type: ValType) !void { + try validateMemoryIndex(module_); + try validator.popType(store_type); + try validator.popType(.I32); + } + + fn validateVectorLane(comptime T: type, laneidx: u32) !void { + const vec_type_info = @typeInfo(T).Vector; + if (vec_type_info.len <= laneidx) { + return error.ValidationInvalidLaneIndex; + } + } + + fn validateLoadLaneOp(validator: *ModuleValidator, module_: *const ModuleDefinition, instruction_: Instruction, comptime T: type) !void { + try validateVectorLane(T, instruction_.immediate.MemoryOffsetAndLane.laneidx); + try validator.popType(.V128); + try validator.popType(.I32); + try validateMemoryIndex(module_); + try validator.pushType(.V128); + } + + fn validateStoreLaneOp(validator: *ModuleValidator, module_: *const ModuleDefinition, instruction_: Instruction, comptime T: type) !void { + try validateVectorLane(T, instruction_.immediate.MemoryOffsetAndLane.laneidx); + try validator.popType(.V128); + try validator.popType(.I32); + try validateMemoryIndex(module_); + } + + fn validateVecExtractLane(comptime T: type, validator: *ModuleValidator, instruction_: Instruction) !void { + try validateVectorLane(T, instruction_.immediate.Index); + const lane_valtype = vecLaneTypeToValtype(@typeInfo(T).Vector.child); + try validator.popType(.V128); + try validator.pushType(lane_valtype); + } + + fn validateVecReplaceLane(comptime T: type, validator: *ModuleValidator, instruction_: Instruction) !void { + try validateVectorLane(T, instruction_.immediate.Index); + const lane_valtype = vecLaneTypeToValtype(@typeInfo(T).Vector.child); + try validator.popType(lane_valtype); + try validator.popType(.V128); + try validator.pushType(.V128); + } + + fn getControlTypes(validator: *ModuleValidator, control_index: usize) ![]const ValType { + if (validator.control_stack.items.len <= control_index) { + return error.ValidationUnknownLabel; + } + const stack_index = validator.control_stack.items.len - control_index - 1; + var frame: *ControlFrame = &validator.control_stack.items[stack_index]; + return if (frame.opcode != .Loop) frame.end_types else frame.start_types; + } + + fn markFrameInstructionsUnreachable(validator: *ModuleValidator) !void { + var frame: *ControlFrame = &validator.control_stack.items[validator.control_stack.items.len - 1]; + try validator.type_stack.resize(frame.types_stack_height); + frame.is_unreachable = true; + } + + fn popPushFuncTypes(validator: *ModuleValidator, type_index: usize, module_: *const ModuleDefinition) !void { + const func_type: *const FunctionTypeDefinition = &module_.types.items[type_index]; + + try popReturnTypes(validator, func_type.getParams()); + for (func_type.getReturns()) |valtype| { + try validator.pushType(valtype); + } + } + }; + switch (instruction.opcode) { + .Invalid => unreachable, + .Unreachable => { + try Helpers.markFrameInstructionsUnreachable(self); + }, + .DebugTrap, .Noop => {}, + .Drop => { + _ = try self.popAnyType(); + }, + .Block => { + try Helpers.enterBlock(self, module, instruction); + }, + .Loop => { + try Helpers.enterBlock(self, module, instruction); + }, + .If, .IfNoElse => { + try self.popType(.I32); + try Helpers.enterBlock(self, module, instruction); + }, + .Else => { + const frame: ControlFrame = try self.popControl(); + if (frame.opcode.isIf() == false) { + return error.ValidationIfElseMismatch; + } + try self.pushControl(.Else, frame.start_types, frame.end_types); + }, + .End => { + const frame: ControlFrame = try self.popControl(); + + // if must have matching else block when returns are expected and the params don't match + if (frame.opcode.isIf() and !std.mem.eql(ValType, frame.start_types, frame.end_types)) { + return error.ValidationTypeMismatch; + } + + if (self.control_stack.items.len > 0) { + for (frame.end_types) |valtype| { + try self.pushType(valtype); + } + } + try self.freeControlTypes(&frame); + }, + .Branch => { + const control_index: u64 = instruction.immediate.LabelId; + const block_return_types: []const ValType = try Helpers.getControlTypes(self, control_index); + + try Helpers.popReturnTypes(self, block_return_types); + try Helpers.markFrameInstructionsUnreachable(self); + }, + .Branch_If => { + const control_index: u64 = instruction.immediate.LabelId; + const block_return_types: []const ValType = try Helpers.getControlTypes(self, control_index); + try self.popType(.I32); + + try Helpers.popReturnTypes(self, block_return_types); + for (block_return_types) |valtype| { + try self.pushType(valtype); + } + }, + .Branch_Table => { + var immediates: *const BranchTableImmediates = &module.code.branch_table.items[instruction.immediate.Index]; + + const fallback_block_return_types: []const ValType = try Helpers.getControlTypes(self, immediates.fallback_id); + + try self.popType(.I32); + + for (immediates.label_ids.items) |control_index| { + const block_return_types: []const ValType = try Helpers.getControlTypes(self, control_index); + + if (fallback_block_return_types.len != block_return_types.len) { + return error.ValidationTypeMismatch; + } + + // Seems like the wabt validation code for br_table is implemented by "peeking" at types on the stack + // instead of actually popping/pushing them. This allows certain block type mismatches to be considered + // valid when the current block is marked unreachable. + const frame: *const ControlFrame = &self.control_stack.items[control_index]; + const type_stack: []const ?ValType = self.type_stack.items[frame.types_stack_height..]; + + var i: usize = block_return_types.len; + while (i > 0) : (i -= 1) { + if (!frame.is_unreachable and frame.types_stack_height < type_stack.len) { + if (type_stack[type_stack.len - i] != block_return_types[block_return_types.len - i]) { + return error.ValidationTypeMismatch; + } + } + } + } + + try Helpers.popReturnTypes(self, fallback_block_return_types); + try Helpers.markFrameInstructionsUnreachable(self); + }, + .Return => { + const block_return_types: []const ValType = try Helpers.getControlTypes(self, self.control_stack.items.len - 1); + try Helpers.popReturnTypes(self, block_return_types); + try Helpers.markFrameInstructionsUnreachable(self); + }, + .Call => { + const func_index: u64 = instruction.immediate.Index; + if (module.imports.functions.items.len + module.functions.items.len <= func_index) { + return error.ValidationUnknownFunction; + } + + var type_index: usize = module.getFuncTypeIndex(func_index); + try Helpers.popPushFuncTypes(self, type_index, module); + }, + .Call_Indirect => { + const immediates: CallIndirectImmediates = instruction.immediate.CallIndirect; + + try validateTypeIndex(immediates.type_index, module); + try validateTableIndex(immediates.table_index, module); + + try self.popType(.I32); + + try Helpers.popPushFuncTypes(self, immediates.type_index, module); + }, + .Select => { + try self.popType(.I32); + const valtype1_or_null: ?ValType = try self.popAnyType(); + const valtype2_or_null: ?ValType = try self.popAnyType(); + if (valtype1_or_null == null) { + try self.pushType(valtype2_or_null); + } else if (valtype2_or_null == null) { + try self.pushType(valtype1_or_null); + } else { + const valtype1 = valtype1_or_null.?; + const valtype2 = valtype2_or_null.?; + if (valtype1 != valtype2) { + return error.ValidationTypeMismatch; + } + if (valtype1.isRefType()) { + return error.ValidationTypeMustBeNumeric; + } + try self.pushType(valtype1); + } + }, + .Select_T => { + const valtype: ValType = instruction.immediate.ValType; + try self.popType(.I32); + try self.popType(valtype); + try self.popType(valtype); + try self.pushType(valtype); + }, + .Local_Get => { + const valtype = try Helpers.getLocalValtype(self, module, func, instruction.immediate.Index); + try self.pushType(valtype); + }, + .Local_Set => { + const valtype = try Helpers.getLocalValtype(self, module, func, instruction.immediate.Index); + try self.popType(valtype); + }, + .Local_Tee => { + const valtype = try Helpers.getLocalValtype(self, module, func, instruction.immediate.Index); + try self.popType(valtype); + try self.pushType(valtype); + }, + .Global_Get => { + const valtype = try Helpers.getGlobalValtype(module, instruction.immediate.Index, .None); + try self.pushType(valtype); + }, + .Global_Set => { + const valtype = try Helpers.getGlobalValtype(module, instruction.immediate.Index, .Mutable); + try self.popType(valtype); + }, + .Table_Get => { + const reftype = try getTableReftype(module, instruction.immediate.Index); + try self.popType(.I32); + try self.pushType(reftype); + }, + .Table_Set => { + const reftype = try getTableReftype(module, instruction.immediate.Index); + try self.popType(reftype); + try self.popType(.I32); + }, + .I32_Load, .I32_Load8_S, .I32_Load8_U, .I32_Load16_S, .I32_Load16_U => { + try Helpers.validateLoadOp(self, module, .I32); + }, + .I64_Load, .I64_Load8_S, .I64_Load8_U, .I64_Load16_S, .I64_Load16_U, .I64_Load32_S, .I64_Load32_U => { + try Helpers.validateLoadOp(self, module, .I64); + }, + .F32_Load => { + try Helpers.validateLoadOp(self, module, .F32); + }, + .F64_Load => { + try Helpers.validateLoadOp(self, module, .F64); + }, + .I32_Store, .I32_Store8, .I32_Store16 => { + try Helpers.validateStoreOp(self, module, .I32); + }, + .I64_Store, .I64_Store8, .I64_Store16, .I64_Store32 => { + try Helpers.validateStoreOp(self, module, .I64); + }, + .F32_Store => { + try Helpers.validateStoreOp(self, module, .F32); + }, + .F64_Store => { + try Helpers.validateStoreOp(self, module, .F64); + }, + .Memory_Size => { + try validateMemoryIndex(module); + try self.pushType(.I32); + }, + .Memory_Grow => { + try validateMemoryIndex(module); + try self.popType(.I32); + try self.pushType(.I32); + }, + .I32_Const => { + try self.pushType(.I32); + }, + .I64_Const => { + try self.pushType(.I64); + }, + .F32_Const => { + try self.pushType(.F32); + }, + .F64_Const => { + try self.pushType(.F64); + }, + .I32_Eqz, .I32_Clz, .I32_Ctz, .I32_Popcnt => { + try Helpers.validateNumericUnaryOp(self, .I32, .I32); + }, + .I32_Eq, + .I32_NE, + .I32_LT_S, + .I32_LT_U, + .I32_GT_S, + .I32_GT_U, + .I32_LE_S, + .I32_LE_U, + .I32_GE_S, + .I32_GE_U, + .I32_Add, + .I32_Sub, + .I32_Mul, + .I32_Div_S, + .I32_Div_U, + .I32_Rem_S, + .I32_Rem_U, + .I32_And, + .I32_Or, + .I32_Xor, + .I32_Shl, + .I32_Shr_S, + .I32_Shr_U, + .I32_Rotl, + .I32_Rotr, + => { + try Helpers.validateNumericBinaryOp(self, .I32, .I32); + }, + .I64_Clz, .I64_Ctz, .I64_Popcnt => { + try Helpers.validateNumericUnaryOp(self, .I64, .I64); + }, + .I64_Eqz => { + try Helpers.validateNumericUnaryOp(self, .I64, .I32); + }, + .I64_Eq, .I64_NE, .I64_LT_S, .I64_LT_U, .I64_GT_S, .I64_GT_U, .I64_LE_S, .I64_LE_U, .I64_GE_S, .I64_GE_U => { + try Helpers.validateNumericBinaryOp(self, .I64, .I32); + }, + .I64_Add, + .I64_Sub, + .I64_Mul, + .I64_Div_S, + .I64_Div_U, + .I64_Rem_S, + .I64_Rem_U, + .I64_And, + .I64_Or, + .I64_Xor, + .I64_Shl, + .I64_Shr_S, + .I64_Shr_U, + .I64_Rotl, + .I64_Rotr, + => { + try Helpers.validateNumericBinaryOp(self, .I64, .I64); + }, + .F32_EQ, .F32_NE, .F32_LT, .F32_GT, .F32_LE, .F32_GE => { + try Helpers.validateNumericBinaryOp(self, .F32, .I32); + }, + .F32_Add, .F32_Sub, .F32_Mul, .F32_Div, .F32_Min, .F32_Max, .F32_Copysign => { + try Helpers.validateNumericBinaryOp(self, .F32, .F32); + }, + .F32_Abs, .F32_Neg, .F32_Ceil, .F32_Floor, .F32_Trunc, .F32_Nearest, .F32_Sqrt => { + try Helpers.validateNumericUnaryOp(self, .F32, .F32); + }, + .F64_Abs, .F64_Neg, .F64_Ceil, .F64_Floor, .F64_Trunc, .F64_Nearest, .F64_Sqrt => { + try Helpers.validateNumericUnaryOp(self, .F64, .F64); + }, + .F64_EQ, .F64_NE, .F64_LT, .F64_GT, .F64_LE, .F64_GE => { + try Helpers.validateNumericBinaryOp(self, .F64, .I32); + }, + .F64_Add, .F64_Sub, .F64_Mul, .F64_Div, .F64_Min, .F64_Max, .F64_Copysign => { + try Helpers.validateNumericBinaryOp(self, .F64, .F64); + }, + .I32_Wrap_I64 => { + try Helpers.validateNumericUnaryOp(self, .I64, .I32); + }, + .I32_Trunc_F32_S, .I32_Trunc_F32_U => { + try Helpers.validateNumericUnaryOp(self, .F32, .I32); + }, + .I32_Trunc_F64_S, .I32_Trunc_F64_U => { + try Helpers.validateNumericUnaryOp(self, .F64, .I32); + }, + .I64_Extend_I32_S, .I64_Extend_I32_U => { + try Helpers.validateNumericUnaryOp(self, .I32, .I64); + }, + .I64_Trunc_F32_S, .I64_Trunc_F32_U => { + try Helpers.validateNumericUnaryOp(self, .F32, .I64); + }, + .I64_Trunc_F64_S, .I64_Trunc_F64_U => { + try Helpers.validateNumericUnaryOp(self, .F64, .I64); + }, + .F32_Convert_I32_S, .F32_Convert_I32_U => { + try Helpers.validateNumericUnaryOp(self, .I32, .F32); + }, + .F32_Convert_I64_S, .F32_Convert_I64_U => { + try Helpers.validateNumericUnaryOp(self, .I64, .F32); + }, + .F32_Demote_F64 => { + try Helpers.validateNumericUnaryOp(self, .F64, .F32); + }, + .F64_Convert_I32_S, .F64_Convert_I32_U => { + try Helpers.validateNumericUnaryOp(self, .I32, .F64); + }, + .F64_Convert_I64_S, .F64_Convert_I64_U => { + try Helpers.validateNumericUnaryOp(self, .I64, .F64); + }, + .F64_Promote_F32 => { + try Helpers.validateNumericUnaryOp(self, .F32, .F64); + }, + .I32_Reinterpret_F32 => { + try Helpers.validateNumericUnaryOp(self, .F32, .I32); + }, + .I64_Reinterpret_F64 => { + try Helpers.validateNumericUnaryOp(self, .F64, .I64); + }, + .F32_Reinterpret_I32 => { + try Helpers.validateNumericUnaryOp(self, .I32, .F32); + }, + .F64_Reinterpret_I64 => { + try Helpers.validateNumericUnaryOp(self, .I64, .F64); + }, + .I32_Extend8_S, .I32_Extend16_S => { + try Helpers.validateNumericUnaryOp(self, .I32, .I32); + }, + .I64_Extend8_S, .I64_Extend16_S, .I64_Extend32_S => { + try Helpers.validateNumericUnaryOp(self, .I64, .I64); + }, + .Ref_Null => { + try self.pushType(instruction.immediate.ValType); + }, + .Ref_Is_Null => { + var valtype_or_null: ?ValType = try self.popAnyType(); + if (valtype_or_null) |valtype| { + if (valtype.isRefType() == false) { + return error.ValidationTypeMismatch; + } + } + try self.pushType(.I32); + }, + .Ref_Func => { + const func_index: u32 = instruction.immediate.Index; + try validateFunctionIndex(func_index, module); + + const is_referencing_current_function: bool = module.imports.functions.items.len <= func_index and + &module.functions.items[func_index - module.imports.functions.items.len] == func; + + // references to the current function must be declared in element segments + if (is_referencing_current_function) { + var needs_declaration: bool = true; + skip_outer: for (module.elements.items) |elem_def| { + if (elem_def.mode == .Declarative and elem_def.reftype == .FuncRef) { + if (elem_def.elems_value.items.len > 0) { + for (elem_def.elems_value.items) |val| { + if (val.FuncRef.index == func_index) { + needs_declaration = false; + break :skip_outer; + } + } + } else { + for (elem_def.elems_expr.items) |expr| { + if (std.meta.activeTag(expr) == .Value) { + if (expr.Value.val.FuncRef.index == func_index) { + needs_declaration = false; + break :skip_outer; + } + } + } + } + } + } + + if (needs_declaration) { + return error.ValidationFuncRefUndeclared; + } + } + + try self.pushType(.FuncRef); + }, + .I32_Trunc_Sat_F32_S, .I32_Trunc_Sat_F32_U => { + try Helpers.validateNumericUnaryOp(self, .F32, .I32); + }, + .I32_Trunc_Sat_F64_S, .I32_Trunc_Sat_F64_U => { + try Helpers.validateNumericUnaryOp(self, .F64, .I32); + }, + .I64_Trunc_Sat_F32_S, .I64_Trunc_Sat_F32_U => { + try Helpers.validateNumericUnaryOp(self, .F32, .I64); + }, + .I64_Trunc_Sat_F64_S, .I64_Trunc_Sat_F64_U => { + try Helpers.validateNumericUnaryOp(self, .F64, .I64); + }, + .Memory_Init => { + try validateMemoryIndex(module); + try validateDataIndex(instruction.immediate.Index, module); + try self.popType(.I32); + try self.popType(.I32); + try self.popType(.I32); + }, + .Data_Drop => { + if (module.data_count != null) { + try validateDataIndex(instruction.immediate.Index, module); + } else { + return error.MalformedMissingDataCountSection; + } + }, + .Memory_Copy, .Memory_Fill => { + try validateMemoryIndex(module); + try self.popType(.I32); + try self.popType(.I32); + try self.popType(.I32); + }, + .Table_Init => { + const pair: TablePairImmediates = instruction.immediate.TablePair; + const elem_index = pair.index_x; + const table_index = pair.index_y; + try validateTableIndex(table_index, module); + try validateElementIndex(elem_index, module); + + const elem_reftype: ValType = module.elements.items[elem_index].reftype; + const table_reftype: ValType = module.tables.items[table_index].reftype; + + if (elem_reftype != table_reftype) { + return error.ValidationTypeMismatch; + } + + try self.popType(.I32); + try self.popType(.I32); + try self.popType(.I32); + }, + .Elem_Drop => { + try validateElementIndex(instruction.immediate.Index, module); + }, + .Table_Copy => { + const pair: TablePairImmediates = instruction.immediate.TablePair; + const dest_table_index = pair.index_x; + const src_table_index = pair.index_y; + try validateTableIndex(dest_table_index, module); + try validateTableIndex(src_table_index, module); + + const dest_reftype: ValType = module.tables.items[dest_table_index].reftype; + const src_reftype: ValType = module.tables.items[src_table_index].reftype; + + if (dest_reftype != src_reftype) { + return error.ValidationTypeMismatch; + } + + try self.popType(.I32); + try self.popType(.I32); + try self.popType(.I32); + }, + .Table_Grow => { + try validateTableIndex(instruction.immediate.Index, module); + + try self.popType(.I32); + if (try self.popAnyType()) |init_type| { + var table_reftype: ValType = try getTableReftype(module, instruction.immediate.Index); + if (init_type != table_reftype) { + return error.ValidationTypeMismatch; + } + } + + try self.pushType(.I32); + }, + .Table_Size => { + try validateTableIndex(instruction.immediate.Index, module); + try self.pushType(.I32); + }, + .Table_Fill => { + try validateTableIndex(instruction.immediate.Index, module); + try self.popType(.I32); + if (try self.popAnyType()) |valtype| { + var table_reftype: ValType = try getTableReftype(module, instruction.immediate.Index); + if (valtype != table_reftype) { + return error.ValidationTypeMismatch; + } + } + try self.popType(.I32); + }, + .V128_Load, + .V128_Load8x8_S, + .V128_Load8x8_U, + .V128_Load16x4_S, + .V128_Load16x4_U, + .V128_Load32x2_S, + .V128_Load32x2_U, + .V128_Load8_Splat, + .V128_Load16_Splat, + .V128_Load32_Splat, + .V128_Load64_Splat, + => { + try Helpers.validateLoadOp(self, module, .V128); + }, + .I8x16_Splat, .I16x8_Splat, .I32x4_Splat => { + try Helpers.validateNumericUnaryOp(self, .I32, .V128); + }, + .I64x2_Splat => { + try Helpers.validateNumericUnaryOp(self, .I64, .V128); + }, + .F32x4_Splat => { + try Helpers.validateNumericUnaryOp(self, .F32, .V128); + }, + .F64x2_Splat => { + try Helpers.validateNumericUnaryOp(self, .F64, .V128); + }, + .I8x16_Extract_Lane_S, .I8x16_Extract_Lane_U => { + try Helpers.validateVecExtractLane(i8x16, self, instruction); + }, + .I8x16_Replace_Lane => { + try Helpers.validateVecReplaceLane(i8x16, self, instruction); + }, + .I16x8_Extract_Lane_S, .I16x8_Extract_Lane_U => { + try Helpers.validateVecExtractLane(i16x8, self, instruction); + }, + .I16x8_Replace_Lane => { + try Helpers.validateVecReplaceLane(i16x8, self, instruction); + }, + .I32x4_Extract_Lane => { + try Helpers.validateVecExtractLane(i32x4, self, instruction); + }, + .I32x4_Replace_Lane => { + try Helpers.validateVecReplaceLane(i32x4, self, instruction); + }, + .I64x2_Extract_Lane => { + try Helpers.validateVecExtractLane(i64x2, self, instruction); + }, + .I64x2_Replace_Lane => { + try Helpers.validateVecReplaceLane(i64x2, self, instruction); + }, + .F32x4_Extract_Lane => { + try Helpers.validateVecExtractLane(f32x4, self, instruction); + }, + .F32x4_Replace_Lane => { + try Helpers.validateVecReplaceLane(f32x4, self, instruction); + }, + .F64x2_Extract_Lane => { + try Helpers.validateVecExtractLane(f64x2, self, instruction); + }, + .F64x2_Replace_Lane => { + try Helpers.validateVecReplaceLane(f64x2, self, instruction); + }, + .V128_Store => { + try Helpers.validateStoreOp(self, module, .V128); + }, + .V128_Const => { + try self.pushType(.V128); + }, + .I8x16_Shuffle => { + for (instruction.immediate.VecShuffle16) |v| { + if (v >= 32) { + return ValidationError.ValidationInvalidLaneIndex; + } + } + try Helpers.validateNumericBinaryOp(self, .V128, .V128); + }, + .I8x16_Swizzle => { + try Helpers.validateNumericBinaryOp(self, .V128, .V128); + }, + .V128_Not, + .F32x4_Demote_F64x2_Zero, + .F64x2_Promote_Low_F32x4, + .I8x16_Abs, + .I8x16_Neg, + .I8x16_Popcnt, + .F32x4_Ceil, + .F32x4_Floor, + .F32x4_Trunc, + .F32x4_Nearest, + .F64x2_Ceil, + .F64x2_Floor, + .F64x2_Trunc, + .F64x2_Nearest, + .I16x8_Extadd_Pairwise_I8x16_S, + .I16x8_Extadd_Pairwise_I8x16_U, + .I32x4_Extadd_Pairwise_I16x8_S, + .I32x4_Extadd_Pairwise_I16x8_U, + .I16x8_Abs, + .I16x8_Neg, + .I16x8_Extend_Low_I8x16_S, + .I16x8_Extend_High_I8x16_S, + .I16x8_Extend_Low_I8x16_U, + .I16x8_Extend_High_I8x16_U, + .I32x4_Abs, + .I32x4_Neg, + .I32x4_Extend_Low_I16x8_S, + .I32x4_Extend_High_I16x8_S, + .I32x4_Extend_Low_I16x8_U, + .I32x4_Extend_High_I16x8_U, + .I64x2_Abs, + .I64x2_Neg, + .I64x2_Extend_Low_I32x4_S, + .I64x2_Extend_High_I32x4_S, + .I64x2_Extend_Low_I32x4_U, + .I64x2_Extend_High_I32x4_U, + .F32x4_Abs, + .F32x4_Neg, + .F32x4_Sqrt, + .F64x2_Abs, + .F64x2_Neg, + .F64x2_Sqrt, + .F32x4_Trunc_Sat_F32x4_S, + .F32x4_Trunc_Sat_F32x4_U, + .F32x4_Convert_I32x4_S, + .F32x4_Convert_I32x4_U, + .I32x4_Trunc_Sat_F64x2_S_Zero, + .I32x4_Trunc_Sat_F64x2_U_Zero, + .F64x2_Convert_Low_I32x4_S, + .F64x2_Convert_Low_I32x4_U, + => { + try Helpers.validateNumericUnaryOp(self, .V128, .V128); + }, + .I8x16_EQ, + .I8x16_NE, + .I8x16_LT_S, + .I8x16_LT_U, + .I8x16_GT_S, + .I8x16_GT_U, + .I8x16_LE_S, + .I8x16_LE_U, + .I8x16_GE_S, + .I8x16_GE_U, + .I16x8_EQ, + .I16x8_NE, + .I16x8_LT_S, + .I16x8_LT_U, + .I16x8_GT_S, + .I16x8_GT_U, + .I16x8_LE_S, + .I16x8_LE_U, + .I16x8_GE_S, + .I16x8_GE_U, + .I32x4_EQ, + .I32x4_NE, + .I32x4_LT_S, + .I32x4_LT_U, + .I32x4_GT_S, + .I32x4_GT_U, + .I32x4_LE_S, + .I32x4_LE_U, + .I32x4_GE_S, + .I32x4_GE_U, + .F32x4_EQ, + .F32x4_NE, + .F32x4_LT, + .F32x4_GT, + .F32x4_LE, + .F32x4_GE, + .F64x2_EQ, + .F64x2_NE, + .F64x2_LT, + .F64x2_GT, + .F64x2_LE, + .F64x2_GE, + .I64x2_EQ, + .I64x2_NE, + .I64x2_LT_S, + .I64x2_GT_S, + .I64x2_LE_S, + .I64x2_GE_S, + .I64x2_Extmul_Low_I32x4_S, + .I64x2_Extmul_High_I32x4_S, + .I64x2_Extmul_Low_I32x4_U, + .I64x2_Extmul_High_I32x4_U, + => { + try Helpers.validateNumericBinaryOp(self, .V128, .V128); + }, + .V128_AnyTrue, + .I8x16_AllTrue, + .I8x16_Bitmask, + .I16x8_AllTrue, + .I16x8_Bitmask, + .I32x4_AllTrue, + .I32x4_Bitmask, + .I64x2_AllTrue, + .I64x2_Bitmask, + => { + try Helpers.validateNumericUnaryOp(self, .V128, .I32); + }, + .V128_Load8_Lane => { + try Helpers.validateLoadLaneOp(self, module, instruction, i8x16); + }, + .V128_Load16_Lane => { + try Helpers.validateLoadLaneOp(self, module, instruction, i16x8); + }, + .V128_Load32_Lane => { + try Helpers.validateLoadLaneOp(self, module, instruction, i32x4); + }, + .V128_Load64_Lane => { + try Helpers.validateLoadLaneOp(self, module, instruction, i64x2); + }, + .V128_Store8_Lane => { + try Helpers.validateStoreLaneOp(self, module, instruction, i8x16); + }, + .V128_Store16_Lane => { + try Helpers.validateStoreLaneOp(self, module, instruction, i16x8); + }, + .V128_Store32_Lane => { + try Helpers.validateStoreLaneOp(self, module, instruction, i32x4); + }, + .V128_Store64_Lane => { + try Helpers.validateStoreLaneOp(self, module, instruction, i64x2); + }, + .V128_Load32_Zero => { + try Helpers.validateLoadOp(self, module, .V128); + }, + .V128_Load64_Zero => { + try Helpers.validateLoadOp(self, module, .V128); + }, + .V128_And, + .V128_AndNot, + .V128_Or, + .V128_Xor, + .I8x16_Narrow_I16x8_S, + .I8x16_Narrow_I16x8_U, + .I8x16_Add, + .I8x16_Add_Sat_S, + .I8x16_Add_Sat_U, + .I8x16_Sub, + .I8x16_Sub_Sat_S, + .I8x16_Sub_Sat_U, + .I8x16_Min_S, + .I8x16_Min_U, + .I8x16_Max_S, + .I8x16_Max_U, + .I8x16_Avgr_U, + .I16x8_Narrow_I32x4_S, + .I16x8_Narrow_I32x4_U, + .I16x8_Add, + .I16x8_Add_Sat_S, + .I16x8_Add_Sat_U, + .I16x8_Sub, + .I16x8_Sub_Sat_S, + .I16x8_Sub_Sat_U, + .I16x8_Mul, + .I16x8_Min_S, + .I16x8_Min_U, + .I16x8_Max_S, + .I16x8_Max_U, + .I16x8_Avgr_U, + .I16x8_Q15mulr_Sat_S, + .I16x8_Extmul_Low_I8x16_S, + .I16x8_Extmul_High_I8x16_S, + .I16x8_Extmul_Low_I8x16_U, + .I16x8_Extmul_High_I8x16_U, + .I32x4_Add, + .I32x4_Sub, + .I32x4_Mul, + .I32x4_Min_S, + .I32x4_Min_U, + .I32x4_Max_S, + .I32x4_Max_U, + .I32x4_Dot_I16x8_S, + .I32x4_Extmul_Low_I16x8_S, + .I32x4_Extmul_High_I16x8_S, + .I32x4_Extmul_Low_I16x8_U, + .I32x4_Extmul_High_I16x8_U, + .I64x2_Add, + .I64x2_Sub, + .I64x2_Mul, + .F32x4_Add, + .F32x4_Sub, + .F32x4_Mul, + .F32x4_Div, + .F32x4_Min, + .F32x4_Max, + .F32x4_PMin, + .F32x4_PMax, + .F64x2_Add, + .F64x2_Sub, + .F64x2_Mul, + .F64x2_Div, + .F64x2_Min, + .F64x2_Max, + .F64x2_PMin, + .F64x2_PMax, + => { + try Helpers.validateNumericBinaryOp(self, .V128, .V128); + }, + .I8x16_Shl, + .I8x16_Shr_S, + .I8x16_Shr_U, + .I16x8_Shl, + .I16x8_Shr_S, + .I16x8_Shr_U, + .I32x4_Shl, + .I32x4_Shr_S, + .I32x4_Shr_U, + .I64x2_Shl, + .I64x2_Shr_S, + .I64x2_Shr_U, + => { + try self.popType(.I32); + try self.popType(.V128); + try self.pushType(.V128); + }, + .V128_Bitselect => { + try self.popType(.V128); + try self.popType(.V128); + try self.popType(.V128); + try self.pushType(.V128); + }, + } + } + + fn endValidateCode(self: *ModuleValidator) !void { + try self.type_stack.resize(0); + try self.control_stack.resize(0); + try self.control_types.resize(0); + } + + fn pushType(self: *ModuleValidator, valtype: ?ValType) !void { + try self.type_stack.append(valtype); + } + + fn popAnyType(self: *ModuleValidator) !?ValType { + const top_frame: *const ControlFrame = &self.control_stack.items[self.control_stack.items.len - 1]; + const types: []?ValType = self.type_stack.items; + + if (top_frame.is_unreachable and types.len == top_frame.types_stack_height) { + return null; + } + + if (self.type_stack.items.len <= top_frame.types_stack_height) { + return error.ValidationTypeMismatch; + } + return self.type_stack.pop(); + } + + fn popType(self: *ModuleValidator, expected_or_null: ?ValType) !void { + const valtype_or_null = try self.popAnyType(); + if (valtype_or_null != expected_or_null and valtype_or_null != null and expected_or_null != null) { + return error.ValidationTypeMismatch; + } + } + + fn pushControl(self: *ModuleValidator, opcode: Opcode, start_types: []const ValType, end_types: []const ValType) !void { + const control_types_start_index: usize = self.control_types.items.len; + try self.control_types.appendSlice(start_types); + var control_start_types: []const ValType = self.control_types.items[control_types_start_index..self.control_types.items.len]; + + const control_types_end_index: usize = self.control_types.items.len; + try self.control_types.appendSlice(end_types); + var control_end_types: []const ValType = self.control_types.items[control_types_end_index..self.control_types.items.len]; + + try self.control_stack.append(ControlFrame{ + .opcode = opcode, + .start_types = control_start_types, + .end_types = control_end_types, + .types_stack_height = self.type_stack.items.len, + .is_function = true, + .is_unreachable = false, + }); + + if (opcode != .Call) { + for (start_types) |valtype| { + try self.pushType(valtype); + } + } + } + + fn popControl(self: *ModuleValidator) !ControlFrame { + const frame: *const ControlFrame = &self.control_stack.items[self.control_stack.items.len - 1]; + + var i = frame.end_types.len; + while (i > 0) : (i -= 1) { + if (frame.is_unreachable and self.type_stack.items.len == frame.types_stack_height) { + break; + } + try self.popType(frame.end_types[i - 1]); + } + + if (self.type_stack.items.len != frame.types_stack_height) { + return error.ValidationTypeStackHeightMismatch; + } + + _ = self.control_stack.pop(); + + return frame.*; + } + + fn freeControlTypes(self: *ModuleValidator, frame: *const ControlFrame) !void { + var num_used_types: usize = frame.start_types.len + frame.end_types.len; + try self.control_types.resize(self.control_types.items.len - num_used_types); + } +}; + +pub const ModuleDefinitionOpts = struct { + debug_name: []const u8 = "", +}; + +pub const ModuleDefinition = struct { + const Code = struct { + instructions: std.ArrayList(Instruction), + + wasm_address_to_instruction_index: std.AutoHashMap(u32, u32), + + // Instruction.immediate indexes these arrays depending on the opcode + branch_table: std.ArrayList(BranchTableImmediates), + }; + + const Imports = struct { + functions: std.ArrayList(FunctionImportDefinition), + tables: std.ArrayList(TableImportDefinition), + memories: std.ArrayList(MemoryImportDefinition), + globals: std.ArrayList(GlobalImportDefinition), + }; + + const Exports = struct { + functions: std.ArrayList(ExportDefinition), + tables: std.ArrayList(ExportDefinition), + memories: std.ArrayList(ExportDefinition), + globals: std.ArrayList(ExportDefinition), + }; + + allocator: std.mem.Allocator, + + code: Code, + + types: std.ArrayList(FunctionTypeDefinition), + imports: Imports, + functions: std.ArrayList(FunctionDefinition), + globals: std.ArrayList(GlobalDefinition), + tables: std.ArrayList(TableDefinition), + memories: std.ArrayList(MemoryDefinition), + elements: std.ArrayList(ElementDefinition), + exports: Exports, + datas: std.ArrayList(DataDefinition), + custom_sections: std.ArrayList(CustomSection), + + name_section: NameCustomSection, + + debug_name: []const u8, + start_func_index: ?u32 = null, + data_count: ?u32 = null, + + is_decoded: bool = false, + + pub fn create(allocator: std.mem.Allocator, opts: ModuleDefinitionOpts) AllocError!*ModuleDefinition { + var def = try allocator.create(ModuleDefinition); + def.* = ModuleDefinition{ + .allocator = allocator, + .code = Code{ + .instructions = std.ArrayList(Instruction).init(allocator), + .wasm_address_to_instruction_index = std.AutoHashMap(u32, u32).init(allocator), + .branch_table = std.ArrayList(BranchTableImmediates).init(allocator), + }, + .types = std.ArrayList(FunctionTypeDefinition).init(allocator), + .imports = Imports{ + .functions = std.ArrayList(FunctionImportDefinition).init(allocator), + .tables = std.ArrayList(TableImportDefinition).init(allocator), + .memories = std.ArrayList(MemoryImportDefinition).init(allocator), + .globals = std.ArrayList(GlobalImportDefinition).init(allocator), + }, + .functions = std.ArrayList(FunctionDefinition).init(allocator), + .globals = std.ArrayList(GlobalDefinition).init(allocator), + .tables = std.ArrayList(TableDefinition).init(allocator), + .memories = std.ArrayList(MemoryDefinition).init(allocator), + .elements = std.ArrayList(ElementDefinition).init(allocator), + .exports = Exports{ + .functions = std.ArrayList(ExportDefinition).init(allocator), + .tables = std.ArrayList(ExportDefinition).init(allocator), + .memories = std.ArrayList(ExportDefinition).init(allocator), + .globals = std.ArrayList(ExportDefinition).init(allocator), + }, + .datas = std.ArrayList(DataDefinition).init(allocator), + .custom_sections = std.ArrayList(CustomSection).init(allocator), + .name_section = NameCustomSection.init(allocator), + .debug_name = try allocator.dupe(u8, opts.debug_name), + }; + return def; + } + + pub fn decode(self: *ModuleDefinition, wasm: []const u8) anyerror!void { + std.debug.assert(self.is_decoded == false); + + self.decode_internal(wasm) catch |e| { + var wrapped_error: anyerror = switch (e) { + error.EndOfStream => error.MalformedUnexpectedEnd, + else => e, + }; + return wrapped_error; + }; + } + + fn decode_internal(self: *ModuleDefinition, wasm: []const u8) anyerror!void { + const DecodeHelpers = struct { + fn readRefValue(valtype: ValType, reader: anytype) !Val { + switch (valtype) { + .FuncRef => { + const func_index = try common.decodeLEB128(u32, reader); + return Val.funcrefFromIndex(func_index); + }, + .ExternRef => { + unreachable; // TODO + }, + else => unreachable, + } + } + + // TODO move these names into a string pool + fn readName(reader: anytype, _allocator: std.mem.Allocator) ![]const u8 { + const name_length = try common.decodeLEB128(u32, reader); + + var name: []u8 = try _allocator.alloc(u8, name_length); + errdefer _allocator.free(name); + var read_length = try reader.read(name); + if (read_length != name_length) { + return error.MalformedUnexpectedEnd; + } + + if (std.unicode.utf8ValidateSlice(name) == false) { + return error.MalformedUTF8Encoding; + } + + return name; + } + }; + + var allocator = self.allocator; + var validator = ModuleValidator.init(allocator); + defer validator.deinit(); + + var stream = std.io.fixedBufferStream(wasm); + var reader = stream.reader(); + + // wasm header + { + const magic = try reader.readIntBig(u32); + if (magic != 0x0061736D) { + return error.MalformedMagicSignature; + } + const version = try reader.readIntLittle(u32); + if (version != 1) { + return error.MalformedUnsupportedWasmVersion; + } + } + + var num_functions_parsed: u32 = 0; + + while (stream.pos < stream.buffer.len) { + const section_id: Section = std.meta.intToEnum(Section, try reader.readByte()) catch { + return error.MalformedSectionId; + }; + const section_size_bytes: usize = try common.decodeLEB128(u32, reader); + const section_start_pos = stream.pos; + + switch (section_id) { + .Custom => { + if (section_size_bytes == 0) { + return error.MalformedUnexpectedEnd; + } + + var name = try DecodeHelpers.readName(reader, allocator); + errdefer allocator.free(name); + + var section = CustomSection{ + .name = name, + .data = std.ArrayList(u8).init(allocator), + }; + + const name_length: usize = stream.pos - section_start_pos; + const data_length: usize = section_size_bytes - name_length; + try section.data.resize(data_length); + const data_length_read = try reader.read(section.data.items); + if (data_length != data_length_read) { + return error.MalformedUnexpectedEnd; + } + + try self.custom_sections.append(section); + + if (std.mem.eql(u8, section.name, "name")) { + try self.name_section.decode(self, section.data.items); + } + }, + .FunctionType => { + const num_types = try common.decodeLEB128(u32, reader); + + try self.types.ensureTotalCapacity(num_types); + + var types_index: u32 = 0; + while (types_index < num_types) : (types_index += 1) { + const sentinel = try reader.readByte(); + if (sentinel != k_function_type_sentinel_byte) { + return error.MalformedTypeSentinel; + } + + const num_params = try common.decodeLEB128(u32, reader); + + var func = FunctionTypeDefinition{ .num_params = num_params, .types = std.ArrayList(ValType).init(allocator) }; + errdefer func.types.deinit(); + + var params_left = num_params; + while (params_left > 0) { + params_left -= 1; + + var param_type = try ValType.decode(reader); + try func.types.append(param_type); + } + + const num_returns = try common.decodeLEB128(u32, reader); + var returns_left = num_returns; + while (returns_left > 0) { + returns_left -= 1; + + var return_type = try ValType.decode(reader); + try func.types.append(return_type); + } + + try self.types.append(func); + } + }, + .Import => { + const num_imports = try common.decodeLEB128(u32, reader); + + var import_index: u32 = 0; + while (import_index < num_imports) : (import_index += 1) { + var module_name: []const u8 = try DecodeHelpers.readName(reader, allocator); + errdefer allocator.free(module_name); + + var import_name: []const u8 = try DecodeHelpers.readName(reader, allocator); + errdefer allocator.free(import_name); + + const names = ImportNames{ + .module_name = module_name, + .import_name = import_name, + }; + + const desc = try reader.readByte(); + switch (desc) { + 0x00 => { + const type_index = try common.decodeLEB128(u32, reader); + try ModuleValidator.validateTypeIndex(type_index, self); + try self.imports.functions.append(FunctionImportDefinition{ + .names = names, + .type_index = type_index, + }); + }, + 0x01 => { + const valtype = try ValType.decode(reader); + if (valtype.isRefType() == false) { + return error.MalformedInvalidImport; + } + const limits = try Limits.decode(reader); + try self.imports.tables.append(TableImportDefinition{ + .names = names, + .reftype = valtype, + .limits = limits, + }); + }, + 0x02 => { + const limits = try Limits.decode(reader); + try self.imports.memories.append(MemoryImportDefinition{ + .names = names, + .limits = limits, + }); + }, + 0x03 => { + const valtype = try ValType.decode(reader); + const mut = try GlobalMut.decode(reader); + + try self.imports.globals.append(GlobalImportDefinition{ + .names = names, + .valtype = valtype, + .mut = mut, + }); + }, + else => return error.MalformedInvalidImport, + } + } + }, + .Function => { + const num_funcs = try common.decodeLEB128(u32, reader); + + try self.functions.ensureTotalCapacity(num_funcs); + + var func_index: u32 = 0; + while (func_index < num_funcs) : (func_index += 1) { + var func = FunctionDefinition{ + .type_index = try common.decodeLEB128(u32, reader), + .locals = std.ArrayList(ValType).init(allocator), + + // we'll fix these up later when we find them in the Code section + .instructions_begin = 0, + .instructions_end = 0, + .continuation = 0, + }; + + self.functions.addOneAssumeCapacity().* = func; + } + }, + .Table => { + const num_tables = try common.decodeLEB128(u32, reader); + + try self.tables.ensureTotalCapacity(num_tables); + + var table_index: u32 = 0; + while (table_index < num_tables) : (table_index += 1) { + const valtype = try ValType.decode(reader); + if (valtype.isRefType() == false) { + return error.InvalidTableType; + } + + const limits = try Limits.decode(reader); + + try self.tables.append(TableDefinition{ + .reftype = valtype, + .limits = limits, + }); + } + }, + .Memory => { + const num_memories = try common.decodeLEB128(u32, reader); + + if (num_memories > 1) { + return error.ValidationMultipleMemories; + } + + try self.memories.ensureTotalCapacity(num_memories); + + var memory_index: u32 = 0; + while (memory_index < num_memories) : (memory_index += 1) { + var limits = try Limits.decode(reader); + + if (limits.min > MemoryDefinition.k_max_pages) { + return error.ValidationMemoryMaxPagesExceeded; + } + + if (limits.max) |max| { + if (max < limits.min) { + return error.ValidationMemoryInvalidMaxLimit; + } + if (max > MemoryDefinition.k_max_pages) { + return error.ValidationMemoryMaxPagesExceeded; + } + } + + var def = MemoryDefinition{ + .limits = limits, + }; + try self.memories.append(def); + } + }, + .Global => { + const num_globals = try common.decodeLEB128(u32, reader); + + try self.globals.ensureTotalCapacity(num_globals); + + var global_index: u32 = 0; + while (global_index < num_globals) : (global_index += 1) { + var valtype = try ValType.decode(reader); + var mut = try GlobalMut.decode(reader); + + const expr = try ConstantExpression.decode(reader, self, .Immutable, valtype); + + if (std.meta.activeTag(expr) == .Value) { + if (expr.Value.type == .FuncRef) { + if (expr.Value.val.isNull() == false) { + const index: u32 = expr.Value.val.FuncRef.index; + try ModuleValidator.validateFunctionIndex(index, self); + } + } + } + + try self.globals.append(GlobalDefinition{ + .valtype = valtype, + .expr = expr, + .mut = mut, + }); + } + }, + .Export => { + const num_exports = try common.decodeLEB128(u32, reader); + + var export_names = std.StringHashMap(void).init(allocator); + defer export_names.deinit(); + + var export_index: u32 = 0; + while (export_index < num_exports) : (export_index += 1) { + var name: []const u8 = try DecodeHelpers.readName(reader, allocator); + errdefer allocator.free(name); + + { + var getOrPutResult = try export_names.getOrPut(name); + if (getOrPutResult.found_existing == true) { + return error.ValidationDuplicateExportName; + } + } + + const exportType = @as(ExportType, @enumFromInt(try reader.readByte())); + const item_index = try common.decodeLEB128(u32, reader); + const def = ExportDefinition{ .name = name, .index = item_index }; + + switch (exportType) { + .Function => { + try ModuleValidator.validateFunctionIndex(item_index, self); + try self.exports.functions.append(def); + }, + .Table => { + try ModuleValidator.validateTableIndex(item_index, self); + try self.exports.tables.append(def); + }, + .Memory => { + if (self.imports.memories.items.len + self.memories.items.len <= item_index) { + return error.ValidationUnknownMemory; + } + try self.exports.memories.append(def); + }, + .Global => { + try ModuleValidator.validateGlobalIndex(item_index, self); + try self.exports.globals.append(def); + }, + } + } + }, + .Start => { + if (self.start_func_index != null) { + return error.MalformedMultipleStartSections; + } + + self.start_func_index = try common.decodeLEB128(u32, reader); + + if (self.imports.functions.items.len + self.functions.items.len <= self.start_func_index.?) { + return error.ValidationUnknownFunction; + } + + var func_type_index: usize = undefined; + if (self.start_func_index.? < self.imports.functions.items.len) { + func_type_index = self.imports.functions.items[self.start_func_index.?].type_index; + } else { + var local_func_index = self.start_func_index.? - self.imports.functions.items.len; + func_type_index = self.functions.items[local_func_index].type_index; + } + + const func_type: *const FunctionTypeDefinition = &self.types.items[func_type_index]; + if (func_type.types.items.len > 0) { + return error.ValidationStartFunctionType; + } + }, + .Element => { + const ElementHelpers = struct { + fn readOffsetExpr(_reader: anytype, _module: *const ModuleDefinition) !ConstantExpression { + var expr = try ConstantExpression.decode(_reader, _module, .Immutable, .I32); + return expr; + } + + fn readElemsVal(elems: *std.ArrayList(Val), valtype: ValType, _reader: anytype, _module: *const ModuleDefinition) !void { + const num_elems = try common.decodeLEB128(u32, _reader); + try elems.ensureTotalCapacity(num_elems); + + var elem_index: u32 = 0; + while (elem_index < num_elems) : (elem_index += 1) { + const ref: Val = try DecodeHelpers.readRefValue(valtype, _reader); + if (valtype == .FuncRef) { + try ModuleValidator.validateFunctionIndex(ref.FuncRef.index, _module); + } + try elems.append(ref); + } + } + + fn readElemsExpr(elems: *std.ArrayList(ConstantExpression), _reader: anytype, _module: *const ModuleDefinition, expected_reftype: ValType) !void { + const num_elems = try common.decodeLEB128(u32, _reader); + try elems.ensureTotalCapacity(num_elems); + + var elem_index: u32 = 0; + while (elem_index < num_elems) : (elem_index += 1) { + var expr = try ConstantExpression.decode(_reader, _module, .Any, expected_reftype); + try elems.append(expr); + } + } + + fn readNullElemkind(_reader: anytype) !void { + var null_elemkind = try _reader.readByte(); + if (null_elemkind != 0x00) { + return error.MalformedBytecode; + } + } + }; + + const num_segments = try common.decodeLEB128(u32, reader); + + try self.elements.ensureTotalCapacity(num_segments); + + var segment_index: u32 = 0; + while (segment_index < num_segments) : (segment_index += 1) { + var flags = try common.decodeLEB128(u32, reader); + + var def = ElementDefinition{ + .mode = ElementMode.Active, + .reftype = ValType.FuncRef, + .table_index = 0, + .offset = null, + .elems_value = std.ArrayList(Val).init(allocator), + .elems_expr = std.ArrayList(ConstantExpression).init(allocator), + }; + errdefer def.elems_value.deinit(); + errdefer def.elems_expr.deinit(); + + switch (flags) { + 0x00 => { + def.offset = try ElementHelpers.readOffsetExpr(reader, self); + try ElementHelpers.readElemsVal(&def.elems_value, def.reftype, reader, self); + }, + 0x01 => { + def.mode = .Passive; + try ElementHelpers.readNullElemkind(reader); + try ElementHelpers.readElemsVal(&def.elems_value, def.reftype, reader, self); + }, + 0x02 => { + def.table_index = try common.decodeLEB128(u32, reader); + def.offset = try ElementHelpers.readOffsetExpr(reader, self); + try ElementHelpers.readNullElemkind(reader); + try ElementHelpers.readElemsVal(&def.elems_value, def.reftype, reader, self); + }, + 0x03 => { + def.mode = .Declarative; + try ElementHelpers.readNullElemkind(reader); + try ElementHelpers.readElemsVal(&def.elems_value, def.reftype, reader, self); + }, + 0x04 => { + def.offset = try ElementHelpers.readOffsetExpr(reader, self); + try ElementHelpers.readElemsExpr(&def.elems_expr, reader, self, def.reftype); + }, + 0x05 => { + def.mode = .Passive; + def.reftype = try ValType.decodeReftype(reader); + try ElementHelpers.readElemsExpr(&def.elems_expr, reader, self, def.reftype); + }, + 0x06 => { + def.table_index = try common.decodeLEB128(u32, reader); + def.offset = try ElementHelpers.readOffsetExpr(reader, self); + def.reftype = try ValType.decodeReftype(reader); + try ElementHelpers.readElemsExpr(&def.elems_expr, reader, self, def.reftype); + }, + 0x07 => { + def.mode = .Declarative; + def.reftype = try ValType.decodeReftype(reader); + try ElementHelpers.readElemsExpr(&def.elems_expr, reader, self, def.reftype); + }, + else => { + return error.MalformedElementType; + }, + } + + try self.elements.append(def); + } + }, + .Code => { + const BlockData = struct { + begin_index: u32, + opcode: Opcode, + }; + var block_stack = std.ArrayList(BlockData).init(allocator); + defer block_stack.deinit(); + + var if_to_else_offsets = std.AutoHashMap(u32, u32).init(allocator); + defer if_to_else_offsets.deinit(); + + var instructions = &self.code.instructions; + + const num_codes = try common.decodeLEB128(u32, reader); + + if (num_codes != self.functions.items.len) { + return error.MalformedFunctionCodeSectionMismatch; + } + + const wasm_code_address_begin: usize = stream.pos; + + var code_index: u32 = 0; + while (code_index < num_codes) { + const code_size = try common.decodeLEB128(u32, reader); + const code_begin_pos = stream.pos; + + var func_def: *FunctionDefinition = &self.functions.items[code_index]; + + // parse locals + { + const num_locals = try common.decodeLEB128(u32, reader); + + const TypeCount = struct { + valtype: ValType, + count: u32, + }; + var local_types = std.ArrayList(TypeCount).init(allocator); + defer local_types.deinit(); + try local_types.ensureTotalCapacity(num_locals); + + var locals_total: usize = 0; + var locals_index: u32 = 0; + try func_def.locals.ensureTotalCapacity(num_locals); + while (locals_index < num_locals) : (locals_index += 1) { + const n = try common.decodeLEB128(u32, reader); + const local_type = try ValType.decode(reader); + + locals_total += n; + if (locals_total >= std.math.maxInt(u32)) { + return error.MalformedTooManyLocals; + } + local_types.appendAssumeCapacity(TypeCount{ .valtype = local_type, .count = n }); + } + + try func_def.locals.ensureTotalCapacity(locals_total); + + for (local_types.items) |type_count| { + func_def.locals.appendNTimesAssumeCapacity(type_count.valtype, type_count.count); + } + } + + func_def.instructions_begin = @intCast(instructions.items.len); + try block_stack.append(BlockData{ + .begin_index = @intCast(func_def.instructions_begin), + .opcode = .Block, + }); + + try validator.beginValidateCode(self, func_def); + + var parsing_code = true; + while (parsing_code) { + const instruction_index = @as(u32, @intCast(instructions.items.len)); + + const wasm_instruction_address = stream.pos - wasm_code_address_begin; + + var instruction: Instruction = try Instruction.decode(reader, self); + + if (instruction.opcode.beginsBlock()) { + try block_stack.append(BlockData{ + .begin_index = instruction_index, + .opcode = instruction.opcode, + }); + } else if (instruction.opcode == .Else) { + const block: *const BlockData = &block_stack.items[block_stack.items.len - 1]; + try if_to_else_offsets.putNoClobber(block.begin_index, instruction_index); + // the else gets the matching if's immediates + instruction.immediate = instructions.items[block.begin_index].immediate; + // and the if will have its else_continuation updated when .End is parsed + } else if (instruction.opcode == .End) { + const block: BlockData = block_stack.orderedRemove(block_stack.items.len - 1); + if (block_stack.items.len == 0) { + parsing_code = false; + + func_def.continuation = instruction_index; + + block_stack.clearRetainingCapacity(); + + num_functions_parsed += 1; + } else { + var block_instruction: *Instruction = &instructions.items[block.begin_index]; + + // fixup the block continuations in previously-emitted Instructions + if (block.opcode == .Loop) { + block_instruction.immediate.Block.continuation = block.begin_index; + } else { + switch (block_instruction.immediate) { + .Block => |*v| v.continuation = instruction_index, + .If => |*v| { + v.end_continuation = instruction_index; + v.else_continuation = instruction_index; + }, + else => unreachable, + } + + var else_index_or_null = if_to_else_offsets.get(block.begin_index); + if (else_index_or_null) |index| { + var else_instruction: *Instruction = &instructions.items[index]; + else_instruction.immediate = block_instruction.immediate; + block_instruction.immediate.If.else_continuation = index; + } else if (block_instruction.opcode == .If) { + block_instruction.opcode = .IfNoElse; + } + } + } + } + + try validator.validateCode(self, func_def, instruction); + + try self.code.wasm_address_to_instruction_index.put(@as(u32, @intCast(wasm_instruction_address)), instruction_index); + + switch (instruction.opcode) { + .Noop => {}, // no need to emit noops since they don't do anything + else => { + try instructions.append(instruction); + }, + } + } + + try validator.endValidateCode(); + + func_def.instructions_end = @intCast(instructions.items.len); + + const code_actual_size = stream.pos - code_begin_pos; + if (code_actual_size != code_size) { + return error.MalformedSectionSizeMismatch; + } + + code_index += 1; + } + + // TODO flatten all instructions into a binary stream + }, + .Data => { + const num_datas = try common.decodeLEB128(u32, reader); + + if (self.data_count != null and num_datas != self.data_count.?) { + return error.MalformedDataCountMismatch; + } + + var data_index: u32 = 0; + while (data_index < num_datas) : (data_index += 1) { + var data = try DataDefinition.decode(reader, self, allocator); + try self.datas.append(data); + } + }, + .DataCount => { + self.data_count = try common.decodeLEB128(u32, reader); + try self.datas.ensureTotalCapacity(self.data_count.?); + }, + } + + var consumed_bytes = stream.pos - section_start_pos; + if (section_size_bytes != consumed_bytes) { + return error.MalformedSectionSizeMismatch; + } + } + + for (self.elements.items) |elem_def| { + if (elem_def.mode == .Active) { + const valtype = try ModuleValidator.getTableReftype(self, elem_def.table_index); + if (elem_def.reftype != valtype) { + return error.ValidationTypeMismatch; + } + } + } + + if (self.imports.memories.items.len + self.memories.items.len > 1) { + return error.ValidationMultipleMemories; + } + + if (num_functions_parsed != self.functions.items.len) { + return error.MalformedFunctionCodeSectionMismatch; + } + } + + pub fn destroy(self: *ModuleDefinition) void { + self.code.instructions.deinit(); + self.code.wasm_address_to_instruction_index.deinit(); + for (self.code.branch_table.items) |*item| { + item.label_ids.deinit(); + } + self.code.branch_table.deinit(); + + for (self.imports.functions.items) |*item| { + self.allocator.free(item.names.module_name); + self.allocator.free(item.names.import_name); + } + for (self.imports.tables.items) |*item| { + self.allocator.free(item.names.module_name); + self.allocator.free(item.names.import_name); + } + for (self.imports.memories.items) |*item| { + self.allocator.free(item.names.module_name); + self.allocator.free(item.names.import_name); + } + for (self.imports.globals.items) |*item| { + self.allocator.free(item.names.module_name); + self.allocator.free(item.names.import_name); + } + + for (self.exports.functions.items) |*item| { + self.allocator.free(item.name); + } + for (self.exports.tables.items) |*item| { + self.allocator.free(item.name); + } + for (self.exports.memories.items) |*item| { + self.allocator.free(item.name); + } + for (self.exports.globals.items) |*item| { + self.allocator.free(item.name); + } + + for (self.types.items) |*item| { + item.types.deinit(); + } + for (self.functions.items) |*item| { + item.locals.deinit(); + } + for (self.elements.items) |*item| { + item.elems_value.deinit(); + item.elems_expr.deinit(); + } + + self.types.deinit(); + self.imports.functions.deinit(); + self.imports.tables.deinit(); + self.imports.memories.deinit(); + self.imports.globals.deinit(); + self.functions.deinit(); + self.globals.deinit(); + self.tables.deinit(); + self.memories.deinit(); + self.elements.deinit(); + self.exports.functions.deinit(); + self.exports.tables.deinit(); + self.exports.memories.deinit(); + self.exports.globals.deinit(); + self.datas.deinit(); + self.name_section.deinit(); + + for (self.custom_sections.items) |*item| { + self.allocator.free(item.name); + item.data.deinit(); + } + self.custom_sections.deinit(); + + self.allocator.free(self.debug_name); + + var allocator = self.allocator; + allocator.destroy(self); + } + + pub fn getCustomSection(self: *const ModuleDefinition, name: []const u8) ?[]u8 { + for (self.custom_sections.items) |section| { + if (std.mem.eql(u8, section.name, name)) { + return section.data.items; + } + } + + return null; + } + + pub fn getFunctionExport(self: *const ModuleDefinition, func_handle: FunctionHandle) FunctionExport { + const type_index = switch (func_handle.type) { + .Export => self.functions.items[func_handle.index].type_index, + .Import => self.imports.functions.items[func_handle.index].type_index, + }; + + const type_def: *const FunctionTypeDefinition = &self.types.items[type_index]; + var params: []const ValType = type_def.getParams(); + var returns: []const ValType = type_def.getReturns(); + + return FunctionExport{ + .params = params, + .returns = returns, + }; + } + + pub fn dump(self: *const ModuleDefinition, writer: anytype) !void { + const Helpers = struct { + fn function(_writer: anytype, functype: *const FunctionTypeDefinition) !void { + const params: []const ValType = functype.getParams(); + const returns: []const ValType = functype.getReturns(); + + try _writer.print("(", .{}); + for (params, 0..) |v, i| { + try _writer.print("{s}", .{valtype(v)}); + if (i != params.len - 1) { + try _writer.print(", ", .{}); + } + } + try _writer.print(") -> ", .{}); + + if (returns.len == 0) { + try _writer.print("void", .{}); + } else { + for (returns, 0..) |v, i| { + try _writer.print("{s}", .{valtype(v)}); + if (i != returns.len - 1) { + try _writer.print(", ", .{}); + } + } + } + + try _writer.print("\n", .{}); + } + + fn limits(_writer: anytype, l: *const Limits) !void { + try _writer.print("limits (min {}, max {?})\n", .{ l.min, l.max }); + } + + fn valtype(v: ValType) []const u8 { + return switch (v) { + .I32 => "i32", + .I64 => "i64", + .F32 => "f32", + .F64 => "f64", + .V128 => "v128", + .FuncRef => "funcref", + .ExternRef => "externref", + }; + } + + fn mut(m: GlobalMut) []const u8 { + return switch (m) { + .Immutable => "immutable", + .Mutable => "mutable", + }; + } + }; + + try writer.print("Imports:\n", .{}); + + try writer.print("\tFunctions: {}\n", .{self.imports.functions.items.len}); + for (self.imports.functions.items) |*import| { + try writer.print("\t\t{s}.{s}", .{ import.names.module_name, import.names.import_name }); + try Helpers.function(writer, &self.types.items[import.type_index]); + } + + try writer.print("\tGlobals: {}\n", .{self.imports.globals.items.len}); + for (self.imports.globals.items) |import| { + try writer.print("\t\t{s}.{s}: type {s}, mut: {s}\n", .{ + import.names.module_name, + import.names.import_name, + Helpers.valtype(import.valtype), + Helpers.mut(import.mut), + }); + } + + try writer.print("\tMemories: {}\n", .{self.imports.memories.items.len}); + for (self.imports.memories.items) |import| { + try writer.print("\t\t{s}.{s}: ", .{ import.names.module_name, import.names.import_name }); + try Helpers.limits(writer, &import.limits); + } + + try writer.print("\tTables: {}\n", .{self.imports.tables.items.len}); + for (self.imports.tables.items) |import| { + try writer.print("\t\t{s}.{s}: type {s}, ", .{ + import.names.module_name, + import.names.import_name, + Helpers.valtype(import.reftype), + }); + try Helpers.limits(writer, &import.limits); + } + + try writer.print("Exports:\n", .{}); + + try writer.print("\tFunctions: {}\n", .{self.exports.functions.items.len}); + for (self.exports.functions.items) |*ex| { + try writer.print("\t\t{s}", .{ex.name}); + const func_type: *const FunctionTypeDefinition = &self.types.items[self.getFuncTypeIndex(ex.index)]; + try Helpers.function(writer, func_type); + } + + try writer.print("\tGlobal: {}\n", .{self.exports.globals.items.len}); + for (self.exports.globals.items) |*ex| { + var valtype: ValType = undefined; + var mut: GlobalMut = undefined; + if (ex.index < self.imports.globals.items.len) { + valtype = self.imports.globals.items[ex.index].valtype; + mut = self.imports.globals.items[ex.index].mut; + } else { + const instance_index: usize = ex.index - self.imports.globals.items.len; + valtype = self.globals.items[instance_index].valtype; + mut = self.globals.items[instance_index].mut; + } + try writer.print("\t\t{s}: type {s}, mut: {s}\n", .{ ex.name, Helpers.valtype(valtype), Helpers.mut(mut) }); + } + + try writer.print("\tMemories: {}\n", .{self.exports.memories.items.len}); + for (self.exports.memories.items) |*ex| { + var limits: *const Limits = undefined; + if (ex.index < self.imports.memories.items.len) { + limits = &self.imports.memories.items[ex.index].limits; + } else { + const instance_index: usize = ex.index - self.imports.memories.items.len; + limits = &self.memories.items[instance_index].limits; + } + try writer.print("\t\t{s}: ", .{ex.name}); + try Helpers.limits(writer, limits); + } + + try writer.print("\tTables: {}\n", .{self.exports.tables.items.len}); + for (self.exports.tables.items) |*ex| { + var reftype: ValType = undefined; + var limits: *const Limits = undefined; + if (ex.index < self.imports.tables.items.len) { + reftype = self.imports.tables.items[ex.index].reftype; + limits = &self.imports.tables.items[ex.index].limits; + } else { + const instance_index: usize = ex.index - self.imports.tables.items.len; + reftype = self.tables.items[instance_index].reftype; + limits = &self.tables.items[instance_index].limits; + } + try writer.print("\t\t{s}: type {s}, ", .{ ex.name, Helpers.valtype(reftype) }); + try Helpers.limits(writer, limits); + } + } + + fn getFuncTypeIndex(self: *const ModuleDefinition, func_index: usize) usize { + if (func_index < self.imports.functions.items.len) { + const func_def: *const FunctionImportDefinition = &self.imports.functions.items[func_index]; + return func_def.type_index; + } else { + const module_func_index = func_index - self.imports.functions.items.len; + const func_def: *const FunctionDefinition = &self.functions.items[module_func_index]; + return func_def.type_index; + } + } +}; diff --git a/src/ext/bytebox/src/instance.zig b/src/ext/bytebox/src/instance.zig new file mode 100644 index 00000000..30907f27 --- /dev/null +++ b/src/ext/bytebox/src/instance.zig @@ -0,0 +1,1234 @@ +const std = @import("std"); +const AllocError = std.mem.Allocator.Error; + +const builtin = @import("builtin"); + +const common = @import("common.zig"); +const StableArray = common.StableArray; + +const opcodes = @import("opcode.zig"); +const Opcode = opcodes.Opcode; +const WasmOpcode = opcodes.WasmOpcode; + +const def = @import("definition.zig"); +const ConstantExpression = def.ConstantExpression; +const FunctionDefinition = def.FunctionDefinition; +const FunctionExport = def.FunctionExport; +const FunctionHandle = def.FunctionHandle; +const FunctionHandleType = def.FunctionHandleType; +const FunctionTypeDefinition = def.FunctionTypeDefinition; +const GlobalDefinition = def.GlobalDefinition; +const GlobalMut = def.GlobalMut; +const ImportNames = def.ImportNames; +const Limits = def.Limits; +const MemoryDefinition = def.MemoryDefinition; +const ModuleDefinition = def.ModuleDefinition; +const NameCustomSection = def.NameCustomSection; +const Val = def.Val; +const ValType = def.ValType; +const GlobalExport = def.GlobalExport; + +pub const UnlinkableError = error{ + UnlinkableUnknownImport, + UnlinkableIncompatibleImportType, +}; + +pub const UninstantiableError = error{ + UninstantiableOutOfBoundsTableAccess, + UninstantiableOutOfBoundsMemoryAccess, +}; + +pub const ExportError = error{ + ExportUnknownFunction, + ExportUnknownGlobal, +}; + +pub const TrapError = error{ + TrapDebug, + TrapUnreachable, + TrapIntegerDivisionByZero, + TrapIntegerOverflow, + TrapIndirectCallTypeMismatch, + TrapInvalidIntegerConversion, + TrapOutOfBoundsMemoryAccess, + TrapUndefinedElement, + TrapUninitializedElement, + TrapOutOfBoundsTableAccess, + TrapStackExhausted, + TrapUnknown, +}; + +pub const DebugTrace = struct { + pub const Mode = enum { + None, + Function, + Instruction, + }; + + pub fn setMode(new_mode: Mode) bool { + if (builtin.mode == .Debug) { + mode = new_mode; + return true; + } + + return false; + } + + pub fn shouldTraceFunctions() bool { + return builtin.mode == .Debug and mode == .Function; + } + + pub fn shouldTraceInstructions() bool { + return builtin.mode == .Debug and mode == .Instruction; + } + + pub fn printIndent(indent: u32) void { + var indent_level: u32 = 0; + while (indent_level < indent) : (indent_level += 1) { + std.debug.print(" ", .{}); + } + } + + pub fn traceHostFunction(module_instance: *const ModuleInstance, indent: u32, import_name: []const u8) void { + if (shouldTraceFunctions()) { + _ = module_instance; + const module_name = ""; + + printIndent(indent); + std.debug.print("{s}!{s}\n", .{ module_name, import_name }); + } + } + + pub fn traceFunction(module_instance: *const ModuleInstance, indent: u32, func_index: usize) void { + if (shouldTraceFunctions()) { + const func_name_index: usize = func_index + module_instance.module_def.imports.functions.items.len; + + const name_section: *const NameCustomSection = &module_instance.module_def.name_section; + const module_name = name_section.getModuleName(); + const function_name = name_section.findFunctionName(func_name_index); + + printIndent(indent); + std.debug.print("{s}!{s}\n", .{ module_name, function_name }); + } + } + + var mode: Mode = .None; +}; + +pub const GlobalInstance = struct { + def: *GlobalDefinition, + value: Val, +}; + +pub const TableInstance = struct { + refs: std.ArrayList(Val), // should only be reftypes + reftype: ValType, + limits: Limits, + + pub fn init(reftype: ValType, limits: Limits, allocator: std.mem.Allocator) !TableInstance { + std.debug.assert(reftype.isRefType()); + + var table = TableInstance{ + .refs = std.ArrayList(Val).init(allocator), + .reftype = reftype, + .limits = limits, + }; + + if (limits.min > 0) { + try table.refs.appendNTimes(try Val.nullRef(reftype), limits.min); + } + return table; + } + + pub fn deinit(table: *TableInstance) void { + table.refs.deinit(); + } + + pub fn grow(table: *TableInstance, length: usize, init_value: Val) bool { + const max = if (table.limits.max) |m| m else std.math.maxInt(i32); + std.debug.assert(table.refs.items.len == table.limits.min); + + var old_length: usize = table.limits.min; + if (old_length + length > max) { + return false; + } + + table.limits.min = @as(u32, @intCast(old_length + length)); + + table.refs.appendNTimes(init_value, length) catch return false; + return true; + } + + fn init_range_val(table: *TableInstance, module: *ModuleInstance, elems: []const Val, init_length: u32, start_elem_index: u32, start_table_index: u32) !void { + if (table.refs.items.len < start_table_index + init_length) { + return error.TrapOutOfBoundsTableAccess; + } + + if (elems.len < start_elem_index + init_length) { + return error.TrapOutOfBoundsTableAccess; + } + + var elem_range = elems[start_elem_index .. start_elem_index + init_length]; + var table_range = table.refs.items[start_table_index .. start_table_index + init_length]; + + var index: u32 = 0; + while (index < elem_range.len) : (index += 1) { + var val: Val = elem_range[index]; + + if (table.reftype == .FuncRef) { + val.FuncRef.module_instance = module; + } + + table_range[index] = val; + } + } + + fn init_range_expr(table: *TableInstance, module: *ModuleInstance, elems: []const ConstantExpression, init_length: u32, start_elem_index: u32, start_table_index: u32, store: *Store) !void { + if (start_table_index < 0 or table.refs.items.len < start_table_index + init_length) { + return error.TrapOutOfBoundsTableAccess; + } + + if (start_elem_index < 0 or elems.len < start_elem_index + init_length) { + return error.TrapOutOfBoundsTableAccess; + } + + var elem_range = elems[start_elem_index .. start_elem_index + init_length]; + var table_range = table.refs.items[start_table_index .. start_table_index + init_length]; + + var index: u32 = 0; + while (index < elem_range.len) : (index += 1) { + var val: Val = elem_range[index].resolve(store); + + if (table.reftype == .FuncRef) { + val.FuncRef.module_instance = module; + } + + table_range[index] = val; + } + } +}; + +pub const WasmMemoryResizeFunction = *const fn (mem: ?[*]u8, new_size_bytes: usize, old_size_bytes: usize, userdata: ?*anyopaque) ?[*]u8; +pub const WasmMemoryFreeFunction = *const fn (mem: ?[*]u8, size_bytes: usize, userdata: ?*anyopaque) void; + +pub const WasmMemoryExternal = struct { + resize_callback: WasmMemoryResizeFunction, + free_callback: WasmMemoryFreeFunction, + userdata: ?*anyopaque, +}; + +pub const MemoryInstance = struct { + const BackingMemoryType = enum(u8) { + Internal, + External, + }; + + const BackingMemory = union(BackingMemoryType) { + Internal: StableArray(u8), + External: struct { + buffer: []u8, + params: WasmMemoryExternal, + }, + }; + + pub const k_page_size: usize = MemoryDefinition.k_page_size; + pub const k_max_pages: usize = MemoryDefinition.k_max_pages; + + limits: Limits, + mem: BackingMemory, + + pub fn init(limits: Limits, params: ?WasmMemoryExternal) MemoryInstance { + const max_pages = if (limits.max) |max| @max(1, max) else k_max_pages; + + var mem = if (params == null) BackingMemory{ + .Internal = StableArray(u8).init(max_pages * k_page_size), + } else BackingMemory{ .External = .{ + .buffer = &[0]u8{}, + .params = params.?, + } }; + + var instance = MemoryInstance{ + .limits = Limits{ .min = 0, .max = @as(u32, @intCast(max_pages)) }, + .mem = mem, + }; + + return instance; + } + + pub fn deinit(self: *MemoryInstance) void { + switch (self.mem) { + .Internal => |*m| m.deinit(), + .External => |*m| m.params.free_callback(m.buffer.ptr, m.buffer.len, m.params.userdata), + } + } + + pub fn size(self: *const MemoryInstance) usize { + return switch (self.mem) { + .Internal => |m| m.items.len / k_page_size, + .External => |m| m.buffer.len / k_page_size, + }; + } + + pub fn grow(self: *MemoryInstance, num_pages: usize) bool { + if (num_pages == 0) { + return true; + } + + const total_pages = self.limits.min + num_pages; + const max_pages = if (self.limits.max) |max| max else k_max_pages; + + if (total_pages > max_pages) { + return false; + } + + const commit_size: usize = (self.limits.min + num_pages) * k_page_size; + + switch (self.mem) { + .Internal => |*m| m.resize(commit_size) catch return false, + .External => |*m| { + var new_mem: ?[*]u8 = m.params.resize_callback(m.buffer.ptr, commit_size, m.buffer.len, m.params.userdata); + if (new_mem == null) { + return false; + } + m.buffer = new_mem.?[0..commit_size]; + }, + } + + self.limits.min = @as(u32, @intCast(total_pages)); + + return true; + } + + pub fn growAbsolute(self: *MemoryInstance, total_pages: usize) bool { + if (self.limits.min >= total_pages) { + return true; + } + + const pages_to_grow = total_pages - self.limits.min; + return self.grow(pages_to_grow); + } + + pub fn buffer(self: *const MemoryInstance) []u8 { + return switch (self.mem) { + .Internal => |m| m.items, + .External => |m| m.buffer, + }; + } + + fn ensureMinSize(self: *MemoryInstance, size_bytes: usize) !void { + if (self.limits.min * k_page_size < size_bytes) { + var num_min_pages = std.math.divCeil(usize, size_bytes, k_page_size) catch unreachable; + if (num_min_pages > self.limits.max.?) { + return error.TrapOutOfBoundsMemoryAccess; + } + + var needed_pages = num_min_pages - self.limits.min; + if (self.resize(needed_pages) == false) { + unreachable; + } + } + } +}; + +pub const ElementInstance = struct { + refs: std.ArrayList(Val), + reftype: ValType, +}; + +const ImportType = enum(u8) { + Host, + Wasm, +}; + +const HostFunctionCallback = *const fn (userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void; + +const HostFunction = struct { + userdata: ?*anyopaque, + func_def: FunctionTypeDefinition, + callback: HostFunctionCallback, +}; + +const ImportDataWasm = struct { + module_instance: *ModuleInstance, + index: u32, +}; + +pub const FunctionImport = struct { + name: []const u8, + data: union(ImportType) { + Host: HostFunction, + Wasm: ImportDataWasm, + }, + + fn dupe(import: *const FunctionImport, allocator: std.mem.Allocator) !FunctionImport { + var copy = import.*; + copy.name = try allocator.dupe(u8, copy.name); + switch (copy.data) { + .Host => |*data| { + var func_def = FunctionTypeDefinition{ + .types = std.ArrayList(ValType).init(allocator), + .num_params = data.func_def.num_params, + }; + try func_def.types.appendSlice(data.func_def.types.items); + data.func_def = func_def; + }, + .Wasm => {}, + } + + return copy; + } + + pub fn isTypeSignatureEql(import: *const FunctionImport, type_signature: *const FunctionTypeDefinition) bool { + var type_comparer = FunctionTypeDefinition.SortContext{}; + switch (import.data) { + .Host => |data| { + return type_comparer.eql(&data.func_def, type_signature); + }, + .Wasm => |data| { + var func_type_def: *const FunctionTypeDefinition = data.module_instance.findFuncTypeDef(data.index); + return type_comparer.eql(func_type_def, type_signature); + }, + } + } +}; + +pub const TableImport = struct { + name: []const u8, + data: union(ImportType) { + Host: *TableInstance, + Wasm: ImportDataWasm, + }, + + fn dupe(import: *const TableImport, allocator: std.mem.Allocator) !TableImport { + var copy = import.*; + copy.name = try allocator.dupe(u8, copy.name); + return copy; + } +}; + +pub const MemoryImport = struct { + name: []const u8, + data: union(ImportType) { + Host: *MemoryInstance, + Wasm: ImportDataWasm, + }, + + fn dupe(import: *const MemoryImport, allocator: std.mem.Allocator) !MemoryImport { + var copy = import.*; + copy.name = try allocator.dupe(u8, copy.name); + return copy; + } +}; + +pub const GlobalImport = struct { + name: []const u8, + data: union(ImportType) { + Host: *GlobalInstance, + Wasm: ImportDataWasm, + }, + + fn dupe(import: *const GlobalImport, allocator: std.mem.Allocator) !GlobalImport { + var copy = import.*; + copy.name = try allocator.dupe(u8, copy.name); + return copy; + } +}; + +pub const ModuleImportPackage = struct { + name: []const u8, + instance: ?*ModuleInstance, + userdata: ?*anyopaque, + functions: std.ArrayList(FunctionImport), + tables: std.ArrayList(TableImport), + memories: std.ArrayList(MemoryImport), + globals: std.ArrayList(GlobalImport), + allocator: std.mem.Allocator, + + pub fn init(name: []const u8, instance: ?*ModuleInstance, userdata: ?*anyopaque, allocator: std.mem.Allocator) std.mem.Allocator.Error!ModuleImportPackage { + return ModuleImportPackage{ + .name = try allocator.dupe(u8, name), + .instance = instance, + .userdata = userdata, + .functions = std.ArrayList(FunctionImport).init(allocator), + .tables = std.ArrayList(TableImport).init(allocator), + .memories = std.ArrayList(MemoryImport).init(allocator), + .globals = std.ArrayList(GlobalImport).init(allocator), + .allocator = allocator, + }; + } + + pub fn addHostFunction(self: *ModuleImportPackage, name: []const u8, param_types: []const ValType, return_types: []const ValType, callback: HostFunctionCallback, userdata: ?*anyopaque) std.mem.Allocator.Error!void { + std.debug.assert(self.instance == null); // cannot add host functions to an imports that is intended to be bound to a module instance + + var type_list = std.ArrayList(ValType).init(self.allocator); + try type_list.appendSlice(param_types); + try type_list.appendSlice(return_types); + + try self.functions.append(FunctionImport{ + .name = try self.allocator.dupe(u8, name), + .data = .{ + .Host = HostFunction{ + .userdata = userdata, + .func_def = FunctionTypeDefinition{ + .types = type_list, + .num_params = @as(u32, @intCast(param_types.len)), + }, + .callback = callback, + }, + }, + }); + } + + pub fn deinit(self: *ModuleImportPackage) void { + self.allocator.free(self.name); + + for (self.functions.items) |*item| { + self.allocator.free(item.name); + switch (item.data) { + .Host => |h| h.func_def.types.deinit(), + else => {}, + } + } + self.functions.deinit(); + + for (self.tables.items) |*item| { + self.allocator.free(item.name); + } + self.tables.deinit(); + + for (self.memories.items) |*item| { + self.allocator.free(item.name); + } + self.memories.deinit(); + + for (self.globals.items) |*item| { + self.allocator.free(item.name); + } + self.globals.deinit(); + } +}; + +pub const Store = struct { + tables: std.ArrayList(TableInstance), + memories: std.ArrayList(MemoryInstance), + globals: std.ArrayList(GlobalInstance), + elements: std.ArrayList(ElementInstance), + imports: struct { + functions: std.ArrayList(FunctionImport), + tables: std.ArrayList(TableImport), + memories: std.ArrayList(MemoryImport), + globals: std.ArrayList(GlobalImport), + }, + + fn init(allocator: std.mem.Allocator) Store { + var store = Store{ + .imports = .{ + .functions = std.ArrayList(FunctionImport).init(allocator), + .tables = std.ArrayList(TableImport).init(allocator), + .memories = std.ArrayList(MemoryImport).init(allocator), + .globals = std.ArrayList(GlobalImport).init(allocator), + }, + .tables = std.ArrayList(TableInstance).init(allocator), + .memories = std.ArrayList(MemoryInstance).init(allocator), + .globals = std.ArrayList(GlobalInstance).init(allocator), + .elements = std.ArrayList(ElementInstance).init(allocator), + }; + + return store; + } + + fn deinit(self: *Store) void { + for (self.tables.items) |*item| { + item.deinit(); + } + self.tables.deinit(); + + for (self.memories.items) |*item| { + item.deinit(); + } + self.memories.deinit(); + + self.globals.deinit(); + self.elements.deinit(); + } + + pub fn getTable(self: *Store, index: usize) *TableInstance { + if (self.imports.tables.items.len <= index) { + var instance_index = index - self.imports.tables.items.len; + return &self.tables.items[instance_index]; + } else { + var import: *TableImport = &self.imports.tables.items[index]; + return switch (import.data) { + .Host => |data| data, + .Wasm => |data| data.module_instance.store.getTable(data.index), + }; + } + } + + pub fn getMemory(self: *Store, index: usize) *MemoryInstance { + if (self.imports.memories.items.len <= index) { + var instance_index = index - self.imports.memories.items.len; + return &self.memories.items[instance_index]; + } else { + var import: *MemoryImport = &self.imports.memories.items[index]; + return switch (import.data) { + .Host => |data| data, + .Wasm => |data| data.module_instance.store.getMemory(data.index), + }; + } + } + + pub fn getGlobal(self: *Store, index: usize) *GlobalInstance { // TODO make private + if (self.imports.globals.items.len <= index) { + var instance_index = index - self.imports.globals.items.len; + return &self.globals.items[instance_index]; + } else { + var import: *GlobalImport = &self.imports.globals.items[index]; + return switch (import.data) { + .Host => |data| data, + .Wasm => |data| data.module_instance.store.getGlobal(data.index), + }; + } + } +}; + +pub const ModuleInstantiateOpts = struct { + /// imports is not owned by ModuleInstance - caller must ensure its memory outlives ModuleInstance + imports: ?[]const ModuleImportPackage = null, + wasm_memory_external: ?WasmMemoryExternal = null, + stack_size: usize = 0, + enable_debug: bool = false, +}; + +pub const InvokeOpts = struct { + trap_on_start: bool = false, +}; + +pub const DebugTrapInstructionMode = enum { + Enable, + Disable, +}; + +pub const VM = struct { + const InitFn = *const fn (vm: *VM) void; + const DeinitFn = *const fn (vm: *VM) void; + const InstantiateFn = *const fn (vm: *VM, module: *ModuleInstance, opts: ModuleInstantiateOpts) anyerror!void; + const InvokeFn = *const fn (vm: *VM, module: *ModuleInstance, handle: FunctionHandle, params: [*]const Val, returns: [*]Val, opts: InvokeOpts) anyerror!void; + const InvokeWithIndexFn = *const fn (vm: *VM, module: *ModuleInstance, func_index: usize, params: [*]const Val, returns: [*]Val) anyerror!void; + const ResumeInvokeFn = *const fn (vm: *VM, module: *ModuleInstance, returns: []Val) anyerror!void; + const StepFn = *const fn (vm: *VM, module: *ModuleInstance, returns: []Val) anyerror!void; + const SetDebugTrapFn = *const fn (vm: *VM, module: *ModuleInstance, wasm_address: u32, mode: DebugTrapInstructionMode) anyerror!bool; + const FormatBacktraceFn = *const fn (vm: *VM, indent: u8, allocator: std.mem.Allocator) anyerror!std.ArrayList(u8); + const FindFuncTypeDefFn = *const fn (vm: *VM, module: *ModuleInstance, local_func_index: usize) *const FunctionTypeDefinition; + + deinit_fn: DeinitFn, + instantiate_fn: InstantiateFn, + invoke_fn: InvokeFn, + invoke_with_index_fn: InvokeWithIndexFn, + resume_invoke_fn: ResumeInvokeFn, + step_fn: StepFn, + set_debug_trap_fn: SetDebugTrapFn, + format_backtrace_fn: FormatBacktraceFn, + find_func_type_def_fn: FindFuncTypeDefFn, + + allocator: std.mem.Allocator, + mem: []u8, // VM and impl memory live here + + impl: *anyopaque, + + pub fn create(comptime T: type, allocator: std.mem.Allocator) AllocError!*VM { + const alignment = @max(@alignOf(VM), @alignOf(T)); + const vm_alloc_size = std.mem.alignForward(usize, @sizeOf(VM), alignment); + const impl_alloc_size = std.mem.alignForward(usize, @sizeOf(T), alignment); + const total_alloc_size = vm_alloc_size + impl_alloc_size; + + var mem = try allocator.alloc(u8, total_alloc_size); + + var vm: *VM = @as(*VM, @alignCast(@ptrCast(mem.ptr))); + var impl: *T = @as(*T, @alignCast(@ptrCast(mem[vm_alloc_size..].ptr))); + + vm.deinit_fn = T.deinit; + vm.instantiate_fn = T.instantiate; + vm.invoke_fn = T.invoke; + vm.invoke_with_index_fn = T.invokeWithIndex; + vm.resume_invoke_fn = T.resumeInvoke; + vm.step_fn = T.step; + vm.set_debug_trap_fn = T.setDebugTrap; + vm.format_backtrace_fn = T.formatBacktrace; + vm.find_func_type_def_fn = T.findFuncTypeDef; + vm.allocator = allocator; + vm.mem = mem; + vm.impl = impl; + + T.init(vm); + + return vm; + } + + fn destroy(vm: *VM) void { + vm.deinit_fn(vm); + + var allocator = vm.allocator; + var mem = vm.mem; + allocator.free(mem); + } + + fn instantiate(vm: *VM, module: *ModuleInstance, opts: ModuleInstantiateOpts) anyerror!void { + try vm.instantiate_fn(vm, module, opts); + } + + pub fn invoke(vm: *VM, module: *ModuleInstance, handle: FunctionHandle, params: [*]const Val, returns: [*]Val, opts: InvokeOpts) anyerror!void { + try vm.invoke_fn(vm, module, handle, params, returns, opts); + } + + pub fn invokeWithIndex(vm: *VM, module: *ModuleInstance, func_index: usize, params: [*]const Val, returns: [*]Val) anyerror!void { + try vm.invoke_with_index_fn(vm, module, func_index, params, returns); + } + + pub fn resumeInvoke(vm: *VM, module: *ModuleInstance, returns: []Val) anyerror!void { + try vm.resume_invoke_fn(vm, module, returns); + } + + pub fn step(vm: *VM, module: *ModuleInstance, returns: []Val) anyerror!void { + try vm.step_fn(vm, module, returns); + } + + pub fn setDebugTrap(vm: *VM, module: *ModuleInstance, wasm_address: u32, mode: DebugTrapInstructionMode) anyerror!bool { + return try vm.set_debug_trap_fn(vm, module, wasm_address, mode); + } + + pub fn formatBacktrace(vm: *VM, indent: u8, allocator: std.mem.Allocator) anyerror!std.ArrayList(u8) { + return vm.format_backtrace_fn(vm, indent, allocator); + } + + pub fn findFuncTypeDef(vm: *VM, module: *ModuleInstance, local_func_index: usize) *const FunctionTypeDefinition { + return vm.find_func_type_def_fn(vm, module, local_func_index); + } +}; + +pub const ModuleInstance = struct { + allocator: std.mem.Allocator, + store: Store, + module_def: *const ModuleDefinition, + userdata: ?*anyopaque = null, // any host data associated with this module + is_instantiated: bool = false, + vm: *VM, + + pub fn create(module_def: *const ModuleDefinition, vm: *VM, allocator: std.mem.Allocator) AllocError!*ModuleInstance { + var inst = try allocator.create(ModuleInstance); + inst.* = ModuleInstance{ + .allocator = allocator, + .store = Store.init(allocator), + .module_def = module_def, + .vm = vm, + }; + return inst; + } + + pub fn destroy(self: *ModuleInstance) void { + self.vm.destroy(); + self.store.deinit(); + + var allocator = self.allocator; + allocator.destroy(self); + } + + pub fn instantiate(self: *ModuleInstance, opts: ModuleInstantiateOpts) !void { + const Helpers = struct { + fn areLimitsCompatible(def_limits: *const Limits, instance_limits: *const Limits) bool { + if (def_limits.max != null and instance_limits.max == null) { + return false; + } + + var def_max: u32 = if (def_limits.max) |max| max else std.math.maxInt(u32); + var instance_max: u32 = if (instance_limits.max) |max| max else 0; + + return def_limits.min <= instance_limits.min and def_max >= instance_max; + } + + // TODO probably should change the imports search to a hashed lookup of module_name+item_name -> array of items to make this faster + fn findImportInMultiple(comptime T: type, names: *const ImportNames, imports_or_null: ?[]const ModuleImportPackage) UnlinkableError!*const T { + if (imports_or_null) |_imports| { + for (_imports) |*module_imports| { + const wildcard_name = std.mem.eql(u8, module_imports.name, "*"); + if (wildcard_name or std.mem.eql(u8, names.module_name, module_imports.name)) { + switch (T) { + FunctionImport => { + if (findImportInSingle(FunctionImport, names, module_imports)) |import| { + return import; + } + if (findImportInSingle(TableImport, names, module_imports)) |_| { + return error.UnlinkableIncompatibleImportType; + } + if (findImportInSingle(MemoryImport, names, module_imports)) |_| { + return error.UnlinkableIncompatibleImportType; + } + if (findImportInSingle(GlobalImport, names, module_imports)) |_| { + return error.UnlinkableIncompatibleImportType; + } + }, + TableImport => { + if (findImportInSingle(TableImport, names, module_imports)) |import| { + return import; + } + if (findImportInSingle(FunctionImport, names, module_imports)) |_| { + return error.UnlinkableIncompatibleImportType; + } + if (findImportInSingle(MemoryImport, names, module_imports)) |_| { + return error.UnlinkableIncompatibleImportType; + } + if (findImportInSingle(GlobalImport, names, module_imports)) |_| { + return error.UnlinkableIncompatibleImportType; + } + }, + MemoryImport => { + if (findImportInSingle(MemoryImport, names, module_imports)) |import| { + return import; + } + if (findImportInSingle(FunctionImport, names, module_imports)) |_| { + return error.UnlinkableIncompatibleImportType; + } + if (findImportInSingle(TableImport, names, module_imports)) |_| { + return error.UnlinkableIncompatibleImportType; + } + if (findImportInSingle(GlobalImport, names, module_imports)) |_| { + return error.UnlinkableIncompatibleImportType; + } + }, + GlobalImport => { + if (findImportInSingle(GlobalImport, names, module_imports)) |import| { + return import; + } + if (findImportInSingle(FunctionImport, names, module_imports)) |_| { + return error.UnlinkableIncompatibleImportType; + } + if (findImportInSingle(TableImport, names, module_imports)) |_| { + return error.UnlinkableIncompatibleImportType; + } + if (findImportInSingle(MemoryImport, names, module_imports)) |_| { + return error.UnlinkableIncompatibleImportType; + } + }, + else => unreachable, + } + break; + } + } + } + + return error.UnlinkableUnknownImport; + } + + fn findImportInSingle(comptime T: type, names: *const ImportNames, module_imports: *const ModuleImportPackage) ?*const T { + var items: []const T = switch (T) { + FunctionImport => module_imports.functions.items, + TableImport => module_imports.tables.items, + MemoryImport => module_imports.memories.items, + GlobalImport => module_imports.globals.items, + else => unreachable, + }; + + for (items) |*item| { + if (std.mem.eql(u8, names.import_name, item.name)) { + return item; + } + } + + return null; + } + }; + + std.debug.assert(self.is_instantiated == false); + + var store: *Store = &self.store; + var module_def: *const ModuleDefinition = self.module_def; + var allocator = self.allocator; + + for (module_def.imports.functions.items) |*func_import_def| { + var import_func: *const FunctionImport = try Helpers.findImportInMultiple(FunctionImport, &func_import_def.names, opts.imports); + + const type_def: *const FunctionTypeDefinition = &module_def.types.items[func_import_def.type_index]; + const is_type_signature_eql: bool = import_func.isTypeSignatureEql(type_def); + + if (is_type_signature_eql == false) { + return error.UnlinkableIncompatibleImportType; + } + + try store.imports.functions.append(try import_func.dupe(allocator)); + } + + for (module_def.imports.tables.items) |*table_import_def| { + var import_table: *const TableImport = try Helpers.findImportInMultiple(TableImport, &table_import_def.names, opts.imports); + + var is_eql: bool = undefined; + switch (import_table.data) { + .Host => |table_instance| { + is_eql = table_instance.reftype == table_import_def.reftype and + Helpers.areLimitsCompatible(&table_import_def.limits, &table_instance.limits); + }, + .Wasm => |data| { + const table_instance: *const TableInstance = data.module_instance.store.getTable(data.index); + is_eql = table_instance.reftype == table_import_def.reftype and + Helpers.areLimitsCompatible(&table_import_def.limits, &table_instance.limits); + }, + } + + if (is_eql == false) { + return error.UnlinkableIncompatibleImportType; + } + + try store.imports.tables.append(try import_table.dupe(allocator)); + } + + for (module_def.imports.memories.items) |*memory_import_def| { + var import_memory: *const MemoryImport = try Helpers.findImportInMultiple(MemoryImport, &memory_import_def.names, opts.imports); + + var is_eql: bool = undefined; + switch (import_memory.data) { + .Host => |memory_instance| { + is_eql = Helpers.areLimitsCompatible(&memory_import_def.limits, &memory_instance.limits); + }, + .Wasm => |data| { + const memory_instance: *const MemoryInstance = data.module_instance.store.getMemory(data.index); + is_eql = Helpers.areLimitsCompatible(&memory_import_def.limits, &memory_instance.limits); + }, + } + + if (is_eql == false) { + return error.UnlinkableIncompatibleImportType; + } + + try store.imports.memories.append(try import_memory.dupe(allocator)); + } + + for (module_def.imports.globals.items) |*global_import_def| { + var import_global: *const GlobalImport = try Helpers.findImportInMultiple(GlobalImport, &global_import_def.names, opts.imports); + + var is_eql: bool = undefined; + switch (import_global.data) { + .Host => |global_instance| { + is_eql = global_import_def.valtype == global_instance.def.valtype and + global_import_def.mut == global_instance.def.mut; + }, + .Wasm => |data| { + const global_instance: *const GlobalInstance = data.module_instance.store.getGlobal(data.index); + is_eql = global_import_def.valtype == global_instance.def.valtype and + global_import_def.mut == global_instance.def.mut; + }, + } + + if (is_eql == false) { + return error.UnlinkableIncompatibleImportType; + } + + try store.imports.globals.append(try import_global.dupe(allocator)); + } + + // instantiate the rest of the needed module definitions + try self.vm.instantiate(self, opts); + + try store.tables.ensureTotalCapacity(module_def.imports.tables.items.len + module_def.tables.items.len); + + for (module_def.tables.items) |*def_table| { + var t = try TableInstance.init(def_table.reftype, def_table.limits, allocator); + try store.tables.append(t); + } + + try store.memories.ensureTotalCapacity(module_def.imports.memories.items.len + module_def.memories.items.len); + + for (module_def.memories.items) |*def_memory| { + var memory = MemoryInstance.init(def_memory.limits, opts.wasm_memory_external); + if (memory.grow(def_memory.limits.min) == false) { + unreachable; + } + try store.memories.append(memory); + } + + try store.globals.ensureTotalCapacity(module_def.imports.globals.items.len + module_def.globals.items.len); + + for (module_def.globals.items) |*def_global| { + var global = GlobalInstance{ + .def = def_global, + .value = def_global.expr.resolve(store), + }; + if (def_global.valtype == .FuncRef) { + global.value.FuncRef.module_instance = self; + } + try store.globals.append(global); + } + + // iterate over elements and init the ones needed + try store.elements.ensureTotalCapacity(module_def.elements.items.len); + for (module_def.elements.items) |*def_elem| { + var elem = ElementInstance{ + .refs = std.ArrayList(Val).init(allocator), + .reftype = def_elem.reftype, + }; + + // instructions using passive elements just use the module definition's data to avoid an extra copy + if (def_elem.mode == .Active) { + std.debug.assert(def_elem.table_index < store.imports.tables.items.len + store.tables.items.len); + + var table: *TableInstance = store.getTable(def_elem.table_index); + + var start_table_index_i32: i32 = if (def_elem.offset) |offset| offset.resolveTo(store, i32) else 0; + if (start_table_index_i32 < 0) { + return error.UninstantiableOutOfBoundsTableAccess; + } + + var start_table_index = @as(u32, @intCast(start_table_index_i32)); + + if (def_elem.elems_value.items.len > 0) { + var elems = def_elem.elems_value.items; + try table.init_range_val(self, elems, @as(u32, @intCast(elems.len)), 0, start_table_index); + } else { + var elems = def_elem.elems_expr.items; + try table.init_range_expr(self, elems, @as(u32, @intCast(elems.len)), 0, start_table_index, store); + } + } else if (def_elem.mode == .Passive) { + if (def_elem.elems_value.items.len > 0) { + try elem.refs.resize(def_elem.elems_value.items.len); + var index: usize = 0; + while (index < elem.refs.items.len) : (index += 1) { + elem.refs.items[index] = def_elem.elems_value.items[index]; + if (elem.reftype == .FuncRef) { + elem.refs.items[index].FuncRef.module_instance = self; + } + } + } else { + try elem.refs.resize(def_elem.elems_expr.items.len); + var index: usize = 0; + while (index < elem.refs.items.len) : (index += 1) { + elem.refs.items[index] = def_elem.elems_expr.items[index].resolve(store); + if (elem.reftype == .FuncRef) { + elem.refs.items[index].FuncRef.module_instance = self; + } + } + } + } + + store.elements.appendAssumeCapacity(elem); + } + + for (module_def.datas.items) |*def_data| { + // instructions using passive elements just use the module definition's data to avoid an extra copy + if (def_data.mode == .Active) { + var memory_index: u32 = def_data.memory_index.?; + var memory: *MemoryInstance = store.getMemory(memory_index); + + const num_bytes: usize = def_data.bytes.items.len; + const offset_begin: usize = (def_data.offset.?).resolveTo(store, u32); + const offset_end: usize = offset_begin + num_bytes; + + const mem_buffer: []u8 = memory.buffer(); + + if (mem_buffer.len < offset_end) { + return error.UninstantiableOutOfBoundsMemoryAccess; + } + + var destination = mem_buffer[offset_begin..offset_end]; + std.mem.copy(u8, destination, def_data.bytes.items); + } + } + + if (module_def.start_func_index) |func_index| { + var no_vals: []Val = &[0]Val{}; + try self.vm.invokeWithIndex(self, func_index, no_vals.ptr, no_vals.ptr); + } + } + + pub fn exports(self: *ModuleInstance, name: []const u8) !ModuleImportPackage { + var imports = try ModuleImportPackage.init(name, self, null, self.allocator); + + for (self.module_def.exports.functions.items) |*item| { + try imports.functions.append(FunctionImport{ + .name = try imports.allocator.dupe(u8, item.name), + .data = .{ + .Wasm = ImportDataWasm{ + .module_instance = self, + .index = item.index, + }, + }, + }); + } + + for (self.module_def.exports.tables.items) |*item| { + try imports.tables.append(TableImport{ + .name = try imports.allocator.dupe(u8, item.name), + .data = .{ + .Wasm = ImportDataWasm{ + .module_instance = self, + .index = item.index, + }, + }, + }); + } + + for (self.module_def.exports.memories.items) |*item| { + try imports.memories.append(MemoryImport{ + .name = try imports.allocator.dupe(u8, item.name), + .data = .{ + .Wasm = ImportDataWasm{ + .module_instance = self, + .index = item.index, + }, + }, + }); + } + + for (self.module_def.exports.globals.items) |*item| { + try imports.globals.append(GlobalImport{ + .name = try imports.allocator.dupe(u8, item.name), + .data = .{ + .Wasm = ImportDataWasm{ + .module_instance = self, + .index = item.index, + }, + }, + }); + } + + return imports; + } + + pub fn getFunctionHandle(self: *const ModuleInstance, func_name: []const u8) ExportError!FunctionHandle { + for (self.module_def.exports.functions.items) |func_export| { + if (std.mem.eql(u8, func_name, func_export.name)) { + if (func_export.index >= self.module_def.imports.functions.items.len) { + var func_index: usize = func_export.index - self.module_def.imports.functions.items.len; + return FunctionHandle{ + .index = @as(u32, @intCast(func_index)), + .type = .Export, + }; + } else { + return FunctionHandle{ + .index = @as(u32, @intCast(func_export.index)), + .type = .Import, + }; + } + } + } + + for (self.store.imports.functions.items, 0..) |*func_import, i| { + if (std.mem.eql(u8, func_name, func_import.name)) { + return FunctionHandle{ + .index = @as(u32, @intCast(i)), + .type = .Import, + }; + } + } + + return error.ExportUnknownFunction; + } + + pub fn getFunctionInfo(self: *const ModuleInstance, handle: FunctionHandle) FunctionExport { + return self.module_def.getFunctionExport(handle); + } + + pub fn getGlobalExport(self: *ModuleInstance, global_name: []const u8) ExportError!GlobalExport { + for (self.module_def.exports.globals.items) |*global_export| { + if (std.mem.eql(u8, global_name, global_export.name)) { + var global: *GlobalInstance = self.getGlobalWithIndex(global_export.index); + return GlobalExport{ + .val = &global.value, + .valtype = global.def.valtype, + .mut = global.def.mut, + }; + } + } + + return error.ExportUnknownGlobal; + } + + pub fn invoke(self: *ModuleInstance, handle: FunctionHandle, params: [*]const Val, returns: [*]Val, opts: InvokeOpts) anyerror!void { + try self.vm.invoke(self, handle, params, returns, opts); + } + + /// Use to resume an invoked function after it returned error.DebugTrap + pub fn resumeInvoke(self: *ModuleInstance, returns: []Val) anyerror!void { + try self.vm.resumeInvoke(self, returns); + } + + pub fn step(self: *ModuleInstance, returns: []Val) anyerror!void { + try self.vm.step(self, returns); + } + + pub fn setDebugTrap(self: *ModuleInstance, wasm_address: u32, mode: DebugTrapInstructionMode) anyerror!bool { + try self.vm.setDebugTrap(self, wasm_address, mode); + } + + pub fn memorySlice(self: *ModuleInstance, offset: usize, length: usize) []u8 { + const memory: *MemoryInstance = self.store.getMemory(0); + + const buffer = memory.buffer(); + if (offset + length < buffer.len) { + var data: []u8 = buffer[offset .. offset + length]; + return data; + } + + return ""; + } + + pub fn memoryAll(self: *ModuleInstance) []u8 { + const memory: *MemoryInstance = self.store.getMemory(0); + const buffer = memory.buffer(); + return buffer; + } + + pub fn memoryGrow(self: *ModuleInstance, num_pages: usize) bool { + const memory: *MemoryInstance = self.store.getMemory(0); + return memory.grow(num_pages); + } + + pub fn memoryGrowAbsolute(self: *ModuleInstance, total_pages: usize) bool { + const memory: *MemoryInstance = self.store.getMemory(0); + return memory.growAbsolute(total_pages); + } + + pub fn memoryWriteInt(self: *ModuleInstance, comptime T: type, value: T, offset: usize) bool { + var bytes: [(@typeInfo(T).Int.bits + 7) / 8]u8 = undefined; + std.mem.writeIntLittle(T, &bytes, value); + + var destination = self.memorySlice(offset, bytes.len); + if (destination.len == bytes.len) { + std.mem.copy(u8, destination, &bytes); + return true; + } + + return false; + } + + /// Caller owns returned memory and must free via allocator.free() + pub fn formatBacktrace(self: *ModuleInstance, indent: u8, allocator: std.mem.Allocator) anyerror!std.ArrayList(u8) { + return self.vm.format_backtrace_fn(self.vm, indent, allocator); + } + + fn findFuncTypeDef(self: *ModuleInstance, index: usize) *const FunctionTypeDefinition { + const num_imports: usize = self.store.imports.functions.items.len; + if (index >= num_imports) { + var local_func_index: usize = index - num_imports; + return self.vm.findFuncTypeDef(self, local_func_index); + } else { + var import: *const FunctionImport = &self.store.imports.functions.items[index]; + var func_type_def: *const FunctionTypeDefinition = switch (import.data) { + .Host => |data| &data.func_def, + .Wasm => |data| data.module_instance.findFuncTypeDef(data.index), + }; + return func_type_def; + } + } + + fn getGlobalWithIndex(self: *ModuleInstance, index: usize) *GlobalInstance { + const num_imports: usize = self.module_def.imports.globals.items.len; + if (index >= num_imports) { + var local_global_index: usize = index - self.module_def.imports.globals.items.len; + return &self.store.globals.items[local_global_index]; + } else { + var import: *const GlobalImport = &self.store.imports.globals.items[index]; + return switch (import.data) { + .Host => |data| data, + .Wasm => |data| data.module_instance.getGlobalWithIndex(data.index), + }; + } + } +}; diff --git a/src/ext/bytebox/src/opcode.zig b/src/ext/bytebox/src/opcode.zig new file mode 100644 index 00000000..9a29ad49 --- /dev/null +++ b/src/ext/bytebox/src/opcode.zig @@ -0,0 +1,1436 @@ +const std = @import("std"); +const common = @import("common.zig"); + +// A compressed version of the wasm opcodes for better table-oriented lookup (no holes). See WasmOpcode for the actual wasm representation. +pub const Opcode = enum(u16) { + Invalid, // Has no corresponding mapping in WasmOpcode. + Unreachable, + DebugTrap, // Has no corresponding mapping in WasmOpcode, intended for use in returning control flow to invoker + Noop, + Block, + Loop, + If, + IfNoElse, // variant of If that assumes no else branch + Else, + End, + Branch, + Branch_If, + Branch_Table, + Return, + Call, + Call_Indirect, + Drop, + Select, + Select_T, + Local_Get, + Local_Set, + Local_Tee, + Global_Get, + Global_Set, + Table_Get, + Table_Set, + I32_Load, + I64_Load, + F32_Load, + F64_Load, + I32_Load8_S, + I32_Load8_U, + I32_Load16_S, + I32_Load16_U, + I64_Load8_S, + I64_Load8_U, + I64_Load16_S, + I64_Load16_U, + I64_Load32_S, + I64_Load32_U, + I32_Store, + I64_Store, + F32_Store, + F64_Store, + I32_Store8, + I32_Store16, + I64_Store8, + I64_Store16, + I64_Store32, + Memory_Size, + Memory_Grow, + I32_Const, + I64_Const, + F32_Const, + F64_Const, + I32_Eqz, + I32_Eq, + I32_NE, + I32_LT_S, + I32_LT_U, + I32_GT_S, + I32_GT_U, + I32_LE_S, + I32_LE_U, + I32_GE_S, + I32_GE_U, + I64_Eqz, + I64_Eq, + I64_NE, + I64_LT_S, + I64_LT_U, + I64_GT_S, + I64_GT_U, + I64_LE_S, + I64_LE_U, + I64_GE_S, + I64_GE_U, + F32_EQ, + F32_NE, + F32_LT, + F32_GT, + F32_LE, + F32_GE, + F64_EQ, + F64_NE, + F64_LT, + F64_GT, + F64_LE, + F64_GE, + I32_Clz, + I32_Ctz, + I32_Popcnt, + I32_Add, + I32_Sub, + I32_Mul, + I32_Div_S, + I32_Div_U, + I32_Rem_S, + I32_Rem_U, + I32_And, + I32_Or, + I32_Xor, + I32_Shl, + I32_Shr_S, + I32_Shr_U, + I32_Rotl, + I32_Rotr, + I64_Clz, + I64_Ctz, + I64_Popcnt, + I64_Add, + I64_Sub, + I64_Mul, + I64_Div_S, + I64_Div_U, + I64_Rem_S, + I64_Rem_U, + I64_And, + I64_Or, + I64_Xor, + I64_Shl, + I64_Shr_S, + I64_Shr_U, + I64_Rotl, + I64_Rotr, + F32_Abs, + F32_Neg, + F32_Ceil, + F32_Floor, + F32_Trunc, + F32_Nearest, + F32_Sqrt, + F32_Add, + F32_Sub, + F32_Mul, + F32_Div, + F32_Min, + F32_Max, + F32_Copysign, + F64_Abs, + F64_Neg, + F64_Ceil, + F64_Floor, + F64_Trunc, + F64_Nearest, + F64_Sqrt, + F64_Add, + F64_Sub, + F64_Mul, + F64_Div, + F64_Min, + F64_Max, + F64_Copysign, + I32_Wrap_I64, + I32_Trunc_F32_S, + I32_Trunc_F32_U, + I32_Trunc_F64_S, + I32_Trunc_F64_U, + I64_Extend_I32_S, + I64_Extend_I32_U, + I64_Trunc_F32_S, + I64_Trunc_F32_U, + I64_Trunc_F64_S, + I64_Trunc_F64_U, + F32_Convert_I32_S, + F32_Convert_I32_U, + F32_Convert_I64_S, + F32_Convert_I64_U, + F32_Demote_F64, + F64_Convert_I32_S, + F64_Convert_I32_U, + F64_Convert_I64_S, + F64_Convert_I64_U, + F64_Promote_F32, + I32_Reinterpret_F32, + I64_Reinterpret_F64, + F32_Reinterpret_I32, + F64_Reinterpret_I64, + I32_Extend8_S, + I32_Extend16_S, + I64_Extend8_S, + I64_Extend16_S, + I64_Extend32_S, + Ref_Null, + Ref_Is_Null, + Ref_Func, + I32_Trunc_Sat_F32_S, + I32_Trunc_Sat_F32_U, + I32_Trunc_Sat_F64_S, + I32_Trunc_Sat_F64_U, + I64_Trunc_Sat_F32_S, + I64_Trunc_Sat_F32_U, + I64_Trunc_Sat_F64_S, + I64_Trunc_Sat_F64_U, + Memory_Init, + Data_Drop, + Memory_Copy, + Memory_Fill, + Table_Init, + Elem_Drop, + Table_Copy, + Table_Grow, + Table_Size, + Table_Fill, + V128_Load, + V128_Load8x8_S, + V128_Load8x8_U, + V128_Load16x4_S, + V128_Load16x4_U, + V128_Load32x2_S, + V128_Load32x2_U, + V128_Load8_Splat, + V128_Load16_Splat, + V128_Load32_Splat, + V128_Load64_Splat, + V128_Store, + V128_Const, + I8x16_Shuffle, + I8x16_Swizzle, + I8x16_Splat, + I16x8_Splat, + I32x4_Splat, + I64x2_Splat, + F32x4_Splat, + F64x2_Splat, + I8x16_Extract_Lane_S, + I8x16_Extract_Lane_U, + I8x16_Replace_Lane, + I16x8_Extract_Lane_S, + I16x8_Extract_Lane_U, + I16x8_Replace_Lane, + I32x4_Extract_Lane, + I32x4_Replace_Lane, + I64x2_Extract_Lane, + I64x2_Replace_Lane, + F32x4_Extract_Lane, + F32x4_Replace_Lane, + F64x2_Extract_Lane, + F64x2_Replace_Lane, + I8x16_EQ, + I8x16_NE, + I8x16_LT_S, + I8x16_LT_U, + I8x16_GT_S, + I8x16_GT_U, + I8x16_LE_S, + I8x16_LE_U, + I8x16_GE_S, + I8x16_GE_U, + I16x8_EQ, + I16x8_NE, + I16x8_LT_S, + I16x8_LT_U, + I16x8_GT_S, + I16x8_GT_U, + I16x8_LE_S, + I16x8_LE_U, + I16x8_GE_S, + I16x8_GE_U, + I32x4_EQ, + I32x4_NE, + I32x4_LT_S, + I32x4_LT_U, + I32x4_GT_S, + I32x4_GT_U, + I32x4_LE_S, + I32x4_LE_U, + I32x4_GE_S, + I32x4_GE_U, + F32x4_EQ, + F32x4_NE, + F32x4_LT, + F32x4_GT, + F32x4_LE, + F32x4_GE, + F64x2_EQ, + F64x2_NE, + F64x2_LT, + F64x2_GT, + F64x2_LE, + F64x2_GE, + V128_Not, + V128_And, + V128_AndNot, + V128_Or, + V128_Xor, + V128_Bitselect, + V128_AnyTrue, + V128_Load8_Lane, + V128_Load16_Lane, + V128_Load32_Lane, + V128_Load64_Lane, + V128_Store8_Lane, + V128_Store16_Lane, + V128_Store32_Lane, + V128_Store64_Lane, + V128_Load32_Zero, + V128_Load64_Zero, + F32x4_Demote_F64x2_Zero, + F64x2_Promote_Low_F32x4, + I8x16_Abs, + I8x16_Neg, + I8x16_Popcnt, + I8x16_AllTrue, + I8x16_Bitmask, + I8x16_Narrow_I16x8_S, + I8x16_Narrow_I16x8_U, + F32x4_Ceil, + F32x4_Floor, + F32x4_Trunc, + F32x4_Nearest, + I8x16_Shl, + I8x16_Shr_S, + I8x16_Shr_U, + I8x16_Add, + I8x16_Add_Sat_S, + I8x16_Add_Sat_U, + I8x16_Sub, + I8x16_Sub_Sat_S, + I8x16_Sub_Sat_U, + F64x2_Ceil, + F64x2_Floor, + I8x16_Min_S, + I8x16_Min_U, + I8x16_Max_S, + I8x16_Max_U, + F64x2_Trunc, + I8x16_Avgr_U, + I16x8_Extadd_Pairwise_I8x16_S, + I16x8_Extadd_Pairwise_I8x16_U, + I32x4_Extadd_Pairwise_I16x8_S, + I32x4_Extadd_Pairwise_I16x8_U, + I16x8_Abs, + I16x8_Neg, + I16x8_Q15mulr_Sat_S, + I16x8_AllTrue, + I16x8_Bitmask, + I16x8_Narrow_I32x4_S, + I16x8_Narrow_I32x4_U, + I16x8_Extend_Low_I8x16_S, + I16x8_Extend_High_I8x16_S, + I16x8_Extend_Low_I8x16_U, + I16x8_Extend_High_I8x16_U, + I16x8_Shl, + I16x8_Shr_S, + I16x8_Shr_U, + I16x8_Add, + I16x8_Add_Sat_S, + I16x8_Add_Sat_U, + I16x8_Sub, + I16x8_Sub_Sat_S, + I16x8_Sub_Sat_U, + F64x2_Nearest, + I16x8_Mul, + I16x8_Min_S, + I16x8_Min_U, + I16x8_Max_S, + I16x8_Max_U, + I16x8_Avgr_U, + I16x8_Extmul_Low_I8x16_S, + I16x8_Extmul_High_I8x16_S, + I16x8_Extmul_Low_I8x16_U, + I16x8_Extmul_High_I8x16_U, + I32x4_Abs, + I32x4_Neg, + I32x4_AllTrue, + I32x4_Bitmask, + I32x4_Extend_Low_I16x8_S, + I32x4_Extend_High_I16x8_S, + I32x4_Extend_Low_I16x8_U, + I32x4_Extend_High_I16x8_U, + I32x4_Shl, + I32x4_Shr_S, + I32x4_Shr_U, + I32x4_Add, + I32x4_Sub, + I32x4_Mul, + I32x4_Min_S, + I32x4_Min_U, + I32x4_Max_S, + I32x4_Max_U, + I32x4_Dot_I16x8_S, + I32x4_Extmul_Low_I16x8_S, + I32x4_Extmul_High_I16x8_S, + I32x4_Extmul_Low_I16x8_U, + I32x4_Extmul_High_I16x8_U, + I64x2_Abs, + I64x2_Neg, + I64x2_AllTrue, + I64x2_Bitmask, + I64x2_Extend_Low_I32x4_S, + I64x2_Extend_High_I32x4_S, + I64x2_Extend_Low_I32x4_U, + I64x2_Extend_High_I32x4_U, + I64x2_Shl, + I64x2_Shr_S, + I64x2_Shr_U, + I64x2_Add, + I64x2_Sub, + I64x2_Mul, + I64x2_EQ, + I64x2_NE, + I64x2_LT_S, + I64x2_GT_S, + I64x2_LE_S, + I64x2_GE_S, + I64x2_Extmul_Low_I32x4_S, + I64x2_Extmul_High_I32x4_S, + I64x2_Extmul_Low_I32x4_U, + I64x2_Extmul_High_I32x4_U, + F32x4_Abs, + F32x4_Neg, + F32x4_Sqrt, + F32x4_Add, + F32x4_Sub, + F32x4_Mul, + F32x4_Div, + F32x4_Min, + F32x4_Max, + F32x4_PMin, + F32x4_PMax, + F64x2_Abs, + F64x2_Neg, + F64x2_Sqrt, + F64x2_Add, + F64x2_Sub, + F64x2_Mul, + F64x2_Div, + F64x2_Min, + F64x2_Max, + F64x2_PMin, + F64x2_PMax, + F32x4_Trunc_Sat_F32x4_S, + F32x4_Trunc_Sat_F32x4_U, + F32x4_Convert_I32x4_S, + F32x4_Convert_I32x4_U, + I32x4_Trunc_Sat_F64x2_S_Zero, + I32x4_Trunc_Sat_F64x2_U_Zero, + F64x2_Convert_Low_I32x4_S, + F64x2_Convert_Low_I32x4_U, + + pub fn beginsBlock(opcode: Opcode) bool { + return switch (opcode) { + .Block => true, + .Loop => true, + .If => true, + else => false, + }; + } + + pub fn isIf(opcode: Opcode) bool { + return switch (opcode) { + .If, .IfNoElse => true, + else => false, + }; + } +}; + +pub const WasmOpcode = enum(u16) { + Unreachable = 0x00, + Noop = 0x01, + Block = 0x02, + Loop = 0x03, + If = 0x04, + Else = 0x05, + End = 0x0B, + Branch = 0x0C, + Branch_If = 0x0D, + Branch_Table = 0x0E, + Return = 0x0F, + Call = 0x10, + Call_Indirect = 0x11, + Drop = 0x1A, + Select = 0x1B, + Select_T = 0x1C, + Local_Get = 0x20, + Local_Set = 0x21, + Local_Tee = 0x22, + Global_Get = 0x23, + Global_Set = 0x24, + Table_Get = 0x25, + Table_Set = 0x26, + I32_Load = 0x28, + I64_Load = 0x29, + F32_Load = 0x2A, + F64_Load = 0x2B, + I32_Load8_S = 0x2C, + I32_Load8_U = 0x2D, + I32_Load16_S = 0x2E, + I32_Load16_U = 0x2F, + I64_Load8_S = 0x30, + I64_Load8_U = 0x31, + I64_Load16_S = 0x32, + I64_Load16_U = 0x33, + I64_Load32_S = 0x34, + I64_Load32_U = 0x35, + I32_Store = 0x36, + I64_Store = 0x37, + F32_Store = 0x38, + F64_Store = 0x39, + I32_Store8 = 0x3A, + I32_Store16 = 0x3B, + I64_Store8 = 0x3C, + I64_Store16 = 0x3D, + I64_Store32 = 0x3E, + Memory_Size = 0x3F, + Memory_Grow = 0x40, + I32_Const = 0x41, + I64_Const = 0x42, + F32_Const = 0x43, + F64_Const = 0x44, + I32_Eqz = 0x45, + I32_Eq = 0x46, + I32_NE = 0x47, + I32_LT_S = 0x48, + I32_LT_U = 0x49, + I32_GT_S = 0x4A, + I32_GT_U = 0x4B, + I32_LE_S = 0x4C, + I32_LE_U = 0x4D, + I32_GE_S = 0x4E, + I32_GE_U = 0x4F, + I64_Eqz = 0x50, + I64_Eq = 0x51, + I64_NE = 0x52, + I64_LT_S = 0x53, + I64_LT_U = 0x54, + I64_GT_S = 0x55, + I64_GT_U = 0x56, + I64_LE_S = 0x57, + I64_LE_U = 0x58, + I64_GE_S = 0x59, + I64_GE_U = 0x5A, + F32_EQ = 0x5B, + F32_NE = 0x5C, + F32_LT = 0x5D, + F32_GT = 0x5E, + F32_LE = 0x5F, + F32_GE = 0x60, + F64_EQ = 0x61, + F64_NE = 0x62, + F64_LT = 0x63, + F64_GT = 0x64, + F64_LE = 0x65, + F64_GE = 0x66, + I32_Clz = 0x67, + I32_Ctz = 0x68, + I32_Popcnt = 0x69, + I32_Add = 0x6A, + I32_Sub = 0x6B, + I32_Mul = 0x6C, + I32_Div_S = 0x6D, + I32_Div_U = 0x6E, + I32_Rem_S = 0x6F, + I32_Rem_U = 0x70, + I32_And = 0x71, + I32_Or = 0x72, + I32_Xor = 0x73, + I32_Shl = 0x74, + I32_Shr_S = 0x75, + I32_Shr_U = 0x76, + I32_Rotl = 0x77, + I32_Rotr = 0x78, + I64_Clz = 0x79, + I64_Ctz = 0x7A, + I64_Popcnt = 0x7B, + I64_Add = 0x7C, + I64_Sub = 0x7D, + I64_Mul = 0x7E, + I64_Div_S = 0x7F, + I64_Div_U = 0x80, + I64_Rem_S = 0x81, + I64_Rem_U = 0x82, + I64_And = 0x83, + I64_Or = 0x84, + I64_Xor = 0x85, + I64_Shl = 0x86, + I64_Shr_S = 0x87, + I64_Shr_U = 0x88, + I64_Rotl = 0x89, + I64_Rotr = 0x8A, + F32_Abs = 0x8B, + F32_Neg = 0x8C, + F32_Ceil = 0x8D, + F32_Floor = 0x8E, + F32_Trunc = 0x8F, + F32_Nearest = 0x90, + F32_Sqrt = 0x91, + F32_Add = 0x92, + F32_Sub = 0x93, + F32_Mul = 0x94, + F32_Div = 0x95, + F32_Min = 0x96, + F32_Max = 0x97, + F32_Copysign = 0x98, + F64_Abs = 0x99, + F64_Neg = 0x9A, + F64_Ceil = 0x9B, + F64_Floor = 0x9C, + F64_Trunc = 0x9D, + F64_Nearest = 0x9E, + F64_Sqrt = 0x9F, + F64_Add = 0xA0, + F64_Sub = 0xA1, + F64_Mul = 0xA2, + F64_Div = 0xA3, + F64_Min = 0xA4, + F64_Max = 0xA5, + F64_Copysign = 0xA6, + I32_Wrap_I64 = 0xA7, + I32_Trunc_F32_S = 0xA8, + I32_Trunc_F32_U = 0xA9, + I32_Trunc_F64_S = 0xAA, + I32_Trunc_F64_U = 0xAB, + I64_Extend_I32_S = 0xAC, + I64_Extend_I32_U = 0xAD, + I64_Trunc_F32_S = 0xAE, + I64_Trunc_F32_U = 0xAF, + I64_Trunc_F64_S = 0xB0, + I64_Trunc_F64_U = 0xB1, + F32_Convert_I32_S = 0xB2, + F32_Convert_I32_U = 0xB3, + F32_Convert_I64_S = 0xB4, + F32_Convert_I64_U = 0xB5, + F32_Demote_F64 = 0xB6, + F64_Convert_I32_S = 0xB7, + F64_Convert_I32_U = 0xB8, + F64_Convert_I64_S = 0xB9, + F64_Convert_I64_U = 0xBA, + F64_Promote_F32 = 0xBB, + I32_Reinterpret_F32 = 0xBC, + I64_Reinterpret_F64 = 0xBD, + F32_Reinterpret_I32 = 0xBE, + F64_Reinterpret_I64 = 0xBF, + I32_Extend8_S = 0xC0, + I32_Extend16_S = 0xC1, + I64_Extend8_S = 0xC2, + I64_Extend16_S = 0xC3, + I64_Extend32_S = 0xC4, + Ref_Null = 0xD0, + Ref_Is_Null = 0xD1, + Ref_Func = 0xD2, + I32_Trunc_Sat_F32_S = 0xFC00, + I32_Trunc_Sat_F32_U = 0xFC01, + I32_Trunc_Sat_F64_S = 0xFC02, + I32_Trunc_Sat_F64_U = 0xFC03, + I64_Trunc_Sat_F32_S = 0xFC04, + I64_Trunc_Sat_F32_U = 0xFC05, + I64_Trunc_Sat_F64_S = 0xFC06, + I64_Trunc_Sat_F64_U = 0xFC07, + Memory_Init = 0xFC08, + Data_Drop = 0xFC09, + Memory_Copy = 0xFC0A, + Memory_Fill = 0xFC0B, + Table_Init = 0xFC0C, + Elem_Drop = 0xFC0D, + Table_Copy = 0xFC0E, + Table_Grow = 0xFC0F, + Table_Size = 0xFC10, + Table_Fill = 0xFC11, + V128_Load = 0xFD00, + V128_Load8x8_S = 0xFD01, + V128_Load8x8_U = 0xFD02, + V128_Load16x4_S = 0xFD03, + V128_Load16x4_U = 0xFD04, + V128_Load32x2_S = 0xFD05, + V128_Load32x2_U = 0xFD06, + V128_Load8_Splat = 0xFD07, + V128_Load16_Splat = 0xFD08, + V128_Load32_Splat = 0xFD09, + V128_Load64_Splat = 0xFD0A, + V128_Store = 0xFD0B, + V128_Const = 0xFD0C, + I8x16_Shuffle = 0xFD0D, + I8x16_Swizzle = 0xFD0E, + I8x16_Splat = 0xFD0F, + I16x8_Splat = 0xFD10, + I32x4_Splat = 0xFD11, + I64x2_Splat = 0xFD12, + F32x4_Splat = 0xFD13, + F64x2_Splat = 0xFD14, + I8x16_Extract_Lane_S = 0xFD15, + I8x16_Extract_Lane_U = 0xFD16, + I8x16_Replace_Lane = 0xFD17, + I16x8_Extract_Lane_S = 0xFD18, + I16x8_Extract_Lane_U = 0xFD19, + I16x8_Replace_Lane = 0xFD1A, + I32x4_Extract_Lane = 0xFD1B, + I32x4_Replace_Lane = 0xFD1C, + I64x2_Extract_Lane = 0xFD1D, + I64x2_Replace_Lane = 0xFD1E, + F32x4_Extract_Lane = 0xFD1F, + F32x4_Replace_Lane = 0xFD20, + F64x2_Extract_Lane = 0xFD21, + F64x2_Replace_Lane = 0xFD22, + I8x16_EQ = 0xFD23, + I8x16_NE = 0xFD24, + I8x16_LT_S = 0xFD25, + I8x16_LT_U = 0xFD26, + I8x16_GT_S = 0xFD27, + I8x16_GT_U = 0xFD28, + I8x16_LE_S = 0xFD29, + I8x16_LE_U = 0xFD2A, + I8x16_GE_S = 0xFD2B, + I8x16_GE_U = 0xFD2C, + I16x8_EQ = 0xFD2D, + I16x8_NE = 0xFD2E, + I16x8_LT_S = 0xFD2F, + I16x8_LT_U = 0xFD30, + I16x8_GT_S = 0xFD31, + I16x8_GT_U = 0xFD32, + I16x8_LE_S = 0xFD33, + I16x8_LE_U = 0xFD34, + I16x8_GE_S = 0xFD35, + I16x8_GE_U = 0xFD36, + I32x4_EQ = 0xFD37, + I32x4_NE = 0xFD38, + I32x4_LT_S = 0xFD39, + I32x4_LT_U = 0xFD3A, + I32x4_GT_S = 0xFD3B, + I32x4_GT_U = 0xFD3C, + I32x4_LE_S = 0xFD3D, + I32x4_LE_U = 0xFD3E, + I32x4_GE_S = 0xFD3F, + I32x4_GE_U = 0xFD40, + F32x4_EQ = 0xFD41, + F32x4_NE = 0xFD42, + F32x4_LT = 0xFD43, + F32x4_GT = 0xFD44, + F32x4_LE = 0xFD45, + F32x4_GE = 0xFD46, + F64x2_EQ = 0xFD47, + F64x2_NE = 0xFD48, + F64x2_LT = 0xFD49, + F64x2_GT = 0xFD4A, + F64x2_LE = 0xFD4B, + F64x2_GE = 0xFD4C, + V128_Not = 0xFD4D, + V128_And = 0xFD4E, + V128_AndNot = 0xFD4F, + V128_Or = 0xFD50, + V128_Xor = 0xFD51, + V128_Bitselect = 0xFD52, + V128_AnyTrue = 0xFD53, + V128_Load8_Lane = 0xFD54, + V128_Load16_Lane = 0xFD55, + V128_Load32_Lane = 0xFD56, + V128_Load64_Lane = 0xFD57, + V128_Store8_Lane = 0xFD58, + V128_Store16_Lane = 0xFD59, + V128_Store32_Lane = 0xFD5A, + V128_Store64_Lane = 0xFD5B, + V128_Load32_Zero = 0xFD5C, + V128_Load64_Zero = 0xFD5D, + F32x4_Demote_F64x2_Zero = 0xFD5E, + F64x2_Promote_Low_F32x4 = 0xFD5F, + I8x16_Abs = 0xFD60, + I8x16_Neg = 0xFD61, + I8x16_Popcnt = 0xFD62, + I8x16_AllTrue = 0xFD63, + I8x16_Bitmask = 0xFD64, + II8x16_Narrow_I16x8_S = 0xFD65, + II8x16_Narrow_I16x8_U = 0xFD66, + F32x4_Ceil = 0xFD67, + F32x4_Floor = 0xFD68, + F32x4_Trunc = 0xFD69, + F32x4_Nearest = 0xFD6A, + I8x16_Shl = 0xFD6B, + I8x16_Shr_S = 0xFD6C, + I8x16_Shr_U = 0xFD6D, + I8x16_Add = 0xFD6E, + I8x16_Add_Sat_S = 0xFD6F, + I8x16_Add_Sat_U = 0xFD70, + I8x16_Sub = 0xFD71, + I8x16_Sub_Sat_S = 0xFD72, + I8x16_Sub_Sat_U = 0xFD73, + F64x2_Ceil = 0xFD74, + F64x2_Floor = 0xFD75, + I8x16_Min_S = 0xFD76, + I8x16_Min_U = 0xFD77, + I8x16_Max_S = 0xFD78, + I8x16_Max_U = 0xFD79, + F64x2_Trunc = 0xFD7A, + I8x16_Avgr_U = 0xFD7B, + I16x8_Extadd_Pairwise_I8x16_S = 0xFD7C, + I16x8_Extadd_Pairwise_I8x16_U = 0xFD7D, + I32x4_Extadd_Pairwise_I16x8_S = 0xFD7E, + I32x4_Extadd_Pairwise_I16x8_U = 0xFD7F, + I16x8_Abs = 0xFD80, + I16x8_Neg = 0xFD81, + I16x8_Q15mulr_Sat_S = 0xFD82, + I16x8_AllTrue = 0xFD83, + I16x8_Bitmask = 0xFD84, + I16x8_Narrow_I32x4_S = 0xFD85, + I16x8_Narrow_I32x4_U = 0xFD86, + I16x8_Extend_Low_I8x16_S = 0xFD87, + I16x8_Extend_High_I8x16_S = 0xFD88, + I16x8_Extend_Low_I8x16_U = 0xFD89, + I16x8_Extend_High_I8x16_U = 0xFD8A, + I16x8_Shl = 0xFD8B, + I16x8_Shr_S = 0xFD8C, + I16x8_Shr_U = 0xFD8D, + I16x8_Add = 0xFD8E, + I16x8_Add_Sat_S = 0xFD8F, + I16x8_Add_Sat_U = 0xFD90, + I16x8_Sub = 0xFD91, + I16x8_Sub_Sat_S = 0xFD92, + I16x8_Sub_Sat_U = 0xFD93, + F64x2_Nearest = 0xFD94, + I16x8_Mul = 0xFD95, + I16x8_Min_S = 0xFD96, + I16x8_Min_U = 0xFD97, + I16x8_Max_S = 0xFD98, + I16x8_Max_U = 0xFD99, + I16x8_Avgr_U = 0xFD9B, + I16x8_Extmul_Low_I8x16_S = 0xFD9C, + I16x8_Extmul_High_I8x16_S = 0xFD9D, + I16x8_Extmul_Low_I8x16_U = 0xFD9E, + I16x8_Extmul_High_I8x16_U = 0xFD9F, + I32x4_Abs = 0xFDA0, + I32x4_Neg = 0xFDA1, + I32x4_AllTrue = 0xFDA3, + I32x4_Bitmask = 0xFDA4, + I32x4_Extend_Low_I16x8_S = 0xFDA7, + I32x4_Extend_High_I16x8_S = 0xFDA8, + I32x4_Extend_Low_I16x8_U = 0xFDA9, + I32x4_Extend_High_I16x8_U = 0xFDAA, + I32x4_Shl = 0xFDAB, + I32x4_Shr_S = 0xFDAC, + I32x4_Shr_U = 0xFDAD, + I32x4_Add = 0xFDAE, + I32x4_Sub = 0xFDB1, + I32x4_Mul = 0xFDB5, + I32x4_Min_S = 0xFDB6, + I32x4_Min_U = 0xFDB7, + I32x4_Max_S = 0xFDB8, + I32x4_Max_U = 0xFDB9, + I32x4_Dot_I16x8_S = 0xFDBA, + I32x4_Extmul_Low_I16x8_S = 0xFDBC, + I32x4_Extmul_High_I16x8_S = 0xFDBD, + I32x4_Extmul_Low_I16x8_U = 0xFDBE, + I32x4_Extmul_High_I16x8_U = 0xFDBF, + I64x2_Abs = 0xFDC0, + I64x2_Neg = 0xFDC1, + I64x2_AllTrue = 0xFDC3, + I64x2_Bitmask = 0xFDC4, + I64x2_Extend_Low_I32x4_S = 0xFDC7, + I64x2_Extend_High_I32x4_S = 0xFDC8, + I64x2_Extend_Low_I32x4_U = 0xFDC9, + I64x2_Extend_High_I32x4_U = 0xFDCA, + I64x2_Shl = 0xFDCB, + I64x2_Shr_S = 0xFDCC, + I64x2_Shr_U = 0xFDCD, + I64x2_Add = 0xFDCE, + I64x2_Sub = 0xFDD1, + I64x2_Mul = 0xFDD5, + I64x2_EQ = 0xFDD6, + I64x2_NE = 0xFDD7, + I64x2_LT_S = 0xFDD8, + I64x2_GT_S = 0xFDD9, + I64x2_LE_S = 0xFDDA, + I64x2_GE_S = 0xFDDB, + I64x2_Extmul_Low_I32x4_S = 0xFDDC, + I64x2_Extmul_High_I32x4_S = 0xFDDD, + I64x2_Extmul_Low_I32x4_U = 0xFDDE, + I64x2_Extmul_High_I32x4_U = 0xFDDF, + F32x4_Abs = 0xFDE0, + F32x4_Neg = 0xFDE1, + F32x4_Sqrt = 0xFDE3, + F32x4_Add = 0xFDE4, + F32x4_Sub = 0xFDE5, + F32x4_Mul = 0xFDE6, + F32x4_Div = 0xFDE7, + F32x4_Min = 0xFDE8, + F32x4_Max = 0xFDE9, + F32x4_PMin = 0xFDEA, + F32x4_PMax = 0xFDEB, + F64x2_Abs = 0xFDEC, + F64x2_Neg = 0xFDED, + F64x2_Sqrt = 0xFDEF, + F64x2_Add = 0xFDF0, + F64x2_Sub = 0xFDF1, + F64x2_Mul = 0xFDF2, + F64x2_Div = 0xFDF3, + F64x2_Min = 0xFDF4, + F64x2_Max = 0xFDF5, + F64x2_PMin = 0xFDF6, + F64x2_PMax = 0xFDF7, + F32x4_Trunc_Sat_F32x4_S = 0xFDF8, + F32x4_Trunc_Sat_F32x4_U = 0xFDF9, + F32x4_Convert_I32x4_S = 0xFDFA, + F32x4_Convert_I32x4_U = 0xFDFB, + I32x4_Trunc_Sat_F64x2_S_Zero = 0xFDFC, + I32x4_Trunc_Sat_F64x2_U_Zero = 0xFDFD, + F64x2_Convert_Low_I32x4_S = 0xFDFE, + F64x2_Convert_Low_I32x4_U = 0xFDFF, + + pub fn toOpcode(wasm: WasmOpcode) Opcode { + const opcode_int = @intFromEnum(wasm); + var opcode: Opcode = undefined; + if (opcode_int < ConversionTables.wasmOpcodeToOpcodeTable.len) { + opcode = ConversionTables.wasmOpcodeToOpcodeTable[opcode_int]; + } else if (opcode_int >= 0xFC00 and opcode_int < 0xFCD0) { + opcode = ConversionTables.wasmFCOpcodeToOpcodeTable[opcode_int - 0xFC00]; + } else { + opcode = ConversionTables.wasmFDOpcodeToOpcodeTable[opcode_int - 0xFD00]; + } + std.debug.assert(opcode != .Invalid); + return opcode; + } + + pub fn decode(reader: anytype) !WasmOpcode { + var byte = try reader.readByte(); + var wasm_op: WasmOpcode = undefined; + if (byte == 0xFC or byte == 0xFD) { + var type_opcode = try common.decodeLEB128(u32, reader); + if (type_opcode > std.math.maxInt(u8)) { + return error.MalformedIllegalOpcode; + } + var byte2 = @as(u8, @intCast(type_opcode)); + var extended: u16 = byte; + extended = extended << 8; + extended |= byte2; + + wasm_op = std.meta.intToEnum(WasmOpcode, extended) catch { + return error.MalformedIllegalOpcode; + }; + } else { + wasm_op = std.meta.intToEnum(WasmOpcode, byte) catch { + return error.MalformedIllegalOpcode; + }; + } + return wasm_op; + } +}; + +const ConversionTables = struct { + const wasmOpcodeToOpcodeTable = [_]Opcode{ + Opcode.Unreachable, // 0x00 + Opcode.Noop, // 0x01 + Opcode.Block, // 0x02 + Opcode.Loop, // 0x03 + Opcode.If, // 0x04 + Opcode.Else, // 0x05 + Opcode.Invalid, // 0x06 + Opcode.Invalid, // 0x07 + Opcode.Invalid, // 0x08 + Opcode.Invalid, // 0x09 + Opcode.Invalid, // 0x0A + Opcode.End, // 0x0B, + Opcode.Branch, // 0x0C + Opcode.Branch_If, // 0x0D + Opcode.Branch_Table, // 0x0E + Opcode.Return, // 0x0F + Opcode.Call, // 0x10 + Opcode.Call_Indirect, // 0x11 + Opcode.Invalid, // 0x12 + Opcode.Invalid, // 0x13 + Opcode.Invalid, // 0x14 + Opcode.Invalid, // 0x15 + Opcode.Invalid, // 0x16 + Opcode.Invalid, // 0x17 + Opcode.Invalid, // 0x18 + Opcode.Invalid, // 0x19 + Opcode.Drop, // 0x1A + Opcode.Select, // 0x1B + Opcode.Select_T, // 0x1C + Opcode.Invalid, // 0x1D + Opcode.Invalid, // 0x1E + Opcode.Invalid, // 0x1F + Opcode.Local_Get, // 0x20 + Opcode.Local_Set, // 0x21 + Opcode.Local_Tee, // 0x22 + Opcode.Global_Get, // 0x23 + Opcode.Global_Set, // 0x24 + Opcode.Table_Get, // 0x25 + Opcode.Table_Set, // 0x26 + Opcode.Invalid, // 0x27 + Opcode.I32_Load, // 0x28 + Opcode.I64_Load, // 0x29 + Opcode.F32_Load, // 0x2A + Opcode.F64_Load, // 0x2B + Opcode.I32_Load8_S, // 0x2C + Opcode.I32_Load8_U, // 0x2D + Opcode.I32_Load16_S, // 0x2E + Opcode.I32_Load16_U, // 0x2F + Opcode.I64_Load8_S, // 0x30 + Opcode.I64_Load8_U, // 0x31 + Opcode.I64_Load16_S, // 0x32 + Opcode.I64_Load16_U, // 0x33 + Opcode.I64_Load32_S, // 0x34 + Opcode.I64_Load32_U, // 0x35 + Opcode.I32_Store, // 0x36 + Opcode.I64_Store, // 0x37 + Opcode.F32_Store, // 0x38 + Opcode.F64_Store, // 0x39 + Opcode.I32_Store8, // 0x3A + Opcode.I32_Store16, // 0x3B + Opcode.I64_Store8, // 0x3C + Opcode.I64_Store16, // 0x3D + Opcode.I64_Store32, // 0x3E + Opcode.Memory_Size, // 0x3F + Opcode.Memory_Grow, // 0x40 + Opcode.I32_Const, // 0x41 + Opcode.I64_Const, // 0x42 + Opcode.F32_Const, // 0x43 + Opcode.F64_Const, // 0x44 + Opcode.I32_Eqz, // 0x45 + Opcode.I32_Eq, // 0x46 + Opcode.I32_NE, // 0x47 + Opcode.I32_LT_S, // 0x48 + Opcode.I32_LT_U, // 0x49 + Opcode.I32_GT_S, // 0x4A + Opcode.I32_GT_U, // 0x4B + Opcode.I32_LE_S, // 0x4C + Opcode.I32_LE_U, // 0x4D + Opcode.I32_GE_S, // 0x4E + Opcode.I32_GE_U, // 0x4F + Opcode.I64_Eqz, // 0x50 + Opcode.I64_Eq, // 0x51 + Opcode.I64_NE, // 0x52 + Opcode.I64_LT_S, // 0x53 + Opcode.I64_LT_U, // 0x54 + Opcode.I64_GT_S, // 0x55 + Opcode.I64_GT_U, // 0x56 + Opcode.I64_LE_S, // 0x57 + Opcode.I64_LE_U, // 0x58 + Opcode.I64_GE_S, // 0x59 + Opcode.I64_GE_U, // 0x5A + Opcode.F32_EQ, // 0x5B + Opcode.F32_NE, // 0x5C + Opcode.F32_LT, // 0x5D + Opcode.F32_GT, // 0x5E + Opcode.F32_LE, // 0x5F + Opcode.F32_GE, // 0x60 + Opcode.F64_EQ, // 0x61 + Opcode.F64_NE, // 0x62 + Opcode.F64_LT, // 0x63 + Opcode.F64_GT, // 0x64 + Opcode.F64_LE, // 0x65 + Opcode.F64_GE, // 0x66 + Opcode.I32_Clz, // 0x67 + Opcode.I32_Ctz, // 0x68 + Opcode.I32_Popcnt, // 0x69 + Opcode.I32_Add, // 0x6A + Opcode.I32_Sub, // 0x6B + Opcode.I32_Mul, // 0x6C + Opcode.I32_Div_S, // 0x6D + Opcode.I32_Div_U, // 0x6E + Opcode.I32_Rem_S, // 0x6F + Opcode.I32_Rem_U, // 0x70 + Opcode.I32_And, // 0x71 + Opcode.I32_Or, // 0x72 + Opcode.I32_Xor, // 0x73 + Opcode.I32_Shl, // 0x74 + Opcode.I32_Shr_S, // 0x75 + Opcode.I32_Shr_U, // 0x76 + Opcode.I32_Rotl, // 0x77 + Opcode.I32_Rotr, // 0x78 + Opcode.I64_Clz, // 0x79 + Opcode.I64_Ctz, // 0x7A + Opcode.I64_Popcnt, // 0x7B + Opcode.I64_Add, // 0x7C + Opcode.I64_Sub, // 0x7D + Opcode.I64_Mul, // 0x7E + Opcode.I64_Div_S, // 0x7F + Opcode.I64_Div_U, // 0x80 + Opcode.I64_Rem_S, // 0x81 + Opcode.I64_Rem_U, // 0x82 + Opcode.I64_And, // 0x83 + Opcode.I64_Or, // 0x84 + Opcode.I64_Xor, // 0x85 + Opcode.I64_Shl, // 0x86 + Opcode.I64_Shr_S, // 0x87 + Opcode.I64_Shr_U, // 0x88 + Opcode.I64_Rotl, // 0x89 + Opcode.I64_Rotr, // 0x8A + Opcode.F32_Abs, // 0x8B + Opcode.F32_Neg, // 0x8C + Opcode.F32_Ceil, // 0x8D + Opcode.F32_Floor, // 0x8E + Opcode.F32_Trunc, // 0x8F + Opcode.F32_Nearest, // 0x90 + Opcode.F32_Sqrt, // 0x91 + Opcode.F32_Add, // 0x92 + Opcode.F32_Sub, // 0x93 + Opcode.F32_Mul, // 0x94 + Opcode.F32_Div, // 0x95 + Opcode.F32_Min, // 0x96 + Opcode.F32_Max, // 0x97 + Opcode.F32_Copysign, // 0x98 + Opcode.F64_Abs, // 0x99 + Opcode.F64_Neg, // 0x9A + Opcode.F64_Ceil, // 0x9B + Opcode.F64_Floor, // 0x9C + Opcode.F64_Trunc, // 0x9D + Opcode.F64_Nearest, // 0x9E + Opcode.F64_Sqrt, // 0x9F + Opcode.F64_Add, // 0xA0 + Opcode.F64_Sub, // 0xA1 + Opcode.F64_Mul, // 0xA2 + Opcode.F64_Div, // 0xA3 + Opcode.F64_Min, // 0xA4 + Opcode.F64_Max, // 0xA5 + Opcode.F64_Copysign, // 0xA6 + Opcode.I32_Wrap_I64, // 0xA7 + Opcode.I32_Trunc_F32_S, // 0xA8 + Opcode.I32_Trunc_F32_U, // 0xA9 + Opcode.I32_Trunc_F64_S, // 0xAA + Opcode.I32_Trunc_F64_U, // 0xAB + Opcode.I64_Extend_I32_S, // 0xAC + Opcode.I64_Extend_I32_U, // 0xAD + Opcode.I64_Trunc_F32_S, // 0xAE + Opcode.I64_Trunc_F32_U, // 0xAF + Opcode.I64_Trunc_F64_S, // 0xB0 + Opcode.I64_Trunc_F64_U, // 0xB1 + Opcode.F32_Convert_I32_S, // 0xB2 + Opcode.F32_Convert_I32_U, // 0xB3 + Opcode.F32_Convert_I64_S, // 0xB4 + Opcode.F32_Convert_I64_U, // 0xB5 + Opcode.F32_Demote_F64, // 0xB6 + Opcode.F64_Convert_I32_S, // 0xB7 + Opcode.F64_Convert_I32_U, // 0xB8 + Opcode.F64_Convert_I64_S, // 0xB9 + Opcode.F64_Convert_I64_U, // 0xBA + Opcode.F64_Promote_F32, // 0xBB + Opcode.I32_Reinterpret_F32, // 0xBC + Opcode.I64_Reinterpret_F64, // 0xBD + Opcode.F32_Reinterpret_I32, // 0xBE + Opcode.F64_Reinterpret_I64, // 0xBF + Opcode.I32_Extend8_S, // 0xC0 + Opcode.I32_Extend16_S, // 0xC1 + Opcode.I64_Extend8_S, // 0xC2 + Opcode.I64_Extend16_S, // 0xC3 + Opcode.I64_Extend32_S, // 0xC4 + Opcode.Invalid, // 0xC5 + Opcode.Invalid, // 0xC6 + Opcode.Invalid, // 0xC7 + Opcode.Invalid, // 0xC8 + Opcode.Invalid, // 0xC9 + Opcode.Invalid, // 0xCA + Opcode.Invalid, // 0xCB + Opcode.Invalid, // 0xCC + Opcode.Invalid, // 0xCD + Opcode.Invalid, // 0xCE + Opcode.Invalid, // 0xCF + Opcode.Ref_Null, // 0xD0 + Opcode.Ref_Is_Null, // 0xD1 + Opcode.Ref_Func, // 0xD2 + }; + + const wasmFCOpcodeToOpcodeTable = [_]Opcode{ + Opcode.I32_Trunc_Sat_F32_S, // 0xFC00 + Opcode.I32_Trunc_Sat_F32_U, // 0xFC01 + Opcode.I32_Trunc_Sat_F64_S, // 0xFC02 + Opcode.I32_Trunc_Sat_F64_U, // 0xFC03 + Opcode.I64_Trunc_Sat_F32_S, // 0xFC04 + Opcode.I64_Trunc_Sat_F32_U, // 0xFC05 + Opcode.I64_Trunc_Sat_F64_S, // 0xFC06 + Opcode.I64_Trunc_Sat_F64_U, // 0xFC07 + Opcode.Memory_Init, // 0xFC08 + Opcode.Data_Drop, // 0xFC09 + Opcode.Memory_Copy, // 0xFC0A + Opcode.Memory_Fill, // 0xFC0B + Opcode.Table_Init, // 0xFC0C + Opcode.Elem_Drop, // 0xFC0D + Opcode.Table_Copy, // 0xFC0E + Opcode.Table_Grow, // 0xFC0F + Opcode.Table_Size, // 0xFC10 + Opcode.Table_Fill, // 0xFC11 + }; + + const wasmFDOpcodeToOpcodeTable = [_]Opcode{ + Opcode.V128_Load, // 0xFD00 + Opcode.V128_Load8x8_S, // 0xFD01 + Opcode.V128_Load8x8_U, // 0xFD02 + Opcode.V128_Load16x4_S, // 0xFD03 + Opcode.V128_Load16x4_U, // 0xFD04 + Opcode.V128_Load32x2_S, // 0xFD05 + Opcode.V128_Load32x2_U, // 0xFD06 + Opcode.V128_Load8_Splat, // 0xFD07 + Opcode.V128_Load16_Splat, // 0xFD08 + Opcode.V128_Load32_Splat, // 0xFD09 + Opcode.V128_Load64_Splat, // 0xFD0A + Opcode.V128_Store, // 0xFD0B + Opcode.V128_Const, // 0xFD0C + Opcode.I8x16_Shuffle, // 0xFD0D + Opcode.I8x16_Swizzle, // 0xFD0E + Opcode.I8x16_Splat, // 0xFD0F + Opcode.I16x8_Splat, // 0xFD10 + Opcode.I32x4_Splat, // 0xFD11 + Opcode.I64x2_Splat, // 0xFD12 + Opcode.F32x4_Splat, // 0xFD13 + Opcode.F64x2_Splat, // 0xFD14 + Opcode.I8x16_Extract_Lane_S, // 0xFD15 + Opcode.I8x16_Extract_Lane_U, // 0xFD16 + Opcode.I8x16_Replace_Lane, // 0xFD17 + Opcode.I16x8_Extract_Lane_S, // 0xFD18 + Opcode.I16x8_Extract_Lane_U, // 0xFD19 + Opcode.I16x8_Replace_Lane, // 0xFD1A + Opcode.I32x4_Extract_Lane, // 0xFD1B + Opcode.I32x4_Replace_Lane, // 0xFD1C + Opcode.I64x2_Extract_Lane, // 0xFD1D + Opcode.I64x2_Replace_Lane, // 0xFD1E + Opcode.F32x4_Extract_Lane, // 0xFD1F + Opcode.F32x4_Replace_Lane, // 0xFD20 + Opcode.F64x2_Extract_Lane, // 0xFD21 + Opcode.F64x2_Replace_Lane, // 0xFD22 + Opcode.I8x16_EQ, // 0xFD23 + Opcode.I8x16_NE, // 0xFD24 + Opcode.I8x16_LT_S, // 0xFD25 + Opcode.I8x16_LT_U, // 0xFD26 + Opcode.I8x16_GT_S, // 0xFD27 + Opcode.I8x16_GT_U, // 0xFD28 + Opcode.I8x16_LE_S, // 0xFD29 + Opcode.I8x16_LE_U, // 0xFD2A + Opcode.I8x16_GE_S, // 0xFD2B + Opcode.I8x16_GE_U, // 0xFD2C + Opcode.I16x8_EQ, // 0xFD2D + Opcode.I16x8_NE, // 0xFD2E + Opcode.I16x8_LT_S, // 0xFD2F + Opcode.I16x8_LT_U, // 0xFD30 + Opcode.I16x8_GT_S, // 0xFD31 + Opcode.I16x8_GT_U, // 0xFD32 + Opcode.I16x8_LE_S, // 0xFD33 + Opcode.I16x8_LE_U, // 0xFD34 + Opcode.I16x8_GE_S, // 0xFD35 + Opcode.I16x8_GE_U, // 0xFD36 + Opcode.I32x4_EQ, // 0xFD37 + Opcode.I32x4_NE, // 0xFD38 + Opcode.I32x4_LT_S, // 0xFD39 + Opcode.I32x4_LT_U, // 0xFD3A + Opcode.I32x4_GT_S, // 0xFD3B + Opcode.I32x4_GT_U, // 0xFD3C + Opcode.I32x4_LE_S, // 0xFD3D + Opcode.I32x4_LE_U, // 0xFD3E + Opcode.I32x4_GE_S, // 0xFD3F + Opcode.I32x4_GE_U, // 0xFD40 + Opcode.F32x4_EQ, // 0xFD41 + Opcode.F32x4_NE, // 0xFD42 + Opcode.F32x4_LT, // 0xFD43 + Opcode.F32x4_GT, // 0xFD44 + Opcode.F32x4_LE, // 0xFD45 + Opcode.F32x4_GE, // 0xFD46 + Opcode.F64x2_EQ, // 0xFD47 + Opcode.F64x2_NE, // 0xFD48 + Opcode.F64x2_LT, // 0xFD49 + Opcode.F64x2_GT, // 0xFD4A + Opcode.F64x2_LE, // 0xFD4B + Opcode.F64x2_GE, // 0xFD4C + Opcode.V128_Not, // 0xFD4D + Opcode.V128_And, // 0xFD4E + Opcode.V128_AndNot, // 0xFD4F + Opcode.V128_Or, // 0xFD50 + Opcode.V128_Xor, // 0xFD51 + Opcode.V128_Bitselect, // 0xFD52 + Opcode.V128_AnyTrue, // 0xFD53 + Opcode.V128_Load8_Lane, // 0xFD54 + Opcode.V128_Load16_Lane, // 0xFD55 + Opcode.V128_Load32_Lane, // 0xFD56 + Opcode.V128_Load64_Lane, // 0xFD57 + Opcode.V128_Store8_Lane, // 0xFD58 + Opcode.V128_Store16_Lane, // 0xFD59 + Opcode.V128_Store32_Lane, // 0xFD5A + Opcode.V128_Store64_Lane, // 0xFD5B + Opcode.V128_Load32_Zero, // 0xFD5C + Opcode.V128_Load64_Zero, // 0xFD5D + Opcode.F32x4_Demote_F64x2_Zero, // 0xFD5E + Opcode.F64x2_Promote_Low_F32x4, // 0xFD5F + Opcode.I8x16_Abs, // 0xFD60 + Opcode.I8x16_Neg, // 0xFD61 + Opcode.I8x16_Popcnt, // 0xFD62 + Opcode.I8x16_AllTrue, // 0xFD63 + Opcode.I8x16_Bitmask, // 0xFD64 + Opcode.I8x16_Narrow_I16x8_S, // 0xFD65 + Opcode.I8x16_Narrow_I16x8_U, // 0xFD66 + Opcode.F32x4_Ceil, // 0xFD67 + Opcode.F32x4_Floor, // 0xFD68 + Opcode.F32x4_Trunc, // 0xFD69 + Opcode.F32x4_Nearest, // 0xFD6A + Opcode.I8x16_Shl, // 0xFD6B + Opcode.I8x16_Shr_S, // 0xFD6C + Opcode.I8x16_Shr_U, // 0xFD6D + Opcode.I8x16_Add, // 0xFD6E + Opcode.I8x16_Add_Sat_S, // 0xFD6F + Opcode.I8x16_Add_Sat_U, // 0xFD70 + Opcode.I8x16_Sub, // 0xFD71 + Opcode.I8x16_Sub_Sat_S, // 0xFD72 + Opcode.I8x16_Sub_Sat_U, // 0xFD73 + Opcode.F64x2_Ceil, // 0xFD74 + Opcode.F64x2_Floor, // 0xFD75 + Opcode.I8x16_Min_S, // 0xFD76 + Opcode.I8x16_Min_U, // 0xFD77 + Opcode.I8x16_Max_S, // 0xFD78 + Opcode.I8x16_Max_U, // 0xFD79 + Opcode.F64x2_Trunc, // 0xFD7A + Opcode.I8x16_Avgr_U, // 0xFD7B + Opcode.I16x8_Extadd_Pairwise_I8x16_S, // 0xFD7C + Opcode.I16x8_Extadd_Pairwise_I8x16_U, // 0xFD7D + Opcode.I32x4_Extadd_Pairwise_I16x8_S, // 0xFD7E + Opcode.I32x4_Extadd_Pairwise_I16x8_U, // 0xFD7F + Opcode.I16x8_Abs, // 0xFD80 + Opcode.I16x8_Neg, // 0xFD81 + Opcode.I16x8_Q15mulr_Sat_S, // 0xFD82 + Opcode.I16x8_AllTrue, // 0xFD83 + Opcode.I16x8_Bitmask, // 0xFD84 + Opcode.I16x8_Narrow_I32x4_S, // 0xFD85 + Opcode.I16x8_Narrow_I32x4_U, // 0xFD86 + Opcode.I16x8_Extend_Low_I8x16_S, // 0xFD87 + Opcode.I16x8_Extend_High_I8x16_S, // 0xFD88 + Opcode.I16x8_Extend_Low_I8x16_U, // 0xFD89 + Opcode.I16x8_Extend_High_I8x16_U, // 0xFD8A + Opcode.I16x8_Shl, // 0xFD8B + Opcode.I16x8_Shr_S, // 0xFD8C + Opcode.I16x8_Shr_U, // 0xFD8D + Opcode.I16x8_Add, // 0xFD8E + Opcode.I16x8_Add_Sat_S, // 0xFD8F + Opcode.I16x8_Add_Sat_U, // 0xFD90 + Opcode.I16x8_Sub, // 0xFD91 + Opcode.I16x8_Sub_Sat_S, // 0xFD92 + Opcode.I16x8_Sub_Sat_U, // 0xFD93 + Opcode.F64x2_Nearest, // 0xFD94 + Opcode.I16x8_Mul, // 0xFD95 + Opcode.I16x8_Min_S, // 0xFD96 + Opcode.I16x8_Min_U, // 0xFD97 + Opcode.I16x8_Max_S, // 0xFD98 + Opcode.I16x8_Max_U, // 0xFD99 + Opcode.Invalid, // 0xFD9A + Opcode.I16x8_Avgr_U, // 0xFD9B + Opcode.I16x8_Extmul_Low_I8x16_S, // 0xFD9C + Opcode.I16x8_Extmul_High_I8x16_S, // 0xFD9D + Opcode.I16x8_Extmul_Low_I8x16_U, // 0xFD9E + Opcode.I16x8_Extmul_High_I8x16_U, // 0xFD9F + Opcode.I32x4_Abs, // 0xFDA0 + Opcode.I32x4_Neg, // 0xFDA1 + Opcode.Invalid, // 0xFDA2 + Opcode.I32x4_AllTrue, // 0xFDA3 + Opcode.I32x4_Bitmask, // 0xFDA4 + Opcode.Invalid, // 0xFDA5 + Opcode.Invalid, // 0xFDA6 + Opcode.I32x4_Extend_Low_I16x8_S, // 0xFDA7 + Opcode.I32x4_Extend_High_I16x8_S, // 0xFDA8 + Opcode.I32x4_Extend_Low_I16x8_U, // 0xFDA9 + Opcode.I32x4_Extend_High_I16x8_U, // 0xFDAA + Opcode.I32x4_Shl, // 0xFDAB + Opcode.I32x4_Shr_S, // 0xFDAC + Opcode.I32x4_Shr_U, // 0xFDAD + Opcode.I32x4_Add, // 0xFDAE + Opcode.Invalid, // 0xFDAF + Opcode.Invalid, // 0xFDB0 + Opcode.I32x4_Sub, // 0xFDB1 + Opcode.Invalid, // 0xFDB2 + Opcode.Invalid, // 0xFDB3 + Opcode.Invalid, // 0xFDB4 + Opcode.I32x4_Mul, // 0xFDB5 + Opcode.I32x4_Min_S, // 0xFDB6 + Opcode.I32x4_Min_U, // 0xFDB7 + Opcode.I32x4_Max_S, // 0xFDB8 + Opcode.I32x4_Max_U, // 0xFDB9 + Opcode.I32x4_Dot_I16x8_S, // 0xFDBA + Opcode.Invalid, // 0xFDBB + Opcode.I32x4_Extmul_Low_I16x8_S, // 0xFDBC + Opcode.I32x4_Extmul_High_I16x8_S, // 0xFDBD + Opcode.I32x4_Extmul_Low_I16x8_U, // 0xFDBE + Opcode.I32x4_Extmul_High_I16x8_U, // 0xFDBF + Opcode.I64x2_Abs, // 0xFDC0 + Opcode.I64x2_Neg, // 0xFDC1 + Opcode.Invalid, // 0xFDC2 + Opcode.I64x2_AllTrue, // 0xFDC3 + Opcode.I64x2_Bitmask, // 0xFDC4 + Opcode.Invalid, // 0xFDC5 + Opcode.Invalid, // 0xFDC6 + Opcode.I64x2_Extend_Low_I32x4_S, // 0xFDC7 + Opcode.I64x2_Extend_High_I32x4_S, // 0xFDC8 + Opcode.I64x2_Extend_Low_I32x4_U, // 0xFDC9 + Opcode.I64x2_Extend_High_I32x4_U, // 0xFDCA + Opcode.I64x2_Shl, // 0xFDCB + Opcode.I64x2_Shr_S, // 0xFDCC + Opcode.I64x2_Shr_U, // 0xFDCD + Opcode.I64x2_Add, // 0xFDCE + Opcode.Invalid, // 0xFDCF + Opcode.Invalid, // 0xFDD0 + Opcode.I64x2_Sub, // 0xFDD1 + Opcode.Invalid, // 0xFDD2 + Opcode.Invalid, // 0xFDD3 + Opcode.Invalid, // 0xFDD4 + Opcode.I64x2_Mul, // 0xFDD5 + Opcode.I64x2_EQ, // 0xFDD6 + Opcode.I64x2_NE, // 0xFDD7 + Opcode.I64x2_LT_S, // 0xFDD8 + Opcode.I64x2_GT_S, // 0xFDD9 + Opcode.I64x2_LE_S, // 0xFDDA + Opcode.I64x2_GE_S, // 0xFDDB + Opcode.I64x2_Extmul_Low_I32x4_S, // 0xFDDC + Opcode.I64x2_Extmul_High_I32x4_S, // 0xFDDD + Opcode.I64x2_Extmul_Low_I32x4_U, // 0xFDDE + Opcode.I64x2_Extmul_High_I32x4_U, // 0xFDDF + Opcode.F32x4_Abs, // 0xFDE0 + Opcode.F32x4_Neg, // 0xFDE1 + Opcode.Invalid, // 0xFDE2 + Opcode.F32x4_Sqrt, // 0xFDE3 + Opcode.F32x4_Add, // 0xFDE4 + Opcode.F32x4_Sub, // 0xFDE5 + Opcode.F32x4_Mul, // 0xFDE6 + Opcode.F32x4_Div, // 0xFDE7 + Opcode.F32x4_Min, // 0xFDE8 + Opcode.F32x4_Max, // 0xFDE9 + Opcode.F32x4_PMin, // 0xFDEA + Opcode.F32x4_PMax, // 0xFDEB + Opcode.F64x2_Abs, // 0xFDEC + Opcode.F64x2_Neg, // 0xFDED + Opcode.Invalid, // 0xFDEE + Opcode.F64x2_Sqrt, // 0xFDEF + Opcode.F64x2_Add, // 0xFDF0 + Opcode.F64x2_Sub, // 0xFDF1 + Opcode.F64x2_Mul, // 0xFDF2 + Opcode.F64x2_Div, // 0xFDF3 + Opcode.F64x2_Min, // 0xFDF4 + Opcode.F64x2_Max, // 0xFDF5 + Opcode.F64x2_PMin, // 0xFDF6 + Opcode.F64x2_PMax, // 0xFDF7 + Opcode.F32x4_Trunc_Sat_F32x4_S, // 0xFDF8 + Opcode.F32x4_Trunc_Sat_F32x4_U, // 0xFDF9 + Opcode.F32x4_Convert_I32x4_S, // 0xFDFA + Opcode.F32x4_Convert_I32x4_U, // 0xFDFB + Opcode.I32x4_Trunc_Sat_F64x2_S_Zero, // 0xFDFC + Opcode.I32x4_Trunc_Sat_F64x2_U_Zero, // 0xFDFD + Opcode.F64x2_Convert_Low_I32x4_S, // 0xFDFE + Opcode.F64x2_Convert_Low_I32x4_U, // 0xFDFF + }; +}; diff --git a/src/ext/bytebox/src/stringpool.zig b/src/ext/bytebox/src/stringpool.zig new file mode 100644 index 00000000..d2566bef --- /dev/null +++ b/src/ext/bytebox/src/stringpool.zig @@ -0,0 +1,120 @@ +const std = @import("std"); +const StableArray = @import("zig-stable-array/stable_array.zig").StableArray; + +const hashString = std.hash_map.hashString; +const StringHashLookupTable = std.hash_map.AutoHashMap(u64, usize); + +const StringPool = @This(); + +buffer: StableArray(u8), +lookup: StringHashLookupTable, + +const StringLenType = u16; + +pub const PutError = error{StringLengthTooLong}; + +pub fn init(max_stringpool_bytes: usize, allocator: std.mem.Allocator) StringPool { + return StringPool{ + .buffer = StableArray(u8).init(max_stringpool_bytes), + .lookup = StringHashLookupTable.init(allocator), + }; +} + +pub fn deinit(self: *StringPool) void { + self.buffer.deinit(); + self.lookup.deinit(); +} + +pub fn put(self: *StringPool, str: []const u8) ![]const u8 { + if (str.len > std.math.maxInt(StringLenType)) { + return error.StringLengthTooLong; + } + + const hash: u64 = hashString(str); + + // alignment requirements for StringLenType may require the buffer to be 1 byte larger than string size + sizeOf(StringLenType) + // so take care not to include the final byte in the string + size byte buffer + const string_and_size_num_bytes: usize = str.len + @sizeOf(StringLenType); + const alloc_size = std.mem.alignForward(usize, string_and_size_num_bytes, @alignOf(StringLenType)); + const str_offset_begin: usize = self.buffer.items.len; + const str_offset_end: usize = str_offset_begin + string_and_size_num_bytes; + const aligned_buffer_end: usize = str_offset_begin + alloc_size; + + try self.buffer.resize(aligned_buffer_end); + try self.lookup.put(hash, str_offset_begin); + + var bytes: []u8 = self.buffer.items[str_offset_begin..str_offset_end]; + var str_len: *StringLenType = @alignCast(@ptrCast(bytes.ptr)); + str_len.* = @as(StringLenType, @intCast(str.len)); + var str_bytes: []u8 = bytes[@sizeOf(StringLenType)..]; + std.mem.copy(u8, str_bytes, str); + + return str_bytes; +} + +pub fn find(self: *StringPool, str: []const u8) ?[]const u8 { + const hash: u64 = hashString(str); + + if (self.lookup.get(hash)) |string_bytes_begin| { + var str_bytes: [*]u8 = self.buffer.items[string_bytes_begin..].ptr; + var str_len: *StringLenType = @alignCast(@ptrCast(str_bytes)); + var pooled_str: []u8 = str_bytes[@sizeOf(StringLenType) .. @sizeOf(StringLenType) + str_len.*]; + return pooled_str; + } + + return null; +} + +pub fn findOrPut(self: *StringPool, str: []const u8) ![]const u8 { + if (self.find(str)) |found| { + return found; + } + + return try self.put(str); +} + +test "basic" { + const test_str: []const u8 = "test"; + const test1_str: []const u8 = "test"; + const test2_str: []const u8 = "test2"; + const long_str: []const u8 = "a very long string that has no end repeated many times! a very long string that has no end repeated many times! a very long string that has no end repeated many times!"; + + var pool = StringPool.init(4096, std.testing.allocator); + defer pool.deinit(); + + const test_str_added = try pool.put(test_str); + const test1_str_added = try pool.put(test1_str); + const test2_str_added = try pool.put(test2_str); + const long_str_added = try pool.put(long_str); + + try std.testing.expect(test_str_added.ptr != test_str.ptr); + try std.testing.expect(test1_str_added.ptr != test1_str.ptr); + try std.testing.expect(test2_str_added.ptr != test2_str.ptr); + try std.testing.expect(long_str_added.ptr != long_str.ptr); + + const test_str_found = pool.find(test_str); + const test1_str_found = pool.find(test1_str); + const test2_str_found = pool.find(test2_str); + const long_str_found = pool.find(long_str); + + try std.testing.expect(test_str_found != null); + try std.testing.expect(test1_str_found != null); + try std.testing.expect(test2_str_found != null); + try std.testing.expect(long_str_found != null); + + try std.testing.expect(test_str_found.?.ptr != test_str.ptr); + try std.testing.expect(test1_str_found.?.ptr != test1_str.ptr); + try std.testing.expect(test2_str_found.?.ptr != test2_str.ptr); + try std.testing.expect(long_str_found.?.ptr != long_str.ptr); + + std.debug.print("found: {s}, existing: {s}\n", .{ test_str_found.?, test_str }); + + try std.testing.expect(std.mem.eql(u8, test_str_found.?, test_str)); + try std.testing.expect(std.mem.eql(u8, test1_str_found.?, test1_str)); + try std.testing.expect(std.mem.eql(u8, test2_str_found.?, test2_str)); + try std.testing.expect(std.mem.eql(u8, long_str_found.?, long_str)); + + var lazyadd_str1 = try pool.findOrPut("lazy put"); + var lazyadd_str2 = try pool.findOrPut("lazy put"); + try std.testing.expect(lazyadd_str1.ptr == lazyadd_str2.ptr); +} diff --git a/src/ext/bytebox/src/tests.zig b/src/ext/bytebox/src/tests.zig new file mode 100644 index 00000000..2258a3e0 --- /dev/null +++ b/src/ext/bytebox/src/tests.zig @@ -0,0 +1,118 @@ +const std = @import("std"); +const testing = std.testing; +const expectEqual = testing.expectEqual; + +const core = @import("core.zig"); +const Limits = core.Limits; +const MemoryInstance = core.MemoryInstance; + +test "StackVM.Integration" { + const wasm_filepath = "zig-out/lib/mandelbrot.wasm"; + + var allocator = std.testing.allocator; + + var cwd = std.fs.cwd(); + var wasm_data: []u8 = try cwd.readFileAlloc(allocator, wasm_filepath, 1024 * 1024 * 128); + defer allocator.free(wasm_data); + + const module_def_opts = core.ModuleDefinitionOpts{ + .debug_name = std.fs.path.basename(wasm_filepath), + }; + var module_def = try core.createModuleDefinition(allocator, module_def_opts); + defer module_def.destroy(); + + try module_def.decode(wasm_data); + + var module_inst = try core.createModuleInstance(.Stack, module_def, allocator); + defer module_inst.destroy(); +} + +test "MemoryInstance.init" { + { + const limits = Limits{ + .min = 0, + .max = null, + }; + var memory = MemoryInstance.init(limits, null); + defer memory.deinit(); + try expectEqual(memory.limits.min, 0); + try expectEqual(memory.limits.max, MemoryInstance.k_max_pages); + try expectEqual(memory.size(), 0); + try expectEqual(memory.mem.Internal.items.len, 0); + } + + { + const limits = Limits{ + .min = 25, + .max = 25, + }; + var memory = MemoryInstance.init(limits, null); + defer memory.deinit(); + try expectEqual(memory.limits.min, 0); + try expectEqual(memory.limits.max, limits.max); + try expectEqual(memory.mem.Internal.items.len, 0); + } +} + +test "MemoryInstance.Internal.grow" { + { + const limits = Limits{ + .min = 0, + .max = null, + }; + var memory = MemoryInstance.init(limits, null); + defer memory.deinit(); + try expectEqual(memory.grow(0), true); + try expectEqual(memory.grow(1), true); + try expectEqual(memory.size(), 1); + try expectEqual(memory.grow(1), true); + try expectEqual(memory.size(), 2); + try expectEqual(memory.grow(MemoryInstance.k_max_pages - memory.size()), true); + try expectEqual(memory.size(), MemoryInstance.k_max_pages); + } + + { + const limits = Limits{ + .min = 0, + .max = 25, + }; + var memory = MemoryInstance.init(limits, null); + defer memory.deinit(); + try expectEqual(memory.grow(25), true); + try expectEqual(memory.size(), 25); + try expectEqual(memory.grow(1), false); + try expectEqual(memory.size(), 25); + } +} + +test "MemoryInstance.Internal.growAbsolute" { + { + const limits = Limits{ + .min = 0, + .max = null, + }; + var memory = MemoryInstance.init(limits, null); + defer memory.deinit(); + try expectEqual(memory.growAbsolute(0), true); + try expectEqual(memory.size(), 0); + try expectEqual(memory.growAbsolute(1), true); + try expectEqual(memory.size(), 1); + try expectEqual(memory.growAbsolute(5), true); + try expectEqual(memory.size(), 5); + try expectEqual(memory.growAbsolute(MemoryInstance.k_max_pages), true); + try expectEqual(memory.size(), MemoryInstance.k_max_pages); + } + + { + const limits = Limits{ + .min = 0, + .max = 25, + }; + var memory = MemoryInstance.init(limits, null); + defer memory.deinit(); + try expectEqual(memory.growAbsolute(25), true); + try expectEqual(memory.size(), 25); + try expectEqual(memory.growAbsolute(26), false); + try expectEqual(memory.size(), 25); + } +} diff --git a/src/ext/bytebox/src/vm_register.zig b/src/ext/bytebox/src/vm_register.zig new file mode 100644 index 00000000..b9886924 --- /dev/null +++ b/src/ext/bytebox/src/vm_register.zig @@ -0,0 +1,1503 @@ +const std = @import("std"); +const assert = std.debug.assert; + +const builtin = @import("builtin"); + +const AllocError = std.mem.Allocator.Error; + +const common = @import("common.zig"); +const StableArray = common.StableArray; + +const opcodes = @import("opcode.zig"); +const Opcode = opcodes.Opcode; +const WasmOpcode = opcodes.WasmOpcode; + +const def = @import("definition.zig"); +pub const i8x16 = def.i8x16; +pub const u8x16 = def.u8x16; +pub const i16x8 = def.i16x8; +pub const u16x8 = def.u16x8; +pub const i32x4 = def.i32x4; +pub const u32x4 = def.u32x4; +pub const i64x2 = def.i64x2; +pub const u64x2 = def.u64x2; +pub const f32x4 = def.f32x4; +pub const f64x2 = def.f64x2; +pub const v128 = def.v128; +const BlockImmediates = def.BlockImmediates; +const BranchTableImmediates = def.BranchTableImmediates; +const CallIndirectImmediates = def.CallIndirectImmediates; +const ConstantExpression = def.ConstantExpression; +const DataDefinition = def.DataDefinition; +const ElementDefinition = def.ElementDefinition; +const ElementMode = def.ElementMode; +const FunctionDefinition = def.FunctionDefinition; +const FunctionExport = def.FunctionExport; +const FunctionHandle = def.FunctionHandle; +const FunctionHandleType = def.FunctionHandleType; +const FunctionTypeDefinition = def.FunctionTypeDefinition; +const GlobalDefinition = def.GlobalDefinition; +const GlobalMut = def.GlobalMut; +const IfImmediates = def.IfImmediates; +const ImportNames = def.ImportNames; +const Instruction = def.Instruction; +const Limits = def.Limits; +const MemoryDefinition = def.MemoryDefinition; +const MemoryOffsetAndLaneImmediates = def.MemoryOffsetAndLaneImmediates; +const ModuleDefinition = def.ModuleDefinition; +const NameCustomSection = def.NameCustomSection; +const TableDefinition = def.TableDefinition; +const TablePairImmediates = def.TablePairImmediates; +const Val = def.Val; +const ValType = def.ValType; +const TaggedVal = def.TaggedVal; + +const inst = @import("instance.zig"); +const TrapError = inst.TrapError; +const VM = inst.VM; +const ModuleInstance = inst.ModuleInstance; +const InvokeOpts = inst.InvokeOpts; +const DebugTrapInstructionMode = inst.DebugTrapInstructionMode; +const ModuleInstantiateOpts = inst.ModuleInstantiateOpts; + +const INVALID_INSTRUCTION_INDEX: u32 = std.math.maxInt(u32); + +// High-level strategy: +// 1. Transform the ModuleDefinition's bytecode into a sea-of-nodes type of IR. +// 2. Perform constant folding, and other peephole optimizations. +// 3. Perform register allocation +// 4. Generate new bytecode +// 5. Implement the runtime instructions for the register-based bytecode + +const IRNode = struct { + opcode: Opcode, + is_phi: bool, + instruction_index: u32, + edges_in: ?[*]*IRNode, + edges_in_count: u32, + edges_out: ?[*]*IRNode, + edges_out_count: u32, + + fn createWithInstruction(compiler: *FunctionCompiler, instruction_index: u32) AllocError!*IRNode { + var node: *IRNode = compiler.ir.addOne() catch return AllocError.OutOfMemory; + node.* = IRNode{ + .opcode = compiler.module_def.code.instructions.items[instruction_index].opcode, + .is_phi = false, + .instruction_index = instruction_index, + .edges_in = null, + .edges_in_count = 0, + .edges_out = null, + .edges_out_count = 0, + }; + return node; + } + + fn createStandalone(compiler: *FunctionCompiler, opcode: Opcode) AllocError!*IRNode { + var node: *IRNode = compiler.ir.addOne() catch return AllocError.OutOfMemory; + node.* = IRNode{ + .opcode = opcode, + .is_phi = false, + .instruction_index = INVALID_INSTRUCTION_INDEX, + .edges_in = null, + .edges_in_count = 0, + .edges_out = null, + .edges_out_count = 0, + }; + return node; + } + + fn createPhi(compiler: *FunctionCompiler) AllocError!*IRNode { + var node: *IRNode = compiler.ir.addOne() catch return AllocError.OutOfMemory; + node.* = IRNode{ + .opcode = .Invalid, + .is_phi = true, + .instruction_index = 0, + .edges_in = null, + .edges_in_count = 0, + .edges_out = null, + .edges_out_count = 0, + }; + return node; + } + + fn deinit(node: IRNode, allocator: std.mem.Allocator) void { + if (node.edges_in) |e| allocator.free(e[0..node.edges_in_count]); + if (node.edges_out) |e| allocator.free(e[0..node.edges_out_count]); + } + + fn instruction(node: IRNode, module_def: ModuleDefinition) ?*Instruction { + return if (node.instruction_index != INVALID_INSTRUCTION_INDEX) + &module_def.code.instructions.items[node.instruction_index] + else + null; + } + + fn edgesIn(node: IRNode) []*IRNode { + return if (node.edges_in) |e| e[0..node.edges_in_count] else &[0]*IRNode{}; + } + + fn edgesOut(node: IRNode) []*IRNode { + return if (node.edges_out) |e| e[0..node.edges_out_count] else &[0]*IRNode{}; + } + + const EdgeDirection = enum { + In, + Out, + }; + + fn pushEdges(node: *IRNode, comptime direction: EdgeDirection, edges: []*IRNode, allocator: std.mem.Allocator) AllocError!void { + const existing = if (direction == .In) node.edgesIn() else node.edgesOut(); + var new = try allocator.alloc(*IRNode, existing.len + edges.len); + @memcpy(new[0..existing.len], existing); + @memcpy(new[existing.len .. existing.len + edges.len], edges); + if (existing.len > 0) { + allocator.free(existing); + } + switch (direction) { + .In => { + node.edges_in = new.ptr; + node.edges_in_count = @intCast(new.len); + }, + .Out => { + node.edges_out = new.ptr; + node.edges_out_count = @intCast(new.len); + }, + } + + if (node.is_phi) { + std.debug.assert(node.edges_in_count <= 2); + std.debug.assert(node.edges_out_count <= 1); + } + } + + fn hasSideEffects(node: *IRNode) bool { + // We define a side-effect instruction as any that could affect the Store or control flow + return switch (node.opcode) { + .Call => true, + else => false, + }; + } + + fn isFlowControl(node: *IRNode) bool { + return switch (node.opcode) { + .If, + .IfNoElse, + .Else, + .Return, + .Branch, + .Branch_If, + .Branch_Table, + => true, + else => false, + }; + } + + fn needsRegisterSlot(node: *IRNode) bool { + // TODO fill this out + return switch (node.opcode) { + .If, + .IfNoElse, + .Else, + .Return, + .Branch, + .Branch_If, + .Branch_Table, + => false, + else => true, + }; + } + + fn numRegisterSlots(node: *IRNode) u32 { + return switch (node.opcode) { + .If, + .IfNoElse, + .Else, + .Return, + .Branch, + .Branch_If, + .Branch_Table, + => 0, + else => 1, + }; + } + + // a node that has no out edges to instructions with side effects or control flow + fn isIsland(node: *IRNode, unvisited: *std.ArrayList(*IRNode)) AllocError!bool { + if (node.opcode == .Return) { + return false; + } + + unvisited.clearRetainingCapacity(); + + for (node.edgesOut()) |edge| { + try unvisited.append(edge); + } + + while (unvisited.items.len > 0) { + var next: *IRNode = unvisited.pop(); + if (next.opcode == .Return or next.hasSideEffects() or node.isFlowControl()) { + return false; + } + for (next.edgesOut()) |edge| { + try unvisited.append(edge); + } + } + + unvisited.clearRetainingCapacity(); + + return true; + } +}; + +const RegisterSlots = struct { + const Slot = struct { + node: ?*IRNode, + prev: ?u32, + }; + + slots: std.ArrayList(Slot), + last_free: ?u32, + + fn init(allocator: std.mem.Allocator) RegisterSlots { + return RegisterSlots{ + .slots = std.ArrayList(Slot).init(allocator), + .last_free = null, + }; + } + + fn deinit(self: *RegisterSlots) void { + self.slots.deinit(); + } + + fn alloc(self: *RegisterSlots, node: *IRNode) AllocError!u32 { + if (self.last_free == null) { + self.last_free = @intCast(self.slots.items.len); + try self.slots.append(Slot{ + .node = null, + .prev = null, + }); + } + + var index = self.last_free.?; + var slot: *Slot = &self.slots.items[index]; + self.last_free = slot.prev; + slot.node = node; + slot.prev = null; + + std.debug.print("pushed node {*} with opcode {} to index {}\n", .{ node, node.opcode, index }); + + return index; + } + + fn freeAt(self: *RegisterSlots, node: *IRNode, index: u32) void { + var succes: bool = false; + var slot: *Slot = &self.slots.items[index]; + if (slot.node == node) { + slot.node = null; + slot.prev = self.last_free; + self.last_free = index; + succes = true; + } + + std.debug.print("attempting to free node {*} with opcode {} at index {}: {}\n", .{ node, node.opcode, index, succes }); + } +}; + +const FunctionIR = struct { + def_index: usize = 0, + type_def_index: usize = 0, + ir_root: ?*IRNode = null, + + // fn definition(func: FunctionIR, module_def: ModuleDefinition) *FunctionDefinition { + // return &module_def.functions.items[func.def_index]; + // } + + fn regalloc(func: *FunctionIR, compile_data: *IntermediateCompileData, allocator: std.mem.Allocator) AllocError!void { + std.debug.assert(func.ir_root != null); + + var ir_root = func.ir_root.?; + std.debug.assert(ir_root.opcode == .Return); // TODO need to update other places in the code to ensure this is a thing + + var slots = RegisterSlots.init(allocator); + defer slots.deinit(); + + var visit_queue = std.ArrayList(*IRNode).init(allocator); + defer visit_queue.deinit(); + try visit_queue.append(ir_root); + + var visited = std.AutoHashMap(*IRNode, void).init(allocator); + defer visited.deinit(); + + while (visit_queue.items.len > 0) { + var node: *IRNode = visit_queue.orderedRemove(0); // visit the graph in breadth-first order (FIFO queue) + try visited.put(node, {}); + + // mark output node slots as free - this is safe because the dataflow graph flows one way and the + // output can't be reused higher up in the graph + for (node.edgesOut()) |output_node| { + if (compile_data.register_map.get(output_node)) |index| { + slots.freeAt(output_node, index); + } + } + + // allocate slots for this instruction + // TODO handle multiple output slots (e.g. results of a function call) + if (node.needsRegisterSlot()) { + const index: u32 = try slots.alloc(node); + try compile_data.register_map.put(node, index); + } + + // add inputs to the FIFO visit queue + for (node.edgesIn()) |input_node| { + if (visited.contains(input_node) == false) { + try visit_queue.append(input_node); + } + } + } + } + + // TODO call this from the compiler compile function, have the compile function take instructions and local_types arrays passed down from module instantiate + // TODO inline regalloc into this function + fn codegen(func: FunctionIR, store: *FunctionStore, compile_data: IntermediateCompileData, module_def: ModuleDefinition, allocator: std.mem.Allocator) AllocError!void { + std.debug.assert(func.ir_root != null); + + // walk the graph in breadth-first order, starting from the last Return node + // when a node is visited, emit its instruction + // reverse the instructions array when finished (alternatively just emit in reverse order if we have the node count from regalloc) + + const start_instruction_offset = store.instructions.items.len; + + var visit_queue = std.ArrayList(*IRNode).init(allocator); + defer visit_queue.deinit(); + try visit_queue.append(func.ir_root.?); + + var visited = std.AutoHashMap(*IRNode, void).init(allocator); + defer visited.deinit(); + + while (visit_queue.items.len > 0) { + var node: *IRNode = visit_queue.orderedRemove(0); // visit the graph in breadth-first order (FIFO queue) + + // only emit an instruction once all its out edges have been visited - this ensures all dependent instructions + // will be executed after this one + var all_out_edges_visited: bool = true; + for (node.edgesOut()) |output_node| { + if (visited.contains(output_node) == false) { + all_out_edges_visited = false; + break; + } + } + + if (all_out_edges_visited) { + try visited.put(node, {}); + + try store.instructions.append(RegInstruction{ + .registerSlotOffset = if (compile_data.register_map.get(node)) |slot_index| slot_index else 0, + .opcode = node.opcode, + .immediate = node.instruction(module_def).?.immediate, + }); + } + + for (node.edgesIn()) |input_node| { + if (!visited.contains(input_node)) { // TODO do we need this? + try visit_queue.append(input_node); + } + } + } + + const end_instruction_offset = store.instructions.items.len; + var emitted_instructions = store.instructions.items[start_instruction_offset..end_instruction_offset]; + + std.mem.reverse(RegInstruction, emitted_instructions); + + const func_def: *const FunctionDefinition = &module_def.functions.items[func.def_index]; + const func_type: *const FunctionTypeDefinition = &module_def.types.items[func.type_def_index]; + const param_types: []const ValType = func_type.getParams(); + try store.local_types.ensureTotalCapacity(store.local_types.items.len + param_types.len + func_def.locals.items.len); + + const types_index_begin = store.local_types.items.len; + store.local_types.appendSliceAssumeCapacity(param_types); + store.local_types.appendSliceAssumeCapacity(func_def.locals.items); + const types_index_end = store.local_types.items.len; + + try store.instances.append(FunctionInstance{ + .type_def_index = func.type_def_index, + .def_index = func.def_index, + .instructions_begin = start_instruction_offset, + .instructions_end = end_instruction_offset, + .local_types_begin = types_index_begin, + .local_types_end = types_index_end, + }); + } + + fn dumpVizGraph(func: FunctionIR, path: []u8, module_def: ModuleDefinition, allocator: std.mem.Allocator) !void { + var graph_txt = std.ArrayList(u8).init(allocator); + defer graph_txt.deinit(); + try graph_txt.ensureTotalCapacity(1024 * 16); + + var writer = graph_txt.writer(); + _ = try writer.write("digraph {\n"); + + var nodes = std.ArrayList(*const IRNode).init(allocator); + defer nodes.deinit(); + try nodes.ensureTotalCapacity(1024); + nodes.appendAssumeCapacity(func.ir_root); + + var visited = std.AutoHashMap(*IRNode, void).init(allocator); + defer visited.deinit(); + try visited.put(func.ir_root, {}); + + while (nodes.items.len > 0) { + const n: *const IRNode = nodes.pop(); + const opcode: Opcode = n.opcode; + const instruction = n.instruction(module_def); + + var label_buffer: [256]u8 = undefined; + const label = switch (opcode) { + .I32_Const => std.fmt.bufPrint(&label_buffer, ": {}", .{instruction.?.immediate.ValueI32}) catch unreachable, + .I64_Const => std.fmt.bufPrint(&label_buffer, ": {}", .{instruction.?.immediate.ValueI64}) catch unreachable, + .F32_Const => std.fmt.bufPrint(&label_buffer, ": {}", .{instruction.?.immediate.ValueF32}) catch unreachable, + .F64_Const => std.fmt.bufPrint(&label_buffer, ": {}", .{instruction.?.immediate.ValueF64}) catch unreachable, + .Call => std.fmt.bufPrint(&label_buffer, ": func {}", .{instruction.?.immediate.Index}) catch unreachable, + .Local_Get, .Local_Set, .Local_Tee => std.fmt.bufPrint(&label_buffer, ": {}", .{instruction.?.immediate.Index}) catch unreachable, + else => &[0]u8{}, + }; + + var register_buffer: [64]u8 = undefined; + const register = blk: { + if (func.register_map.get(n)) |slot| { + break :blk std.fmt.bufPrint(®ister_buffer, " @reg {}", .{slot}) catch unreachable; + } else { + break :blk &[0]u8{}; + } + }; + + try writer.print("\"{*}\" [label=\"{}{s}{s}\"]\n", .{ n, opcode, label, register }); + + for (n.edgesOut()) |e| { + try writer.print("\"{*}\" -> \"{*}\"\n", .{ n, e }); + + if (!visited.contains(e)) { + try nodes.append(e); + try visited.put(e, {}); + } + } + + for (n.edgesIn()) |e| { + if (!visited.contains(e)) { + try nodes.append(e); + try visited.put(e, {}); + } + } + } + + _ = try writer.write("}\n"); + + try std.fs.cwd().writeFile(path, graph_txt.items); + } +}; + +const IntermediateCompileData = struct { + const UniqueValueToIRNodeMap = std.HashMap(TaggedVal, *IRNode, TaggedVal.HashMapContext, std.hash_map.default_max_load_percentage); + + const PendingContinuationEdge = struct { + continuation: usize, + node: *IRNode, + }; + + const BlockStack = struct { + const Block = struct { + node_start_index: u32, + continuation: usize, // in instruction index space + phi_nodes: []*IRNode, + }; + + nodes: std.ArrayList(*IRNode), + blocks: std.ArrayList(Block), + phi_nodes: std.ArrayList(*IRNode), + + // const ContinuationType = enum { + // .Normal, + // .Loop, + // }; + + fn init(allocator: std.mem.Allocator) BlockStack { + return BlockStack{ + .nodes = std.ArrayList(*IRNode).init(allocator), + .blocks = std.ArrayList(Block).init(allocator), + .phi_nodes = std.ArrayList(*IRNode).init(allocator), + }; + } + + fn deinit(self: BlockStack) void { + self.nodes.deinit(); + self.blocks.deinit(); + } + + fn pushBlock(self: *BlockStack, continuation: usize) AllocError!void { + try self.blocks.append(Block{ + .node_start_index = @intCast(self.nodes.items.len), + .continuation = continuation, + .phi_nodes = &[_]*IRNode{}, + }); + } + + fn pushBlockWithPhi(self: *BlockStack, continuation: u32, phi_nodes: []*IRNode) AllocError!void { + const start_slice_index = self.phi_nodes.items.len; + try self.phi_nodes.appendSlice(phi_nodes); + + try self.blocks.append(Block{ + .node_start_index = @intCast(self.nodes.items.len), + .continuation = continuation, + .phi_nodes = self.phi_nodes.items[start_slice_index..], + }); + } + + fn pushNode(self: *BlockStack, node: *IRNode) AllocError!void { + try self.nodes.append(node); + } + + fn popBlock(self: *BlockStack) void { + const block: Block = self.blocks.pop(); + + std.debug.assert(block.node_start_index <= self.nodes.items.len); + + // should never grow these arrays + self.nodes.resize(block.node_start_index) catch unreachable; + self.phi_nodes.resize(self.phi_nodes.items.len - block.phi_nodes.len) catch unreachable; + } + + fn currentBlockNodes(self: *BlockStack) []*IRNode { + // std.debug.print(">>>>>>>> num block: {}\n", .{self.blocks.items.len}); + const index: u32 = self.blocks.items[self.blocks.items.len - 1].node_start_index; + return self.nodes.items[index..]; + } + + fn reset(self: *BlockStack) void { + self.nodes.clearRetainingCapacity(); + self.blocks.clearRetainingCapacity(); + } + }; + + allocator: std.mem.Allocator, + + // all_nodes: std.ArrayList(*IRNode), + + blocks: BlockStack, + + // This stack is a record of the nodes to push values onto the stack. If an instruction would push + // multiple values onto the stack, it would be in this list as many times as values it pushed. Note + // that we don't have to do any type checking here because the module has already been validated. + value_stack: std.ArrayList(*IRNode), + + // records the current block continuation + // label_continuations: std.ArrayList(u32), + + pending_continuation_edges: std.ArrayList(PendingContinuationEdge), + + // when hitting an unconditional control transfer, we need to mark the rest of the stack values as unreachable just like in validation + is_unreachable: bool, + + // This is a bit weird - since the Local_* instructions serve to just manipulate the locals into the stack, + // we need a way to represent what's in the locals slot as an SSA node. This array lets us do that. We also + // reuse the Local_Get instructions to indicate the "initial value" of the slot. Since our IRNode only stores + // indices to instructions, we'll just lazily set these when they're fetched for the first time. + locals: std.ArrayList(?*IRNode), + + // Lets us collapse multiple const IR nodes with the same type/value into a single one + unique_constants: UniqueValueToIRNodeMap, + + // + register_map: std.AutoHashMap(*const IRNode, u32), + + scratch_node_list_1: std.ArrayList(*IRNode), + scratch_node_list_2: std.ArrayList(*IRNode), + + fn init(allocator: std.mem.Allocator) IntermediateCompileData { + return IntermediateCompileData{ + .allocator = allocator, + // .all_nodes = std.ArrayList(*IRNode).init(allocator), + .blocks = BlockStack.init(allocator), + .value_stack = std.ArrayList(*IRNode).init(allocator), + // .label_continuations = std.ArrayList(u32).init(allocator), + .pending_continuation_edges = std.ArrayList(PendingContinuationEdge).init(allocator), + .is_unreachable = false, + .locals = std.ArrayList(?*IRNode).init(allocator), + .unique_constants = UniqueValueToIRNodeMap.init(allocator), + .register_map = std.AutoHashMap(*const IRNode, u32).init(allocator), + .scratch_node_list_1 = std.ArrayList(*IRNode).init(allocator), + .scratch_node_list_2 = std.ArrayList(*IRNode).init(allocator), + }; + } + + fn warmup(self: *IntermediateCompileData, func_def: FunctionDefinition, module_def: ModuleDefinition) AllocError!void { + try self.locals.appendNTimes(null, func_def.numParamsAndLocals(module_def)); + try self.scratch_node_list_1.ensureTotalCapacity(4096); + try self.scratch_node_list_2.ensureTotalCapacity(4096); + try self.register_map.ensureTotalCapacity(1024); + // try self.label_continuations.append(func_def.continuation); + self.is_unreachable = false; + } + + fn reset(self: *IntermediateCompileData) void { + // self.all_nodes.clearRetainingCapacity(); + self.blocks.reset(); + self.value_stack.clearRetainingCapacity(); + // self.label_continuations.clearRetainingCapacity(); + self.pending_continuation_edges.clearRetainingCapacity(); + self.locals.clearRetainingCapacity(); + self.unique_constants.clearRetainingCapacity(); + self.register_map.clearRetainingCapacity(); + self.scratch_node_list_1.clearRetainingCapacity(); + self.scratch_node_list_2.clearRetainingCapacity(); + } + + fn deinit(self: *IntermediateCompileData) void { + // self.all_nodes.deinit(); + self.blocks.deinit(); + self.value_stack.deinit(); + // self.label_continuations.deinit(); + self.pending_continuation_edges.deinit(); + self.locals.deinit(); + self.unique_constants.deinit(); + self.register_map.deinit(); + self.scratch_node_list_1.deinit(); + self.scratch_node_list_2.deinit(); + } + + fn popPushValueStackNodes(self: *IntermediateCompileData, node: *IRNode, num_consumed: usize, num_pushed: usize) AllocError!void { + if (self.is_unreachable) { + return; + } + + var edges_buffer: [8]*IRNode = undefined; // 8 should be more stack slots than any one instruction can pop + std.debug.assert(num_consumed <= edges_buffer.len); + + var edges = edges_buffer[0..num_consumed]; + for (edges) |*e| { + e.* = self.value_stack.pop(); + } + try node.pushEdges(.In, edges, self.allocator); + for (edges) |e| { + var consumer_edges = [_]*IRNode{node}; + try e.pushEdges(.Out, &consumer_edges, self.allocator); + } + try self.value_stack.appendNTimes(node, num_pushed); + } + + fn foldConstant(self: *IntermediateCompileData, compiler: *FunctionCompiler, comptime valtype: ValType, instruction_index: u32, instruction: Instruction) AllocError!*IRNode { + var val: TaggedVal = undefined; + val.type = valtype; + val.val = switch (valtype) { + .I32 => Val{ .I32 = instruction.immediate.ValueI32 }, + .I64 => Val{ .I64 = instruction.immediate.ValueI64 }, + .F32 => Val{ .F32 = instruction.immediate.ValueF32 }, + .F64 => Val{ .F64 = instruction.immediate.ValueF64 }, + .V128 => Val{ .V128 = instruction.immediate.ValueVec }, + else => @compileError("Unsupported const instruction"), + }; + + var res = try self.unique_constants.getOrPut(val); + if (res.found_existing == false) { + var node = try IRNode.createWithInstruction(compiler, instruction_index); + res.value_ptr.* = node; + } + if (self.is_unreachable == false) { + try self.value_stack.append(res.value_ptr.*); + } + return res.value_ptr.*; + } + + fn addPendingEdgeLabel(self: *IntermediateCompileData, node: *IRNode, label_id: u32) !void { + const last_block_index = self.blocks.blocks.items.len - 1; + var continuation: usize = self.blocks.blocks.items[last_block_index - label_id].continuation; + try self.pending_continuation_edges.append(PendingContinuationEdge{ + .node = node, + .continuation = continuation, + }); + } + + fn addPendingEdgeContinuation(self: *IntermediateCompileData, node: *IRNode, continuation: u32) !void { + try self.pending_continuation_edges.append(PendingContinuationEdge{ + .node = node, + .continuation = continuation, + }); + } +}; + +const FunctionCompiler = struct { + allocator: std.mem.Allocator, + module_def: *const ModuleDefinition, + ir: StableArray(IRNode), + + fn init(allocator: std.mem.Allocator, module_def: *const ModuleDefinition) FunctionCompiler { + return FunctionCompiler{ + .allocator = allocator, + .module_def = module_def, + .ir = StableArray(IRNode).init(1024 * 1024 * 8), + }; + } + + fn deinit(compiler: *FunctionCompiler) void { + for (compiler.ir.items) |node| { + node.deinit(compiler.allocator); + } + compiler.ir.deinit(); + } + + fn compile(compiler: *FunctionCompiler, store: *FunctionStore) AllocError!void { + var compile_data = IntermediateCompileData.init(compiler.allocator); + defer compile_data.deinit(); + + // TODO could + for (0..compiler.module_def.functions.items.len) |i| { + std.debug.print("compiler.module_def.functions.items.len: {}, i: {}\n\n", .{ compiler.module_def.functions.items.len, i }); + var function_ir = try compiler.compileFunc(i, &compile_data); + if (function_ir.ir_root != null) { + try function_ir.regalloc(&compile_data, compiler.allocator); + try function_ir.codegen(store, compile_data, compiler.module_def.*, compiler.allocator); + } + + compile_data.reset(); + } + } + + fn compileFunc(compiler: *FunctionCompiler, index: usize, compile_data: *IntermediateCompileData) AllocError!FunctionIR { + const UniqueValueToIRNodeMap = std.HashMap(TaggedVal, *IRNode, TaggedVal.HashMapContext, std.hash_map.default_max_load_percentage); + + const Helpers = struct { + fn opcodeHasDefaultIRMapping(opcode: Opcode) bool { + return switch (opcode) { + .Noop, + .Block, + .Loop, + .End, + .Drop, + .I32_Const, + .I64_Const, + .F32_Const, + .F64_Const, + .Local_Get, + .Local_Set, + .Local_Tee, + => false, + else => true, + }; + } + }; + + const func: *const FunctionDefinition = &compiler.module_def.functions.items[index]; + const func_type: *const FunctionTypeDefinition = func.typeDefinition(compiler.module_def.*); + + std.debug.print("compiling func index {}\n", .{index}); + + try compile_data.warmup(func.*, compiler.module_def.*); + + try compile_data.blocks.pushBlock(func.continuation); + + var locals = compile_data.locals.items; // for convenience later + + // Lets us collapse multiple const IR nodes with the same type/value into a single one + var unique_constants = UniqueValueToIRNodeMap.init(compiler.allocator); + defer unique_constants.deinit(); + + const instructions: []Instruction = func.instructions(compiler.module_def.*); + if (instructions.len == 0) { + std.log.warn("Skipping function with no instructions (index {}).", .{index}); + return FunctionIR{}; + } + + var ir_root: ?*IRNode = null; + + for (instructions, 0..) |instruction, local_instruction_index| { + const instruction_index: u32 = @intCast(func.instructions_begin + local_instruction_index); + + var node: ?*IRNode = null; + if (Helpers.opcodeHasDefaultIRMapping(instruction.opcode)) { + node = try IRNode.createWithInstruction(compiler, instruction_index); + } + + std.debug.print("opcode: {}\n", .{instruction.opcode}); + + switch (instruction.opcode) { + // .Loop => { + // instruction. + // }, + // .If => {}, + .Block => { + // compile_data.label_stack += 1; + + // try compile_data.label_stack.append(node); + // try compile_data.label_continuations.append(instruction.immediate.Block.continuation); + try compile_data.blocks.pushBlock(instruction.immediate.Block.continuation); + }, + .Loop => { + // compile_data.label_stack += 1; + // compile_data.label_stack.append(node); + // try compile_data.label_continuations.append(instruction.immediate.Block.continuation); + try compile_data.blocks.pushBlock(instruction.immediate.Block.continuation); // TODO record the kind of block so we know this is a loop? + }, + .If => { + var phi_nodes: *std.ArrayList(*IRNode) = &compile_data.scratch_node_list_1; + defer compile_data.scratch_node_list_1.clearRetainingCapacity(); + + std.debug.assert(phi_nodes.items.len == 0); + + for (0..instruction.immediate.If.num_returns) |_| { + try phi_nodes.append(try IRNode.createPhi(compiler)); + } + + try compile_data.blocks.pushBlockWithPhi(instruction.immediate.If.end_continuation, phi_nodes.items[0..]); + try compile_data.addPendingEdgeContinuation(node.?, instruction.immediate.If.end_continuation + 1); + try compile_data.addPendingEdgeContinuation(node.?, instruction.immediate.If.else_continuation); + + try compile_data.popPushValueStackNodes(node.?, 1, 0); + + // after the if consumes the value it needs, push the phi nodes on since these will be the return values + // of the block + try compile_data.value_stack.appendSlice(phi_nodes.items); + }, + .IfNoElse => { + try compile_data.blocks.pushBlock(instruction.immediate.If.end_continuation); + try compile_data.addPendingEdgeContinuation(node.?, instruction.immediate.If.end_continuation + 1); + try compile_data.addPendingEdgeContinuation(node.?, instruction.immediate.If.else_continuation); + try compile_data.popPushValueStackNodes(node.?, 1, 0); + + // TODO figure out if there needs to be any phi nodes and if so what two inputs they have + }, + .Else => { + try compile_data.addPendingEdgeContinuation(node.?, instruction.immediate.If.end_continuation + 1); + try compile_data.addPendingEdgeContinuation(node.?, instruction.immediate.If.else_continuation); + + // TODO hook up the phi nodes with the stuffs + }, + .End => { + // TODO finish up anything with phi nodes? + + // the last End opcode returns the values on the stack + // if (compile_data.label_continuations.items.len == 1) { + if (compile_data.blocks.blocks.items.len == 1) { + node = try IRNode.createStandalone(compiler, .Return); + try compile_data.popPushValueStackNodes(node.?, func_type.getReturns().len, 0); + // _ = compile_data.label_continuations.pop(); + } + + // At the end of every block, we ensure all nodes with side effects are still in the graph. Order matters + // since mutations to the Store or control flow changes must happen in the order of the original instructions. + { + var nodes_with_side_effects: *std.ArrayList(*IRNode) = &compile_data.scratch_node_list_1; + defer nodes_with_side_effects.clearRetainingCapacity(); + + var current_block_nodes: []*IRNode = compile_data.blocks.currentBlockNodes(); + + for (current_block_nodes) |block_node| { + if (block_node.hasSideEffects() or block_node.isFlowControl()) { + try nodes_with_side_effects.append(block_node); + } + } + + if (nodes_with_side_effects.items.len >= 2) { + var i: i32 = @intCast(nodes_with_side_effects.items.len - 2); + while (i >= 0) : (i -= 1) { + var ii: u32 = @intCast(i); + var node_a: *IRNode = nodes_with_side_effects.items[ii]; + if (try node_a.isIsland(&compile_data.scratch_node_list_2)) { + var node_b: *IRNode = nodes_with_side_effects.items[ii + 1]; + + var in_edges = [_]*IRNode{node_b}; + try node_a.pushEdges(.Out, &in_edges, compile_data.allocator); + + var out_edges = [_]*IRNode{node_a}; + try node_b.pushEdges(.In, &out_edges, compile_data.allocator); + } + } + } + } + + compile_data.blocks.popBlock(); + }, + .Branch => { + try compile_data.addPendingEdgeLabel(node.?, instruction.immediate.LabelId); + compile_data.is_unreachable = true; + }, + .Branch_If => { + try compile_data.popPushValueStackNodes(node.?, 1, 0); + }, + .Branch_Table => { + assert(node != null); + + try compile_data.popPushValueStackNodes(node.?, 1, 0); + + // var continuation_edges: std.ArrayList(*IRNode).init(allocator); + // defer continuation_edges.deinit(); + + const immediates: *const BranchTableImmediates = &compiler.module_def.code.branch_table.items[instruction.immediate.Index]; + + try compile_data.addPendingEdgeLabel(node.?, immediates.fallback_id); + for (immediates.label_ids.items) |continuation| { + try compile_data.addPendingEdgeLabel(node.?, continuation); + } + + compile_data.is_unreachable = true; + + // try label_ids.append(immediates.fallback_id); + // try label_ids.appendSlice(immediates.label_ids.items); + + // node.pushEdges(.Out, ) + // TODO need to somehow connect to the various labels it wants to jump to? + }, + .Return => { + try compile_data.popPushValueStackNodes(node.?, func_type.getReturns().len, 0); + compile_data.is_unreachable = true; + }, + .Call => { + const calling_func_def: *const FunctionDefinition = &compiler.module_def.functions.items[index]; + const calling_func_type: *const FunctionTypeDefinition = calling_func_def.typeDefinition(compiler.module_def.*); + const num_returns: usize = calling_func_type.getReturns().len; + const num_params: usize = calling_func_type.getParams().len; + + try compile_data.popPushValueStackNodes(node.?, num_params, num_returns); + }, + // .Call_Indirect + .Drop => { + if (compile_data.is_unreachable == false) { + _ = compile_data.value_stack.pop(); + } + }, + .I32_Const => { + assert(node == null); + node = try compile_data.foldConstant(compiler, .I32, instruction_index, instruction); + }, + .I64_Const => { + assert(node == null); + node = try compile_data.foldConstant(compiler, .I64, instruction_index, instruction); + }, + .F32_Const => { + assert(node == null); + node = try compile_data.foldConstant(compiler, .F32, instruction_index, instruction); + }, + .F64_Const => { + assert(node == null); + node = try compile_data.foldConstant(compiler, .F64, instruction_index, instruction); + }, + .I32_Eq, + .I32_NE, + .I32_LT_S, + .I32_LT_U, + .I32_GT_S, + .I32_GT_U, + .I32_LE_S, + .I32_LE_U, + .I32_GE_S, + .I32_GE_U, + .I32_Add, + .I32_Sub, + .I32_Mul, + .I32_Div_S, + .I32_Div_U, + .I32_Rem_S, + .I32_Rem_U, + .I32_And, + .I32_Or, + .I32_Xor, + .I32_Shl, + .I32_Shr_S, + .I32_Shr_U, + .I32_Rotl, + .I32_Rotr, + // TODO add a lot more of these simpler opcodes + => { + try compile_data.popPushValueStackNodes(node.?, 2, 1); + }, + .I32_Eqz, + .I32_Clz, + .I32_Ctz, + .I32_Popcnt, + .I32_Extend8_S, + .I32_Extend16_S, + .I64_Clz, + .I64_Ctz, + .I64_Popcnt, + .F32_Neg, + .F64_Neg, + => { + try compile_data.popPushValueStackNodes(node.?, 1, 1); + }, + .Local_Get => { + assert(node == null); + + if (compile_data.is_unreachable == false) { + const local: *?*IRNode = &locals[instruction.immediate.Index]; + if (local.* == null) { + local.* = try IRNode.createWithInstruction(compiler, instruction_index); + } + node = local.*; + try compile_data.value_stack.append(node.?); + } + }, + .Local_Set => { + assert(node == null); + + if (compile_data.is_unreachable == false) { + var n: *IRNode = compile_data.value_stack.pop(); + locals[instruction.immediate.Index] = n; + } + }, + .Local_Tee => { + assert(node == null); + if (compile_data.is_unreachable == false) { + var n: *IRNode = compile_data.value_stack.items[compile_data.value_stack.items.len - 1]; + locals[instruction.immediate.Index] = n; + } + }, + else => { + std.log.warn("skipping node {}", .{instruction.opcode}); + }, + } + + // resolve any pending continuations with the current node. + if (node) |current_node| { + var i: usize = 0; + while (i < compile_data.pending_continuation_edges.items.len) { + var pending: *IntermediateCompileData.PendingContinuationEdge = &compile_data.pending_continuation_edges.items[i]; + + if (pending.continuation == instruction_index) { + var out_edges = [_]*IRNode{current_node}; + try pending.node.pushEdges(.Out, &out_edges, compile_data.allocator); + + var in_edges = [_]*IRNode{pending.node}; + try current_node.pushEdges(.In, &in_edges, compile_data.allocator); + + _ = compile_data.pending_continuation_edges.swapRemove(i); + } else { + i += 1; + } + } + + // try compile_data.all_nodes.append(current_node); + + try compile_data.blocks.pushNode(current_node); + } + + // TODO don't assume only one return node - there can be multiple in real functions + if (node) |n| { + if (n.opcode == .Return) { + std.debug.assert(ir_root == null); + ir_root = node; + } + } + } + + // resolve any nodes that have side effects that somehow became isolated + // TODO will have to stress test this with a bunch of different cases of nodes + // for (compile_data.all_nodes.items[0 .. compile_data.all_nodes.items.len - 1]) |node| { + // if (node.hasSideEffects()) { + // if (try node.isIsland(&compile_data.scratch_node_list_1)) { + // var last_node: *IRNode = compile_data.all_nodes.items[compile_data.all_nodes.items.len - 1]; + + // var out_edges = [_]*IRNode{last_node}; + // try node.pushEdges(.Out, &out_edges, compile_data.allocator); + + // var in_edges = [_]*IRNode{node}; + // try last_node.pushEdges(.In, &in_edges, compile_data.allocator); + // } + // } + // } + + return FunctionIR{ + .def_index = index, + .type_def_index = func.type_index, + .ir_root = ir_root, + }; + + // return FunctionIR.init( + // index, + // func.type_index, + // ir_root.?, + // compiler.allocator, + // ); + + // try compiler.functions.append(FunctionIR.init( + // index, + // func.type_index, + // ir_root.?, + // compiler.allocator, + // )); + + // try compiler.functions.items[compiler.functions.items.len - 1].regalloc(compiler.allocator); + } +}; + +const FunctionInstance = struct { + type_def_index: usize, + def_index: usize, + instructions_begin: usize, + instructions_end: usize, + local_types_begin: usize, + local_types_end: usize, + + fn instructions(func: FunctionInstance, store: FunctionStore) []RegInstruction { + return store.instructions.items[func.instructions_begin..func.instructions_end]; + } + + fn localTypes(func: FunctionInstance, store: FunctionStore) []ValType { + return store.local_types.items[func.local_types_begin..func.local_types_end]; + } + + fn typeDefinition(func: FunctionInstance, module_def: ModuleDefinition) *const FunctionTypeDefinition { + return &module_def.types.items[func.type_def_index]; + } + + fn definition(func: FunctionInstance, module_def: ModuleDefinition) *const FunctionDefinition { + return &module_def.functions.items[func.def_index]; + } +}; + +const CompiledFunctions = struct { + local_types: std.ArrayList(ValType), + instructions: std.ArrayList(RegInstruction), + instances: std.ArrayList(FunctionInstance), +}; + +const Label = struct { + // TODO figure out what this struct should be + // num_returns: u32, + continuation: u32, + // start_offset_values: u32, +}; + +const CallFrame = struct { + func: *const FunctionInstance, + module_instance: *ModuleInstance, + num_returns: u32, + registers_begin: u32, // offset into registers + labels_begin: u32, // offset into labels +}; + +const MachineState = struct { + const AllocOpts = struct { + max_registers: usize, + max_labels: usize, + max_frames: usize, + }; + + registers: []Val, + labels: []Label, + frames: []CallFrame, + num_registers: u32, + num_labels: u16, + num_frames: u16, + mem: []u8, + allocator: std.mem.Allocator, + + fn init(allocator: std.mem.Allocator) MachineState { + return MachineState{ + .registers = &[_]Val{}, + .labels = &[_]Label{}, + .frames = &[_]CallFrame{}, + .num_registers = 0, + .num_labels = 0, + .num_frames = 0, + .mem = &[_]u8{}, + .allocator = allocator, + }; + } + + fn deinit(ms: *MachineState) void { + if (ms.mem.len > 0) { + ms.allocator.free(ms.mem); + } + } + + fn allocMemory(ms: *MachineState, opts: AllocOpts) AllocError!void { + const alignment = @max(@alignOf(Val), @alignOf(Label), @alignOf(CallFrame)); + const values_alloc_size = std.mem.alignForward(usize, @as(usize, @intCast(opts.max_registers)) * @sizeOf(Val), alignment); + const labels_alloc_size = std.mem.alignForward(usize, @as(usize, @intCast(opts.max_labels)) * @sizeOf(Label), alignment); + const frames_alloc_size = std.mem.alignForward(usize, @as(usize, @intCast(opts.max_frames)) * @sizeOf(CallFrame), alignment); + const total_alloc_size: usize = values_alloc_size + labels_alloc_size + frames_alloc_size; + + const begin_labels = values_alloc_size; + const begin_frames = values_alloc_size + labels_alloc_size; + + ms.mem = try ms.allocator.alloc(u8, total_alloc_size); + ms.registers.ptr = @as([*]Val, @alignCast(@ptrCast(ms.mem.ptr))); + ms.registers.len = opts.max_registers; + ms.labels.ptr = @as([*]Label, @alignCast(@ptrCast(ms.mem[begin_labels..].ptr))); + ms.labels.len = opts.max_labels; + ms.frames.ptr = @as([*]CallFrame, @alignCast(@ptrCast(ms.mem[begin_frames..].ptr))); + ms.frames.len = opts.max_frames; + } + + fn checkExhausted(ms: MachineState, extra_registers: u32) TrapError!void { + if (ms.num_registers + extra_registers >= ms.registers.len) { + return error.TrapStackExhausted; + } + } + + fn reset(ms: *MachineState) void { + ms.num_registers = 0; + ms.num_labels = 0; + ms.num_frames = 0; + } + + fn get(ms: MachineState, register_local: u32) Val { + var frame: *CallFrame = topFrame(); + var slot = frame.registers_begin + register_local; + return ms.registers[slot]; + } + + fn getI32(ms: MachineState, register_local: u32) i32 { + return ms.get(register_local).I32; + } + + fn getI64(ms: MachineState, register_local: u32) i64 { + return ms.get(register_local).I64; + } + + fn getF32(ms: MachineState, register_local: u32) f32 { + return ms.get(register_local).F32; + } + + fn getF64(ms: MachineState, register_local: u32) f64 { + return ms.get(register_local).F64; + } + + fn set(ms: *MachineState, register_local: u32, val: Val) void { + var frame: *CallFrame = topFrame(); + var slot = frame.registers_begin + register_local; + ms.registers[slot] = val; + } + + fn setI32(ms: *MachineState, register_local: u32, val: i32) void { + var frame: *CallFrame = topFrame(); + var slot = frame.registers_begin + register_local; + ms.registers[slot].I32 = val; + } + + fn setI64(ms: *MachineState, register_local: u32, val: i64) void { + var frame: *CallFrame = topFrame(); + var slot = frame.registers_begin + register_local; + ms.registers[slot].I64 = val; + } + + fn setF32(ms: *MachineState, register_local: u32, val: f32) void { + var frame: *CallFrame = topFrame(); + var slot = frame.registers_begin + register_local; + ms.registers[slot].F32 = val; + } + + fn setF64(ms: *MachineState, register_local: u32, val: f64) void { + var frame: *CallFrame = topFrame(); + var slot = frame.registers_begin + register_local; + ms.registers[slot].F64 = val; + } + + fn topFrame(ms: MachineState) *CallFrame { + return &ms.frames[ms.num_frames - 1]; + } +}; + +const FunctionStore = struct { + local_types: std.ArrayList(ValType), + instructions: std.ArrayList(RegInstruction), + instances: std.ArrayList(FunctionInstance), +}; + +pub const RegisterVM = struct { + functions: FunctionStore, + ms: MachineState, + + fn fromVM(vm: *VM) *RegisterVM { + return @as(*RegisterVM, @alignCast(@ptrCast(vm.impl))); + } + + pub fn init(vm: *VM) void { + var self: *RegisterVM = fromVM(vm); + + self.functions.local_types = std.ArrayList(ValType).init(vm.allocator); + self.functions.instructions = std.ArrayList(RegInstruction).init(vm.allocator); + self.functions.instances = std.ArrayList(FunctionInstance).init(vm.allocator); + self.ms = MachineState.init(vm.allocator); + } + + pub fn deinit(vm: *VM) void { + var self: *RegisterVM = fromVM(vm); + + self.functions.local_types.deinit(); + self.functions.instructions.deinit(); + self.functions.instances.deinit(); + self.ms.deinit(); + } + + pub fn instantiate(vm: *VM, module: *ModuleInstance, opts: ModuleInstantiateOpts) anyerror!void { + var self: *RegisterVM = fromVM(vm); + + const stack_size = if (opts.stack_size > 0) opts.stack_size else 1024 * 128; + const stack_size_f = @as(f64, @floatFromInt(stack_size)); + + try self.ms.allocMemory(.{ + .max_registers = @as(usize, @intFromFloat(stack_size_f * 0.85)), + .max_labels = @as(usize, @intFromFloat(stack_size_f * 0.14)), + .max_frames = @as(usize, @intFromFloat(stack_size_f * 0.01)), + }); + + var compiler = FunctionCompiler.init(vm.allocator, module.module_def); + defer compiler.deinit(); + + try compiler.compile(&self.functions); + + // wasm bytecode -> IR graph -> register-assigned IR graph -> + + // TODO create functions? + + return error.Unimplemented; + } + + pub fn invoke(vm: *VM, module: *ModuleInstance, handle: FunctionHandle, params: [*]const Val, returns: [*]Val, opts: InvokeOpts) anyerror!void { + _ = vm; + _ = module; + _ = handle; + _ = params; + _ = returns; + _ = opts; + return error.Unimplemented; + } + + pub fn invokeWithIndex(vm: *VM, module: *ModuleInstance, func_index: usize, params: [*]const Val, returns: [*]Val) anyerror!void { + _ = vm; + _ = module; + _ = func_index; + _ = params; + _ = returns; + return error.Unimplemented; + } + + pub fn resumeInvoke(vm: *VM, module: *ModuleInstance, returns: []Val) anyerror!void { + _ = vm; + _ = module; + _ = returns; + return error.Unimplemented; + } + + pub fn step(vm: *VM, module: *ModuleInstance, returns: []Val) anyerror!void { + _ = vm; + _ = module; + _ = returns; + return error.Unimplemented; + } + + pub fn setDebugTrap(vm: *VM, module: *ModuleInstance, wasm_address: u32, mode: DebugTrapInstructionMode) anyerror!bool { + _ = vm; + _ = module; + _ = wasm_address; + _ = mode; + return error.Unimplemented; + } + + pub fn formatBacktrace(vm: *VM, indent: u8, allocator: std.mem.Allocator) anyerror!std.ArrayList(u8) { + _ = vm; + _ = indent; + _ = allocator; + return error.Unimplemented; + } + + pub fn findFuncTypeDef(vm: *VM, module: *ModuleInstance, local_func_index: usize) *const FunctionTypeDefinition { + var self: *RegisterVM = fromVM(vm); + return self.functions.instances.items[local_func_index].typeDefinition(module.module_def.*); + } +}; + +// register instructions get a slice of the overall set of register slots, which are pointers to actual +// registers (?) + +const RegInstruction = struct { + registerSlotOffset: u32, // offset within the function register slot space to start + opcode: Opcode, + immediate: def.InstructionImmediates, + + fn numRegisters(self: RegInstruction) u4 { + switch (self.opcode) {} + } + + fn registers(self: RegInstruction, register_slice: []Val) []Val { + return register_slice[self.registerOffset .. self.registerOffset + self.numRegisters()]; + } +}; + +fn runTestWithViz(wasm_filepath: []const u8, viz_dir: []const u8) !void { + var allocator = std.testing.allocator; + + var cwd = std.fs.cwd(); + var wasm_data: []u8 = try cwd.readFileAlloc(allocator, wasm_filepath, 1024 * 1024 * 128); + defer allocator.free(wasm_data); + + const module_def_opts = def.ModuleDefinitionOpts{ + .debug_name = std.fs.path.basename(wasm_filepath), + }; + var module_def = try ModuleDefinition.create(allocator, module_def_opts); + defer module_def.destroy(); + + try module_def.decode(wasm_data); + + var compiler = FunctionCompiler.init(allocator, module_def); + defer compiler.deinit(); + try compiler.compile(); + for (compiler.functions.items, 0..) |func, i| { + var viz_path_buffer: [256]u8 = undefined; + const viz_path = std.fmt.bufPrint(&viz_path_buffer, "{s}\\viz_{}.txt", .{ viz_dir, i }) catch unreachable; + std.debug.print("gen graph for func {}\n", .{i}); + try func.dumpVizGraph(viz_path, module_def.*, std.testing.allocator); + } +} + +// test "ir1" { +// const filename = +// // \\E:\Dev\zig_projects\bytebox\test\wasm\br_table\br_table.0.wasm +// \\E:\Dev\zig_projects\bytebox\test\wasm\return\return.0.wasm +// // \\E:\Dev\third_party\zware\test\fact.wasm +// // \\E:\Dev\zig_projects\bytebox\test\wasm\i32\i32.0.wasm +// ; +// const viz_dir = +// \\E:\Dev\zig_projects\bytebox\viz +// ; +// try runTestWithViz(filename, viz_dir); + +// // var allocator = std.testing.allocator; + +// // var cwd = std.fs.cwd(); +// // var wasm_data: []u8 = try cwd.readFileAlloc(allocator, filename, 1024 * 1024 * 128); +// // defer allocator.free(wasm_data); + +// // const module_def_opts = def.ModuleDefinitionOpts{ +// // .debug_name = std.fs.path.basename(filename), +// // }; +// // var module_def = ModuleDefinition.init(allocator, module_def_opts); +// // defer module_def.deinit(); + +// // try module_def.decode(wasm_data); + +// // var compiler = FunctionCompiler.init(allocator, &module_def); +// // defer compiler.deinit(); +// // try compiler.compile(); +// // for (compiler.functions.items, 0..) |func, i| { +// // var viz_path_buffer: [256]u8 = undefined; +// // const path_format = +// // \\E:\Dev\zig_projects\bytebox\viz\viz_{}.txt +// // ; +// // const viz_path = std.fmt.bufPrint(&viz_path_buffer, path_format, .{i}) catch unreachable; +// // std.debug.print("gen graph for func {}\n", .{i}); +// // try func.dumpVizGraph(viz_path, module_def, std.testing.allocator); +// // } +// } + +// test "ir2" { +// const filename = +// // \\E:\Dev\zig_projects\bytebox\test\wasm\br_table\br_table.0.wasm +// \\E:\Dev\zig_projects\bytebox\test\reg\add.wasm +// // \\E:\Dev\third_party\zware\test\fact.wasm +// // \\E:\Dev\zig_projects\bytebox\test\wasm\i32\i32.0.wasm +// ; +// const viz_dir = +// \\E:\Dev\zig_projects\bytebox\test\reg\ +// ; +// try runTestWithViz(filename, viz_dir); +// } diff --git a/src/ext/bytebox/src/vm_stack.zig b/src/ext/bytebox/src/vm_stack.zig new file mode 100644 index 00000000..d1912ec7 --- /dev/null +++ b/src/ext/bytebox/src/vm_stack.zig @@ -0,0 +1,5458 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const common = @import("common.zig"); +const StableArray = common.StableArray; + +const opcodes = @import("opcode.zig"); +const Opcode = opcodes.Opcode; +const WasmOpcode = opcodes.WasmOpcode; + +const def = @import("definition.zig"); +pub const i8x16 = def.i8x16; +pub const u8x16 = def.u8x16; +pub const i16x8 = def.i16x8; +pub const u16x8 = def.u16x8; +pub const i32x4 = def.i32x4; +pub const u32x4 = def.u32x4; +pub const i64x2 = def.i64x2; +pub const u64x2 = def.u64x2; +pub const f32x4 = def.f32x4; +pub const f64x2 = def.f64x2; +pub const v128 = def.v128; +const BlockImmediates = def.BlockImmediates; +const BranchTableImmediates = def.BranchTableImmediates; +const CallIndirectImmediates = def.CallIndirectImmediates; +const ConstantExpression = def.ConstantExpression; +const DataDefinition = def.DataDefinition; +const ElementDefinition = def.ElementDefinition; +const ElementMode = def.ElementMode; +const FunctionDefinition = def.FunctionDefinition; +const FunctionExport = def.FunctionExport; +const FunctionHandle = def.FunctionHandle; +const FunctionHandleType = def.FunctionHandleType; +const FunctionTypeDefinition = def.FunctionTypeDefinition; +const GlobalDefinition = def.GlobalDefinition; +const GlobalMut = def.GlobalMut; +const IfImmediates = def.IfImmediates; +const ImportNames = def.ImportNames; +const Instruction = def.Instruction; +const Limits = def.Limits; +const MemoryDefinition = def.MemoryDefinition; +const MemoryOffsetAndLaneImmediates = def.MemoryOffsetAndLaneImmediates; +const ModuleDefinition = def.ModuleDefinition; +const NameCustomSection = def.NameCustomSection; +const TableDefinition = def.TableDefinition; +const TablePairImmediates = def.TablePairImmediates; +const Val = def.Val; +const ValType = def.ValType; + +const inst = @import("instance.zig"); +const UnlinkableError = inst.UnlinkableError; +const UninstantiableError = inst.UninstantiableError; +const ExportError = inst.ExportError; +const TrapError = inst.TrapError; +const DebugTrace = inst.DebugTrace; +const TableInstance = inst.TableInstance; +const MemoryInstance = inst.MemoryInstance; +const GlobalInstance = inst.GlobalInstance; +const ElementInstance = inst.ElementInstance; +const FunctionImport = inst.FunctionImport; +const TableImport = inst.TableImport; +const MemoryImport = inst.MemoryImport; +const GlobalImport = inst.GlobalImport; +const ModuleImportPackage = inst.ModuleImportPackage; +const ModuleInstance = inst.ModuleInstance; +const VM = inst.VM; +const Store = inst.Store; +const ModuleInstantiateOpts = inst.ModuleInstantiateOpts; +const InvokeOpts = inst.InvokeOpts; +const DebugTrapInstructionMode = inst.DebugTrapInstructionMode; + +const DebugTraceStackVM = struct { + fn traceInstruction(instruction_name: []const u8, pc: u32, stack: *const Stack) void { + if (DebugTrace.shouldTraceInstructions()) { + const frame: *const CallFrame = stack.topFrame(); + const name_section: *const NameCustomSection = &frame.module_instance.module_def.name_section; + const module_name = name_section.getModuleName(); + const function_name = name_section.findFunctionName(frame.func.def_index); + + std.debug.print("\t0x{x} - {s}!{s}: {s}\n", .{ pc, module_name, function_name, instruction_name }); + } + } +}; + +const FunctionInstance = struct { + type_def_index: usize, + def_index: usize, + instructions_begin: usize, + local_types: std.ArrayList(ValType), +}; + +const Label = struct { + num_returns: u32, + continuation: u32, + start_offset_values: u32, +}; + +const CallFrame = struct { + func: *const FunctionInstance, + module_instance: *ModuleInstance, + locals: []Val, + num_returns: u32, + start_offset_values: u32, + start_offset_labels: u16, +}; + +const FuncCallData = struct { + code: [*]const Instruction, + continuation: u32, +}; + +const Stack = struct { + values: []Val, + labels: []Label, + frames: []CallFrame, + num_values: u32, + num_labels: u16, + num_frames: u16, + mem: []u8, + allocator: std.mem.Allocator, + + const AllocOpts = struct { + max_values: u32, + max_labels: u16, + max_frames: u16, + }; + + fn init(allocator: std.mem.Allocator) Stack { + var stack = Stack{ + .values = &[_]Val{}, + .labels = &[_]Label{}, + .frames = &[_]CallFrame{}, + .num_values = 0, + .num_labels = 0, + .num_frames = 0, + .mem = &[_]u8{}, + .allocator = allocator, + }; + + return stack; + } + + fn deinit(stack: *Stack) void { + if (stack.mem.len > 0) { + stack.allocator.free(stack.mem); + } + } + + fn allocMemory(stack: *Stack, opts: AllocOpts) !void { + const alignment = @max(@alignOf(Val), @alignOf(Label), @alignOf(CallFrame)); + const values_alloc_size = std.mem.alignForward(usize, @as(usize, @intCast(opts.max_values)) * @sizeOf(Val), alignment); + const labels_alloc_size = std.mem.alignForward(usize, @as(usize, @intCast(opts.max_labels)) * @sizeOf(Label), alignment); + const frames_alloc_size = std.mem.alignForward(usize, @as(usize, @intCast(opts.max_frames)) * @sizeOf(CallFrame), alignment); + const total_alloc_size: usize = values_alloc_size + labels_alloc_size + frames_alloc_size; + + const begin_labels = values_alloc_size; + const begin_frames = values_alloc_size + labels_alloc_size; + + stack.mem = try stack.allocator.alloc(u8, total_alloc_size); + stack.values.ptr = @as([*]Val, @alignCast(@ptrCast(stack.mem.ptr))); + stack.values.len = opts.max_values; + stack.labels.ptr = @as([*]Label, @alignCast(@ptrCast(stack.mem[begin_labels..].ptr))); + stack.labels.len = opts.max_labels; + stack.frames.ptr = @as([*]CallFrame, @alignCast(@ptrCast(stack.mem[begin_frames..].ptr))); + stack.frames.len = opts.max_frames; + } + + fn checkExhausted(stack: *Stack, extra_values: u32) !void { + if (stack.num_values + extra_values >= stack.values.len) { + return error.TrapStackExhausted; + } + } + + fn pushValue(stack: *Stack, value: Val) void { + stack.values[stack.num_values] = value; + stack.num_values += 1; + } + + fn pushI32(stack: *Stack, v: i32) void { + stack.values[stack.num_values] = Val{ .I32 = v }; + stack.num_values += 1; + } + + fn pushI64(stack: *Stack, v: i64) void { + stack.values[stack.num_values] = Val{ .I64 = v }; + stack.num_values += 1; + } + + fn pushF32(stack: *Stack, v: f32) void { + stack.values[stack.num_values] = Val{ .F32 = v }; + stack.num_values += 1; + } + + fn pushF64(stack: *Stack, v: f64) void { + stack.values[stack.num_values] = Val{ .F64 = v }; + stack.num_values += 1; + } + + fn pushV128(stack: *Stack, v: v128) void { + stack.values[stack.num_values] = Val{ .V128 = v }; + stack.num_values += 1; + } + + fn popValue(stack: *Stack) Val { + stack.num_values -= 1; + var value: Val = stack.values[stack.num_values]; + return value; + } + + fn topValue(stack: *const Stack) Val { + return stack.values[stack.num_values - 1]; + } + + fn popI32(stack: *Stack) i32 { + stack.num_values -= 1; + return stack.values[stack.num_values].I32; + } + + fn popI64(stack: *Stack) i64 { + stack.num_values -= 1; + return stack.values[stack.num_values].I64; + } + + fn popF32(stack: *Stack) f32 { + stack.num_values -= 1; + return stack.values[stack.num_values].F32; + } + + fn popF64(stack: *Stack) f64 { + stack.num_values -= 1; + return stack.values[stack.num_values].F64; + } + + fn popV128(stack: *Stack) v128 { + stack.num_values -= 1; + return stack.values[stack.num_values].V128; + } + + fn pushLabel(stack: *Stack, num_returns: u32, continuation: u32) !void { + if (stack.num_labels < stack.labels.len) { + stack.labels[stack.num_labels] = Label{ + .num_returns = num_returns, + .continuation = continuation, + .start_offset_values = stack.num_values, + }; + stack.num_labels += 1; + } else { + return error.TrapStackExhausted; + } + } + + fn popLabel(stack: *Stack) void { + stack.num_labels -= 1; + } + + fn findLabel(stack: *const Stack, id: u32) *const Label { + const index: usize = (stack.num_labels - 1) - id; + return &stack.labels[index]; + } + + fn topLabel(stack: *const Stack) *const Label { + return &stack.labels[stack.num_labels - 1]; + } + + fn frameLabel(stack: *const Stack) *const Label { + var frame: *const CallFrame = stack.topFrame(); + var frame_label: *const Label = &stack.labels[frame.start_offset_labels]; + return frame_label; + } + + fn popAllUntilLabelId(stack: *Stack, label_id: u64, pop_final_label: bool, num_returns: usize) void { + var label_index: u16 = @as(u16, @intCast((stack.num_labels - label_id) - 1)); + var label: *const Label = &stack.labels[label_index]; + + if (pop_final_label) { + const source_begin: usize = stack.num_values - num_returns; + const source_end: usize = stack.num_values; + const dest_begin: usize = label.start_offset_values; + const dest_end: usize = label.start_offset_values + num_returns; + + const returns_source: []const Val = stack.values[source_begin..source_end]; + const returns_dest: []Val = stack.values[dest_begin..dest_end]; + std.mem.copy(Val, returns_dest, returns_source); + + stack.num_values = @as(u32, @intCast(dest_end)); + stack.num_labels = label_index; + } else { + stack.num_values = label.start_offset_values; + stack.num_labels = label_index + 1; + } + } + + fn pushFrame(stack: *Stack, func: *const FunctionInstance, module_instance: *ModuleInstance, param_types: []const ValType, all_local_types: []const ValType, num_returns: u32) !void { + const non_param_types: []const ValType = all_local_types[param_types.len..]; + + // the stack should already be populated with the params to the function, so all that's + // left to do is initialize the locals to their default values + var values_index_begin: u32 = stack.num_values - @as(u32, @intCast(param_types.len)); + var values_index_end: u32 = stack.num_values + @as(u32, @intCast(non_param_types.len)); + + if (stack.num_frames < stack.frames.len and values_index_end < stack.values.len) { + var locals_and_params: []Val = stack.values[values_index_begin..values_index_end]; + var locals = stack.values[stack.num_values..values_index_end]; + + stack.num_values = values_index_end; + + for (non_param_types, 0..) |valtype, i| { + locals[i] = Val.default(valtype); + } + + stack.frames[stack.num_frames] = CallFrame{ + .func = func, + .module_instance = module_instance, + .locals = locals_and_params, + .num_returns = num_returns, + .start_offset_values = values_index_begin, + .start_offset_labels = stack.num_labels, + }; + stack.num_frames += 1; + } else { + return error.TrapStackExhausted; + } + } + + fn popFrame(stack: *Stack) ?FuncCallData { + var frame: *CallFrame = stack.topFrame(); + var frame_label: Label = stack.labels[frame.start_offset_labels]; + + const num_returns: usize = frame.num_returns; + const source_begin: usize = stack.num_values - num_returns; + const source_end: usize = stack.num_values; + const dest_begin: usize = frame.start_offset_values; + const dest_end: usize = frame.start_offset_values + num_returns; + + const returns_source: []const Val = stack.values[source_begin..source_end]; + const returns_dest: []Val = stack.values[dest_begin..dest_end]; + std.mem.copy(Val, returns_dest, returns_source); + + stack.num_values = @as(u32, @intCast(dest_end)); + stack.num_labels = frame.start_offset_labels; + stack.num_frames -= 1; + + if (stack.num_frames > 0) { + return FuncCallData{ + .code = stack.topFrame().module_instance.module_def.code.instructions.items.ptr, + .continuation = frame_label.continuation, + }; + } + + return null; + } + + fn topFrame(stack: *const Stack) *CallFrame { + return &stack.frames[stack.num_frames - 1]; + } + + fn popAll(stack: *Stack) void { + stack.num_values = 0; + stack.num_labels = 0; + stack.num_frames = 0; + } +}; + +// TODO move all definition stuff into definition.zig and vm stuff into vm_stack.zig + +// new idea: +// embed immediates with the opcodes in a stream of opaque data. each opcode knows how much data it uses and +// increments the program counter by that amount. so it would look something like this: +// op1 +// op1 immediate +// op2 (no immediate) +// op3 +// op3 imm1 +// op3 imm2 +// op3 imm3 +// +// this way the opcode and immediate data is all in cache in the same stream, but there are no wasted bytes +// due to union padding. +// could experiment with adding alignment padding later + +// const InstructionFunc = *const fn (pc: u32, code: [*]const u8, stack: *Stack) anyerror!void; + +// pc is the "program counter", which points to the next instruction to execute +const InstructionFunc = *const fn (pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void; + +// Maps all instructions to an execution function, to map opcodes directly to function pointers +// which avoids a giant switch statement. Because the switch-style has a single conditional +// branch for every opcode, the branch predictor cannot reliably predict the next opcode. However, +// giving each instruction its own branch allows the branch predictor to cache heuristics for each +// instruction, instead of a single branch. This approach is combined with tail calls to ensure the +// stack doesn't overflow and help optimize the generated asm. +// In the past, this style of opcode dispatch has been called the poorly-named "threaded code" approach. +// See the "continuation-passing style" section of this article: +// http://www.complang.tuwien.ac.at/forth/threaded-code.html +const InstructionFuncs = struct { + const opcodeToFuncTable = [_]InstructionFunc{ + &op_Invalid, + &op_Unreachable, + &op_DebugTrap, + &op_Noop, + &op_Block, + &op_Loop, + &op_If, + &op_IfNoElse, + &op_Else, + &op_End, + &op_Branch, + &op_Branch_If, + &op_Branch_Table, + &op_Return, + &op_Call, + &op_Call_Indirect, + &op_Drop, + &op_Select, + &op_Select_T, + &op_Local_Get, + &op_Local_Set, + &op_Local_Tee, + &op_Global_Get, + &op_Global_Set, + &op_Table_Get, + &op_Table_Set, + &op_I32_Load, + &op_I64_Load, + &op_F32_Load, + &op_F64_Load, + &op_I32_Load8_S, + &op_I32_Load8_U, + &op_I32_Load16_S, + &op_I32_Load16_U, + &op_I64_Load8_S, + &op_I64_Load8_U, + &op_I64_Load16_S, + &op_I64_Load16_U, + &op_I64_Load32_S, + &op_I64_Load32_U, + &op_I32_Store, + &op_I64_Store, + &op_F32_Store, + &op_F64_Store, + &op_I32_Store8, + &op_I32_Store16, + &op_I64_Store8, + &op_I64_Store16, + &op_I64_Store32, + &op_Memory_Size, + &op_Memory_Grow, + &op_I32_Const, + &op_I64_Const, + &op_F32_Const, + &op_F64_Const, + &op_I32_Eqz, + &op_I32_Eq, + &op_I32_NE, + &op_I32_LT_S, + &op_I32_LT_U, + &op_I32_GT_S, + &op_I32_GT_U, + &op_I32_LE_S, + &op_I32_LE_U, + &op_I32_GE_S, + &op_I32_GE_U, + &op_I64_Eqz, + &op_I64_Eq, + &op_I64_NE, + &op_I64_LT_S, + &op_I64_LT_U, + &op_I64_GT_S, + &op_I64_GT_U, + &op_I64_LE_S, + &op_I64_LE_U, + &op_I64_GE_S, + &op_I64_GE_U, + &op_F32_EQ, + &op_F32_NE, + &op_F32_LT, + &op_F32_GT, + &op_F32_LE, + &op_F32_GE, + &op_F64_EQ, + &op_F64_NE, + &op_F64_LT, + &op_F64_GT, + &op_F64_LE, + &op_F64_GE, + &op_I32_Clz, + &op_I32_Ctz, + &op_I32_Popcnt, + &op_I32_Add, + &op_I32_Sub, + &op_I32_Mul, + &op_I32_Div_S, + &op_I32_Div_U, + &op_I32_Rem_S, + &op_I32_Rem_U, + &op_I32_And, + &op_I32_Or, + &op_I32_Xor, + &op_I32_Shl, + &op_I32_Shr_S, + &op_I32_Shr_U, + &op_I32_Rotl, + &op_I32_Rotr, + &op_I64_Clz, + &op_I64_Ctz, + &op_I64_Popcnt, + &op_I64_Add, + &op_I64_Sub, + &op_I64_Mul, + &op_I64_Div_S, + &op_I64_Div_U, + &op_I64_Rem_S, + &op_I64_Rem_U, + &op_I64_And, + &op_I64_Or, + &op_I64_Xor, + &op_I64_Shl, + &op_I64_Shr_S, + &op_I64_Shr_U, + &op_I64_Rotl, + &op_I64_Rotr, + &op_F32_Abs, + &op_F32_Neg, + &op_F32_Ceil, + &op_F32_Floor, + &op_F32_Trunc, + &op_F32_Nearest, + &op_F32_Sqrt, + &op_F32_Add, + &op_F32_Sub, + &op_F32_Mul, + &op_F32_Div, + &op_F32_Min, + &op_F32_Max, + &op_F32_Copysign, + &op_F64_Abs, + &op_F64_Neg, + &op_F64_Ceil, + &op_F64_Floor, + &op_F64_Trunc, + &op_F64_Nearest, + &op_F64_Sqrt, + &op_F64_Add, + &op_F64_Sub, + &op_F64_Mul, + &op_F64_Div, + &op_F64_Min, + &op_F64_Max, + &op_F64_Copysign, + &op_I32_Wrap_I64, + &op_I32_Trunc_F32_S, + &op_I32_Trunc_F32_U, + &op_I32_Trunc_F64_S, + &op_I32_Trunc_F64_U, + &op_I64_Extend_I32_S, + &op_I64_Extend_I32_U, + &op_I64_Trunc_F32_S, + &op_I64_Trunc_F32_U, + &op_I64_Trunc_F64_S, + &op_I64_Trunc_F64_U, + &op_F32_Convert_I32_S, + &op_F32_Convert_I32_U, + &op_F32_Convert_I64_S, + &op_F32_Convert_I64_U, + &op_F32_Demote_F64, + &op_F64_Convert_I32_S, + &op_F64_Convert_I32_U, + &op_F64_Convert_I64_S, + &op_F64_Convert_I64_U, + &op_F64_Promote_F32, + &op_I32_Reinterpret_F32, + &op_I64_Reinterpret_F64, + &op_F32_Reinterpret_I32, + &op_F64_Reinterpret_I64, + &op_I32_Extend8_S, + &op_I32_Extend16_S, + &op_I64_Extend8_S, + &op_I64_Extend16_S, + &op_I64_Extend32_S, + &op_Ref_Null, + &op_Ref_Is_Null, + &op_Ref_Func, + &op_I32_Trunc_Sat_F32_S, + &op_I32_Trunc_Sat_F32_U, + &op_I32_Trunc_Sat_F64_S, + &op_I32_Trunc_Sat_F64_U, + &op_I64_Trunc_Sat_F32_S, + &op_I64_Trunc_Sat_F32_U, + &op_I64_Trunc_Sat_F64_S, + &op_I64_Trunc_Sat_F64_U, + &op_Memory_Init, + &op_Data_Drop, + &op_Memory_Copy, + &op_Memory_Fill, + &op_Table_Init, + &op_Elem_Drop, + &op_Table_Copy, + &op_Table_Grow, + &op_Table_Size, + &op_Table_Fill, + &op_V128_Load, + &op_V128_Load8x8_S, + &op_V128_Load8x8_U, + &op_V128_Load16x4_S, + &op_V128_Load16x4_U, + &op_V128_Load32x2_S, + &op_V128_Load32x2_U, + &op_V128_Load8_Splat, + &op_V128_Load16_Splat, + &op_V128_Load32_Splat, + &op_V128_Load64_Splat, + &op_V128_Store, + &op_V128_Const, + &op_I8x16_Shuffle, + &op_I8x16_Swizzle, + &op_I8x16_Splat, + &op_I16x8_Splat, + &op_I32x4_Splat, + &op_I64x2_Splat, + &op_F32x4_Splat, + &op_F64x2_Splat, + &op_I8x16_Extract_Lane_S, + &op_I8x16_Extract_Lane_U, + &op_I8x16_Replace_Lane, + &op_I16x8_Extract_Lane_S, + &op_I16x8_Extract_Lane_U, + &op_I16x8_Replace_Lane, + &op_I32x4_Extract_Lane, + &op_I32x4_Replace_Lane, + &op_I64x2_Extract_Lane, + &op_I64x2_Replace_Lane, + &op_F32x4_Extract_Lane, + &op_F32x4_Replace_Lane, + &op_F64x2_Extract_Lane, + &op_F64x2_Replace_Lane, + &op_I8x16_EQ, + &op_I8x16_NE, + &op_I8x16_LT_S, + &op_I8x16_LT_U, + &op_I8x16_GT_S, + &op_I8x16_GT_U, + &op_I8x16_LE_S, + &op_I8x16_LE_U, + &op_I8x16_GE_S, + &op_I8x16_GE_U, + &op_I16x8_EQ, + &op_I16x8_NE, + &op_I16x8_LT_S, + &op_I16x8_LT_U, + &op_I16x8_GT_S, + &op_I16x8_GT_U, + &op_I16x8_LE_S, + &op_I16x8_LE_U, + &op_I16x8_GE_S, + &op_I16x8_GE_U, + &op_I32x4_EQ, + &op_I32x4_NE, + &op_I32x4_LT_S, + &op_I32x4_LT_U, + &op_I32x4_GT_S, + &op_I32x4_GT_U, + &op_I32x4_LE_S, + &op_I32x4_LE_U, + &op_I32x4_GE_S, + &op_I32x4_GE_U, + &op_F32x4_EQ, + &op_F32x4_NE, + &op_F32x4_LT, + &op_F32x4_GT, + &op_F32x4_LE, + &op_F32x4_GE, + &op_F64x2_EQ, + &op_F64x2_NE, + &op_F64x2_LT, + &op_F64x2_GT, + &op_F64x2_LE, + &op_F64x2_GE, + &op_V128_Not, + &op_V128_And, + &op_V128_AndNot, + &op_V128_Or, + &op_V128_Xor, + &op_V128_Bitselect, + &op_V128_AnyTrue, + &op_V128_Load8_Lane, + &op_V128_Load16_Lane, + &op_V128_Load32_Lane, + &op_V128_Load64_Lane, + &op_V128_Store8_Lane, + &op_V128_Store16_Lane, + &op_V128_Store32_Lane, + &op_V128_Store64_Lane, + &op_V128_Load32_Zero, + &op_V128_Load64_Zero, + &op_F32x4_Demote_F64x2_Zero, + &op_F64x2_Promote_Low_F32x4, + &op_I8x16_Abs, + &op_I8x16_Neg, + &op_I8x16_Popcnt, + &op_I8x16_AllTrue, + &op_I8x16_Bitmask, + &op_I8x16_Narrow_I16x8_S, + &op_I8x16_Narrow_I16x8_U, + &op_F32x4_Ceil, + &op_F32x4_Floor, + &op_F32x4_Trunc, + &op_F32x4_Nearest, + &op_I8x16_Shl, + &op_I8x16_Shr_S, + &op_I8x16_Shr_U, + &op_I8x16_Add, + &op_I8x16_Add_Sat_S, + &op_I8x16_Add_Sat_U, + &op_I8x16_Sub, + &op_I8x16_Sub_Sat_S, + &op_I8x16_Sub_Sat_U, + &op_F64x2_Ceil, + &op_F64x2_Floor, + &op_I8x16_Min_S, + &op_I8x16_Min_U, + &op_I8x16_Max_S, + &op_I8x16_Max_U, + &op_F64x2_Trunc, + &op_I8x16_Avgr_U, + &op_I16x8_Extadd_Pairwise_I8x16_S, + &op_I16x8_Extadd_Pairwise_I8x16_U, + &op_I32x4_Extadd_Pairwise_I16x8_S, + &op_I32x4_Extadd_Pairwise_I16x8_U, + &op_I16x8_Abs, + &op_I16x8_Neg, + &op_I16x8_Q15mulr_Sat_S, + &op_I16x8_AllTrue, + &op_I16x8_Bitmask, + &op_I16x8_Narrow_I32x4_S, + &op_I16x8_Narrow_I32x4_U, + &op_I16x8_Extend_Low_I8x16_S, + &op_I16x8_Extend_High_I8x16_S, + &op_I16x8_Extend_Low_I8x16_U, + &op_I16x8_Extend_High_I8x16_U, + &op_I16x8_Shl, + &op_I16x8_Shr_S, + &op_I16x8_Shr_U, + &op_I16x8_Add, + &op_I16x8_Add_Sat_S, + &op_I16x8_Add_Sat_U, + &op_I16x8_Sub, + &op_I16x8_Sub_Sat_S, + &op_I16x8_Sub_Sat_U, + &op_F64x2_Nearest, + &op_I16x8_Mul, + &op_I16x8_Min_S, + &op_I16x8_Min_U, + &op_I16x8_Max_S, + &op_I16x8_Max_U, + &op_I16x8_Avgr_U, + &op_I16x8_Extmul_Low_I8x16_S, + &op_I16x8_Extmul_High_I8x16_S, + &op_I16x8_Extmul_Low_I8x16_U, + &op_I16x8_Extmul_High_I8x16_U, + &op_I32x4_Abs, + &op_I32x4_Neg, + &op_I32x4_AllTrue, + &op_I32x4_Bitmask, + &op_I32x4_Extend_Low_I16x8_S, + &op_I32x4_Extend_High_I16x8_S, + &op_I32x4_Extend_Low_I16x8_U, + &op_I32x4_Extend_High_I16x8_U, + &op_I32x4_Shl, + &op_I32x4_Shr_S, + &op_I32x4_Shr_U, + &op_I32x4_Add, + &op_I32x4_Sub, + &op_I32x4_Mul, + &op_I32x4_Min_S, + &op_I32x4_Min_U, + &op_I32x4_Max_S, + &op_I32x4_Max_U, + &op_I32x4_Dot_I16x8_S, + &op_I32x4_Extmul_Low_I16x8_S, + &op_I32x4_Extmul_High_I16x8_S, + &op_I32x4_Extmul_Low_I16x8_U, + &op_I32x4_Extmul_High_I16x8_U, + &op_I64x2_Abs, + &op_I64x2_Neg, + &op_I64x2_AllTrue, + &op_I64x2_Bitmask, + &op_I64x2_Extend_Low_I32x4_S, + &op_I64x2_Extend_High_I32x4_S, + &op_I64x2_Extend_Low_I32x4_U, + &op_I64x2_Extend_High_I32x4_U, + &op_I64x2_Shl, + &op_I64x2_Shr_S, + &op_I64x2_Shr_U, + &op_I64x2_Add, + &op_I64x2_Sub, + &op_I64x2_Mul, + &op_I64x2_EQ, + &op_I64x2_NE, + &op_I64x2_LT_S, + &op_I64x2_GT_S, + &op_I64x2_LE_S, + &op_I64x2_GE_S, + &op_I64x2_Extmul_Low_I32x4_S, + &op_I64x2_Extmul_High_I32x4_S, + &op_I64x2_Extmul_Low_I32x4_U, + &op_I64x2_Extmul_High_I32x4_U, + &op_F32x4_Abs, + &op_F32x4_Neg, + &op_F32x4_Sqrt, + &op_F32x4_Add, + &op_F32x4_Sub, + &op_F32x4_Mul, + &op_F32x4_Div, + &op_F32x4_Min, + &op_F32x4_Max, + &op_F32x4_PMin, + &op_F32x4_PMax, + &op_F64x2_Abs, + &op_F64x2_Neg, + &op_F64x2_Sqrt, + &op_F64x2_Add, + &op_F64x2_Sub, + &op_F64x2_Mul, + &op_F64x2_Div, + &op_F64x2_Min, + &op_F64x2_Max, + &op_F64x2_PMin, + &op_F64x2_PMax, + &op_F32x4_Trunc_Sat_F32x4_S, + &op_F32x4_Trunc_Sat_F32x4_U, + &op_F32x4_Convert_I32x4_S, + &op_F32x4_Convert_I32x4_U, + &op_I32x4_Trunc_Sat_F64x2_S_Zero, + &op_I32x4_Trunc_Sat_F64x2_U_Zero, + &op_F64x2_Convert_Low_I32x4_S, + &op_F64x2_Convert_Low_I32x4_U, + }; + + fn run(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try @call(.always_tail, InstructionFuncs.lookup(code[pc].opcode), .{ pc, code, stack }); + } + + fn lookup(opcode: Opcode) InstructionFunc { + return opcodeToFuncTable[@intFromEnum(opcode)]; + } + + const OpHelpers = struct { + const NanPropagateOp = enum { + Min, + Max, + }; + + fn propagateNanWithOp(comptime op: NanPropagateOp, v1: anytype, v2: @TypeOf(v1)) @TypeOf(v1) { + if (std.math.isNan(v1)) { + return v1; + } else if (std.math.isNan(v2)) { + return v2; + } else { + return switch (op) { + .Min => @min(v1, v2), + .Max => @max(v1, v2), + }; + } + } + + fn truncateTo(comptime T: type, value: anytype) !T { + switch (T) { + i32 => {}, + u32 => {}, + i64 => {}, + u64 => {}, + else => @compileError("Only i32 and i64 are supported outputs."), + } + switch (@TypeOf(value)) { + f32 => {}, + f64 => {}, + else => @compileError("Only f32 and f64 are supported inputs."), + } + + var truncated = @trunc(value); + + if (std.math.isNan(truncated)) { + return error.TrapInvalidIntegerConversion; + } else if (truncated < std.math.minInt(T)) { + return error.TrapIntegerOverflow; + } else { + if (@typeInfo(T).Int.bits < @typeInfo(@TypeOf(truncated)).Float.bits) { + if (truncated > std.math.maxInt(T)) { + return error.TrapIntegerOverflow; + } + } else { + if (truncated >= std.math.maxInt(T)) { + return error.TrapIntegerOverflow; + } + } + } + return @as(T, @intFromFloat(truncated)); + } + + fn saturatedTruncateTo(comptime T: type, value: anytype) T { + switch (T) { + i32 => {}, + u32 => {}, + i64 => {}, + u64 => {}, + else => @compileError("Only i32 and i64 are supported outputs."), + } + switch (@TypeOf(value)) { + f32 => {}, + f64 => {}, + else => @compileError("Only f32 and f64 are supported inputs."), + } + + var truncated = @trunc(value); + + if (std.math.isNan(truncated)) { + return 0; + } else if (truncated < std.math.minInt(T)) { + return std.math.minInt(T); + } else { + if (@typeInfo(T).Int.bits < @typeInfo(@TypeOf(truncated)).Float.bits) { + if (truncated > std.math.maxInt(T)) { + return std.math.maxInt(T); + } + } else { + if (truncated >= std.math.maxInt(T)) { + return std.math.maxInt(T); + } + } + } + return @as(T, @intFromFloat(truncated)); + } + + fn loadFromMem(comptime T: type, store: *Store, offset_from_memarg: usize, offset_from_stack: i32) !T { + if (offset_from_stack < 0) { + return error.TrapOutOfBoundsMemoryAccess; + } + + const memory: *const MemoryInstance = store.getMemory(0); + const offset: usize = offset_from_memarg + @as(usize, @intCast(offset_from_stack)); + + const bit_count = @bitSizeOf(T); + const read_type = switch (bit_count) { + 8 => u8, + 16 => u16, + 32 => u32, + 64 => u64, + 128 => u128, + else => @compileError("Only types with bit counts of 8, 16, 32, or 64 are supported."), + }; + + const end = offset + (bit_count / 8); + + const buffer = memory.buffer(); + if (buffer.len < end) { + return error.TrapOutOfBoundsMemoryAccess; + } + + const mem = buffer[offset..end]; + const value = std.mem.readIntSliceLittle(read_type, mem); + return @as(T, @bitCast(value)); + } + + fn loadArrayFromMem(comptime read_type: type, comptime out_type: type, comptime array_len: usize, store: *Store, offset_from_memarg: usize, offset_from_stack: i32) ![array_len]out_type { + if (offset_from_stack < 0) { + return error.TrapOutOfBoundsMemoryAccess; + } + + const memory: *const MemoryInstance = store.getMemory(0); + const offset: usize = offset_from_memarg + @as(usize, @intCast(offset_from_stack)); + + const byte_count = @sizeOf(read_type); + const end = offset + (byte_count * array_len); + + const buffer = memory.buffer(); + if (buffer.len < end) { + return error.TrapOutOfBoundsMemoryAccess; + } + + var ret: [array_len]out_type = undefined; + const mem = buffer[offset..end]; + var i: usize = 0; + while (i < array_len) : (i += 1) { + const value_start = i * byte_count; + const value_end = value_start + byte_count; + ret[i] = std.mem.readIntSliceLittle(read_type, mem[value_start..value_end]); + } + return ret; + } + + fn storeInMem(value: anytype, store: *Store, offset_from_memarg: usize, offset_from_stack: i32) !void { + if (offset_from_stack < 0) { + return error.TrapOutOfBoundsMemoryAccess; + } + + const memory: *MemoryInstance = store.getMemory(0); + const offset: usize = offset_from_memarg + @as(u32, @intCast(offset_from_stack)); + + const bit_count = @bitSizeOf(@TypeOf(value)); + const write_type = switch (bit_count) { + 8 => u8, + 16 => u16, + 32 => u32, + 64 => u64, + 128 => u128, + else => @compileError("Only types with bit counts of 8, 16, 32, or 64 are supported."), + }; + + const end = offset + (bit_count / 8); + const buffer = memory.buffer(); + + if (buffer.len < end) { + return error.TrapOutOfBoundsMemoryAccess; + } + + const write_value = @as(write_type, @bitCast(value)); + + const mem = buffer[offset..end]; + std.mem.writeIntSliceLittle(write_type, mem, write_value); + } + + fn call(pc: u32, stack: *Stack, module_instance: *ModuleInstance, func: *const FunctionInstance) !FuncCallData { + const functype: *const FunctionTypeDefinition = &module_instance.module_def.types.items[func.type_def_index]; + const param_types: []const ValType = functype.getParams(); + const return_types: []const ValType = functype.getReturns(); + const continuation: u32 = pc + 1; + + try stack.pushFrame(func, module_instance, param_types, func.local_types.items, functype.calcNumReturns()); + try stack.pushLabel(@as(u32, @intCast(return_types.len)), continuation); + + DebugTrace.traceFunction(module_instance, stack.num_frames, func.def_index); + + return FuncCallData{ + .code = module_instance.module_def.code.instructions.items.ptr, + .continuation = @intCast(func.instructions_begin), + }; + } + + fn callImport(pc: u32, stack: *Stack, func: *const FunctionImport) !FuncCallData { + switch (func.data) { + .Host => |data| { + const params_len: u32 = @as(u32, @intCast(data.func_def.getParams().len)); + const returns_len: u32 = @as(u32, @intCast(data.func_def.calcNumReturns())); + + if (stack.num_values + returns_len < stack.values.len) { + var module: *ModuleInstance = stack.topFrame().module_instance; + var params = stack.values[stack.num_values - params_len .. stack.num_values]; + var returns_temp = stack.values[stack.num_values .. stack.num_values + returns_len]; + + DebugTrace.traceHostFunction(module, stack.num_frames + 1, func.name); + + data.callback(data.userdata, module, params.ptr, returns_temp.ptr); + + stack.num_values = (stack.num_values - params_len) + returns_len; + var returns_dest = stack.values[stack.num_values - returns_len .. stack.num_values]; + + std.mem.copy(Val, returns_dest, returns_temp); + + return FuncCallData{ + .code = stack.topFrame().module_instance.module_def.code.instructions.items.ptr, + .continuation = pc + 1, + }; + } else { + return error.TrapStackExhausted; + } + }, + .Wasm => |data| { + var stack_vm: *StackVM = StackVM.fromVM(data.module_instance.vm); + const func_instance: *const FunctionInstance = &stack_vm.functions.items[data.index]; + return try call(pc, stack, data.module_instance, func_instance); + }, + } + } + + fn branch(stack: *Stack, label_id: u32) ?FuncCallData { + const label: *const Label = stack.findLabel(@as(u32, @intCast(label_id))); + const frame_label: *const Label = stack.frameLabel(); + // TODO generate BranchToFunctionEnd up if this can be statically determined at decode time (or just generate a Return?) + if (label == frame_label) { + return stack.popFrame(); + } + + // TODO split branches up into different types to avoid this lookup and if statement + const module_def: *const ModuleDefinition = stack.topFrame().module_instance.module_def; + const is_loop_continuation: bool = module_def.code.instructions.items[label.continuation].opcode == .Loop; + + if (is_loop_continuation == false or label_id != 0) { + const pop_final_label = !is_loop_continuation; + stack.popAllUntilLabelId(label_id, pop_final_label, label.num_returns); + } + + return FuncCallData{ + .code = stack.topFrame().module_instance.module_def.code.instructions.items.ptr, + .continuation = label.continuation + 1, // branching takes care of popping/pushing values so skip the End instruction + }; + } + + const VectorUnaryOp = enum(u8) { + Ceil, + Floor, + Trunc, + Nearest, + }; + + fn vectorUnOp(comptime T: type, op: VectorUnaryOp, stack: *Stack) void { + const vec = @as(T, @bitCast(stack.popV128())); + const type_info = @typeInfo(T).Vector; + const child_type = type_info.child; + const result = switch (op) { + .Ceil => @ceil(vec), + .Floor => @floor(vec), + .Trunc => @trunc(vec), + .Nearest => blk: { + const zeroes: T = @splat(0); + const twos: T = @splat(2); + + const ceil = @ceil(vec); + const floor = @floor(vec); + const is_half = (ceil - vec) == (vec - floor); + const evens = @select(child_type, @mod(ceil, twos) == zeroes, ceil, floor); + const rounded = @round(vec); + break :blk @select(child_type, is_half, evens, rounded); + }, + }; + stack.pushV128(@as(v128, @bitCast(result))); + } + + const VectorBinaryOp = enum(u8) { + Add, + Add_Sat, + Sub, + Sub_Sat, + Mul, + Div, + Min, + PMin, + Max, + PMax, + And, + AndNot, + Or, + Xor, + }; + + fn vectorOr(comptime len: usize, v1: @Vector(len, bool), v2: @Vector(len, bool)) @Vector(len, bool) { + var arr: [len]bool = undefined; + for (&arr, 0..) |*v, i| { + v.* = v1[i] or v2[i]; + } + return arr; + } + + fn vectorBinOp(comptime T: type, comptime op: VectorBinaryOp, stack: *Stack) void { + const type_info = @typeInfo(T).Vector; + const child_type = type_info.child; + const v2 = @as(T, @bitCast(stack.popV128())); + const v1 = @as(T, @bitCast(stack.popV128())); + const result = switch (op) { + .Add => blk: { + break :blk switch (@typeInfo(child_type)) { + .Int => v1 +% v2, + .Float => v1 + v2, + else => unreachable, + }; + }, + .Add_Sat => v1 +| v2, + .Sub => blk: { + break :blk switch (@typeInfo(child_type)) { + .Int => v1 -% v2, + .Float => v1 - v2, + else => unreachable, + }; + }, + .Sub_Sat => v1 -| v2, + .Mul => blk: { + break :blk switch (@typeInfo(child_type)) { + .Int => v1 *% v2, + .Float => v1 * v2, + else => unreachable, + }; + }, + .Div => v1 / v2, + .Min => blk: { + break :blk switch (@typeInfo(child_type)) { + .Int => @min(v1, v2), + .Float => blk2: { + const is_nan = v1 != v1; + const is_min = v1 < v2; + const pred = vectorOr(type_info.len, is_nan, is_min); + const r = @select(child_type, pred, v1, v2); + break :blk2 r; + }, + else => unreachable, + }; + }, + .PMin => @select(child_type, (v2 < v1), v2, v1), + .Max => blk: { + break :blk switch (@typeInfo(child_type)) { + .Int => @max(v1, v2), + .Float => blk2: { + const is_nan = v1 != v1; + const is_min = v1 > v2; + const pred = vectorOr(type_info.len, is_nan, is_min); + const r = @select(child_type, pred, v1, v2); + break :blk2 r; + }, + else => unreachable, + }; + }, + .PMax => @select(child_type, (v2 > v1), v2, v1), + .And => v1 & v2, + .AndNot => v1 & (~v2), + .Or => v1 | v2, + .Xor => v1 ^ v2, + }; + stack.pushV128(@as(v128, @bitCast(result))); + } + + fn vectorAbs(comptime T: type, stack: *Stack) void { + const type_info = @typeInfo(T).Vector; + const child_type = type_info.child; + const vec = @as(T, @bitCast(stack.popV128())); + var arr: [type_info.len]child_type = undefined; + for (&arr, 0..) |*v, i| { + v.* = @as(child_type, @bitCast(std.math.absCast(vec[i]))); + } + const abs: T = arr; + stack.pushV128(@as(v128, @bitCast(abs))); + } + + fn vectorAvgrU(comptime T: type, stack: *Stack) void { + const type_info = @typeInfo(T).Vector; + const child_type = type_info.child; + const type_big_width = std.meta.Int(.unsigned, @bitSizeOf(child_type) * 2); + + const v1 = @as(T, @bitCast(stack.popV128())); + const v2 = @as(T, @bitCast(stack.popV128())); + var arr: [type_info.len]child_type = undefined; + for (&arr, 0..) |*v, i| { + const vv1: type_big_width = v1[i]; + const vv2: type_big_width = v2[i]; + v.* = @as(child_type, @intCast(@divTrunc(vv1 + vv2 + 1, 2))); + } + const result: T = arr; + stack.pushV128(@as(v128, @bitCast(result))); + } + + const VectorBoolOp = enum(u8) { + Eq, + Ne, + Lt, + Gt, + Le, + Ge, + }; + + fn vectorBoolOp(comptime T: type, comptime op: VectorBoolOp, stack: *Stack) void { + const v2 = @as(T, @bitCast(stack.popV128())); + const v1 = @as(T, @bitCast(stack.popV128())); + const bools = switch (op) { + .Eq => v1 == v2, + .Ne => v1 != v2, + .Lt => v1 < v2, + .Gt => v1 > v2, + .Le => v1 <= v2, + .Ge => v1 >= v2, + }; + const vec_type_info = @typeInfo(T).Vector; + + const no_bits: std.meta.Int(.unsigned, @bitSizeOf(vec_type_info.child)) = 0; + const yes_bits = ~no_bits; + + const yes_vec: T = @splat(@bitCast(yes_bits)); + const no_vec: T = @splat(@bitCast(no_bits)); + const result: T = @select(vec_type_info.child, bools, yes_vec, no_vec); + stack.pushV128(@as(v128, @bitCast(result))); + } + + const VectorShiftDirection = enum { + Left, + Right, + }; + + fn vectorShift(comptime T: type, comptime direction: VectorShiftDirection, stack: *Stack) void { + const shift_unsafe: i32 = stack.popI32(); + const vec = @as(T, @bitCast(stack.popV128())); + const shift_safe = std.math.mod(i32, shift_unsafe, @bitSizeOf(@typeInfo(T).Vector.child)) catch unreachable; + const shift_fn = if (direction == .Left) std.math.shl else std.math.shr; + const shifted = shift_fn(T, vec, shift_safe); + stack.pushV128(@as(v128, @bitCast(shifted))); + } + + fn vectorAllTrue(comptime T: type, vec: v128) i32 { + const v = @as(T, @bitCast(vec)); + const zeroes: T = @splat(0); + const bools = v != zeroes; + const any_true: bool = @reduce(.And, bools); + return if (any_true) 1 else 0; + } + + fn vectorBitmask(comptime T: type, vec: v128) i32 { + switch (@typeInfo(T)) { + .Vector => |vec_type_info| { + switch (@typeInfo(vec_type_info.child)) { + .Int => {}, + else => @compileError("Vector child type must be an int"), + } + }, + else => @compileError("Expected T to be a vector type"), + } + + const child_type: type = @typeInfo(T).Vector.child; + + if (child_type == i8) { + const high_bit: u8 = 1 << (@bitSizeOf(u8) - 1); + const high_bits_mask: @Vector(16, u8) = @splat(high_bit); + + const shift_type = std.meta.Int(.unsigned, std.math.log2(@bitSizeOf(u16))); + const shifts_left: @Vector(16, shift_type) = @splat(8); + var shifts_right_array: [16]shift_type = undefined; + for (&shifts_right_array, 0..) |*element, i| { + element.* = @as(shift_type, @intCast(15 - i)); + } + const shifts_right = @as(@Vector(16, shift_type), shifts_right_array); + + const v = @as(@Vector(16, u8), @bitCast(vec)); + const v_high_bits = high_bits_mask & v; + const v_high_bits_u16: @Vector(16, u16) = v_high_bits; + const v_high_bits_shifted_left = @shlExact(v_high_bits_u16, shifts_left); + const v_high_bits_shifted_right = @shrExact(v_high_bits_shifted_left, shifts_right); + const reduction: u32 = @reduce(.Or, v_high_bits_shifted_right); + const bitmask = @as(i32, @bitCast(reduction)); + return bitmask; + } else { + const vec_len = @typeInfo(T).Vector.len; + const int_type: type = std.meta.Int(.unsigned, @bitSizeOf(child_type)); + + const high_bit: int_type = 1 << (@bitSizeOf(int_type) - 1); + const high_bits_mask: @Vector(vec_len, int_type) = @splat(high_bit); + + const shift_type = std.meta.Int(.unsigned, std.math.log2(@bitSizeOf(int_type))); + var shifts_right_array: [vec_len]shift_type = undefined; + for (&shifts_right_array, 0..) |*element, i| { + element.* = @as(shift_type, @intCast((@bitSizeOf(int_type) - 1) - i)); + } + const shifts_right = @as(@Vector(vec_len, shift_type), shifts_right_array); + + const v = @as(@Vector(vec_len, int_type), @bitCast(vec)); + const v_high_bits = high_bits_mask & v; + const v_high_bits_shifted_right = @shrExact(v_high_bits, shifts_right); + const reduction: u32 = @as(u32, @intCast(@reduce(.Or, v_high_bits_shifted_right))); // cast should be fine thanks to the rshift + const bitmask = @as(i32, @bitCast(reduction)); + return bitmask; + } + } + + fn vectorLoadLane(comptime T: type, instruction: Instruction, stack: *Stack) !void { + const vec_type_info = @typeInfo(T).Vector; + + var vec = @as(T, @bitCast(stack.popV128())); + const immediate = instruction.immediate.MemoryOffsetAndLane; + const offset_from_stack: i32 = stack.popI32(); + const scalar = try loadFromMem(vec_type_info.child, &stack.topFrame().module_instance.store, immediate.offset, offset_from_stack); + vec[immediate.laneidx] = scalar; + stack.pushV128(@as(v128, @bitCast(vec))); + } + + fn vectorLoadExtend(comptime mem_type: type, comptime extend_type: type, comptime len: usize, mem_offset: usize, stack: *Stack) !void { + const offset_from_stack: i32 = stack.popI32(); + const array: [len]extend_type = try OpHelpers.loadArrayFromMem(mem_type, extend_type, len, &stack.topFrame().module_instance.store, mem_offset, offset_from_stack); + const vec: @Vector(len, extend_type) = array; + stack.pushV128(@as(v128, @bitCast(vec))); + } + + fn vectorLoadLaneZero(comptime T: type, instruction: Instruction, stack: *Stack) !void { + const vec_type_info = @typeInfo(T).Vector; + + const mem_offset = instruction.immediate.MemoryOffset; + const offset_from_stack: i32 = stack.popI32(); + const scalar = try loadFromMem(vec_type_info.child, &stack.topFrame().module_instance.store, mem_offset, offset_from_stack); + var vec: T = @splat(0); + vec[0] = scalar; + stack.pushV128(@as(v128, @bitCast(vec))); + } + + fn vectorStoreLane(comptime T: type, instruction: Instruction, stack: *Stack) !void { + var vec = @as(T, @bitCast(stack.popV128())); + const immediate = instruction.immediate.MemoryOffsetAndLane; + const offset_from_stack: i32 = stack.popI32(); + const scalar = vec[immediate.laneidx]; + try storeInMem(scalar, &stack.topFrame().module_instance.store, immediate.offset, offset_from_stack); + stack.pushV128(@as(v128, @bitCast(vec))); + } + + fn vectorExtractLane(comptime T: type, lane: u32, stack: *Stack) void { + const vec = @as(T, @bitCast(stack.popV128())); + const lane_value = vec[lane]; + + const child_type = @typeInfo(T).Vector.child; + switch (child_type) { + i8, u8, i16, u16, i32 => stack.pushI32(lane_value), + i64 => stack.pushI64(lane_value), + f32 => stack.pushF32(lane_value), + f64 => stack.pushF64(lane_value), + else => unreachable, + } + } + + fn vectorReplaceLane(comptime T: type, lane: u32, stack: *Stack) void { + const child_type = @typeInfo(T).Vector.child; + const lane_value = switch (child_type) { + i8, i16, i32 => @as(child_type, @truncate(stack.popI32())), + i64 => stack.popI64(), + f32 => stack.popF32(), + f64 => stack.popF64(), + else => unreachable, + }; + var vec = @as(T, @bitCast(stack.popV128())); + vec[lane] = lane_value; + stack.pushV128(@as(v128, @bitCast(vec))); + } + + const VectorSide = enum { + Low, + High, + }; + + const VectorConvert = enum { + SafeCast, + Saturate, + }; + + fn vectorAddPairwise(comptime in_type: type, comptime out_type: type, stack: *Stack) void { + const out_info = @typeInfo(out_type).Vector; + + const vec = @as(in_type, @bitCast(stack.popV128())); + var arr: [out_info.len]out_info.child = undefined; + for (&arr, 0..) |*v, i| { + const v1: out_info.child = vec[i * 2]; + const v2: out_info.child = vec[(i * 2) + 1]; + v.* = v1 + v2; + } + const sum: out_type = arr; + stack.pushV128(@as(v128, @bitCast(sum))); + } + + fn vectorMulPairwise(comptime in_type: type, comptime out_type: type, side: OpHelpers.VectorSide, stack: *Stack) void { + const info_out = @typeInfo(out_type).Vector; + + const vec2 = @as(in_type, @bitCast(stack.popV128())); + const vec1 = @as(in_type, @bitCast(stack.popV128())); + + var arr: [info_out.len]info_out.child = undefined; + for (&arr, 0..) |*v, i| { + const index = if (side == .Low) i else i + info_out.len; + const v1: info_out.child = vec1[index]; + const v2: info_out.child = vec2[index]; + v.* = v1 * v2; + } + const product = arr; + stack.pushV128(@as(v128, @bitCast(product))); + } + + fn vectorExtend(comptime in_type: type, comptime out_type: type, comptime side: VectorSide, stack: *Stack) void { + const in_info = @typeInfo(in_type).Vector; + const out_info = @typeInfo(out_type).Vector; + const side_offset = if (side == .Low) 0 else in_info.len / 2; + + const vec = @as(in_type, @bitCast(stack.popV128())); + var arr: [out_info.len]out_info.child = undefined; + for (&arr, 0..) |*v, i| { + v.* = vec[i + side_offset]; + } + const extended: out_type = arr; + stack.pushV128(@as(v128, @bitCast(extended))); + } + + fn saturate(comptime T: type, v: anytype) @TypeOf(v) { + switch (@typeInfo(T)) { + .Int => {}, + else => unreachable, + } + const min = std.math.minInt(T); + const max = std.math.maxInt(T); + const clamped = std.math.clamp(v, min, max); + return clamped; + } + + fn vectorConvert(comptime in_type: type, comptime out_type: type, comptime side: VectorSide, convert: VectorConvert, stack: *Stack) void { + const in_info = @typeInfo(in_type).Vector; + const out_info = @typeInfo(out_type).Vector; + const side_offset = if (side == .Low) 0 else in_info.len / 2; + + const vec_in = @as(in_type, @bitCast(stack.popV128())); + var arr: [out_info.len]out_info.child = undefined; + for (arr, 0..) |_, i| { + const v: in_info.child = if (i < in_info.len) vec_in[i + side_offset] else 0; + switch (@typeInfo(out_info.child)) { + .Int => arr[i] = blk: { + if (convert == .SafeCast) { + break :blk @as(out_info.child, @intFromFloat(v)); + } else { + break :blk saturatedTruncateTo(out_info.child, v); + } + }, + .Float => arr[i] = @as(out_info.child, @floatFromInt(v)), + else => unreachable, + } + } + const vec_out: out_type = arr; + stack.pushV128(@as(v128, @bitCast(vec_out))); + } + + fn vectorNarrowingSaturate(comptime in_type: type, comptime out_type: type, vec: in_type) out_type { + const in_info = @typeInfo(in_type).Vector; + const out_info = @typeInfo(out_type).Vector; + const T: type = out_info.child; + + std.debug.assert(out_info.len == in_info.len); + + var arr: [out_info.len]T = undefined; + for (&arr, 0..) |*v, i| { + v.* = @as(T, @intCast(std.math.clamp(vec[i], std.math.minInt(T), std.math.maxInt(T)))); + } + return arr; + } + + fn vectorNarrow(comptime in_type: type, comptime out_type: type, stack: *Stack) void { + const out_info = @typeInfo(out_type).Vector; + + const out_type_half = @Vector(out_info.len / 2, out_info.child); + + const v2 = @as(in_type, @bitCast(stack.popV128())); + const v1 = @as(in_type, @bitCast(stack.popV128())); + const v1_narrow: out_type_half = vectorNarrowingSaturate(in_type, out_type_half, v1); + const v2_narrow: out_type_half = vectorNarrowingSaturate(in_type, out_type_half, v2); + const mask = switch (out_info.len) { + 16 => @Vector(16, i32){ 0, 1, 2, 3, 4, 5, 6, 7, -1, -2, -3, -4, -5, -6, -7, -8 }, + 8 => @Vector(8, i32){ 0, 1, 2, 3, -1, -2, -3, -4 }, + 4 => @Vector(8, i32){ 0, 1, -1, -2 }, + else => unreachable, + }; + + const mix = @shuffle(out_info.child, v1_narrow, v2_narrow, mask); + stack.pushV128(@as(v128, @bitCast(mix))); + } + }; + + fn debugPreamble(name: []const u8, pc: u32, code: [*]const Instruction, stack: *Stack) TrapError!void { + _ = code; + + const root_module_instance: *ModuleInstance = stack.frames[0].module_instance; + const root_stackvm: *StackVM = StackVM.fromVM(root_module_instance.vm); + if (root_stackvm.debug_state) |*debug_state| { + if (debug_state.trap_counter > 0) { + debug_state.trap_counter -= 1; + if (debug_state.trap_counter == 0) { + debug_state.pc = pc; + return error.TrapDebug; + } + } + } + + DebugTraceStackVM.traceInstruction(name, pc, stack); + } + + fn op_Invalid(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Invalid", pc, code, stack); + unreachable; + } + + fn op_Unreachable(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Unreachable", pc, code, stack); + return error.TrapUnreachable; + } + + fn op_DebugTrap(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("DebugTrap", pc, code, stack); + var root_module_instance: *ModuleInstance = stack.frames[0].module_instance; + const stack_vm = StackVM.fromVM(root_module_instance.vm); + + std.debug.assert(stack_vm.debug_state != null); + stack_vm.debug_state.?.pc = pc; + + return error.TrapDebug; + } + + fn op_Noop(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Noop", pc, code, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Block(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Block", pc, code, stack); + try stack.pushLabel(code[pc].immediate.Block.num_returns, code[pc].immediate.Block.continuation); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Loop(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Loop", pc, code, stack); + try stack.pushLabel(code[pc].immediate.Block.num_returns, code[pc].immediate.Block.continuation); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_If(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("If", pc, code, stack); + var next_pc: u32 = undefined; + + const condition = stack.popI32(); + if (condition != 0) { + try stack.pushLabel(code[pc].immediate.If.num_returns, code[pc].immediate.If.end_continuation); + next_pc = pc + 1; + } else { + try stack.pushLabel(code[pc].immediate.If.num_returns, code[pc].immediate.If.end_continuation); + next_pc = code[pc].immediate.If.else_continuation + 1; + } + + try @call(.always_tail, InstructionFuncs.lookup(code[next_pc].opcode), .{ next_pc, code, stack }); + } + + fn op_IfNoElse(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("IfNoElse", pc, code, stack); + var next_pc: u32 = undefined; + + const condition = stack.popI32(); + if (condition != 0) { + try stack.pushLabel(code[pc].immediate.If.num_returns, code[pc].immediate.If.end_continuation); + next_pc = pc + 1; + } else { + next_pc = code[pc].immediate.If.else_continuation + 1; + } + + try @call(.always_tail, InstructionFuncs.lookup(code[next_pc].opcode), .{ next_pc, code, stack }); + } + + fn op_Else(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Else", pc, code, stack); + // getting here means we reached the end of the if opcode chain, so skip to the true end opcode + var next_pc: u32 = code[pc].immediate.If.end_continuation; + try @call(.always_tail, InstructionFuncs.lookup(code[next_pc].opcode), .{ next_pc, code, stack }); + } + + fn op_End(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("End", pc, code, stack); + + // TODO - this instruction tries to determine at runtime what behavior to take, but we can + // probably determine this in the decode phase and split into 3 different end instructions + // to avoid branching. Probably could bake the return types length into the immediate to avoid + // cache misses on the lookup we're currently doing. + + var next: FuncCallData = undefined; + + // determine if this is a a scope or function call exit + const top_label: *const Label = stack.topLabel(); + const frame_label: *const Label = stack.frameLabel(); + if (top_label != frame_label) { + // Since the only values on the stack should be the returns from the block, we just pop the + // label, which leaves the value stack alone. + stack.popLabel(); + + next = FuncCallData{ + .continuation = pc + 1, + .code = code, + }; + } else { + next = stack.popFrame() orelse return; + } + + try @call(.always_tail, InstructionFuncs.lookup(next.code[next.continuation].opcode), .{ next.continuation, next.code, stack }); + } + + fn op_Branch(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Branch", pc, code, stack); + const label_id: u32 = code[pc].immediate.LabelId; + const next: FuncCallData = OpHelpers.branch(stack, label_id) orelse return; + try @call(.always_tail, InstructionFuncs.lookup(next.code[next.continuation].opcode), .{ next.continuation, next.code, stack }); + } + + fn op_Branch_If(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Branch_If", pc, code, stack); + var next: FuncCallData = undefined; + const v = stack.popI32(); + if (v != 0) { + const label_id: u32 = code[pc].immediate.LabelId; + next = OpHelpers.branch(stack, label_id) orelse return; + } else { + next = FuncCallData{ + .code = code, + .continuation = pc + 1, + }; + } + try @call(.always_tail, InstructionFuncs.lookup(next.code[next.continuation].opcode), .{ next.continuation, next.code, stack }); + } + + fn op_Branch_Table(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Branch_Table", pc, code, stack); + const immediates: *const BranchTableImmediates = &stack.topFrame().module_instance.module_def.code.branch_table.items[code[pc].immediate.Index]; + const table: []const u32 = immediates.label_ids.items; + + const label_index = stack.popI32(); + const label_id: u32 = if (label_index >= 0 and label_index < table.len) table[@as(usize, @intCast(label_index))] else immediates.fallback_id; + const next: FuncCallData = OpHelpers.branch(stack, label_id) orelse return; + + try @call(.always_tail, InstructionFuncs.lookup(next.code[next.continuation].opcode), .{ next.continuation, next.code, stack }); + } + + fn op_Return(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Return", pc, code, stack); + var next: FuncCallData = stack.popFrame() orelse return; + try @call(.always_tail, InstructionFuncs.lookup(next.code[next.continuation].opcode), .{ next.continuation, next.code, stack }); + } + + fn op_Call(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Call", pc, code, stack); + + const func_index: u32 = code[pc].immediate.Index; + const module_instance: *ModuleInstance = stack.topFrame().module_instance; + const store: *const Store = &module_instance.store; + const stack_vm = StackVM.fromVM(module_instance.vm); + + var next: FuncCallData = undefined; + if (func_index >= store.imports.functions.items.len) { + const func_instance_index = func_index - store.imports.functions.items.len; + const func: *const FunctionInstance = &stack_vm.functions.items[@as(usize, @intCast(func_instance_index))]; + next = try OpHelpers.call(pc, stack, module_instance, func); + } else { + var func_import = &store.imports.functions.items[func_index]; + next = try OpHelpers.callImport(pc, stack, func_import); + } + + try @call(.always_tail, InstructionFuncs.lookup(next.code[next.continuation].opcode), .{ next.continuation, next.code, stack }); + } + + fn op_Call_Indirect(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Call_Indirect", pc, code, stack); + + const current_module: *ModuleInstance = stack.topFrame().module_instance; + const immediates: *const CallIndirectImmediates = &code[pc].immediate.CallIndirect; + const table_index: u32 = immediates.table_index; + + const table: *const TableInstance = current_module.store.getTable(table_index); + + const ref_index = stack.popI32(); + if (table.refs.items.len <= ref_index or ref_index < 0) { + return error.TrapUndefinedElement; + } + + const ref: Val = table.refs.items[@as(usize, @intCast(ref_index))]; + if (ref.isNull()) { + return error.TrapUninitializedElement; + } + + const func_index = ref.FuncRef.index; + + std.debug.assert(ref.FuncRef.module_instance != null); // Should have been set in module instantiation + + var call_module: *ModuleInstance = ref.FuncRef.module_instance.?; + var call_store = &call_module.store; + var call_stackvm = StackVM.fromVM(call_module.vm); + + var next: FuncCallData = undefined; + if (func_index >= call_store.imports.functions.items.len) { + const func: *const FunctionInstance = &call_stackvm.functions.items[func_index - call_store.imports.functions.items.len]; + if (func.type_def_index != immediates.type_index) { + const func_type_def: *const FunctionTypeDefinition = &call_module.module_def.types.items[func.type_def_index]; + const immediate_type_def: *const FunctionTypeDefinition = &call_module.module_def.types.items[immediates.type_index]; + + var type_comparer = FunctionTypeDefinition.SortContext{}; + if (type_comparer.eql(func_type_def, immediate_type_def) == false) { + return error.TrapIndirectCallTypeMismatch; + } + } + next = try OpHelpers.call(pc, stack, call_module, func); + } else { + var func_import: *const FunctionImport = &call_store.imports.functions.items[func_index]; + var func_type_def: *const FunctionTypeDefinition = &call_module.module_def.types.items[immediates.type_index]; + if (func_import.isTypeSignatureEql(func_type_def) == false) { + return error.TrapIndirectCallTypeMismatch; + } + next = try OpHelpers.callImport(pc, stack, func_import); + } + + try @call(.always_tail, InstructionFuncs.lookup(next.code[next.continuation].opcode), .{ next.continuation, next.code, stack }); + } + + fn op_Drop(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Drop", pc, code, stack); + _ = stack.popValue(); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Select(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Select", pc, code, stack); + + var boolean: i32 = stack.popI32(); + var v2: Val = stack.popValue(); + var v1: Val = stack.popValue(); + + if (boolean != 0) { + stack.pushValue(v1); + } else { + stack.pushValue(v2); + } + + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Select_T(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Select_T", pc, code, stack); + + var boolean: i32 = stack.popI32(); + var v2: Val = stack.popValue(); + var v1: Val = stack.popValue(); + + if (boolean != 0) { + stack.pushValue(v1); + } else { + stack.pushValue(v2); + } + + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Local_Get(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Local_Get", pc, code, stack); + try stack.checkExhausted(1); + var locals_index: u32 = code[pc].immediate.Index; + var frame: *const CallFrame = stack.topFrame(); + var v: Val = frame.locals[locals_index]; + stack.pushValue(v); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Local_Set(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Local_Set", pc, code, stack); + + var locals_index: u32 = code[pc].immediate.Index; + var frame: *CallFrame = stack.topFrame(); + var v: Val = stack.popValue(); + frame.locals[locals_index] = v; + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Local_Tee(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Local_Tee", pc, code, stack); + var locals_index: u32 = code[pc].immediate.Index; + var frame: *CallFrame = stack.topFrame(); + var v: Val = stack.topValue(); + frame.locals[locals_index] = v; + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Global_Get(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Global_Get", pc, code, stack); + try stack.checkExhausted(1); + var global_index: u32 = code[pc].immediate.Index; + var global: *GlobalInstance = stack.topFrame().module_instance.store.getGlobal(global_index); + stack.pushValue(global.value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Global_Set(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Global_Set", pc, code, stack); + var global_index: u32 = code[pc].immediate.Index; + var global: *GlobalInstance = stack.topFrame().module_instance.store.getGlobal(global_index); + global.value = stack.popValue(); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Table_Get(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Table_Get", pc, code, stack); + const table_index: u32 = code[pc].immediate.Index; + const table: *const TableInstance = stack.topFrame().module_instance.store.getTable(table_index); + const index: i32 = stack.popI32(); + if (table.refs.items.len <= index or index < 0) { + return error.TrapOutOfBoundsTableAccess; + } + const ref = table.refs.items[@as(usize, @intCast(index))]; + stack.pushValue(ref); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Table_Set(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Table_Set", pc, code, stack); + const table_index: u32 = code[pc].immediate.Index; + var table: *TableInstance = stack.topFrame().module_instance.store.getTable(table_index); + const ref = stack.popValue(); + const index: i32 = stack.popI32(); + if (table.refs.items.len <= index or index < 0) { + return error.TrapOutOfBoundsTableAccess; + } + table.refs.items[@as(usize, @intCast(index))] = ref; + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Load(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Load", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value = try OpHelpers.loadFromMem(i32, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Load(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Load", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value = try OpHelpers.loadFromMem(i64, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Load(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Load", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value = try OpHelpers.loadFromMem(f32, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Load(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Load", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value = try OpHelpers.loadFromMem(f64, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Load8_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Load8_S", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value: i32 = try OpHelpers.loadFromMem(i8, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Load8_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Load8_U", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value: u32 = try OpHelpers.loadFromMem(u8, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushI32(@as(i32, @bitCast(value))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Load16_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Load16_S", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value: i32 = try OpHelpers.loadFromMem(i16, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Load16_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Load16_U", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value: u32 = try OpHelpers.loadFromMem(u16, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushI32(@as(i32, @bitCast(value))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Load8_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Load8_S", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value: i64 = try OpHelpers.loadFromMem(i8, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Load8_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Load8_U", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value: u64 = try OpHelpers.loadFromMem(u8, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushI64(@as(i64, @bitCast(value))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Load16_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Load16_S", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value: i64 = try OpHelpers.loadFromMem(i16, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Load16_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Load16_U", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value: u64 = try OpHelpers.loadFromMem(u16, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushI64(@as(i64, @bitCast(value))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Load32_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Load32_S", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value: i64 = try OpHelpers.loadFromMem(i32, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Load32_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Load32_U", pc, code, stack); + var offset_from_stack: i32 = stack.popI32(); + var value: u64 = try OpHelpers.loadFromMem(u32, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushI64(@as(i64, @bitCast(value))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Store(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Store", pc, code, stack); + const value: i32 = stack.popI32(); + const offset_from_stack: i32 = stack.popI32(); + try OpHelpers.storeInMem(value, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Store(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Store", pc, code, stack); + const value: i64 = stack.popI64(); + const offset_from_stack: i32 = stack.popI32(); + try OpHelpers.storeInMem(value, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Store(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Store", pc, code, stack); + const value: f32 = stack.popF32(); + const offset_from_stack: i32 = stack.popI32(); + try OpHelpers.storeInMem(value, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Store(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Store", pc, code, stack); + const value: f64 = stack.popF64(); + const offset_from_stack: i32 = stack.popI32(); + try OpHelpers.storeInMem(value, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Store8(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Store8", pc, code, stack); + const value: i8 = @as(i8, @truncate(stack.popI32())); + const offset_from_stack: i32 = stack.popI32(); + try OpHelpers.storeInMem(value, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Store16(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Store16", pc, code, stack); + const value: i16 = @as(i16, @truncate(stack.popI32())); + const offset_from_stack: i32 = stack.popI32(); + try OpHelpers.storeInMem(value, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Store8(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Store8", pc, code, stack); + const value: i8 = @as(i8, @truncate(stack.popI64())); + const offset_from_stack: i32 = stack.popI32(); + try OpHelpers.storeInMem(value, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Store16(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Store16", pc, code, stack); + const value: i16 = @as(i16, @truncate(stack.popI64())); + const offset_from_stack: i32 = stack.popI32(); + try OpHelpers.storeInMem(value, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Store32(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Store32", pc, code, stack); + const value: i32 = @as(i32, @truncate(stack.popI64())); + const offset_from_stack: i32 = stack.popI32(); + try OpHelpers.storeInMem(value, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Memory_Size(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Memory_Size", pc, code, stack); + try stack.checkExhausted(1); + const memory_index: usize = 0; + var memory_instance: *const MemoryInstance = stack.topFrame().module_instance.store.getMemory(memory_index); + + const num_pages: i32 = @as(i32, @intCast(memory_instance.size())); + stack.pushI32(num_pages); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Memory_Grow(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Memory_Grow", pc, code, stack); + const memory_index: usize = 0; + var memory_instance: *MemoryInstance = stack.topFrame().module_instance.store.getMemory(memory_index); + + const old_num_pages: i32 = @as(i32, @intCast(memory_instance.limits.min)); + const num_pages: i32 = stack.popI32(); + + if (num_pages >= 0 and memory_instance.grow(@as(usize, @intCast(num_pages)))) { + stack.pushI32(old_num_pages); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } else { + stack.pushI32(-1); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + } + + fn op_I32_Const(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Const", pc, code, stack); + try stack.checkExhausted(1); + var v: i32 = code[pc].immediate.ValueI32; + stack.pushI32(v); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Const(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Const", pc, code, stack); + try stack.checkExhausted(1); + var v: i64 = code[pc].immediate.ValueI64; + stack.pushI64(v); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Const(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Const", pc, code, stack); + try stack.checkExhausted(1); + var v: f32 = code[pc].immediate.ValueF32; + stack.pushF32(v); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Const(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Const", pc, code, stack); + try stack.checkExhausted(1); + var v: f64 = code[pc].immediate.ValueF64; + stack.pushF64(v); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Eqz(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Eqz", pc, code, stack); + var v1: i32 = stack.popI32(); + var result: i32 = if (v1 == 0) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Eq(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Eq", pc, code, stack); + var v2: i32 = stack.popI32(); + var v1: i32 = stack.popI32(); + var result: i32 = if (v1 == v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_NE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_NE", pc, code, stack); + var v2: i32 = stack.popI32(); + var v1: i32 = stack.popI32(); + var result: i32 = if (v1 != v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_LT_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_LT_S", pc, code, stack); + var v2: i32 = stack.popI32(); + var v1: i32 = stack.popI32(); + var result: i32 = if (v1 < v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_LT_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_LT_U", pc, code, stack); + var v2: u32 = @as(u32, @bitCast(stack.popI32())); + var v1: u32 = @as(u32, @bitCast(stack.popI32())); + var result: i32 = if (v1 < v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_GT_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_GT_S", pc, code, stack); + var v2: i32 = stack.popI32(); + var v1: i32 = stack.popI32(); + var result: i32 = if (v1 > v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_GT_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_GT_U", pc, code, stack); + var v2: u32 = @as(u32, @bitCast(stack.popI32())); + var v1: u32 = @as(u32, @bitCast(stack.popI32())); + var result: i32 = if (v1 > v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_LE_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_LE_S", pc, code, stack); + var v2: i32 = stack.popI32(); + var v1: i32 = stack.popI32(); + var result: i32 = if (v1 <= v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_LE_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_LE_U", pc, code, stack); + var v2: u32 = @as(u32, @bitCast(stack.popI32())); + var v1: u32 = @as(u32, @bitCast(stack.popI32())); + var result: i32 = if (v1 <= v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_GE_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_GE_S", pc, code, stack); + var v2: i32 = stack.popI32(); + var v1: i32 = stack.popI32(); + var result: i32 = if (v1 >= v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_GE_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_GE_U", pc, code, stack); + var v2: u32 = @as(u32, @bitCast(stack.popI32())); + var v1: u32 = @as(u32, @bitCast(stack.popI32())); + var result: i32 = if (v1 >= v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Eqz(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Eqz", pc, code, stack); + var v1: i64 = stack.popI64(); + var result: i32 = if (v1 == 0) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Eq(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Eq", pc, code, stack); + var v2: i64 = stack.popI64(); + var v1: i64 = stack.popI64(); + var result: i32 = if (v1 == v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_NE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_NE", pc, code, stack); + var v2: i64 = stack.popI64(); + var v1: i64 = stack.popI64(); + var result: i32 = if (v1 != v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_LT_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_LT_S", pc, code, stack); + var v2: i64 = stack.popI64(); + var v1: i64 = stack.popI64(); + var result: i32 = if (v1 < v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_LT_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_LT_U", pc, code, stack); + var v2: u64 = @as(u64, @bitCast(stack.popI64())); + var v1: u64 = @as(u64, @bitCast(stack.popI64())); + var result: i32 = if (v1 < v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_GT_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_GT_S", pc, code, stack); + var v2: i64 = stack.popI64(); + var v1: i64 = stack.popI64(); + var result: i32 = if (v1 > v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_GT_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_GT_U", pc, code, stack); + var v2: u64 = @as(u64, @bitCast(stack.popI64())); + var v1: u64 = @as(u64, @bitCast(stack.popI64())); + var result: i32 = if (v1 > v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_LE_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_LE_S", pc, code, stack); + var v2: i64 = stack.popI64(); + var v1: i64 = stack.popI64(); + var result: i32 = if (v1 <= v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_LE_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_LE_U", pc, code, stack); + var v2: u64 = @as(u64, @bitCast(stack.popI64())); + var v1: u64 = @as(u64, @bitCast(stack.popI64())); + var result: i32 = if (v1 <= v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_GE_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_GE_S", pc, code, stack); + var v2: i64 = stack.popI64(); + var v1: i64 = stack.popI64(); + var result: i32 = if (v1 >= v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_GE_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_GE_U", pc, code, stack); + var v2: u64 = @as(u64, @bitCast(stack.popI64())); + var v1: u64 = @as(u64, @bitCast(stack.popI64())); + var result: i32 = if (v1 >= v2) 1 else 0; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_EQ(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_EQ", pc, code, stack); + var v2 = stack.popF32(); + var v1 = stack.popF32(); + var value: i32 = if (v1 == v2) 1 else 0; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_NE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_NE", pc, code, stack); + var v2 = stack.popF32(); + var v1 = stack.popF32(); + var value: i32 = if (v1 != v2) 1 else 0; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_LT(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_LT", pc, code, stack); + var v2 = stack.popF32(); + var v1 = stack.popF32(); + var value: i32 = if (v1 < v2) 1 else 0; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_GT(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_GT", pc, code, stack); + var v2 = stack.popF32(); + var v1 = stack.popF32(); + var value: i32 = if (v1 > v2) 1 else 0; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_LE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_LE", pc, code, stack); + var v2 = stack.popF32(); + var v1 = stack.popF32(); + var value: i32 = if (v1 <= v2) 1 else 0; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_GE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_GE", pc, code, stack); + var v2 = stack.popF32(); + var v1 = stack.popF32(); + var value: i32 = if (v1 >= v2) 1 else 0; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_EQ(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_EQ", pc, code, stack); + var v2 = stack.popF64(); + var v1 = stack.popF64(); + var value: i32 = if (v1 == v2) 1 else 0; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_NE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_NE", pc, code, stack); + var v2 = stack.popF64(); + var v1 = stack.popF64(); + var value: i32 = if (v1 != v2) 1 else 0; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_LT(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_LT", pc, code, stack); + var v2 = stack.popF64(); + var v1 = stack.popF64(); + var value: i32 = if (v1 < v2) 1 else 0; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_GT(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_GT", pc, code, stack); + var v2 = stack.popF64(); + var v1 = stack.popF64(); + var value: i32 = if (v1 > v2) 1 else 0; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_LE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_LE", pc, code, stack); + var v2 = stack.popF64(); + var v1 = stack.popF64(); + var value: i32 = if (v1 <= v2) 1 else 0; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_GE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_GE", pc, code, stack); + var v2 = stack.popF64(); + var v1 = stack.popF64(); + var value: i32 = if (v1 >= v2) 1 else 0; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Clz(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Clz", pc, code, stack); + var v: i32 = stack.popI32(); + var num_zeroes = @clz(v); + stack.pushI32(num_zeroes); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Ctz(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Ctz", pc, code, stack); + var v: i32 = stack.popI32(); + var num_zeroes = @ctz(v); + stack.pushI32(num_zeroes); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Popcnt(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Popcnt", pc, code, stack); + var v: i32 = stack.popI32(); + var num_bits_set = @popCount(v); + stack.pushI32(num_bits_set); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Add(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Add", pc, code, stack); + var v2: i32 = stack.popI32(); + var v1: i32 = stack.popI32(); + var result = v1 +% v2; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Sub(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Sub", pc, code, stack); + var v2: i32 = stack.popI32(); + var v1: i32 = stack.popI32(); + var result = v1 -% v2; + stack.pushI32(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Mul(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Mul", pc, code, stack); + var v2: i32 = stack.popI32(); + var v1: i32 = stack.popI32(); + var value = v1 *% v2; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Div_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Div_S", pc, code, stack); + var v2: i32 = stack.popI32(); + var v1: i32 = stack.popI32(); + var value = std.math.divTrunc(i32, v1, v2) catch |e| { + if (e == error.DivisionByZero) { + return error.TrapIntegerDivisionByZero; + } else if (e == error.Overflow) { + return error.TrapIntegerOverflow; + } else { + return e; + } + }; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Div_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Div_U", pc, code, stack); + var v2: u32 = @as(u32, @bitCast(stack.popI32())); + var v1: u32 = @as(u32, @bitCast(stack.popI32())); + var value_unsigned = std.math.divFloor(u32, v1, v2) catch |e| { + if (e == error.DivisionByZero) { + return error.TrapIntegerDivisionByZero; + } else if (e == error.Overflow) { + return error.TrapIntegerOverflow; + } else { + return e; + } + }; + var value = @as(i32, @bitCast(value_unsigned)); + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Rem_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Rem_S", pc, code, stack); + var v2: i32 = stack.popI32(); + var v1: i32 = stack.popI32(); + var denom = try std.math.absInt(v2); + var value = std.math.rem(i32, v1, denom) catch |e| { + if (e == error.DivisionByZero) { + return error.TrapIntegerDivisionByZero; + } else { + return e; + } + }; + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Rem_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Rem_U", pc, code, stack); + var v2: u32 = @as(u32, @bitCast(stack.popI32())); + var v1: u32 = @as(u32, @bitCast(stack.popI32())); + var value_unsigned = std.math.rem(u32, v1, v2) catch |e| { + if (e == error.DivisionByZero) { + return error.TrapIntegerDivisionByZero; + } else { + return e; + } + }; + var value = @as(i32, @bitCast(value_unsigned)); + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_And(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_And", pc, code, stack); + var v2: u32 = @as(u32, @bitCast(stack.popI32())); + var v1: u32 = @as(u32, @bitCast(stack.popI32())); + var value = @as(i32, @bitCast(v1 & v2)); + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Or(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Or", pc, code, stack); + var v2: u32 = @as(u32, @bitCast(stack.popI32())); + var v1: u32 = @as(u32, @bitCast(stack.popI32())); + var value = @as(i32, @bitCast(v1 | v2)); + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Xor(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Xor", pc, code, stack); + var v2: u32 = @as(u32, @bitCast(stack.popI32())); + var v1: u32 = @as(u32, @bitCast(stack.popI32())); + var value = @as(i32, @bitCast(v1 ^ v2)); + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Shl(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Shl", pc, code, stack); + var shift_unsafe: i32 = stack.popI32(); + var int: i32 = stack.popI32(); + var shift: i32 = try std.math.mod(i32, shift_unsafe, 32); + var value = std.math.shl(i32, int, shift); + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Shr_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Shr_S", pc, code, stack); + var shift_unsafe: i32 = stack.popI32(); + var int: i32 = stack.popI32(); + var shift = try std.math.mod(i32, shift_unsafe, 32); + var value = std.math.shr(i32, int, shift); + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Shr_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Shr_U", pc, code, stack); + var shift_unsafe: u32 = @as(u32, @bitCast(stack.popI32())); + var int: u32 = @as(u32, @bitCast(stack.popI32())); + var shift = try std.math.mod(u32, shift_unsafe, 32); + var value = @as(i32, @bitCast(std.math.shr(u32, int, shift))); + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Rotl(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Rotl", pc, code, stack); + var rot: u32 = @as(u32, @bitCast(stack.popI32())); + var int: u32 = @as(u32, @bitCast(stack.popI32())); + var value = @as(i32, @bitCast(std.math.rotl(u32, int, rot))); + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Rotr(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Rotr", pc, code, stack); + var rot: u32 = @as(u32, @bitCast(stack.popI32())); + var int: u32 = @as(u32, @bitCast(stack.popI32())); + var value = @as(i32, @bitCast(std.math.rotr(u32, int, rot))); + stack.pushI32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Clz(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Clz", pc, code, stack); + var v: i64 = stack.popI64(); + var num_zeroes = @clz(v); + stack.pushI64(num_zeroes); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Ctz(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Ctz", pc, code, stack); + var v: i64 = stack.popI64(); + var num_zeroes = @ctz(v); + stack.pushI64(num_zeroes); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Popcnt(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Popcnt", pc, code, stack); + var v: i64 = stack.popI64(); + var num_bits_set = @popCount(v); + stack.pushI64(num_bits_set); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Add(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Add", pc, code, stack); + var v2: i64 = stack.popI64(); + var v1: i64 = stack.popI64(); + var result = v1 +% v2; + stack.pushI64(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Sub(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Sub", pc, code, stack); + var v2: i64 = stack.popI64(); + var v1: i64 = stack.popI64(); + var result = v1 -% v2; + stack.pushI64(result); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Mul(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Mul", pc, code, stack); + var v2: i64 = stack.popI64(); + var v1: i64 = stack.popI64(); + var value = v1 *% v2; + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Div_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Div_S", pc, code, stack); + var v2: i64 = stack.popI64(); + var v1: i64 = stack.popI64(); + var value = std.math.divTrunc(i64, v1, v2) catch |e| { + if (e == error.DivisionByZero) { + return error.TrapIntegerDivisionByZero; + } else if (e == error.Overflow) { + return error.TrapIntegerOverflow; + } else { + return e; + } + }; + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Div_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Div_U", pc, code, stack); + var v2: u64 = @as(u64, @bitCast(stack.popI64())); + var v1: u64 = @as(u64, @bitCast(stack.popI64())); + var value_unsigned = std.math.divFloor(u64, v1, v2) catch |e| { + if (e == error.DivisionByZero) { + return error.TrapIntegerDivisionByZero; + } else if (e == error.Overflow) { + return error.TrapIntegerOverflow; + } else { + return e; + } + }; + var value = @as(i64, @bitCast(value_unsigned)); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Rem_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Rem_S", pc, code, stack); + var v2: i64 = stack.popI64(); + var v1: i64 = stack.popI64(); + var denom = try std.math.absInt(v2); + var value = std.math.rem(i64, v1, denom) catch |e| { + if (e == error.DivisionByZero) { + return error.TrapIntegerDivisionByZero; + } else { + return e; + } + }; + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Rem_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Rem_U", pc, code, stack); + var v2: u64 = @as(u64, @bitCast(stack.popI64())); + var v1: u64 = @as(u64, @bitCast(stack.popI64())); + var value_unsigned = std.math.rem(u64, v1, v2) catch |e| { + if (e == error.DivisionByZero) { + return error.TrapIntegerDivisionByZero; + } else { + return e; + } + }; + var value = @as(i64, @bitCast(value_unsigned)); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_And(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_And", pc, code, stack); + var v2: u64 = @as(u64, @bitCast(stack.popI64())); + var v1: u64 = @as(u64, @bitCast(stack.popI64())); + var value = @as(i64, @bitCast(v1 & v2)); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Or(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Or", pc, code, stack); + var v2: u64 = @as(u64, @bitCast(stack.popI64())); + var v1: u64 = @as(u64, @bitCast(stack.popI64())); + var value = @as(i64, @bitCast(v1 | v2)); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Xor(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Xor", pc, code, stack); + var v2: u64 = @as(u64, @bitCast(stack.popI64())); + var v1: u64 = @as(u64, @bitCast(stack.popI64())); + var value = @as(i64, @bitCast(v1 ^ v2)); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Shl(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Shl", pc, code, stack); + var shift_unsafe: i64 = stack.popI64(); + var int: i64 = stack.popI64(); + var shift: i64 = try std.math.mod(i64, shift_unsafe, 64); + var value = std.math.shl(i64, int, shift); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Shr_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Shr_S", pc, code, stack); + var shift_unsafe: i64 = stack.popI64(); + var int: i64 = stack.popI64(); + var shift = try std.math.mod(i64, shift_unsafe, 64); + var value = std.math.shr(i64, int, shift); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Shr_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Shr_U", pc, code, stack); + var shift_unsafe: u64 = @as(u64, @bitCast(stack.popI64())); + var int: u64 = @as(u64, @bitCast(stack.popI64())); + var shift = try std.math.mod(u64, shift_unsafe, 64); + var value = @as(i64, @bitCast(std.math.shr(u64, int, shift))); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Rotl(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Rotl", pc, code, stack); + var rot: u64 = @as(u64, @bitCast(stack.popI64())); + var int: u64 = @as(u64, @bitCast(stack.popI64())); + var value = @as(i64, @bitCast(std.math.rotl(u64, int, rot))); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Rotr(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Rotr", pc, code, stack); + var rot: u64 = @as(u64, @bitCast(stack.popI64())); + var int: u64 = @as(u64, @bitCast(stack.popI64())); + var value = @as(i64, @bitCast(std.math.rotr(u64, int, rot))); + stack.pushI64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Abs(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Abs", pc, code, stack); + var f = stack.popF32(); + var value = std.math.fabs(f); + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Neg(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Neg", pc, code, stack); + var f = stack.popF32(); + stack.pushF32(-f); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Ceil(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Ceil", pc, code, stack); + var f = stack.popF32(); + var value = @ceil(f); + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Floor(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Floor", pc, code, stack); + var f = stack.popF32(); + var value = @floor(f); + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Trunc(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Trunc", pc, code, stack); + var f = stack.popF32(); + var value = std.math.trunc(f); + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Nearest(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Nearest", pc, code, stack); + var f = stack.popF32(); + var value: f32 = undefined; + var ceil = @ceil(f); + var floor = @floor(f); + if (ceil - f == f - floor) { + value = if (@mod(ceil, 2) == 0) ceil else floor; + } else { + value = @round(f); + } + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Sqrt(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Sqrt", pc, code, stack); + var f = stack.popF32(); + var value = std.math.sqrt(f); + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Add(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Add", pc, code, stack); + var v2 = stack.popF32(); + var v1 = stack.popF32(); + var value = v1 + v2; + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Sub(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Sub", pc, code, stack); + var v2 = stack.popF32(); + var v1 = stack.popF32(); + var value = v1 - v2; + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Mul(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Mul", pc, code, stack); + var v2 = stack.popF32(); + var v1 = stack.popF32(); + var value = v1 * v2; + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Div(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Div", pc, code, stack); + var v2 = stack.popF32(); + var v1 = stack.popF32(); + var value = v1 / v2; + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Min(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Min", pc, code, stack); + var v2 = stack.popF32(); + var v1 = stack.popF32(); + var value = OpHelpers.propagateNanWithOp(.Min, v1, v2); + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Max(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Max", pc, code, stack); + var v2 = stack.popF32(); + var v1 = stack.popF32(); + var value = OpHelpers.propagateNanWithOp(.Max, v1, v2); + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Copysign(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Copysign", pc, code, stack); + var v2 = stack.popF32(); + var v1 = stack.popF32(); + var value = std.math.copysign(v1, v2); + stack.pushF32(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Abs(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Abs", pc, code, stack); + var f = stack.popF64(); + var value = std.math.fabs(f); + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Neg(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Neg", pc, code, stack); + var f = stack.popF64(); + stack.pushF64(-f); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Ceil(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Ceil", pc, code, stack); + var f = stack.popF64(); + var value = @ceil(f); + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Floor(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Floor", pc, code, stack); + var f = stack.popF64(); + var value = @floor(f); + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Trunc(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Trunc", pc, code, stack); + var f = stack.popF64(); + var value = @trunc(f); + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Nearest(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Nearest", pc, code, stack); + var f = stack.popF64(); + var value: f64 = undefined; + var ceil = @ceil(f); + var floor = @floor(f); + if (ceil - f == f - floor) { + value = if (@mod(ceil, 2) == 0) ceil else floor; + } else { + value = @round(f); + } + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Sqrt(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Sqrt", pc, code, stack); + var f = stack.popF64(); + var value = std.math.sqrt(f); + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Add(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Add", pc, code, stack); + var v2 = stack.popF64(); + var v1 = stack.popF64(); + var value = v1 + v2; + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Sub(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Sub", pc, code, stack); + var v2 = stack.popF64(); + var v1 = stack.popF64(); + var value = v1 - v2; + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Mul(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Mul", pc, code, stack); + var v2 = stack.popF64(); + var v1 = stack.popF64(); + var value = v1 * v2; + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Div(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Div", pc, code, stack); + var v2 = stack.popF64(); + var v1 = stack.popF64(); + var value = v1 / v2; + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Min(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Min", pc, code, stack); + var v2 = stack.popF64(); + var v1 = stack.popF64(); + var value = OpHelpers.propagateNanWithOp(.Min, v1, v2); + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Max(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Max", pc, code, stack); + var v2 = stack.popF64(); + var v1 = stack.popF64(); + var value = OpHelpers.propagateNanWithOp(.Max, v1, v2); + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Copysign(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Copysign", pc, code, stack); + var v2 = stack.popF64(); + var v1 = stack.popF64(); + var value = std.math.copysign(v1, v2); + stack.pushF64(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Wrap_I64(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Wrap_I64", pc, code, stack); + var v = stack.popI64(); + var mod = @as(i32, @truncate(v)); + stack.pushI32(mod); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Trunc_F32_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Trunc_F32_S", pc, code, stack); + var v = stack.popF32(); + var int = try OpHelpers.truncateTo(i32, v); + stack.pushI32(int); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Trunc_F32_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Trunc_F32_U", pc, code, stack); + var v = stack.popF32(); + var int = try OpHelpers.truncateTo(u32, v); + stack.pushI32(@as(i32, @bitCast(int))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Trunc_F64_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Trunc_F64_S", pc, code, stack); + var v = stack.popF64(); + var int = try OpHelpers.truncateTo(i32, v); + stack.pushI32(int); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Trunc_F64_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Trunc_F64_U", pc, code, stack); + var v = stack.popF64(); + var int = try OpHelpers.truncateTo(u32, v); + stack.pushI32(@as(i32, @bitCast(int))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Extend_I32_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Extend_I32_S", pc, code, stack); + var v32 = stack.popI32(); + var v64: i64 = v32; + stack.pushI64(v64); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Extend_I32_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Extend_I32_U", pc, code, stack); + var v32 = stack.popI32(); + var v64: u64 = @as(u32, @bitCast(v32)); + stack.pushI64(@as(i64, @bitCast(v64))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Trunc_F32_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Trunc_F32_S", pc, code, stack); + var v = stack.popF32(); + var int = try OpHelpers.truncateTo(i64, v); + stack.pushI64(int); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Trunc_F32_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Trunc_F32_U", pc, code, stack); + var v = stack.popF32(); + var int = try OpHelpers.truncateTo(u64, v); + stack.pushI64(@as(i64, @bitCast(int))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Trunc_F64_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Trunc_F64_S", pc, code, stack); + var v = stack.popF64(); + var int = try OpHelpers.truncateTo(i64, v); + stack.pushI64(int); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Trunc_F64_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Trunc_F64_U", pc, code, stack); + var v = stack.popF64(); + var int = try OpHelpers.truncateTo(u64, v); + stack.pushI64(@as(i64, @bitCast(int))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Convert_I32_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Convert_I32_S", pc, code, stack); + var v = stack.popI32(); + stack.pushF32(@as(f32, @floatFromInt(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Convert_I32_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Convert_I32_U", pc, code, stack); + var v = @as(u32, @bitCast(stack.popI32())); + stack.pushF32(@as(f32, @floatFromInt(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Convert_I64_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Convert_I64_S", pc, code, stack); + var v = stack.popI64(); + stack.pushF32(@as(f32, @floatFromInt(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Convert_I64_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Convert_I64_U", pc, code, stack); + var v = @as(u64, @bitCast(stack.popI64())); + stack.pushF32(@as(f32, @floatFromInt(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Demote_F64(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Demote_F64", pc, code, stack); + var v = stack.popF64(); + stack.pushF32(@as(f32, @floatCast(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Convert_I32_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Convert_I32_S", pc, code, stack); + var v = stack.popI32(); + stack.pushF64(@as(f64, @floatFromInt(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Convert_I32_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Convert_I32_U", pc, code, stack); + var v = @as(u32, @bitCast(stack.popI32())); + stack.pushF64(@as(f64, @floatFromInt(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Convert_I64_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Convert_I64_S", pc, code, stack); + var v = stack.popI64(); + stack.pushF64(@as(f64, @floatFromInt(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Convert_I64_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Convert_I64_U", pc, code, stack); + var v = @as(u64, @bitCast(stack.popI64())); + stack.pushF64(@as(f64, @floatFromInt(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Promote_F32(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Promote_F32", pc, code, stack); + var v = stack.popF32(); + stack.pushF64(@as(f64, @floatCast(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Reinterpret_F32(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Reinterpret_F32", pc, code, stack); + var v = stack.popF32(); + stack.pushI32(@as(i32, @bitCast(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Reinterpret_F64(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Reinterpret_F64", pc, code, stack); + var v = stack.popF64(); + stack.pushI64(@as(i64, @bitCast(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32_Reinterpret_I32(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32_Reinterpret_I32", pc, code, stack); + var v = stack.popI32(); + stack.pushF32(@as(f32, @bitCast(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64_Reinterpret_I64(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64_Reinterpret_I64", pc, code, stack); + var v = stack.popI64(); + stack.pushF64(@as(f64, @bitCast(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Extend8_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Extend8_S", pc, code, stack); + var v = stack.popI32(); + var v_truncated = @as(i8, @truncate(v)); + var v_extended: i32 = v_truncated; + stack.pushI32(v_extended); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Extend16_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Extend16_S", pc, code, stack); + var v = stack.popI32(); + var v_truncated = @as(i16, @truncate(v)); + var v_extended: i32 = v_truncated; + stack.pushI32(v_extended); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Extend8_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Extend8_S", pc, code, stack); + var v = stack.popI64(); + var v_truncated = @as(i8, @truncate(v)); + var v_extended: i64 = v_truncated; + stack.pushI64(v_extended); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Extend16_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Extend16_S", pc, code, stack); + var v = stack.popI64(); + var v_truncated = @as(i16, @truncate(v)); + var v_extended: i64 = v_truncated; + stack.pushI64(v_extended); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Extend32_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Extend32_S", pc, code, stack); + var v = stack.popI64(); + var v_truncated = @as(i32, @truncate(v)); + var v_extended: i64 = v_truncated; + stack.pushI64(v_extended); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Ref_Null(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Ref_Null", pc, code, stack); + try stack.checkExhausted(1); + var valtype = code[pc].immediate.ValType; + var val = try Val.nullRef(valtype); + stack.pushValue(val); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Ref_Is_Null(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Ref_Is_Null", pc, code, stack); + const val: Val = stack.popValue(); + const boolean: i32 = if (val.isNull()) 1 else 0; + stack.pushI32(boolean); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Ref_Func(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Ref_Func", pc, code, stack); + try stack.checkExhausted(1); + const func_index: u32 = code[pc].immediate.Index; + const val = Val{ .FuncRef = .{ .index = func_index, .module_instance = stack.topFrame().module_instance } }; + stack.pushValue(val); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Trunc_Sat_F32_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Trunc_Sat_F32_S", pc, code, stack); + var v = stack.popF32(); + var int = OpHelpers.saturatedTruncateTo(i32, v); + stack.pushI32(int); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Trunc_Sat_F32_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Trunc_Sat_F32_U", pc, code, stack); + var v = stack.popF32(); + var int = OpHelpers.saturatedTruncateTo(u32, v); + stack.pushI32(@as(i32, @bitCast(int))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Trunc_Sat_F64_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Trunc_Sat_F64_S", pc, code, stack); + var v = stack.popF64(); + var int = OpHelpers.saturatedTruncateTo(i32, v); + stack.pushI32(int); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32_Trunc_Sat_F64_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32_Trunc_Sat_F64_U", pc, code, stack); + var v = stack.popF64(); + var int = OpHelpers.saturatedTruncateTo(u32, v); + stack.pushI32(@as(i32, @bitCast(int))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Trunc_Sat_F32_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Trunc_Sat_F32_S", pc, code, stack); + var v = stack.popF32(); + var int = OpHelpers.saturatedTruncateTo(i64, v); + stack.pushI64(int); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Trunc_Sat_F32_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Trunc_Sat_F32_U", pc, code, stack); + var v = stack.popF32(); + var int = OpHelpers.saturatedTruncateTo(u64, v); + stack.pushI64(@as(i64, @bitCast(int))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Trunc_Sat_F64_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Trunc_Sat_F64_S", pc, code, stack); + var v = stack.popF64(); + var int = OpHelpers.saturatedTruncateTo(i64, v); + stack.pushI64(int); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64_Trunc_Sat_F64_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64_Trunc_Sat_F64_U", pc, code, stack); + var v = stack.popF64(); + var int = OpHelpers.saturatedTruncateTo(u64, v); + stack.pushI64(@as(i64, @bitCast(int))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Memory_Init(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Memory_Init", pc, code, stack); + const data_index: u32 = code[pc].immediate.Index; + const data: *const DataDefinition = &stack.topFrame().module_instance.module_def.datas.items[data_index]; + const memory: *MemoryInstance = &stack.topFrame().module_instance.store.memories.items[0]; + + const length = stack.popI32(); + const data_offset = stack.popI32(); + const memory_offset = stack.popI32(); + + if (length < 0) { + return error.TrapOutOfBoundsMemoryAccess; + } + if (data.bytes.items.len < data_offset + length or data_offset < 0) { + return error.TrapOutOfBoundsMemoryAccess; + } + + const buffer = memory.buffer(); + if (buffer.len < memory_offset + length or memory_offset < 0) { + return error.TrapOutOfBoundsMemoryAccess; + } + + const data_offset_u32 = @as(u32, @intCast(data_offset)); + const memory_offset_u32 = @as(u32, @intCast(memory_offset)); + const length_u32 = @as(u32, @intCast(length)); + + var source = data.bytes.items[data_offset_u32 .. data_offset_u32 + length_u32]; + var destination = buffer[memory_offset_u32 .. memory_offset_u32 + length_u32]; + std.mem.copy(u8, destination, source); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Data_Drop(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Data_Drop", pc, code, stack); + const data_index: u32 = code[pc].immediate.Index; + var data: *DataDefinition = &stack.topFrame().module_instance.module_def.datas.items[data_index]; + data.bytes.clearAndFree(); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Memory_Copy(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Memory_Copy", pc, code, stack); + const memory: *MemoryInstance = &stack.topFrame().module_instance.store.memories.items[0]; + + const length = stack.popI32(); + const source_offset = stack.popI32(); + const dest_offset = stack.popI32(); + + if (length < 0) { + return error.TrapOutOfBoundsMemoryAccess; + } + + const buffer = memory.buffer(); + if (buffer.len < source_offset + length or source_offset < 0) { + return error.TrapOutOfBoundsMemoryAccess; + } + if (buffer.len < dest_offset + length or dest_offset < 0) { + return error.TrapOutOfBoundsMemoryAccess; + } + + const source_offset_u32 = @as(u32, @intCast(source_offset)); + const dest_offset_u32 = @as(u32, @intCast(dest_offset)); + const length_u32 = @as(u32, @intCast(length)); + + var source = buffer[source_offset_u32 .. source_offset_u32 + length_u32]; + var destination = buffer[dest_offset_u32 .. dest_offset_u32 + length_u32]; + + if (@intFromPtr(destination.ptr) < @intFromPtr(source.ptr)) { + std.mem.copy(u8, destination, source); + } else { + std.mem.copyBackwards(u8, destination, source); + } + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Memory_Fill(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Memory_Fill", pc, code, stack); + const memory: *MemoryInstance = &stack.topFrame().module_instance.store.memories.items[0]; + + const length = stack.popI32(); + const value: u8 = @as(u8, @truncate(@as(u32, @bitCast(stack.popI32())))); + const offset = stack.popI32(); + + if (length < 0) { + return error.TrapOutOfBoundsMemoryAccess; + } + + const buffer = memory.buffer(); + if (buffer.len < offset + length or offset < 0) { + return error.TrapOutOfBoundsMemoryAccess; + } + + const offset_u32 = @as(u32, @intCast(offset)); + const length_u32 = @as(u32, @intCast(length)); + + var destination = buffer[offset_u32 .. offset_u32 + length_u32]; + + @memset(destination, value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Table_Init(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Table_Init", pc, code, stack); + const pair: TablePairImmediates = code[pc].immediate.TablePair; + const elem_index = pair.index_x; + const table_index = pair.index_y; + + const elem: *const ElementInstance = &stack.topFrame().module_instance.store.elements.items[elem_index]; + const table: *TableInstance = stack.topFrame().module_instance.store.getTable(table_index); + + const length_i32 = stack.popI32(); + const elem_start_index = stack.popI32(); + const table_start_index = stack.popI32(); + + if (elem_start_index + length_i32 > elem.refs.items.len or elem_start_index < 0) { + return error.TrapOutOfBoundsTableAccess; + } + if (table_start_index + length_i32 > table.refs.items.len or table_start_index < 0) { + return error.TrapOutOfBoundsTableAccess; + } + if (length_i32 < 0) { + return error.TrapOutOfBoundsTableAccess; + } + + const elem_begin = @as(usize, @intCast(elem_start_index)); + const table_begin = @as(usize, @intCast(table_start_index)); + const length = @as(usize, @intCast(length_i32)); + + var dest: []Val = table.refs.items[table_begin .. table_begin + length]; + var src: []const Val = elem.refs.items[elem_begin .. elem_begin + length]; + std.mem.copy(Val, dest, src); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Elem_Drop(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Elem_Drop", pc, code, stack); + const elem_index: u32 = code[pc].immediate.Index; + var elem: *ElementInstance = &stack.topFrame().module_instance.store.elements.items[elem_index]; + elem.refs.clearAndFree(); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Table_Copy(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Table_Copy", pc, code, stack); + const pair: TablePairImmediates = code[pc].immediate.TablePair; + const dest_table_index = pair.index_x; + const src_table_index = pair.index_y; + + const dest_table: *TableInstance = stack.topFrame().module_instance.store.getTable(dest_table_index); + const src_table: *const TableInstance = stack.topFrame().module_instance.store.getTable(src_table_index); + + const length_i32 = stack.popI32(); + const src_start_index = stack.popI32(); + const dest_start_index = stack.popI32(); + + if (src_start_index + length_i32 > src_table.refs.items.len or src_start_index < 0) { + return error.TrapOutOfBoundsTableAccess; + } + if (dest_start_index + length_i32 > dest_table.refs.items.len or dest_start_index < 0) { + return error.TrapOutOfBoundsTableAccess; + } + if (length_i32 < 0) { + return error.TrapOutOfBoundsTableAccess; + } + + const dest_begin = @as(usize, @intCast(dest_start_index)); + const src_begin = @as(usize, @intCast(src_start_index)); + const length = @as(usize, @intCast(length_i32)); + + var dest: []Val = dest_table.refs.items[dest_begin .. dest_begin + length]; + var src: []const Val = src_table.refs.items[src_begin .. src_begin + length]; + if (dest_start_index <= src_start_index) { + std.mem.copy(Val, dest, src); + } else { + std.mem.copyBackwards(Val, dest, src); + } + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Table_Grow(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Table_Grow", pc, code, stack); + const table_index: u32 = code[pc].immediate.Index; + const table: *TableInstance = stack.topFrame().module_instance.store.getTable(table_index); + const length = @as(u32, @bitCast(stack.popI32())); + const init_value = stack.popValue(); + const old_length = @as(i32, @intCast(table.refs.items.len)); + const return_value: i32 = if (table.grow(length, init_value)) old_length else -1; + stack.pushI32(return_value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Table_Size(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Table_Size", pc, code, stack); + const table_index: u32 = code[pc].immediate.Index; + const table: *TableInstance = stack.topFrame().module_instance.store.getTable(table_index); + const length = @as(i32, @intCast(table.refs.items.len)); + stack.pushI32(length); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_Table_Fill(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("Table_Fill", pc, code, stack); + const table_index: u32 = code[pc].immediate.Index; + const table: *TableInstance = stack.topFrame().module_instance.store.getTable(table_index); + + const length_i32 = stack.popI32(); + const funcref = stack.popValue(); + const dest_table_index = stack.popI32(); + + if (dest_table_index + length_i32 > table.refs.items.len or length_i32 < 0) { + return error.TrapOutOfBoundsTableAccess; + } + + const dest_begin = @as(usize, @intCast(dest_table_index)); + const length = @as(usize, @intCast(length_i32)); + + var dest: []Val = table.refs.items[dest_begin .. dest_begin + length]; + + @memset(dest, funcref); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load", pc, code, stack); + const offset_from_stack: i32 = stack.popI32(); + const value = try OpHelpers.loadFromMem(v128, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + stack.pushV128(value); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load8x8_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load8x8_S", pc, code, stack); + try OpHelpers.vectorLoadExtend(i8, i16, 8, code[pc].immediate.MemoryOffset, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load8x8_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load8x8_S", pc, code, stack); + try OpHelpers.vectorLoadExtend(u8, i16, 8, code[pc].immediate.MemoryOffset, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load16x4_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load16x4_S", pc, code, stack); + try OpHelpers.vectorLoadExtend(i16, i32, 4, code[pc].immediate.MemoryOffset, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load16x4_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load16x4_U", pc, code, stack); + try OpHelpers.vectorLoadExtend(u16, i32, 4, code[pc].immediate.MemoryOffset, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load32x2_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load32x2_S", pc, code, stack); + try OpHelpers.vectorLoadExtend(i32, i64, 2, code[pc].immediate.MemoryOffset, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load32x2_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load32x2_U", pc, code, stack); + try OpHelpers.vectorLoadExtend(u32, i64, 2, code[pc].immediate.MemoryOffset, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load8_Splat(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load8_Splat", pc, code, stack); + const offset_from_stack: i32 = stack.popI32(); + const scalar = try OpHelpers.loadFromMem(u8, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + const vec: u8x16 = @splat(scalar); + stack.pushV128(@as(v128, @bitCast(vec))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load16_Splat(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load16_Splat", pc, code, stack); + const offset_from_stack: i32 = stack.popI32(); + const scalar = try OpHelpers.loadFromMem(u16, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + const vec: u16x8 = @splat(scalar); + stack.pushV128(@as(v128, @bitCast(vec))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load32_Splat(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load32_Splat", pc, code, stack); + const offset_from_stack: i32 = stack.popI32(); + const scalar = try OpHelpers.loadFromMem(u32, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + const vec: u32x4 = @splat(scalar); + stack.pushV128(@as(v128, @bitCast(vec))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load64_Splat(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load64_Splat", pc, code, stack); + const offset_from_stack: i32 = stack.popI32(); + const scalar = try OpHelpers.loadFromMem(u64, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + const vec: u64x2 = @splat(scalar); + stack.pushV128(@as(v128, @bitCast(vec))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Splat(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Splat", pc, code, stack); + const scalar = @as(i8, @truncate(stack.popI32())); + const vec: i8x16 = @splat(scalar); + stack.pushV128(@as(v128, @bitCast(vec))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Splat(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Splat", pc, code, stack); + const scalar = @as(i16, @truncate(stack.popI32())); + const vec: i16x8 = @splat(scalar); + stack.pushV128(@as(v128, @bitCast(vec))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Splat(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Splat", pc, code, stack); + const scalar = stack.popI32(); + const vec: i32x4 = @splat(scalar); + stack.pushV128(@as(v128, @bitCast(vec))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Splat(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Splat", pc, code, stack); + const scalar = stack.popI64(); + const vec: i64x2 = @splat(scalar); + stack.pushV128(@as(v128, @bitCast(vec))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Splat(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Splat", pc, code, stack); + const scalar = stack.popF32(); + const vec: f32x4 = @splat(scalar); + stack.pushV128(@as(v128, @bitCast(vec))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Splat(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Splat", pc, code, stack); + const scalar = stack.popF64(); + const vec: f64x2 = @splat(scalar); + stack.pushV128(@as(v128, @bitCast(vec))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Extract_Lane_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Extract_Lane_S", pc, code, stack); + OpHelpers.vectorExtractLane(i8x16, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Extract_Lane_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Extract_Lane_U", pc, code, stack); + OpHelpers.vectorExtractLane(u8x16, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Replace_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Replace_Lane", pc, code, stack); + OpHelpers.vectorReplaceLane(i8x16, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Extract_Lane_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Extract_Lane_S", pc, code, stack); + OpHelpers.vectorExtractLane(i16x8, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Extract_Lane_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Extract_Lane_U", pc, code, stack); + OpHelpers.vectorExtractLane(u16x8, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Replace_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Replace_Lane", pc, code, stack); + OpHelpers.vectorReplaceLane(i16x8, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Extract_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Extract_Lane", pc, code, stack); + OpHelpers.vectorExtractLane(i32x4, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Replace_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Replace_Lane", pc, code, stack); + OpHelpers.vectorReplaceLane(i32x4, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Extract_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Extract_Lane", pc, code, stack); + OpHelpers.vectorExtractLane(i64x2, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Replace_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Replace_Lane", pc, code, stack); + OpHelpers.vectorReplaceLane(i64x2, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Extract_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Extract_Lane", pc, code, stack); + OpHelpers.vectorExtractLane(f32x4, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Replace_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Replace_Lane", pc, code, stack); + OpHelpers.vectorReplaceLane(f32x4, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Extract_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Extract_Lane", pc, code, stack); + OpHelpers.vectorExtractLane(f64x2, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Replace_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Replace_Lane", pc, code, stack); + OpHelpers.vectorReplaceLane(f64x2, code[pc].immediate.Index, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_EQ(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_EQ", pc, code, stack); + OpHelpers.vectorBoolOp(i8x16, .Eq, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_NE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_NE", pc, code, stack); + OpHelpers.vectorBoolOp(i8x16, .Ne, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_LT_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_LT_S", pc, code, stack); + OpHelpers.vectorBoolOp(i8x16, .Lt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_LT_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_LT_U", pc, code, stack); + OpHelpers.vectorBoolOp(u8x16, .Lt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_GT_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_GT_S", pc, code, stack); + OpHelpers.vectorBoolOp(i8x16, .Gt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_GT_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_GT_U", pc, code, stack); + OpHelpers.vectorBoolOp(u8x16, .Gt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_LE_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_LE_S", pc, code, stack); + OpHelpers.vectorBoolOp(i8x16, .Le, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_LE_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_LE_U", pc, code, stack); + OpHelpers.vectorBoolOp(u8x16, .Le, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_GE_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_GE_S", pc, code, stack); + OpHelpers.vectorBoolOp(i8x16, .Ge, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_GE_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_GE_U", pc, code, stack); + OpHelpers.vectorBoolOp(u8x16, .Ge, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_EQ(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_EQ", pc, code, stack); + OpHelpers.vectorBoolOp(i16x8, .Eq, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_NE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_NE", pc, code, stack); + OpHelpers.vectorBoolOp(i16x8, .Ne, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_LT_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_LT_S", pc, code, stack); + OpHelpers.vectorBoolOp(i16x8, .Lt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_LT_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_LT_U", pc, code, stack); + OpHelpers.vectorBoolOp(u16x8, .Lt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_GT_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_GT_S", pc, code, stack); + OpHelpers.vectorBoolOp(i16x8, .Gt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_GT_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_GT_U", pc, code, stack); + OpHelpers.vectorBoolOp(u16x8, .Gt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_LE_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_LE_S", pc, code, stack); + OpHelpers.vectorBoolOp(i16x8, .Le, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_LE_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_LE_U", pc, code, stack); + OpHelpers.vectorBoolOp(u16x8, .Le, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_GE_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_GE_S", pc, code, stack); + OpHelpers.vectorBoolOp(i16x8, .Ge, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_GE_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_GE_U", pc, code, stack); + OpHelpers.vectorBoolOp(u16x8, .Ge, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_EQ(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_EQ", pc, code, stack); + OpHelpers.vectorBoolOp(i32x4, .Eq, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_NE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_NE", pc, code, stack); + OpHelpers.vectorBoolOp(i32x4, .Ne, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_LT_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_LT_S", pc, code, stack); + OpHelpers.vectorBoolOp(i32x4, .Lt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_LT_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_LT_U", pc, code, stack); + OpHelpers.vectorBoolOp(u32x4, .Lt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_GT_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_GT_S", pc, code, stack); + OpHelpers.vectorBoolOp(i32x4, .Gt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_GT_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_GT_U", pc, code, stack); + OpHelpers.vectorBoolOp(u32x4, .Gt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_LE_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_LE_S", pc, code, stack); + OpHelpers.vectorBoolOp(i32x4, .Le, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_LE_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_LE_U", pc, code, stack); + OpHelpers.vectorBoolOp(u32x4, .Le, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_GE_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_GE_S", pc, code, stack); + OpHelpers.vectorBoolOp(i32x4, .Ge, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_GE_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_GE_U", pc, code, stack); + OpHelpers.vectorBoolOp(u32x4, .Ge, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_EQ(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_EQ", pc, code, stack); + OpHelpers.vectorBoolOp(f32x4, .Eq, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_NE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_NE", pc, code, stack); + OpHelpers.vectorBoolOp(f32x4, .Ne, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_LT(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_LT", pc, code, stack); + OpHelpers.vectorBoolOp(f32x4, .Lt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_GT(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_GT", pc, code, stack); + OpHelpers.vectorBoolOp(f32x4, .Gt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_LE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_LE", pc, code, stack); + OpHelpers.vectorBoolOp(f32x4, .Le, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_GE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_GE", pc, code, stack); + OpHelpers.vectorBoolOp(f32x4, .Ge, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_EQ(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_EQ", pc, code, stack); + OpHelpers.vectorBoolOp(f64x2, .Eq, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_NE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_NE", pc, code, stack); + OpHelpers.vectorBoolOp(f64x2, .Ne, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_LT(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_LT", pc, code, stack); + OpHelpers.vectorBoolOp(f64x2, .Lt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_GT(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_GT", pc, code, stack); + OpHelpers.vectorBoolOp(f64x2, .Gt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_LE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_LE", pc, code, stack); + OpHelpers.vectorBoolOp(f64x2, .Le, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_GE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_GE", pc, code, stack); + OpHelpers.vectorBoolOp(f64x2, .Ge, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Store(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Store", pc, code, stack); + + const value: v128 = stack.popV128(); + const offset_from_stack: i32 = stack.popI32(); + try OpHelpers.storeInMem(value, &stack.topFrame().module_instance.store, code[pc].immediate.MemoryOffset, offset_from_stack); + + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Const(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Const", pc, code, stack); + try stack.checkExhausted(1); + const v: v128 = code[pc].immediate.ValueVec; + stack.pushV128(v); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Shuffle(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Shuffle", pc, code, stack); + const v2 = @as(i8x16, @bitCast(stack.popV128())); + const v1 = @as(i8x16, @bitCast(stack.popV128())); + const indices: u8x16 = code[pc].immediate.VecShuffle16; + + var concat: [32]i8 = undefined; + for (concat[0..16], 0..) |_, i| { + concat[i] = v1[i]; + concat[i + 16] = v2[i]; + } + const concat_v: @Vector(32, i8) = concat; + + var arr: [16]i8 = undefined; + for (&arr, 0..) |*v, i| { + const laneidx = indices[i]; + v.* = concat_v[laneidx]; + } + const shuffled: i8x16 = arr; + + stack.pushV128(@as(v128, @bitCast(shuffled))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Swizzle(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Swizzle", pc, code, stack); + const indices: i8x16 = @as(i8x16, @bitCast(stack.popV128())); + var vec: i8x16 = @as(i8x16, @bitCast(stack.popV128())); + var swizzled: i8x16 = undefined; + var i: usize = 0; + while (i < 16) : (i += 1) { + const value = if (indices[i] >= 0 and indices[i] < 16) vec[@as(usize, @intCast(indices[i]))] else @as(i8, 0); + swizzled[i] = value; + } + stack.pushV128(@as(v128, @bitCast(swizzled))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Not(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Not", pc, code, stack); + const v = @as(i8x16, @bitCast(stack.popV128())); + const inverted = ~v; + stack.pushV128(@as(v128, @bitCast(inverted))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_And(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_And", pc, code, stack); + OpHelpers.vectorBinOp(i8x16, .And, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_AndNot(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_AndNot", pc, code, stack); + OpHelpers.vectorBinOp(i8x16, .AndNot, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Or(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Or", pc, code, stack); + OpHelpers.vectorBinOp(i8x16, .Or, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Xor(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Xor", pc, code, stack); + OpHelpers.vectorBinOp(i8x16, .Xor, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Bitselect(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Bitselect", pc, code, stack); + const u1x128 = @Vector(128, u1); + const c = @as(@Vector(128, bool), @bitCast(stack.popV128())); + const v2 = @as(u1x128, @bitCast(stack.popV128())); + const v1 = @as(u1x128, @bitCast(stack.popV128())); + const v = @select(u1, c, v1, v2); + stack.pushV128(@as(v128, @bitCast(v))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_AnyTrue(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_AnyTrue", pc, code, stack); + const v = @as(u128, @bitCast(stack.popV128())); + const boolean: i32 = if (v != 0) 1 else 0; + stack.pushI32(boolean); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load8_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load8_Lane", pc, code, stack); + try OpHelpers.vectorLoadLane(u8x16, code[pc], stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load16_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load16_Lane", pc, code, stack); + try OpHelpers.vectorLoadLane(u16x8, code[pc], stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load32_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load32_Lane", pc, code, stack); + try OpHelpers.vectorLoadLane(u32x4, code[pc], stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load64_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load64_Lane", pc, code, stack); + try OpHelpers.vectorLoadLane(u64x2, code[pc], stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Store8_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Store8_Lane", pc, code, stack); + try OpHelpers.vectorStoreLane(u8x16, code[pc], stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Store16_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Store16_Lane", pc, code, stack); + try OpHelpers.vectorStoreLane(u16x8, code[pc], stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Store32_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Store32_Lane", pc, code, stack); + try OpHelpers.vectorStoreLane(u32x4, code[pc], stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Store64_Lane(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Store64_Lane", pc, code, stack); + try OpHelpers.vectorStoreLane(u64x2, code[pc], stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load32_Zero(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load32_Zero", pc, code, stack); + try OpHelpers.vectorLoadLaneZero(u32x4, code[pc], stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_V128_Load64_Zero(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("V128_Load64_Zero", pc, code, stack); + try OpHelpers.vectorLoadLaneZero(u64x2, code[pc], stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Demote_F64x2_Zero(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Demote_F64x2_Zero", pc, code, stack); + const vec = @as(f64x2, @bitCast(stack.popV128())); + var arr: [4]f32 = undefined; + arr[0] = @as(f32, @floatCast(vec[0])); + arr[1] = @as(f32, @floatCast(vec[1])); + arr[2] = 0.0; + arr[3] = 0.0; + const demoted: f32x4 = arr; + stack.pushV128(@as(v128, @bitCast(demoted))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Promote_Low_F32x4(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Promote_Low_F32x4", pc, code, stack); + const vec = @as(f32x4, @bitCast(stack.popV128())); + var arr: [2]f64 = undefined; + arr[0] = vec[0]; + arr[1] = vec[1]; + const promoted: f64x2 = arr; + stack.pushV128(@as(v128, @bitCast(promoted))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Abs(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Abs", pc, code, stack); + OpHelpers.vectorAbs(i8x16, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Neg(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Neg", pc, code, stack); + const vec = @as(i8x16, @bitCast(stack.popV128())); + const negated = -%vec; + stack.pushV128(@as(v128, @bitCast(negated))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Popcnt(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Popcnt", pc, code, stack); + const vec = @as(i8x16, @bitCast(stack.popV128())); + const result: u8x16 = @popCount(vec); + stack.pushV128(@as(v128, @bitCast(@as(v128, @bitCast(result))))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_AllTrue(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_AllTrue", pc, code, stack); + const boolean = OpHelpers.vectorAllTrue(i8x16, stack.popV128()); + stack.pushI32(boolean); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Bitmask(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Bitmask", pc, code, stack); + const bitmask: i32 = OpHelpers.vectorBitmask(i8x16, stack.popV128()); + stack.pushI32(bitmask); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Narrow_I16x8_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Narrow_I16x8_S", pc, code, stack); + OpHelpers.vectorNarrow(i16x8, i8x16, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Narrow_I16x8_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Narrow_I16x8_U", pc, code, stack); + OpHelpers.vectorNarrow(i16x8, u8x16, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Ceil(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Ceil", pc, code, stack); + OpHelpers.vectorUnOp(f32x4, .Ceil, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Floor(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Floor", pc, code, stack); + OpHelpers.vectorUnOp(f32x4, .Floor, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Trunc(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Trunc", pc, code, stack); + OpHelpers.vectorUnOp(f32x4, .Trunc, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Nearest(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Nearest", pc, code, stack); + OpHelpers.vectorUnOp(f32x4, .Nearest, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Shl(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Shl", pc, code, stack); + OpHelpers.vectorShift(i8x16, .Left, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Shr_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Shr_S", pc, code, stack); + OpHelpers.vectorShift(i8x16, .Right, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Shr_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Shr_U", pc, code, stack); + OpHelpers.vectorShift(u8x16, .Right, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Add(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Add", pc, code, stack); + OpHelpers.vectorBinOp(u8x16, .Add, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Add_Sat_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Add_Sat_S", pc, code, stack); + OpHelpers.vectorBinOp(i8x16, .Add_Sat, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Add_Sat_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Add_Sat_U", pc, code, stack); + OpHelpers.vectorBinOp(u8x16, .Add_Sat, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Sub(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Sub", pc, code, stack); + OpHelpers.vectorBinOp(u8x16, .Sub, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Sub_Sat_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Sub_Sat_S", pc, code, stack); + OpHelpers.vectorBinOp(i8x16, .Sub_Sat, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Sub_Sat_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Sub_Sat_U", pc, code, stack); + OpHelpers.vectorBinOp(u8x16, .Sub_Sat, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Ceil(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Ceil", pc, code, stack); + OpHelpers.vectorUnOp(f64x2, .Ceil, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Floor(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Floor", pc, code, stack); + OpHelpers.vectorUnOp(f64x2, .Floor, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Min_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Min_S", pc, code, stack); + OpHelpers.vectorBinOp(i8x16, .Min, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Min_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Min_U", pc, code, stack); + OpHelpers.vectorBinOp(u8x16, .Min, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Max_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Max_S", pc, code, stack); + OpHelpers.vectorBinOp(i8x16, .Max, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Max_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Max_U", pc, code, stack); + OpHelpers.vectorBinOp(u8x16, .Max, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Trunc(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Trunc", pc, code, stack); + OpHelpers.vectorUnOp(f64x2, .Trunc, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I8x16_Avgr_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I8x16_Avgr_U", pc, code, stack); + OpHelpers.vectorAvgrU(u8x16, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Extadd_Pairwise_I8x16_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Extadd_Pairwise_I8x16_S", pc, code, stack); + OpHelpers.vectorAddPairwise(i8x16, i16x8, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Extadd_Pairwise_I8x16_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Extadd_Pairwise_I8x16_U", pc, code, stack); + OpHelpers.vectorAddPairwise(u8x16, u16x8, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Extadd_Pairwise_I16x8_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Extadd_Pairwise_I16x8_S", pc, code, stack); + OpHelpers.vectorAddPairwise(i16x8, i32x4, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Extadd_Pairwise_I16x8_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Extadd_Pairwise_I16x8_U", pc, code, stack); + OpHelpers.vectorAddPairwise(u16x8, u32x4, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Abs(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Abs", pc, code, stack); + OpHelpers.vectorAbs(i16x8, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Neg(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Neg", pc, code, stack); + const vec = @as(u16x8, @bitCast(stack.popV128())); + const negated = -%vec; + stack.pushV128(@as(v128, @bitCast(negated))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Q15mulr_Sat_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Q15mulr_Sat_S", pc, code, stack); + const v2 = @as(i16x8, @bitCast(stack.popV128())); + const v1 = @as(i16x8, @bitCast(stack.popV128())); + const power: i32 = comptime std.math.powi(i32, 2, 14) catch unreachable; + + var arr: [8]i16 = undefined; + for (&arr, 0..) |*v, i| { + const product = @as(i32, v1[i]) * @as(i32, v2[i]) + power; + const shifted = product >> 15; + const saturated = std.math.clamp(shifted, std.math.minInt(i16), std.math.maxInt(i16)); + v.* = @as(i16, @intCast(saturated)); + } + + const result: i16x8 = arr; + stack.pushV128(@as(v128, @bitCast(result))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_AllTrue(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_AllTrue", pc, code, stack); + const boolean: i32 = OpHelpers.vectorAllTrue(i16x8, stack.popV128()); + stack.pushI32(boolean); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Bitmask(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Bitmask", pc, code, stack); + const bitmask: i32 = OpHelpers.vectorBitmask(i16x8, stack.popV128()); + stack.pushI32(bitmask); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Narrow_I32x4_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Narrow_I32x4_S", pc, code, stack); + OpHelpers.vectorNarrow(i32x4, i16x8, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Narrow_I32x4_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Narrow_I32x4_U", pc, code, stack); + OpHelpers.vectorNarrow(i32x4, u16x8, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Extend_Low_I8x16_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Extend_Low_I8x16_S", pc, code, stack); + OpHelpers.vectorExtend(i8x16, i16x8, .Low, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Extend_High_I8x16_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Extend_High_I8x16_S", pc, code, stack); + OpHelpers.vectorExtend(i8x16, i16x8, .High, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Extend_Low_I8x16_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Extend_Low_I8x16_U", pc, code, stack); + OpHelpers.vectorExtend(u8x16, i16x8, .Low, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + fn op_I16x8_Extend_High_I8x16_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Extend_High_I8x16_U", pc, code, stack); + OpHelpers.vectorExtend(u8x16, i16x8, .High, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Shl(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Shl", pc, code, stack); + OpHelpers.vectorShift(i16x8, .Left, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Shr_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Shr_S", pc, code, stack); + OpHelpers.vectorShift(i16x8, .Right, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Shr_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Shr_U", pc, code, stack); + OpHelpers.vectorShift(u16x8, .Right, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Add(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Add", pc, code, stack); + OpHelpers.vectorBinOp(i16x8, .Add, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Add_Sat_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Add_Sat_S", pc, code, stack); + OpHelpers.vectorBinOp(i16x8, .Add_Sat, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Add_Sat_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Add_Sat_U", pc, code, stack); + OpHelpers.vectorBinOp(u16x8, .Add_Sat, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Sub(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Sub", pc, code, stack); + OpHelpers.vectorBinOp(i16x8, .Sub, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Sub_Sat_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Sub_Sat_S", pc, code, stack); + OpHelpers.vectorBinOp(i16x8, .Sub_Sat, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Sub_Sat_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Sub_Sat_U", pc, code, stack); + OpHelpers.vectorBinOp(u16x8, .Sub_Sat, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Nearest(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Nearest", pc, code, stack); + OpHelpers.vectorUnOp(f64x2, .Nearest, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Mul(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Mul", pc, code, stack); + OpHelpers.vectorBinOp(i16x8, .Mul, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Min_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Min_S", pc, code, stack); + OpHelpers.vectorBinOp(i16x8, .Min, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Min_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Min_U", pc, code, stack); + OpHelpers.vectorBinOp(u16x8, .Min, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Max_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Max_S", pc, code, stack); + OpHelpers.vectorBinOp(i16x8, .Max, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Max_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Max_U", pc, code, stack); + OpHelpers.vectorBinOp(u16x8, .Max, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Avgr_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Avgr_U", pc, code, stack); + OpHelpers.vectorAvgrU(u16x8, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Extmul_Low_I8x16_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Extmul_Low_I8x16_S", pc, code, stack); + OpHelpers.vectorMulPairwise(i8x16, i16x8, .Low, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Extmul_High_I8x16_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Extmul_High_I8x16_S", pc, code, stack); + OpHelpers.vectorMulPairwise(i8x16, i16x8, .High, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Extmul_Low_I8x16_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Extmul_Low_I8x16_U", pc, code, stack); + OpHelpers.vectorMulPairwise(u8x16, u16x8, .Low, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I16x8_Extmul_High_I8x16_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I16x8_Extmul_High_I8x16_U", pc, code, stack); + OpHelpers.vectorMulPairwise(u8x16, u16x8, .High, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Abs(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Abs", pc, code, stack); + OpHelpers.vectorAbs(i32x4, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Neg(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Neg", pc, code, stack); + const vec = @as(i32x4, @bitCast(stack.popV128())); + const negated = -%vec; + stack.pushV128(@as(v128, @bitCast(negated))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_AllTrue(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_AllTrue", pc, code, stack); + const boolean: i32 = OpHelpers.vectorAllTrue(i32x4, stack.popV128()); + stack.pushI32(boolean); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Bitmask(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Bitmask", pc, code, stack); + const bitmask: i32 = OpHelpers.vectorBitmask(i32x4, stack.popV128()); + stack.pushI32(bitmask); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Extend_Low_I16x8_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Extend_Low_I16x8_S", pc, code, stack); + OpHelpers.vectorExtend(i16x8, i32x4, .Low, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Extend_High_I16x8_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Extend_High_I16x8_S", pc, code, stack); + OpHelpers.vectorExtend(i16x8, i32x4, .High, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Extend_Low_I16x8_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Extend_Low_I16x8_U", pc, code, stack); + OpHelpers.vectorExtend(u16x8, i32x4, .Low, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Extend_High_I16x8_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Extend_High_I16x8_U", pc, code, stack); + OpHelpers.vectorExtend(u16x8, i32x4, .High, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Shl(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Shl", pc, code, stack); + OpHelpers.vectorShift(i32x4, .Left, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Shr_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Shr_S", pc, code, stack); + OpHelpers.vectorShift(i32x4, .Right, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Shr_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Shr_U", pc, code, stack); + OpHelpers.vectorShift(u32x4, .Right, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Abs(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Abs", pc, code, stack); + OpHelpers.vectorAbs(i64x2, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Neg(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Neg", pc, code, stack); + const vec = @as(i64x2, @bitCast(stack.popV128())); + const negated = -%vec; + stack.pushV128(@as(v128, @bitCast(negated))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_AllTrue(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_AllTrue", pc, code, stack); + const boolean = OpHelpers.vectorAllTrue(i64x2, stack.popV128()); + stack.pushI32(boolean); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Bitmask(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Bitmask", pc, code, stack); + const bitmask: i32 = OpHelpers.vectorBitmask(i64x2, stack.popV128()); + stack.pushI32(bitmask); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Extend_Low_I32x4_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Extend_Low_I32x4_S", pc, code, stack); + OpHelpers.vectorExtend(i32x4, i64x2, .Low, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Extend_High_I32x4_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Extend_High_I32x4_S", pc, code, stack); + OpHelpers.vectorExtend(i32x4, i64x2, .High, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Extend_Low_I32x4_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Extend_Low_I32x4_U", pc, code, stack); + OpHelpers.vectorExtend(u32x4, i64x2, .Low, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Extend_High_I32x4_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Extend_High_I32x4_U", pc, code, stack); + OpHelpers.vectorExtend(u32x4, i64x2, .High, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Shl(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Shl", pc, code, stack); + OpHelpers.vectorShift(i64x2, .Left, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Shr_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Shr_S", pc, code, stack); + OpHelpers.vectorShift(i64x2, .Right, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Shr_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Shr_U", pc, code, stack); + OpHelpers.vectorShift(u64x2, .Right, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Add(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Add", pc, code, stack); + OpHelpers.vectorBinOp(i32x4, .Add, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Sub(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Sub", pc, code, stack); + OpHelpers.vectorBinOp(i32x4, .Sub, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Mul(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Mul", pc, code, stack); + OpHelpers.vectorBinOp(i32x4, .Mul, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Min_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Min_S", pc, code, stack); + OpHelpers.vectorBinOp(i32x4, .Min, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Min_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Min_U", pc, code, stack); + OpHelpers.vectorBinOp(u32x4, .Min, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Max_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Max_S", pc, code, stack); + OpHelpers.vectorBinOp(i32x4, .Max, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Max_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Max_U", pc, code, stack); + OpHelpers.vectorBinOp(u32x4, .Max, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Dot_I16x8_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Dot_I16x8_S", pc, code, stack); + const i32x8 = @Vector(8, i32); + const v1: i32x8 = @as(i16x8, @bitCast(stack.popV128())); + const v2: i32x8 = @as(i16x8, @bitCast(stack.popV128())); + const product = v1 * v2; + var arr: [4]i32 = undefined; + for (&arr, 0..) |*v, i| { + const p1: i32 = product[i * 2]; + const p2: i32 = product[(i * 2) + 1]; + v.* = p1 +% p2; + } + const dot: i32x4 = arr; + stack.pushV128(@as(v128, @bitCast(dot))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Extmul_Low_I16x8_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Extmul_Low_I16x8_S", pc, code, stack); + OpHelpers.vectorMulPairwise(i16x8, i32x4, .Low, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Extmul_High_I16x8_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Extmul_High_I16x8_S", pc, code, stack); + OpHelpers.vectorMulPairwise(i16x8, i32x4, .High, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Extmul_Low_I16x8_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Extmul_Low_I16x8_U", pc, code, stack); + OpHelpers.vectorMulPairwise(u16x8, u32x4, .Low, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Extmul_High_I16x8_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Extmul_High_I16x8_U", pc, code, stack); + OpHelpers.vectorMulPairwise(u16x8, u32x4, .High, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Add(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Add", pc, code, stack); + OpHelpers.vectorBinOp(i64x2, .Add, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Sub(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Sub", pc, code, stack); + OpHelpers.vectorBinOp(i64x2, .Sub, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Mul(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_Mul", pc, code, stack); + OpHelpers.vectorBinOp(i64x2, .Mul, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_EQ(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_EQ", pc, code, stack); + OpHelpers.vectorBoolOp(i64x2, .Eq, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_NE(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_NE", pc, code, stack); + OpHelpers.vectorBoolOp(i64x2, .Ne, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_LT_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_LT_S", pc, code, stack); + OpHelpers.vectorBoolOp(i64x2, .Lt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_GT_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_GT_S", pc, code, stack); + OpHelpers.vectorBoolOp(i64x2, .Gt, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_LE_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_LE_S", pc, code, stack); + OpHelpers.vectorBoolOp(i64x2, .Le, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_GE_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_GE_S", pc, code, stack); + OpHelpers.vectorBoolOp(i64x2, .Ge, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I64x2_Extmul_Low_I32x4_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_GE_S", pc, code, stack); + OpHelpers.vectorMulPairwise(i32x4, i64x2, .Low, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + fn op_I64x2_Extmul_High_I32x4_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_GE_S", pc, code, stack); + OpHelpers.vectorMulPairwise(i32x4, i64x2, .High, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + fn op_I64x2_Extmul_Low_I32x4_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_GE_S", pc, code, stack); + OpHelpers.vectorMulPairwise(u32x4, u64x2, .Low, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + fn op_I64x2_Extmul_High_I32x4_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I64x2_GE_S", pc, code, stack); + OpHelpers.vectorMulPairwise(u32x4, u64x2, .High, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Abs(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Abs", pc, code, stack); + const vec = @as(f32x4, @bitCast(stack.popV128())); + const abs = @fabs(vec); + stack.pushV128(@as(v128, @bitCast(abs))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Neg(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Neg", pc, code, stack); + const vec = @as(f32x4, @bitCast(stack.popV128())); + const negated = -vec; + stack.pushV128(@as(v128, @bitCast(negated))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Sqrt(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Sqrt", pc, code, stack); + const vec = @as(f32x4, @bitCast(stack.popV128())); + const root = @sqrt(vec); + stack.pushV128(@as(v128, @bitCast(root))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Add(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Add", pc, code, stack); + OpHelpers.vectorBinOp(f32x4, .Add, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Sub(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Sub", pc, code, stack); + OpHelpers.vectorBinOp(f32x4, .Sub, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Mul(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Mul", pc, code, stack); + OpHelpers.vectorBinOp(f32x4, .Mul, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Div(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Div", pc, code, stack); + OpHelpers.vectorBinOp(f32x4, .Div, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Min(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Min", pc, code, stack); + OpHelpers.vectorBinOp(f32x4, .Min, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Max(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Max", pc, code, stack); + OpHelpers.vectorBinOp(f32x4, .Max, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_PMin(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_PMin", pc, code, stack); + OpHelpers.vectorBinOp(f32x4, .PMin, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_PMax(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_PMax", pc, code, stack); + OpHelpers.vectorBinOp(f32x4, .PMax, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Abs(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Abs", pc, code, stack); + const vec = @as(f64x2, @bitCast(stack.popV128())); + const abs = @fabs(vec); + stack.pushV128(@as(v128, @bitCast(abs))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Neg(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Neg", pc, code, stack); + const vec = @as(f64x2, @bitCast(stack.popV128())); + const negated = -vec; + stack.pushV128(@as(v128, @bitCast(negated))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Sqrt(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Sqrt", pc, code, stack); + const vec = @as(f64x2, @bitCast(stack.popV128())); + const root = @sqrt(vec); + stack.pushV128(@as(v128, @bitCast(root))); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Add(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Add", pc, code, stack); + OpHelpers.vectorBinOp(f64x2, .Add, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Sub(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Sub", pc, code, stack); + OpHelpers.vectorBinOp(f64x2, .Sub, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Mul(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Mul", pc, code, stack); + OpHelpers.vectorBinOp(f64x2, .Mul, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Div(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Div", pc, code, stack); + OpHelpers.vectorBinOp(f64x2, .Div, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Min(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Min", pc, code, stack); + OpHelpers.vectorBinOp(f64x2, .Min, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Max(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Max", pc, code, stack); + OpHelpers.vectorBinOp(f64x2, .Max, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_PMin(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_PMin", pc, code, stack); + OpHelpers.vectorBinOp(f64x2, .PMin, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_PMax(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_PMax", pc, code, stack); + OpHelpers.vectorBinOp(f64x2, .PMax, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Trunc_Sat_F32x4_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Trunc_Sat_F32x4_S", pc, code, stack); + OpHelpers.vectorConvert(f32x4, i32x4, .Low, .Saturate, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Trunc_Sat_F32x4_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Trunc_Sat_F32x4_U", pc, code, stack); + OpHelpers.vectorConvert(f32x4, u32x4, .Low, .Saturate, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Convert_I32x4_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Convert_I32x4_S", pc, code, stack); + OpHelpers.vectorConvert(i32x4, f32x4, .Low, .SafeCast, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F32x4_Convert_I32x4_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F32x4_Convert_I32x4_U", pc, code, stack); + OpHelpers.vectorConvert(u32x4, f32x4, .Low, .SafeCast, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Trunc_Sat_F64x2_S_Zero(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Trunc_Sat_F64x2_S_Zero", pc, code, stack); + OpHelpers.vectorConvert(f64x2, i32x4, .Low, .Saturate, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_I32x4_Trunc_Sat_F64x2_U_Zero(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("I32x4_Trunc_Sat_F64x2_U_Zero", pc, code, stack); + OpHelpers.vectorConvert(f64x2, u32x4, .Low, .Saturate, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Convert_Low_I32x4_S(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Convert_Low_I32x4_S", pc, code, stack); + OpHelpers.vectorConvert(i32x4, f64x2, .Low, .SafeCast, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } + + fn op_F64x2_Convert_Low_I32x4_U(pc: u32, code: [*]const Instruction, stack: *Stack) anyerror!void { + try debugPreamble("F64x2_Convert_Low_I32x4_U", pc, code, stack); + OpHelpers.vectorConvert(u32x4, f64x2, .Low, .SafeCast, stack); + try @call(.always_tail, InstructionFuncs.lookup(code[pc + 1].opcode), .{ pc + 1, code, stack }); + } +}; + +pub const StackVM = struct { + const TrapType = enum { + Step, + Explicit, + }; + + const TrappedOpcode = struct { + address: u32, + opcode: Opcode, + type: TrapType, + }; + + const DebugState = struct { + trapped_opcodes: std.ArrayList(TrappedOpcode), // TODO multiarraylist? + pc: u32 = 0, + trap_counter: u32 = 0, // used for trapping on step + is_invoking: bool = false, + + fn onInvokeFinished(state: *DebugState) void { + state.pc = 0; + state.is_invoking = false; + state.trap_counter = 0; + } + }; + + stack: Stack, + functions: std.ArrayList(FunctionInstance), + debug_state: ?DebugState, + + fn fromVM(vm: *VM) *StackVM { + return @as(*StackVM, @alignCast(@ptrCast(vm.impl))); + } + + pub fn init(vm: *VM) void { + var self: *StackVM = fromVM(vm); + self.stack = Stack.init(vm.allocator); + self.functions = std.ArrayList(FunctionInstance).init(vm.allocator); + self.debug_state = null; + } + + pub fn deinit(vm: *VM) void { + var self: *StackVM = fromVM(vm); + self.functions.deinit(); + self.stack.deinit(); + if (self.debug_state) |*debug_state| { + debug_state.trapped_opcodes.deinit(); + } + } + + pub fn instantiate(vm: *VM, module: *ModuleInstance, opts: ModuleInstantiateOpts) anyerror!void { + var self: *StackVM = fromVM(vm); + + if (opts.enable_debug) { + self.debug_state = DebugState{ + .pc = 0, + .trapped_opcodes = std.ArrayList(TrappedOpcode).init(vm.allocator), + }; + } + + const stack_size = if (opts.stack_size > 0) opts.stack_size else 1024 * 128; + const stack_size_f = @as(f64, @floatFromInt(stack_size)); + + try self.stack.allocMemory(.{ + .max_values = @as(u32, @intFromFloat(stack_size_f * 0.85)), + .max_labels = @as(u16, @intFromFloat(stack_size_f * 0.14)), + .max_frames = @as(u16, @intFromFloat(stack_size_f * 0.01)), + }); + + try self.functions.ensureTotalCapacity(module.module_def.functions.items.len); + for (module.module_def.functions.items, 0..) |*def_func, i| { + const func_type: *const FunctionTypeDefinition = &module.module_def.types.items[def_func.type_index]; + const param_types: []const ValType = func_type.getParams(); + + var local_types = std.ArrayList(ValType).init(vm.allocator); + try local_types.ensureTotalCapacity(param_types.len + def_func.locals.items.len); + local_types.appendSliceAssumeCapacity(param_types); + local_types.appendSliceAssumeCapacity(def_func.locals.items); + + var f = FunctionInstance{ + .type_def_index = def_func.type_index, + .def_index = @as(u32, @intCast(i)), + .instructions_begin = def_func.instructions_begin, + .local_types = local_types, + }; + try self.functions.append(f); + } + } + + pub fn invoke(vm: *VM, module: *ModuleInstance, handle: FunctionHandle, params: [*]const Val, returns: [*]Val, opts: InvokeOpts) anyerror!void { + var self: *StackVM = fromVM(vm); + + if (self.debug_state) |*debug_state| { + debug_state.pc = 0; + debug_state.is_invoking = true; + + if (opts.trap_on_start) { + debug_state.trap_counter = 1; + } + } + + switch (handle.type) { + .Export => try self.invokeInternal(module, handle.index, params, returns), + .Import => try invokeImportInternal(module, handle.index, params, returns, opts), + } + } + + pub fn invokeWithIndex(vm: *VM, module: *ModuleInstance, func_index: usize, params: [*]const Val, returns: [*]Val) anyerror!void { + var self: *StackVM = fromVM(vm); + + const num_imports = module.module_def.imports.functions.items.len; + if (func_index >= num_imports) { + var instance_index = func_index - num_imports; + try self.invokeInternal(module, instance_index, params, returns); + } else { + try invokeImportInternal(module, func_index, params, returns, .{}); + } + } + + pub fn resumeInvoke(vm: *VM, module: *ModuleInstance, returns: []Val) anyerror!void { + var self: *StackVM = fromVM(vm); + + std.debug.assert(self.debug_state != null); + std.debug.assert(self.debug_state.?.is_invoking); + + const debug_state = &self.debug_state.?; + const opcode: Opcode = blk: { + for (debug_state.trapped_opcodes.items) |op| { + if (op.address == debug_state.pc) { + break :blk op.opcode; + } + } + unreachable; // Should never get into a state where a trapped opcode doesn't have an associated record + }; + + const op_func = InstructionFuncs.lookup(opcode); + try op_func(debug_state.pc, module.module_def.code.instructions.items.ptr, &self.stack); + + if (returns.len > 0) { + var index: i32 = @as(i32, @intCast(returns.len - 1)); + while (index >= 0) { + returns[@as(usize, @intCast(index))] = self.stack.popValue(); + index -= 1; + } + } + + debug_state.onInvokeFinished(); + } + + pub fn step(vm: *VM, module: *ModuleInstance, returns: []Val) !void { + var self: *StackVM = fromVM(vm); + + const debug_state = &self.debug_state.?; + + if (debug_state.is_invoking == false) { + return; + } + + // Don't trap on the first instruction executed, but the next. Note that we can't just trap pc + 1 + // since the current instruction may branch. + debug_state.trap_counter = 2; + + try vm.resumeInvoke(module, returns); + } + + pub fn setDebugTrap(vm: *VM, module: *ModuleInstance, wasm_address: u32, mode: DebugTrapInstructionMode) !bool { + var self: *StackVM = fromVM(vm); + + std.debug.assert(self.debug_state != null); + const instruction_index = module.module_def.code.wasm_address_to_instruction_index.get(wasm_address) orelse return false; + + var debug_state = &self.debug_state.?; + for (debug_state.trapped_opcodes.items, 0..) |*existing, i| { + if (existing.address == instruction_index and (existing.type == .Step or existing.type == .Explicit)) { + switch (mode) { + .Enable => {}, + .Disable => { + _ = debug_state.trapped_opcodes.swapRemove(i); + }, + } + return true; + } + } + + if (mode == .Enable) { + var instructions: []Instruction = module.module_def.code.instructions.items; + const original_op = instructions[instruction_index].opcode; + instructions[instruction_index].opcode = .DebugTrap; + + try debug_state.trapped_opcodes.append(TrappedOpcode{ + .opcode = original_op, + .address = instruction_index, + .type = .Explicit, + }); + return true; + } + + return false; + } + + pub fn formatBacktrace(vm: *VM, indent: u8, allocator: std.mem.Allocator) anyerror!std.ArrayList(u8) { + var self: *StackVM = fromVM(vm); + + var buffer = std.ArrayList(u8).init(allocator); + try buffer.ensureTotalCapacity(512); + var writer = buffer.writer(); + + for (self.stack.frames[0..self.stack.num_frames], 0..) |_, i| { + const reverse_index = (self.stack.num_frames - 1) - i; + const frame: *const CallFrame = &self.stack.frames[reverse_index]; + + var indent_level: usize = 0; + while (indent_level < indent) : (indent_level += 1) { + try writer.print("\t", .{}); + } + + const name_section: *const NameCustomSection = &frame.module_instance.module_def.name_section; + const module_name = name_section.getModuleName(); + + const func_name_index: usize = frame.func.def_index + frame.module_instance.module_def.imports.functions.items.len; + const function_name = name_section.findFunctionName(func_name_index); + + try writer.print("{}: {s}!{s}\n", .{ reverse_index, module_name, function_name }); + } + + return buffer; + } + + pub fn findFuncTypeDef(vm: *VM, module: *ModuleInstance, local_func_index: usize) *const FunctionTypeDefinition { + var self: *StackVM = fromVM(vm); + + var func_instance: *const FunctionInstance = &self.functions.items[local_func_index]; + var func_type_def: *const FunctionTypeDefinition = &module.module_def.types.items[func_instance.type_def_index]; + return func_type_def; + } + + fn invokeInternal(self: *StackVM, module: *ModuleInstance, func_instance_index: usize, params: [*]const Val, returns: [*]Val) !void { + const func: FunctionInstance = self.functions.items[func_instance_index]; + const func_def: FunctionDefinition = module.module_def.functions.items[func.def_index]; + const func_type: *const FunctionTypeDefinition = &module.module_def.types.items[func.type_def_index]; + const param_types: []const ValType = func_type.getParams(); + const return_types: []const ValType = func_type.getReturns(); + + var params_slice = params[0..param_types.len]; + var returns_slice = returns[0..return_types.len]; + + // Ensure any leftover stack state doesn't pollute this invoke. Can happen if the previous invoke returned an error. + self.stack.popAll(); + + // pushFrame() assumes the stack already contains the params to the function, so ensure they exist + // on the value stack + for (params_slice) |v| { + self.stack.pushValue(v); + } + + try self.stack.pushFrame(&func, module, param_types, func.local_types.items, func_type.calcNumReturns()); + try self.stack.pushLabel(@as(u32, @intCast(return_types.len)), @intCast(func_def.continuation)); + + DebugTrace.traceFunction(module, self.stack.num_frames, func.def_index); + + try InstructionFuncs.run(@intCast(func.instructions_begin), module.module_def.code.instructions.items.ptr, &self.stack); + + if (returns_slice.len > 0) { + var index: i32 = @as(i32, @intCast(returns_slice.len - 1)); + while (index >= 0) { + returns_slice[@as(usize, @intCast(index))] = self.stack.popValue(); + index -= 1; + } + } + + if (self.debug_state) |*debug_state| { + debug_state.onInvokeFinished(); + } + } + + fn invokeImportInternal(module: *ModuleInstance, import_index: usize, params: [*]const Val, returns: [*]Val, opts: InvokeOpts) !void { + const func_import: *const FunctionImport = &module.store.imports.functions.items[import_index]; + switch (func_import.data) { + .Host => |data| { + DebugTrace.traceHostFunction(module, 1, func_import.name); + + data.callback(data.userdata, module, params, returns); + }, + .Wasm => |data| { + var import_instance: *ModuleInstance = data.module_instance; + const handle: FunctionHandle = try import_instance.getFunctionHandle(func_import.name); // TODO could cache this in the func_import + try import_instance.vm.invoke(import_instance, handle, params, returns, opts); + }, + } + } +}; diff --git a/src/ext/bytebox/src/wasi.zig b/src/ext/bytebox/src/wasi.zig new file mode 100644 index 00000000..76e2b890 --- /dev/null +++ b/src/ext/bytebox/src/wasi.zig @@ -0,0 +1,2616 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const core = @import("core.zig"); + +const StringPool = @import("stringpool.zig"); + +const Val = core.Val; +const ValType = core.ValType; +const ModuleInstance = core.ModuleInstance; +const ModuleImportPackage = core.ModuleImportPackage; + +const WasiContext = struct { + const FdInfo = struct { + fd: std.os.fd_t, + path_absolute: []const u8, + rights: WasiRights, + is_preopen: bool, + open_handles: u32 = 1, + dir_entries: std.ArrayList(WasiDirEntry), + }; + + cwd: []const u8, + argv: [][]const u8 = &[_][]u8{}, + env: [][]const u8 = &[_][]u8{}, + dirs: [][]const u8 = &[_][]u8{}, + + // having a master table with a side table of wasi file descriptors lets us map multiple wasi fds into the same + // master entry and avoid duplicating OS handles, which has proved buggy on win32 + fd_table: std.ArrayList(FdInfo), + fd_table_freelist: std.ArrayList(u32), + fd_wasi_table: std.AutoHashMap(u32, u32), // fd_wasi -> fd_table index + fd_path_lookup: std.StringHashMap(u32), // path_absolute -> fd_table index + + strings: StringPool, + next_fd_id: u32 = 3, + allocator: std.mem.Allocator, + + fn init(opts: *const WasiOpts, allocator: std.mem.Allocator) !WasiContext { + var context = WasiContext{ + .cwd = "", + .fd_table = std.ArrayList(FdInfo).init(allocator), + .fd_table_freelist = std.ArrayList(u32).init(allocator), + .fd_wasi_table = std.AutoHashMap(u32, u32).init(allocator), + .fd_path_lookup = std.StringHashMap(u32).init(allocator), + .strings = StringPool.init(1024 * 1024 * 4, allocator), // 4MB for absolute paths + .allocator = allocator, + }; + + { + var cwd_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const cwd: []const u8 = try std.os.getcwd(&cwd_buffer); + context.cwd = try context.strings.put(cwd); + } + + if (opts.argv) |argv| { + context.argv = try context.allocator.dupe([]const u8, argv); + for (argv, 0..) |arg, i| { + context.argv[i] = try context.strings.put(arg); + } + } + + if (opts.env) |env| { + context.env = try context.allocator.dupe([]const u8, env); + for (env, 0..) |e, i| { + context.env[i] = try context.strings.put(e); + } + } + + if (opts.dirs) |dirs| { + context.dirs = try context.allocator.dupe([]const u8, dirs); + for (dirs, 0..) |dir, i| { + context.dirs[i] = try context.resolveAndCache(null, dir); + } + } + + const path_stdin = try context.strings.put("stdin"); + const path_stdout = try context.strings.put("stdout"); + const path_stderr = try context.strings.put("stderr"); + + var empty_dir_entries = std.ArrayList(WasiDirEntry).init(allocator); + + try context.fd_table.ensureTotalCapacity(3 + context.dirs.len); + context.fd_table.appendAssumeCapacity(FdInfo{ .fd = std.io.getStdIn().handle, .path_absolute = path_stdin, .rights = .{}, .is_preopen = true, .dir_entries = empty_dir_entries }); + context.fd_table.appendAssumeCapacity(FdInfo{ .fd = std.io.getStdOut().handle, .path_absolute = path_stdout, .rights = .{}, .is_preopen = true, .dir_entries = empty_dir_entries }); + context.fd_table.appendAssumeCapacity(FdInfo{ .fd = std.io.getStdErr().handle, .path_absolute = path_stderr, .rights = .{}, .is_preopen = true, .dir_entries = empty_dir_entries }); + try context.fd_wasi_table.put(0, 0); + try context.fd_wasi_table.put(1, 1); + try context.fd_wasi_table.put(2, 2); + + for (context.dirs) |dir_path| { + const openflags = WasiOpenFlags{ + .creat = false, + .directory = true, + .excl = false, + .trunc = false, + }; + const fdflags = WasiFdFlags{ + .append = false, + .dsync = false, + .nonblock = false, + .rsync = false, + .sync = false, + }; + const rights = WasiRights{ + .fd_read = true, + .fd_write = false, // we don't need to edit the directory itself + .fd_seek = false, // directories don't have seek rights + }; + const lookupflags = WasiLookupFlags{ + .symlink_follow = true, + }; + var unused: Errno = undefined; + const is_preopen = true; + _ = context.fdOpen(null, dir_path, lookupflags, openflags, fdflags, rights, is_preopen, &unused); + } + + return context; + } + + fn deinit(self: *WasiContext) void { + for (self.fd_table.items) |item| { + item.dir_entries.deinit(); + } + self.fd_table.deinit(); + self.fd_wasi_table.deinit(); + self.fd_path_lookup.deinit(); + self.strings.deinit(); + } + + fn resolveAndCache(self: *WasiContext, fd_info_dir: ?*FdInfo, path: []const u8) ![]const u8 { + if (std.mem.indexOf(u8, path, &[_]u8{0})) |_| { + return error.NullTerminatedPath; + } + + // validate the scope of the path never leaves the preopen root + { + var depth: i32 = 0; + var token_iter = std.mem.tokenize(u8, path, &[_]u8{ '/', '\\' }); + while (token_iter.next()) |item| { + if (std.mem.eql(u8, item, "..")) { + depth -= 1; + } else { + depth += 1; + } + if (depth < 0) { + return error.PathInvalidDepth; + } + } + } + + var static_path_buffer: [std.fs.MAX_PATH_BYTES * 2]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&static_path_buffer); + const allocator = fba.allocator(); + + const dir_path = if (fd_info_dir) |info| info.path_absolute else self.cwd; + const paths = [_][]const u8{ dir_path, path }; + + if (std.fs.path.resolve(allocator, &paths)) |resolved_path| { + // preserve trailing slash + var final_path = resolved_path; + const last_char = path[path.len - 1]; + if (last_char == '/' or last_char == '\\') { + final_path = try allocator.realloc(resolved_path, resolved_path.len + 1); + final_path[final_path.len - 1] = std.fs.path.sep; + } + + const cached_path: []const u8 = try self.strings.findOrPut(final_path); + return cached_path; + } else |err| { + return err; + } + } + + fn fdLookup(self: *const WasiContext, fd_wasi: u32, errno: *Errno) ?*FdInfo { + if (self.fd_wasi_table.get(fd_wasi)) |fd_table_index| { + return &self.fd_table.items[fd_table_index]; + } + + errno.* = Errno.BADF; + return null; + } + + fn fdDirPath(self: *WasiContext, fd_wasi: u32, errno: *Errno) ?[]const u8 { + if (Helpers.isStdioHandle(fd_wasi) == false) { // std handles are 0, 1, 2 so they're not valid paths + if (self.fd_wasi_table.get(fd_wasi)) |fd_table_index| { + const info: *FdInfo = &self.fd_table.items[fd_table_index]; + const path_relative = info.path_absolute[self.cwd.len + 1 ..]; // +1 to skip the last path separator + return path_relative; + } + } + + errno.* = Errno.BADF; + return null; + } + + fn fdOpen(self: *WasiContext, fd_info_dir: ?*FdInfo, path: []const u8, lookupflags: WasiLookupFlags, openflags: WasiOpenFlags, fdflags: WasiFdFlags, rights: WasiRights, is_preopen: bool, errno: *Errno) ?u32 { + if (self.resolveAndCache(fd_info_dir, path)) |resolved_path| { + // Found an entry for this path, just reuse it while creating a new wasi fd + if (self.fd_path_lookup.get(resolved_path)) |fd_table_index| { + var fd_wasi: u32 = self.next_fd_id; + self.next_fd_id += 1; + self.fd_wasi_table.put(fd_wasi, fd_table_index) catch |err| { + errno.* = Errno.translateError(err); + return null; + }; + self.fd_table.items[fd_table_index].open_handles += 1; + return fd_wasi; + } + + const open_func = if (builtin.os.tag == .windows) Helpers.openPathWindows else Helpers.openPathPosix; + + // if a path ends with a separator, posix treats it as a directory even if the flag isn't set, so make sure + // we explicitly set the directory flag for similar behavior on windows + var openflags2 = openflags; + if (std.mem.endsWith(u8, resolved_path, std.fs.path.sep_str)) { + openflags2.directory = true; + } + + if (open_func(resolved_path, lookupflags, openflags2, fdflags, rights, errno)) |fd_os| { + var fd_wasi: u32 = self.next_fd_id; + self.next_fd_id += 1; + + var info: *FdInfo = undefined; + var fd_table_index: u32 = undefined; + + if (self.fd_table_freelist.popOrNull()) |free_index| { + fd_table_index = free_index; + info = &self.fd_table.items[free_index]; + } else { + self.fd_table_freelist.ensureTotalCapacity(self.fd_table.items.len + 1) catch |err| { + errno.* = Errno.translateError(err); + return null; + }; + fd_table_index = @intCast(self.fd_table.items.len); + info = self.fd_table.addOne() catch |err| { + errno.* = Errno.translateError(err); + return null; + }; + } + + info.fd = fd_os; + info.path_absolute = resolved_path; + info.rights = rights; + info.is_preopen = is_preopen; + info.open_handles = 1; + info.dir_entries = std.ArrayList(WasiDirEntry).init(self.allocator); + + self.fd_wasi_table.put(fd_wasi, fd_table_index) catch |err| { + errno.* = Errno.translateError(err); + return null; + }; + self.fd_path_lookup.put(resolved_path, fd_table_index) catch |err| { + errno.* = Errno.translateError(err); + return null; + }; + + return fd_wasi; + } + } else |err| { + errno.* = Errno.translateError(err); + } + + return null; + } + + fn fdUpdate(self: *WasiContext, fd_wasi: u32, new_fd: std.os.fd_t) void { + if (self.fd_wasi_table.get(fd_wasi)) |fd_table_index| { + self.fd_table.items[fd_table_index].fd = new_fd; + } else { + unreachable; // fdUpdate should always be nested inside an fdLookup + } + } + + fn fdRenumber(self: *WasiContext, fd_wasi: u32, fd_wasi_new: u32, errno: *Errno) void { + if (self.fd_wasi_table.get(fd_wasi)) |fd_table_index| { + const fd_info: *const FdInfo = &self.fd_table.items[fd_table_index]; + + if (fd_info.is_preopen) { + errno.* = Errno.NOTSUP; + return; + } + + if (self.fd_wasi_table.get(fd_wasi_new)) |fd_other_table_index| { + // need to replace the existing entry with the new one + if (fd_other_table_index != fd_table_index) { + const fd_info_other: *const FdInfo = &self.fd_table.items[fd_table_index]; + if (fd_info_other.is_preopen) { + errno.* = Errno.NOTSUP; + return; + } + + var unused: Errno = undefined; + self.fdClose(fd_wasi_new, &unused); + } + } + + self.fd_wasi_table.put(fd_wasi_new, fd_table_index) catch |err| { + errno.* = Errno.translateError(err); + return; + }; + + _ = self.fd_wasi_table.remove(fd_wasi); + } else { + errno.* = Errno.BADF; + } + } + + fn fdClose(self: *WasiContext, fd_wasi: u32, errno: *Errno) void { + if (self.fd_wasi_table.get(fd_wasi)) |fd_table_index| { + var fd_info: *FdInfo = &self.fd_table.items[fd_table_index]; + + _ = self.fd_wasi_table.remove(fd_wasi); + _ = self.fd_path_lookup.remove(fd_info.path_absolute); + + fd_info.open_handles -= 1; + if (fd_info.open_handles == 0) { + std.os.close(fd_info.fd); + self.fd_table_freelist.appendAssumeCapacity(fd_table_index); // capacity was allocated when the associated fd_table slot was allocated + } + } else { + errno.* = Errno.BADF; + } + } + + // The main intention for this function is to close all wasi fd when a path is unlinked. + fn fdCleanup(self: *WasiContext, path_absolute: []const u8) void { + if (self.fd_path_lookup.get(path_absolute)) |fd_table_index| { + var iter = self.fd_wasi_table.iterator(); + while (iter.next()) |kv| { + if (kv.value_ptr.* == fd_table_index) { + self.fd_wasi_table.removeByPtr(kv.key_ptr); + } + } + + _ = self.fd_path_lookup.remove(path_absolute); + + var fd_info: *FdInfo = &self.fd_table.items[fd_table_index]; + std.os.close(fd_info.fd); + fd_info.open_handles = 0; + self.fd_table_freelist.appendAssumeCapacity(fd_table_index); // capacity was allocated when the associated fd_table slot was allocated + } + } + + fn hasPathAccess(self: *WasiContext, fd_info: *const FdInfo, relative_path: []const u8, errno: *Errno) bool { + errno.* = Errno.PERM; + + if (self.dirs.len > 0) { + const paths = [_][]const u8{ fd_info.path_absolute, relative_path }; + + if (std.fs.path.resolve(self.allocator, &paths)) |resolved_path| { + defer self.allocator.free(resolved_path); + for (self.dirs) |allowdir| { + // can use startsWith to check because all the paths have been passed through resolve() already + if (std.mem.startsWith(u8, resolved_path, allowdir)) { + errno.* = Errno.SUCCESS; + return true; + } + } + } else |err| { + errno.* = Errno.translateError(err); + } + } + + return false; + } + + fn fromUserdata(userdata: ?*anyopaque) *WasiContext { + std.debug.assert(userdata != null); + return @as(*WasiContext, @alignCast(@ptrCast(userdata.?))); + } +}; + +// Values taken from https://github.com/AssemblyScript/wasi-shim/blob/main/assembly/bindings/ +const Errno = enum(u8) { + SUCCESS = 0, // No error occurred. System call completed successfully. + TOOBIG = 1, // Argument list too long. + ACCES = 2, // Permission denied. + ADDRINUSE = 3, // Address in use. + ADDRNOTAVAIL = 4, // Address not available. + AFNOSUPPORT = 5, // Address family not supported. + AGAIN = 6, // Resource unavailable, or operation would block. + ALREADY = 7, // Connection already in progress. + BADF = 8, // Bad file descriptor. + BADMSG = 9, // Bad message. + BUSY = 10, // Device or resource busy. + CANCELED = 11, // Operation canceled. + CHILD = 12, // No child processes. + CONNABORTED = 13, // Connection aborted. + CONNREFUSED = 14, // Connection refused. + CONNRESET = 15, // Connection reset. + DEADLK = 16, // Resource deadlock would occur. + DESTADDRREQ = 17, // Destination address required. + DOM = 18, // Mathematics argument out of domain of function. + DQUOT = 19, // Reserved. + EXIST = 20, // File exists. + FAULT = 21, // Bad address. + FBIG = 22, // File too large. + HOSTUNREACH = 23, // Host is unreachable. + IDRM = 24, // Identifier removed. + ILSEQ = 25, // Illegal byte sequence. + INPROGRESS = 26, // Operation in progress. + INTR = 27, // Interrupted function. + INVAL = 28, // Invalid argument. + IO = 29, // I/O error. + ISCONN = 30, // Socket is connected. + ISDIR = 31, // Is a directory. + LOOP = 32, // Too many levels of symbolic links. + MFILE = 33, // File descriptor value too large. + MLINK = 34, // Too many links. + MSGSIZE = 35, // Message too large. + MULTIHOP = 36, // Reserved. + NAMETOOLONG = 37, // Filename too long. + NETDOWN = 38, // Network is down. + NETRESET = 39, // Connection aborted by network. + NETUNREACH = 40, // Network unreachable. + NFILE = 41, // Too many files open in system. + NOBUFS = 42, // No buffer space available. + NODEV = 43, // No such device. + NOENT = 44, // No such file or directory. + NOEXEC = 45, // Executable file format error. + NOLCK = 46, // No locks available. + NOLINK = 47, // Reserved. + NOMEM = 48, // Not enough space. + NOMSG = 49, // No message of the desired type. + NOPROTOOPT = 50, // Protocol not available. + NOSPC = 51, // No space left on device. + NOSYS = 52, // Function not supported. + NOTCONN = 53, // The socket is not connected. + NOTDIR = 54, // Not a directory or a symbolic link to a directory. + NOTEMPTY = 55, // Directory not empty. + NOTRECOVERABLE = 56, // State not recoverable. + NOTSOCK = 57, // Not a socket. + NOTSUP = 58, // Not supported, or operation not supported on socket. + NOTTY = 59, // Inappropriate I/O control operation. + NXIO = 60, // No such device or address. + OVERFLOW = 61, // Value too large to be stored in data type. + OWNERDEAD = 62, // Previous owner died. + PERM = 63, // Operation not permitted. + PIPE = 64, // Broken pipe. + PROTO = 65, // Protocol error. + PROTONOSUPPORT = 66, // Protocol not supported. + PROTOTYPE = 67, // Protocol wrong type for socket. + RANGE = 68, // Result too large. + ROFS = 69, // Read-only file system. + SPIPE = 70, // Invalid seek. + SRCH = 71, // No such process. + STALE = 72, // Reserved. + TIMEDOUT = 73, // Connection timed out. + TXTBSY = 74, // Text file busy. + XDEV = 75, // Cross-device link. + NOTCAPABLE = 76, // Extension: Capabilities insufficient. + + fn translateError(err: anyerror) Errno { + return switch (err) { + error.AccessDenied => .ACCES, + error.DeviceBusy => .BUSY, + error.DirNotEmpty => .NOTEMPTY, + error.DiskQuota => .DQUOT, + error.FileBusy => .TXTBSY, + error.FileLocksNotSupported => .NOTSUP, + error.FileNotFound => .NOENT, + error.FileTooBig => .FBIG, + error.FileSystem => .IO, + error.InputOutput => .IO, + error.IsDir => .ISDIR, + error.LinkQuotaExceeded => .MLINK, + error.NameTooLong => .NAMETOOLONG, + error.NoDevice => .NODEV, + error.NoSpaceLeft => .NOSPC, + error.NotDir => .NOTDIR, + error.OutOfMemory => .NOMEM, + error.PathAlreadyExists => .EXIST, + error.ProcessFdQuotaExceeded => .MFILE, + error.ReadOnlyFileSystem => .ROFS, + error.SymLinkLoop => .LOOP, + error.SystemFdQuotaExceeded => .NFILE, + error.SystemResources => .NOMEM, + error.Unseekable => .SPIPE, + error.WouldBlock => .AGAIN, + error.InvalidUtf8 => .INVAL, + error.BadPathName => .INVAL, + error.NullTerminatedPath => .ILSEQ, + error.PathInvalidDepth => .PERM, + else => .INVAL, + }; + } + + fn getLastWin32Error() Errno { + const err = std.os.windows.kernel32.GetLastError(); + return switch (err) { + else => .INVAL, + }; + } +}; + +const WasiLookupFlags = packed struct { + symlink_follow: bool, +}; + +const WasiOpenFlags = packed struct { + creat: bool, + directory: bool, + excl: bool, + trunc: bool, +}; + +const WasiRights = packed struct { + fd_datasync: bool = true, + fd_read: bool = true, + fd_seek: bool = true, + fd_fdstat_set_flags: bool = true, + fd_sync: bool = true, + fd_tell: bool = true, + fd_write: bool = true, + fd_advise: bool = true, + fd_allocate: bool = true, + path_create_directory: bool = true, + path_create_file: bool = true, + path_link_source: bool = true, + path_link_target: bool = true, + path_open: bool = true, + fd_readdir: bool = true, + path_readlink: bool = true, + path_rename_source: bool = true, + path_rename_target: bool = true, + path_filestat_get: bool = true, + path_filestat_set_size: bool = true, + path_filestat_set_times: bool = true, + fd_filestat_get: bool = true, + fd_filestat_set_size: bool = true, + fd_filestat_set_times: bool = true, + path_symlink: bool = true, + path_remove_directory: bool = true, + path_unlink_file: bool = true, + poll_fd_readwrite: bool = true, + sock_shutdown: bool = true, + sock_accept: bool = true, +}; + +const WasiFdFlags = packed struct { + append: bool, + dsync: bool, + nonblock: bool, + rsync: bool, + sync: bool, +}; + +const WasiDirEntry = struct { + inode: u64, + filetype: std.os.wasi.filetype_t, + filename: []u8, +}; + +const Whence = enum(u8) { + Set, + Cur, + End, + + fn fromInt(int: i32) ?Whence { + return switch (int) { + 0 => .Set, + 1 => .Cur, + 2 => .End, + else => null, + }; + } +}; + +// Since the windows API is so large, wrapping the win32 API is not in the scope of the stdlib, so it +// prefers to only declare windows functions it uses. In these cases we just declare the needed functions +// and types here. +const WindowsApi = struct { + const windows = std.os.windows; + + const BOOL = windows.BOOL; + const DWORD = windows.DWORD; + const FILETIME = windows.FILETIME; + const HANDLE = windows.HANDLE; + const LARGE_INTEGER = windows.LARGE_INTEGER; + const ULONG = windows.ULONG; + const WCHAR = windows.WCHAR; + const WINAPI = windows.WINAPI; + const LPCWSTR = windows.LPCWSTR; + + const CLOCK = struct { + const REALTIME = 0; + const MONOTONIC = 1; + const PROCESS_CPUTIME_ID = 2; + const THREAD_CPUTIME_ID = 3; + }; + + const BY_HANDLE_FILE_INFORMATION = extern struct { + dwFileAttributes: DWORD, + ftCreationTime: FILETIME, + ftLastAccessTime: FILETIME, + ftLastWriteTime: FILETIME, + dwVolumeSerialNumber: DWORD, + nFileSizeHigh: DWORD, + nFileSizeLow: DWORD, + nNumberOfLinks: DWORD, + nFileIndexHigh: DWORD, + nFileIndexLow: DWORD, + }; + + const FILE_ID_FULL_DIR_INFORMATION = extern struct { + NextEntryOffset: ULONG, + FileIndex: ULONG, + CreationTime: LARGE_INTEGER, + LastAccessTime: LARGE_INTEGER, + LastWriteTime: LARGE_INTEGER, + ChangeTime: LARGE_INTEGER, + EndOfFile: LARGE_INTEGER, + AllocationSize: LARGE_INTEGER, + FileAttributes: ULONG, + FileNameLength: ULONG, + EaSize: ULONG, + FileId: LARGE_INTEGER, + FileName: [1]WCHAR, + }; + + const SYMBOLIC_LINK_FLAG_FILE: DWORD = 0x0; + const SYMBOLIC_LINK_FLAG_DIRECTORY: DWORD = 0x1; + const SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE: DWORD = 0x2; + + extern "kernel32" fn GetSystemTimeAdjustment(timeAdjustment: *DWORD, timeIncrement: *DWORD, timeAdjustmentDisabled: *BOOL) callconv(WINAPI) BOOL; + extern "kernel32" fn GetThreadTimes(in_hProcess: HANDLE, creationTime: *FILETIME, exitTime: *FILETIME, kernelTime: *FILETIME, userTime: *FILETIME) callconv(WINAPI) BOOL; + extern "kernel32" fn GetFileInformationByHandle(file: HANDLE, fileInformation: *BY_HANDLE_FILE_INFORMATION) callconv(WINAPI) BOOL; + extern "kernel32" fn CreateSymbolicLinkW(symlinkFileName: LPCWSTR, lpTargetFileName: LPCWSTR, flags: DWORD) callconv(WINAPI) BOOL; + extern "kernel32" fn SetEndOfFile(file: HANDLE) callconv(WINAPI) BOOL; + + const GetCurrentProcess = std.os.windows.kernel32.GetCurrentProcess; +}; + +const FD_OS_INVALID = switch (builtin.os.tag) { + .windows => std.os.windows.INVALID_HANDLE_VALUE, + else => -1, +}; + +const Helpers = struct { + fn signedCast(comptime T: type, value: anytype, errno: *Errno) T { + if (value >= 0) { + return @as(T, @intCast(value)); + } + errno.* = Errno.INVAL; + return 0; + } + + fn resolvePath(fd_info: *const WasiContext.FdInfo, path_relative: []const u8, path_buffer: []u8, _: *Errno) ?[]const u8 { + var fba = std.heap.FixedBufferAllocator.init(path_buffer[std.fs.MAX_PATH_BYTES..]); + const allocator = fba.allocator(); + + const paths = [_][]const u8{ fd_info.path_absolute, path_relative }; + const resolved_path = std.fs.path.resolve(allocator, &paths) catch unreachable; + return resolved_path; + } + + fn getMemorySlice(module: *ModuleInstance, offset: usize, length: usize, errno: *Errno) ?[]u8 { + const mem: []u8 = module.memorySlice(offset, length); + if (mem.len != length) { + errno.* = Errno.FAULT; + return null; + } + return mem; + } + + fn writeIntToMemory(comptime T: type, value: T, offset: usize, module: *ModuleInstance, errno: *Errno) void { + if (module.memoryWriteInt(T, value, offset) == false) { + errno.* = Errno.FAULT; + } + } + + fn writeFilestatToMemory(stat: *const std.os.wasi.filestat_t, offset: u32, module: *ModuleInstance, errno: *Errno) void { + const filetype = @intFromEnum(stat.filetype); + Helpers.writeIntToMemory(u64, stat.dev, offset + 0, module, errno); + Helpers.writeIntToMemory(u64, stat.ino, offset + 8, module, errno); + Helpers.writeIntToMemory(u8, filetype, offset + 16, module, errno); + Helpers.writeIntToMemory(u64, stat.nlink, offset + 24, module, errno); + Helpers.writeIntToMemory(u64, stat.size, offset + 32, module, errno); + Helpers.writeIntToMemory(u64, stat.atim, offset + 40, module, errno); + Helpers.writeIntToMemory(u64, stat.mtim, offset + 48, module, errno); + Helpers.writeIntToMemory(u64, stat.ctim, offset + 56, module, errno); + } + + fn isStdioHandle(fd_wasi: u32) bool { + return fd_wasi < 3; // std handles are 0, 1, 2 (stdin, stdout, stderr) + } + + fn stringsSizesGet(module: *ModuleInstance, strings: [][]const u8, params: [*]const Val, returns: [*]Val) void { + const strings_count: u32 = @as(u32, @intCast(strings.len)); + var strings_length: u32 = 0; + for (strings) |string| { + strings_length += @as(u32, @intCast(string.len)) + 1; // +1 for required null terminator of each string + } + + var errno = Errno.SUCCESS; + + const dest_string_count = Helpers.signedCast(u32, params[0].I32, &errno); + const dest_string_length = Helpers.signedCast(u32, params[1].I32, &errno); + + if (errno == .SUCCESS) { + writeIntToMemory(u32, strings_count, dest_string_count, module, &errno); + writeIntToMemory(u32, strings_length, dest_string_length, module, &errno); + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; + } + + fn stringsGet(module: *ModuleInstance, strings: [][]const u8, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const dest_string_ptrs_begin = Helpers.signedCast(u32, params[0].I32, &errno); + const dest_string_mem_begin = Helpers.signedCast(u32, params[1].I32, &errno); + + if (errno == .SUCCESS) { + var dest_string_ptrs: u32 = dest_string_ptrs_begin; + var dest_string_strings: u32 = dest_string_mem_begin; + + for (strings) |string| { + writeIntToMemory(u32, dest_string_strings, dest_string_ptrs, module, &errno); + + if (getMemorySlice(module, dest_string_strings, string.len + 1, &errno)) |mem| { + std.mem.copy(u8, mem[0..string.len], string); + mem[string.len] = 0; // null terminator + + dest_string_ptrs += @sizeOf(u32); + dest_string_strings += @as(u32, @intCast(string.len + 1)); + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; + } + + fn convertClockId(wasi_clockid: i32, errno: *Errno) i32 { + return switch (wasi_clockid) { + std.os.wasi.CLOCK.REALTIME => if (builtin.os.tag != .windows) std.os.system.CLOCK.REALTIME else WindowsApi.CLOCK.REALTIME, + std.os.wasi.CLOCK.MONOTONIC => if (builtin.os.tag != .windows) std.os.system.CLOCK.MONOTONIC else WindowsApi.CLOCK.MONOTONIC, + std.os.wasi.CLOCK.PROCESS_CPUTIME_ID => if (builtin.os.tag != .windows) std.os.system.CLOCK.PROCESS_CPUTIME_ID else WindowsApi.CLOCK.PROCESS_CPUTIME_ID, + std.os.wasi.CLOCK.THREAD_CPUTIME_ID => if (builtin.os.tag != .windows) std.os.system.CLOCK.THREAD_CPUTIME_ID else WindowsApi.CLOCK.THREAD_CPUTIME_ID, + else => { + errno.* = Errno.INVAL; + return 0; + }, + }; + } + + fn posixTimespecToWasi(ts: std.os.system.timespec) std.os.wasi.timestamp_t { + const ns_per_second = 1000000000; + const sec_part = @as(u64, @intCast(ts.tv_sec)); + const nsec_part = @as(u64, @intCast(ts.tv_nsec)); + const timestamp_ns: u64 = (sec_part * ns_per_second) + nsec_part; + return timestamp_ns; + } + + fn filetimeToU64(ft: std.os.windows.FILETIME) u64 { + const v: u64 = (@as(u64, @intCast(ft.dwHighDateTime)) << 32) | ft.dwLowDateTime; + return v; + } + + fn windowsFiletimeToWasi(ft: std.os.windows.FILETIME) std.os.wasi.timestamp_t { + // Windows epoch starts on Jan 1, 1601. Unix epoch starts on Jan 1, 1970. + const win_epoch_to_unix_epoch_100ns: u64 = 116444736000000000; + const timestamp_windows_100ns: u64 = Helpers.filetimeToU64(ft); + + const timestamp_100ns: u64 = timestamp_windows_100ns - win_epoch_to_unix_epoch_100ns; + const timestamp_ns: u64 = timestamp_100ns * 100; + return timestamp_ns; + } + + fn decodeLookupFlags(value: i32) WasiLookupFlags { + return WasiLookupFlags{ + .symlink_follow = (value & 0x01) != 0, + }; + } + + fn decodeOpenFlags(value: i32) WasiOpenFlags { + return WasiOpenFlags{ + .creat = (value & 0x01) != 0, + .directory = (value & 0x02) != 0, + .excl = (value & 0x04) != 0, + .trunc = (value & 0x08) != 0, + }; + } + + fn decodeRights(value: i64) WasiRights { + return WasiRights{ + .fd_datasync = (value & 0x0001) != 0, + .fd_read = (value & 0x0002) != 0, + .fd_seek = (value & 0x0004) != 0, + .fd_fdstat_set_flags = (value & 0x0008) != 0, + + .fd_sync = (value & 0x0010) != 0, + .fd_tell = (value & 0x0020) != 0, + .fd_write = (value & 0x0040) != 0, + .fd_advise = (value & 0x0080) != 0, + + .fd_allocate = (value & 0x0100) != 0, + .path_create_directory = (value & 0x0200) != 0, + .path_create_file = (value & 0x0400) != 0, + .path_link_source = (value & 0x0800) != 0, + + .path_link_target = (value & 0x1000) != 0, + .path_open = (value & 0x2000) != 0, + .fd_readdir = (value & 0x4000) != 0, + .path_readlink = (value & 0x8000) != 0, + + .path_rename_source = (value & 0x10000) != 0, + .path_rename_target = (value & 0x20000) != 0, + .path_filestat_get = (value & 0x40000) != 0, + .path_filestat_set_size = (value & 0x80000) != 0, + + .path_filestat_set_times = (value & 0x100000) != 0, + .fd_filestat_get = (value & 0x200000) != 0, + .fd_filestat_set_size = (value & 0x400000) != 0, + .fd_filestat_set_times = (value & 0x800000) != 0, + + .path_symlink = (value & 0x1000000) != 0, + .path_remove_directory = (value & 0x2000000) != 0, + .path_unlink_file = (value & 0x4000000) != 0, + .poll_fd_readwrite = (value & 0x8000000) != 0, + + .sock_shutdown = (value & 0x10000000) != 0, + .sock_accept = (value & 0x20000000) != 0, + }; + } + + fn decodeFdFlags(value: i32) WasiFdFlags { + return WasiFdFlags{ + .append = (value & 0x01) != 0, + .dsync = (value & 0x02) != 0, + .nonblock = (value & 0x04) != 0, + .rsync = (value & 0x08) != 0, + .sync = (value & 0x10) != 0, + }; + } + + fn fdflagsToFlagsPosix(fdflags: WasiFdFlags) u32 { + var flags: u32 = 0; + + if (fdflags.append) { + flags |= std.os.O.APPEND; + } + if (fdflags.dsync) { + flags |= std.os.O.DSYNC; + } + if (fdflags.nonblock) { + flags |= std.os.O.NONBLOCK; + } + if (builtin.os.tag != .macos and fdflags.rsync) { + flags |= std.os.O.RSYNC; + } + if (fdflags.sync) { + flags |= std.os.O.SYNC; + } + + return flags; + } + + fn windowsFileAttributeToWasiFiletype(fileAttributes: WindowsApi.DWORD) std.os.wasi.filetype_t { + if (fileAttributes & std.os.windows.FILE_ATTRIBUTE_DIRECTORY != 0) { + return .DIRECTORY; + } else if (fileAttributes & std.os.windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { + return .SYMBOLIC_LINK; + } else { + return .REGULAR_FILE; + } + } + + fn posixModeToWasiFiletype(mode: std.os.mode_t) std.os.wasi.filetype_t { + if (std.os.S.ISREG(mode)) { + return .REGULAR_FILE; + } else if (std.os.S.ISDIR(mode)) { + return .DIRECTORY; + } else if (std.os.S.ISCHR(mode)) { + return .CHARACTER_DEVICE; + } else if (std.os.S.ISBLK(mode)) { + return .BLOCK_DEVICE; + } else if (std.os.S.ISLNK(mode)) { + return .SYMBOLIC_LINK; + // } else if (std.os.S.ISSOCK(mode)) { + // stat_wasi.fs_filetype = std.os.wasi.filetype_t.SOCKET_STREAM; // not sure if this is SOCKET_STREAM or SOCKET_DGRAM + // } + } else { + return .UNKNOWN; + } + } + + fn fdstatGetWindows(fd: std.os.fd_t, errno: *Errno) std.os.wasi.fdstat_t { + if (builtin.os.tag != .windows) { + @compileError("This function should only be called on the Windows OS."); + } + + var stat_wasi = std.os.wasi.fdstat_t{ + .fs_filetype = std.os.wasi.filetype_t.REGULAR_FILE, + .fs_flags = 0, + .fs_rights_base = std.os.wasi.RIGHT.ALL, + .fs_rights_inheriting = std.os.wasi.RIGHT.ALL, + }; + + var info: WindowsApi.BY_HANDLE_FILE_INFORMATION = undefined; + if (WindowsApi.GetFileInformationByHandle(fd, &info) == std.os.windows.TRUE) { + stat_wasi.fs_filetype = windowsFileAttributeToWasiFiletype(info.dwFileAttributes); + + if (stat_wasi.fs_filetype == .DIRECTORY) { + stat_wasi.fs_rights_base &= ~std.os.wasi.RIGHT.FD_SEEK; + } + + if (info.dwFileAttributes & std.os.windows.FILE_ATTRIBUTE_READONLY != 0) { + stat_wasi.fs_rights_base &= ~std.os.wasi.RIGHT.FD_WRITE; + } + } else { + errno.* = Errno.getLastWin32Error(); + } + + stat_wasi.fs_rights_inheriting = stat_wasi.fs_rights_base; + + return stat_wasi; + } + + fn fdstatGetPosix(fd: std.os.fd_t, errno: *Errno) std.os.wasi.fdstat_t { + if (builtin.os.tag == .windows) { + @compileError("This function should only be called on an OS that supports posix APIs."); + } + + var stat_wasi = std.os.wasi.fdstat_t{ + .fs_filetype = std.os.wasi.filetype_t.UNKNOWN, + .fs_flags = 0, + .fs_rights_base = std.os.wasi.RIGHT.ALL, + .fs_rights_inheriting = std.os.wasi.RIGHT.ALL, + }; + + if (std.os.fcntl(fd, std.os.F.GETFL, 0)) |fd_flags| { + if (std.os.fstat(fd)) |fd_stat| { + + // filetype + stat_wasi.fs_filetype = posixModeToWasiFiletype(fd_stat.mode); + + // flags + if (fd_flags & std.os.O.APPEND != 0) { + stat_wasi.fs_flags |= std.os.wasi.FDFLAG.APPEND; + } + if (fd_flags & std.os.O.DSYNC != 0) { + stat_wasi.fs_flags |= std.os.wasi.FDFLAG.DSYNC; + } + if (fd_flags & std.os.O.NONBLOCK != 0) { + stat_wasi.fs_flags |= std.os.wasi.FDFLAG.NONBLOCK; + } + if (builtin.os.tag != .macos and fd_flags & std.os.O.RSYNC != 0) { + stat_wasi.fs_flags |= std.os.wasi.FDFLAG.RSYNC; + } + if (fd_flags & std.os.O.SYNC != 0) { + stat_wasi.fs_flags |= std.os.wasi.FDFLAG.SYNC; + } + + // rights + if (fd_flags & std.os.O.RDWR != 0) { + // noop since all rights includes this by default + } else if (fd_flags & std.os.O.RDONLY != 0) { + stat_wasi.fs_rights_base &= ~std.os.wasi.RIGHT.FD_WRITE; + } else if (fd_flags & std.os.O.WRONLY != 0) { + stat_wasi.fs_rights_base &= ~std.os.wasi.RIGHT.FD_READ; + } + + if (stat_wasi.fs_filetype == .DIRECTORY) { + stat_wasi.fs_rights_base &= ~std.os.wasi.RIGHT.FD_SEEK; + } + } else |err| { + errno.* = Errno.translateError(err); + } + } else |err| { + errno.* = Errno.translateError(err); + } + + return stat_wasi; + } + + fn fdstatSetFlagsWindows(fd_info: *const WasiContext.FdInfo, fdflags: WasiFdFlags, errno: *Errno) ?std.os.fd_t { + const w = std.os.windows; + + const file_pos = w.SetFilePointerEx_CURRENT_get(fd_info.fd) catch |err| { + errno.* = Errno.translateError(err); + return null; + }; + + w.CloseHandle(fd_info.fd); + + const pathspace_w: w.PathSpace = w.sliceToPrefixedFileW(fd_info.path_absolute) catch |err| { + errno.* = Errno.translateError(err); + return null; + }; + const path_w: [:0]const u16 = pathspace_w.span(); + + var access_mask: w.ULONG = w.READ_CONTROL | w.FILE_WRITE_ATTRIBUTES | w.SYNCHRONIZE; + const write_flags: w.ULONG = if (fdflags.append) w.FILE_APPEND_DATA else w.GENERIC_WRITE; + if (fd_info.rights.fd_read and fd_info.rights.fd_write) { + access_mask |= w.GENERIC_READ | write_flags; + } else if (fd_info.rights.fd_write) { + access_mask |= write_flags; + } else { + access_mask |= w.GENERIC_READ | write_flags; + } + + const path_len_bytes = @as(u16, @intCast(path_w.len * 2)); + var unicode_str = w.UNICODE_STRING{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @as([*]u16, @ptrFromInt(@intFromPtr(path_w.ptr))), + }; + var attr = w.OBJECT_ATTRIBUTES{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = null, + .Attributes = w.OBJ_CASE_INSENSITIVE, + .ObjectName = &unicode_str, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + + var fd_new: w.HANDLE = undefined; + var io: w.IO_STATUS_BLOCK = undefined; + const rc = w.ntdll.NtCreateFile( + &fd_new, + access_mask, + &attr, + &io, + null, + w.FILE_ATTRIBUTE_NORMAL, + w.FILE_SHARE_WRITE | w.FILE_SHARE_READ | w.FILE_SHARE_DELETE, + w.FILE_OPEN, + w.FILE_NON_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT, + null, + 0, + ); + + switch (rc) { + .SUCCESS => {}, + .OBJECT_NAME_INVALID => unreachable, + .INVALID_PARAMETER => unreachable, + .OBJECT_PATH_SYNTAX_BAD => unreachable, + .INVALID_HANDLE => unreachable, + .OBJECT_NAME_NOT_FOUND => errno.* = Errno.NOENT, + .OBJECT_PATH_NOT_FOUND => errno.* = Errno.NOENT, + .NO_MEDIA_IN_DEVICE => errno.* = Errno.NODEV, + .SHARING_VIOLATION => errno.* = Errno.ACCES, + .ACCESS_DENIED => errno.* = Errno.ACCES, + .USER_MAPPED_FILE => errno.* = Errno.ACCES, + .PIPE_BUSY => errno.* = Errno.BUSY, + .OBJECT_NAME_COLLISION => errno.* = Errno.EXIST, + .FILE_IS_A_DIRECTORY => errno.* = Errno.ISDIR, + .NOT_A_DIRECTORY => errno.* = Errno.NOTDIR, + else => errno.* = Errno.INVAL, + } + + if (errno.* != Errno.SUCCESS) { + return null; + } + + // at this point we need to return fd_new, but don't want to silently swallow errors + w.SetFilePointerEx_BEGIN(fd_new, file_pos) catch |err| { + errno.* = Errno.translateError(err); + }; + + return fd_new; + } + + fn fdstatSetFlagsPosix(fd_info: *const WasiContext.FdInfo, fdflags: WasiFdFlags, errno: *Errno) ?std.os.fd_t { + const flags: u32 = fdflagsToFlagsPosix(fdflags); + + if (std.os.fcntl(fd_info.fd, std.os.F.SETFL, flags)) |_| {} else |err| { + errno.* = Errno.translateError(err); + } + + // don't need to update the fd on posix platforms, so just return null + return null; + } + + fn fdFilestatSetTimesWindows(fd: std.os.fd_t, timestamp_wasi_access: u64, timestamp_wasi_modified: u64, fstflags: u32, errno: *Errno) void { + var filetime_now: WindowsApi.FILETIME = undefined; // helps avoid 2 calls to GetSystemTimeAsFiletime + var filetime_now_needs_set: bool = true; + + var access_time: std.os.windows.FILETIME = undefined; + var access_time_was_set: bool = false; + if (fstflags & std.os.wasi.FILESTAT_SET_ATIM != 0) { + access_time = std.os.windows.nanoSecondsToFileTime(timestamp_wasi_access); + access_time_was_set = true; + } + if (fstflags & std.os.wasi.FILESTAT_SET_ATIM_NOW != 0) { + std.os.windows.kernel32.GetSystemTimeAsFileTime(&filetime_now); + filetime_now_needs_set = false; + access_time = filetime_now; + access_time_was_set = true; + } + + var modify_time: std.os.windows.FILETIME = undefined; + var modify_time_was_set: bool = false; + if (fstflags & std.os.wasi.FILESTAT_SET_MTIM != 0) { + modify_time = std.os.windows.nanoSecondsToFileTime(timestamp_wasi_modified); + modify_time_was_set = true; + } + if (fstflags & std.os.wasi.FILESTAT_SET_MTIM_NOW != 0) { + if (filetime_now_needs_set) { + std.os.windows.kernel32.GetSystemTimeAsFileTime(&filetime_now); + } + modify_time = filetime_now; + modify_time_was_set = true; + } + + const access_time_ptr: ?*std.os.windows.FILETIME = if (access_time_was_set) &access_time else null; + const modify_time_ptr: ?*std.os.windows.FILETIME = if (modify_time_was_set) &modify_time else null; + + std.os.windows.SetFileTime(fd, null, access_time_ptr, modify_time_ptr) catch |err| { + errno.* = Errno.translateError(err); + }; + } + + fn fdFilestatSetTimesPosix(fd: std.os.fd_t, timestamp_wasi_access: u64, timestamp_wasi_modified: u64, fstflags: u32, errno: *Errno) void { + const is_darwin = builtin.os.tag.isDarwin(); + const UTIME_NOW: i64 = if (is_darwin) @as(i32, -1) else (1 << 30) - 1; + const UTIME_OMIT: i64 = if (is_darwin) @as(i32, -2) else (1 << 30) - 2; + + var times = [2]std.os.timespec{ + .{ // access time + .tv_sec = 0, + .tv_nsec = UTIME_OMIT, + }, + .{ // modification time + .tv_sec = 0, + .tv_nsec = UTIME_OMIT, + }, + }; + + if (fstflags & std.os.wasi.FILESTAT_SET_ATIM != 0) { + var ts: std.os.wasi.timespec = std.os.wasi.timespec.fromTimestamp(timestamp_wasi_access); + times[0].tv_sec = ts.tv_sec; + times[0].tv_nsec = ts.tv_nsec; + } + if (fstflags & std.os.wasi.FILESTAT_SET_ATIM_NOW != 0) { + times[0].tv_nsec = UTIME_NOW; + } + if (fstflags & std.os.wasi.FILESTAT_SET_MTIM != 0) { + var ts: std.os.wasi.timespec = std.os.wasi.timespec.fromTimestamp(timestamp_wasi_modified); + times[1].tv_sec = ts.tv_sec; + times[1].tv_nsec = ts.tv_nsec; + } + if (fstflags & std.os.wasi.FILESTAT_SET_MTIM_NOW != 0) { + times[1].tv_nsec = UTIME_NOW; + } + + std.os.futimens(fd, ×) catch |err| { + errno.* = Errno.translateError(err); + }; + } + + fn partsToU64(high: u64, low: u64) u64 { + return (high << 32) | low; + } + + fn filestatGetWindows(fd: std.os.fd_t, errno: *Errno) std.os.wasi.filestat_t { + if (builtin.os.tag != .windows) { + @compileError("This function should only be called on an OS that supports posix APIs."); + } + + var stat_wasi: std.os.wasi.filestat_t = undefined; + + var info: WindowsApi.BY_HANDLE_FILE_INFORMATION = undefined; + if (WindowsApi.GetFileInformationByHandle(fd, &info) == std.os.windows.TRUE) { + stat_wasi.dev = 0; + stat_wasi.ino = partsToU64(info.nFileIndexHigh, info.nFileIndexLow); + stat_wasi.filetype = windowsFileAttributeToWasiFiletype(info.dwFileAttributes); + stat_wasi.nlink = info.nNumberOfLinks; + stat_wasi.size = partsToU64(info.nFileSizeHigh, info.nFileSizeLow); + stat_wasi.atim = windowsFiletimeToWasi(info.ftLastAccessTime); + stat_wasi.mtim = windowsFiletimeToWasi(info.ftLastWriteTime); + stat_wasi.ctim = windowsFiletimeToWasi(info.ftCreationTime); + } else { + errno.* = Errno.getLastWin32Error(); + } + + return stat_wasi; + } + + fn filestatGetPosix(fd: std.os.fd_t, errno: *Errno) std.os.wasi.filestat_t { + if (builtin.os.tag == .windows) { + @compileError("This function should only be called on an OS that supports posix APIs."); + } + + var stat_wasi: std.os.wasi.filestat_t = undefined; + + if (std.os.fstat(fd)) |stat| { + stat_wasi.dev = if (builtin.os.tag == .macos) @as(u32, @bitCast(stat.dev)) else stat.dev; + stat_wasi.ino = stat.ino; + stat_wasi.filetype = posixModeToWasiFiletype(stat.mode); + stat_wasi.nlink = stat.nlink; + stat_wasi.size = if (std.math.cast(u64, stat.size)) |s| s else 0; + if (builtin.os.tag == .macos) { + stat_wasi.atim = posixTimespecToWasi(stat.atimespec); + stat_wasi.mtim = posixTimespecToWasi(stat.mtimespec); + stat_wasi.ctim = posixTimespecToWasi(stat.ctimespec); + } else { + stat_wasi.atim = posixTimespecToWasi(stat.atim); + stat_wasi.mtim = posixTimespecToWasi(stat.mtim); + stat_wasi.ctim = posixTimespecToWasi(stat.ctim); + } + } else |err| { + errno.* = Errno.translateError(err); + } + + return stat_wasi; + } + + // As of this 0.10.1, the zig stdlib has a bug in std.os.open() that doesn't respect the append flag properly. + // To get this working, we'll just use NtCreateFile directly. + fn openPathWindows(path: []const u8, lookupflags: WasiLookupFlags, openflags: WasiOpenFlags, fdflags: WasiFdFlags, rights: WasiRights, errno: *Errno) ?std.os.fd_t { + if (builtin.os.tag != .windows) { + @compileError("This function should only be called on an OS that supports windows APIs."); + } + + const w = std.os.windows; + + const pathspace_w: w.PathSpace = w.sliceToPrefixedFileW(path) catch |err| { + errno.* = Errno.translateError(err); + return null; + }; + const path_w: [:0]const u16 = pathspace_w.span(); + + var access_mask: w.ULONG = w.READ_CONTROL | w.FILE_WRITE_ATTRIBUTES | w.SYNCHRONIZE; + const write_flags: w.ULONG = if (fdflags.append) w.FILE_APPEND_DATA else w.GENERIC_WRITE; + if (rights.fd_read and rights.fd_write) { + access_mask |= w.GENERIC_READ | write_flags; + } else if (rights.fd_write) { + access_mask |= write_flags; + } else { + access_mask |= w.GENERIC_READ | write_flags; + } + + const creation: w.ULONG = if (openflags.creat) w.FILE_CREATE else w.FILE_OPEN; + + const file_or_dir_flag: w.ULONG = if (openflags.directory) w.FILE_DIRECTORY_FILE else w.FILE_NON_DIRECTORY_FILE; + const io_mode_flag: w.ULONG = w.FILE_SYNCHRONOUS_IO_NONALERT; + const reparse_flags: w.ULONG = if (lookupflags.symlink_follow) 0 else w.FILE_OPEN_REPARSE_POINT; + const flags: w.ULONG = file_or_dir_flag | io_mode_flag | reparse_flags; + + if (path_w.len > std.math.maxInt(u16)) { + errno.* = Errno.NAMETOOLONG; + return null; + } + + const path_len_bytes = @as(u16, @intCast(path_w.len * 2)); + var unicode_str = w.UNICODE_STRING{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @as([*]u16, @ptrFromInt(@intFromPtr(path_w.ptr))), + }; + var attr = w.OBJECT_ATTRIBUTES{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = null, + .Attributes = w.OBJ_CASE_INSENSITIVE, + .ObjectName = &unicode_str, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + + var fd: w.HANDLE = undefined; + var io: w.IO_STATUS_BLOCK = undefined; + const rc = std.os.windows.ntdll.NtCreateFile( + &fd, + access_mask, + &attr, + &io, + null, + w.FILE_ATTRIBUTE_NORMAL, + w.FILE_SHARE_WRITE | w.FILE_SHARE_READ | w.FILE_SHARE_DELETE, + creation, + flags, + null, + 0, + ); + + // emulate the posix behavior on windows + if (lookupflags.symlink_follow == false) { + if (rc != .OBJECT_NAME_INVALID) { + const attributes: w.DWORD = w.GetFileAttributesW(path_w) catch 0; + if (windowsFileAttributeToWasiFiletype(attributes) == .SYMBOLIC_LINK) { + if (openflags.directory) { + errno.* = Errno.NOTDIR; + } else { + errno.* = Errno.LOOP; + } + if (rc == .SUCCESS) { + std.os.close(fd); + } + return null; + } + } + } + + switch (rc) { + .SUCCESS => return fd, + .INVALID_PARAMETER => unreachable, + .OBJECT_PATH_SYNTAX_BAD => unreachable, + .INVALID_HANDLE => unreachable, + .OBJECT_NAME_NOT_FOUND => errno.* = Errno.NOENT, + .OBJECT_NAME_INVALID => errno.* = Errno.NOENT, + .OBJECT_PATH_NOT_FOUND => errno.* = Errno.NOENT, + .NO_MEDIA_IN_DEVICE => errno.* = Errno.NODEV, + .SHARING_VIOLATION => errno.* = Errno.ACCES, + .ACCESS_DENIED => errno.* = Errno.ACCES, + .USER_MAPPED_FILE => errno.* = Errno.ACCES, + .PIPE_BUSY => errno.* = Errno.BUSY, + .OBJECT_NAME_COLLISION => errno.* = Errno.EXIST, + .FILE_IS_A_DIRECTORY => errno.* = Errno.ISDIR, + .NOT_A_DIRECTORY => errno.* = Errno.NOTDIR, + else => { + errno.* = Errno.INVAL; + }, + } + + return null; + } + + fn openPathPosix(path: []const u8, lookupflags: WasiLookupFlags, openflags: WasiOpenFlags, fdflags: WasiFdFlags, rights: WasiRights, errno: *Errno) ?std.os.fd_t { + if (builtin.os.tag == .windows) { + @compileError("This function should only be called on an OS that supports posix APIs."); + } + + var flags: u32 = 0; + if (openflags.creat) { + flags |= std.os.O.CREAT; + } + if (openflags.directory) { + flags |= std.os.O.DIRECTORY; + } + if (openflags.excl) { + flags |= std.os.O.EXCL; + } + if (openflags.trunc) { + flags |= std.os.O.TRUNC; + } + + if (lookupflags.symlink_follow == false) { + flags |= std.os.O.NOFOLLOW; + } + + const fdflags_os = fdflagsToFlagsPosix(fdflags); + flags |= fdflags_os; + + if (rights.fd_read and rights.fd_write) { + if (openflags.directory) { + flags |= std.os.O.RDONLY; + } else { + flags |= std.os.O.RDWR; + } + } else if (rights.fd_read) { + flags |= std.os.O.RDONLY; + } else if (rights.fd_write) { + flags |= std.os.O.WRONLY; + } + + const S = std.os.linux.S; + const mode: std.os.mode_t = S.IRUSR | S.IWUSR | S.IRGRP | S.IWGRP | S.IROTH; + if (std.os.open(path, flags, mode)) |fd| { + return fd; + } else |err| { + errno.* = Errno.translateError(err); + return null; + } + } + + fn enumerateDirEntries(fd_info: *WasiContext.FdInfo, start_cookie: u64, out_buffer: []u8, errno: *Errno) u32 { + comptime std.debug.assert(std.os.wasi.DIRCOOKIE_START == 0); + var restart_scan = (start_cookie == 0); + + if (restart_scan) { + fd_info.dir_entries.clearRetainingCapacity(); + } + + const osFunc = switch (builtin.os.tag) { + .windows => Helpers.enumerateDirEntriesWindows, + .linux => Helpers.enumerateDirEntriesLinux, + else => comptime blk: { + if (builtin.os.tag.isDarwin()) { + break :blk Helpers.enumerateDirEntriesDarwin; + } + unreachable; // TODO add support for this platform + }, + }; + + var file_index = start_cookie; + + var fbs = std.io.fixedBufferStream(out_buffer); + var writer = fbs.writer(); + + while (fbs.pos < fbs.buffer.len and errno.* == .SUCCESS) { + if (file_index < fd_info.dir_entries.items.len) { + for (fd_info.dir_entries.items[file_index..]) |entry| { + const cookie = file_index + 1; + writer.writeIntLittle(u64, cookie) catch break; + writer.writeIntLittle(u64, entry.inode) catch break; + writer.writeIntLittle(u32, signedCast(u32, entry.filename.len, errno)) catch break; + writer.writeIntLittle(u32, @intFromEnum(entry.filetype)) catch break; + _ = writer.write(entry.filename) catch break; + + file_index += 1; + } + } + + // load more entries for the next loop iteration + if (fbs.pos < fbs.buffer.len and errno.* == .SUCCESS) { + if (osFunc(fd_info, restart_scan, errno) == false) { + // no more files or error + break; + } + } + restart_scan = false; + } + + var bytes_written = signedCast(u32, fbs.pos, errno); + return bytes_written; + } + + fn enumerateDirEntriesWindows(fd_info: *WasiContext.FdInfo, restart_scan: bool, errno: *Errno) bool { + comptime std.debug.assert(std.os.wasi.DIRCOOKIE_START == 0); + + const restart_scan_win32: std.os.windows.BOOLEAN = if (restart_scan) std.os.windows.TRUE else std.os.windows.FALSE; + + var file_info_buffer: [1024]u8 align(@alignOf(WindowsApi.FILE_ID_FULL_DIR_INFORMATION)) = undefined; + var io: std.os.windows.IO_STATUS_BLOCK = undefined; + var rc: std.os.windows.NTSTATUS = std.os.windows.ntdll.NtQueryDirectoryFile( + fd_info.fd, + null, + null, + null, + &io, + &file_info_buffer, + file_info_buffer.len, + .FileIdFullDirectoryInformation, + std.os.windows.TRUE, + null, + restart_scan_win32, + ); + switch (rc) { + .SUCCESS => {}, + .NO_MORE_FILES => { + return false; + }, + .BUFFER_OVERFLOW => { + unreachable; + }, + .INVALID_INFO_CLASS => unreachable, + .INVALID_PARAMETER => unreachable, + else => { + unreachable; + }, + } + + if (rc == .SUCCESS) { + const file_info = @as(*WindowsApi.FILE_ID_FULL_DIR_INFORMATION, @ptrCast(&file_info_buffer)); + + const filename_utf16le = @as([*]u16, @ptrCast(&file_info.FileName))[0 .. file_info.FileNameLength / @sizeOf(u16)]; + + var static_path_buffer: [std.fs.MAX_PATH_BYTES * 2]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&static_path_buffer); + var allocator = fba.allocator(); + const filename: []u8 = std.unicode.utf16leToUtf8Alloc(allocator, filename_utf16le) catch unreachable; + + var filetype: std.os.wasi.filetype_t = .REGULAR_FILE; + if (file_info.FileAttributes & std.os.windows.FILE_ATTRIBUTE_DIRECTORY != 0) { + filetype = .DIRECTORY; + } else if (file_info.FileAttributes & std.os.windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { + filetype = .SYMBOLIC_LINK; + } + + var filename_duped = fd_info.dir_entries.allocator.dupe(u8, filename) catch |err| { + errno.* = Errno.translateError(err); + return false; + }; + + fd_info.dir_entries.append(WasiDirEntry{ + .inode = @as(u64, @bitCast(file_info.FileId)), + .filetype = filetype, + .filename = filename_duped, + }) catch |err| { + errno.* = Errno.translateError(err); + return false; + }; + } + + return true; + } + + fn enumerateDirEntriesDarwin(fd_info: *WasiContext.FdInfo, restart_scan: bool, errno: *Errno) bool { + if (restart_scan) { + std.os.lseek_SET(fd_info.fd, 0) catch |err| { + errno.* = Errno.translateError(err); + return false; + }; + } + + const dirent_t = std.c.dirent; + + var dirent_buffer: [1024]u8 align(@alignOf(dirent_t)) = undefined; + var unused_seek: i64 = 0; + const rc = std.os.system.__getdirentries64(fd_info.fd, &dirent_buffer, dirent_buffer.len, &unused_seek); + errno.* = switch (std.c.getErrno(rc)) { + .SUCCESS => .SUCCESS, + .BADF => .BADF, + .FAULT => .FAULT, + .IO => .IO, + .NOTDIR => .NOTDIR, + else => .INVAL, + }; + + if (errno.* != .SUCCESS) { + return false; + } + + if (rc == 0) { + return false; + } + + var buffer_offset: usize = 0; + while (buffer_offset < rc) { + const dirent_entry = @as(*align(1) dirent_t, @ptrCast(dirent_buffer[buffer_offset..])); + buffer_offset += dirent_entry.d_reclen; + + // TODO length should be (d_reclen - 2 - offsetof(dirent64, d_name)) + // const filename: []u8 = std.mem.sliceTo(@ptrCast([*:0]u8, &dirent_entry.d_name), 0); + const filename: []u8 = @as([*]u8, @ptrCast(&dirent_entry.d_name))[0..dirent_entry.d_namlen]; + + const filetype: std.os.wasi.filetype_t = switch (dirent_entry.d_type) { + std.c.DT.UNKNOWN => .UNKNOWN, + std.c.DT.FIFO => .UNKNOWN, + std.c.DT.CHR => .CHARACTER_DEVICE, + std.c.DT.DIR => .DIRECTORY, + std.c.DT.BLK => .BLOCK_DEVICE, + std.c.DT.REG => .REGULAR_FILE, + std.c.DT.LNK => .SYMBOLIC_LINK, + std.c.DT.SOCK => .SOCKET_DGRAM, + std.c.DT.WHT => .UNKNOWN, + else => .UNKNOWN, + }; + + var filename_duped = fd_info.dir_entries.allocator.dupe(u8, filename) catch |err| { + errno.* = Errno.translateError(err); + break; + }; + + fd_info.dir_entries.append(WasiDirEntry{ + .inode = dirent_entry.d_ino, + .filetype = filetype, + .filename = filename_duped, + }) catch |err| { + errno.* = Errno.translateError(err); + break; + }; + } + + return true; + } + + fn enumerateDirEntriesLinux(fd_info: *WasiContext.FdInfo, restart_scan: bool, errno: *Errno) bool { + if (restart_scan) { + std.os.lseek_SET(fd_info.fd, 0) catch |err| { + errno.* = Errno.translateError(err); + return false; + }; + } + + var dirent_buffer: [1024]u8 align(@alignOf(std.os.linux.dirent64)) = undefined; + const rc = std.os.linux.getdents64(fd_info.fd, &dirent_buffer, dirent_buffer.len); + errno.* = switch (std.os.linux.getErrno(rc)) { + .SUCCESS => Errno.SUCCESS, + .BADF => unreachable, // should never happen since this call is wrapped by fdLookup + .FAULT => Errno.FAULT, + .NOTDIR => Errno.NOTDIR, + .NOENT => Errno.NOENT, // can happen if the fd_info.fd directory was deleted + else => Errno.INVAL, + }; + + if (errno.* != .SUCCESS) { + return false; + } + + if (rc == 0) { + return false; + } + + var buffer_offset: usize = 0; + while (buffer_offset < rc) { + const dirent_entry = @as(*align(1) std.os.linux.dirent64, @ptrCast(dirent_buffer[buffer_offset..])); + buffer_offset += dirent_entry.d_reclen; + + // TODO length should be (d_reclen - 2 - offsetof(dirent64, d_name)) + const filename: []u8 = std.mem.sliceTo(@as([*:0]u8, @ptrCast(&dirent_entry.d_name)), 0); + + const filetype: std.os.wasi.filetype_t = switch (dirent_entry.d_type) { + std.os.linux.DT.BLK => .BLOCK_DEVICE, + std.os.linux.DT.CHR => .CHARACTER_DEVICE, + std.os.linux.DT.DIR => .DIRECTORY, + std.os.linux.DT.FIFO => .UNKNOWN, + std.os.linux.DT.LNK => .SYMBOLIC_LINK, + std.os.linux.DT.REG => .REGULAR_FILE, + std.os.linux.DT.SOCK => .SOCKET_DGRAM, // TODO handle SOCKET_DGRAM + else => .UNKNOWN, + }; + + var filename_duped = fd_info.dir_entries.allocator.dupe(u8, filename) catch |err| { + errno.* = Errno.translateError(err); + break; + }; + + fd_info.dir_entries.append(WasiDirEntry{ + .inode = @as(u64, @bitCast(dirent_entry.d_ino)), + .filetype = filetype, + .filename = filename_duped, + }) catch |err| { + errno.* = Errno.translateError(err); + break; + }; + } + + return true; + } + + fn initIovecs(comptime iov_type: type, stack_iov: []iov_type, errno: *Errno, module: *ModuleInstance, iovec_array_begin: u32, iovec_array_count: u32) ?[]iov_type { + if (iovec_array_count < stack_iov.len) { + const iov = stack_iov[0..iovec_array_count]; + const iovec_array_bytes_length = @sizeOf(u32) * 2 * iovec_array_count; + if (getMemorySlice(module, iovec_array_begin, iovec_array_bytes_length, errno)) |iovec_mem| { + var stream = std.io.fixedBufferStream(iovec_mem); + var reader = stream.reader(); + + for (iov) |*iovec| { + const iov_base: u32 = reader.readIntLittle(u32) catch { + errno.* = Errno.INVAL; + return null; + }; + + const iov_len: u32 = reader.readIntLittle(u32) catch { + errno.* = Errno.INVAL; + return null; + }; + + if (getMemorySlice(module, iov_base, iov_len, errno)) |mem| { + iovec.iov_base = mem.ptr; + iovec.iov_len = mem.len; + } + } + + return iov; + } + } else { + errno.* = Errno.TOOBIG; + } + + return null; + } +}; + +fn wasi_proc_exit(_: ?*anyopaque, _: *ModuleInstance, params: [*]const Val, _: [*]Val) void { + const raw_exit_code = params[0].I32; + + if (raw_exit_code >= 0 and raw_exit_code < std.math.maxInt(u8)) { + const exit_code = @as(u8, @intCast(raw_exit_code)); + std.os.exit(exit_code); + } else { + std.os.exit(1); + } +} + +fn wasi_args_sizes_get(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var context = WasiContext.fromUserdata(userdata); + Helpers.stringsSizesGet(module, context.argv, params, returns); +} + +fn wasi_args_get(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var context = WasiContext.fromUserdata(userdata); + Helpers.stringsGet(module, context.argv, params, returns); +} + +fn wasi_environ_sizes_get(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var context = WasiContext.fromUserdata(userdata); + Helpers.stringsSizesGet(module, context.env, params, returns); +} + +fn wasi_environ_get(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var context = WasiContext.fromUserdata(userdata); + Helpers.stringsGet(module, context.env, params, returns); +} + +fn wasi_clock_res_get(_: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const system_clockid: i32 = Helpers.convertClockId(params[0].I32, &errno); + const timestamp_mem_begin = Helpers.signedCast(u32, params[1].I32, &errno); + + if (errno == .SUCCESS) { + var freqency_ns: u64 = 0; + if (builtin.os.tag == .windows) { + // Follow the mingw pattern since clock_getres() isn't linked in libc for windows + if (system_clockid == std.os.wasi.CLOCK.REALTIME or system_clockid == std.os.wasi.CLOCK.MONOTONIC) { + const ns_per_second: u64 = 1000000000; + const tick_frequency: u64 = std.os.windows.QueryPerformanceFrequency(); + freqency_ns = (ns_per_second + (tick_frequency >> 1)) / tick_frequency; + if (freqency_ns < 1) { + freqency_ns = 1; + } + } else { + var timeAdjustment: WindowsApi.DWORD = undefined; + var timeIncrement: WindowsApi.DWORD = undefined; + var timeAdjustmentDisabled: WindowsApi.BOOL = undefined; + if (WindowsApi.GetSystemTimeAdjustment(&timeAdjustment, &timeIncrement, &timeAdjustmentDisabled) == std.os.windows.TRUE) { + freqency_ns = timeIncrement * 100; + } else { + errno = Errno.INVAL; + } + } + } else { + var ts: std.os.system.timespec = undefined; + if (std.os.clock_getres(system_clockid, &ts)) { + freqency_ns = @as(u64, @intCast(ts.tv_nsec)); + } else |_| { + errno = Errno.INVAL; + } + } + + Helpers.writeIntToMemory(u64, freqency_ns, timestamp_mem_begin, module, &errno); + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn wasi_clock_time_get(_: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const system_clockid: i32 = Helpers.convertClockId(params[0].I32, &errno); + //const precision = params[1].I64; // unused + const timestamp_mem_begin = Helpers.signedCast(u32, params[2].I32, &errno); + + if (errno == .SUCCESS) { + const ns_per_second = 1000000000; + var timestamp_ns: u64 = 0; + + if (builtin.os.tag == .windows) { + switch (system_clockid) { + std.os.wasi.CLOCK.REALTIME => { + var ft: WindowsApi.FILETIME = undefined; + std.os.windows.kernel32.GetSystemTimeAsFileTime(&ft); + + timestamp_ns = Helpers.windowsFiletimeToWasi(ft); + }, + std.os.wasi.CLOCK.MONOTONIC => { + const ticks: u64 = std.os.windows.QueryPerformanceCounter(); + const ticks_per_second: u64 = std.os.windows.QueryPerformanceFrequency(); + + // break up into 2 calculations to avoid overflow + const timestamp_secs_part: u64 = ticks / ticks_per_second; + const timestamp_ns_part: u64 = ((ticks % ticks_per_second) * ns_per_second + (ticks_per_second >> 1)) / ticks_per_second; + + timestamp_ns = timestamp_secs_part + timestamp_ns_part; + }, + std.os.wasi.CLOCK.PROCESS_CPUTIME_ID => { + var createTime: WindowsApi.FILETIME = undefined; + var exitTime: WindowsApi.FILETIME = undefined; + var kernelTime: WindowsApi.FILETIME = undefined; + var userTime: WindowsApi.FILETIME = undefined; + if (std.os.windows.kernel32.GetProcessTimes(WindowsApi.GetCurrentProcess(), &createTime, &exitTime, &kernelTime, &userTime) == std.os.windows.TRUE) { + const timestamp_100ns: u64 = Helpers.filetimeToU64(kernelTime) + Helpers.filetimeToU64(userTime); + timestamp_ns = timestamp_100ns * 100; + } else { + errno = Errno.INVAL; + } + }, + std.os.wasi.CLOCK.THREAD_CPUTIME_ID => { + var createTime: WindowsApi.FILETIME = undefined; + var exitTime: WindowsApi.FILETIME = undefined; + var kernelTime: WindowsApi.FILETIME = undefined; + var userTime: WindowsApi.FILETIME = undefined; + if (WindowsApi.GetThreadTimes(WindowsApi.GetCurrentProcess(), &createTime, &exitTime, &kernelTime, &userTime) == std.os.windows.TRUE) { + const timestamp_100ns: u64 = Helpers.filetimeToU64(kernelTime) + Helpers.filetimeToU64(userTime); + timestamp_ns = timestamp_100ns * 100; + } else { + errno = Errno.INVAL; + } + }, + else => unreachable, + } + } else { + var ts: std.os.system.timespec = undefined; + if (std.os.clock_gettime(system_clockid, &ts)) { + timestamp_ns = Helpers.posixTimespecToWasi(ts); + } else |_| { + errno = Errno.INVAL; + } + } + + Helpers.writeIntToMemory(u64, timestamp_ns, timestamp_mem_begin, module, &errno); + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_datasync(userdata: ?*anyopaque, _: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + const context = WasiContext.fromUserdata(userdata); + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + + var errno = Errno.SUCCESS; + + if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + std.os.fdatasync(fd_info.fd) catch |err| { + errno = Errno.translateError(err); + }; + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_fdstat_get(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const context = WasiContext.fromUserdata(userdata); + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const fdstat_mem_offset = Helpers.signedCast(u32, params[1].I32, &errno); + + if (errno == .SUCCESS) { + if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + const fd_os: std.os.fd_t = fd_info.fd; + const stat: std.os.wasi.fdstat_t = if (builtin.os.tag == .windows) Helpers.fdstatGetWindows(fd_os, &errno) else Helpers.fdstatGetPosix(fd_os, &errno); + + if (errno == .SUCCESS) { + Helpers.writeIntToMemory(u8, @intFromEnum(stat.fs_filetype), fdstat_mem_offset + 0, module, &errno); + Helpers.writeIntToMemory(u16, stat.fs_flags, fdstat_mem_offset + 2, module, &errno); + Helpers.writeIntToMemory(u64, stat.fs_rights_base, fdstat_mem_offset + 8, module, &errno); + Helpers.writeIntToMemory(u64, stat.fs_rights_inheriting, fdstat_mem_offset + 16, module, &errno); + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_fdstat_set_flags(userdata: ?*anyopaque, _: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const context = WasiContext.fromUserdata(userdata); + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const fdflags: WasiFdFlags = Helpers.decodeFdFlags(params[1].I32); + + if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + const fdstat_set_flags_func = if (builtin.os.tag == .windows) Helpers.fdstatSetFlagsWindows else Helpers.fdstatSetFlagsPosix; + if (fdstat_set_flags_func(fd_info, fdflags, &errno)) |new_fd| { + context.fdUpdate(fd_wasi, new_fd); + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_prestat_get(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const context = WasiContext.fromUserdata(userdata); + const fd_dir_wasi = @as(u32, @bitCast(params[0].I32)); + const prestat_mem_offset = Helpers.signedCast(u32, params[1].I32, &errno); + + if (errno == .SUCCESS) { + if (context.fdDirPath(fd_dir_wasi, &errno)) |path_source| { + const name_len: u32 = @as(u32, @intCast(path_source.len)); + + Helpers.writeIntToMemory(u32, std.os.wasi.PREOPENTYPE_DIR, prestat_mem_offset + 0, module, &errno); + Helpers.writeIntToMemory(u32, name_len, prestat_mem_offset + @sizeOf(u32), module, &errno); + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_prestat_dir_name(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const context = WasiContext.fromUserdata(userdata); + const fd_dir_wasi = Helpers.signedCast(u32, params[0].I32, &errno); + const path_mem_offset = Helpers.signedCast(u32, params[1].I32, &errno); + const path_mem_length = Helpers.signedCast(u32, params[2].I32, &errno); + + if (errno == .SUCCESS) { + if (context.fdDirPath(fd_dir_wasi, &errno)) |path_source| { + if (Helpers.getMemorySlice(module, path_mem_offset, path_mem_length, &errno)) |path_dest| { + if (path_source.len <= path_dest.len) { + std.mem.copy(u8, path_dest, path_source); + + // add null terminator if there's room + if (path_dest.len > path_source.len) { + path_dest[path_source.len] = 0; + } + } else { + errno = Errno.NAMETOOLONG; + } + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_read(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const iovec_array_begin = Helpers.signedCast(u32, params[1].I32, &errno); + const iovec_array_count = Helpers.signedCast(u32, params[2].I32, &errno); + const bytes_read_out_offset = Helpers.signedCast(u32, params[3].I32, &errno); + + if (errno == .SUCCESS) { + if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + var stack_iov = [_]std.os.iovec{undefined} ** 1024; + if (Helpers.initIovecs(std.os.iovec, &stack_iov, &errno, module, iovec_array_begin, iovec_array_count)) |iov| { + if (std.os.readv(fd_info.fd, iov)) |read_bytes| { + if (read_bytes <= std.math.maxInt(u32)) { + Helpers.writeIntToMemory(u32, @as(u32, @intCast(read_bytes)), bytes_read_out_offset, module, &errno); + } else { + errno = Errno.TOOBIG; + } + } else |err| { + errno = Errno.translateError(err); + } + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_readdir(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const dirent_mem_offset = Helpers.signedCast(u32, params[1].I32, &errno); + const dirent_mem_length = Helpers.signedCast(u32, params[2].I32, &errno); + const cookie = Helpers.signedCast(u64, params[3].I64, &errno); + const bytes_written_out_offset = Helpers.signedCast(u32, params[4].I32, &errno); + + if (errno == .SUCCESS) { + if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + if (Helpers.getMemorySlice(module, dirent_mem_offset, dirent_mem_length, &errno)) |dirent_buffer| { + var bytes_written = Helpers.enumerateDirEntries(fd_info, cookie, dirent_buffer, &errno); + Helpers.writeIntToMemory(u32, bytes_written, bytes_written_out_offset, module, &errno); + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_renumber(userdata: ?*anyopaque, _: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const fd_to_wasi = @as(u32, @bitCast(params[1].I32)); + + context.fdRenumber(fd_wasi, fd_to_wasi, &errno); + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_pread(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const iovec_array_begin = Helpers.signedCast(u32, params[1].I32, &errno); + const iovec_array_count = Helpers.signedCast(u32, params[2].I32, &errno); + const read_offset = @as(u64, @bitCast(params[3].I64)); + const bytes_read_out_offset = Helpers.signedCast(u32, params[4].I32, &errno); + + if (errno == .SUCCESS) { + if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + var stack_iov = [_]std.os.iovec{undefined} ** 1024; + if (Helpers.initIovecs(std.os.iovec, &stack_iov, &errno, module, iovec_array_begin, iovec_array_count)) |iov| { + if (std.os.preadv(fd_info.fd, iov, read_offset)) |read_bytes| { + if (read_bytes <= std.math.maxInt(u32)) { + Helpers.writeIntToMemory(u32, @as(u32, @intCast(read_bytes)), bytes_read_out_offset, module, &errno); + } else { + errno = Errno.TOOBIG; + } + } else |err| { + errno = Errno.translateError(err); + } + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_advise(userdata: ?*anyopaque, _: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const offset: i64 = params[1].I64; + const length: i64 = params[2].I64; + const advice_wasi = @as(u32, @bitCast(params[3].I32)); + + if (Helpers.isStdioHandle(fd_wasi) == false) { + if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + // fadvise isn't available on windows or macos, but fadvise is just an optimization hint, so don't + // return a bad error code + if (builtin.os.tag == .linux) { + const advice: usize = switch (advice_wasi) { + std.os.wasi.ADVICE_NORMAL => std.os.POSIX_FADV.NORMAL, + std.os.wasi.ADVICE_SEQUENTIAL => std.os.POSIX_FADV.SEQUENTIAL, + std.os.wasi.ADVICE_RANDOM => std.os.POSIX_FADV.RANDOM, + std.os.wasi.ADVICE_WILLNEED => std.os.POSIX_FADV.WILLNEED, + std.os.wasi.ADVICE_DONTNEED => std.os.POSIX_FADV.DONTNEED, + std.os.wasi.ADVICE_NOREUSE => std.os.POSIX_FADV.NOREUSE, + else => blk: { + errno = Errno.INVAL; + break :blk 0; + }, + }; + + if (errno == .SUCCESS) { + const ret = @as(std.os.linux.E, @enumFromInt(std.os.system.fadvise(fd_info.fd, offset, length, advice))); + errno = switch (ret) { + .SUCCESS => Errno.SUCCESS, + .SPIPE => Errno.SPIPE, + .INVAL => Errno.INVAL, + .BADF => unreachable, // should never happen since we protect against this in fdLookup + else => unreachable, + }; + } + } + } + } else { + errno = Errno.BADF; + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_allocate(userdata: ?*anyopaque, _: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const offset: i64 = params[1].I64; + const length_relative: i64 = params[2].I64; + + if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + if (builtin.os.tag == .windows) { + const stat: std.os.wasi.filestat_t = Helpers.filestatGetWindows(fd_info.fd, &errno); + if (errno == .SUCCESS) { + if (stat.size < offset + length_relative) { + const length_total: u64 = @as(u64, @intCast(offset + length_relative)); + std.os.windows.SetFilePointerEx_BEGIN(fd_info.fd, length_total) catch |err| { + errno = Errno.translateError(err); + }; + + if (errno == .SUCCESS) { + if (WindowsApi.SetEndOfFile(fd_info.fd) != std.os.windows.TRUE) { + errno = Errno.INVAL; + } + } + } + } + } else if (builtin.os.tag == .linux) { + const mode = 0; + const rc = std.os.linux.fallocate(fd_info.fd, mode, offset, length_relative); + errno = switch (std.os.linux.getErrno(rc)) { + .SUCCESS => Errno.SUCCESS, + .BADF => unreachable, // should never happen since this call is wrapped by fdLookup + .FBIG => Errno.FBIG, + .INTR => Errno.INTR, + .IO => Errno.IO, + .NODEV => Errno.NODEV, + .NOSPC => Errno.NOSPC, + .NOSYS => Errno.NOSYS, + .OPNOTSUPP => Errno.NOTSUP, + .PERM => Errno.PERM, + .SPIPE => Errno.SPIPE, + .TXTBSY => Errno.TXTBSY, + else => Errno.INVAL, + }; + } else if (builtin.os.tag.isDarwin()) { + var stat: std.c.Stat = undefined; + if (std.c.fstat(fd_info.fd, &stat) != -1) { + // fallocate() doesn't truncate the file if the total is less than the actual file length + // so we need to emulate that behavior here + const length_total = @as(u64, @intCast(@as(i128, offset) + length_relative)); + if (stat.size < length_total) { + std.os.ftruncate(fd_info.fd, length_total) catch |err| { + errno = Errno.translateError(err); + }; + } + } + } else { + unreachable; // TODO implement support for this platform + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_close(userdata: ?*anyopaque, _: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + + if (errno == .SUCCESS) { + context.fdClose(fd_wasi, &errno); + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_filestat_get(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const context = WasiContext.fromUserdata(userdata); + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const filestat_out_mem_offset = Helpers.signedCast(u32, params[1].I32, &errno); + + if (errno == .SUCCESS) { + if (Helpers.isStdioHandle(fd_wasi)) { + const zeroes = std.mem.zeroes(std.os.wasi.filestat_t); + Helpers.writeFilestatToMemory(&zeroes, filestat_out_mem_offset, module, &errno); + } else { + if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + const stat: std.os.wasi.filestat_t = if (builtin.os.tag == .windows) Helpers.filestatGetWindows(fd_info.fd, &errno) else Helpers.filestatGetPosix(fd_info.fd, &errno); + if (errno == .SUCCESS) { + Helpers.writeFilestatToMemory(&stat, filestat_out_mem_offset, module, &errno); + } + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_filestat_set_size(userdata: ?*anyopaque, _: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const context = WasiContext.fromUserdata(userdata); + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const size = Helpers.signedCast(u64, params[1].I64, &errno); + + if (errno == .SUCCESS) { + if (Helpers.isStdioHandle(fd_wasi)) { + errno = Errno.BADF; + } else if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + std.os.ftruncate(fd_info.fd, size) catch |err| { + errno = Errno.translateError(err); + }; + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_filestat_set_times(userdata: ?*anyopaque, _: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const context = WasiContext.fromUserdata(userdata); + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const timestamp_wasi_access = Helpers.signedCast(u64, params[1].I64, &errno); + const timestamp_wasi_modified = Helpers.signedCast(u64, params[2].I64, &errno); + const fstflags = @as(u32, @bitCast(params[3].I32)); + + if (errno == .SUCCESS) { + if (fstflags & std.os.wasi.FILESTAT_SET_ATIM != 0 and fstflags & std.os.wasi.FILESTAT_SET_ATIM_NOW != 0) { + errno = Errno.INVAL; + } + + if (fstflags & std.os.wasi.FILESTAT_SET_MTIM != 0 and fstflags & std.os.wasi.FILESTAT_SET_MTIM_NOW != 0) { + errno = Errno.INVAL; + } + } + + if (errno == .SUCCESS) { + if (Helpers.isStdioHandle(fd_wasi)) { + errno = Errno.BADF; + } else if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + const fd_filestat_set_times_func = if (builtin.os.tag == .windows) Helpers.fdFilestatSetTimesWindows else Helpers.fdFilestatSetTimesPosix; + fd_filestat_set_times_func(fd_info.fd, timestamp_wasi_access, timestamp_wasi_modified, fstflags, &errno); + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_seek(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const offset = params[1].I64; + const whence_raw = params[2].I32; + const filepos_out_offset = Helpers.signedCast(u32, params[3].I32, &errno); + + if (errno == .SUCCESS) { + if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + if (fd_info.rights.fd_seek) { + const fd_os: std.os.fd_t = fd_info.fd; + if (Whence.fromInt(whence_raw)) |whence| { + switch (whence) { + .Set => { + if (offset >= 0) { + const offset_unsigned = @as(u64, @intCast(offset)); + std.os.lseek_SET(fd_os, offset_unsigned) catch |err| { + errno = Errno.translateError(err); + }; + } + }, + .Cur => { + std.os.lseek_CUR(fd_os, offset) catch |err| { + errno = Errno.translateError(err); + }; + }, + .End => { + std.os.lseek_END(fd_os, offset) catch |err| { + errno = Errno.translateError(err); + }; + }, + } + + if (std.os.lseek_CUR_get(fd_os)) |filepos| { + Helpers.writeIntToMemory(u64, filepos, filepos_out_offset, module, &errno); + } else |err| { + errno = Errno.translateError(err); + } + } else { + errno = Errno.INVAL; + } + } else { + errno = Errno.ISDIR; + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_tell(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const context = WasiContext.fromUserdata(userdata); + + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const filepos_out_offset = Helpers.signedCast(u32, params[1].I32, &errno); + + if (errno == .SUCCESS) { + if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + if (std.os.lseek_CUR_get(fd_info.fd)) |filepos| { + Helpers.writeIntToMemory(u64, filepos, filepos_out_offset, module, &errno); + } else |err| { + errno = Errno.translateError(err); + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_write(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const iovec_array_begin = Helpers.signedCast(u32, params[1].I32, &errno); + const iovec_array_count = Helpers.signedCast(u32, params[2].I32, &errno); + const bytes_written_out_offset = Helpers.signedCast(u32, params[3].I32, &errno); + + if (errno == .SUCCESS) { + if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + var stack_iov = [_]std.os.iovec_const{undefined} ** 1024; + if (Helpers.initIovecs(std.os.iovec_const, &stack_iov, &errno, module, iovec_array_begin, iovec_array_count)) |iov| { + if (std.os.writev(fd_info.fd, iov)) |written_bytes| { + Helpers.writeIntToMemory(u32, @as(u32, @intCast(written_bytes)), bytes_written_out_offset, module, &errno); + } else |err| { + errno = Errno.translateError(err); + } + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn fd_wasi_pwrite(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + const fd_wasi = @as(u32, @bitCast(params[0].I32)); + const iovec_array_begin = Helpers.signedCast(u32, params[1].I32, &errno); + const iovec_array_count = Helpers.signedCast(u32, params[2].I32, &errno); + const write_offset = Helpers.signedCast(u64, params[3].I64, &errno); + const bytes_written_out_offset = Helpers.signedCast(u32, params[4].I32, &errno); + + if (errno == .SUCCESS) { + if (context.fdLookup(fd_wasi, &errno)) |fd_info| { + var stack_iov = [_]std.os.iovec_const{undefined} ** 1024; + if (Helpers.initIovecs(std.os.iovec_const, &stack_iov, &errno, module, iovec_array_begin, iovec_array_count)) |iov| { + if (std.os.pwritev(fd_info.fd, iov, write_offset)) |written_bytes| { + Helpers.writeIntToMemory(u32, @as(u32, @intCast(written_bytes)), bytes_written_out_offset, module, &errno); + } else |err| { + errno = Errno.translateError(err); + } + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn wasi_path_create_directory(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const context = WasiContext.fromUserdata(userdata); + const fd_dir_wasi = @as(u32, @bitCast(params[0].I32)); + const path_mem_offset: u32 = Helpers.signedCast(u32, params[1].I32, &errno); + const path_mem_length: u32 = Helpers.signedCast(u32, params[2].I32, &errno); + + if (errno == .SUCCESS) { + if (context.fdLookup(fd_dir_wasi, &errno)) |fd_info| { + if (Helpers.getMemorySlice(module, path_mem_offset, path_mem_length, &errno)) |path| { + if (context.hasPathAccess(fd_info, path, &errno)) { + const S = std.os.linux.S; + const mode: std.os.mode_t = if (builtin.os.tag == .windows) undefined else S.IRWXU | S.IRWXG | S.IROTH; + std.os.mkdirat(fd_info.fd, path, mode) catch |err| { + errno = Errno.translateError(err); + }; + } + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn wasi_path_filestat_get(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const context = WasiContext.fromUserdata(userdata); + const fd_dir_wasi = @as(u32, @bitCast(params[0].I32)); + const lookup_flags: WasiLookupFlags = Helpers.decodeLookupFlags(params[1].I32); + const path_mem_offset: u32 = Helpers.signedCast(u32, params[2].I32, &errno); + const path_mem_length: u32 = Helpers.signedCast(u32, params[3].I32, &errno); + const filestat_out_mem_offset = Helpers.signedCast(u32, params[4].I32, &errno); + + if (errno == .SUCCESS) { + if (context.fdLookup(fd_dir_wasi, &errno)) |fd_info| { + if (Helpers.getMemorySlice(module, path_mem_offset, path_mem_length, &errno)) |path| { + if (context.hasPathAccess(fd_info, path, &errno)) { + var flags: u32 = std.os.O.RDONLY; + if (lookup_flags.symlink_follow == false) { + flags |= std.os.O.NOFOLLOW; + } + + const mode: std.os.mode_t = if (builtin.os.tag != .windows) 644 else undefined; + + if (std.os.openat(fd_info.fd, path, flags, mode)) |fd_opened| { + defer std.os.close(fd_opened); + + const stat: std.os.wasi.filestat_t = if (builtin.os.tag == .windows) Helpers.filestatGetWindows(fd_opened, &errno) else Helpers.filestatGetPosix(fd_opened, &errno); + if (errno == .SUCCESS) { + Helpers.writeFilestatToMemory(&stat, filestat_out_mem_offset, module, &errno); + } + } else |err| { + errno = Errno.translateError(err); + } + } + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn wasi_path_open(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + const fd_dir_wasi: u32 = Helpers.signedCast(u32, params[0].I32, &errno); + const lookupflags: WasiLookupFlags = Helpers.decodeLookupFlags(params[1].I32); + const path_mem_offset: u32 = Helpers.signedCast(u32, params[2].I32, &errno); + const path_mem_length: u32 = Helpers.signedCast(u32, params[3].I32, &errno); + const openflags: WasiOpenFlags = Helpers.decodeOpenFlags(params[4].I32); + const rights_base: WasiRights = Helpers.decodeRights(params[5].I64); + // const rights_inheriting: WasiRights = Helpers.decodeRights(params[6].I64); + const fdflags: WasiFdFlags = Helpers.decodeFdFlags(params[7].I32); + const fd_out_mem_offset = Helpers.signedCast(u32, params[8].I32, &errno); + + // use pathCreateDirectory if creating a directory + if (openflags.creat and openflags.directory) { + errno = Errno.INVAL; + } + + if (errno == .SUCCESS) { + if (Helpers.getMemorySlice(module, path_mem_offset, path_mem_length, &errno)) |path| { + if (context.fdLookup(fd_dir_wasi, &errno)) |fd_info| { + if (context.hasPathAccess(fd_info, path, &errno)) { + var rights_sanitized = rights_base; + rights_sanitized.fd_seek = !openflags.directory; // directories don't have seek rights + + const is_preopen = false; + if (context.fdOpen(fd_info, path, lookupflags, openflags, fdflags, rights_sanitized, is_preopen, &errno)) |fd_opened_wasi| { + Helpers.writeIntToMemory(u32, fd_opened_wasi, fd_out_mem_offset, module, &errno); + } + } + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn wasi_path_remove_directory(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + + const fd_dir_wasi = Helpers.signedCast(u32, params[0].I32, &errno); + const path_mem_offset = Helpers.signedCast(u32, params[1].I32, &errno); + const path_mem_length = Helpers.signedCast(u32, params[2].I32, &errno); + + if (errno == .SUCCESS) { + if (Helpers.getMemorySlice(module, path_mem_offset, path_mem_length, &errno)) |path| { + if (context.fdLookup(fd_dir_wasi, &errno)) |fd_info| { + if (context.hasPathAccess(fd_info, path, &errno)) { + var static_path_buffer: [std.fs.MAX_PATH_BYTES * 2]u8 = undefined; + if (Helpers.resolvePath(fd_info, path, &static_path_buffer, &errno)) |resolved_path| { + std.os.unlinkat(FD_OS_INVALID, resolved_path, std.os.AT.REMOVEDIR) catch |err| { + errno = Errno.translateError(err); + }; + + if (errno == .SUCCESS) { + context.fdCleanup(resolved_path); + } + } + } + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn wasi_path_symlink(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + + const link_contents_mem_offset = Helpers.signedCast(u32, params[0].I32, &errno); + const link_contents_mem_length = Helpers.signedCast(u32, params[1].I32, &errno); + const fd_dir_wasi = Helpers.signedCast(u32, params[2].I32, &errno); + const link_path_mem_offset = Helpers.signedCast(u32, params[3].I32, &errno); + const link_path_mem_length = Helpers.signedCast(u32, params[4].I32, &errno); + + if (Helpers.getMemorySlice(module, link_contents_mem_offset, link_contents_mem_length, &errno)) |link_contents| { + if (Helpers.getMemorySlice(module, link_path_mem_offset, link_path_mem_length, &errno)) |link_path| { + if (errno == .SUCCESS) { + if (context.fdLookup(fd_dir_wasi, &errno)) |fd_info| { + if (context.hasPathAccess(fd_info, link_contents, &errno)) { + if (context.hasPathAccess(fd_info, link_path, &errno)) { + if (builtin.os.tag == .windows) { + var static_path_buffer: [std.fs.MAX_PATH_BYTES * 2]u8 = undefined; + if (Helpers.resolvePath(fd_info, link_path, &static_path_buffer, &errno)) |resolved_link_path| { + const w = std.os.windows; + + const link_contents_w: w.PathSpace = w.sliceToPrefixedFileW(link_contents) catch |err| blk: { + errno = Errno.translateError(err); + break :blk undefined; + }; + + const resolved_link_path_w: w.PathSpace = w.sliceToPrefixedFileW(resolved_link_path) catch |err| blk: { + errno = Errno.translateError(err); + break :blk undefined; + }; + + if (errno == .SUCCESS) { + const flags: w.DWORD = w.SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + if (WindowsApi.CreateSymbolicLinkW(resolved_link_path_w.span(), link_contents_w.span(), flags) == w.FALSE) { + errno = Errno.NOTSUP; + } + } + } + } else { + std.os.symlinkat(link_contents, fd_info.fd, link_path) catch |err| { + errno = Errno.translateError(err); + }; + } + } + } + } + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn wasi_path_unlink_file(userdata: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + var context = WasiContext.fromUserdata(userdata); + + const fd_dir_wasi = Helpers.signedCast(u32, params[0].I32, &errno); + const path_mem_offset = Helpers.signedCast(u32, params[1].I32, &errno); + const path_mem_length = Helpers.signedCast(u32, params[2].I32, &errno); + + if (errno == .SUCCESS) { + if (Helpers.getMemorySlice(module, path_mem_offset, path_mem_length, &errno)) |path| { + if (context.fdLookup(fd_dir_wasi, &errno)) |fd_info| { + if (context.hasPathAccess(fd_info, path, &errno)) { + var static_path_buffer: [std.fs.MAX_PATH_BYTES * 2]u8 = undefined; + if (Helpers.resolvePath(fd_info, path, &static_path_buffer, &errno)) |resolved_path| { + std.os.unlinkat(FD_OS_INVALID, resolved_path, 0) catch |err| { + errno = Errno.translateError(err); + }; + + if (errno == .SUCCESS) { + context.fdCleanup(resolved_path); + } + } + } + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +fn wasi_random_get(_: ?*anyopaque, module: *ModuleInstance, params: [*]const Val, returns: [*]Val) void { + var errno = Errno.SUCCESS; + + const array_begin_offset: u32 = Helpers.signedCast(u32, params[0].I32, &errno); + const array_length: u32 = Helpers.signedCast(u32, params[1].I32, &errno); + + if (errno == .SUCCESS) { + if (array_length > 0) { + if (Helpers.getMemorySlice(module, array_begin_offset, array_length, &errno)) |mem| { + std.crypto.random.bytes(mem); + } + } + } + + returns[0] = Val{ .I32 = @intFromEnum(errno) }; +} + +pub const WasiOpts = struct { + argv: ?[][]const u8 = null, + env: ?[][]const u8 = null, + dirs: ?[][]const u8 = null, +}; + +pub fn initImports(opts: WasiOpts, allocator: std.mem.Allocator) !ModuleImportPackage { + var context: *WasiContext = try allocator.create(WasiContext); + errdefer allocator.destroy(context); + context.* = try WasiContext.init(&opts, allocator); + errdefer context.deinit(); + + var imports: ModuleImportPackage = try ModuleImportPackage.init("wasi_snapshot_preview1", null, context, allocator); + + const void_returns = &[0]ValType{}; + + try imports.addHostFunction("args_get", &[_]ValType{ .I32, .I32 }, &[_]ValType{.I32}, wasi_args_get, context); + try imports.addHostFunction("args_sizes_get", &[_]ValType{ .I32, .I32 }, &[_]ValType{.I32}, wasi_args_sizes_get, context); + try imports.addHostFunction("clock_res_get", &[_]ValType{ .I32, .I32 }, &[_]ValType{.I32}, wasi_clock_res_get, context); + try imports.addHostFunction("clock_time_get", &[_]ValType{ .I32, .I64, .I32 }, &[_]ValType{.I32}, wasi_clock_time_get, context); + try imports.addHostFunction("environ_get", &[_]ValType{ .I32, .I32 }, &[_]ValType{.I32}, wasi_environ_get, context); + try imports.addHostFunction("environ_sizes_get", &[_]ValType{ .I32, .I32 }, &[_]ValType{.I32}, wasi_environ_sizes_get, context); + try imports.addHostFunction("fd_advise", &[_]ValType{ .I32, .I64, .I64, .I32 }, &[_]ValType{.I32}, fd_wasi_advise, context); + try imports.addHostFunction("fd_allocate", &[_]ValType{ .I32, .I64, .I64 }, &[_]ValType{.I32}, fd_wasi_allocate, context); + try imports.addHostFunction("fd_close", &[_]ValType{.I32}, &[_]ValType{.I32}, fd_wasi_close, context); + try imports.addHostFunction("fd_datasync", &[_]ValType{.I32}, &[_]ValType{.I32}, fd_wasi_datasync, context); + try imports.addHostFunction("fd_fdstat_get", &[_]ValType{ .I32, .I32 }, &[_]ValType{.I32}, fd_wasi_fdstat_get, context); + try imports.addHostFunction("fd_fdstat_set_flags", &[_]ValType{ .I32, .I32 }, &[_]ValType{.I32}, fd_wasi_fdstat_set_flags, context); + try imports.addHostFunction("fd_filestat_get", &[_]ValType{ .I32, .I32 }, &[_]ValType{.I32}, fd_wasi_filestat_get, context); + try imports.addHostFunction("fd_filestat_set_size", &[_]ValType{ .I32, .I64 }, &[_]ValType{.I32}, fd_wasi_filestat_set_size, context); + try imports.addHostFunction("fd_filestat_set_times", &[_]ValType{ .I32, .I64, .I64, .I32 }, &[_]ValType{.I32}, fd_wasi_filestat_set_times, context); + try imports.addHostFunction("fd_pread", &[_]ValType{ .I32, .I32, .I32, .I64, .I32 }, &[_]ValType{.I32}, fd_wasi_pread, context); + try imports.addHostFunction("fd_prestat_dir_name", &[_]ValType{ .I32, .I32, .I32 }, &[_]ValType{.I32}, fd_wasi_prestat_dir_name, context); + try imports.addHostFunction("fd_prestat_get", &[_]ValType{ .I32, .I32 }, &[_]ValType{.I32}, fd_wasi_prestat_get, context); + try imports.addHostFunction("fd_pwrite", &[_]ValType{ .I32, .I32, .I32, .I64, .I32 }, &[_]ValType{.I32}, fd_wasi_pwrite, context); + try imports.addHostFunction("fd_read", &[_]ValType{ .I32, .I32, .I32, .I32 }, &[_]ValType{.I32}, fd_wasi_read, context); + try imports.addHostFunction("fd_readdir", &[_]ValType{ .I32, .I32, .I32, .I64, .I32 }, &[_]ValType{.I32}, fd_wasi_readdir, context); + try imports.addHostFunction("fd_renumber", &[_]ValType{ .I32, .I32 }, &[_]ValType{.I32}, fd_wasi_renumber, context); + try imports.addHostFunction("fd_seek", &[_]ValType{ .I32, .I64, .I32, .I32 }, &[_]ValType{.I32}, fd_wasi_seek, context); + try imports.addHostFunction("fd_tell", &[_]ValType{ .I32, .I32 }, &[_]ValType{.I32}, fd_wasi_tell, context); + try imports.addHostFunction("fd_write", &[_]ValType{ .I32, .I32, .I32, .I32 }, &[_]ValType{.I32}, fd_wasi_write, context); + try imports.addHostFunction("path_create_directory", &[_]ValType{ .I32, .I32, .I32 }, &[_]ValType{.I32}, wasi_path_create_directory, context); + try imports.addHostFunction("path_filestat_get", &[_]ValType{ .I32, .I32, .I32, .I32, .I32 }, &[_]ValType{.I32}, wasi_path_filestat_get, context); + try imports.addHostFunction("path_open", &[_]ValType{ .I32, .I32, .I32, .I32, .I32, .I64, .I64, .I32, .I32 }, &[_]ValType{.I32}, wasi_path_open, context); + try imports.addHostFunction("path_remove_directory", &[_]ValType{ .I32, .I32, .I32 }, &[_]ValType{.I32}, wasi_path_remove_directory, context); + try imports.addHostFunction("path_symlink", &[_]ValType{ .I32, .I32, .I32, .I32, .I32 }, &[_]ValType{.I32}, wasi_path_symlink, context); + try imports.addHostFunction("path_unlink_file", &[_]ValType{ .I32, .I32, .I32 }, &[_]ValType{.I32}, wasi_path_unlink_file, context); + try imports.addHostFunction("proc_exit", &[_]ValType{.I32}, void_returns, wasi_proc_exit, context); + try imports.addHostFunction("random_get", &[_]ValType{ .I32, .I32 }, &[_]ValType{.I32}, wasi_random_get, context); + + return imports; +} + +pub fn deinitImports(imports: *ModuleImportPackage) void { + var context = WasiContext.fromUserdata(imports.userdata); + context.deinit(); + imports.allocator.destroy(context); + + imports.deinit(); +} diff --git a/src/ext/bytebox/src/zig-stable-array/LICENSE b/src/ext/bytebox/src/zig-stable-array/LICENSE new file mode 100644 index 00000000..96c02f5f --- /dev/null +++ b/src/ext/bytebox/src/zig-stable-array/LICENSE @@ -0,0 +1,54 @@ +The license for this software is below: + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +======================================================================= + +Substantial portions of Zig's ArrayList source code were used in making +the implementation of this software. Accordingly, the Zig software license +is: + +The MIT License (Expat) + +Copyright (c) 2015-2022, Zig contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/ext/bytebox/src/zig-stable-array/README.md b/src/ext/bytebox/src/zig-stable-array/README.md new file mode 100644 index 00000000..59f0ab6f --- /dev/null +++ b/src/ext/bytebox/src/zig-stable-array/README.md @@ -0,0 +1,19 @@ +# zig-stable-array +Address-stable array with a max size that allocates directly from virtual memory. Memory is only committed when actually used, and virtual page table mappings are relatively cheap, so you only pay for the memory that you're actually using. Additionally, since all memory remains inplace, and new memory is committed incrementally at the end of the array, there are no additional recopies of data made when the array is enlarged. + +Ideal use cases are for large arrays that potentially grow over time. When reallocating a dynamic array with a high upper bound would be a waste of memory, and depending on dynamic resizing may incur high recopy costs due to the size of the array, consider using this array type. Another good use case is when stable pointers or slices to the array contents are desired; since the memory is never moved, pointers to the contents of the array will not be invalidated when growing. Not recommended for small arrays, since the minimum allocation size is the platform's minimum page size. Also not for use with platforms that don't support virtual memory, such as WASM. + +Typical usage is to specify a large size up-front that the array should not encounter, such as 2GB+. Then use the array as usual. If freeing memory is desired, `shrinkAndFree()` will decommit memory at the end of the array. Total memory usage can be calculated with `calcTotalUsedBytes()`. The interface is very similar to ArrayList, except for the allocator semantics. Since typical heap semantics don't apply to this array, the memory is manually managed using mmap/munmap and VirtualAlloc/VirtualFree on nix and Windows platforms, respectively. + +Usage: +``` +var array = StableArray(u8).init(TEST_VIRTUAL_ALLOC_SIZE); +try array.appendSlice(&[_]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); +assert(array.calcTotalUsedBytes() == mem.page_size); +for (array.items) |v, i| { + assert(v == i); +} +array.shrinkAndFree(5); +assert(array.calcTotalUsedBytes() == mem.page_size); +array.deinit(); +``` diff --git a/src/ext/bytebox/src/zig-stable-array/stable_array.zig b/src/ext/bytebox/src/zig-stable-array/stable_array.zig new file mode 100644 index 00000000..e6d39a77 --- /dev/null +++ b/src/ext/bytebox/src/zig-stable-array/stable_array.zig @@ -0,0 +1,424 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const os = std.os; +const mem = std.mem; +const assert = std.debug.assert; + +const darwin = struct { + extern "c" fn madvise(ptr: [*]align(mem.page_size) u8, length: usize, advice: c_int) c_int; +}; + +pub fn StableArray(comptime T: type) type { + return StableArrayAligned(T, @alignOf(T)); +} + +pub fn StableArrayAligned(comptime T: type, comptime alignment: u29) type { + if (@sizeOf(T) == 0) { + @compileError("StableArray does not support types of size 0. Use ArrayList instead."); + } + + return struct { + const Self = @This(); + + pub const Slice = []align(alignment) T; + pub const VariableSlice = [*]align(alignment) T; + + pub const k_sizeof: usize = if (alignment > @sizeOf(T)) alignment else @sizeOf(T); + + items: Slice, + capacity: usize, + max_virtual_alloc_bytes: usize, + + pub fn init(max_virtual_alloc_bytes: usize) Self { + assert(@mod(max_virtual_alloc_bytes, mem.page_size) == 0); // max_virtual_alloc_bytes must be a multiple of mem.page_size + return Self{ + .items = &[_]T{}, + .capacity = 0, + .max_virtual_alloc_bytes = max_virtual_alloc_bytes, + }; + } + + pub fn initCapacity(max_virtual_alloc_bytes: usize, capacity: usize) !Self { + var self = Self.init(max_virtual_alloc_bytes); + try self.ensureTotalCapacity(capacity); + return self; + } + + pub fn deinit(self: *Self) void { + self.clearAndFree(); + } + + pub fn insert(self: *Self, n: usize, item: T) !void { + try self.ensureUnusedCapacity(1); + self.items.len += 1; + + mem.copyBackwards(T, self.items[n + 1 .. self.items.len], self.items[n .. self.items.len - 1]); + self.items[n] = item; + } + + pub fn insertSlice(self: *Self, i: usize, items: []const T) !void { + try self.ensureUnusedCapacity(items.len); + self.items.len += items.len; + + mem.copyBackwards(T, self.items[i + items.len .. self.items.len], self.items[i .. self.items.len - items.len]); + mem.copy(T, self.items[i .. i + items.len], items); + } + + pub fn replaceRange(self: *Self, start: usize, len: usize, new_items: []const T) !void { + const after_range = start + len; + const range = self.items[start..after_range]; + + if (range.len == new_items.len) + mem.copy(T, range, new_items) + else if (range.len < new_items.len) { + const first = new_items[0..range.len]; + const rest = new_items[range.len..]; + + mem.copy(T, range, first); + try self.insertSlice(after_range, rest); + } else { + mem.copy(T, range, new_items); + const after_subrange = start + new_items.len; + + for (self.items[after_range..], 0..) |item, i| { + self.items[after_subrange..][i] = item; + } + + self.items.len -= len - new_items.len; + } + } + + pub fn append(self: *Self, item: T) !void { + const new_item_ptr = try self.addOne(); + new_item_ptr.* = item; + } + + pub fn appendAssumeCapacity(self: *Self, item: T) void { + const new_item_ptr = self.addOneAssumeCapacity(); + new_item_ptr.* = item; + } + + pub fn appendSlice(self: *Self, items: []const T) !void { + try self.ensureUnusedCapacity(items.len); + self.appendSliceAssumeCapacity(items); + } + + pub fn appendSliceAssumeCapacity(self: *Self, items: []const T) void { + const old_len = self.items.len; + const new_len = old_len + items.len; + assert(new_len <= self.capacity); + self.items.len = new_len; + mem.copy(T, self.items[old_len..], items); + } + + pub fn appendNTimes(self: *Self, value: T, n: usize) !void { + const old_len = self.items.len; + try self.resize(self.items.len + n); + @memset(self.items[old_len..self.items.len], value); + } + + pub fn appendNTimesAssumeCapacity(self: *Self, value: T, n: usize) void { + const new_len = self.items.len + n; + assert(new_len <= self.capacity); + @memset(self.items.ptr[self.items.len..new_len], value); + self.items.len = new_len; + } + + pub const Writer = if (T != u8) + @compileError("The Writer interface is only defined for StableArray(u8) " ++ + "but the given type is StableArray(" ++ @typeName(T) ++ ")") + else + std.io.Writer(*Self, error{OutOfMemory}, appendWrite); + + pub fn writer(self: *Self) Writer { + return .{ .context = self }; + } + + fn appendWrite(self: *Self, m: []const u8) !usize { + try self.appendSlice(m); + return m.len; + } + + pub fn addOne(self: *Self) !*T { + const newlen = self.items.len + 1; + try self.ensureTotalCapacity(newlen); + return self.addOneAssumeCapacity(); + } + + pub fn addOneAssumeCapacity(self: *Self) *T { + assert(self.items.len < self.capacity); + + self.items.len += 1; + return &self.items[self.items.len - 1]; + } + + pub fn addManyAsArray(self: *Self, comptime n: usize) !*[n]T { + const prev_len = self.items.len; + try self.resize(self.items.len + n); + return self.items[prev_len..][0..n]; + } + + pub fn addManyAsArrayAssumeCapacity(self: *Self, comptime n: usize) *[n]T { + assert(self.items.len + n <= self.capacity); + const prev_len = self.items.len; + self.items.len += n; + return self.items[prev_len..][0..n]; + } + + pub fn orderedRemove(self: *Self, i: usize) T { + const newlen = self.items.len - 1; + if (newlen == i) return self.pop(); + + const old_item = self.items[i]; + for (self.items[i..newlen], 0..) |*b, j| b.* = self.items[i + 1 + j]; + self.items[newlen] = undefined; + self.items.len = newlen; + return old_item; + } + + pub fn swapRemove(self: *Self, i: usize) T { + if (self.items.len - 1 == i) return self.pop(); + + const old_item = self.items[i]; + self.items[i] = self.pop(); + return old_item; + } + + pub fn resize(self: *Self, new_len: usize) !void { + try self.ensureTotalCapacity(new_len); + self.items.len = new_len; + } + + pub fn shrinkAndFree(self: *Self, new_len: usize) void { + assert(new_len <= self.items.len); + + const new_capacity_bytes = calcBytesUsedForCapacity(new_len); + const current_capacity_bytes: usize = calcBytesUsedForCapacity(self.capacity); + + if (new_capacity_bytes < current_capacity_bytes) { + const bytes_to_free: usize = current_capacity_bytes - new_capacity_bytes; + + if (builtin.os.tag == .windows) { + const w = os.windows; + const addr: usize = @intFromPtr(self.items.ptr) + new_capacity_bytes; + w.VirtualFree(@as(w.PVOID, @ptrFromInt(addr)), bytes_to_free, w.MEM_DECOMMIT); + } else { + var base_addr: usize = @intFromPtr(self.items.ptr); + var offset_addr: usize = base_addr + new_capacity_bytes; + var addr: [*]align(mem.page_size) u8 = @ptrFromInt(offset_addr); + if (comptime builtin.target.isDarwin()) { + const MADV_DONTNEED = 4; + const err: c_int = darwin.madvise(addr, bytes_to_free, MADV_DONTNEED); + switch (@as(os.darwin.E, @enumFromInt(err))) { + os.E.INVAL => unreachable, + os.E.NOMEM => unreachable, + else => {}, + } + } else { + os.madvise(addr, bytes_to_free, std.c.MADV.DONTNEED) catch unreachable; + } + } + + self.capacity = new_capacity_bytes / k_sizeof; + } + + self.items.len = new_len; + } + + pub fn shrinkRetainingCapacity(self: *Self, new_len: usize) void { + assert(new_len <= self.items.len); + self.items.len = new_len; + } + + pub fn clearRetainingCapacity(self: *Self) void { + self.items.len = 0; + } + + pub fn clearAndFree(self: *Self) void { + if (self.capacity > 0) { + if (builtin.os.tag == .windows) { + const w = os.windows; + w.VirtualFree(@as(*anyopaque, @ptrCast(self.items.ptr)), 0, w.MEM_RELEASE); + } else { + var slice: []align(mem.page_size) const u8 = undefined; + slice.ptr = @alignCast(@as([*]u8, @ptrCast(self.items.ptr))); + slice.len = self.max_virtual_alloc_bytes; + os.munmap(slice); + } + } + + self.capacity = 0; + self.items = &[_]T{}; + } + + pub fn ensureTotalCapacity(self: *Self, new_capacity: usize) !void { + const new_capacity_bytes = calcBytesUsedForCapacity(new_capacity); + const current_capacity_bytes: usize = calcBytesUsedForCapacity(self.capacity); + + if (current_capacity_bytes < new_capacity_bytes) { + if (self.capacity == 0) { + if (builtin.os.tag == .windows) { + const w = os.windows; + const addr: w.PVOID = try w.VirtualAlloc(null, self.max_virtual_alloc_bytes, w.MEM_RESERVE, w.PAGE_READWRITE); + self.items.ptr = @alignCast(@ptrCast(addr)); + self.items.len = 0; + } else { + const prot: u32 = std.c.PROT.READ | std.c.PROT.WRITE; + const map: u32 = std.c.MAP.PRIVATE | std.c.MAP.ANONYMOUS; + const fd: os.fd_t = -1; + const offset: usize = 0; + var slice = try os.mmap(null, self.max_virtual_alloc_bytes, prot, map, fd, offset); + self.items.ptr = @alignCast(@ptrCast(slice.ptr)); + self.items.len = 0; + } + } else if (current_capacity_bytes == self.max_virtual_alloc_bytes) { + // If you hit this, you likely either didn't reserve enough space up-front, or have a leak that is allocating too many elements + return error.OutOfMemory; + } + + if (builtin.os.tag == .windows) { + const w = std.os.windows; + _ = try w.VirtualAlloc(@as(w.PVOID, @ptrCast(self.items.ptr)), new_capacity_bytes, w.MEM_COMMIT, w.PAGE_READWRITE); + } + } + + self.capacity = new_capacity; + } + + pub fn ensureUnusedCapacity(self: *Self, additional_count: usize) !void { + return self.ensureTotalCapacity(self.items.len + additional_count); + } + + pub fn expandToCapacity(self: *Self) void { + self.items.len = self.capacity; + } + + pub fn pop(self: *Self) T { + const val = self.items[self.items.len - 1]; + self.items.len -= 1; + return val; + } + + pub fn popOrNull(self: *Self) ?T { + if (self.items.len == 0) return null; + return self.pop(); + } + + pub fn allocatedSlice(self: Self) Slice { + return self.items.ptr[0..self.capacity]; + } + + // Make sure to update self.items.len if you indend for any writes to this + // to modify the length of the array. + pub fn unusedCapacitySlice(self: Self) Slice { + return self.allocatedSlice()[self.items.len..]; + } + + pub fn calcTotalUsedBytes(self: Self) usize { + return calcBytesUsedForCapacity(self.capacity); + } + + fn calcBytesUsedForCapacity(capacity: usize) usize { + return mem.alignForward(usize, k_sizeof * capacity, mem.page_size); + } + }; +} + +const TEST_VIRTUAL_ALLOC_SIZE = 1024 * 1024 * 2; // 2 MB + +test "init" { + var a = StableArray(u8).init(TEST_VIRTUAL_ALLOC_SIZE); + assert(a.items.len == 0); + assert(a.capacity == 0); + assert(a.max_virtual_alloc_bytes == TEST_VIRTUAL_ALLOC_SIZE); + a.deinit(); + + var b = StableArrayAligned(u8, 16).init(TEST_VIRTUAL_ALLOC_SIZE); + assert(b.items.len == 0); + assert(b.capacity == 0); + assert(b.max_virtual_alloc_bytes == TEST_VIRTUAL_ALLOC_SIZE); + b.deinit(); +} + +test "append" { + var a = StableArray(u8).init(TEST_VIRTUAL_ALLOC_SIZE); + try a.appendSlice(&[_]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + assert(a.calcTotalUsedBytes() == mem.page_size); + for (a.items, 0..) |v, i| { + assert(v == i); + } + a.deinit(); + + var b = StableArrayAligned(u8, mem.page_size).init(TEST_VIRTUAL_ALLOC_SIZE); + try b.appendSlice(&[_]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + assert(b.calcTotalUsedBytes() == mem.page_size * 10); + for (b.items, 0..) |v, i| { + assert(v == i); + } + b.deinit(); +} + +test "shrinkAndFree" { + var a = StableArray(u8).init(TEST_VIRTUAL_ALLOC_SIZE); + try a.appendSlice(&[_]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + a.shrinkAndFree(5); + + assert(a.calcTotalUsedBytes() == mem.page_size); + assert(a.items.len == 5); + for (a.items, 0..) |v, i| { + assert(v == i); + } + a.deinit(); + + var b = StableArrayAligned(u8, mem.page_size).init(TEST_VIRTUAL_ALLOC_SIZE); + try b.appendSlice(&[_]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + b.shrinkAndFree(5); + assert(b.calcTotalUsedBytes() == mem.page_size * 5); + assert(b.items.len == 5); + for (b.items, 0..) |v, i| { + assert(v == i); + } + b.deinit(); + + var c = StableArrayAligned(u8, 2048).init(TEST_VIRTUAL_ALLOC_SIZE); + try c.appendSlice(&[_]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + c.shrinkAndFree(5); + assert(c.calcTotalUsedBytes() == mem.page_size * 3); + assert(c.capacity == 6); + assert(c.items.len == 5); + for (c.items, 0..) |v, i| { + assert(v == i); + } + c.deinit(); +} + +test "resize" { + const max: usize = 1024 * 1024 * 1; + var a = StableArray(u8).init(max); + + var size: usize = 512; + while (size <= max) { + try a.resize(size); + size *= 2; + } +} + +test "out of memory" { + var a = StableArrayAligned(u8, mem.page_size).init(TEST_VIRTUAL_ALLOC_SIZE); + const max_capacity: usize = TEST_VIRTUAL_ALLOC_SIZE / mem.page_size; + try a.appendNTimes(0xFF, max_capacity); + for (a.items) |v| { + assert(v == 0xFF); + } + assert(a.max_virtual_alloc_bytes == a.calcTotalUsedBytes()); + assert(a.capacity == max_capacity); + assert(a.items.len == max_capacity); + + var didCatchError: bool = false; + a.append(0) catch |err| { + didCatchError = true; + assert(err == error.OutOfMemory); + }; + assert(didCatchError == true); + a.deinit(); +} diff --git a/src/ext/bytebox/test/main.zig b/src/ext/bytebox/test/main.zig new file mode 100644 index 00000000..65a57437 --- /dev/null +++ b/src/ext/bytebox/test/main.zig @@ -0,0 +1,1538 @@ +const std = @import("std"); +const bytebox = @import("bytebox"); +const ValType = bytebox.ValType; +const Val = bytebox.Val; +const TaggedVal = bytebox.TaggedVal; +const VmType = bytebox.VmType; +const v128 = bytebox.v128; +const i8x16 = bytebox.i8x16; +const i16x8 = bytebox.i16x8; +const i32x4 = bytebox.i32x4; +const i64x2 = bytebox.i64x2; +const f32x4 = bytebox.f32x4; +const f64x2 = bytebox.f64x2; + +const print = std.debug.print; + +var g_verbose_logging = false; + +fn logVerbose(comptime msg: []const u8, params: anytype) void { + if (g_verbose_logging) { + print(msg, params); + } +} + +const TestSuiteError = error{ + Fail, +}; + +const CommandType = enum { + DecodeModule, + Register, + AssertReturn, + AssertTrap, + AssertMalformed, + AssertInvalid, + AssertUnlinkable, + AssertUninstantiable, +}; + +const ActionType = enum { + Invocation, + Get, +}; + +const Action = struct { + type: ActionType, + module: []const u8, + field: []const u8, + args: std.ArrayList(LaneTypedVal), +}; + +const BadModuleError = struct { + module: []const u8, + expected_error: []const u8, +}; + +const CommandDecodeModule = struct { + module_filename: []const u8, + module_name: []const u8, +}; + +const CommandRegister = struct { + module_filename: []const u8, + module_name: []const u8, + import_name: []const u8, +}; + +const CommandAssertReturn = struct { + action: Action, + expected_returns: ?std.ArrayList(LaneTypedVal), +}; + +const CommandAssertTrap = struct { + action: Action, + expected_error: []const u8, +}; + +const CommandAssertMalformed = struct { + err: BadModuleError, +}; + +const CommandAssertInvalid = struct { + err: BadModuleError, +}; + +const CommandAssertUnlinkable = struct { + err: BadModuleError, +}; + +const CommandAssertUninstantiable = struct { + err: BadModuleError, +}; + +const Command = union(CommandType) { + DecodeModule: CommandDecodeModule, + Register: CommandRegister, + AssertReturn: CommandAssertReturn, + AssertTrap: CommandAssertTrap, + AssertMalformed: CommandAssertMalformed, + AssertInvalid: CommandAssertInvalid, + AssertUnlinkable: CommandAssertUnlinkable, + AssertUninstantiable: CommandAssertUninstantiable, + + fn getCommandName(self: *const Command) []const u8 { + return switch (self.*) { + .DecodeModule => "decode_module", + .Register => "register", + .AssertReturn => "assert_return", + .AssertTrap => "assert_trap", + .AssertMalformed => "assert_malformed", + .AssertInvalid => "assert_invalid", + .AssertUnlinkable => "assert_unlinkable", + .AssertUninstantiable => "assert_uninstantiable", + }; + } + + fn getModuleFilename(self: *const Command) []const u8 { + return switch (self.*) { + .DecodeModule => |c| c.module_filename, + .Register => |c| c.module_filename, + else => return getModuleName(self), + }; + } + + fn getModuleName(self: *const Command) []const u8 { + return switch (self.*) { + .DecodeModule => |c| c.module_name, + .Register => |c| c.module_name, + .AssertReturn => |c| c.action.module, + .AssertTrap => |c| c.action.module, + .AssertMalformed => |c| c.err.module, + .AssertInvalid => |c| c.err.module, + .AssertUnlinkable => |c| c.err.module, + .AssertUninstantiable => |c| c.err.module, + }; + } + + fn deinitAction(action: *Action, allocator: std.mem.Allocator) void { + allocator.free(action.module); + allocator.free(action.field); + action.args.deinit(); + } + + fn deinitBadModuleError(err: *BadModuleError, allocator: std.mem.Allocator) void { + allocator.free(err.module); + allocator.free(err.expected_error); + } + + fn deinit(self: *Command, allocator: std.mem.Allocator) void { + switch (self.*) { + .DecodeModule => |*v| { + allocator.free(v.module_filename); + allocator.free(v.module_name); + }, + .Register => |*v| { + allocator.free(v.module_filename); + allocator.free(v.module_name); + allocator.free(v.import_name); + }, + .AssertReturn => |*v| { + deinitAction(&v.action, allocator); + if (v.expected_returns) |returns| { + returns.deinit(); + } + }, + .AssertTrap => |*v| { + deinitAction(&v.action, allocator); + allocator.free(v.expected_error); + }, + .AssertMalformed => |*v| { + deinitBadModuleError(&v.err, allocator); + }, + .AssertInvalid => |*v| { + deinitBadModuleError(&v.err, allocator); + }, + .AssertUnlinkable => |*v| { + deinitBadModuleError(&v.err, allocator); + }, + .AssertUninstantiable => |*v| { + deinitBadModuleError(&v.err, allocator); + }, + } + } +}; + +fn strcmp(a: []const u8, b: []const u8) bool { + return std.mem.eql(u8, a, b); +} + +fn parseVal(obj: std.json.ObjectMap) !TaggedVal { + const Helpers = struct { + fn parseI8(str: []const u8) !i8 { + return std.fmt.parseInt(i8, str, 10) catch @as(i8, @bitCast(try std.fmt.parseInt(u8, str, 10))); + } + + fn parseI16(str: []const u8) !i16 { + return std.fmt.parseInt(i16, str, 10) catch @as(i16, @bitCast(try std.fmt.parseInt(u16, str, 10))); + } + + fn parseI32(str: []const u8) !i32 { + return std.fmt.parseInt(i32, str, 10) catch @as(i32, @bitCast(try std.fmt.parseInt(u32, str, 10))); + } + + fn parseI64(str: []const u8) !i64 { + return std.fmt.parseInt(i64, str, 10) catch @as(i64, @bitCast(try std.fmt.parseInt(u64, str, 10))); + } + + fn parseF32(str: []const u8) !f32 { + if (std.mem.startsWith(u8, str, "nan:")) { + return std.math.nan_f32; // don't differentiate between arithmetic/canonical nan + } else { + const int = try std.fmt.parseInt(u32, str, 10); + return @as(f32, @bitCast(int)); + } + } + + fn parseF64(str: []const u8) !f64 { + if (std.mem.startsWith(u8, str, "nan:")) { + return std.math.nan_f64; // don't differentiate between arithmetic/canonical nan + } else { + const int = try std.fmt.parseInt(u64, str, 10); + return @as(f64, @bitCast(int)); + } + } + + fn parseValuesIntoVec(comptime T: type, json_strings: []std.json.Value) !v128 { + std.debug.assert(json_strings.len * @sizeOf(T) == @sizeOf(v128)); + const num_items = @sizeOf(v128) / @sizeOf(T); + var parsed_values: [num_items]T = undefined; + + const parse_func = switch (T) { + i8 => parseI8, + i16 => parseI16, + i32 => parseI32, + i64 => parseI64, + f32 => parseF32, + f64 => parseF64, + else => unreachable, + }; + + for (&parsed_values, 0..) |*v, i| { + v.* = try parse_func(json_strings[i].string); + } + + var parsed_bytes = std.mem.sliceAsBytes(&parsed_values); + var bytes: [16]u8 = undefined; + std.mem.copy(u8, &bytes, parsed_bytes); + return std.mem.bytesToValue(v128, &bytes); + } + }; + + const json_type = obj.get("type").?; + const json_value = obj.get("value").?; + + if (strcmp("i32", json_type.string)) { + const int = try Helpers.parseI32(json_value.string); + return TaggedVal{ .type = .I32, .val = Val{ .I32 = int } }; + } else if (strcmp("i64", json_type.string)) { + const int = try Helpers.parseI64(json_value.string); + return TaggedVal{ .type = .I64, .val = Val{ .I64 = int } }; + } else if (strcmp("f32", json_type.string)) { + var float: f32 = try Helpers.parseF32(json_value.string); + return TaggedVal{ .type = .F32, .val = Val{ .F32 = float } }; + } else if (strcmp("f64", json_type.string)) { + var float: f64 = try Helpers.parseF64(json_value.string); + return TaggedVal{ .type = .F64, .val = Val{ .F64 = float } }; + } else if (strcmp("v128", json_type.string)) { + const json_lane_type = obj.get("lane_type").?; + var vec: v128 = undefined; + if (strcmp("i8", json_lane_type.string)) { + vec = try Helpers.parseValuesIntoVec(i8, json_value.array.items); + } else if (strcmp("i16", json_lane_type.string)) { + vec = try Helpers.parseValuesIntoVec(i16, json_value.array.items); + } else if (strcmp("i32", json_lane_type.string)) { + vec = try Helpers.parseValuesIntoVec(i32, json_value.array.items); + } else if (strcmp("i64", json_lane_type.string)) { + vec = try Helpers.parseValuesIntoVec(i64, json_value.array.items); + } else if (strcmp("f32", json_lane_type.string)) { + vec = try Helpers.parseValuesIntoVec(f32, json_value.array.items); + } else if (strcmp("f64", json_lane_type.string)) { + vec = try Helpers.parseValuesIntoVec(f64, json_value.array.items); + } else { + unreachable; + } + + return TaggedVal{ .type = .V128, .val = Val{ .V128 = vec } }; + } else if (strcmp("externref", json_type.string)) { + const val: Val = blk: { + if (strcmp("null", json_value.string)) { + break :blk Val.nullRef(ValType.ExternRef) catch unreachable; + } else { + const int = try std.fmt.parseInt(u32, json_value.string, 10); + break :blk Val{ .ExternRef = int }; + } + }; + return TaggedVal{ .type = .ExternRef, .val = val }; + } else if (strcmp("funcref", json_type.string) and strcmp("null", json_value.string)) { + return TaggedVal{ .type = .FuncRef, .val = Val.nullRef(ValType.FuncRef) catch unreachable }; + } else { + print("Failed to parse value of type '{s}' with value '{s}'\n", .{ json_type.string, json_value.string }); + } + + unreachable; +} + +const V128LaneType = enum(u8) { + I8x16, + I16x8, + I32x4, + I64x2, + F32x4, + F64x2, + + fn fromString(str: []const u8) V128LaneType { + if (strcmp("i8", str)) { + return .I8x16; + } else if (strcmp("i16", str)) { + return .I16x8; + } else if (strcmp("i32", str)) { + return .I32x4; + } else if (strcmp("i64", str)) { + return .I64x2; + } else if (strcmp("f32", str)) { + return .F32x4; + } else if (strcmp("f64", str)) { + return .F64x2; + } + + unreachable; + } +}; + +const LaneTypedVal = struct { + v: TaggedVal, + lane_type: V128LaneType, // only valid when v contains a V128 + + fn toValArrayList(typed: []const LaneTypedVal, allocator: std.mem.Allocator) !std.ArrayList(Val) { + var vals = std.ArrayList(Val).init(allocator); + try vals.ensureTotalCapacityPrecise(typed.len); + for (typed) |v| { + try vals.append(v.v.val); + } + return vals; + } +}; + +fn parseLaneTypedVal(obj: std.json.ObjectMap) !LaneTypedVal { + var v: TaggedVal = try parseVal(obj); + var lane_type = V128LaneType.I8x16; + if (v.type == .V128) { + const json_lane_type = obj.get("lane_type").?; + lane_type = V128LaneType.fromString(json_lane_type.string); + } + + return LaneTypedVal{ + .v = v, + .lane_type = lane_type, + }; +} + +fn isSameError(err: anyerror, err_string: []const u8) bool { + return switch (err) { + bytebox.MalformedError.MalformedMagicSignature => strcmp(err_string, "magic header not detected"), + bytebox.MalformedError.MalformedUnexpectedEnd => strcmp(err_string, "unexpected end") or + strcmp(err_string, "unexpected end of section or function") or + strcmp(err_string, "length out of bounds"), + bytebox.MalformedError.MalformedUnsupportedWasmVersion => strcmp(err_string, "unknown binary version"), + bytebox.MalformedError.MalformedSectionId => strcmp(err_string, "malformed section id"), + bytebox.MalformedError.MalformedTypeSentinel => strcmp(err_string, "integer representation too long") or strcmp(err_string, "integer too large"), + bytebox.MalformedError.MalformedLEB128 => strcmp(err_string, "integer representation too long") or strcmp(err_string, "integer too large"), + bytebox.MalformedError.MalformedMissingZeroByte => strcmp(err_string, "zero byte expected"), + bytebox.MalformedError.MalformedTooManyLocals => strcmp(err_string, "too many locals"), + bytebox.MalformedError.MalformedFunctionCodeSectionMismatch => strcmp(err_string, "function and code section have inconsistent lengths"), + bytebox.MalformedError.MalformedMissingDataCountSection => strcmp(err_string, "data count section required") or strcmp(err_string, "unknown data segment"), + bytebox.MalformedError.MalformedDataCountMismatch => strcmp(err_string, "data count and data section have inconsistent lengths"), + bytebox.MalformedError.MalformedDataType => strcmp(err_string, "integer representation too long") or strcmp(err_string, "integer too large"), + bytebox.MalformedError.MalformedIllegalOpcode => strcmp(err_string, "illegal opcode") or strcmp(err_string, "integer representation too long"), + bytebox.MalformedError.MalformedReferenceType => strcmp(err_string, "malformed reference type"), + bytebox.MalformedError.MalformedSectionSizeMismatch => strcmp(err_string, "section size mismatch") or + strcmp(err_string, "malformed section id") or + strcmp(err_string, "function and code section have inconsistent lengths"), // this one is a bit of a hack to resolve custom.8.wasm + bytebox.MalformedError.MalformedInvalidImport => strcmp(err_string, "malformed import kind"), + bytebox.MalformedError.MalformedLimits => strcmp(err_string, "integer too large") or strcmp(err_string, "integer representation too long"), + bytebox.MalformedError.MalformedMultipleStartSections => strcmp(err_string, "multiple start sections") or + strcmp(err_string, "unexpected content after last section"), + bytebox.MalformedError.MalformedElementType => strcmp(err_string, "integer representation too long") or strcmp(err_string, "integer too large"), + bytebox.MalformedError.MalformedUTF8Encoding => strcmp(err_string, "malformed UTF-8 encoding"), + bytebox.MalformedError.MalformedMutability => strcmp(err_string, "malformed mutability"), + + // ValidationTypeMismatch: result arity handles select.2.wasm which is the exact same binary as select.1.wasm but the test expects a different error :/ + bytebox.ValidationError.ValidationTypeMismatch => strcmp(err_string, "type mismatch") or strcmp(err_string, "invalid result arity"), + bytebox.ValidationError.ValidationTypeMustBeNumeric => strcmp(err_string, "type mismatch"), + bytebox.ValidationError.ValidationUnknownType => strcmp(err_string, "unknown type"), + bytebox.ValidationError.ValidationUnknownFunction => std.mem.startsWith(u8, err_string, "unknown function"), + bytebox.ValidationError.ValidationUnknownGlobal => std.mem.startsWith(u8, err_string, "unknown global"), + bytebox.ValidationError.ValidationUnknownLocal => std.mem.startsWith(u8, err_string, "unknown local"), + bytebox.ValidationError.ValidationUnknownTable => std.mem.startsWith(u8, err_string, "unknown table"), + bytebox.ValidationError.ValidationUnknownMemory => std.mem.startsWith(u8, err_string, "unknown memory"), + bytebox.ValidationError.ValidationUnknownElement => strcmp(err_string, "unknown element") or std.mem.startsWith(u8, err_string, "unknown elem segment"), + bytebox.ValidationError.ValidationUnknownData => strcmp(err_string, "unknown data") or std.mem.startsWith(u8, err_string, "unknown data segment"), + bytebox.ValidationError.ValidationTypeStackHeightMismatch => strcmp(err_string, "type mismatch"), + bytebox.ValidationError.ValidationBadAlignment => strcmp(err_string, "alignment must not be larger than natural"), + bytebox.ValidationError.ValidationUnknownLabel => strcmp(err_string, "unknown label"), + bytebox.ValidationError.ValidationImmutableGlobal => strcmp(err_string, "global is immutable"), + bytebox.ValidationError.ValidationBadConstantExpression => strcmp(err_string, "constant expression required") or strcmp(err_string, "type mismatch"), + bytebox.ValidationError.ValidationGlobalReferencingMutableGlobal => strcmp(err_string, "constant expression required"), + bytebox.ValidationError.ValidationUnknownBlockTypeIndex => strcmp(err_string, "type mismatch") or + strcmp(err_string, "unexpected end"), // bit of a hack for binary.166.wasm + bytebox.ValidationError.ValidationSelectArity => strcmp(err_string, "invalid result arity"), + bytebox.ValidationError.ValidationMultipleMemories => strcmp(err_string, "multiple memories"), + bytebox.ValidationError.ValidationMemoryInvalidMaxLimit => strcmp(err_string, "size minimum must not be greater than maximum"), + bytebox.ValidationError.ValidationMemoryMaxPagesExceeded => strcmp(err_string, "memory size must be at most 65536 pages (4GiB)"), + bytebox.ValidationError.ValidationConstantExpressionGlobalMustBeImport => strcmp(err_string, "unknown global"), + bytebox.ValidationError.ValidationConstantExpressionGlobalMustBeImmutable => strcmp(err_string, "constant expression required"), + bytebox.ValidationError.ValidationStartFunctionType => strcmp(err_string, "start function"), + bytebox.ValidationError.ValidationLimitsMinMustNotBeLargerThanMax => strcmp(err_string, "size minimum must not be greater than maximum"), + bytebox.ValidationError.ValidationConstantExpressionTypeMismatch => strcmp(err_string, "type mismatch") or strcmp(err_string, "constant expression required"), + bytebox.ValidationError.ValidationDuplicateExportName => strcmp(err_string, "duplicate export name"), + bytebox.ValidationError.ValidationFuncRefUndeclared => strcmp(err_string, "undeclared function reference"), + bytebox.ValidationError.ValidationIfElseMismatch => strcmp(err_string, "END opcode expected"), + bytebox.ValidationError.ValidationInvalidLaneIndex => strcmp(err_string, "invalid lane index"), + + bytebox.UnlinkableError.UnlinkableUnknownImport => strcmp(err_string, "unknown import"), + bytebox.UnlinkableError.UnlinkableIncompatibleImportType => strcmp(err_string, "incompatible import type"), + + bytebox.UninstantiableError.UninstantiableOutOfBoundsTableAccess => strcmp(err_string, "out of bounds table access"), + bytebox.UninstantiableError.UninstantiableOutOfBoundsMemoryAccess => strcmp(err_string, "out of bounds memory access"), + + bytebox.TrapError.TrapIntegerDivisionByZero => strcmp(err_string, "integer divide by zero"), + bytebox.TrapError.TrapIntegerOverflow => strcmp(err_string, "integer overflow"), + bytebox.TrapError.TrapIndirectCallTypeMismatch => strcmp(err_string, "indirect call type mismatch"), + bytebox.TrapError.TrapInvalidIntegerConversion => strcmp(err_string, "invalid conversion to integer"), + bytebox.TrapError.TrapOutOfBoundsMemoryAccess => strcmp(err_string, "out of bounds memory access"), + bytebox.TrapError.TrapUndefinedElement => strcmp(err_string, "undefined element"), + bytebox.TrapError.TrapUninitializedElement => std.mem.startsWith(u8, err_string, "uninitialized element"), + bytebox.TrapError.TrapOutOfBoundsTableAccess => strcmp(err_string, "out of bounds table access"), + bytebox.TrapError.TrapStackExhausted => strcmp(err_string, "call stack exhausted"), + bytebox.TrapError.TrapUnreachable => strcmp(err_string, "unreachable"), + + else => false, + }; +} + +fn parseCommands(json_path: []const u8, allocator: std.mem.Allocator) !std.ArrayList(Command) { + const Helpers = struct { + fn parseAction(json_action: *std.json.Value, fallback_module: []const u8, _allocator: std.mem.Allocator) !Action { + const json_type = json_action.object.getPtr("type").?; + var action_type: ActionType = undefined; + if (strcmp("invoke", json_type.string)) { + action_type = .Invocation; + } else if (strcmp("get", json_type.string)) { + action_type = .Get; + } else { + unreachable; + } + + const json_field = json_action.object.getPtr("field").?; + + const json_args_or_null = json_action.object.getPtr("args"); + var args = std.ArrayList(LaneTypedVal).init(_allocator); + if (json_args_or_null) |json_args| { + for (json_args.array.items) |item| { + var val: LaneTypedVal = try parseLaneTypedVal(item.object); + try args.append(val); + } + } + + var module: []const u8 = try _allocator.dupe(u8, fallback_module); + const json_module_or_null = json_action.object.getPtr("module"); + if (json_module_or_null) |json_module| { + module = try _allocator.dupe(u8, json_module.string); + } + + return Action{ + .type = action_type, + .module = module, + .field = try _allocator.dupe(u8, json_field.string), + .args = args, + }; + } + + fn parseBadModuleError(json_command: *const std.json.Value, _allocator: std.mem.Allocator) !BadModuleError { + const json_filename = json_command.object.get("filename").?; + const json_expected = json_command.object.get("text").?; + + return BadModuleError{ + .module = try _allocator.dupe(u8, json_filename.string), + .expected_error = try _allocator.dupe(u8, json_expected.string), + }; + } + }; + + // print("json_path: {s}\n", .{json_path}); + var json_data = try std.fs.cwd().readFileAlloc(allocator, json_path, 1024 * 1024 * 8); + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_data, .{}); + + var fallback_module: []const u8 = ""; + defer allocator.free(fallback_module); + + var commands = std.ArrayList(Command).init(allocator); + + const json_commands = parsed.value.object.getPtr("commands").?; + for (json_commands.array.items) |json_command| { + const json_command_type = json_command.object.getPtr("type").?; + + if (strcmp("module", json_command_type.string)) { + const json_filename = json_command.object.getPtr("filename").?; + var filename: []const u8 = try allocator.dupe(u8, json_filename.string); + fallback_module = filename; + + var name = try allocator.dupe(u8, filename); + if (json_command.object.getPtr("name")) |json_module_name| { + name = try allocator.dupe(u8, json_module_name.string); + } + + var command = Command{ + .DecodeModule = CommandDecodeModule{ + .module_filename = try allocator.dupe(u8, filename), + .module_name = name, + }, + }; + try commands.append(command); + } else if (strcmp("register", json_command_type.string)) { + const json_as = json_command.object.getPtr("as").?; + var json_import_name: []const u8 = json_as.string; + var json_module_name: []const u8 = fallback_module; + if (json_command.object.getPtr("name")) |json_name| { + json_module_name = json_name.string; + } + + // print("json_module_name: {s}, json_import_name: {s}\n", .{ json_module_name, json_import_name }); + + var command = Command{ + .Register = CommandRegister{ + .module_filename = try allocator.dupe(u8, fallback_module), + .module_name = try allocator.dupe(u8, json_module_name), + .import_name = try allocator.dupe(u8, json_import_name), + }, + }; + try commands.append(command); + } else if (strcmp("assert_return", json_command_type.string) or strcmp("action", json_command_type.string)) { + const json_action = json_command.object.getPtr("action").?; + + var action = try Helpers.parseAction(json_action, fallback_module, allocator); + + var expected_returns_or_null: ?std.ArrayList(LaneTypedVal) = null; + const json_expected_or_null = json_command.object.getPtr("expected"); + if (json_expected_or_null) |json_expected| { + var expected_returns = std.ArrayList(LaneTypedVal).init(allocator); + for (json_expected.array.items) |item| { + try expected_returns.append(try parseLaneTypedVal(item.object)); + } + expected_returns_or_null = expected_returns; + } + + var command = Command{ + .AssertReturn = CommandAssertReturn{ + .action = action, + .expected_returns = expected_returns_or_null, + }, + }; + try commands.append(command); + } else if (strcmp("assert_trap", json_command_type.string) or strcmp("assert_exhaustion", json_command_type.string)) { + const json_action = json_command.object.getPtr("action").?; + + var action = try Helpers.parseAction(json_action, fallback_module, allocator); + + const json_text = json_command.object.getPtr("text").?; + + var command = Command{ + .AssertTrap = CommandAssertTrap{ + .action = action, + .expected_error = try allocator.dupe(u8, json_text.string), + }, + }; + try commands.append(command); + } else if (strcmp("assert_malformed", json_command_type.string)) { + var command = Command{ + .AssertMalformed = CommandAssertMalformed{ + .err = try Helpers.parseBadModuleError(&json_command, allocator), + }, + }; + if (std.mem.endsWith(u8, command.AssertMalformed.err.module, ".wasm")) { + try commands.append(command); + } + } else if (strcmp("assert_invalid", json_command_type.string)) { + var command = Command{ + .AssertInvalid = CommandAssertInvalid{ + .err = try Helpers.parseBadModuleError(&json_command, allocator), + }, + }; + try commands.append(command); + } else if (strcmp("assert_unlinkable", json_command_type.string)) { + var command = Command{ + .AssertUnlinkable = CommandAssertUnlinkable{ + .err = try Helpers.parseBadModuleError(&json_command, allocator), + }, + }; + try commands.append(command); + } else if (strcmp("assert_uninstantiable", json_command_type.string)) { + var command = Command{ + .AssertUninstantiable = CommandAssertUninstantiable{ + .err = try Helpers.parseBadModuleError(&json_command, allocator), + }, + }; + try commands.append(command); + } else { + print("unknown command type: {s}\n", .{json_command_type.string}); + unreachable; + } + } + + return commands; +} + +const Module = struct { + filename: []const u8 = "", + def: ?*bytebox.ModuleDefinition = null, + inst: ?*bytebox.ModuleInstance = null, +}; + +const TestOpts = struct { + vm_type: VmType = .Stack, + suite_filter_or_null: ?[]const u8 = null, + test_filter_or_null: ?[]const u8 = null, + command_filter_or_null: ?[]const u8 = null, + module_filter_or_null: ?[]const u8 = null, + force_wasm_regen_only: bool = false, + log_suite: bool = false, +}; + +fn makeSpectestImports(allocator: std.mem.Allocator) !bytebox.ModuleImportPackage { + const Functions = struct { + fn printI32(_: ?*anyopaque, _: *bytebox.ModuleInstance, _: [*]const Val, _: [*]Val) void { + // std.debug.print("{}", .{params[0].I32}); + } + + fn printI64(_: ?*anyopaque, _: *bytebox.ModuleInstance, _: [*]const Val, _: [*]Val) void { + // std.debug.print("{}", .{params[0].I64}); + } + + fn printF32(_: ?*anyopaque, _: *bytebox.ModuleInstance, _: [*]const Val, _: [*]Val) void { + // std.debug.print("{}", .{params[0].F32}); + } + + fn printF64(_: ?*anyopaque, _: *bytebox.ModuleInstance, _: [*]const Val, _: [*]Val) void { + // std.debug.print("{}", .{params[0].F64}); + } + + fn printI32F32(_: ?*anyopaque, _: *bytebox.ModuleInstance, _: [*]const Val, _: [*]Val) void { + // std.debug.print("{} {}", .{ params[0].I32, params[1].F32 }); + } + + fn printF64F64(_: ?*anyopaque, _: *bytebox.ModuleInstance, _: [*]const Val, _: [*]Val) void { + // std.debug.print("{} {}", .{ params[0].F64, params[1].F64 }); + } + + fn print(_: ?*anyopaque, _: *bytebox.ModuleInstance, _: [*]const Val, _: [*]Val) void { + // std.debug.print("\n", .{}); + } + }; + + const Helpers = struct { + fn addGlobal(imports: *bytebox.ModuleImportPackage, _allocator: std.mem.Allocator, mut: bytebox.GlobalMut, comptime T: type, value: T, name: []const u8) !void { + const valtype = switch (T) { + i32 => ValType.I32, + i64 => ValType.I64, + f32 => ValType.F32, + f64 => ValType.F64, + else => unreachable, + }; + const val: Val = switch (T) { + i32 => Val{ .I32 = value }, + i64 => Val{ .I64 = value }, + f32 => Val{ .F32 = value }, + f64 => Val{ .F64 = value }, + else => unreachable, + }; + var global_definition = try _allocator.create(bytebox.GlobalDefinition); + global_definition.* = bytebox.GlobalDefinition{ + .valtype = valtype, + .mut = mut, + .expr = undefined, // unused + }; + var global_instance = try _allocator.create(bytebox.GlobalInstance); + global_instance.* = bytebox.GlobalInstance{ + .def = global_definition, + .value = val, + }; + try imports.globals.append(bytebox.GlobalImport{ + .name = try _allocator.dupe(u8, name), + .data = .{ .Host = global_instance }, + }); + } + }; + var imports: bytebox.ModuleImportPackage = try bytebox.ModuleImportPackage.init("spectest", null, null, allocator); + + const no_returns = &[0]ValType{}; + + try imports.addHostFunction("print_i32", &[_]ValType{.I32}, no_returns, Functions.printI32, null); + try imports.addHostFunction("print_i64", &[_]ValType{.I64}, no_returns, Functions.printI64, null); + try imports.addHostFunction("print_f32", &[_]ValType{.F32}, no_returns, Functions.printF32, null); + try imports.addHostFunction("print_f64", &[_]ValType{.F64}, no_returns, Functions.printF64, null); + try imports.addHostFunction("print_i32_f32", &[_]ValType{ .I32, .F32 }, no_returns, Functions.printI32F32, null); + try imports.addHostFunction("print_f64_f64", &[_]ValType{ .F64, .F64 }, no_returns, Functions.printF64F64, null); + try imports.addHostFunction("print_f64_f64", &[_]ValType{ .F64, .F64 }, no_returns, Functions.printF64F64, null); + try imports.addHostFunction("print", &[_]ValType{}, no_returns, Functions.print, null); + + const TableInstance = bytebox.TableInstance; + + var table = try allocator.create(TableInstance); + table.* = try TableInstance.init(ValType.FuncRef, bytebox.Limits{ .min = 10, .max = 20 }, allocator); + try imports.tables.append(bytebox.TableImport{ + .name = try allocator.dupe(u8, "table"), + .data = .{ .Host = table }, + }); + + const MemoryInstance = bytebox.MemoryInstance; + + var memory = try allocator.create(MemoryInstance); + memory.* = MemoryInstance.init(bytebox.Limits{ + .min = 1, + .max = 2, + }, null); + _ = memory.grow(1); + try imports.memories.append(bytebox.MemoryImport{ + .name = try allocator.dupe(u8, "memory"), + .data = .{ .Host = memory }, + }); + + try Helpers.addGlobal(&imports, allocator, bytebox.GlobalMut.Immutable, i32, 666, "global_i32"); + try Helpers.addGlobal(&imports, allocator, bytebox.GlobalMut.Immutable, i64, 666, "global_i64"); + try Helpers.addGlobal(&imports, allocator, bytebox.GlobalMut.Immutable, f32, 666.6, "global_f32"); + try Helpers.addGlobal(&imports, allocator, bytebox.GlobalMut.Immutable, f64, 666.6, "global_f64"); + try Helpers.addGlobal(&imports, allocator, bytebox.GlobalMut.Immutable, i32, 0, "global-i32"); + try Helpers.addGlobal(&imports, allocator, bytebox.GlobalMut.Immutable, f32, 0, "global-f32"); + try Helpers.addGlobal(&imports, allocator, bytebox.GlobalMut.Mutable, i32, 0, "global-mut-i32"); + try Helpers.addGlobal(&imports, allocator, bytebox.GlobalMut.Mutable, i64, 0, "global-mut-i64"); + + return imports; +} + +fn run(allocator: std.mem.Allocator, suite_path: []const u8, opts: *const TestOpts) !bool { + var did_fail_any_test: bool = false; + + var commands: std.ArrayList(Command) = try parseCommands(suite_path, allocator); + defer { + for (commands.items) |*command| { + command.deinit(allocator); + } + commands.deinit(); + } + + const suite_dir = std.fs.path.dirname(suite_path).?; + + var name_to_module = std.StringHashMap(Module).init(allocator); + defer { + var name_to_module_iter = name_to_module.iterator(); + while (name_to_module_iter.next()) |kv| { + // key memory is owned by commands list, so no need to free + + allocator.free(kv.value_ptr.filename); // ^^^ + if (kv.value_ptr.def) |def| { + def.destroy(); + } + if (kv.value_ptr.inst) |inst| { + inst.destroy(); + } + } + name_to_module.deinit(); + } + + // this should be enough to avoid resizing, just bump it up if it's not + // note that module instance uses the pointer to the stored struct so it's important that the stored instances never move + name_to_module.ensureTotalCapacity(256) catch unreachable; + + // NOTE this shares the same copies of the import arrays, since the modules must share instances + var imports = std.ArrayList(bytebox.ModuleImportPackage).init(allocator); + defer { + var spectest_imports = imports.items[0]; + for (spectest_imports.tables.items) |*item| { + allocator.free(item.name); + item.data.Host.deinit(); + allocator.destroy(item.data.Host); + } + for (spectest_imports.memories.items) |*item| { + allocator.free(item.name); + item.data.Host.deinit(); + allocator.destroy(item.data.Host); + } + for (spectest_imports.globals.items) |*item| { + allocator.free(item.name); + allocator.destroy(item.data.Host.def); + allocator.destroy(item.data.Host); + } + + for (imports.items[1..]) |*item| { + item.deinit(); + } + imports.deinit(); + } + + try imports.append(try makeSpectestImports(allocator)); + + for (commands.items) |*command| { + const module_filename = command.getModuleFilename(); + const module_name = command.getModuleName(); + if (opts.module_filter_or_null) |filter| { + if (strcmp(filter, module_filename) == false) { + continue; + } + } + // std.debug.print("looking for (name/filename) {s}:{s}\n", .{ module_name, module_filename }); + + var entry = name_to_module.getOrPutAssumeCapacity(module_name); + var module: *Module = entry.value_ptr; + if (entry.found_existing == false) { + module.* = Module{}; + } + + switch (command.*) { + .AssertReturn => {}, + .AssertTrap => {}, + else => logVerbose("{s}: {s}|{s}\n", .{ command.getCommandName(), module_name, module_filename }), + } + + switch (command.*) { + .Register => |c| { + if (module.inst == null) { + print( + "Register: module instance {s}|{s} was not found in the cache by the name '{s}'. Is the wast malformed?\n", + .{ c.module_name, module_filename, module_name }, + ); + did_fail_any_test = true; + continue; + } + + logVerbose("\tSetting export module name to {s}\n", .{c.import_name}); + + var module_imports: bytebox.ModuleImportPackage = try (module.inst.?).exports(c.import_name); + try imports.append(module_imports); + continue; + }, + else => {}, + } + + if (module.inst == null) { + var module_path = try std.fs.path.join(allocator, &[_][]const u8{ suite_dir, module_filename }); + + var cwd = std.fs.cwd(); + var module_data = try cwd.readFileAlloc(allocator, module_path, 1024 * 1024 * 8); + + var decode_expected_error: ?[]const u8 = null; + switch (command.*) { + .AssertMalformed => |c| { + decode_expected_error = c.err.expected_error; + }, + else => {}, + } + + var validate_expected_error: ?[]const u8 = null; + switch (command.*) { + .AssertInvalid => |c| { + validate_expected_error = c.err.expected_error; + }, + else => {}, + } + + module.filename = try allocator.dupe(u8, module_filename); + + module.def = try bytebox.createModuleDefinition(allocator, .{ .debug_name = std.fs.path.basename(module_filename) }); + (module.def.?).decode(module_data) catch |e| { + var expected_str_or_null: ?[]const u8 = null; + if (decode_expected_error) |unwrapped_expected| { + expected_str_or_null = unwrapped_expected; + } + if (expected_str_or_null == null) { + if (validate_expected_error) |unwrapped_expected| { + expected_str_or_null = unwrapped_expected; + } + } + + if (expected_str_or_null) |expected_str| { + if (isSameError(e, expected_str)) { + logVerbose("\tSuccess!\n", .{}); + } else { + if (!g_verbose_logging) { + print("{s}: {s}\n", .{ command.getCommandName(), module.filename }); + } + print("\tFail: module init failed with error {}, but expected '{s}'\n", .{ e, expected_str }); + did_fail_any_test = true; + } + } else { + if (!g_verbose_logging) { + print("{s}: {s}\n", .{ command.getCommandName(), module.filename }); + } + print("\tDecode failed with error: {}\n", .{e}); + did_fail_any_test = true; + return e; + } + continue; + }; + + if (decode_expected_error) |expected| { + if (!g_verbose_logging) { + print("{s}: {s}\n", .{ command.getCommandName(), module.filename }); + } + print("\tFail: module init succeeded, but it should have failed with error '{s}'\n", .{expected}); + did_fail_any_test = true; + continue; + } + + if (validate_expected_error) |expected| { + if (!g_verbose_logging) { + print("{s}: {s}\n", .{ command.getCommandName(), module.filename }); + } + print("\tFail: module init succeeded, but it should have failed with error '{s}'\n", .{expected}); + did_fail_any_test = true; + continue; + } + + var instantiate_expected_error: ?[]const u8 = null; + switch (command.*) { + .AssertUninstantiable => |c| { + instantiate_expected_error = c.err.expected_error; + }, + .AssertUnlinkable => |c| { + instantiate_expected_error = c.err.expected_error; + }, + else => {}, + } + + module.inst = try bytebox.createModuleInstance(opts.vm_type, module.def.?, allocator); + (module.inst.?).instantiate(.{ .imports = imports.items }) catch |e| { + if (instantiate_expected_error) |expected_str| { + if (isSameError(e, expected_str)) { + logVerbose("\tSuccess!\n", .{}); + } else { + if (!g_verbose_logging) { + print("{s}: {s}\n", .{ command.getCommandName(), module.filename }); + } + print("\tFail: instantiate failed with error {}, but expected '{s}'\n", .{ e, expected_str }); + did_fail_any_test = true; + } + } else { + if (!g_verbose_logging) { + print("{s}: {s}\n", .{ command.getCommandName(), module.filename }); + } + print("\tInstantiate failed with error: {}\n", .{e}); + did_fail_any_test = true; + } + continue; + }; + + if (instantiate_expected_error) |expected_str| { + if (!g_verbose_logging) { + print("{s}: {s}\n", .{ command.getCommandName(), module.filename }); + } + print("\tFail: instantiate succeeded, but it should have failed with error '{s}'\n", .{expected_str}); + did_fail_any_test = true; + continue; + } + } + + switch (command.*) { + .AssertReturn => |c| { + const PrintTestHelper = struct { + fn logVerbose(filename: []const u8, field: []const u8, values: []LaneTypedVal) void { + if (g_verbose_logging) { + log(filename, field, values); + } + } + + fn log(filename: []const u8, field: []const u8, values: []LaneTypedVal) void { + print("assert_return: {s}:{s}(", .{ filename, field }); + for (values) |v| { + switch (v.v.type) { + .V128 => { + printVector(v.lane_type, v.v.val.V128); + }, + else => print("{}", .{v}), + } + } + print(")\n", .{}); + } + + fn printVector(lane_type: V128LaneType, vec: v128) void { + switch (lane_type) { + .I8x16 => print("{}, ", .{@as(i8x16, @bitCast(vec))}), + .I16x8 => print("{}, ", .{@as(i16x8, @bitCast(vec))}), + .I32x4 => print("{}, ", .{@as(i32x4, @bitCast(vec))}), + .I64x2 => print("{}, ", .{@as(i64x2, @bitCast(vec))}), + .F32x4 => print("{}, ", .{@as(f32x4, @bitCast(vec))}), + .F64x2 => print("{}, ", .{@as(f64x2, @bitCast(vec))}), + } + } + }; + + if (opts.command_filter_or_null) |filter| { + if (strcmp("assert_return", filter) == false) { + continue; + } + } + + if (opts.test_filter_or_null) |filter| { + if (strcmp(filter, c.action.field) == false) { + logVerbose("\tskipped {s}...\n", .{c.action.field}); + continue; + } + } + + const num_expected_returns = if (c.expected_returns) |returns| returns.items.len else 0; + var returns_placeholder = std.ArrayList(bytebox.Val).init(allocator); + defer returns_placeholder.deinit(); + + try returns_placeholder.resize(num_expected_returns); + var returns = returns_placeholder.items; + var return_types: ?[]ValType = null; + defer if (return_types) |types| allocator.free(types); + + PrintTestHelper.logVerbose(module.filename, c.action.field, c.action.args.items); + // logVerbose("assert_return: {s}:{s}({any})\n", .{ module.filename, c.action.field, c.action.args.items }); + + var action_succeeded = true; + switch (c.action.type) { + .Invocation => { + var vals = try LaneTypedVal.toValArrayList(c.action.args.items, allocator); + defer vals.deinit(); + + const func_handle: bytebox.FunctionHandle = try (module.inst.?).getFunctionHandle(c.action.field); + (module.inst.?).invoke(func_handle, vals.items.ptr, returns.ptr, .{}) catch |e| { + if (!g_verbose_logging) { + PrintTestHelper.log(module.filename, c.action.field, c.action.args.items); + // print("assert_return: {s}:{s}({any})\n", .{ module.filename, c.action.field, c.action.args.items }); + } + print("\tInvoke fail with error: {}\n", .{e}); + action_succeeded = false; + }; + + const func_export = module.inst.?.module_def.getFunctionExport(func_handle); + return_types = try allocator.dupe(ValType, func_export.returns); + }, + .Get => { + if ((module.inst.?).getGlobalExport(c.action.field)) |global_export| { + returns[0] = global_export.val.*; + return_types = try allocator.dupe(ValType, &[_]ValType{global_export.valtype}); + } else |e| { + if (!g_verbose_logging) { + PrintTestHelper.log(module.filename, c.action.field, c.action.args.items); + // print("assert_return: {s}:{s}({any})\n", .{ module.filename, c.action.field, c.action.args.items }); + } + print("\tGet fail with error: {}\n", .{e}); + action_succeeded = false; + } + }, + } + + if (action_succeeded) { + if (c.expected_returns) |expected| { + for (returns, 0..) |r, i| { + var pass = false; + + const return_type: ValType = return_types.?[i]; + + const expected_value: TaggedVal = expected.items[i].v; + if (expected_value.type != .V128) { + if (expected_value.type == .F32 and std.math.isNan(expected_value.val.F32)) { + pass = return_type == .F32 and std.math.isNan(r.F32); + } else if (expected_value.type == .F64 and std.math.isNan(expected_value.val.F64)) { + pass = return_type == .F64 and std.math.isNan(r.F64); + } else { + pass = Val.eql(expected_value.type, r, expected_value.val); + } + + if (pass == false) { + if (!g_verbose_logging) { + print("assert_return: {s}:{s}({any})\n", .{ module.filename, c.action.field, c.action.args.items }); + } + + print("\tFail on return {}/{}. Expected: {}, Actual: {}\n", .{ i + 1, returns.len, expected_value, r }); + action_succeeded = false; + } + } else { + const V128ExpectHelper = struct { + fn expect(comptime VectorType: type, actual_value: v128, expected_value_: v128, return_index: usize, returns_length: usize, module_: *const Module, command_: *const CommandAssertReturn) bool { + const actual_typed = @as(VectorType, @bitCast(actual_value)); + const expected_typed = @as(VectorType, @bitCast(expected_value_)); + + var is_equal = true; + const child_type = @typeInfo(VectorType).Vector.child; + switch (child_type) { + i8, i16, i32, i64 => { + is_equal = std.meta.eql(actual_typed, expected_typed); + }, + f32, f64 => { + const len = @typeInfo(VectorType).Vector.len; + var vec_i: u32 = 0; + while (vec_i < len) : (vec_i += 1) { + if (std.math.isNan(expected_typed[vec_i])) { + is_equal = is_equal and std.math.isNan(actual_typed[vec_i]); + } else { + is_equal = is_equal and expected_typed[vec_i] == actual_typed[vec_i]; + } + } + }, + else => @compileError("unsupported vector child type"), + } + + if (is_equal == false) { + if (!g_verbose_logging) { + print("assert_return: {s}:{s}({any})\n", .{ module_.filename, command_.action.field, command_.action.args.items }); + } + + print("\tFail on return {}/{}. Expected: {}, Actual: {}\n", .{ return_index + 1, returns_length, expected_typed, actual_typed }); + } + return is_equal; + } + }; + + switch (expected.items[i].lane_type) { + .I8x16 => { + action_succeeded = V128ExpectHelper.expect(i8x16, r.V128, expected_value.val.V128, i, returns.len, module, &c); + }, + .I16x8 => { + action_succeeded = V128ExpectHelper.expect(i16x8, r.V128, expected_value.val.V128, i, returns.len, module, &c); + }, + .I32x4 => { + action_succeeded = V128ExpectHelper.expect(i32x4, r.V128, expected_value.val.V128, i, returns.len, module, &c); + }, + .I64x2 => { + action_succeeded = V128ExpectHelper.expect(i64x2, r.V128, expected_value.val.V128, i, returns.len, module, &c); + }, + .F32x4 => { + action_succeeded = V128ExpectHelper.expect(f32x4, r.V128, expected_value.val.V128, i, returns.len, module, &c); + }, + .F64x2 => { + action_succeeded = V128ExpectHelper.expect(f64x2, r.V128, expected_value.val.V128, i, returns.len, module, &c); + }, + } + } + } + } + + if (action_succeeded) { + logVerbose("\tSuccess!\n", .{}); + } else { + did_fail_any_test = true; + } + } + }, + .AssertTrap => |c| { + if (opts.command_filter_or_null) |filter| { + if (strcmp("assert_trap", filter) == false) { + continue; + } + } + + if (opts.test_filter_or_null) |filter| { + if (strcmp(filter, c.action.field) == false) { + logVerbose("assert_return: skipping {s}:{s}\n", .{ module.filename, c.action.field }); + continue; + } + } + + logVerbose("assert_trap: {s}:{s}({any})\n", .{ module.filename, c.action.field, c.action.args.items }); + + var returns_placeholder: [8]Val = undefined; + var returns = returns_placeholder[0..]; + + var action_failed = false; + var action_failed_with_correct_trap = false; + var caught_error: ?anyerror = null; + + switch (c.action.type) { + .Invocation => { + var vals = try LaneTypedVal.toValArrayList(c.action.args.items, allocator); + defer vals.deinit(); + + const func_handle: bytebox.FunctionHandle = try (module.inst.?).getFunctionHandle(c.action.field); + (module.inst.?).invoke(func_handle, vals.items.ptr, returns.ptr, .{}) catch |e| { + action_failed = true; + caught_error = e; + + if (isSameError(e, c.expected_error)) { + action_failed_with_correct_trap = true; + } + }; + }, + .Get => { + if ((module.inst.?).getGlobalExport(c.action.field)) |global_export| { + returns[0] = global_export.val.*; + } else |e| { + action_failed = true; + caught_error = e; + + if (isSameError(e, c.expected_error)) { + action_failed_with_correct_trap = true; + } + } + }, + } + + if (action_failed and action_failed_with_correct_trap) { + logVerbose("\tSuccess!\n", .{}); + } else { + if (!g_verbose_logging) { + print("assert_trap: {s}:{s}({any})\n", .{ module.filename, c.action.field, c.action.args.items }); + } + if (action_failed_with_correct_trap == false) { + print("\tInvoke trapped, but got error '{}'' instead of expected '{s}':\n", .{ caught_error.?, c.expected_error }); + did_fail_any_test = true; + } else { + print("\tInvoke succeeded instead of trapping on expected {s}:\n", .{c.expected_error}); + did_fail_any_test = true; + } + } + }, + else => {}, + } + } + + return !did_fail_any_test; +} + +pub fn parse_vm_type(backend_str: []const u8) VmType { + if (strcmp("stack", backend_str)) { + return .Stack; + } else if (strcmp("register", backend_str)) { + return .Register; + } else { + print("Failed parsing backend string '{s}'. Expected 'stack' or 'register'.", .{backend_str}); + return .Stack; + } +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var allocator: std.mem.Allocator = gpa.allocator(); + + var args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + var opts = TestOpts{}; + + var args_index: u32 = 1; // skip program name + while (args_index < args.len) : (args_index += 1) { + var arg = args[args_index]; + if (strcmp("--help", arg) or strcmp("-h", arg) or strcmp("help", arg)) { + const help_text = + \\ + \\Usage: {s} [OPTION]... + \\ --backend + \\ Options are: stack (default), register + \\ + \\ --suite + \\ Only run tests belonging to the given suite. Examples: i32, br_if, + \\ utf8-import-field, unwind + \\ + \\ --module + \\ Only decode and initialize the given module. Only tests belonging to the + \\ given module file are run. + \\ + \\ --command + \\ Only run tests with the given command type. Examples: assert_return + \\ assert_trap, assert_invalid + \\ + \\ --test + \\ Run all tests where the 'field' in the json driver matches this filter. + \\ + \\ --force-wasm-regen-only + \\ By default, if a given testsuite can't find its' .json file driver, it will + \\ regenerate the wasm files and json driver, then run the test. This command + \\ will force regeneration of said files and skip running all tests. + \\ + \\ --log-suite + \\ Log the name of each suite and aggregate test result. + \\ + \\ --verbose + \\ Turn on verbose logging for each step of the test suite run. + \\ + \\ + ; + print(help_text, .{args[0]}); + return; + } else if (strcmp("--backend", arg)) { + args_index += 1; + opts.vm_type = parse_vm_type(args[args_index]); + } else if (strcmp("--suite", arg)) { + args_index += 1; + opts.suite_filter_or_null = args[args_index]; + print("found suite filter: {s}\n", .{opts.suite_filter_or_null.?}); + } else if (strcmp("--module", arg)) { + args_index += 1; + opts.module_filter_or_null = args[args_index]; + print("found module filter: {s}\n", .{opts.module_filter_or_null.?}); + } else if (strcmp("--command", arg)) { + args_index += 1; + opts.command_filter_or_null = args[args_index]; + print("found command filter: {s}\n", .{opts.command_filter_or_null.?}); + } else if (strcmp("--test", arg)) { + args_index += 1; + opts.test_filter_or_null = args[args_index]; + print("found test filter: {s}\n", .{opts.test_filter_or_null.?}); + } else if (strcmp("--force-wasm-regen-only", arg)) { + opts.force_wasm_regen_only = true; + print("Force-regenerating wasm files and driver .json, skipping test run\n", .{}); + } else if (strcmp("--log-suite", arg)) { + opts.log_suite = true; + } else if (strcmp("--verbose", arg) or strcmp("-v", arg)) { + g_verbose_logging = true; + print("verbose logging: on\n", .{}); + } + } + + const all_suites = [_][]const u8{ + "address", + "align", + "binary", + "binary-leb128", + "block", + "br", + "br_if", + "br_table", + "bulk", + "call", + "call_indirect", + "comments", + "const", + "conversions", + "custom", + "data", + "elem", + "endianness", + "exports", + "f32", + "f32_bitwise", + "f32_cmp", + "f64", + "f64_bitwise", + "f64_cmp", + "fac", + "float_exprs", + "float_literals", + "float_memory", + "float_misc", + "forward", + "func", + "func_ptrs", + "global", + "i32", + "i64", + "if", + "imports", + "inline-module", + "int_exprs", + "int_literals", + "labels", + "left-to-right", + "linking", + "load", + "local_get", + "local_set", + "local_tee", + "loop", + "memory", + "memory_copy", + "memory_fill", + "memory_grow", + "memory_init", + "memory_redundancy", + "memory_size", + "memory_trap", + "names", + "nop", + "ref_func", + "ref_is_null", + "ref_null", + "return", + "select", + "simd_address", + "simd_align", + "simd_bitwise", + "simd_bit_shift", + "simd_boolean", + "simd_const", + "simd_conversions", + "simd_f32x4", + "simd_f32x4_arith", + "simd_f32x4_cmp", + "simd_f32x4_pmin_pmax", + "simd_f32x4_rounding", + "simd_f64x2", + "simd_f64x2_arith", + "simd_f64x2_cmp", + "simd_f64x2_pmin_pmax", + "simd_f64x2_rounding", + "simd_i16x8_arith", + "simd_i16x8_arith2", + "simd_i16x8_cmp", + "simd_i16x8_extadd_pairwise_i8x16", + "simd_i16x8_extmul_i8x16", + "simd_i16x8_q15mulr_sat_s", + "simd_i16x8_sat_arith", + "simd_i32x4_arith", + "simd_i32x4_arith2", + "simd_i32x4_cmp", + "simd_i32x4_dot_i16x8", + "simd_i32x4_extadd_pairwise_i16x8", + "simd_i32x4_extmul_i16x8", + "simd_i32x4_trunc_sat_f32x4", + "simd_i32x4_trunc_sat_f64x2", + "simd_i64x2_arith", + "simd_i64x2_arith2", + "simd_i64x2_cmp", + "simd_i64x2_extmul_i32x4", + "simd_i8x16_arith", + "simd_i8x16_arith2", + "simd_i8x16_cmp", + "simd_i8x16_sat_arith", + "simd_int_to_int_extend", + "simd_lane", + "simd_load", + "simd_load16_lane", + "simd_load32_lane", + "simd_load64_lane", + "simd_load8_lane", + "simd_load_extend", + "simd_load_splat", + "simd_load_zero", + "simd_splat", + "simd_store", + "simd_store16_lane", + "simd_store32_lane", + "simd_store64_lane", + "simd_store8_lane", + "skip-stack-guard-page", + "stack", + "start", + "store", + "switch", + "table", + "table-sub", + "table_copy", + "table_fill", + "table_get", + "table_grow", + "table_init", + "table_set", + "table_size", + "token", + "traps", + "type", + "unreachable", + "unreached-invalid", + "unreached-valid", + "unwind", + "utf8-custom-section-id", + "utf8-import-field", + "utf8-import-module", + "utf8-invalid-encoding", + }; + + var did_all_succeed: bool = true; + + for (all_suites) |suite| { + if (opts.suite_filter_or_null) |filter| { + if (strcmp(filter, suite) == false) { + continue; + } + } + + var suite_path_no_extension: []const u8 = try std.fs.path.join(allocator, &[_][]const u8{ "test", "wasm", suite, suite }); + defer allocator.free(suite_path_no_extension); + + var suite_path = try std.mem.join(allocator, "", &[_][]const u8{ suite_path_no_extension, ".json" }); + defer allocator.free(suite_path); + + var needs_regen: bool = false; + if (opts.force_wasm_regen_only) { + needs_regen = true; + } else { + std.fs.cwd().access(suite_path, .{ .mode = .read_only }) catch |e| { + if (e == std.os.AccessError.FileNotFound) { + needs_regen = true; + } + }; + } + + if (needs_regen) { + logVerbose("Regenerating wasm and json driver for suite {s}\n", .{suite}); + + // var suite_wast_path_no_extension = try std.fs.path.join(allocator, &[_][]const u8{ "test", "testsuite", suite }); + var suite_wast_path_no_extension = try std.fs.path.join(allocator, &[_][]const u8{ "../../testsuite", suite }); + defer allocator.free(suite_wast_path_no_extension); + + var suite_wast_path = try std.mem.join(allocator, "", &[_][]const u8{ suite_wast_path_no_extension, ".wast" }); + defer allocator.free(suite_wast_path); + + var suite_wasm_folder: []const u8 = try std.fs.path.join(allocator, &[_][]const u8{ "test", "wasm", suite }); + defer allocator.free(suite_wasm_folder); + + std.fs.cwd().makeDir("test/wasm") catch |e| { + if (e != error.PathAlreadyExists) { + return e; + } + }; + + std.fs.cwd().makeDir(suite_wasm_folder) catch |e| { + if (e != error.PathAlreadyExists) { + return e; + } + }; + + var process = std.ChildProcess.init(&[_][]const u8{ "wast2json", suite_wast_path }, allocator); + + process.cwd = suite_wasm_folder; + + _ = try process.spawnAndWait(); + } + + if (opts.force_wasm_regen_only == false) { + if (opts.log_suite or g_verbose_logging) { + print("Running test suite: {s}\n", .{suite}); + } + + var success: bool = try run(allocator, suite_path, &opts); + did_all_succeed = did_all_succeed and success; + + if (success and opts.log_suite and !g_verbose_logging) { + print("\tSuccess\n", .{}); + } + } + } + + if (did_all_succeed == false) { + std.os.exit(1); + } +} diff --git a/src/ext/bytebox/test/wasi/bytebox_adapter.py b/src/ext/bytebox/test/wasi/bytebox_adapter.py new file mode 100644 index 00000000..70ab91f0 --- /dev/null +++ b/src/ext/bytebox/test/wasi/bytebox_adapter.py @@ -0,0 +1,35 @@ +# Based on the wasmtime adapter in wasi-testsuite +import argparse +import subprocess +import sys +import os +import shlex + +current_file_path = os.path.dirname(os.path.realpath(__file__)) +bytebox_relative_path = "../../zig-out/bin/bytebox" +if sys.platform == 'Windows': + bytebox_relative_path += ".exe" +BYTEBOX = os.path.join(current_file_path, bytebox_relative_path) + +parser = argparse.ArgumentParser() +parser.add_argument("--version", action="store_true") +parser.add_argument("--test-file", action="store") +parser.add_argument("--arg", action="append", default=[]) +parser.add_argument("--env", action="append", default=[]) +parser.add_argument("--dir", action="append", default=[]) + +args = parser.parse_args() + +if args.version: + subprocess.run([BYTEBOX] + ["--version"]) + sys.exit(0) + +TEST_FILE = args.test_file +PROG_ARGS = args.arg +ENV_ARGS = [j for i in args.env for j in ["--env", i]] +DIR_ARGS = [j for i in args.dir for j in ["--dir", i]] + +ALL_ARGS = [BYTEBOX] + [TEST_FILE] + PROG_ARGS + ENV_ARGS + DIR_ARGS + +r = subprocess.run(ALL_ARGS) +sys.exit(r.returncode) diff --git a/src/ext/bytebox/test/wasi/runtests.sh b/src/ext/bytebox/test/wasi/runtests.sh new file mode 100644 index 00000000..438045b4 --- /dev/null +++ b/src/ext/bytebox/test/wasi/runtests.sh @@ -0,0 +1,2 @@ +#!/bin/bash +python3 wasi-testsuite/test-runner/wasi_test_runner.py -t ./wasi-testsuite/tests/assemblyscript/testsuite/ ./wasi-testsuite/tests/c/testsuite/ -r ./bytebox_adapter.sh diff --git a/src/runtime.c b/src/runtime.c index b9f89887..82bb0d1c 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -22,7 +22,13 @@ #include "runtime_memory.c" #include "wasm/wasm.c" -#include "wasm/backend_wasm3.c" +#if OC_WASM_BACKEND_WASM3 + #include "wasm/backend_wasm3.c" +#elif OC_WASM_BACKEND_BYTEBOX + #include "wasm/backend_bytebox.c" +#else + #error "Unknown wasm backend" +#endif static const char* s_test_wasm_module_path = NULL; diff --git a/src/runtime.h b/src/runtime.h index e91836e0..ab547e5c 100644 --- a/src/runtime.h +++ b/src/runtime.h @@ -135,14 +135,14 @@ void oc_assert_fail_dialog(const char* file, const char* function, int line, con #define OC_ASSERT_DIALOG(test, ...) \ _OC_ASSERT_DIALOG_(test, OC_VA_NOPT("", ##__VA_ARGS__) OC_ARG1(__VA_ARGS__) OC_VA_COMMA_TAIL(__VA_ARGS__)) -#define OC_WASM_TRAP(status) \ - do \ - { \ - if(oc_wasm_status_is_fail(status)) \ - { \ - oc_abort_ext_dialog(__FILE__, __FUNCTION__, __LINE__, "%s", oc_wasm_status_str8(status)); \ - } \ - } \ +#define OC_WASM_TRAP(status) \ + do \ + { \ + if(oc_wasm_status_is_fail(status)) \ + { \ + oc_abort_ext_dialog(__FILE__, __FUNCTION__, __LINE__, "test %.*s", oc_wasm_status_str8(status)); \ + } \ + } \ while(0) #endif //__RUNTIME_H_ diff --git a/src/tool/bundle.c b/src/tool/bundle.c index 8415a382..39b09076 100644 --- a/src/tool/bundle.c +++ b/src/tool/bundle.c @@ -142,19 +142,22 @@ int winBundle( TRY(oc_sys_mkdirs(wasmDir)); TRY(oc_sys_mkdirs(dataDir)); - oc_str8 sdk_dir = version.len > 0 - ? get_version_dir(a, version, true) - : current_version_dir(a, true); + oc_str8 sdkDir = version.len > 0 + ? get_version_dir(a, version, true) + : current_version_dir(a, true); //----------------------------------------------------------- - //NOTE: link runtime objects and application icon into exe + //NOTE: copy exe into directory and embed application icon //----------------------------------------------------------- { + oc_str8 exe_in = oc_path_append(a, sdkDir, OC_STR8("bin/orca_runtime.exe")); + oc_str8 exe_out = oc_path_append(a, exeDir, oc_str8_pushf(a, "%.*s.exe", oc_str8_ip(name))); + TRY(oc_sys_copy(exe_in, exe_out)); + oc_str8 temp_dir = oc_path_append(a, outDir, OC_STR8("temporary")); - oc_str8 res_path = { 0 }; if(icon.len > 0) { - if (oc_sys_exists(icon)) + if(oc_sys_exists(icon)) { if(oc_sys_exists(temp_dir)) { @@ -167,14 +170,10 @@ int winBundle( fprintf(stderr, "failed to create windows icon for \"%.*s\"\n", oc_str8_ip(icon)); } - res_path = oc_path_append(a, temp_dir, OC_STR8("icon.res")); - if(!resource_file_from_icon(a, ico_path, res_path)) + if(!embed_icon_into_exe(a, exe_out, ico_path)) { - fprintf(stderr, "failed to create windows resource file for \"%.*s\"\n", oc_str8_ip(ico_path)); - res_path.ptr = 0; - res_path.len = 0; + fprintf(stderr, "failed to embed icon into exe file %.*s", (int)exe_out.len, exe_out.ptr); } - } else { @@ -182,26 +181,19 @@ int winBundle( } } - oc_str8 exe_out = oc_path_append(a, exeDir, oc_str8_pushf(a, "%.*s.exe", oc_str8_ip(name))); - oc_str8 libpath = oc_path_append(a, sdk_dir, OC_STR8("bin")); - oc_str8 cmd = oc_str8_pushf(a, "link.exe /nologo /LIBPATH:%s runtime.obj orca.dll.lib wasm3.lib %.*s /out:%s", - libpath.ptr, oc_str8_ip(res_path), exe_out.ptr); - i32 result = system(cmd.ptr); - oc_sys_rmdir(temp_dir); - if(result) + if(oc_sys_exists(temp_dir)) { - fprintf(stderr, "failed to link application executable\n"); - return result; + oc_sys_rmdir(temp_dir); } } //----------------------------------------------------------- //NOTE: copy orca libraries //----------------------------------------------------------- - oc_str8 orcaLib = oc_path_append(a, sdk_dir, OC_STR8("bin/orca.dll")); - oc_str8 glesLib = oc_path_append(a, sdk_dir, OC_STR8("bin/libGLESv2.dll")); - oc_str8 eglLib = oc_path_append(a, sdk_dir, OC_STR8("bin/libEGL.dll")); - oc_str8 wgpuLib = oc_path_append(a, sdk_dir, OC_STR8("bin/webgpu.dll")); + oc_str8 orcaLib = oc_path_append(a, sdkDir, OC_STR8("bin/orca.dll")); + oc_str8 glesLib = oc_path_append(a, sdkDir, OC_STR8("bin/libGLESv2.dll")); + oc_str8 eglLib = oc_path_append(a, sdkDir, OC_STR8("bin/libEGL.dll")); + oc_str8 wgpuLib = oc_path_append(a, sdkDir, OC_STR8("bin/webgpu.dll")); TRY(oc_sys_copy(orcaLib, exeDir)); TRY(oc_sys_copy(glesLib, exeDir)); @@ -244,8 +236,8 @@ int winBundle( //----------------------------------------------------------- //NOTE: copy runtime resources //----------------------------------------------------------- - TRY(oc_sys_copy(oc_path_append(a, sdk_dir, OC_STR8("resources/Menlo.ttf")), resDir)); - TRY(oc_sys_copy(oc_path_append(a, sdk_dir, OC_STR8("resources/Menlo Bold.ttf")), resDir)); + TRY(oc_sys_copy(oc_path_append(a, sdkDir, OC_STR8("resources/Menlo.ttf")), resDir)); + TRY(oc_sys_copy(oc_path_append(a, sdkDir, OC_STR8("resources/Menlo Bold.ttf")), resDir)); return 0; } @@ -291,17 +283,17 @@ int macBundle( TRY(oc_sys_mkdirs(wasmDir)); TRY(oc_sys_mkdirs(dataDir)); - oc_str8 sdk_dir = version.len > 0 ? get_version_dir(a, version, true) - : current_version_dir(a, true); + oc_str8 sdkDir = version.len > 0 ? get_version_dir(a, version, true) + : current_version_dir(a, true); //----------------------------------------------------------- //NOTE: copy orca runtime executable and libraries //----------------------------------------------------------- - oc_str8 orcaExe = oc_path_append(a, sdk_dir, OC_STR8("bin/orca_runtime")); - oc_str8 orcaLib = oc_path_append(a, sdk_dir, OC_STR8("bin/liborca.dylib")); - oc_str8 glesLib = oc_path_append(a, sdk_dir, OC_STR8("bin/libGLESv2.dylib")); - oc_str8 eglLib = oc_path_append(a, sdk_dir, OC_STR8("bin/libEGL.dylib")); - oc_str8 wgpu_lib = oc_path_append(a, sdk_dir, OC_STR8("bin/libwebgpu.dylib")); + oc_str8 orcaExe = oc_path_append(a, sdkDir, OC_STR8("bin/orca_runtime")); + oc_str8 orcaLib = oc_path_append(a, sdkDir, OC_STR8("bin/liborca.dylib")); + oc_str8 glesLib = oc_path_append(a, sdkDir, OC_STR8("bin/libGLESv2.dylib")); + oc_str8 eglLib = oc_path_append(a, sdkDir, OC_STR8("bin/libEGL.dylib")); + oc_str8 wgpu_lib = oc_path_append(a, sdkDir, OC_STR8("bin/libwebgpu.dylib")); TRY(oc_sys_copy(orcaExe, exeDir)); TRY(oc_sys_copy(orcaLib, exeDir)); @@ -348,15 +340,15 @@ int macBundle( //----------------------------------------------------------- //NOTE: copy runtime resources //----------------------------------------------------------- - TRY(oc_sys_copy(oc_path_append(a, sdk_dir, OC_STR8("resources/Menlo.ttf")), resDir)); - TRY(oc_sys_copy(oc_path_append(a, sdk_dir, OC_STR8("resources/Menlo Bold.ttf")), resDir)); + TRY(oc_sys_copy(oc_path_append(a, sdkDir, OC_STR8("resources/Menlo.ttf")), resDir)); + TRY(oc_sys_copy(oc_path_append(a, sdkDir, OC_STR8("resources/Menlo Bold.ttf")), resDir)); //----------------------------------------------------------- //NOTE make icon //----------------------------------------------------------- if(icon.len) { - if (oc_sys_exists(icon)) + if(oc_sys_exists(icon)) { oc_str8 icon_dir = oc_path_slice_directory(icon); oc_str8 iconset = oc_path_append(a, icon_dir, OC_STR8("icon.iconset")); diff --git a/src/tool/win32_icon.c b/src/tool/win32_icon.c index 64ba96c2..c9eb2489 100644 --- a/src/tool/win32_icon.c +++ b/src/tool/win32_icon.c @@ -11,19 +11,46 @@ typedef struct u16 reserved_zero; // must always be zero u16 image_type; // 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid. u16 num_images; // Specifies number of images in the file. -} IcoHeader; +} IcoHeaderDisk; typedef struct { u8 image_width; u8 image_height; - u8 num_colors_in_palette; // should be 0 if the image does not use a color palette - u8 reserved_zero; // must be 0 - u16 color_planes; // should be 0 or 1 + u8 num_colors_in_palette; // should be 0 if the image does not use a color palette + u8 reserved_zero; // must be 0 + u16 color_planes; // should be 0 or 1 u16 bits_per_pixel; u32 image_size_in_bytes; - u32 offset; // offset of the bmp or png data from the beginning of the ico file -} IcoEntry; + u32 offset; // offset of the bmp or png data from the beginning of the ico file +} IcoEntryDisk; + +// The icon data format is slightly different on disk vs in-resource +#pragma pack(push,2) +typedef struct +{ + u8 image_width; + u8 image_height; + u8 num_colors_in_palette; // should be 0 if the image does not use a color palette + u8 reserved_zero; // must be 0 + u8 color_planes; // should be 0 or 1 + u8 bit_count; + u16 bits_per_pixel; + u16 image_size_in_bytes; + u16 reserved_zero2; // must be 0 + u16 id; // 1-based index of entry +} IcoEntryRc; +#pragma pack(pop) + +#pragma pack(push,2) +typedef struct +{ + u16 reserved_zero; + u16 image_type; + u16 num_images; + IcoEntryRc entries[]; +} IcoHeaderRc; +#pragma pack(pop) bool icon_from_image(oc_arena* a, oc_str8 image_path, oc_str8 ico_path) { @@ -39,7 +66,7 @@ bool icon_from_image(oc_arena* a, oc_str8 image_path, oc_str8 ico_path) u8* images[oc_array_size(sizes)] = { 0 }; i32 image_sizes_bytes[oc_array_size(sizes)] = { 0 }; - IcoHeader header = { 0 }; + IcoHeaderDisk header = { 0 }; header.image_type = 1; header.num_images = (u16)num_sizes; @@ -91,12 +118,12 @@ bool icon_from_image(oc_arena* a, oc_str8 image_path, oc_str8 ico_path) //----------------------------------------------------------------------------- // Write icon directory entries //----------------------------------------------------------------------------- - u32 data_offset = sizeof(IcoHeader) + (num_sizes * sizeof(IcoEntry)); + u32 data_offset = sizeof(IcoHeaderDisk) + (num_sizes * sizeof(IcoEntryDisk)); for(i32 i = 0; i < num_sizes; ++i) { u32 size = sizes[i]; - u8 entry_size = size == 256 ? 0 : (u8)size; // NOTE(shaw): 0 means 256 pixels in IcoEntry - IcoEntry entry = { + u8 entry_size = size == 256 ? 0 : (u8)size; // NOTE(shaw): 0 means 256 pixels in IcoEntryDisk + IcoEntryDisk entry = { .image_width = entry_size, .image_height = entry_size, .num_colors_in_palette = 0, @@ -137,6 +164,104 @@ bool icon_from_image(oc_arena* a, oc_str8 image_path, oc_str8 ico_path) return result; } +bool embed_icon_into_exe(oc_arena* a, oc_str8 exe_path, oc_str8 ico_path) +{ + bool result = true; + oc_arena_scope scratch = oc_scratch_begin_next(a); + oc_file ico_file = {0}; + + ico_file = oc_file_open(ico_path, OC_FILE_ACCESS_READ, OC_FILE_OPEN_NONE); + if(oc_file_is_nil(ico_file)) + { + result = false; + goto cleanup; + } + + u64 ico_file_size = oc_file_size(ico_file); + u8* ico_file_data = oc_arena_push_array(a, u8, ico_file_size); + u64 total_read = oc_file_read(ico_file, ico_file_size, ico_file_data); + if (total_read < ico_file_size) + { + result = false; + goto cleanup; + } + + IcoHeaderDisk* ico_header_disk = (IcoHeaderDisk*)ico_file_data; + IcoEntryDisk* ico_entries_disk = (IcoEntryDisk*)(ico_file_data + sizeof(IcoHeaderDisk)); + + u64 ico_meta_size = sizeof(IcoHeaderRc) + sizeof(IcoEntryRc) * ico_header_disk->num_images; + u8* ico_meta_data = oc_arena_push_array(a, u8, ico_meta_size); + IcoHeaderRc* ico_header = (IcoHeaderRc*)ico_meta_data; + IcoEntryRc* ico_entries = (IcoEntryRc*)(ico_meta_data + sizeof(IcoHeaderRc)); + + ico_header->reserved_zero = ico_header_disk->reserved_zero; + ico_header->image_type = ico_header_disk->image_type; + ico_header->num_images = ico_header_disk->num_images; + + void** images = oc_arena_push_array(a, u8*, ico_header->num_images); + + for (int i = 0; i < ico_header->num_images; ++i) + { + ico_entries[i].image_width = ico_entries_disk[i].image_width; + ico_entries[i].image_height = ico_entries_disk[i].image_height; + ico_entries[i].num_colors_in_palette = ico_entries_disk[i].num_colors_in_palette; + ico_entries[i].reserved_zero = 0; + ico_entries[i].color_planes = ico_entries_disk[i].color_planes; + ico_entries[i].bit_count = 0; + ico_entries[i].bits_per_pixel = ico_entries_disk[i].bits_per_pixel; + ico_entries[i].image_size_in_bytes = (u16)ico_entries_disk[i].image_size_in_bytes; + ico_entries[i].reserved_zero2 = 0; + ico_entries[i].id = i + 1; + + size_t image_size = ico_entries[i].image_size_in_bytes; + images[i] = (void*)oc_arena_push_array(a, u8, image_size); + memcpy(images[i], ico_file_data + ico_entries_disk[i].offset, image_size); // TODO test this!!! + } + + // The ascii versions of the UpdateResource functions seem to always fail with INVALID_PARAMETER + // so we use the wide versions which don't have this problem. + size_t exe_path_wide_count = mbstowcs(NULL, exe_path.ptr, exe_path.len); + wchar_t* exe_path_wide = oc_arena_push_array(a, wchar_t, exe_path_wide_count); + mbstowcs(exe_path_wide, exe_path.ptr, exe_path.len); + + BOOL delete_existing_resources = TRUE; + HANDLE rc_handle = BeginUpdateResourceW(exe_path_wide, delete_existing_resources); + if (rc_handle == INVALID_HANDLE_VALUE) + { + result = false; + goto cleanup; + } + + WORD langid_en_us = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); + BOOL discard = FALSE; + // orca is compiled without /DUNICODE so we need to manually recreate RT_GROUP_ICON in wide string format as + // WinUser.h doesn't provide explicit wide versions + LPWSTR RT_ICON_W = MAKEINTRESOURCEW(3); + LPWSTR RT_GROUP_ICON_W = MAKEINTRESOURCEW((ULONG_PTR)(RT_ICON_W) + DIFFERENCE); + if (!UpdateResourceW(rc_handle, RT_GROUP_ICON_W, MAKEINTRESOURCEW(0), langid_en_us, ico_meta_data, ico_meta_size)) + { + result = false; + discard = TRUE; + } + + for (int i = 0; i < ico_header->num_images; ++i) + { + if (!UpdateResourceW(rc_handle, RT_ICON_W, MAKEINTRESOURCEW(i + 1), langid_en_us, images[i], ico_entries[i].image_size_in_bytes)) + { + result = false; + discard = TRUE; + break; + } + } + + EndUpdateResourceW(rc_handle, discard); + +cleanup: + oc_file_close(ico_file); + oc_scratch_end(scratch); + return result; +} + bool resource_file_from_icon(oc_arena* a, oc_str8 ico_path, oc_str8 res_path) { bool result = true; diff --git a/src/wasm/backend_bytebox.c b/src/wasm/backend_bytebox.c new file mode 100644 index 00000000..3c958bb4 --- /dev/null +++ b/src/wasm/backend_bytebox.c @@ -0,0 +1,411 @@ +/************************************************************************* +* +* Orca +* Copyright 2024 Martin Fouilleul and the Orca project contributors +* See LICENSE.txt for licensing information +* +**************************************************************************/ + +#include "wasm.h" + +#include "bytebox.h" + +typedef struct oc_wasm_binding_bytebox +{ + oc_wasm_host_proc proc; + oc_wasm* wasm; + i16 countParams; + i16 countReturns; +} oc_wasm_binding_bytebox; + +typedef struct oc_wasm_binding_elt_bytebox +{ + oc_list_elt listElt; + oc_wasm_binding_bytebox binding; +} oc_wasm_binding_elt_bytebox; + +typedef struct oc_wasm +{ + oc_arena arena; + oc_list bindings; + oc_wasm_mem_callbacks memCallbacks; + + bb_import_package* imports; + bb_module_definition* definition; + bb_module_instance* instance; +} oc_wasm; + +typedef union +{ + bb_func_handle bb; + oc_wasm_function_handle* oc; +} oc_wasm_bytebox_func_handle_convert; + +static_assert(sizeof(bb_func_handle) == sizeof(oc_wasm_function_handle*), "function handle struct size mismatch"); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Helpers + +static bb_valtype oc_wasm_valtype_to_bytebox_valtype(oc_wasm_valtype valtype) +{ + switch(valtype) + { + case OC_WASM_VALTYPE_I32: + return BB_VALTYPE_I32; + case OC_WASM_VALTYPE_I64: + return BB_VALTYPE_I64; + case OC_WASM_VALTYPE_F32: + return BB_VALTYPE_F32; + case OC_WASM_VALTYPE_F64: + return BB_VALTYPE_F64; + } + + OC_ASSERT(false, "unhandled case %d", valtype); + + return BB_VALTYPE_I32; +} + +static oc_wasm_valtype bytebox_valtype_to_oc_valtype(bb_valtype bbType) +{ + switch(bbType) + { + case BB_VALTYPE_I32: + return OC_WASM_VALTYPE_I32; + case BB_VALTYPE_I64: + return OC_WASM_VALTYPE_I64; + case BB_VALTYPE_F32: + return OC_WASM_VALTYPE_F32; + case BB_VALTYPE_F64: + return OC_WASM_VALTYPE_F64; + default: + break; + } + + OC_ASSERT(false, "Unexpected bytebox type here: %d", bbType); + + return OC_WASM_VALTYPE_I32; +} + +static oc_wasm_val bytebox_val_to_oc_val(bb_val val) +{ + // TODO handle v128 + oc_wasm_val v; + v.I64 = val.i64_val; + return v; +} + +static bb_val oc_wasm_val_to_bytebox_val(oc_wasm_val val) +{ + // TODO handle v128 + bb_val v; + v.i64_val = val.I64; + return v; +} + +static void oc_wasm_binding_bytebox_thunk(void* userdata, bb_module_instance* module, const bb_val* params, bb_val* returns) +{ + bb_slice mem = bb_module_instance_mem_all(module); + + oc_wasm_binding_bytebox* binding = userdata; + + // we have to do a temp translation due to bb_val being 128 bits in size, but the orca wasm layer expects 64-bit sized values + i64 orca_params[255]; + i64 orca_returns[255]; + + OC_ASSERT(binding->countParams <= oc_array_size(orca_params)); + OC_ASSERT(binding->countReturns <= oc_array_size(orca_returns)); + + for(int i = 0; i < binding->countParams; ++i) + { + orca_params[i] = params[i].i64_val; + } + + binding->proc((const i64*)orca_params, (i64*)orca_returns, (u8*)mem.data, binding->wasm); + + for(int i = 0; i < binding->countReturns; ++i) + { + returns[i].i64_val = orca_returns[i]; + } +} + +static void* oc_wasm_mem_resize_bytebox(void* mem, size_t new_size_bytes, size_t old_size_bytes, void* userdata) +{ + oc_wasm_mem_callbacks* memCallbacks = (oc_wasm_mem_callbacks*)userdata; + return memCallbacks->resizeProc(mem, new_size_bytes, memCallbacks->userdata); +} + +static void oc_wasm_mem_free_bytebox(void* mem, size_t size_bytes, void* userdata) +{ + oc_wasm_mem_callbacks* memCallbacks = (oc_wasm_mem_callbacks*)userdata; + memCallbacks->freeProc(mem, memCallbacks->userdata); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Wasm3 implementation of interface functions + +oc_wasm_status oc_wasm_decode(oc_wasm* wasm, oc_str8 wasmBlob) +{ + bb_module_definition_init_opts opts = { 0 }; + wasm->definition = bb_module_definition_create(opts); + bb_error err = bb_module_definition_decode(wasm->definition, wasmBlob.ptr, wasmBlob.len); + if(err != BB_ERROR_OK) + { + oc_log_error("caught error decoding module: %s\n", bb_error_str(err)); + return OC_WASM_STATUS_FAIL_DECODE; + } + return OC_WASM_STATUS_SUCCESS; +} + +oc_wasm_status oc_wasm_add_binding(oc_wasm* wasm, oc_wasm_binding* binding) +{ + if(wasm->imports == NULL) + { + wasm->imports = bb_import_package_init("env"); + } + + oc_wasm_binding_elt_bytebox* elt = oc_arena_push_type(&wasm->arena, oc_wasm_binding_elt_bytebox); + oc_list_push(&wasm->bindings, &elt->listElt); + + elt->binding.proc = binding->proc; + elt->binding.countParams = binding->countParams; + elt->binding.countReturns = binding->countReturns; + elt->binding.wasm = wasm; + + bb_valtype param_types[64]; + OC_ASSERT(binding->countParams <= oc_array_size(param_types)); + for(int i = 0; i < binding->countParams; ++i) + { + param_types[i] = oc_wasm_valtype_to_bytebox_valtype(binding->params[i]); + } + + bb_valtype return_types[64]; + OC_ASSERT(binding->countReturns <= oc_array_size(return_types)); + for(int i = 0; i < binding->countReturns; ++i) + { + return_types[i] = oc_wasm_valtype_to_bytebox_valtype(binding->returns[i]); + } + + bb_error err = bb_import_package_add_function(wasm->imports, + &oc_wasm_binding_bytebox_thunk, + binding->importName.ptr, + param_types, + binding->countParams, + return_types, + binding->countReturns, + &elt->binding); + if(err != BB_ERROR_OK) + { + oc_log_error("caught error adding function binding: %s\n", bb_error_str(err)); + return OC_WASM_STATUS_FAIL_UNKNOWN; + } + + return OC_WASM_STATUS_SUCCESS; +} + +// TODO move moduleDebugName to oc_wasm_decode() +oc_wasm_status oc_wasm_instantiate(oc_wasm* wasm, oc_str8 moduleDebugName, oc_wasm_mem_callbacks memCallbacks) +{ + OC_ASSERT(memCallbacks.resizeProc); + OC_ASSERT(memCallbacks.freeProc); + + wasm->instance = bb_module_instance_create(wasm->definition); + wasm->memCallbacks = memCallbacks; + + bb_wasm_memory_config memconfig = { + .resize_callback = oc_wasm_mem_resize_bytebox, + .free_callback = oc_wasm_mem_free_bytebox, + .userdata = &wasm->memCallbacks, + }; + + bb_module_instance_instantiate_opts opts = { + .packages = &wasm->imports, + .num_packages = 1, + .wasm_memory_config = memconfig, + .stack_size = 65536, + .enable_debug = false, + }; + + bb_error err = bb_module_instance_instantiate(wasm->instance, opts); + if(err != BB_ERROR_OK) + { + oc_log_error("caught error instaniating module: %s\n", bb_error_str(err)); + return OC_WASM_STATUS_FAIL_INSTANTIATE; + } + + return OC_WASM_STATUS_SUCCESS; +} + +u64 oc_wasm_mem_size(oc_wasm* wasm) +{ + bb_slice mem = bb_module_instance_mem_all(wasm->instance); + return mem.length; +} + +oc_str8 oc_wasm_mem_get(oc_wasm* wasm) +{ + bb_slice mem = bb_module_instance_mem_all(wasm->instance); + return (oc_str8){ .ptr = mem.data, .len = mem.length }; +} + +oc_wasm_status oc_wasm_mem_resize(oc_wasm* wasm, u32 countPages) +{ + bb_error err = bb_module_instance_mem_grow_absolute(wasm->instance, countPages); + if(err != BB_ERROR_OK) + { + oc_log_error("caught error resizing wasm memory: %s\n", bb_error_str(err)); + return OC_WASM_STATUS_FAIL_UNKNOWN; // TODO rename this to OC_WASM_STATUS_FAIL + } + + return OC_WASM_STATUS_SUCCESS; +} + +oc_wasm_function_handle* oc_wasm_function_find(oc_wasm* wasm, oc_str8 exportName) +{ + bb_func_handle handle; + bb_error err = bb_module_instance_find_func(wasm->instance, exportName.ptr, &handle); + if(err != BB_ERROR_OK) + { + // NOTE: we don't log an error in this case because orca speculatively looks for exports - + // it's ok if we don't find them. + return NULL; + } + + oc_wasm_bytebox_func_handle_convert convert; + convert.bb = handle; + return convert.oc; +} + +oc_wasm_function_info oc_wasm_function_get_info(oc_arena* scratch, oc_wasm* wasm, oc_wasm_function_handle* handle) +{ + oc_wasm_bytebox_func_handle_convert convert; + convert.oc = handle; + + if(bb_func_handle_isvalid(convert.bb) == false) + { + return (oc_wasm_function_info){ 0 }; + } + + bb_func_info bbInfo = bb_module_instance_func_info(wasm->instance, convert.bb); + + u32 totalTypes = bbInfo.num_params + bbInfo.num_returns; + oc_wasm_valtype* types = (totalTypes > 0) ? oc_arena_push_array(scratch, oc_wasm_valtype, totalTypes) : NULL; + + oc_wasm_function_info info; + info.countParams = (u32)bbInfo.num_params; + info.countReturns = (u32)bbInfo.num_returns; + info.params = types; + info.returns = types + info.countParams; + + for(u32 i = 0; i < info.countParams; ++i) + { + bb_valtype valtype = bbInfo.params[i]; + info.params[i] = bytebox_valtype_to_oc_valtype(valtype); + } + + for(u32 i = 0; i < info.countReturns; ++i) + { + bb_valtype valtype = bbInfo.returns[i]; + info.returns[i] = bytebox_valtype_to_oc_valtype(valtype); + } + + return info; +} + +oc_wasm_status oc_wasm_function_call(oc_wasm* wasm, oc_wasm_function_handle* handle, oc_wasm_val* params, size_t countParams, oc_wasm_val* returns, size_t countReturns) +{ + oc_wasm_bytebox_func_handle_convert convert; + convert.oc = handle; + + bb_val bbParams[64]; + OC_ASSERT(countParams < oc_array_size(bbParams)); + bb_val bbReturns[64]; + OC_ASSERT(countReturns < oc_array_size(bbReturns)); + + for(size_t i = 0; i < countParams; ++i) + { + bbParams[i] = oc_wasm_val_to_bytebox_val(params[i]); + } + + bb_error err = bb_module_instance_invoke(wasm->instance, convert.bb, bbParams, countParams, bbReturns, countReturns, (bb_module_instance_invoke_opts){ 0 }); + if(err != BB_ERROR_OK) + { + oc_log_error("caught error invoking function: %s\n", bb_error_str(err)); + return OC_WASM_STATUS_FAIL_UNKNOWN; + } + + for(size_t i = 0; i < countReturns; ++i) + { + returns[i] = bytebox_val_to_oc_val(bbReturns[i]); + } + + return OC_WASM_STATUS_SUCCESS; +} + +oc_wasm_global_handle* oc_wasm_global_find(oc_wasm* wasm, oc_str8 exportName, oc_wasm_valtype expectedType) +{ + bb_global global = bb_module_instance_find_global(wasm->instance, exportName.ptr); + if(global.value) + { + OC_ASSERT(expectedType == bytebox_valtype_to_oc_valtype(global.type)); + } + + return (oc_wasm_global_handle*)global.value; +} + +oc_wasm_val oc_wasm_global_get_value(oc_wasm_global_handle* global) +{ + if(global) + { + bb_val* val = (bb_val*)global; + return bytebox_val_to_oc_val(*val); + } + + return (oc_wasm_val){ 0 }; +} + +void oc_wasm_global_set_value(oc_wasm_global_handle* global, oc_wasm_val value) +{ + if(global) + { + bb_val* val = (bb_val*)global; + *val = oc_wasm_val_to_bytebox_val(value); + } +} + +oc_wasm_global_pointer oc_wasm_global_pointer_find(oc_wasm* wasm, oc_str8 exportName) +{ + oc_wasm_global_handle* global = oc_wasm_global_find(wasm, exportName, OC_WASM_VALTYPE_I32); + if(global) + { + return (oc_wasm_global_pointer){ .handle = global, .address = oc_wasm_global_get_value(global).I32 }; + } + + return (oc_wasm_global_pointer){ 0 }; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// implementation creation / destruction + +void oc_wasm_destroy(oc_wasm* wasm) +{ + bb_module_instance_destroy(wasm->instance); + bb_import_package_deinit(wasm->imports); + bb_module_definition_destroy(wasm->definition); + + oc_arena arena = wasm->arena; + oc_arena_cleanup(&arena); +} + +oc_wasm* oc_wasm_create(void) +{ + oc_arena arena; + oc_arena_init(&arena); + + oc_wasm* wasm = oc_arena_push_type(&arena, oc_wasm); + memset(wasm, 0, sizeof(oc_wasm)); + + wasm->arena = arena; + oc_list_init(&wasm->bindings); + + return wasm; +} diff --git a/src/wasm/backend_wasm3.c b/src/wasm/backend_wasm3.c index 7d374499..dc7f7d45 100644 --- a/src/wasm/backend_wasm3.c +++ b/src/wasm/backend_wasm3.c @@ -8,15 +8,10 @@ #include "wasm.h" -#if OC_WASM_BACKEND_WASM3 - -// clang-format off #include "m3_compile.h" #include "m3_env.h" #include "wasm3.h" -// clang-format on - typedef struct oc_wasm_binding_wasm3 { oc_wasm_binding info; @@ -431,10 +426,7 @@ oc_wasm_status oc_wasm_function_call(oc_wasm* wasm, oc_wasm_function_handle* han IM3Function m3Func = (IM3Function)handle; const void* valuePtrs[128]; - if(oc_array_size(valuePtrs) < countParams) - { - OC_ASSERT("Need more static storage for params"); - } + OC_ASSERT(countParams < oc_array_size(valuePtrs), "Need more static storage for params"); for(size_t i = 0; i < countParams; ++i) { @@ -449,10 +441,7 @@ oc_wasm_status oc_wasm_function_call(oc_wasm* wasm, oc_wasm_function_handle* han if(countReturns > 0) { - if(oc_array_size(valuePtrs) < countReturns) - { - OC_ASSERT("Need more static storage for returns"); - } + OC_ASSERT(countReturns < oc_array_size(valuePtrs), "Need more static storage for returns"); for(size_t i = 0; i < countReturns; ++i) { @@ -538,5 +527,3 @@ oc_wasm* oc_wasm_create(void) return wasm; } - -#endif // OC_WASM_BACKEND_WASM3 diff --git a/src/wasm/wasm.h b/src/wasm/wasm.h index 2364adec..77950499 100644 --- a/src/wasm/wasm.h +++ b/src/wasm/wasm.h @@ -9,9 +9,12 @@ #ifndef __WASM_H_ #define __WASM_H_ -// When more backends are added, make sure to check for them before automatically defaulting to wasm3 #ifndef OC_WASM_BACKEND_WASM3 - #define OC_WASM_BACKEND_WASM3 1 + #error OC_WASM_BACKEND_WASM3 must be defined to 0 or 1 +#endif + +#ifndef OC_WASM_BACKEND_BYTEBOX + #error OC_WASM_BACKEND_BYTEBOX must be defined to 0 or 1 #endif typedef enum oc_wasm_status From 4a50dd21a446408f0dbe5ac22639441b7c3a563d Mon Sep 17 00:00:00 2001 From: Reuben Dunnington Date: Mon, 6 May 2024 21:00:28 -0700 Subject: [PATCH 2/3] compile bytebox with ReleaseFast by default * matches wasm3 optimization settings so we can have a better comparison --- scripts/dev.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/dev.py b/scripts/dev.py index 8080a65c..006698e5 100644 --- a/scripts/dev.py +++ b/scripts/dev.py @@ -567,9 +567,7 @@ def build_wasm3_lib_mac(release): def build_bytebox(release): print("Building bytebox...") - args = ["zig", "build"] - if release: - args += ["-Doptimize=ReleaseSafe"] + args = ["zig", "build", "-Doptimize=ReleaseFast"] subprocess.run(args, cwd="src/ext/bytebox/") if platform.system() == "Windows": From abe25b53e5d0f18ca84b3e41076f36f5c9b4eae1 Mon Sep 17 00:00:00 2001 From: Reuben Dunnington Date: Mon, 6 May 2024 21:05:05 -0700 Subject: [PATCH 3/3] fixup wasm status print --- src/runtime.h | 16 ++++++++-------- src/wasmbind/gles_api_bind_manual.c | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/runtime.h b/src/runtime.h index ab547e5c..4104d898 100644 --- a/src/runtime.h +++ b/src/runtime.h @@ -135,14 +135,14 @@ void oc_assert_fail_dialog(const char* file, const char* function, int line, con #define OC_ASSERT_DIALOG(test, ...) \ _OC_ASSERT_DIALOG_(test, OC_VA_NOPT("", ##__VA_ARGS__) OC_ARG1(__VA_ARGS__) OC_VA_COMMA_TAIL(__VA_ARGS__)) -#define OC_WASM_TRAP(status) \ - do \ - { \ - if(oc_wasm_status_is_fail(status)) \ - { \ - oc_abort_ext_dialog(__FILE__, __FUNCTION__, __LINE__, "test %.*s", oc_wasm_status_str8(status)); \ - } \ - } \ +#define OC_WASM_TRAP(status) \ + do \ + { \ + if(oc_wasm_status_is_fail(status)) \ + { \ + oc_abort_ext_dialog(__FILE__, __FUNCTION__, __LINE__, "%.*s", oc_str8_ip(oc_wasm_status_str8(status))); \ + } \ + } \ while(0) #endif //__RUNTIME_H_ diff --git a/src/wasmbind/gles_api_bind_manual.c b/src/wasmbind/gles_api_bind_manual.c index b52cce81..d3a4121d 100644 --- a/src/wasmbind/gles_api_bind_manual.c +++ b/src/wasmbind/gles_api_bind_manual.c @@ -1109,11 +1109,11 @@ void glGetStringi_stub(const i64* restrict _params, i64* restrict _returns, u8* int manual_link_gles_api(oc_wasm* wasm) { -#define BINDING_ERROR_HANDLING(name) \ - if(oc_wasm_status_is_fail(status)) \ - { \ - oc_log_error("Couldn't link function " #name " (%s)\n", oc_wasm_status_str8(status)); \ - ret = -1; \ +#define BINDING_ERROR_HANDLING(name) \ + if(oc_wasm_status_is_fail(status)) \ + { \ + oc_log_error("Couldn't link function " #name " (%.*s)\n", oc_str8_ip(oc_wasm_status_str8(status))); \ + ret = -1; \ } oc_wasm_status status;