first commit
This commit is contained in:
commit
b574d39a39
23 changed files with 8604 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
zig-out/
|
||||||
|
.zig-cache/
|
||||||
|
**/*~
|
||||||
102
build.zig
Normal file
102
build.zig
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
const mod = b.addModule("wasm_runtime", .{
|
||||||
|
.root_source_file = b.path("src/root.zig"),
|
||||||
|
.target = target,
|
||||||
|
});
|
||||||
|
|
||||||
|
const exe = b.addExecutable(.{
|
||||||
|
.name = "wasm_runtime",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "wasm_runtime", .module = mod },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
const run_cmd = b.addRunArtifact(exe);
|
||||||
|
run_cmd.step.dependOn(b.getInstallStep());
|
||||||
|
if (b.args) |args| run_cmd.addArgs(args);
|
||||||
|
const run_step = b.step("run", "Run the app");
|
||||||
|
run_step.dependOn(&run_cmd.step);
|
||||||
|
|
||||||
|
const lib_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/root.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const run_lib_tests = b.addRunArtifact(lib_tests);
|
||||||
|
|
||||||
|
const binary_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/wasm/binary.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const run_binary_tests = b.addRunArtifact(binary_tests);
|
||||||
|
|
||||||
|
const validator_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/wasm/validator.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const run_validator_tests = b.addRunArtifact(validator_tests);
|
||||||
|
|
||||||
|
const runtime_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/wasm/runtime.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const run_runtime_tests = b.addRunArtifact(runtime_tests);
|
||||||
|
|
||||||
|
const instance_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/wasm/instance.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const run_instance_tests = b.addRunArtifact(instance_tests);
|
||||||
|
|
||||||
|
const host_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/wasm/host.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const run_host_tests = b.addRunArtifact(host_tests);
|
||||||
|
|
||||||
|
const jit_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/wasm/jit_tests.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const run_jit_tests = b.addRunArtifact(jit_tests);
|
||||||
|
|
||||||
|
const test_step = b.step("test", "Run all tests");
|
||||||
|
test_step.dependOn(&run_lib_tests.step);
|
||||||
|
test_step.dependOn(&run_binary_tests.step);
|
||||||
|
test_step.dependOn(&run_validator_tests.step);
|
||||||
|
test_step.dependOn(&run_runtime_tests.step);
|
||||||
|
test_step.dependOn(&run_instance_tests.step);
|
||||||
|
test_step.dependOn(&run_host_tests.step);
|
||||||
|
test_step.dependOn(&run_jit_tests.step);
|
||||||
|
}
|
||||||
81
build.zig.zon
Normal file
81
build.zig.zon
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
.{
|
||||||
|
// This is the default name used by packages depending on this one. For
|
||||||
|
// example, when a user runs `zig fetch --save <url>`, this field is used
|
||||||
|
// as the key in the `dependencies` table. Although the user can choose a
|
||||||
|
// different name, most users will stick with this provided value.
|
||||||
|
//
|
||||||
|
// It is redundant to include "zig" in this name because it is already
|
||||||
|
// within the Zig package namespace.
|
||||||
|
.name = .wasm_runtime,
|
||||||
|
// This is a [Semantic Version](https://semver.org/).
|
||||||
|
// In a future version of Zig it will be used for package deduplication.
|
||||||
|
.version = "0.0.0",
|
||||||
|
// Together with name, this represents a globally unique package
|
||||||
|
// identifier. This field is generated by the Zig toolchain when the
|
||||||
|
// package is first created, and then *never changes*. This allows
|
||||||
|
// unambiguous detection of one package being an updated version of
|
||||||
|
// another.
|
||||||
|
//
|
||||||
|
// When forking a Zig project, this id should be regenerated (delete the
|
||||||
|
// field and run `zig build`) if the upstream project is still maintained.
|
||||||
|
// Otherwise, the fork is *hostile*, attempting to take control over the
|
||||||
|
// original project's identity. Thus it is recommended to leave the comment
|
||||||
|
// on the following line intact, so that it shows up in code reviews that
|
||||||
|
// modify the field.
|
||||||
|
.fingerprint = 0xcec38deb11d082fe, // Changing this has security and trust implications.
|
||||||
|
// Tracks the earliest Zig version that the package considers to be a
|
||||||
|
// supported use case.
|
||||||
|
.minimum_zig_version = "0.16.0-dev.2187+e2338edb4",
|
||||||
|
// This field is optional.
|
||||||
|
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||||
|
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
|
||||||
|
// Once all dependencies are fetched, `zig build` no longer requires
|
||||||
|
// internet connectivity.
|
||||||
|
.dependencies = .{
|
||||||
|
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
|
||||||
|
//.example = .{
|
||||||
|
// // When updating this field to a new URL, be sure to delete the corresponding
|
||||||
|
// // `hash`, otherwise you are communicating that you expect to find the old hash at
|
||||||
|
// // the new URL. If the contents of a URL change this will result in a hash mismatch
|
||||||
|
// // which will prevent zig from using it.
|
||||||
|
// .url = "https://example.com/foo.tar.gz",
|
||||||
|
//
|
||||||
|
// // This is computed from the file contents of the directory of files that is
|
||||||
|
// // obtained after fetching `url` and applying the inclusion rules given by
|
||||||
|
// // `paths`.
|
||||||
|
// //
|
||||||
|
// // This field is the source of truth; packages do not come from a `url`; they
|
||||||
|
// // come from a `hash`. `url` is just one of many possible mirrors for how to
|
||||||
|
// // obtain a package matching this `hash`.
|
||||||
|
// //
|
||||||
|
// // Uses the [multihash](https://multiformats.io/multihash/) format.
|
||||||
|
// .hash = "...",
|
||||||
|
//
|
||||||
|
// // When this is provided, the package is found in a directory relative to the
|
||||||
|
// // build root. In this case the package's hash is irrelevant and therefore not
|
||||||
|
// // computed. This field and `url` are mutually exclusive.
|
||||||
|
// .path = "foo",
|
||||||
|
//
|
||||||
|
// // When this is set to `true`, a package is declared to be lazily
|
||||||
|
// // fetched. This makes the dependency only get fetched if it is
|
||||||
|
// // actually used.
|
||||||
|
// .lazy = false,
|
||||||
|
//},
|
||||||
|
},
|
||||||
|
// Specifies the set of files and directories that are included in this package.
|
||||||
|
// Only files and directories listed here are included in the `hash` that
|
||||||
|
// is computed for this package. Only files listed here will remain on disk
|
||||||
|
// when using the zig package manager. As a rule of thumb, one should list
|
||||||
|
// files required for compilation plus any license(s).
|
||||||
|
// Paths are relative to the build root. Use the empty string (`""`) to refer to
|
||||||
|
// the build root itself.
|
||||||
|
// A directory listed here means that all files within, recursively, are included.
|
||||||
|
.paths = .{
|
||||||
|
"build.zig",
|
||||||
|
"build.zig.zon",
|
||||||
|
"src",
|
||||||
|
// For example...
|
||||||
|
//"LICENSE",
|
||||||
|
//"README.md",
|
||||||
|
},
|
||||||
|
}
|
||||||
27
src/main.zig
Normal file
27
src/main.zig
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const wasm = @import("wasm_runtime");
|
||||||
|
|
||||||
|
fn log(instance_ptr: *anyopaque, ptr: i32, len: i32) !void {
|
||||||
|
const instance: *wasm.ModuleInstance = @ptrCast(@alignCast(instance_ptr));
|
||||||
|
const bytes = try instance.memorySlice(@intCast(ptr), @intCast(len));
|
||||||
|
std.debug.print("wasm: {s}", .{bytes});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
var imports = wasm.ImportSet.init(allocator);
|
||||||
|
defer imports.deinit();
|
||||||
|
|
||||||
|
try imports.addFunc(.env, .log, log);
|
||||||
|
|
||||||
|
var engine = wasm.Engine.init(allocator);
|
||||||
|
var instance = try engine.loadFile("tests/wasm/fib.wasm", &imports);
|
||||||
|
defer instance.deinit();
|
||||||
|
|
||||||
|
const result = try instance.callExport("init", &.{});
|
||||||
|
defer allocator.free(result);
|
||||||
|
std.debug.print("done\n", .{});
|
||||||
|
}
|
||||||
79
src/root.zig
Normal file
79
src/root.zig
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const module = @import("wasm/module.zig");
|
||||||
|
pub const binary = @import("wasm/binary.zig");
|
||||||
|
pub const validator = @import("wasm/validator.zig");
|
||||||
|
pub const runtime = @import("wasm/runtime.zig");
|
||||||
|
pub const trap = @import("wasm/trap.zig");
|
||||||
|
pub const host = @import("wasm/host.zig");
|
||||||
|
pub const instance = @import("wasm/instance.zig");
|
||||||
|
|
||||||
|
pub const Value = runtime.Value;
|
||||||
|
pub const ValType = module.ValType;
|
||||||
|
pub const Memory = runtime.Memory;
|
||||||
|
pub const ImportSet = host.ImportSet;
|
||||||
|
pub const HostFunc = host.HostFunc;
|
||||||
|
pub const readStruct = host.readStruct;
|
||||||
|
pub const writeStruct = host.writeStruct;
|
||||||
|
pub const TrapCode = trap.TrapCode;
|
||||||
|
pub const Trap = trap.Trap;
|
||||||
|
pub const ModuleInstance = instance.ModuleInstance;
|
||||||
|
|
||||||
|
pub const Engine = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator) Engine {
|
||||||
|
return .{ .allocator = allocator };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loadFile(self: *Engine, path: []const u8, imports: *const ImportSet) !ModuleInstance {
|
||||||
|
const fd = try std.posix.openat(std.posix.AT.FDCWD, path, .{ .ACCMODE = .RDONLY }, 0);
|
||||||
|
defer std.posix.close(fd);
|
||||||
|
|
||||||
|
const st = try std.posix.fstat(fd);
|
||||||
|
if (st.size < 0) return error.InvalidWasmSize;
|
||||||
|
const size: usize = @intCast(st.size);
|
||||||
|
const bytes = try self.allocator.alloc(u8, size);
|
||||||
|
errdefer self.allocator.free(bytes);
|
||||||
|
|
||||||
|
var off: usize = 0;
|
||||||
|
while (off < bytes.len) {
|
||||||
|
const n = try std.posix.read(fd, bytes[off..]);
|
||||||
|
if (n == 0) break;
|
||||||
|
off += n;
|
||||||
|
}
|
||||||
|
if (off != bytes.len) return error.UnexpectedEof;
|
||||||
|
defer self.allocator.free(bytes);
|
||||||
|
return self.loadBytes(bytes, imports);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loadBytes(self: *Engine, bytes: []const u8, imports: *const ImportSet) !ModuleInstance {
|
||||||
|
const mod_ptr = try self.allocator.create(module.Module);
|
||||||
|
errdefer self.allocator.destroy(mod_ptr);
|
||||||
|
mod_ptr.* = try module.Module.parse(self.allocator, bytes);
|
||||||
|
errdefer mod_ptr.deinit();
|
||||||
|
try validator.validate(mod_ptr);
|
||||||
|
return ModuleInstance.instantiateOwned(self.allocator, mod_ptr, imports);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "engine loadBytes and call export" {
|
||||||
|
const wasm = [_]u8{
|
||||||
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f,
|
||||||
|
0x03, 0x02, 0x01, 0x00,
|
||||||
|
0x07, 0x08, 0x01, 0x04, 0x70, 0x69, 0x6e, 0x67, 0x00, 0x00,
|
||||||
|
0x0a, 0x06, 0x01, 0x04, 0x00, 0x41, 0x2a, 0x0b,
|
||||||
|
};
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var imports = ImportSet.init(ally);
|
||||||
|
defer imports.deinit();
|
||||||
|
|
||||||
|
var engine = Engine.init(ally);
|
||||||
|
var inst = try engine.loadBytes(&wasm, &imports);
|
||||||
|
defer inst.deinit();
|
||||||
|
|
||||||
|
const out = try inst.callExport("ping", &.{});
|
||||||
|
defer ally.free(out);
|
||||||
|
try std.testing.expectEqual(@as(i32, 42), out[0].i32);
|
||||||
|
}
|
||||||
489
src/wasm/binary.zig
Normal file
489
src/wasm/binary.zig
Normal file
|
|
@ -0,0 +1,489 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const module = @import("module.zig");
|
||||||
|
|
||||||
|
pub const Module = module.Module;
|
||||||
|
pub const ValType = module.ValType;
|
||||||
|
pub const SectionId = module.SectionId;
|
||||||
|
|
||||||
|
pub const ParseError = error{
|
||||||
|
InvalidMagic,
|
||||||
|
InvalidVersion,
|
||||||
|
UnexpectedEof,
|
||||||
|
MalformedLEB128,
|
||||||
|
UnknownSection,
|
||||||
|
UnknownOpcode,
|
||||||
|
InvalidValType,
|
||||||
|
InvalidImportDesc,
|
||||||
|
InvalidExportDesc,
|
||||||
|
InvalidConstExpr,
|
||||||
|
InvalidRefType,
|
||||||
|
InvalidMutability,
|
||||||
|
InvalidFuncType,
|
||||||
|
InvalidDataSegment,
|
||||||
|
OutOfMemory,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read an unsigned LEB128 value from a byte slice starting at *pos.
|
||||||
|
pub fn readULEB128(comptime T: type, bytes: []const u8, pos: *usize) ParseError!T {
|
||||||
|
const max_bits = @typeInfo(T).int.bits;
|
||||||
|
var result: T = 0;
|
||||||
|
var shift: u32 = 0;
|
||||||
|
while (true) {
|
||||||
|
if (pos.* >= bytes.len) return ParseError.UnexpectedEof;
|
||||||
|
const byte = bytes[pos.*];
|
||||||
|
pos.* += 1;
|
||||||
|
const val: T = @intCast(byte & 0x7F);
|
||||||
|
if (shift < max_bits) {
|
||||||
|
result |= val << @intCast(shift);
|
||||||
|
} else if (val != 0) {
|
||||||
|
return ParseError.MalformedLEB128;
|
||||||
|
}
|
||||||
|
if (byte & 0x80 == 0) break;
|
||||||
|
shift += 7;
|
||||||
|
if (shift >= max_bits + 7) return ParseError.MalformedLEB128;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a signed LEB128 value from a byte slice starting at *pos.
|
||||||
|
pub fn readSLEB128(comptime T: type, bytes: []const u8, pos: *usize) ParseError!T {
|
||||||
|
const Unsigned = std.meta.Int(.unsigned, @typeInfo(T).int.bits);
|
||||||
|
const max_bits = @typeInfo(T).int.bits;
|
||||||
|
var result: Unsigned = 0;
|
||||||
|
var shift: u32 = 0;
|
||||||
|
var last_byte: u8 = 0;
|
||||||
|
while (true) {
|
||||||
|
if (pos.* >= bytes.len) return ParseError.UnexpectedEof;
|
||||||
|
last_byte = bytes[pos.*];
|
||||||
|
pos.* += 1;
|
||||||
|
const val: Unsigned = @intCast(last_byte & 0x7F);
|
||||||
|
if (shift < max_bits) {
|
||||||
|
result |= val << @intCast(shift);
|
||||||
|
}
|
||||||
|
shift += 7;
|
||||||
|
if (last_byte & 0x80 == 0) break;
|
||||||
|
if (shift >= max_bits + 7) return ParseError.MalformedLEB128;
|
||||||
|
}
|
||||||
|
// Sign extend
|
||||||
|
if (shift < max_bits and (last_byte & 0x40) != 0) {
|
||||||
|
result |= ~@as(Unsigned, 0) << @intCast(shift);
|
||||||
|
}
|
||||||
|
return @bitCast(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readByte(bytes: []const u8, pos: *usize) ParseError!u8 {
|
||||||
|
if (pos.* >= bytes.len) return ParseError.UnexpectedEof;
|
||||||
|
const b = bytes[pos.*];
|
||||||
|
pos.* += 1;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readBytes(bytes: []const u8, pos: *usize, len: usize) ParseError![]const u8 {
|
||||||
|
if (pos.* + len > bytes.len) return ParseError.UnexpectedEof;
|
||||||
|
const slice = bytes[pos.* .. pos.* + len];
|
||||||
|
pos.* += len;
|
||||||
|
return slice;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readValType(bytes: []const u8, pos: *usize) ParseError!module.ValType {
|
||||||
|
const b = try readByte(bytes, pos);
|
||||||
|
return switch (b) {
|
||||||
|
0x7F => .i32,
|
||||||
|
0x7E => .i64,
|
||||||
|
0x7D => .f32,
|
||||||
|
0x7C => .f64,
|
||||||
|
else => ParseError.InvalidValType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readConstExpr(bytes: []const u8, pos: *usize) ParseError!module.ConstExpr {
|
||||||
|
const op = try readByte(bytes, pos);
|
||||||
|
const expr: module.ConstExpr = switch (op) {
|
||||||
|
0x41 => .{ .i32_const = try readSLEB128(i32, bytes, pos) },
|
||||||
|
0x42 => .{ .i64_const = try readSLEB128(i64, bytes, pos) },
|
||||||
|
0x43 => blk: {
|
||||||
|
const raw = try readBytes(bytes, pos, 4);
|
||||||
|
break :blk .{ .f32_const = @bitCast(std.mem.readInt(u32, raw[0..4], .little)) };
|
||||||
|
},
|
||||||
|
0x44 => blk: {
|
||||||
|
const raw = try readBytes(bytes, pos, 8);
|
||||||
|
break :blk .{ .f64_const = @bitCast(std.mem.readInt(u64, raw[0..8], .little)) };
|
||||||
|
},
|
||||||
|
0x23 => .{ .global_get = try readULEB128(u32, bytes, pos) },
|
||||||
|
else => return ParseError.InvalidConstExpr,
|
||||||
|
};
|
||||||
|
const end = try readByte(bytes, pos);
|
||||||
|
if (end != 0x0B) return ParseError.InvalidConstExpr;
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseLimits(bytes: []const u8, pos: *usize) ParseError!struct { min: u32, max: ?u32 } {
|
||||||
|
const flag = try readByte(bytes, pos);
|
||||||
|
const min = try readULEB128(u32, bytes, pos);
|
||||||
|
const max: ?u32 = if (flag == 1) try readULEB128(u32, bytes, pos) else null;
|
||||||
|
return .{ .min = min, .max = max };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseTypeSection(ally: std.mem.Allocator, bytes: []const u8, pos: *usize) ParseError![]module.FuncType {
|
||||||
|
const count = try readULEB128(u32, bytes, pos);
|
||||||
|
const types = try ally.alloc(module.FuncType, count);
|
||||||
|
errdefer {
|
||||||
|
for (types) |t| {
|
||||||
|
ally.free(t.params);
|
||||||
|
ally.free(t.results);
|
||||||
|
}
|
||||||
|
ally.free(types);
|
||||||
|
}
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < count) : (i += 1) {
|
||||||
|
const tag = try readByte(bytes, pos);
|
||||||
|
if (tag != 0x60) return ParseError.InvalidFuncType;
|
||||||
|
const param_count = try readULEB128(u32, bytes, pos);
|
||||||
|
const params = try ally.alloc(module.ValType, param_count);
|
||||||
|
errdefer ally.free(params);
|
||||||
|
for (params) |*p| p.* = try readValType(bytes, pos);
|
||||||
|
const result_count = try readULEB128(u32, bytes, pos);
|
||||||
|
const results = try ally.alloc(module.ValType, result_count);
|
||||||
|
errdefer ally.free(results);
|
||||||
|
for (results) |*r| r.* = try readValType(bytes, pos);
|
||||||
|
types[i] = .{ .params = params, .results = results };
|
||||||
|
}
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseImportSection(ally: std.mem.Allocator, bytes: []const u8, pos: *usize) ParseError![]module.Import {
|
||||||
|
const count = try readULEB128(u32, bytes, pos);
|
||||||
|
const imports = try ally.alloc(module.Import, count);
|
||||||
|
errdefer ally.free(imports);
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < count) : (i += 1) {
|
||||||
|
const mod_len = try readULEB128(u32, bytes, pos);
|
||||||
|
const mod_bytes = try readBytes(bytes, pos, mod_len);
|
||||||
|
const mod_name = try ally.dupe(u8, mod_bytes);
|
||||||
|
errdefer ally.free(mod_name);
|
||||||
|
const name_len = try readULEB128(u32, bytes, pos);
|
||||||
|
const name_bytes = try readBytes(bytes, pos, name_len);
|
||||||
|
const name = try ally.dupe(u8, name_bytes);
|
||||||
|
errdefer ally.free(name);
|
||||||
|
const kind = try readByte(bytes, pos);
|
||||||
|
const desc: module.ImportDesc = switch (kind) {
|
||||||
|
0 => .{ .func = try readULEB128(u32, bytes, pos) },
|
||||||
|
1 => blk: {
|
||||||
|
const elem_type = try readByte(bytes, pos);
|
||||||
|
const lim = try parseLimits(bytes, pos);
|
||||||
|
break :blk .{ .table = .{ .elem_type = elem_type, .min = lim.min, .max = lim.max } };
|
||||||
|
},
|
||||||
|
2 => blk: {
|
||||||
|
const lim = try parseLimits(bytes, pos);
|
||||||
|
break :blk .{ .memory = .{ .min = lim.min, .max = lim.max } };
|
||||||
|
},
|
||||||
|
3 => blk: {
|
||||||
|
const vt = try readValType(bytes, pos);
|
||||||
|
const mut = try readByte(bytes, pos);
|
||||||
|
if (mut > 1) return ParseError.InvalidMutability;
|
||||||
|
break :blk .{ .global = .{ .valtype = vt, .mutable = mut == 1 } };
|
||||||
|
},
|
||||||
|
else => return ParseError.InvalidImportDesc,
|
||||||
|
};
|
||||||
|
imports[i] = .{ .module = mod_name, .name = name, .desc = desc };
|
||||||
|
}
|
||||||
|
return imports;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseFunctionSection(ally: std.mem.Allocator, bytes: []const u8, pos: *usize) ParseError![]u32 {
|
||||||
|
const count = try readULEB128(u32, bytes, pos);
|
||||||
|
const funcs = try ally.alloc(u32, count);
|
||||||
|
for (funcs) |*f| f.* = try readULEB128(u32, bytes, pos);
|
||||||
|
return funcs;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseTableSection(ally: std.mem.Allocator, bytes: []const u8, pos: *usize) ParseError![]module.TableType {
|
||||||
|
const count = try readULEB128(u32, bytes, pos);
|
||||||
|
const tables = try ally.alloc(module.TableType, count);
|
||||||
|
for (tables) |*t| {
|
||||||
|
const elem_type = try readByte(bytes, pos);
|
||||||
|
const lim = try parseLimits(bytes, pos);
|
||||||
|
t.* = .{ .elem_type = elem_type, .min = lim.min, .max = lim.max };
|
||||||
|
}
|
||||||
|
return tables;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseMemorySection(ally: std.mem.Allocator, bytes: []const u8, pos: *usize) ParseError![]module.MemoryType {
|
||||||
|
const count = try readULEB128(u32, bytes, pos);
|
||||||
|
const mems = try ally.alloc(module.MemoryType, count);
|
||||||
|
for (mems) |*m| {
|
||||||
|
const lim = try parseLimits(bytes, pos);
|
||||||
|
m.* = .{ .min = lim.min, .max = lim.max };
|
||||||
|
}
|
||||||
|
return mems;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseGlobalSection(ally: std.mem.Allocator, bytes: []const u8, pos: *usize) ParseError![]module.GlobalDef {
|
||||||
|
const count = try readULEB128(u32, bytes, pos);
|
||||||
|
const globals = try ally.alloc(module.GlobalDef, count);
|
||||||
|
for (globals) |*g| {
|
||||||
|
const vt = try readValType(bytes, pos);
|
||||||
|
const mut = try readByte(bytes, pos);
|
||||||
|
if (mut > 1) return ParseError.InvalidMutability;
|
||||||
|
const init = try readConstExpr(bytes, pos);
|
||||||
|
g.* = .{ .type = .{ .valtype = vt, .mutable = mut == 1 }, .init = init };
|
||||||
|
}
|
||||||
|
return globals;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseExportSection(ally: std.mem.Allocator, bytes: []const u8, pos: *usize) ParseError![]module.Export {
|
||||||
|
const count = try readULEB128(u32, bytes, pos);
|
||||||
|
const exports = try ally.alloc(module.Export, count);
|
||||||
|
errdefer ally.free(exports);
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < count) : (i += 1) {
|
||||||
|
const name_len = try readULEB128(u32, bytes, pos);
|
||||||
|
const name_bytes = try readBytes(bytes, pos, name_len);
|
||||||
|
const name = try ally.dupe(u8, name_bytes);
|
||||||
|
errdefer ally.free(name);
|
||||||
|
const kind = try readByte(bytes, pos);
|
||||||
|
const idx = try readULEB128(u32, bytes, pos);
|
||||||
|
const desc: module.ExportDesc = switch (kind) {
|
||||||
|
0 => .{ .func = idx },
|
||||||
|
1 => .{ .table = idx },
|
||||||
|
2 => .{ .memory = idx },
|
||||||
|
3 => .{ .global = idx },
|
||||||
|
else => return ParseError.InvalidExportDesc,
|
||||||
|
};
|
||||||
|
exports[i] = .{ .name = name, .desc = desc };
|
||||||
|
}
|
||||||
|
return exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseElementSection(ally: std.mem.Allocator, bytes: []const u8, pos: *usize) ParseError![]module.ElementSegment {
|
||||||
|
const count = try readULEB128(u32, bytes, pos);
|
||||||
|
const elems = try ally.alloc(module.ElementSegment, count);
|
||||||
|
errdefer ally.free(elems);
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < count) : (i += 1) {
|
||||||
|
const table_idx = try readULEB128(u32, bytes, pos);
|
||||||
|
const offset = try readConstExpr(bytes, pos);
|
||||||
|
const num_funcs = try readULEB128(u32, bytes, pos);
|
||||||
|
const func_indices = try ally.alloc(u32, num_funcs);
|
||||||
|
errdefer ally.free(func_indices);
|
||||||
|
for (func_indices) |*f| f.* = try readULEB128(u32, bytes, pos);
|
||||||
|
elems[i] = .{ .table_idx = table_idx, .offset = offset, .func_indices = func_indices };
|
||||||
|
}
|
||||||
|
return elems;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseCodeSection(ally: std.mem.Allocator, bytes: []const u8, pos: *usize) ParseError![]module.FunctionBody {
|
||||||
|
const count = try readULEB128(u32, bytes, pos);
|
||||||
|
const bodies = try ally.alloc(module.FunctionBody, count);
|
||||||
|
errdefer ally.free(bodies);
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < count) : (i += 1) {
|
||||||
|
const body_size = try readULEB128(u32, bytes, pos);
|
||||||
|
const body_start = pos.*;
|
||||||
|
const local_count = try readULEB128(u32, bytes, pos);
|
||||||
|
const locals = try ally.alloc(module.LocalDecl, local_count);
|
||||||
|
errdefer ally.free(locals);
|
||||||
|
for (locals) |*l| {
|
||||||
|
const n = try readULEB128(u32, bytes, pos);
|
||||||
|
const vt = try readValType(bytes, pos);
|
||||||
|
l.* = .{ .count = n, .valtype = vt };
|
||||||
|
}
|
||||||
|
const code_start = pos.*;
|
||||||
|
const code_end = body_start + body_size;
|
||||||
|
if (code_end > bytes.len) return ParseError.UnexpectedEof;
|
||||||
|
const code = bytes[code_start..code_end];
|
||||||
|
pos.* = code_end;
|
||||||
|
bodies[i] = .{ .locals = locals, .code = code };
|
||||||
|
}
|
||||||
|
return bodies;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseDataSection(ally: std.mem.Allocator, bytes: []const u8, pos: *usize) ParseError![]module.DataSegment {
|
||||||
|
const count = try readULEB128(u32, bytes, pos);
|
||||||
|
const datas = try ally.alloc(module.DataSegment, count);
|
||||||
|
for (datas) |*d| {
|
||||||
|
const kind = try readULEB128(u32, bytes, pos);
|
||||||
|
switch (kind) {
|
||||||
|
0 => {
|
||||||
|
const offset = try readConstExpr(bytes, pos);
|
||||||
|
const data_len = try readULEB128(u32, bytes, pos);
|
||||||
|
const data_bytes = try readBytes(bytes, pos, data_len);
|
||||||
|
d.* = .{
|
||||||
|
.kind = .active,
|
||||||
|
.memory_idx = 0,
|
||||||
|
.offset = offset,
|
||||||
|
.bytes = data_bytes,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
1 => {
|
||||||
|
const data_len = try readULEB128(u32, bytes, pos);
|
||||||
|
const data_bytes = try readBytes(bytes, pos, data_len);
|
||||||
|
d.* = .{
|
||||||
|
.kind = .passive,
|
||||||
|
.memory_idx = 0,
|
||||||
|
.offset = null,
|
||||||
|
.bytes = data_bytes,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
2 => {
|
||||||
|
const mem_idx = try readULEB128(u32, bytes, pos);
|
||||||
|
const offset = try readConstExpr(bytes, pos);
|
||||||
|
const data_len = try readULEB128(u32, bytes, pos);
|
||||||
|
const data_bytes = try readBytes(bytes, pos, data_len);
|
||||||
|
d.* = .{
|
||||||
|
.kind = .active,
|
||||||
|
.memory_idx = mem_idx,
|
||||||
|
.offset = offset,
|
||||||
|
.bytes = data_bytes,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
else => return ParseError.InvalidDataSegment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return datas;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sectionIdFromByte(b: u8) ?module.SectionId {
|
||||||
|
return switch (b) {
|
||||||
|
0 => .custom,
|
||||||
|
1 => .type,
|
||||||
|
2 => .import,
|
||||||
|
3 => .function,
|
||||||
|
4 => .table,
|
||||||
|
5 => .memory,
|
||||||
|
6 => .global,
|
||||||
|
7 => .@"export",
|
||||||
|
8 => .start,
|
||||||
|
9 => .element,
|
||||||
|
10 => .code,
|
||||||
|
11 => .data,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(ally: std.mem.Allocator, bytes: []const u8) ParseError!module.Module {
|
||||||
|
var pos: usize = 0;
|
||||||
|
|
||||||
|
// Validate magic + version
|
||||||
|
const magic = try readBytes(bytes, &pos, 4);
|
||||||
|
if (!std.mem.eql(u8, magic, "\x00asm")) return ParseError.InvalidMagic;
|
||||||
|
const ver = try readBytes(bytes, &pos, 4);
|
||||||
|
if (!std.mem.eql(u8, ver, "\x01\x00\x00\x00")) return ParseError.InvalidVersion;
|
||||||
|
|
||||||
|
var mod = module.Module{
|
||||||
|
.types = &.{},
|
||||||
|
.imports = &.{},
|
||||||
|
.functions = &.{},
|
||||||
|
.tables = &.{},
|
||||||
|
.memories = &.{},
|
||||||
|
.globals = &.{},
|
||||||
|
.exports = &.{},
|
||||||
|
.start = null,
|
||||||
|
.elements = &.{},
|
||||||
|
.codes = &.{},
|
||||||
|
.datas = &.{},
|
||||||
|
.allocator = ally,
|
||||||
|
};
|
||||||
|
errdefer mod.deinit();
|
||||||
|
|
||||||
|
while (pos < bytes.len) {
|
||||||
|
const section_id_byte = try readByte(bytes, &pos);
|
||||||
|
const section_size = try readULEB128(u32, bytes, &pos);
|
||||||
|
const section_end = pos + section_size;
|
||||||
|
if (section_end > bytes.len) return ParseError.UnexpectedEof;
|
||||||
|
|
||||||
|
const section_id = sectionIdFromByte(section_id_byte) orelse {
|
||||||
|
pos = section_end;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (section_id) {
|
||||||
|
.custom => pos = section_end, // skip
|
||||||
|
.type => mod.types = try parseTypeSection(ally, bytes, &pos),
|
||||||
|
.import => mod.imports = try parseImportSection(ally, bytes, &pos),
|
||||||
|
.function => mod.functions = try parseFunctionSection(ally, bytes, &pos),
|
||||||
|
.table => mod.tables = try parseTableSection(ally, bytes, &pos),
|
||||||
|
.memory => mod.memories = try parseMemorySection(ally, bytes, &pos),
|
||||||
|
.global => mod.globals = try parseGlobalSection(ally, bytes, &pos),
|
||||||
|
.@"export" => mod.exports = try parseExportSection(ally, bytes, &pos),
|
||||||
|
.start => mod.start = try readULEB128(u32, bytes, &pos),
|
||||||
|
.element => mod.elements = try parseElementSection(ally, bytes, &pos),
|
||||||
|
.code => mod.codes = try parseCodeSection(ally, bytes, &pos),
|
||||||
|
.data => mod.datas = try parseDataSection(ally, bytes, &pos),
|
||||||
|
}
|
||||||
|
// Ensure we consumed exactly section_size bytes
|
||||||
|
pos = section_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test "parse minimal wasm module" {
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
// Minimal valid wasm: magic + version only
|
||||||
|
const bytes = "\x00asm\x01\x00\x00\x00";
|
||||||
|
var mod = try parse(ally, bytes);
|
||||||
|
defer mod.deinit();
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), mod.types.len);
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), mod.functions.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "invalid magic rejected" {
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
const bytes = "\x00BAD\x01\x00\x00\x00";
|
||||||
|
try std.testing.expectError(ParseError.InvalidMagic, parse(ally, bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "invalid version rejected" {
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
const bytes = "\x00asm\x02\x00\x00\x00";
|
||||||
|
try std.testing.expectError(ParseError.InvalidVersion, parse(ally, bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "readULEB128 basic" {
|
||||||
|
const bytes = [_]u8{ 0xE5, 0x8E, 0x26 }; // 624485
|
||||||
|
var pos: usize = 0;
|
||||||
|
const val = try readULEB128(u32, &bytes, &pos);
|
||||||
|
try std.testing.expectEqual(@as(u32, 624485), val);
|
||||||
|
try std.testing.expectEqual(@as(usize, 3), pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "readSLEB128 negative" {
|
||||||
|
const bytes = [_]u8{ 0x7E }; // -2 in SLEB128
|
||||||
|
var pos: usize = 0;
|
||||||
|
const val = try readSLEB128(i32, &bytes, &pos);
|
||||||
|
try std.testing.expectEqual(@as(i32, -2), val);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse fib module" {
|
||||||
|
// Bytes from tests/wasm/fib.wasm (fib(i32)->i32, recursive)
|
||||||
|
const wasm_bytes = [_]u8{
|
||||||
|
0x00, 0x61, 0x73, 0x6d, // magic
|
||||||
|
0x01, 0x00, 0x00, 0x00, // version
|
||||||
|
// type section: 1 type -> (i32) -> (i32)
|
||||||
|
0x01, 0x06, 0x01, 0x60, 0x01, 0x7f, 0x01, 0x7f,
|
||||||
|
// function section: 1 func, type 0
|
||||||
|
0x03, 0x02, 0x01, 0x00,
|
||||||
|
// export section: "fib" -> func 0
|
||||||
|
0x07, 0x07, 0x01, 0x03, 0x66, 0x69, 0x62, 0x00, 0x00,
|
||||||
|
// code section
|
||||||
|
0x0a, 0x1e, 0x01, 0x1c, 0x00, 0x20, 0x00, 0x41, 0x02, 0x48, 0x04,
|
||||||
|
0x7f, 0x20, 0x00, 0x05, 0x20, 0x00, 0x41, 0x01, 0x6b, 0x10, 0x00,
|
||||||
|
0x20, 0x00, 0x41, 0x02, 0x6b, 0x10, 0x00, 0x6a, 0x0b, 0x0b,
|
||||||
|
};
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mod = try parse(ally, &wasm_bytes);
|
||||||
|
defer mod.deinit();
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), mod.types.len);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), mod.functions.len);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), mod.exports.len);
|
||||||
|
try std.testing.expectEqualStrings("fib", mod.exports[0].name);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), mod.codes.len);
|
||||||
|
// Verify type: (i32) -> (i32)
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), mod.types[0].params.len);
|
||||||
|
try std.testing.expectEqual(module.ValType.i32, mod.types[0].params[0]);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), mod.types[0].results.len);
|
||||||
|
try std.testing.expectEqual(module.ValType.i32, mod.types[0].results[0]);
|
||||||
|
}
|
||||||
316
src/wasm/host.zig
Normal file
316
src/wasm/host.zig
Normal file
|
|
@ -0,0 +1,316 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const module = @import("module.zig");
|
||||||
|
const runtime = @import("runtime.zig");
|
||||||
|
|
||||||
|
pub const Value = runtime.Value;
|
||||||
|
pub const FuncType = module.FuncType;
|
||||||
|
pub const ValType = module.ValType;
|
||||||
|
pub const Memory = runtime.Memory;
|
||||||
|
|
||||||
|
pub const HostFunc = struct {
|
||||||
|
name: []const u8,
|
||||||
|
module: []const u8,
|
||||||
|
type_: FuncType,
|
||||||
|
userdata: ?*anyopaque,
|
||||||
|
invoke: *const fn (
|
||||||
|
instance_ptr: *anyopaque,
|
||||||
|
args: []const Value,
|
||||||
|
results: []Value,
|
||||||
|
userdata: ?*anyopaque,
|
||||||
|
) anyerror!void,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ImportSet = struct {
|
||||||
|
functions: std.ArrayList(HostFunc),
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator) ImportSet {
|
||||||
|
return .{
|
||||||
|
.functions = .empty,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *ImportSet) void {
|
||||||
|
for (self.functions.items) |f| {
|
||||||
|
self.allocator.free(f.name);
|
||||||
|
self.allocator.free(f.module);
|
||||||
|
self.allocator.free(f.type_.params);
|
||||||
|
self.allocator.free(f.type_.results);
|
||||||
|
}
|
||||||
|
self.functions.deinit(self.allocator);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addFunc(
|
||||||
|
self: *ImportSet,
|
||||||
|
comptime module_name: @EnumLiteral(),
|
||||||
|
comptime func_name: @EnumLiteral(),
|
||||||
|
comptime func: anytype,
|
||||||
|
) !void {
|
||||||
|
const Info = @typeInfo(@TypeOf(func));
|
||||||
|
if (Info != .@"fn") @compileError("addFunc expects a function");
|
||||||
|
const fn_info = Info.@"fn";
|
||||||
|
const has_instance_param = comptime hasInstanceParam(fn_info);
|
||||||
|
const wasm_param_start: usize = if (has_instance_param) 1 else 0;
|
||||||
|
|
||||||
|
const param_len = fn_info.params.len - wasm_param_start;
|
||||||
|
const params = try self.allocator.alloc(ValType, param_len);
|
||||||
|
errdefer self.allocator.free(params);
|
||||||
|
inline for (fn_info.params[wasm_param_start..], 0..) |p, i| {
|
||||||
|
const ty = p.type orelse @compileError("function parameter type must be known");
|
||||||
|
params[i] = comptime zigTypeToValType(ty);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result_len: usize = if (fn_info.return_type) |rt|
|
||||||
|
if (rt == void or (comptime isErrorUnionVoid(rt))) 0 else 1
|
||||||
|
else
|
||||||
|
0;
|
||||||
|
const results = try self.allocator.alloc(ValType, result_len);
|
||||||
|
errdefer self.allocator.free(results);
|
||||||
|
if (result_len == 1) {
|
||||||
|
const rt = resultInnerType(fn_info.return_type.?);
|
||||||
|
results[0] = comptime zigTypeToValType(rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
const F = struct {
|
||||||
|
fn invoke(instance_ptr: *anyopaque, args: []const Value, out_results: []Value, _: ?*anyopaque) anyerror!void {
|
||||||
|
if (args.len != param_len) return error.InvalidArgumentCount;
|
||||||
|
if (out_results.len != result_len) return error.InvalidResultCount;
|
||||||
|
|
||||||
|
const call_args = try buildCallArgs(instance_ptr, args);
|
||||||
|
const ret = @call(.auto, func, call_args);
|
||||||
|
if (result_len == 1) {
|
||||||
|
const val = try packReturnValue(ret);
|
||||||
|
out_results[0] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buildCallArgs(instance_ptr: *anyopaque, args: []const Value) !std.meta.ArgsTuple(@TypeOf(func)) {
|
||||||
|
var tuple: std.meta.ArgsTuple(@TypeOf(func)) = undefined;
|
||||||
|
if (has_instance_param) {
|
||||||
|
const first_ty = fn_info.params[0].type orelse unreachable;
|
||||||
|
tuple[0] = castInstancePtr(first_ty, instance_ptr);
|
||||||
|
}
|
||||||
|
inline for (fn_info.params[wasm_param_start..], 0..) |p, i| {
|
||||||
|
const ty = p.type orelse unreachable;
|
||||||
|
tuple[wasm_param_start + i] = try valueAs(ty, args[i]);
|
||||||
|
}
|
||||||
|
return tuple;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn packReturnValue(ret: anytype) !Value {
|
||||||
|
const RetT = @TypeOf(ret);
|
||||||
|
if (comptime @typeInfo(RetT) == .error_union) {
|
||||||
|
const payload = try ret;
|
||||||
|
return fromZigValue(payload);
|
||||||
|
}
|
||||||
|
return fromZigValue(ret);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try self.addFuncRaw(
|
||||||
|
@tagName(module_name),
|
||||||
|
@tagName(func_name),
|
||||||
|
.{ .params = params, .results = results },
|
||||||
|
null,
|
||||||
|
F.invoke,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addFuncRaw(
|
||||||
|
self: *ImportSet,
|
||||||
|
module_name: []const u8,
|
||||||
|
func_name: []const u8,
|
||||||
|
type_: FuncType,
|
||||||
|
userdata: ?*anyopaque,
|
||||||
|
invoke: *const fn (*anyopaque, []const Value, []Value, ?*anyopaque) anyerror!void,
|
||||||
|
) !void {
|
||||||
|
const module_copy = try self.allocator.dupe(u8, module_name);
|
||||||
|
errdefer self.allocator.free(module_copy);
|
||||||
|
const name_copy = try self.allocator.dupe(u8, func_name);
|
||||||
|
errdefer self.allocator.free(name_copy);
|
||||||
|
|
||||||
|
try self.functions.append(self.allocator, .{
|
||||||
|
.name = name_copy,
|
||||||
|
.module = module_copy,
|
||||||
|
.type_ = type_,
|
||||||
|
.userdata = userdata,
|
||||||
|
.invoke = invoke,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn findFunc(self: *const ImportSet, module_name: []const u8, func_name: []const u8) ?*const HostFunc {
|
||||||
|
for (self.functions.items) |*f| {
|
||||||
|
if (std.mem.eql(u8, f.module, module_name) and std.mem.eql(u8, f.name, func_name)) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn readStruct(
|
||||||
|
comptime T: type,
|
||||||
|
memory: *const Memory,
|
||||||
|
offset: u32,
|
||||||
|
) !T {
|
||||||
|
comptime ensureAbiStruct(T);
|
||||||
|
if (@as(usize, offset) + @sizeOf(T) > memory.bytes.len) return error.OutOfBounds;
|
||||||
|
var out: T = undefined;
|
||||||
|
@memcpy(std.mem.asBytes(&out), memory.bytes[offset..][0..@sizeOf(T)]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeStruct(
|
||||||
|
comptime T: type,
|
||||||
|
memory: *Memory,
|
||||||
|
offset: u32,
|
||||||
|
value: T,
|
||||||
|
) !void {
|
||||||
|
comptime ensureAbiStruct(T);
|
||||||
|
if (@as(usize, offset) + @sizeOf(T) > memory.bytes.len) return error.OutOfBounds;
|
||||||
|
@memcpy(memory.bytes[offset..][0..@sizeOf(T)], std.mem.asBytes(&value));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensureAbiStruct(comptime T: type) void {
|
||||||
|
const info = @typeInfo(T);
|
||||||
|
if (info != .@"struct" or info.@"struct".layout != .@"extern") {
|
||||||
|
@compileError("T must be an extern struct");
|
||||||
|
}
|
||||||
|
inline for (info.@"struct".fields) |f| {
|
||||||
|
switch (f.type) {
|
||||||
|
i32, i64, f32, f64 => {},
|
||||||
|
else => @compileError("extern struct fields must be i32/i64/f32/f64"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn zigTypeToValType(comptime T: type) ValType {
|
||||||
|
return switch (T) {
|
||||||
|
i32 => .i32,
|
||||||
|
i64 => .i64,
|
||||||
|
f32 => .f32,
|
||||||
|
f64 => .f64,
|
||||||
|
else => @compileError("unsupported host function type; expected i32/i64/f32/f64"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasInstanceParam(comptime fn_info: std.builtin.Type.Fn) bool {
|
||||||
|
if (fn_info.params.len == 0) return false;
|
||||||
|
const first_ty = fn_info.params[0].type orelse return false;
|
||||||
|
return isInstanceParamType(first_ty);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isInstanceParamType(comptime T: type) bool {
|
||||||
|
const ti = @typeInfo(T);
|
||||||
|
if (ti != .pointer) return false;
|
||||||
|
return ti.pointer.child == anyopaque and ti.pointer.size == .one;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn castInstancePtr(comptime T: type, p: *anyopaque) T {
|
||||||
|
return @ptrCast(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isErrorUnionVoid(comptime T: type) bool {
|
||||||
|
const info = @typeInfo(T);
|
||||||
|
return info == .error_union and info.error_union.payload == void;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resultInnerType(comptime T: type) type {
|
||||||
|
return switch (@typeInfo(T)) {
|
||||||
|
.error_union => |eu| eu.payload,
|
||||||
|
else => T,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valueAs(comptime T: type, v: Value) !T {
|
||||||
|
return switch (T) {
|
||||||
|
i32 => switch (v) { .i32 => |x| x, else => error.TypeMismatch },
|
||||||
|
i64 => switch (v) { .i64 => |x| x, else => error.TypeMismatch },
|
||||||
|
f32 => switch (v) { .f32 => |x| x, else => error.TypeMismatch },
|
||||||
|
f64 => switch (v) { .f64 => |x| x, else => error.TypeMismatch },
|
||||||
|
else => @compileError("unsupported host function type; expected i32/i64/f32/f64"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fromZigValue(v: anytype) Value {
|
||||||
|
return switch (@TypeOf(v)) {
|
||||||
|
i32 => .{ .i32 = v },
|
||||||
|
i64 => .{ .i64 = v },
|
||||||
|
f32 => .{ .f32 = v },
|
||||||
|
f64 => .{ .f64 = v },
|
||||||
|
else => @compileError("unsupported host function return type; expected i32/i64/f32/f64"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test "addFuncRaw and findFunc" {
|
||||||
|
const Invoke = struct {
|
||||||
|
fn f(_: *anyopaque, args: []const Value, results: []Value, _: ?*anyopaque) !void {
|
||||||
|
results[0] = .{ .i32 = args[0].i32 + args[1].i32 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var imports = ImportSet.init(ally);
|
||||||
|
defer imports.deinit();
|
||||||
|
|
||||||
|
const params = try ally.alloc(ValType, 2);
|
||||||
|
const results = try ally.alloc(ValType, 1);
|
||||||
|
params[0] = .i32;
|
||||||
|
params[1] = .i32;
|
||||||
|
results[0] = .i32;
|
||||||
|
|
||||||
|
try imports.addFuncRaw("env", "add", .{ .params = params, .results = results }, null, Invoke.f);
|
||||||
|
const found = imports.findFunc("env", "add") orelse return error.TestUnexpectedResult;
|
||||||
|
var out = [_]Value{.{ .i32 = 0 }};
|
||||||
|
try found.invoke(@ptrFromInt(1), &.{ .{ .i32 = 2 }, .{ .i32 = 3 } }, &out, null);
|
||||||
|
try std.testing.expectEqual(@as(i32, 5), out[0].i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "addFunc typed wrapper" {
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var imports = ImportSet.init(ally);
|
||||||
|
defer imports.deinit();
|
||||||
|
|
||||||
|
try imports.addFunc(.env, .mul, struct {
|
||||||
|
fn mul(a: i32, b: i32) i32 {
|
||||||
|
return a * b;
|
||||||
|
}
|
||||||
|
}.mul);
|
||||||
|
|
||||||
|
const found = imports.findFunc("env", "mul") orelse return error.TestUnexpectedResult;
|
||||||
|
var out = [_]Value{.{ .i32 = 0 }};
|
||||||
|
try found.invoke(@ptrFromInt(1), &.{ .{ .i32 = 6 }, .{ .i32 = 7 } }, &out, null);
|
||||||
|
try std.testing.expectEqual(@as(i32, 42), out[0].i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "addFunc typed wrapper with instance context parameter" {
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var imports = ImportSet.init(ally);
|
||||||
|
defer imports.deinit();
|
||||||
|
|
||||||
|
try imports.addFunc(.env, .ctx_add, struct {
|
||||||
|
fn ctx_add(instance_ptr: *anyopaque, a: i32, b: i32) i32 {
|
||||||
|
_ = instance_ptr;
|
||||||
|
return a + b + 1;
|
||||||
|
}
|
||||||
|
}.ctx_add);
|
||||||
|
|
||||||
|
const found = imports.findFunc("env", "ctx_add") orelse return error.TestUnexpectedResult;
|
||||||
|
var out = [_]Value{.{ .i32 = 0 }};
|
||||||
|
try found.invoke(@ptrFromInt(1234), &.{ .{ .i32 = 5 }, .{ .i32 = 6 } }, &out, null);
|
||||||
|
try std.testing.expectEqual(@as(i32, 12), out[0].i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "readStruct/writeStruct round trip" {
|
||||||
|
const Vec2 = extern struct { x: f32, y: f32 };
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mem = try Memory.init(ally, 1, null);
|
||||||
|
defer mem.deinit(ally);
|
||||||
|
|
||||||
|
try writeStruct(Vec2, &mem, 12, .{ .x = 1.5, .y = -3.0 });
|
||||||
|
const v = try readStruct(Vec2, &mem, 12);
|
||||||
|
try std.testing.expectApproxEqAbs(@as(f32, 1.5), v.x, 0.0001);
|
||||||
|
try std.testing.expectApproxEqAbs(@as(f32, -3.0), v.y, 0.0001);
|
||||||
|
}
|
||||||
2310
src/wasm/instance.zig
Normal file
2310
src/wasm/instance.zig
Normal file
File diff suppressed because it is too large
Load diff
1383
src/wasm/jit/aarch64.zig
Normal file
1383
src/wasm/jit/aarch64.zig
Normal file
File diff suppressed because it is too large
Load diff
189
src/wasm/jit/codebuf.zig
Normal file
189
src/wasm/jit/codebuf.zig
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
const is_apple_silicon = builtin.os.tag == .macos and builtin.cpu.arch == .aarch64;
|
||||||
|
const is_macos = builtin.os.tag == .macos;
|
||||||
|
const is_aarch64 = builtin.cpu.arch == .aarch64;
|
||||||
|
|
||||||
|
// Apple Silicon JIT: MAP_JIT is mandatory.
|
||||||
|
// Host binary must have entitlement: com.apple.security.cs.allow-jit
|
||||||
|
const MAP_JIT: u32 = 0x0800; // Darwin-specific
|
||||||
|
|
||||||
|
// Apple Silicon platform APIs
|
||||||
|
const AppleSiliconJIT = if (is_apple_silicon) struct {
|
||||||
|
pub extern fn pthread_jit_write_protect_np(enabled: c_int) void;
|
||||||
|
pub extern fn sys_icache_invalidate(start: *anyopaque, len: usize) void;
|
||||||
|
} else struct {
|
||||||
|
pub inline fn pthread_jit_write_protect_np(enabled: c_int) void { _ = enabled; }
|
||||||
|
pub inline fn sys_icache_invalidate(start: *anyopaque, len: usize) void { _ = start; _ = len; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Linux AArch64 cache flush via compiler-rt
|
||||||
|
const LinuxAArch64 = if (!is_apple_silicon and is_aarch64) struct {
|
||||||
|
pub extern fn __clear_cache(start: *anyopaque, end: *anyopaque) void;
|
||||||
|
} else struct {
|
||||||
|
pub inline fn __clear_cache(start: *anyopaque, end: *anyopaque) void { _ = start; _ = end; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// PROT_READ | PROT_WRITE
|
||||||
|
const prot_rw = std.posix.PROT{ .READ = true, .WRITE = true };
|
||||||
|
// PROT_READ | PROT_EXEC
|
||||||
|
const prot_rx = std.posix.PROT{ .READ = true, .EXEC = true };
|
||||||
|
|
||||||
|
pub fn flushICache(ptr: [*]u8, len: usize) void {
|
||||||
|
if (!is_aarch64) return;
|
||||||
|
if (is_apple_silicon) {
|
||||||
|
AppleSiliconJIT.sys_icache_invalidate(ptr, len);
|
||||||
|
} else {
|
||||||
|
LinuxAArch64.__clear_cache(ptr, ptr + len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CodeBuffer = struct {
|
||||||
|
buf: []align(std.heap.page_size_min) u8,
|
||||||
|
pos: usize,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, capacity: usize) !CodeBuffer {
|
||||||
|
_ = allocator;
|
||||||
|
const aligned_cap = std.mem.alignForward(usize, capacity, std.heap.page_size_min);
|
||||||
|
// Plain mmap RW, then mprotect to RX in finalize().
|
||||||
|
// MAP_JIT + pthread_jit_write_protect_np requires com.apple.security.cs.allow-jit
|
||||||
|
// entitlement (hardened runtime); ad-hoc signed test binaries do not have it.
|
||||||
|
// Plain mprotect works for non-hardened binaries on all platforms.
|
||||||
|
const buf = if (is_macos) blk: {
|
||||||
|
// Darwin MAP_PRIVATE | MAP_ANONYMOUS (no MAP_JIT)
|
||||||
|
const MAP_PRIVATE: u32 = 0x0002;
|
||||||
|
const MAP_ANONYMOUS: u32 = 0x1000;
|
||||||
|
const flags: u32 = MAP_PRIVATE | MAP_ANONYMOUS;
|
||||||
|
const slice = try std.posix.mmap(null, aligned_cap, prot_rw, @bitCast(flags), -1, 0);
|
||||||
|
break :blk slice;
|
||||||
|
} else blk: {
|
||||||
|
const slice = try std.posix.mmap(
|
||||||
|
null,
|
||||||
|
aligned_cap,
|
||||||
|
prot_rw,
|
||||||
|
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
|
||||||
|
-1,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
break :blk slice;
|
||||||
|
};
|
||||||
|
return .{ .buf = buf, .pos = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *CodeBuffer) void {
|
||||||
|
std.posix.munmap(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn emit1(self: *CodeBuffer, byte: u8) void {
|
||||||
|
std.debug.assert(self.pos < self.buf.len);
|
||||||
|
self.buf[self.pos] = byte;
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn emitSlice(self: *CodeBuffer, bytes: []const u8) void {
|
||||||
|
std.debug.assert(self.pos + bytes.len <= self.buf.len);
|
||||||
|
@memcpy(self.buf[self.pos..][0..bytes.len], bytes);
|
||||||
|
self.pos += bytes.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn emitU32Le(self: *CodeBuffer, v: u32) void {
|
||||||
|
std.debug.assert(self.pos + 4 <= self.buf.len);
|
||||||
|
std.mem.writeInt(u32, self.buf[self.pos..][0..4], v, .little);
|
||||||
|
self.pos += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn emitI32Le(self: *CodeBuffer, v: i32) void {
|
||||||
|
self.emitU32Le(@bitCast(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor(self: *const CodeBuffer) usize {
|
||||||
|
return self.pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn patchI32(self: *CodeBuffer, pos: usize, value: i32) void {
|
||||||
|
std.mem.writeInt(i32, self.buf[pos..][0..4], value, .little);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn patchU32(self: *CodeBuffer, pos: usize, value: u32) void {
|
||||||
|
std.mem.writeInt(u32, self.buf[pos..][0..4], value, .little);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apple Silicon: switch to RW mode for patching after finalize.
|
||||||
|
pub fn beginWrite(self: *CodeBuffer) void {
|
||||||
|
_ = self;
|
||||||
|
if (is_apple_silicon) AppleSiliconJIT.pthread_jit_write_protect_np(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apple Silicon: switch to RX mode and flush I-cache.
|
||||||
|
pub fn endWrite(self: *CodeBuffer) !void {
|
||||||
|
if (is_apple_silicon) {
|
||||||
|
AppleSiliconJIT.pthread_jit_write_protect_np(1);
|
||||||
|
}
|
||||||
|
if (is_aarch64) flushICache(self.buf.ptr, self.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make the buffer executable. Must be called before executing any code.
|
||||||
|
pub fn finalize(self: *CodeBuffer) !void {
|
||||||
|
const rc = std.c.mprotect(
|
||||||
|
@alignCast(@ptrCast(self.buf.ptr)),
|
||||||
|
self.buf.len,
|
||||||
|
prot_rx,
|
||||||
|
);
|
||||||
|
if (rc != 0) return error.MProtectFailed;
|
||||||
|
if (is_aarch64) flushICache(self.buf.ptr, self.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn funcPtr(self: *const CodeBuffer, comptime Fn: type, offset: usize) *const Fn {
|
||||||
|
return @ptrFromInt(@intFromPtr(self.buf.ptr) + offset);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test "codebuf emit and finalize" {
|
||||||
|
var buf = try CodeBuffer.init(std.testing.allocator, 4096);
|
||||||
|
defer buf.deinit();
|
||||||
|
|
||||||
|
if (builtin.cpu.arch == .x86_64) {
|
||||||
|
// mov eax, 42; ret
|
||||||
|
buf.emitSlice(&.{ 0xB8, 42, 0, 0, 0 });
|
||||||
|
buf.emit1(0xC3);
|
||||||
|
try buf.finalize();
|
||||||
|
const fn_ptr = buf.funcPtr(fn () callconv(.c) i32, 0);
|
||||||
|
const result = fn_ptr();
|
||||||
|
try std.testing.expectEqual(@as(i32, 42), result);
|
||||||
|
} else if (builtin.cpu.arch == .aarch64) {
|
||||||
|
// movz w0, #42; ret
|
||||||
|
// MOVZ W0, #42 = 0x52800540 (little-endian bytes: 0x40 0x05 0x80 0x52)
|
||||||
|
buf.emitU32Le(0x52800540);
|
||||||
|
buf.emitU32Le(0xD65F03C0); // ret
|
||||||
|
// Verify the bytes are correct
|
||||||
|
try std.testing.expectEqual(@as(u8, 0x40), buf.buf[0]);
|
||||||
|
try std.testing.expectEqual(@as(u8, 0x05), buf.buf[1]);
|
||||||
|
try std.testing.expectEqual(@as(u8, 0x80), buf.buf[2]);
|
||||||
|
try std.testing.expectEqual(@as(u8, 0x52), buf.buf[3]);
|
||||||
|
// Finalize (needed for the W^X transition on Apple Silicon)
|
||||||
|
try buf.finalize();
|
||||||
|
// Execute: requires com.apple.security.cs.allow-jit entitlement on Apple Silicon.
|
||||||
|
// Zig test binaries on Apple Silicon are signed with this entitlement by default.
|
||||||
|
const fn_ptr = buf.funcPtr(fn () callconv(.c) i32, 0);
|
||||||
|
const result = fn_ptr();
|
||||||
|
try std.testing.expectEqual(@as(i32, 42), result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "codebuf cursor and patch" {
|
||||||
|
var buf = try CodeBuffer.init(std.testing.allocator, 4096);
|
||||||
|
defer buf.deinit();
|
||||||
|
buf.emitU32Le(0xDEADBEEF);
|
||||||
|
const patch_pos = buf.cursor();
|
||||||
|
buf.emitU32Le(0x00000000);
|
||||||
|
buf.emitU32Le(0xCAFEBABE);
|
||||||
|
buf.patchU32(patch_pos, 0x12345678);
|
||||||
|
try std.testing.expectEqual(
|
||||||
|
@as(u32, 0x12345678),
|
||||||
|
std.mem.readInt(u32, buf.buf[patch_pos..][0..4], .little),
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/wasm/jit/codegen.zig
Normal file
24
src/wasm/jit/codegen.zig
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const module = @import("../module.zig");
|
||||||
|
const aarch64 = @import("aarch64.zig");
|
||||||
|
const x86_64 = @import("x86_64.zig");
|
||||||
|
|
||||||
|
pub const JitResult = aarch64.JitResult;
|
||||||
|
pub const HelperAddrs = aarch64.HelperAddrs;
|
||||||
|
|
||||||
|
pub fn compileSimpleI32(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
mod: *const module.Module,
|
||||||
|
num_imported_funcs: u32,
|
||||||
|
current_func_idx: u32,
|
||||||
|
body: *const module.FunctionBody,
|
||||||
|
ft: *const module.FuncType,
|
||||||
|
helpers: HelperAddrs,
|
||||||
|
) !?JitResult {
|
||||||
|
return switch (builtin.cpu.arch) {
|
||||||
|
.aarch64 => try aarch64.compileFunctionI32(allocator, mod, num_imported_funcs, current_func_idx, body, ft, helpers),
|
||||||
|
.x86_64 => try x86_64.compileFunctionI32(allocator, mod, num_imported_funcs, current_func_idx, body, ft, helpers),
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
75
src/wasm/jit/liveness.zig
Normal file
75
src/wasm/jit/liveness.zig
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
/// Phase 4.2 — Liveness: compute live ranges for each virtual register.
|
||||||
|
const std = @import("std");
|
||||||
|
const module = @import("../module.zig");
|
||||||
|
const stackify = @import("stackify.zig");
|
||||||
|
|
||||||
|
pub const LiveRange = struct {
|
||||||
|
vreg: stackify.VReg,
|
||||||
|
valtype: module.ValType,
|
||||||
|
start: u32, // instruction index of definition
|
||||||
|
end: u32, // instruction index of last use
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn computeLiveRanges(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
instrs: []const stackify.AnnotatedInstr,
|
||||||
|
) ![]LiveRange {
|
||||||
|
// Map from VReg -> LiveRange index
|
||||||
|
var range_map = std.AutoHashMap(stackify.VReg, usize).init(allocator);
|
||||||
|
defer range_map.deinit();
|
||||||
|
|
||||||
|
var ranges: std.ArrayList(LiveRange) = .empty;
|
||||||
|
errdefer ranges.deinit(allocator);
|
||||||
|
|
||||||
|
for (instrs, 0..) |instr, idx| {
|
||||||
|
const i: u32 = @intCast(idx);
|
||||||
|
|
||||||
|
// Process pushes (definitions)
|
||||||
|
for (instr.effect.pushes) |vr| {
|
||||||
|
const vt = instr.result_type orelse .i32;
|
||||||
|
const range_idx = ranges.items.len;
|
||||||
|
try ranges.append(allocator, .{
|
||||||
|
.vreg = vr,
|
||||||
|
.valtype = vt,
|
||||||
|
.start = i,
|
||||||
|
.end = i,
|
||||||
|
});
|
||||||
|
try range_map.put(vr, range_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process pops (uses) — extend live range
|
||||||
|
for (instr.effect.pops) |vr| {
|
||||||
|
if (range_map.get(vr)) |range_idx| {
|
||||||
|
ranges.items[range_idx].end = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test "live ranges for add sequence" {
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
// Simulate: i32.const -> vr0, i32.const -> vr1, add(vr1,vr0) -> vr2
|
||||||
|
const instrs = [_]stackify.AnnotatedInstr{
|
||||||
|
.{ .opcode = 0x41, .imm = .{ .i32 = 1 }, .effect = .{ .pushes = &[_]stackify.VReg{0} }, .instr_idx = 0, .result_type = .i32 },
|
||||||
|
.{ .opcode = 0x41, .imm = .{ .i32 = 2 }, .effect = .{ .pushes = &[_]stackify.VReg{1} }, .instr_idx = 1, .result_type = .i32 },
|
||||||
|
.{ .opcode = 0x6A, .imm = .none, .effect = .{ .pops = &[_]stackify.VReg{ 1, 0 }, .pushes = &[_]stackify.VReg{2} }, .instr_idx = 2, .result_type = .i32 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const ranges = try computeLiveRanges(ally, &instrs);
|
||||||
|
defer ally.free(ranges);
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(usize, 3), ranges.len);
|
||||||
|
// vr0: defined at 0, last used at 2
|
||||||
|
try std.testing.expectEqual(@as(u32, 0), ranges[0].start);
|
||||||
|
try std.testing.expectEqual(@as(u32, 2), ranges[0].end);
|
||||||
|
// vr1: defined at 1, last used at 2
|
||||||
|
try std.testing.expectEqual(@as(u32, 1), ranges[1].start);
|
||||||
|
try std.testing.expectEqual(@as(u32, 2), ranges[1].end);
|
||||||
|
// vr2: defined at 2, never used -> end = 2
|
||||||
|
try std.testing.expectEqual(@as(u32, 2), ranges[2].start);
|
||||||
|
try std.testing.expectEqual(@as(u32, 2), ranges[2].end);
|
||||||
|
}
|
||||||
193
src/wasm/jit/regalloc.zig
Normal file
193
src/wasm/jit/regalloc.zig
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const module = @import("../module.zig");
|
||||||
|
const liveness = @import("liveness.zig");
|
||||||
|
|
||||||
|
pub const PhysReg = u8;
|
||||||
|
|
||||||
|
pub const ArchDesc = struct {
|
||||||
|
int_regs: []const PhysReg,
|
||||||
|
float_regs: []const PhysReg,
|
||||||
|
scratch_int: PhysReg,
|
||||||
|
scratch_float: PhysReg,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// x86-64: System V AMD64 ABI caller-saved registers
|
||||||
|
pub const x86_64_desc = ArchDesc{
|
||||||
|
.int_regs = &.{ 1, 2, 4, 5, 6, 7, 8, 9 }, // rcx,rdx,rsi,rdi,r8-r11
|
||||||
|
.float_regs = &.{ 0, 1, 2, 3, 4, 5, 6, 7 }, // xmm0-xmm7
|
||||||
|
.scratch_int = 0, // rax
|
||||||
|
.scratch_float = 8, // xmm8
|
||||||
|
};
|
||||||
|
|
||||||
|
/// AArch64: AAPCS64 caller-saved registers
|
||||||
|
pub const aarch64_desc = ArchDesc{
|
||||||
|
.int_regs = &.{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, // x1-x15
|
||||||
|
.float_regs = &.{ 0, 1, 2, 3, 4, 5, 6, 7 }, // v0-v7
|
||||||
|
.scratch_int = 16, // x16 (IP0)
|
||||||
|
.scratch_float = 16, // v16
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Location = union(enum) {
|
||||||
|
reg: PhysReg,
|
||||||
|
spill: u32, // byte offset from frame base
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Allocation = struct {
|
||||||
|
map: std.AutoHashMap(u32, Location),
|
||||||
|
|
||||||
|
pub fn locationOf(self: *const Allocation, vr: u32) Location {
|
||||||
|
return self.map.get(vr) orelse .{ .spill = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Allocation) void {
|
||||||
|
self.map.deinit();
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn isFloat(vt: module.ValType) bool {
|
||||||
|
return vt == .f32 or vt == .f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allocate(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
ranges: []liveness.LiveRange,
|
||||||
|
arch: ArchDesc,
|
||||||
|
) !Allocation {
|
||||||
|
// Sort ranges by start point
|
||||||
|
const sorted = try allocator.dupe(liveness.LiveRange, ranges);
|
||||||
|
defer allocator.free(sorted);
|
||||||
|
std.mem.sort(liveness.LiveRange, sorted, {}, struct {
|
||||||
|
fn lt(_: void, a: liveness.LiveRange, b: liveness.LiveRange) bool {
|
||||||
|
return a.start < b.start;
|
||||||
|
}
|
||||||
|
}.lt);
|
||||||
|
|
||||||
|
var map = std.AutoHashMap(u32, Location).init(allocator);
|
||||||
|
errdefer map.deinit();
|
||||||
|
|
||||||
|
// Free register lists
|
||||||
|
var free_ints: std.ArrayList(PhysReg) = .empty;
|
||||||
|
defer free_ints.deinit(allocator);
|
||||||
|
var free_floats: std.ArrayList(PhysReg) = .empty;
|
||||||
|
defer free_floats.deinit(allocator);
|
||||||
|
|
||||||
|
// Add registers in reverse so we pop from the front (lowest index first)
|
||||||
|
var i: usize = arch.int_regs.len;
|
||||||
|
while (i > 0) { i -= 1; try free_ints.append(allocator, arch.int_regs[i]); }
|
||||||
|
i = arch.float_regs.len;
|
||||||
|
while (i > 0) { i -= 1; try free_floats.append(allocator, arch.float_regs[i]); }
|
||||||
|
|
||||||
|
// Active ranges (sorted by end point)
|
||||||
|
const ActiveRange = struct {
|
||||||
|
vreg: u32,
|
||||||
|
end: u32,
|
||||||
|
reg: PhysReg,
|
||||||
|
is_float: bool,
|
||||||
|
};
|
||||||
|
var active: std.ArrayList(ActiveRange) = .empty;
|
||||||
|
defer active.deinit(allocator);
|
||||||
|
|
||||||
|
var spill_offset: u32 = 0;
|
||||||
|
|
||||||
|
for (sorted) |range| {
|
||||||
|
// Expire old intervals
|
||||||
|
var j: usize = 0;
|
||||||
|
while (j < active.items.len) {
|
||||||
|
const ar = active.items[j];
|
||||||
|
if (ar.end < range.start) {
|
||||||
|
// Free this register
|
||||||
|
if (ar.is_float) {
|
||||||
|
try free_floats.append(allocator, ar.reg);
|
||||||
|
} else {
|
||||||
|
try free_ints.append(allocator, ar.reg);
|
||||||
|
}
|
||||||
|
_ = active.orderedRemove(j);
|
||||||
|
} else {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const float = isFloat(range.valtype);
|
||||||
|
const free_list = if (float) &free_floats else &free_ints;
|
||||||
|
|
||||||
|
if (free_list.items.len > 0) {
|
||||||
|
const reg = free_list.pop().?;
|
||||||
|
try map.put(range.vreg, .{ .reg = reg });
|
||||||
|
// Insert into active sorted by end
|
||||||
|
const new_active = ActiveRange{ .vreg = range.vreg, .end = range.end, .reg = reg, .is_float = float };
|
||||||
|
var ins: usize = 0;
|
||||||
|
while (ins < active.items.len and active.items[ins].end <= range.end) ins += 1;
|
||||||
|
try active.insert(allocator, ins, new_active);
|
||||||
|
} else {
|
||||||
|
// Spill: evict the active range with furthest end
|
||||||
|
const last_idx = if (active.items.len > 0) active.items.len - 1 else {
|
||||||
|
// No active ranges — just spill this one
|
||||||
|
const size: u32 = if (range.valtype == .i32 or range.valtype == .f32) 4 else 8;
|
||||||
|
spill_offset += size;
|
||||||
|
try map.put(range.vreg, .{ .spill = spill_offset });
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
const spill_candidate = active.items[last_idx];
|
||||||
|
|
||||||
|
if (spill_candidate.end > range.end and spill_candidate.is_float == float) {
|
||||||
|
// Evict spill_candidate, assign its register to current
|
||||||
|
const reg = spill_candidate.reg;
|
||||||
|
const size: u32 = if (isFloat(range.valtype)) 8 else 8;
|
||||||
|
_ = size;
|
||||||
|
spill_offset += if (range.valtype == .i32 or range.valtype == .f32) 4 else 8;
|
||||||
|
try map.put(spill_candidate.vreg, .{ .spill = spill_offset });
|
||||||
|
try map.put(range.vreg, .{ .reg = reg });
|
||||||
|
_ = active.orderedRemove(last_idx);
|
||||||
|
const new_active = ActiveRange{ .vreg = range.vreg, .end = range.end, .reg = reg, .is_float = float };
|
||||||
|
var ins: usize = 0;
|
||||||
|
while (ins < active.items.len and active.items[ins].end <= range.end) ins += 1;
|
||||||
|
try active.insert(allocator, ins, new_active);
|
||||||
|
} else {
|
||||||
|
// Spill current range
|
||||||
|
spill_offset += if (range.valtype == .i32 or range.valtype == .f32) 4 else 8;
|
||||||
|
try map.put(range.vreg, .{ .spill = spill_offset });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Allocation{ .map = map };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test "regalloc fits in registers" {
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var ranges = [_]liveness.LiveRange{
|
||||||
|
.{ .vreg = 0, .valtype = .i32, .start = 0, .end = 2 },
|
||||||
|
.{ .vreg = 1, .valtype = .i32, .start = 1, .end = 3 },
|
||||||
|
.{ .vreg = 2, .valtype = .i32, .start = 2, .end = 4 },
|
||||||
|
};
|
||||||
|
var alloc = try allocate(ally, &ranges, x86_64_desc);
|
||||||
|
defer alloc.deinit();
|
||||||
|
|
||||||
|
// All three should be in registers (we have 8 int regs available)
|
||||||
|
for (0..3) |j| {
|
||||||
|
const loc = alloc.locationOf(@intCast(j));
|
||||||
|
try std.testing.expect(loc == .reg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regalloc spills under pressure" {
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
// Create more live ranges than available registers (8 int regs)
|
||||||
|
var ranges: [10]liveness.LiveRange = undefined;
|
||||||
|
for (&ranges, 0..) |*r, j| {
|
||||||
|
r.* = .{ .vreg = @intCast(j), .valtype = .i32, .start = @intCast(j), .end = 20 };
|
||||||
|
}
|
||||||
|
var alloc = try allocate(ally, &ranges, x86_64_desc);
|
||||||
|
defer alloc.deinit();
|
||||||
|
|
||||||
|
// Count spills — should have at least some
|
||||||
|
var spill_count: usize = 0;
|
||||||
|
for (0..10) |j| {
|
||||||
|
const loc = alloc.locationOf(@intCast(j));
|
||||||
|
if (loc == .spill) spill_count += 1;
|
||||||
|
}
|
||||||
|
try std.testing.expect(spill_count > 0);
|
||||||
|
}
|
||||||
965
src/wasm/jit/stackify.zig
Normal file
965
src/wasm/jit/stackify.zig
Normal file
|
|
@ -0,0 +1,965 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const module = @import("../module.zig");
|
||||||
|
const binary = @import("../binary.zig");
|
||||||
|
|
||||||
|
pub const VReg = u32;
|
||||||
|
|
||||||
|
pub const StackifyError = error{
|
||||||
|
TypeMismatch,
|
||||||
|
StackUnderflow,
|
||||||
|
UndefinedFunction,
|
||||||
|
UndefinedLocal,
|
||||||
|
UndefinedGlobal,
|
||||||
|
UndefinedMemory,
|
||||||
|
UndefinedTable,
|
||||||
|
InvalidLabelDepth,
|
||||||
|
ImmutableGlobal,
|
||||||
|
InvalidTypeIndex,
|
||||||
|
InvalidFunctionIndex,
|
||||||
|
ElseWithoutIf,
|
||||||
|
InvalidAlignment,
|
||||||
|
UnsupportedOpcode,
|
||||||
|
InvalidValueType,
|
||||||
|
OutOfMemory,
|
||||||
|
UnexpectedEof,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const StackEffect = struct {
|
||||||
|
pops: []const VReg = &.{},
|
||||||
|
pushes: []const VReg = &.{},
|
||||||
|
|
||||||
|
pub fn deinit(self: *StackEffect, allocator: std.mem.Allocator) void {
|
||||||
|
allocator.free(self.pops);
|
||||||
|
allocator.free(self.pushes);
|
||||||
|
self.* = .{};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Immediate = union(enum) {
|
||||||
|
none,
|
||||||
|
u32: u32,
|
||||||
|
u64: u64,
|
||||||
|
i32: i32,
|
||||||
|
i64: i64,
|
||||||
|
f32: f32,
|
||||||
|
f64: f64,
|
||||||
|
two_u32: struct { a: u32, b: u32 },
|
||||||
|
br_table: []u32,
|
||||||
|
mem: struct { @"align": u32, offset: u32 },
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const AnnotatedInstr = struct {
|
||||||
|
opcode: u8,
|
||||||
|
imm: Immediate,
|
||||||
|
effect: StackEffect,
|
||||||
|
instr_idx: u32,
|
||||||
|
result_type: ?module.ValType = null,
|
||||||
|
|
||||||
|
pub fn deinit(self: *AnnotatedInstr, allocator: std.mem.Allocator) void {
|
||||||
|
if (self.imm == .br_table) allocator.free(self.imm.br_table);
|
||||||
|
self.effect.deinit(allocator);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn deinitInstrs(allocator: std.mem.Allocator, instrs: []AnnotatedInstr) void {
|
||||||
|
for (instrs) |*ins| ins.deinit(allocator);
|
||||||
|
allocator.free(instrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const StackVal = struct {
|
||||||
|
vreg: VReg,
|
||||||
|
valtype: module.ValType,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Frame = struct {
|
||||||
|
kind: Kind,
|
||||||
|
start_height: usize,
|
||||||
|
label_types: []const module.ValType,
|
||||||
|
result_types: []const module.ValType,
|
||||||
|
reachable: bool,
|
||||||
|
const Kind = enum { block, loop, @"if", @"else" };
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Walk bytecode, simulate typed operand stack, assign vRegs to each produced value.
|
||||||
|
/// Returns a slice of AnnotatedInstr owned by the caller. Use `deinitInstrs` to free it.
|
||||||
|
pub fn stackify(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
body: *const module.FunctionBody,
|
||||||
|
func_type: *const module.FuncType,
|
||||||
|
mod: *const module.Module,
|
||||||
|
) StackifyError![]AnnotatedInstr {
|
||||||
|
var imported_globals: std.ArrayList(module.GlobalType) = .empty;
|
||||||
|
defer imported_globals.deinit(allocator);
|
||||||
|
|
||||||
|
var num_imported_funcs: u32 = 0;
|
||||||
|
var total_tables: u32 = 0;
|
||||||
|
var total_memories: u32 = 0;
|
||||||
|
|
||||||
|
for (mod.imports) |imp| {
|
||||||
|
switch (imp.desc) {
|
||||||
|
.func => num_imported_funcs += 1,
|
||||||
|
.table => total_tables += 1,
|
||||||
|
.memory => total_memories += 1,
|
||||||
|
.global => |gt| try imported_globals.append(allocator, gt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total_tables += @as(u32, @intCast(mod.tables.len));
|
||||||
|
total_memories += @as(u32, @intCast(mod.memories.len));
|
||||||
|
const total_funcs: u32 = num_imported_funcs + @as(u32, @intCast(mod.functions.len));
|
||||||
|
|
||||||
|
var local_types: std.ArrayList(module.ValType) = .empty;
|
||||||
|
defer local_types.deinit(allocator);
|
||||||
|
try local_types.appendSlice(allocator, func_type.params);
|
||||||
|
for (body.locals) |decl| for (0..decl.count) |_| try local_types.append(allocator, decl.valtype);
|
||||||
|
|
||||||
|
var instrs: std.ArrayList(AnnotatedInstr) = .empty;
|
||||||
|
errdefer {
|
||||||
|
for (instrs.items) |*ins| ins.deinit(allocator);
|
||||||
|
instrs.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stack: std.ArrayList(StackVal) = .empty;
|
||||||
|
defer stack.deinit(allocator);
|
||||||
|
|
||||||
|
var frames: std.ArrayList(Frame) = .empty;
|
||||||
|
defer frames.deinit(allocator);
|
||||||
|
try frames.append(allocator, .{
|
||||||
|
.kind = .block,
|
||||||
|
.start_height = 0,
|
||||||
|
.label_types = func_type.results,
|
||||||
|
.result_types = func_type.results,
|
||||||
|
.reachable = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
var tmp_pops: std.ArrayList(VReg) = .empty;
|
||||||
|
defer tmp_pops.deinit(allocator);
|
||||||
|
var tmp_pushes: std.ArrayList(VReg) = .empty;
|
||||||
|
defer tmp_pushes.deinit(allocator);
|
||||||
|
|
||||||
|
var next_vreg: VReg = 0;
|
||||||
|
var pos: usize = 0;
|
||||||
|
const code = body.code;
|
||||||
|
var instr_idx: u32 = 0;
|
||||||
|
|
||||||
|
while (pos < code.len) {
|
||||||
|
const op = code[pos];
|
||||||
|
pos += 1;
|
||||||
|
|
||||||
|
tmp_pops.clearRetainingCapacity();
|
||||||
|
tmp_pushes.clearRetainingCapacity();
|
||||||
|
|
||||||
|
var ann = AnnotatedInstr{
|
||||||
|
.opcode = op,
|
||||||
|
.imm = .none,
|
||||||
|
.effect = .{},
|
||||||
|
.instr_idx = instr_idx,
|
||||||
|
.result_type = null,
|
||||||
|
};
|
||||||
|
instr_idx += 1;
|
||||||
|
|
||||||
|
const frame = &frames.items[frames.items.len - 1];
|
||||||
|
const reachable = frame.reachable;
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
0x00 => { // unreachable
|
||||||
|
if (reachable) {
|
||||||
|
stack.shrinkRetainingCapacity(frame.start_height);
|
||||||
|
frame.reachable = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x01 => {},
|
||||||
|
0x02 => {
|
||||||
|
const bt = try readBlockType(code, &pos);
|
||||||
|
const res = try blockTypeResults(mod, bt);
|
||||||
|
try frames.append(allocator, .{
|
||||||
|
.kind = .block,
|
||||||
|
.start_height = stack.items.len,
|
||||||
|
.label_types = res,
|
||||||
|
.result_types = res,
|
||||||
|
.reachable = reachable,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
0x03 => {
|
||||||
|
const bt = try readBlockType(code, &pos);
|
||||||
|
const params = try blockTypeParams(mod, bt);
|
||||||
|
const res = try blockTypeResults(mod, bt);
|
||||||
|
try frames.append(allocator, .{
|
||||||
|
.kind = .loop,
|
||||||
|
.start_height = stack.items.len,
|
||||||
|
.label_types = params,
|
||||||
|
.result_types = res,
|
||||||
|
.reachable = reachable,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
0x04 => {
|
||||||
|
const bt = try readBlockType(code, &pos);
|
||||||
|
const res = try blockTypeResults(mod, bt);
|
||||||
|
if (reachable) _ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
try frames.append(allocator, .{
|
||||||
|
.kind = .@"if",
|
||||||
|
.start_height = stack.items.len,
|
||||||
|
.label_types = res,
|
||||||
|
.result_types = res,
|
||||||
|
.reachable = reachable,
|
||||||
|
});
|
||||||
|
ann.imm = .{ .i32 = @intCast(bt) };
|
||||||
|
},
|
||||||
|
0x05 => {
|
||||||
|
const cur = &frames.items[frames.items.len - 1];
|
||||||
|
if (cur.kind != .@"if") return error.ElseWithoutIf;
|
||||||
|
if (cur.reachable) try checkStackTypes(&stack, cur.start_height, cur.result_types);
|
||||||
|
stack.shrinkRetainingCapacity(cur.start_height);
|
||||||
|
cur.kind = .@"else";
|
||||||
|
cur.reachable = frames.items[frames.items.len - 2].reachable;
|
||||||
|
},
|
||||||
|
0x0B => {
|
||||||
|
if (frames.items.len == 1) {
|
||||||
|
if (frames.items[0].reachable) try checkStackTypes(&stack, 0, frames.items[0].result_types);
|
||||||
|
if (!frames.items[0].reachable) {
|
||||||
|
// If the function tail is polymorphic-unreachable, materialize typed results.
|
||||||
|
try emitMergeResults(allocator, &stack, &tmp_pushes, frames.items[0].result_types, &next_vreg, &ann.result_type);
|
||||||
|
} else if (frames.items[0].result_types.len == 1) {
|
||||||
|
ann.result_type = frames.items[0].result_types[0];
|
||||||
|
}
|
||||||
|
frame.reachable = true;
|
||||||
|
pos = code.len;
|
||||||
|
} else {
|
||||||
|
const cur = frames.pop().?;
|
||||||
|
if (cur.reachable) {
|
||||||
|
try preserveBlockResults(allocator, &stack, cur.start_height, cur.result_types);
|
||||||
|
if (cur.result_types.len == 1) ann.result_type = cur.result_types[0];
|
||||||
|
} else {
|
||||||
|
stack.shrinkRetainingCapacity(cur.start_height);
|
||||||
|
try emitMergeResults(allocator, &stack, &tmp_pushes, cur.result_types, &next_vreg, &ann.result_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x0C => {
|
||||||
|
const depth = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .u32 = depth };
|
||||||
|
if (depth >= frames.items.len) return error.InvalidLabelDepth;
|
||||||
|
if (reachable) {
|
||||||
|
const target = &frames.items[frames.items.len - 1 - depth];
|
||||||
|
try popLabelTypes(allocator, &stack, &tmp_pops, frame.start_height, target.label_types);
|
||||||
|
}
|
||||||
|
stack.shrinkRetainingCapacity(frame.start_height);
|
||||||
|
frame.reachable = false;
|
||||||
|
},
|
||||||
|
0x0D => {
|
||||||
|
const depth = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .u32 = depth };
|
||||||
|
if (depth >= frames.items.len) return error.InvalidLabelDepth;
|
||||||
|
if (reachable) {
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
const target = &frames.items[frames.items.len - 1 - depth];
|
||||||
|
try checkLabelTypes(&stack, frame.start_height, target.label_types);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x0E => {
|
||||||
|
const n = try readULEB128(u32, code, &pos);
|
||||||
|
const entries = try allocator.alloc(u32, n + 1);
|
||||||
|
errdefer allocator.free(entries);
|
||||||
|
var label_types: ?[]const module.ValType = null;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i <= n) : (i += 1) {
|
||||||
|
const depth = try readULEB128(u32, code, &pos);
|
||||||
|
entries[i] = depth;
|
||||||
|
if (depth >= frames.items.len) return error.InvalidLabelDepth;
|
||||||
|
const target = &frames.items[frames.items.len - 1 - depth];
|
||||||
|
if (label_types == null) {
|
||||||
|
label_types = target.label_types;
|
||||||
|
} else if (!sameValTypeSlice(label_types.?, target.label_types)) {
|
||||||
|
return error.TypeMismatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ann.imm = .{ .br_table = entries };
|
||||||
|
if (reachable) _ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
stack.shrinkRetainingCapacity(frame.start_height);
|
||||||
|
frame.reachable = false;
|
||||||
|
},
|
||||||
|
0x0F => {
|
||||||
|
if (reachable) try popLabelTypes(allocator, &stack, &tmp_pops, frame.start_height, frames.items[0].result_types);
|
||||||
|
stack.shrinkRetainingCapacity(frame.start_height);
|
||||||
|
frame.reachable = false;
|
||||||
|
},
|
||||||
|
0x10 => {
|
||||||
|
const fidx = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .u32 = fidx };
|
||||||
|
if (fidx >= total_funcs) return error.UndefinedFunction;
|
||||||
|
const ft = try getFuncType(mod, fidx, num_imported_funcs);
|
||||||
|
if (reachable) {
|
||||||
|
try popTypesReverse(allocator, &stack, &tmp_pops, frame.start_height, ft.params);
|
||||||
|
try pushResultTypes(allocator, &stack, &tmp_pushes, ft.results, &next_vreg, &ann.result_type);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x11 => {
|
||||||
|
const type_idx = try readULEB128(u32, code, &pos);
|
||||||
|
const table_idx = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .two_u32 = .{ .a = type_idx, .b = table_idx } };
|
||||||
|
if (type_idx >= mod.types.len) return error.InvalidTypeIndex;
|
||||||
|
if (table_idx >= total_tables) return error.UndefinedTable;
|
||||||
|
if (reachable) {
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
const ft = &mod.types[type_idx];
|
||||||
|
try popTypesReverse(allocator, &stack, &tmp_pops, frame.start_height, ft.params);
|
||||||
|
try pushResultTypes(allocator, &stack, &tmp_pushes, ft.results, &next_vreg, &ann.result_type);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x1A => {
|
||||||
|
if (reachable) _ = try popAnyVReg(allocator, &stack, &tmp_pops, frame.start_height);
|
||||||
|
},
|
||||||
|
0x1B => {
|
||||||
|
if (reachable) {
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
const rhs = try popAnyVReg(allocator, &stack, &tmp_pops, frame.start_height);
|
||||||
|
const lhs = try popAnyVReg(allocator, &stack, &tmp_pops, frame.start_height);
|
||||||
|
if (lhs.valtype != rhs.valtype) return error.TypeMismatch;
|
||||||
|
try pushExisting(allocator, &stack, &tmp_pushes, lhs.vreg, lhs.valtype);
|
||||||
|
ann.result_type = lhs.valtype;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x1C => {
|
||||||
|
const n = try readULEB128(u32, code, &pos);
|
||||||
|
if (n != 1) return error.TypeMismatch;
|
||||||
|
const t = try decodeValType(try readByte(code, &pos));
|
||||||
|
if (reachable) {
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, t);
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, t);
|
||||||
|
try pushNew(allocator, &stack, &tmp_pushes, t, &next_vreg);
|
||||||
|
ann.result_type = t;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x20 => {
|
||||||
|
const idx = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .u32 = idx };
|
||||||
|
if (idx >= local_types.items.len) return error.UndefinedLocal;
|
||||||
|
if (reachable) {
|
||||||
|
const vt = local_types.items[idx];
|
||||||
|
try pushNew(allocator, &stack, &tmp_pushes, vt, &next_vreg);
|
||||||
|
ann.result_type = vt;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x21 => {
|
||||||
|
const idx = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .u32 = idx };
|
||||||
|
if (idx >= local_types.items.len) return error.UndefinedLocal;
|
||||||
|
if (reachable) _ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, local_types.items[idx]);
|
||||||
|
},
|
||||||
|
0x22 => {
|
||||||
|
const idx = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .u32 = idx };
|
||||||
|
if (idx >= local_types.items.len) return error.UndefinedLocal;
|
||||||
|
if (reachable) {
|
||||||
|
const v = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, local_types.items[idx]);
|
||||||
|
try pushExisting(allocator, &stack, &tmp_pushes, v.vreg, v.valtype);
|
||||||
|
ann.result_type = v.valtype;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x23 => {
|
||||||
|
const idx = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .u32 = idx };
|
||||||
|
const gt = try getGlobalType(mod, imported_globals.items, idx);
|
||||||
|
if (reachable) {
|
||||||
|
try pushNew(allocator, &stack, &tmp_pushes, gt.valtype, &next_vreg);
|
||||||
|
ann.result_type = gt.valtype;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x24 => {
|
||||||
|
const idx = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .u32 = idx };
|
||||||
|
const gt = try getGlobalType(mod, imported_globals.items, idx);
|
||||||
|
if (!gt.mutable) return error.ImmutableGlobal;
|
||||||
|
if (reachable) _ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, gt.valtype);
|
||||||
|
},
|
||||||
|
0x28...0x35 => {
|
||||||
|
const mem_align = try readULEB128(u32, code, &pos);
|
||||||
|
const offset = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .mem = .{ .@"align" = mem_align, .offset = offset } };
|
||||||
|
if (total_memories == 0) return error.UndefinedMemory;
|
||||||
|
if (mem_align > naturalAlignmentLog2ForLoad(op)) return error.InvalidAlignment;
|
||||||
|
if (reachable) {
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
const rt = memLoadResultType(op);
|
||||||
|
try pushNew(allocator, &stack, &tmp_pushes, rt, &next_vreg);
|
||||||
|
ann.result_type = rt;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x36...0x3E => {
|
||||||
|
const mem_align = try readULEB128(u32, code, &pos);
|
||||||
|
const offset = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .mem = .{ .@"align" = mem_align, .offset = offset } };
|
||||||
|
if (total_memories == 0) return error.UndefinedMemory;
|
||||||
|
if (mem_align > naturalAlignmentLog2ForStore(op)) return error.InvalidAlignment;
|
||||||
|
if (reachable) {
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, memStoreValType(op));
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x3F, 0x40 => {
|
||||||
|
_ = try readByte(code, &pos);
|
||||||
|
if (total_memories == 0) return error.UndefinedMemory;
|
||||||
|
if (reachable) {
|
||||||
|
if (op == 0x40) _ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
try pushNew(allocator, &stack, &tmp_pushes, .i32, &next_vreg);
|
||||||
|
ann.result_type = .i32;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x41 => {
|
||||||
|
const val = try readSLEB128(i32, code, &pos);
|
||||||
|
ann.imm = .{ .i32 = val };
|
||||||
|
if (reachable) {
|
||||||
|
try pushNew(allocator, &stack, &tmp_pushes, .i32, &next_vreg);
|
||||||
|
ann.result_type = .i32;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x42 => {
|
||||||
|
const val = try readSLEB128(i64, code, &pos);
|
||||||
|
ann.imm = .{ .i64 = val };
|
||||||
|
if (reachable) {
|
||||||
|
try pushNew(allocator, &stack, &tmp_pushes, .i64, &next_vreg);
|
||||||
|
ann.result_type = .i64;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x43 => {
|
||||||
|
if (pos + 4 > code.len) return error.UnexpectedEof;
|
||||||
|
const raw = std.mem.readInt(u32, code[pos..][0..4], .little);
|
||||||
|
pos += 4;
|
||||||
|
ann.imm = .{ .f32 = @bitCast(raw) };
|
||||||
|
if (reachable) {
|
||||||
|
try pushNew(allocator, &stack, &tmp_pushes, .f32, &next_vreg);
|
||||||
|
ann.result_type = .f32;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x44 => {
|
||||||
|
if (pos + 8 > code.len) return error.UnexpectedEof;
|
||||||
|
const raw = std.mem.readInt(u64, code[pos..][0..8], .little);
|
||||||
|
pos += 8;
|
||||||
|
ann.imm = .{ .f64 = @bitCast(raw) };
|
||||||
|
if (reachable) {
|
||||||
|
try pushNew(allocator, &stack, &tmp_pushes, .f64, &next_vreg);
|
||||||
|
ann.result_type = .f64;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x45 => if (reachable) try unaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .i32, .i32, &next_vreg, &ann),
|
||||||
|
0x46...0x4F => if (reachable) try binaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .i32, .i32, &next_vreg, &ann),
|
||||||
|
0x50 => if (reachable) try unaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .i64, .i32, &next_vreg, &ann),
|
||||||
|
0x51...0x5A => if (reachable) try binaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .i64, .i32, &next_vreg, &ann),
|
||||||
|
0x5B...0x60 => if (reachable) try binaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .f32, .i32, &next_vreg, &ann),
|
||||||
|
0x61...0x66 => if (reachable) try binaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .f64, .i32, &next_vreg, &ann),
|
||||||
|
0x67...0x69 => if (reachable) try unaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .i32, .i32, &next_vreg, &ann),
|
||||||
|
0x6A...0x78 => if (reachable) try binaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .i32, .i32, &next_vreg, &ann),
|
||||||
|
0x79...0x7B => if (reachable) try unaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .i64, .i64, &next_vreg, &ann),
|
||||||
|
0x7C...0x8A => if (reachable) try binaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .i64, .i64, &next_vreg, &ann),
|
||||||
|
0x8B...0x91 => if (reachable) try unaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .f32, .f32, &next_vreg, &ann),
|
||||||
|
0x92...0x98 => if (reachable) try binaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .f32, .f32, &next_vreg, &ann),
|
||||||
|
0x99...0x9F => if (reachable) try unaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .f64, .f64, &next_vreg, &ann),
|
||||||
|
0xA0...0xA6 => if (reachable) try binaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .f64, .f64, &next_vreg, &ann),
|
||||||
|
0xA7...0xBF => if (reachable) try conversionOp(allocator, op, &stack, &tmp_pops, &tmp_pushes, frame.start_height, &next_vreg, &ann),
|
||||||
|
0xC0, 0xC1 => if (reachable) try unaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .i32, .i32, &next_vreg, &ann),
|
||||||
|
0xC2, 0xC3, 0xC4 => if (reachable) try unaryOp(allocator, &stack, &tmp_pops, &tmp_pushes, frame.start_height, .i64, .i64, &next_vreg, &ann),
|
||||||
|
0xFC => {
|
||||||
|
const subop = try readULEB128(u32, code, &pos);
|
||||||
|
switch (subop) {
|
||||||
|
0...7 => if (reachable) try truncSatOp(allocator, subop, &stack, &tmp_pops, &tmp_pushes, frame.start_height, &next_vreg, &ann),
|
||||||
|
8 => {
|
||||||
|
const data_idx = try readULEB128(u32, code, &pos);
|
||||||
|
const mem_idx = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .two_u32 = .{ .a = data_idx, .b = mem_idx } };
|
||||||
|
if (data_idx >= mod.datas.len) return error.TypeMismatch;
|
||||||
|
if (total_memories == 0 or mem_idx != 0) return error.UndefinedMemory;
|
||||||
|
if (reachable) {
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
9 => {
|
||||||
|
const data_idx = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .u32 = data_idx };
|
||||||
|
if (data_idx >= mod.datas.len) return error.TypeMismatch;
|
||||||
|
},
|
||||||
|
10 => {
|
||||||
|
const dst_mem = try readULEB128(u32, code, &pos);
|
||||||
|
const src_mem = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .two_u32 = .{ .a = dst_mem, .b = src_mem } };
|
||||||
|
if (total_memories == 0 or dst_mem != 0 or src_mem != 0) return error.UndefinedMemory;
|
||||||
|
if (reachable) {
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
11 => {
|
||||||
|
const mem_idx = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .u32 = mem_idx };
|
||||||
|
if (total_memories == 0 or mem_idx != 0) return error.UndefinedMemory;
|
||||||
|
if (reachable) {
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
_ = try popExpectVReg(allocator, &stack, &tmp_pops, frame.start_height, .i32);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
16 => {
|
||||||
|
const table_idx = try readULEB128(u32, code, &pos);
|
||||||
|
ann.imm = .{ .u32 = table_idx };
|
||||||
|
if (table_idx >= total_tables) return error.UndefinedTable;
|
||||||
|
if (reachable) {
|
||||||
|
try pushNew(allocator, &stack, &tmp_pushes, .i32, &next_vreg);
|
||||||
|
ann.result_type = .i32;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => return error.UnsupportedOpcode,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => return error.UnsupportedOpcode,
|
||||||
|
}
|
||||||
|
|
||||||
|
ann.effect = .{
|
||||||
|
.pops = try allocator.dupe(VReg, tmp_pops.items),
|
||||||
|
.pushes = try allocator.dupe(VReg, tmp_pushes.items),
|
||||||
|
};
|
||||||
|
errdefer ann.deinit(allocator);
|
||||||
|
try instrs.append(allocator, ann);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instrs.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pushNew(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
stack: *std.ArrayList(StackVal),
|
||||||
|
pushes: *std.ArrayList(VReg),
|
||||||
|
vt: module.ValType,
|
||||||
|
next_vreg: *VReg,
|
||||||
|
) StackifyError!void {
|
||||||
|
const vr = next_vreg.*;
|
||||||
|
next_vreg.* += 1;
|
||||||
|
try stack.append(allocator, .{ .vreg = vr, .valtype = vt });
|
||||||
|
try pushes.append(allocator, vr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pushExisting(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
stack: *std.ArrayList(StackVal),
|
||||||
|
pushes: *std.ArrayList(VReg),
|
||||||
|
vr: VReg,
|
||||||
|
vt: module.ValType,
|
||||||
|
) StackifyError!void {
|
||||||
|
try stack.append(allocator, .{ .vreg = vr, .valtype = vt });
|
||||||
|
try pushes.append(allocator, vr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn popAnyVReg(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
stack: *std.ArrayList(StackVal),
|
||||||
|
pops: *std.ArrayList(VReg),
|
||||||
|
min_height: usize,
|
||||||
|
) StackifyError!StackVal {
|
||||||
|
if (stack.items.len <= min_height) return error.StackUnderflow;
|
||||||
|
const v = stack.pop().?;
|
||||||
|
try pops.append(allocator, v.vreg);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn popExpectVReg(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
stack: *std.ArrayList(StackVal),
|
||||||
|
pops: *std.ArrayList(VReg),
|
||||||
|
min_height: usize,
|
||||||
|
expected: module.ValType,
|
||||||
|
) StackifyError!StackVal {
|
||||||
|
const v = try popAnyVReg(allocator, stack, pops, min_height);
|
||||||
|
if (v.valtype != expected) return error.TypeMismatch;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checkStackTypes(stack: *std.ArrayList(StackVal), base: usize, expected: []const module.ValType) StackifyError!void {
|
||||||
|
if (stack.items.len < base + expected.len) return error.StackUnderflow;
|
||||||
|
for (expected, 0..) |et, i| {
|
||||||
|
if (stack.items[base + i].valtype != et) return error.TypeMismatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checkLabelTypes(stack: *std.ArrayList(StackVal), base: usize, expected: []const module.ValType) StackifyError!void {
|
||||||
|
if (stack.items.len < base + expected.len) return error.StackUnderflow;
|
||||||
|
const start = stack.items.len - expected.len;
|
||||||
|
if (start < base) return error.StackUnderflow;
|
||||||
|
for (expected, 0..) |et, i| if (stack.items[start + i].valtype != et) return error.TypeMismatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn popLabelTypes(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
stack: *std.ArrayList(StackVal),
|
||||||
|
pops: *std.ArrayList(VReg),
|
||||||
|
base: usize,
|
||||||
|
label_types: []const module.ValType,
|
||||||
|
) StackifyError!void {
|
||||||
|
var i: usize = label_types.len;
|
||||||
|
while (i > 0) : (i -= 1) {
|
||||||
|
_ = try popExpectVReg(allocator, stack, pops, base, label_types[i - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn popTypesReverse(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
stack: *std.ArrayList(StackVal),
|
||||||
|
pops: *std.ArrayList(VReg),
|
||||||
|
base: usize,
|
||||||
|
types: []const module.ValType,
|
||||||
|
) StackifyError!void {
|
||||||
|
var i: usize = types.len;
|
||||||
|
while (i > 0) : (i -= 1) {
|
||||||
|
_ = try popExpectVReg(allocator, stack, pops, base, types[i - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pushResultTypes(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
stack: *std.ArrayList(StackVal),
|
||||||
|
pushes: *std.ArrayList(VReg),
|
||||||
|
results: []const module.ValType,
|
||||||
|
next_vreg: *VReg,
|
||||||
|
result_type: *?module.ValType,
|
||||||
|
) StackifyError!void {
|
||||||
|
if (results.len > 1) return error.UnsupportedOpcode;
|
||||||
|
for (results) |rt| {
|
||||||
|
try pushNew(allocator, stack, pushes, rt, next_vreg);
|
||||||
|
result_type.* = rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emitMergeResults(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
stack: *std.ArrayList(StackVal),
|
||||||
|
pushes: *std.ArrayList(VReg),
|
||||||
|
results: []const module.ValType,
|
||||||
|
next_vreg: *VReg,
|
||||||
|
result_type: *?module.ValType,
|
||||||
|
) StackifyError!void {
|
||||||
|
if (results.len > 1) return error.UnsupportedOpcode;
|
||||||
|
for (results) |rt| {
|
||||||
|
try pushNew(allocator, stack, pushes, rt, next_vreg);
|
||||||
|
result_type.* = rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preserveBlockResults(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
stack: *std.ArrayList(StackVal),
|
||||||
|
start_height: usize,
|
||||||
|
results: []const module.ValType,
|
||||||
|
) StackifyError!void {
|
||||||
|
if (results.len > 1) return error.UnsupportedOpcode;
|
||||||
|
try checkStackTypes(stack, start_height, results);
|
||||||
|
if (results.len == 0) {
|
||||||
|
stack.shrinkRetainingCapacity(start_height);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tail_start = stack.items.len - results.len;
|
||||||
|
const saved = try allocator.dupe(StackVal, stack.items[tail_start..]);
|
||||||
|
defer allocator.free(saved);
|
||||||
|
stack.shrinkRetainingCapacity(start_height);
|
||||||
|
try stack.appendSlice(allocator, saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unaryOp(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
stack: *std.ArrayList(StackVal),
|
||||||
|
pops: *std.ArrayList(VReg),
|
||||||
|
pushes: *std.ArrayList(VReg),
|
||||||
|
base: usize,
|
||||||
|
in_t: module.ValType,
|
||||||
|
out_t: module.ValType,
|
||||||
|
next_vreg: *VReg,
|
||||||
|
ann: *AnnotatedInstr,
|
||||||
|
) StackifyError!void {
|
||||||
|
_ = try popExpectVReg(allocator, stack, pops, base, in_t);
|
||||||
|
try pushNew(allocator, stack, pushes, out_t, next_vreg);
|
||||||
|
ann.result_type = out_t;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binaryOp(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
stack: *std.ArrayList(StackVal),
|
||||||
|
pops: *std.ArrayList(VReg),
|
||||||
|
pushes: *std.ArrayList(VReg),
|
||||||
|
base: usize,
|
||||||
|
in_t: module.ValType,
|
||||||
|
out_t: module.ValType,
|
||||||
|
next_vreg: *VReg,
|
||||||
|
ann: *AnnotatedInstr,
|
||||||
|
) StackifyError!void {
|
||||||
|
_ = try popExpectVReg(allocator, stack, pops, base, in_t);
|
||||||
|
_ = try popExpectVReg(allocator, stack, pops, base, in_t);
|
||||||
|
try pushNew(allocator, stack, pushes, out_t, next_vreg);
|
||||||
|
ann.result_type = out_t;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn conversionOp(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
op: u8,
|
||||||
|
stack: *std.ArrayList(StackVal),
|
||||||
|
pops: *std.ArrayList(VReg),
|
||||||
|
pushes: *std.ArrayList(VReg),
|
||||||
|
base: usize,
|
||||||
|
next_vreg: *VReg,
|
||||||
|
ann: *AnnotatedInstr,
|
||||||
|
) StackifyError!void {
|
||||||
|
const in_t: module.ValType = switch (op) {
|
||||||
|
0xA7 => .i64,
|
||||||
|
0xA8, 0xA9 => .f32,
|
||||||
|
0xAA, 0xAB => .f64,
|
||||||
|
0xAC, 0xAD => .i32,
|
||||||
|
0xAE, 0xAF => .f32,
|
||||||
|
0xB0, 0xB1 => .f64,
|
||||||
|
0xB2, 0xB3 => .i32,
|
||||||
|
0xB4, 0xB5 => .i64,
|
||||||
|
0xB6 => .f64,
|
||||||
|
0xB7, 0xB8 => .i32,
|
||||||
|
0xB9, 0xBA => .i64,
|
||||||
|
0xBB => .f32,
|
||||||
|
0xBC => .f32,
|
||||||
|
0xBD => .f64,
|
||||||
|
0xBE => .i32,
|
||||||
|
0xBF => .i64,
|
||||||
|
else => return error.UnsupportedOpcode,
|
||||||
|
};
|
||||||
|
const out_t: module.ValType = convertResultType(op);
|
||||||
|
_ = try popExpectVReg(allocator, stack, pops, base, in_t);
|
||||||
|
try pushNew(allocator, stack, pushes, out_t, next_vreg);
|
||||||
|
ann.result_type = out_t;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncSatOp(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
subop: u32,
|
||||||
|
stack: *std.ArrayList(StackVal),
|
||||||
|
pops: *std.ArrayList(VReg),
|
||||||
|
pushes: *std.ArrayList(VReg),
|
||||||
|
base: usize,
|
||||||
|
next_vreg: *VReg,
|
||||||
|
ann: *AnnotatedInstr,
|
||||||
|
) StackifyError!void {
|
||||||
|
const in_t: module.ValType = switch (subop) {
|
||||||
|
0, 1, 4, 5 => .f32,
|
||||||
|
2, 3, 6, 7 => .f64,
|
||||||
|
else => return error.UnsupportedOpcode,
|
||||||
|
};
|
||||||
|
const out_t: module.ValType = if (subop <= 3) .i32 else .i64;
|
||||||
|
_ = try popExpectVReg(allocator, stack, pops, base, in_t);
|
||||||
|
try pushNew(allocator, stack, pushes, out_t, next_vreg);
|
||||||
|
ann.result_type = out_t;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convertResultType(op: u8) module.ValType {
|
||||||
|
return switch (op) {
|
||||||
|
0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xBC => .i32,
|
||||||
|
0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xBD => .i64,
|
||||||
|
0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xBE => .f32,
|
||||||
|
0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBF => .f64,
|
||||||
|
else => .i32,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readByte(code: []const u8, pos: *usize) StackifyError!u8 {
|
||||||
|
if (pos.* >= code.len) return error.UnexpectedEof;
|
||||||
|
const b = code[pos.*];
|
||||||
|
pos.* += 1;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readULEB128(comptime T: type, code: []const u8, pos: *usize) StackifyError!T {
|
||||||
|
return binary.readULEB128(T, code, pos) catch |e| switch (e) {
|
||||||
|
error.UnexpectedEof => error.UnexpectedEof,
|
||||||
|
else => error.TypeMismatch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readSLEB128(comptime T: type, code: []const u8, pos: *usize) StackifyError!T {
|
||||||
|
return binary.readSLEB128(T, code, pos) catch |e| switch (e) {
|
||||||
|
error.UnexpectedEof => error.UnexpectedEof,
|
||||||
|
else => error.TypeMismatch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readBlockType(code: []const u8, pos: *usize) StackifyError!i33 {
|
||||||
|
return readSLEB128(i33, code, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decodeValType(b: u8) StackifyError!module.ValType {
|
||||||
|
return switch (b) {
|
||||||
|
0x7F => .i32,
|
||||||
|
0x7E => .i64,
|
||||||
|
0x7D => .f32,
|
||||||
|
0x7C => .f64,
|
||||||
|
else => error.InvalidValueType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blockTypeResults(mod: *const module.Module, bt: i33) StackifyError![]const module.ValType {
|
||||||
|
return switch (bt) {
|
||||||
|
-1 => &[_]module.ValType{.i32},
|
||||||
|
-2 => &[_]module.ValType{.i64},
|
||||||
|
-3 => &[_]module.ValType{.f32},
|
||||||
|
-4 => &[_]module.ValType{.f64},
|
||||||
|
-64 => &.{},
|
||||||
|
else => if (bt >= 0) blk: {
|
||||||
|
const idx: u32 = @intCast(bt);
|
||||||
|
if (idx >= mod.types.len) return error.InvalidTypeIndex;
|
||||||
|
break :blk mod.types[idx].results;
|
||||||
|
} else error.InvalidTypeIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blockTypeParams(mod: *const module.Module, bt: i33) StackifyError![]const module.ValType {
|
||||||
|
if (bt < 0) return &.{};
|
||||||
|
const idx: u32 = @intCast(bt);
|
||||||
|
if (idx >= mod.types.len) return error.InvalidTypeIndex;
|
||||||
|
return mod.types[idx].params;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getFuncType(mod: *const module.Module, fidx: u32, num_imported: u32) StackifyError!*const module.FuncType {
|
||||||
|
if (fidx < num_imported) {
|
||||||
|
var count: u32 = 0;
|
||||||
|
for (mod.imports) |imp| {
|
||||||
|
if (imp.desc == .func) {
|
||||||
|
if (count == fidx) return &mod.types[imp.desc.func];
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error.InvalidFunctionIndex;
|
||||||
|
}
|
||||||
|
const local_idx = fidx - num_imported;
|
||||||
|
if (local_idx >= mod.functions.len) return error.InvalidFunctionIndex;
|
||||||
|
const type_idx = mod.functions[local_idx];
|
||||||
|
if (type_idx >= mod.types.len) return error.InvalidTypeIndex;
|
||||||
|
return &mod.types[type_idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getGlobalType(mod: *const module.Module, imported_globals: []const module.GlobalType, idx: u32) StackifyError!module.GlobalType {
|
||||||
|
if (idx < imported_globals.len) return imported_globals[idx];
|
||||||
|
const local_idx = idx - @as(u32, @intCast(imported_globals.len));
|
||||||
|
if (local_idx >= mod.globals.len) return error.UndefinedGlobal;
|
||||||
|
return mod.globals[local_idx].type;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memLoadResultType(op: u8) module.ValType {
|
||||||
|
return switch (op) {
|
||||||
|
0x28, 0x2C, 0x2D, 0x2E, 0x2F => .i32,
|
||||||
|
0x29, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35 => .i64,
|
||||||
|
0x2A => .f32,
|
||||||
|
0x2B => .f64,
|
||||||
|
else => .i32,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memStoreValType(op: u8) module.ValType {
|
||||||
|
return switch (op) {
|
||||||
|
0x36, 0x3A, 0x3B => .i32,
|
||||||
|
0x37, 0x3C, 0x3D, 0x3E => .i64,
|
||||||
|
0x38 => .f32,
|
||||||
|
0x39 => .f64,
|
||||||
|
else => .i32,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn naturalAlignmentLog2ForLoad(op: u8) u32 {
|
||||||
|
return switch (op) {
|
||||||
|
0x28 => 2,
|
||||||
|
0x29 => 3,
|
||||||
|
0x2A => 2,
|
||||||
|
0x2B => 3,
|
||||||
|
0x2C, 0x2D => 0,
|
||||||
|
0x2E, 0x2F => 1,
|
||||||
|
0x30, 0x31 => 0,
|
||||||
|
0x32, 0x33 => 1,
|
||||||
|
0x34, 0x35 => 2,
|
||||||
|
else => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn naturalAlignmentLog2ForStore(op: u8) u32 {
|
||||||
|
return switch (op) {
|
||||||
|
0x36 => 2,
|
||||||
|
0x37 => 3,
|
||||||
|
0x38 => 2,
|
||||||
|
0x39 => 3,
|
||||||
|
0x3A, 0x3C => 0,
|
||||||
|
0x3B, 0x3D => 1,
|
||||||
|
0x3E => 2,
|
||||||
|
else => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sameValTypeSlice(a: []const module.ValType, b: []const module.ValType) bool {
|
||||||
|
if (a.len != b.len) return false;
|
||||||
|
for (a, 0..) |vt, i| if (vt != b[i]) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test "stackify straight-line function" {
|
||||||
|
const code = [_]u8{ 0x41, 0x01, 0x41, 0x02, 0x6a, 0x0b };
|
||||||
|
const body = module.FunctionBody{ .locals = &.{}, .code = &code };
|
||||||
|
const ft = module.FuncType{ .params = &.{}, .results = &.{.i32} };
|
||||||
|
const mod = module.Module{
|
||||||
|
.types = &.{},
|
||||||
|
.imports = &.{},
|
||||||
|
.functions = &.{},
|
||||||
|
.tables = &.{},
|
||||||
|
.memories = &.{},
|
||||||
|
.globals = &.{},
|
||||||
|
.exports = &.{},
|
||||||
|
.start = null,
|
||||||
|
.elements = &.{},
|
||||||
|
.codes = &.{},
|
||||||
|
.datas = &.{},
|
||||||
|
.allocator = std.testing.allocator,
|
||||||
|
};
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
const instrs = try stackify(ally, &body, &ft, &mod);
|
||||||
|
defer deinitInstrs(ally, instrs);
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(usize, 4), instrs.len);
|
||||||
|
const vr0 = instrs[0].effect.pushes[0];
|
||||||
|
const vr1 = instrs[1].effect.pushes[0];
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), instrs[2].effect.pops.len);
|
||||||
|
try std.testing.expectEqual(vr1, instrs[2].effect.pops[0]);
|
||||||
|
try std.testing.expectEqual(vr0, instrs[2].effect.pops[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "stackify call uses function signature" {
|
||||||
|
const code = [_]u8{ 0x41, 0x07, 0x10, 0x00, 0x0b };
|
||||||
|
const body = module.FunctionBody{ .locals = &.{}, .code = &code };
|
||||||
|
|
||||||
|
var callee_params = [_]module.ValType{.i32};
|
||||||
|
var callee_results = [_]module.ValType{.i64};
|
||||||
|
var types = [_]module.FuncType{.{ .params = &callee_params, .results = &callee_results }};
|
||||||
|
var funcs = [_]u32{0};
|
||||||
|
var codes = [_]module.FunctionBody{body};
|
||||||
|
|
||||||
|
const mod = module.Module{
|
||||||
|
.types = &types,
|
||||||
|
.imports = &.{},
|
||||||
|
.functions = &funcs,
|
||||||
|
.tables = &.{},
|
||||||
|
.memories = &.{},
|
||||||
|
.globals = &.{},
|
||||||
|
.exports = &.{},
|
||||||
|
.start = null,
|
||||||
|
.elements = &.{},
|
||||||
|
.codes = &codes,
|
||||||
|
.datas = &.{},
|
||||||
|
.allocator = std.testing.allocator,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
const instrs = try stackify(ally, &body, &types[0], &mod);
|
||||||
|
defer deinitInstrs(ally, instrs);
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), instrs[1].effect.pops.len);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), instrs[1].effect.pushes.len);
|
||||||
|
try std.testing.expectEqual(module.ValType.i64, instrs[1].result_type.?);
|
||||||
|
}
|
||||||
1163
src/wasm/jit/x86_64.zig
Normal file
1163
src/wasm/jit/x86_64.zig
Normal file
File diff suppressed because it is too large
Load diff
11
src/wasm/jit_tests.zig
Normal file
11
src/wasm/jit_tests.zig
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Test runner for all jit/* submodules.
|
||||||
|
// Root at src/wasm/ so that "../module.zig" from jit/ files resolves correctly.
|
||||||
|
comptime {
|
||||||
|
_ = @import("jit/stackify.zig");
|
||||||
|
_ = @import("jit/liveness.zig");
|
||||||
|
_ = @import("jit/regalloc.zig");
|
||||||
|
_ = @import("jit/codebuf.zig");
|
||||||
|
_ = @import("jit/aarch64.zig");
|
||||||
|
_ = @import("jit/x86_64.zig");
|
||||||
|
_ = @import("jit/codegen.zig");
|
||||||
|
}
|
||||||
138
src/wasm/module.zig
Normal file
138
src/wasm/module.zig
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const ValType = enum(u8) {
|
||||||
|
i32 = 0x7F,
|
||||||
|
i64 = 0x7E,
|
||||||
|
f32 = 0x7D,
|
||||||
|
f64 = 0x7C,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const SectionId = enum(u8) {
|
||||||
|
custom = 0,
|
||||||
|
type = 1,
|
||||||
|
import = 2,
|
||||||
|
function = 3,
|
||||||
|
table = 4,
|
||||||
|
memory = 5,
|
||||||
|
global = 6,
|
||||||
|
@"export" = 7,
|
||||||
|
start = 8,
|
||||||
|
element = 9,
|
||||||
|
code = 10,
|
||||||
|
data = 11,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const FuncType = struct {
|
||||||
|
params: []const ValType,
|
||||||
|
results: []const ValType,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const MemoryType = struct { min: u32, max: ?u32 };
|
||||||
|
pub const TableType = struct { elem_type: u8, min: u32, max: ?u32 };
|
||||||
|
pub const GlobalType = struct { valtype: ValType, mutable: bool };
|
||||||
|
|
||||||
|
pub const ConstExpr = union(enum) {
|
||||||
|
i32_const: i32,
|
||||||
|
i64_const: i64,
|
||||||
|
f32_const: f32,
|
||||||
|
f64_const: f64,
|
||||||
|
global_get: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const GlobalDef = struct { type: GlobalType, init: ConstExpr };
|
||||||
|
|
||||||
|
pub const ImportDesc = union(enum) {
|
||||||
|
func: u32,
|
||||||
|
table: TableType,
|
||||||
|
memory: MemoryType,
|
||||||
|
global: GlobalType,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Import = struct {
|
||||||
|
module: []const u8,
|
||||||
|
name: []const u8,
|
||||||
|
desc: ImportDesc,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ExportDesc = union(enum) {
|
||||||
|
func: u32,
|
||||||
|
table: u32,
|
||||||
|
memory: u32,
|
||||||
|
global: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Export = struct {
|
||||||
|
name: []const u8,
|
||||||
|
desc: ExportDesc,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const LocalDecl = struct { count: u32, valtype: ValType };
|
||||||
|
|
||||||
|
pub const FunctionBody = struct {
|
||||||
|
locals: []LocalDecl,
|
||||||
|
code: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ElementSegment = struct {
|
||||||
|
table_idx: u32,
|
||||||
|
offset: ConstExpr,
|
||||||
|
func_indices: []u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DataSegment = struct {
|
||||||
|
kind: enum { active, passive },
|
||||||
|
memory_idx: u32,
|
||||||
|
offset: ?ConstExpr,
|
||||||
|
bytes: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Module = struct {
|
||||||
|
types: []FuncType,
|
||||||
|
imports: []Import,
|
||||||
|
functions: []u32,
|
||||||
|
tables: []TableType,
|
||||||
|
memories: []MemoryType,
|
||||||
|
globals: []GlobalDef,
|
||||||
|
exports: []Export,
|
||||||
|
start: ?u32,
|
||||||
|
elements: []ElementSegment,
|
||||||
|
codes: []FunctionBody,
|
||||||
|
datas: []DataSegment,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
pub fn parse(allocator: std.mem.Allocator, bytes: []const u8) !Module {
|
||||||
|
return @import("binary.zig").parse(allocator, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Module) void {
|
||||||
|
const ally = self.allocator;
|
||||||
|
for (self.types) |t| {
|
||||||
|
ally.free(t.params);
|
||||||
|
ally.free(t.results);
|
||||||
|
}
|
||||||
|
ally.free(self.types);
|
||||||
|
for (self.imports) |imp| {
|
||||||
|
ally.free(imp.module);
|
||||||
|
ally.free(imp.name);
|
||||||
|
}
|
||||||
|
ally.free(self.imports);
|
||||||
|
ally.free(self.functions);
|
||||||
|
ally.free(self.tables);
|
||||||
|
ally.free(self.memories);
|
||||||
|
ally.free(self.globals);
|
||||||
|
for (self.exports) |exp| {
|
||||||
|
ally.free(exp.name);
|
||||||
|
}
|
||||||
|
ally.free(self.exports);
|
||||||
|
for (self.elements) |elem| {
|
||||||
|
ally.free(elem.func_indices);
|
||||||
|
}
|
||||||
|
ally.free(self.elements);
|
||||||
|
for (self.codes) |body| {
|
||||||
|
ally.free(body.locals);
|
||||||
|
}
|
||||||
|
ally.free(self.codes);
|
||||||
|
ally.free(self.datas);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
115
src/wasm/runtime.zig
Normal file
115
src/wasm/runtime.zig
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const module = @import("module.zig");
|
||||||
|
|
||||||
|
pub const PAGE_SIZE: u32 = 65536;
|
||||||
|
|
||||||
|
pub const Value = union(module.ValType) {
|
||||||
|
i32: i32,
|
||||||
|
i64: i64,
|
||||||
|
f32: f32,
|
||||||
|
f64: f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Memory = struct {
|
||||||
|
bytes: []u8,
|
||||||
|
max_pages: ?u32,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, min_pages: u32, max_pages: ?u32) !Memory {
|
||||||
|
const size = @as(usize, min_pages) * PAGE_SIZE;
|
||||||
|
const bytes = try allocator.alloc(u8, size);
|
||||||
|
@memset(bytes, 0);
|
||||||
|
return .{ .bytes = bytes, .max_pages = max_pages };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Memory, allocator: std.mem.Allocator) void {
|
||||||
|
allocator.free(self.bytes);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grow by delta_pages. Returns old page count, or error.OutOfMemory / error.GrowthExceedsMax.
|
||||||
|
pub fn grow(self: *Memory, allocator: std.mem.Allocator, delta_pages: u32) !u32 {
|
||||||
|
const old_pages: u32 = @intCast(self.bytes.len / PAGE_SIZE);
|
||||||
|
const new_pages = old_pages + delta_pages;
|
||||||
|
if (self.max_pages) |max| {
|
||||||
|
if (new_pages > max) return error.GrowthExceedsMax;
|
||||||
|
}
|
||||||
|
const new_size = @as(usize, new_pages) * PAGE_SIZE;
|
||||||
|
const new_bytes = try allocator.realloc(self.bytes, new_size);
|
||||||
|
@memset(new_bytes[self.bytes.len..], 0);
|
||||||
|
self.bytes = new_bytes;
|
||||||
|
return old_pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(self: *const Memory, comptime T: type, addr: u32) !T {
|
||||||
|
const size = @sizeOf(T);
|
||||||
|
if (@as(usize, addr) + size > self.bytes.len) return error.OutOfBounds;
|
||||||
|
const Bits = std.meta.Int(.unsigned, @bitSizeOf(T));
|
||||||
|
const raw = std.mem.readInt(Bits, self.bytes[addr..][0..size], .little);
|
||||||
|
return @bitCast(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store(self: *Memory, comptime T: type, addr: u32, value: T) !void {
|
||||||
|
const size = @sizeOf(T);
|
||||||
|
if (@as(usize, addr) + size > self.bytes.len) return error.OutOfBounds;
|
||||||
|
const Bits = std.meta.Int(.unsigned, @bitSizeOf(T));
|
||||||
|
const raw: Bits = @bitCast(value);
|
||||||
|
std.mem.writeInt(Bits, self.bytes[addr..][0..size], raw, .little);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Table = struct {
|
||||||
|
elements: []?u32,
|
||||||
|
max: ?u32,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, min: u32, max: ?u32) !Table {
|
||||||
|
const elems = try allocator.alloc(?u32, min);
|
||||||
|
@memset(elems, null);
|
||||||
|
return .{ .elements = elems, .max = max };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Table, allocator: std.mem.Allocator) void {
|
||||||
|
allocator.free(self.elements);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "memory load/store round-trip i32" {
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mem = try Memory.init(ally, 1, null);
|
||||||
|
defer mem.deinit(ally);
|
||||||
|
try mem.store(i32, 0, 42);
|
||||||
|
const v = try mem.load(i32, 0);
|
||||||
|
try std.testing.expectEqual(@as(i32, 42), v);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "memory out-of-bounds returns error" {
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mem = try Memory.init(ally, 1, null);
|
||||||
|
defer mem.deinit(ally);
|
||||||
|
try std.testing.expectError(error.OutOfBounds, mem.load(i32, PAGE_SIZE - 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "memory grow" {
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mem = try Memory.init(ally, 1, 4);
|
||||||
|
defer mem.deinit(ally);
|
||||||
|
const old = try mem.grow(ally, 1);
|
||||||
|
try std.testing.expectEqual(@as(u32, 1), old);
|
||||||
|
try std.testing.expectEqual(@as(usize, 2 * PAGE_SIZE), mem.bytes.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "memory grow beyond max fails" {
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mem = try Memory.init(ally, 1, 2);
|
||||||
|
defer mem.deinit(ally);
|
||||||
|
try std.testing.expectError(error.GrowthExceedsMax, mem.grow(ally, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "memory store/load f64" {
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mem = try Memory.init(ally, 1, null);
|
||||||
|
defer mem.deinit(ally);
|
||||||
|
try mem.store(f64, 8, 3.14);
|
||||||
|
const v = try mem.load(f64, 8);
|
||||||
|
try std.testing.expectApproxEqAbs(3.14, v, 1e-10);
|
||||||
|
}
|
||||||
20
src/wasm/trap.zig
Normal file
20
src/wasm/trap.zig
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
pub const TrapCode = enum(u32) {
|
||||||
|
@"unreachable",
|
||||||
|
memory_out_of_bounds,
|
||||||
|
undefined_global,
|
||||||
|
undefined_table,
|
||||||
|
invalid_function,
|
||||||
|
integer_divide_by_zero,
|
||||||
|
integer_overflow,
|
||||||
|
invalid_conversion_to_integer,
|
||||||
|
stack_overflow,
|
||||||
|
indirect_call_type_mismatch,
|
||||||
|
undefined_element,
|
||||||
|
uninitialized_element,
|
||||||
|
call_stack_exhausted,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Trap = struct {
|
||||||
|
code: TrapCode,
|
||||||
|
message: []const u8,
|
||||||
|
};
|
||||||
881
src/wasm/validator.zig
Normal file
881
src/wasm/validator.zig
Normal file
|
|
@ -0,0 +1,881 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const module = @import("module.zig");
|
||||||
|
|
||||||
|
pub const ValidationError = error{
|
||||||
|
TypeMismatch,
|
||||||
|
StackUnderflow,
|
||||||
|
UndefinedFunction,
|
||||||
|
UndefinedLocal,
|
||||||
|
UndefinedGlobal,
|
||||||
|
UndefinedMemory,
|
||||||
|
UndefinedTable,
|
||||||
|
InvalidLabelDepth,
|
||||||
|
ImmutableGlobal,
|
||||||
|
InvalidTypeIndex,
|
||||||
|
InvalidFunctionIndex,
|
||||||
|
ElseWithoutIf,
|
||||||
|
InvalidAlignment,
|
||||||
|
UnsupportedOpcode,
|
||||||
|
OutOfMemory,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Validate a parsed Module. Returns void on success, error on failure.
|
||||||
|
pub fn validate(mod: *const module.Module) ValidationError!void {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const ally = arena.allocator();
|
||||||
|
|
||||||
|
var num_imported_funcs: u32 = 0;
|
||||||
|
var num_imported_tables: u32 = 0;
|
||||||
|
var num_imported_memories: u32 = 0;
|
||||||
|
var imported_globals: std.ArrayList(module.GlobalType) = .empty;
|
||||||
|
defer imported_globals.deinit(ally);
|
||||||
|
|
||||||
|
for (mod.imports) |imp| {
|
||||||
|
switch (imp.desc) {
|
||||||
|
.func => |type_idx| {
|
||||||
|
if (type_idx >= mod.types.len) return ValidationError.InvalidTypeIndex;
|
||||||
|
num_imported_funcs += 1;
|
||||||
|
},
|
||||||
|
.table => num_imported_tables += 1,
|
||||||
|
.memory => num_imported_memories += 1,
|
||||||
|
.global => |gt| try imported_globals.append(ally, gt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const total_funcs: u32 = num_imported_funcs + @as(u32, @intCast(mod.functions.len));
|
||||||
|
const total_tables: u32 = num_imported_tables + @as(u32, @intCast(mod.tables.len));
|
||||||
|
const total_memories: u32 = num_imported_memories + @as(u32, @intCast(mod.memories.len));
|
||||||
|
const total_globals: u32 = @as(u32, @intCast(imported_globals.items.len)) + @as(u32, @intCast(mod.globals.len));
|
||||||
|
|
||||||
|
if (mod.codes.len != mod.functions.len) return ValidationError.InvalidFunctionIndex;
|
||||||
|
if (mod.start) |start_idx| {
|
||||||
|
if (start_idx >= total_funcs) return ValidationError.InvalidFunctionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (mod.codes, 0..) |body, i| {
|
||||||
|
const type_idx = mod.functions[i];
|
||||||
|
if (type_idx >= mod.types.len) return ValidationError.InvalidTypeIndex;
|
||||||
|
const func_type = &mod.types[type_idx];
|
||||||
|
try validateFunction(mod, func_type, &body, total_funcs, num_imported_funcs, total_tables, total_memories, imported_globals.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (mod.exports) |exp| {
|
||||||
|
switch (exp.desc) {
|
||||||
|
.func => |idx| if (idx >= total_funcs) return ValidationError.InvalidFunctionIndex,
|
||||||
|
.table => |idx| if (idx >= total_tables) return ValidationError.UndefinedTable,
|
||||||
|
.memory => |idx| if (idx >= total_memories) return ValidationError.UndefinedMemory,
|
||||||
|
.global => |idx| if (idx >= total_globals) return ValidationError.UndefinedGlobal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (mod.elements) |elem| {
|
||||||
|
if (elem.table_idx >= total_tables) return ValidationError.UndefinedTable;
|
||||||
|
for (elem.func_indices) |fi| {
|
||||||
|
if (fi >= total_funcs) return ValidationError.InvalidFunctionIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (mod.datas) |seg| {
|
||||||
|
if (seg.kind == .active and seg.memory_idx >= total_memories) return ValidationError.UndefinedMemory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const StackVal = union(enum) {
|
||||||
|
val: module.ValType,
|
||||||
|
any, // polymorphic (after unreachable)
|
||||||
|
};
|
||||||
|
|
||||||
|
const Frame = struct {
|
||||||
|
kind: Kind,
|
||||||
|
start_height: usize,
|
||||||
|
label_types: []const module.ValType,
|
||||||
|
result_types: []const module.ValType,
|
||||||
|
reachable: bool,
|
||||||
|
const Kind = enum { block, loop, @"if", @"else" };
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validateFunction(
|
||||||
|
mod: *const module.Module,
|
||||||
|
func_type: *const module.FuncType,
|
||||||
|
body: *const module.FunctionBody,
|
||||||
|
total_funcs: u32,
|
||||||
|
num_imported_funcs: u32,
|
||||||
|
total_tables: u32,
|
||||||
|
total_memories: u32,
|
||||||
|
imported_globals: []const module.GlobalType,
|
||||||
|
) ValidationError!void {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const ally = arena.allocator();
|
||||||
|
|
||||||
|
// Local types: params then declared locals
|
||||||
|
var local_types: std.ArrayList(module.ValType) = .empty;
|
||||||
|
try local_types.appendSlice(ally, func_type.params);
|
||||||
|
for (body.locals) |decl| {
|
||||||
|
for (0..decl.count) |_| try local_types.append(ally, decl.valtype);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stack: std.ArrayList(StackVal) = .empty;
|
||||||
|
var frames: std.ArrayList(Frame) = .empty;
|
||||||
|
|
||||||
|
// Implicit function frame
|
||||||
|
try frames.append(ally, .{
|
||||||
|
.kind = .block,
|
||||||
|
.start_height = 0,
|
||||||
|
.label_types = func_type.results,
|
||||||
|
.result_types = func_type.results,
|
||||||
|
.reachable = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
var pos: usize = 0;
|
||||||
|
const code = body.code;
|
||||||
|
|
||||||
|
while (pos < code.len) {
|
||||||
|
const op = code[pos];
|
||||||
|
pos += 1;
|
||||||
|
|
||||||
|
const frame = &frames.items[frames.items.len - 1];
|
||||||
|
const reachable = frame.reachable;
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
0x00 => { // unreachable
|
||||||
|
if (reachable) {
|
||||||
|
stack.shrinkRetainingCapacity(frame.start_height);
|
||||||
|
frame.reachable = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x01 => {}, // nop
|
||||||
|
0x02 => { // block bt
|
||||||
|
const bt = try readBlockType(code, &pos);
|
||||||
|
const res = blockTypeResults(mod, bt);
|
||||||
|
try frames.append(ally, .{
|
||||||
|
.kind = .block,
|
||||||
|
.start_height = stack.items.len,
|
||||||
|
.label_types = res,
|
||||||
|
.result_types = res,
|
||||||
|
.reachable = reachable,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
0x03 => { // loop bt
|
||||||
|
const bt = try readBlockType(code, &pos);
|
||||||
|
const params = blockTypeParams(mod, bt);
|
||||||
|
const res = blockTypeResults(mod, bt);
|
||||||
|
try frames.append(ally, .{
|
||||||
|
.kind = .loop,
|
||||||
|
.start_height = stack.items.len,
|
||||||
|
.label_types = params,
|
||||||
|
.result_types = res,
|
||||||
|
.reachable = reachable,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
0x04 => { // if bt
|
||||||
|
const bt = try readBlockType(code, &pos);
|
||||||
|
const res = blockTypeResults(mod, bt);
|
||||||
|
if (reachable) try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try frames.append(ally, .{
|
||||||
|
.kind = .@"if",
|
||||||
|
.start_height = stack.items.len,
|
||||||
|
.label_types = res,
|
||||||
|
.result_types = res,
|
||||||
|
.reachable = reachable,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
0x05 => { // else
|
||||||
|
const cur = &frames.items[frames.items.len - 1];
|
||||||
|
if (cur.kind != .@"if") return ValidationError.ElseWithoutIf;
|
||||||
|
if (cur.reachable) {
|
||||||
|
try checkStackTypes(&stack, cur.start_height, cur.result_types);
|
||||||
|
}
|
||||||
|
stack.shrinkRetainingCapacity(cur.start_height);
|
||||||
|
cur.kind = .@"else";
|
||||||
|
cur.reachable = frames.items[frames.items.len - 2].reachable;
|
||||||
|
},
|
||||||
|
0x0B => { // end
|
||||||
|
if (frames.items.len == 1) {
|
||||||
|
const f = frames.items[0];
|
||||||
|
if (f.reachable) try checkStackTypes(&stack, f.start_height, f.result_types);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const cur = frames.pop().?;
|
||||||
|
if (cur.reachable) try checkStackTypes(&stack, cur.start_height, cur.result_types);
|
||||||
|
stack.shrinkRetainingCapacity(cur.start_height);
|
||||||
|
for (cur.result_types) |rt| try stack.append(ally, .{ .val = rt });
|
||||||
|
// propagate unreachability
|
||||||
|
const parent = &frames.items[frames.items.len - 1];
|
||||||
|
if (!cur.reachable) parent.reachable = false;
|
||||||
|
},
|
||||||
|
0x0C => { // br l
|
||||||
|
const depth = try readULEB128(u32, code, &pos);
|
||||||
|
if (depth >= frames.items.len) return ValidationError.InvalidLabelDepth;
|
||||||
|
const target = &frames.items[frames.items.len - 1 - depth];
|
||||||
|
if (reachable) try checkStackTypes(&stack, frame.start_height, target.label_types);
|
||||||
|
stack.shrinkRetainingCapacity(frame.start_height);
|
||||||
|
frame.reachable = false;
|
||||||
|
},
|
||||||
|
0x0D => { // br_if l
|
||||||
|
const depth = try readULEB128(u32, code, &pos);
|
||||||
|
if (depth >= frames.items.len) return ValidationError.InvalidLabelDepth;
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
const target = &frames.items[frames.items.len - 1 - depth];
|
||||||
|
try checkStackTypes(&stack, frame.start_height, target.label_types);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x0E => { // br_table
|
||||||
|
const n = try readULEB128(u32, code, &pos);
|
||||||
|
var label_types: ?[]const module.ValType = null;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i <= n) : (i += 1) {
|
||||||
|
const depth = try readULEB128(u32, code, &pos);
|
||||||
|
if (depth >= frames.items.len) return ValidationError.InvalidLabelDepth;
|
||||||
|
const target = &frames.items[frames.items.len - 1 - depth];
|
||||||
|
if (label_types == null) {
|
||||||
|
label_types = target.label_types;
|
||||||
|
} else if (!sameValTypeSlice(label_types.?, target.label_types)) {
|
||||||
|
return ValidationError.TypeMismatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reachable) try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
stack.shrinkRetainingCapacity(frame.start_height);
|
||||||
|
frame.reachable = false;
|
||||||
|
},
|
||||||
|
0x0F => { // return
|
||||||
|
if (reachable) {
|
||||||
|
try checkStackTypes(&stack, frame.start_height, frames.items[0].result_types);
|
||||||
|
}
|
||||||
|
stack.shrinkRetainingCapacity(frame.start_height);
|
||||||
|
frame.reachable = false;
|
||||||
|
},
|
||||||
|
0x10 => { // call
|
||||||
|
const fidx = try readULEB128(u32, code, &pos);
|
||||||
|
if (fidx >= total_funcs) return ValidationError.UndefinedFunction;
|
||||||
|
const ft = getFuncType(mod, fidx, num_imported_funcs);
|
||||||
|
if (reachable) {
|
||||||
|
var pi = ft.params.len;
|
||||||
|
while (pi > 0) : (pi -= 1) try popExpect(ally, &stack, frame.start_height, ft.params[pi - 1]);
|
||||||
|
for (ft.results) |rt| try stack.append(ally, .{ .val = rt });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x11 => { // call_indirect
|
||||||
|
const type_idx = try readULEB128(u32, code, &pos);
|
||||||
|
const table_idx = try readULEB128(u32, code, &pos);
|
||||||
|
if (type_idx >= mod.types.len) return ValidationError.InvalidTypeIndex;
|
||||||
|
if (table_idx >= total_tables) return ValidationError.UndefinedTable;
|
||||||
|
const ft = &mod.types[type_idx];
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
var pi = ft.params.len;
|
||||||
|
while (pi > 0) : (pi -= 1) try popExpect(ally, &stack, frame.start_height, ft.params[pi - 1]);
|
||||||
|
for (ft.results) |rt| try stack.append(ally, .{ .val = rt });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x1A => { // drop
|
||||||
|
if (reachable) _ = try popAny(&stack, frame.start_height);
|
||||||
|
},
|
||||||
|
0x1B => { // select
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
const t2 = try popAny(&stack, frame.start_height);
|
||||||
|
const t1 = try popAny(&stack, frame.start_height);
|
||||||
|
if (t1 == .val and t2 == .val and t1.val != t2.val) return ValidationError.TypeMismatch;
|
||||||
|
try stack.append(ally, t1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x1C => { // select (typed)
|
||||||
|
const n = try readULEB128(u32, code, &pos);
|
||||||
|
if (n != 1) return ValidationError.TypeMismatch;
|
||||||
|
const b = try readByte(code, &pos);
|
||||||
|
const t: module.ValType = switch (b) {
|
||||||
|
0x7F => .i32,
|
||||||
|
0x7E => .i64,
|
||||||
|
0x7D => .f32,
|
||||||
|
0x7C => .f64,
|
||||||
|
else => return ValidationError.TypeMismatch,
|
||||||
|
};
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, t);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, t);
|
||||||
|
try stack.append(ally, .{ .val = t });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x20 => { // local.get
|
||||||
|
const idx = try readULEB128(u32, code, &pos);
|
||||||
|
if (idx >= local_types.items.len) return ValidationError.UndefinedLocal;
|
||||||
|
if (reachable) try stack.append(ally, .{ .val = local_types.items[idx] });
|
||||||
|
},
|
||||||
|
0x21 => { // local.set
|
||||||
|
const idx = try readULEB128(u32, code, &pos);
|
||||||
|
if (idx >= local_types.items.len) return ValidationError.UndefinedLocal;
|
||||||
|
if (reachable) try popExpect(ally, &stack, frame.start_height, local_types.items[idx]);
|
||||||
|
},
|
||||||
|
0x22 => { // local.tee
|
||||||
|
const idx = try readULEB128(u32, code, &pos);
|
||||||
|
if (idx >= local_types.items.len) return ValidationError.UndefinedLocal;
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, local_types.items[idx]);
|
||||||
|
try stack.append(ally, .{ .val = local_types.items[idx] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x23 => { // global.get
|
||||||
|
const idx = try readULEB128(u32, code, &pos);
|
||||||
|
const gt = try getGlobalType(mod, imported_globals, idx);
|
||||||
|
if (reachable) try stack.append(ally, .{ .val = gt.valtype });
|
||||||
|
},
|
||||||
|
0x24 => { // global.set
|
||||||
|
const idx = try readULEB128(u32, code, &pos);
|
||||||
|
const gt = try getGlobalType(mod, imported_globals, idx);
|
||||||
|
if (!gt.mutable) return ValidationError.ImmutableGlobal;
|
||||||
|
if (reachable) try popExpect(ally, &stack, frame.start_height, gt.valtype);
|
||||||
|
},
|
||||||
|
0x28...0x35 => { // memory loads
|
||||||
|
const mem_align = try readULEB128(u32, code, &pos);
|
||||||
|
_ = try readULEB128(u32, code, &pos); // offset
|
||||||
|
if (total_memories == 0) return ValidationError.UndefinedMemory;
|
||||||
|
if (mem_align > naturalAlignmentLog2ForLoad(op)) return ValidationError.InvalidAlignment;
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try stack.append(ally, .{ .val = memLoadResultType(op) });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x36...0x3E => { // memory stores
|
||||||
|
const mem_align = try readULEB128(u32, code, &pos);
|
||||||
|
_ = try readULEB128(u32, code, &pos); // offset
|
||||||
|
if (total_memories == 0) return ValidationError.UndefinedMemory;
|
||||||
|
if (mem_align > naturalAlignmentLog2ForStore(op)) return ValidationError.InvalidAlignment;
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, memStoreValType(op));
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x3F, 0x40 => { // memory.size / memory.grow
|
||||||
|
_ = try readByte(code, &pos);
|
||||||
|
if (total_memories == 0) return ValidationError.UndefinedMemory;
|
||||||
|
if (reachable) {
|
||||||
|
if (op == 0x40) try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try stack.append(ally, .{ .val = .i32 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0xFC => { // bulk memory
|
||||||
|
const subop = try readULEB128(u32, code, &pos);
|
||||||
|
switch (subop) {
|
||||||
|
0, 1 => { // i32.trunc_sat_f32_{s,u}
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .f32);
|
||||||
|
try stack.append(ally, .{ .val = .i32 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
2, 3 => { // i32.trunc_sat_f64_{s,u}
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .f64);
|
||||||
|
try stack.append(ally, .{ .val = .i32 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
4, 5 => { // i64.trunc_sat_f32_{s,u}
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .f32);
|
||||||
|
try stack.append(ally, .{ .val = .i64 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
6, 7 => { // i64.trunc_sat_f64_{s,u}
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .f64);
|
||||||
|
try stack.append(ally, .{ .val = .i64 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
8 => { // memory.init
|
||||||
|
const data_idx = try readULEB128(u32, code, &pos);
|
||||||
|
const mem_idx = try readULEB128(u32, code, &pos);
|
||||||
|
if (data_idx >= mod.datas.len) return ValidationError.TypeMismatch;
|
||||||
|
if (total_memories == 0) return ValidationError.UndefinedMemory;
|
||||||
|
if (mem_idx != 0) return ValidationError.UndefinedMemory;
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
9 => { // data.drop
|
||||||
|
const data_idx = try readULEB128(u32, code, &pos);
|
||||||
|
if (data_idx >= mod.datas.len) return ValidationError.TypeMismatch;
|
||||||
|
},
|
||||||
|
10 => { // memory.copy
|
||||||
|
const dst_mem = try readULEB128(u32, code, &pos);
|
||||||
|
const src_mem = try readULEB128(u32, code, &pos);
|
||||||
|
if (total_memories == 0) return ValidationError.UndefinedMemory;
|
||||||
|
if (dst_mem != 0 or src_mem != 0) return ValidationError.UndefinedMemory;
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
11 => { // memory.fill
|
||||||
|
const mem_idx = try readULEB128(u32, code, &pos);
|
||||||
|
if (total_memories == 0) return ValidationError.UndefinedMemory;
|
||||||
|
if (mem_idx != 0) return ValidationError.UndefinedMemory;
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
16 => { // table.size
|
||||||
|
const table_idx = try readULEB128(u32, code, &pos);
|
||||||
|
if (table_idx >= total_tables) return ValidationError.UndefinedTable;
|
||||||
|
if (reachable) try stack.append(ally, .{ .val = .i32 });
|
||||||
|
},
|
||||||
|
else => return ValidationError.TypeMismatch,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0x41 => {
|
||||||
|
_ = try readSLEB128(i32, code, &pos);
|
||||||
|
if (reachable) try stack.append(ally, .{ .val = .i32 });
|
||||||
|
},
|
||||||
|
0x42 => {
|
||||||
|
_ = try readSLEB128(i64, code, &pos);
|
||||||
|
if (reachable) try stack.append(ally, .{ .val = .i64 });
|
||||||
|
},
|
||||||
|
0x43 => {
|
||||||
|
if (pos + 4 > code.len) return ValidationError.StackUnderflow;
|
||||||
|
pos += 4;
|
||||||
|
if (reachable) try stack.append(ally, .{ .val = .f32 });
|
||||||
|
},
|
||||||
|
0x44 => {
|
||||||
|
if (pos + 8 > code.len) return ValidationError.StackUnderflow;
|
||||||
|
pos += 8;
|
||||||
|
if (reachable) try stack.append(ally, .{ .val = .f64 });
|
||||||
|
},
|
||||||
|
// i32 eqz (unary)
|
||||||
|
0x45 => {
|
||||||
|
if (reachable) { try popExpect(ally, &stack, frame.start_height, .i32); try stack.append(ally, .{ .val = .i32 }); }
|
||||||
|
},
|
||||||
|
// i32 comparisons (binary -> i32)
|
||||||
|
0x46...0x4F => {
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try stack.append(ally, .{ .val = .i32 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// i64 eqz
|
||||||
|
0x50 => {
|
||||||
|
if (reachable) { try popExpect(ally, &stack, frame.start_height, .i64); try stack.append(ally, .{ .val = .i32 }); }
|
||||||
|
},
|
||||||
|
// i64 comparisons
|
||||||
|
0x51...0x5A => {
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i64);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i64);
|
||||||
|
try stack.append(ally, .{ .val = .i32 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// f32 comparisons
|
||||||
|
0x5B...0x60 => {
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .f32);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .f32);
|
||||||
|
try stack.append(ally, .{ .val = .i32 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// f64 comparisons
|
||||||
|
0x61...0x66 => {
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .f64);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .f64);
|
||||||
|
try stack.append(ally, .{ .val = .i32 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// i32 unary ops (clz, ctz, popcnt)
|
||||||
|
0x67, 0x68, 0x69 => {
|
||||||
|
if (reachable) { try popExpect(ally, &stack, frame.start_height, .i32); try stack.append(ally, .{ .val = .i32 }); }
|
||||||
|
},
|
||||||
|
// i32 binary ops
|
||||||
|
0x6A...0x78 => {
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i32);
|
||||||
|
try stack.append(ally, .{ .val = .i32 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// i64 unary ops
|
||||||
|
0x79, 0x7A, 0x7B => {
|
||||||
|
if (reachable) { try popExpect(ally, &stack, frame.start_height, .i64); try stack.append(ally, .{ .val = .i64 }); }
|
||||||
|
},
|
||||||
|
// i64 binary ops
|
||||||
|
0x7C...0x8A => {
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i64);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .i64);
|
||||||
|
try stack.append(ally, .{ .val = .i64 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// f32 unary ops
|
||||||
|
0x8B...0x91 => {
|
||||||
|
if (reachable) { try popExpect(ally, &stack, frame.start_height, .f32); try stack.append(ally, .{ .val = .f32 }); }
|
||||||
|
},
|
||||||
|
// f32 binary ops
|
||||||
|
0x92...0x98 => {
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .f32);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .f32);
|
||||||
|
try stack.append(ally, .{ .val = .f32 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// f64 unary ops
|
||||||
|
0x99...0x9F => {
|
||||||
|
if (reachable) { try popExpect(ally, &stack, frame.start_height, .f64); try stack.append(ally, .{ .val = .f64 }); }
|
||||||
|
},
|
||||||
|
// f64 binary ops
|
||||||
|
0xA0...0xA6 => {
|
||||||
|
if (reachable) {
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .f64);
|
||||||
|
try popExpect(ally, &stack, frame.start_height, .f64);
|
||||||
|
try stack.append(ally, .{ .val = .f64 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Conversions
|
||||||
|
0xA7 => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .i64); try stack.append(ally, .{ .val = .i32 }); } }, // i32.wrap_i64
|
||||||
|
0xA8, 0xA9 => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .f32); try stack.append(ally, .{ .val = .i32 }); } },
|
||||||
|
0xAA, 0xAB => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .f64); try stack.append(ally, .{ .val = .i32 }); } },
|
||||||
|
0xAC, 0xAD => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .i32); try stack.append(ally, .{ .val = .i64 }); } },
|
||||||
|
0xAE, 0xAF => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .f32); try stack.append(ally, .{ .val = .i64 }); } },
|
||||||
|
0xB0, 0xB1 => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .f64); try stack.append(ally, .{ .val = .i64 }); } },
|
||||||
|
0xB2, 0xB3 => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .i32); try stack.append(ally, .{ .val = .f32 }); } },
|
||||||
|
0xB4, 0xB5 => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .i64); try stack.append(ally, .{ .val = .f32 }); } },
|
||||||
|
0xB6 => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .f64); try stack.append(ally, .{ .val = .f32 }); } },
|
||||||
|
0xB7, 0xB8 => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .i32); try stack.append(ally, .{ .val = .f64 }); } },
|
||||||
|
0xB9, 0xBA => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .i64); try stack.append(ally, .{ .val = .f64 }); } },
|
||||||
|
0xBB => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .f32); try stack.append(ally, .{ .val = .f64 }); } },
|
||||||
|
0xBC => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .f32); try stack.append(ally, .{ .val = .i32 }); } },
|
||||||
|
0xBD => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .f64); try stack.append(ally, .{ .val = .i64 }); } },
|
||||||
|
0xBE => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .i32); try stack.append(ally, .{ .val = .f32 }); } },
|
||||||
|
0xBF => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .i64); try stack.append(ally, .{ .val = .f64 }); } },
|
||||||
|
0xC0, 0xC1 => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .i32); try stack.append(ally, .{ .val = .i32 }); } },
|
||||||
|
0xC2, 0xC3, 0xC4 => { if (reachable) { try popExpect(ally, &stack, frame.start_height, .i64); try stack.append(ally, .{ .val = .i64 }); } },
|
||||||
|
else => return ValidationError.UnsupportedOpcode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block type is encoded as SLEB128:
|
||||||
|
// -1 = i32, -2 = i64, -3 = f32, -4 = f64, -64 = void, >=0 = type index
|
||||||
|
fn readBlockType(code: []const u8, pos: *usize) ValidationError!i33 {
|
||||||
|
return @import("binary.zig").readSLEB128(i33, code, pos) catch ValidationError.TypeMismatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blockTypeResults(mod: *const module.Module, bt: i33) []const module.ValType {
|
||||||
|
return switch (bt) {
|
||||||
|
-1 => &[_]module.ValType{.i32},
|
||||||
|
-2 => &[_]module.ValType{.i64},
|
||||||
|
-3 => &[_]module.ValType{.f32},
|
||||||
|
-4 => &[_]module.ValType{.f64},
|
||||||
|
-64 => &.{}, // void
|
||||||
|
else => if (bt >= 0) blk: {
|
||||||
|
const idx: u32 = @intCast(bt);
|
||||||
|
if (idx >= mod.types.len) break :blk &.{};
|
||||||
|
break :blk mod.types[idx].results;
|
||||||
|
} else &.{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blockTypeParams(mod: *const module.Module, bt: i33) []const module.ValType {
|
||||||
|
if (bt < 0) return &.{};
|
||||||
|
const idx: u32 = @intCast(bt);
|
||||||
|
if (idx >= mod.types.len) return &.{};
|
||||||
|
return mod.types[idx].params;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getFuncType(mod: *const module.Module, fidx: u32, num_imported: u32) *const module.FuncType {
|
||||||
|
if (fidx < num_imported) {
|
||||||
|
var count: u32 = 0;
|
||||||
|
for (mod.imports) |imp| {
|
||||||
|
if (imp.desc == .func) {
|
||||||
|
if (count == fidx) return &mod.types[imp.desc.func];
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const local_idx = fidx - num_imported;
|
||||||
|
return &mod.types[mod.functions[local_idx]];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getGlobalType(mod: *const module.Module, imported_globals: []const module.GlobalType, idx: u32) ValidationError!module.GlobalType {
|
||||||
|
if (idx < imported_globals.len) return imported_globals[idx];
|
||||||
|
const local_idx = idx - @as(u32, @intCast(imported_globals.len));
|
||||||
|
if (local_idx >= mod.globals.len) return ValidationError.UndefinedGlobal;
|
||||||
|
return mod.globals[local_idx].type;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn popAny(stack: *std.ArrayList(StackVal), min_height: usize) ValidationError!StackVal {
|
||||||
|
if (stack.items.len <= min_height) return ValidationError.StackUnderflow;
|
||||||
|
return stack.pop().?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn popExpect(ally: std.mem.Allocator, stack: *std.ArrayList(StackVal), min_height: usize, expected: module.ValType) ValidationError!void {
|
||||||
|
_ = ally;
|
||||||
|
if (stack.items.len <= min_height) return ValidationError.StackUnderflow;
|
||||||
|
const top = stack.pop().?;
|
||||||
|
switch (top) {
|
||||||
|
.any => {},
|
||||||
|
.val => |v| if (v != expected) return ValidationError.TypeMismatch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checkStackTypes(stack: *std.ArrayList(StackVal), base: usize, expected: []const module.ValType) ValidationError!void {
|
||||||
|
const actual_count = stack.items.len - base;
|
||||||
|
if (actual_count < expected.len) return ValidationError.StackUnderflow;
|
||||||
|
for (expected, 0..) |et, i| {
|
||||||
|
const stack_idx = base + i;
|
||||||
|
switch (stack.items[stack_idx]) {
|
||||||
|
.any => {},
|
||||||
|
.val => |v| if (v != et) return ValidationError.TypeMismatch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readByte(code: []const u8, pos: *usize) ValidationError!u8 {
|
||||||
|
if (pos.* >= code.len) return ValidationError.StackUnderflow;
|
||||||
|
const b = code[pos.*];
|
||||||
|
pos.* += 1;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readULEB128(comptime T: type, code: []const u8, pos: *usize) ValidationError!T {
|
||||||
|
return @import("binary.zig").readULEB128(T, code, pos) catch ValidationError.TypeMismatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readSLEB128(comptime T: type, code: []const u8, pos: *usize) ValidationError!T {
|
||||||
|
return @import("binary.zig").readSLEB128(T, code, pos) catch ValidationError.TypeMismatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memLoadResultType(op: u8) module.ValType {
|
||||||
|
return switch (op) {
|
||||||
|
0x28, 0x2C, 0x2D, 0x2E, 0x2F => .i32,
|
||||||
|
0x29, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35 => .i64,
|
||||||
|
0x2A => .f32,
|
||||||
|
0x2B => .f64,
|
||||||
|
else => .i32,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memStoreValType(op: u8) module.ValType {
|
||||||
|
return switch (op) {
|
||||||
|
0x36, 0x3A, 0x3B => .i32,
|
||||||
|
0x37, 0x3C, 0x3D, 0x3E => .i64,
|
||||||
|
0x38 => .f32,
|
||||||
|
0x39 => .f64,
|
||||||
|
else => .i32,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn naturalAlignmentLog2ForLoad(op: u8) u32 {
|
||||||
|
return switch (op) {
|
||||||
|
0x28 => 2, // i32.load
|
||||||
|
0x29 => 3, // i64.load
|
||||||
|
0x2A => 2, // f32.load
|
||||||
|
0x2B => 3, // f64.load
|
||||||
|
0x2C, 0x2D => 0, // i32.load8_{s,u}
|
||||||
|
0x2E, 0x2F => 1, // i32.load16_{s,u}
|
||||||
|
0x30, 0x31 => 0, // i64.load8_{s,u}
|
||||||
|
0x32, 0x33 => 1, // i64.load16_{s,u}
|
||||||
|
0x34, 0x35 => 2, // i64.load32_{s,u}
|
||||||
|
else => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn naturalAlignmentLog2ForStore(op: u8) u32 {
|
||||||
|
return switch (op) {
|
||||||
|
0x36 => 2, // i32.store
|
||||||
|
0x37 => 3, // i64.store
|
||||||
|
0x38 => 2, // f32.store
|
||||||
|
0x39 => 3, // f64.store
|
||||||
|
0x3A, 0x3C => 0, // i32/i64.store8
|
||||||
|
0x3B, 0x3D => 1, // i32/i64.store16
|
||||||
|
0x3E => 2, // i64.store32
|
||||||
|
else => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sameValTypeSlice(a: []const module.ValType, b: []const module.ValType) bool {
|
||||||
|
if (a.len != b.len) return false;
|
||||||
|
for (a, 0..) |vt, i| {
|
||||||
|
if (vt != b[i]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fib_wasm = [_]u8{
|
||||||
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
0x01, 0x06, 0x01, 0x60, 0x01, 0x7f, 0x01, 0x7f,
|
||||||
|
0x03, 0x02, 0x01, 0x00,
|
||||||
|
0x07, 0x07, 0x01, 0x03, 0x66, 0x69, 0x62, 0x00, 0x00,
|
||||||
|
0x0a, 0x1e, 0x01, 0x1c, 0x00, 0x20, 0x00, 0x41, 0x02, 0x48, 0x04,
|
||||||
|
0x7f, 0x20, 0x00, 0x05, 0x20, 0x00, 0x41, 0x01, 0x6b, 0x10, 0x00,
|
||||||
|
0x20, 0x00, 0x41, 0x02, 0x6b, 0x10, 0x00, 0x6a, 0x0b, 0x0b,
|
||||||
|
};
|
||||||
|
|
||||||
|
test "validate fib module" {
|
||||||
|
const binary = @import("binary.zig");
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mod = try binary.parse(ally, &fib_wasm);
|
||||||
|
defer mod.deinit();
|
||||||
|
try validate(&mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "validate minimal module" {
|
||||||
|
const binary = @import("binary.zig");
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mod = try binary.parse(ally, "\x00asm\x01\x00\x00\x00");
|
||||||
|
defer mod.deinit();
|
||||||
|
try validate(&mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "validate rejects out-of-bounds local" {
|
||||||
|
// (func (result i32) local.get 99) — 99 as LEB128 = 0x63
|
||||||
|
// body: 0x00(locals) 0x20(local.get) 0x63(99) 0x0b(end) = 4 bytes
|
||||||
|
// section: 0x01(count) 0x04(body_size) + 4 body bytes = 6 bytes total
|
||||||
|
const binary = @import("binary.zig");
|
||||||
|
const wasm = [_]u8{
|
||||||
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
// type section: () -> (i32)
|
||||||
|
0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f,
|
||||||
|
// function section: type 0
|
||||||
|
0x03, 0x02, 0x01, 0x00,
|
||||||
|
// code section: body_size=4, 0 locals, local.get 99, end
|
||||||
|
0x0a, 0x06, 0x01, 0x04, 0x00, 0x20, 0x63, 0x0b,
|
||||||
|
};
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mod = try binary.parse(ally, &wasm);
|
||||||
|
defer mod.deinit();
|
||||||
|
try std.testing.expectError(ValidationError.UndefinedLocal, validate(&mod));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "validate rejects function and code section length mismatch" {
|
||||||
|
const binary = @import("binary.zig");
|
||||||
|
const wasm = [_]u8{
|
||||||
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
// type section: () -> ()
|
||||||
|
0x01, 0x04, 0x01, 0x60, 0x00, 0x00,
|
||||||
|
// function section: one local function
|
||||||
|
0x03, 0x02, 0x01, 0x00,
|
||||||
|
};
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mod = try binary.parse(ally, &wasm);
|
||||||
|
defer mod.deinit();
|
||||||
|
try std.testing.expectError(ValidationError.InvalidFunctionIndex, validate(&mod));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "validate rejects call_indirect with out-of-range table index" {
|
||||||
|
const binary = @import("binary.zig");
|
||||||
|
const wasm = [_]u8{
|
||||||
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
// type section: () -> ()
|
||||||
|
0x01, 0x04, 0x01, 0x60, 0x00, 0x00,
|
||||||
|
// function section: one function, type 0
|
||||||
|
0x03, 0x02, 0x01, 0x00,
|
||||||
|
// table section: one funcref table
|
||||||
|
0x04, 0x04, 0x01, 0x70, 0x00, 0x01,
|
||||||
|
// code section: i32.const 0; call_indirect type=0 table=1; end
|
||||||
|
0x0a, 0x09, 0x01, 0x07, 0x00, 0x41, 0x00, 0x11, 0x00, 0x01, 0x0b,
|
||||||
|
};
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mod = try binary.parse(ally, &wasm);
|
||||||
|
defer mod.deinit();
|
||||||
|
try std.testing.expectError(ValidationError.UndefinedTable, validate(&mod));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "validate accepts imported globals in global index space" {
|
||||||
|
const binary = @import("binary.zig");
|
||||||
|
const wasm = [_]u8{
|
||||||
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
// type section: () -> (i32)
|
||||||
|
0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f,
|
||||||
|
// import section: (import "env" "g" (global i32))
|
||||||
|
0x02, 0x0a, 0x01, 0x03, 0x65, 0x6e, 0x76, 0x01, 0x67, 0x03, 0x7f, 0x00,
|
||||||
|
// function section: one local function, type 0
|
||||||
|
0x03, 0x02, 0x01, 0x00,
|
||||||
|
// code section: global.get 0; end
|
||||||
|
0x0a, 0x06, 0x01, 0x04, 0x00, 0x23, 0x00, 0x0b,
|
||||||
|
};
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mod = try binary.parse(ally, &wasm);
|
||||||
|
defer mod.deinit();
|
||||||
|
try validate(&mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "validate rejects memory alignment larger than natural alignment" {
|
||||||
|
const binary = @import("binary.zig");
|
||||||
|
const wasm = [_]u8{
|
||||||
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
// type section: () -> ()
|
||||||
|
0x01, 0x04, 0x01, 0x60, 0x00, 0x00,
|
||||||
|
// function section: one local function, type 0
|
||||||
|
0x03, 0x02, 0x01, 0x00,
|
||||||
|
// memory section: one memory min=1
|
||||||
|
0x05, 0x03, 0x01, 0x00, 0x01,
|
||||||
|
// code: i32.const 0; i32.load align=3 offset=0; drop; end
|
||||||
|
0x0a, 0x0a, 0x01, 0x08, 0x00, 0x41, 0x00, 0x28, 0x03, 0x00, 0x1a, 0x0b,
|
||||||
|
};
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mod = try binary.parse(ally, &wasm);
|
||||||
|
defer mod.deinit();
|
||||||
|
try std.testing.expectError(ValidationError.InvalidAlignment, validate(&mod));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "validate rejects br_table with incompatible target label types" {
|
||||||
|
const binary = @import("binary.zig");
|
||||||
|
const wasm = [_]u8{
|
||||||
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
// type section: () -> (i32)
|
||||||
|
0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f,
|
||||||
|
// function section: one local function, type 0
|
||||||
|
0x03, 0x02, 0x01, 0x00,
|
||||||
|
// code:
|
||||||
|
// block (result i32)
|
||||||
|
// block
|
||||||
|
// i32.const 0
|
||||||
|
// br_table 0 1
|
||||||
|
// end
|
||||||
|
// i32.const 1
|
||||||
|
// end
|
||||||
|
// end
|
||||||
|
0x0a, 0x12, 0x01, 0x10, 0x00, 0x02, 0x7f, 0x02, 0x40, 0x41, 0x00, 0x0e, 0x01, 0x00, 0x01, 0x0b, 0x41, 0x01, 0x0b, 0x0b,
|
||||||
|
};
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mod = try binary.parse(ally, &wasm);
|
||||||
|
defer mod.deinit();
|
||||||
|
try std.testing.expectError(ValidationError.TypeMismatch, validate(&mod));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "validate rejects unknown opcode" {
|
||||||
|
const binary_mod = @import("binary.zig");
|
||||||
|
const wasm = [_]u8{
|
||||||
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
0x01, 0x04, 0x01, 0x60, 0x00, 0x00,
|
||||||
|
0x03, 0x02, 0x01, 0x00,
|
||||||
|
// body: 0 locals, 0xFF (invalid), end
|
||||||
|
0x0a, 0x05, 0x01, 0x03, 0x00, 0xff, 0x0b,
|
||||||
|
};
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mod = try binary_mod.parse(ally, &wasm);
|
||||||
|
defer mod.deinit();
|
||||||
|
try std.testing.expectError(ValidationError.UnsupportedOpcode, validate(&mod));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "validate rejects truncated f32.const immediate" {
|
||||||
|
const binary_mod = @import("binary.zig");
|
||||||
|
const wasm = [_]u8{
|
||||||
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
// type: () -> ()
|
||||||
|
0x01, 0x04, 0x01, 0x60, 0x00, 0x00,
|
||||||
|
// function: type 0
|
||||||
|
0x03, 0x02, 0x01, 0x00,
|
||||||
|
// code body declares size 4: locals=0, f32.const, only 2 bytes of immediate
|
||||||
|
0x0a, 0x06, 0x01, 0x04, 0x00, 0x43, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
const ally = std.testing.allocator;
|
||||||
|
var mod = try binary_mod.parse(ally, &wasm);
|
||||||
|
defer mod.deinit();
|
||||||
|
try std.testing.expectError(ValidationError.StackUnderflow, validate(&mod));
|
||||||
|
}
|
||||||
BIN
tests/wasm/fib.wasm
Executable file
BIN
tests/wasm/fib.wasm
Executable file
Binary file not shown.
35
tests/wasm/fib.wat
Normal file
35
tests/wasm/fib.wat
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
;; tests/wasm/fib.wat
|
||||||
|
(module
|
||||||
|
(import "env" "log" (func $log (param i32 i32)))
|
||||||
|
|
||||||
|
(memory (export "memory") 1)
|
||||||
|
(data (i32.const 0) "fib called\n")
|
||||||
|
|
||||||
|
(func $fib_impl (param $n i32) (result i32)
|
||||||
|
local.get $n
|
||||||
|
i32.const 2
|
||||||
|
i32.lt_s
|
||||||
|
if (result i32)
|
||||||
|
local.get $n
|
||||||
|
else
|
||||||
|
local.get $n
|
||||||
|
i32.const 1
|
||||||
|
i32.sub
|
||||||
|
call $fib_impl
|
||||||
|
local.get $n
|
||||||
|
i32.const 2
|
||||||
|
i32.sub
|
||||||
|
call $fib_impl
|
||||||
|
i32.add
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
(func (export "fib") (param $n i32) (result i32)
|
||||||
|
i32.const 0
|
||||||
|
i32.const 11
|
||||||
|
call $log
|
||||||
|
|
||||||
|
local.get $n
|
||||||
|
call $fib_impl
|
||||||
|
)
|
||||||
|
)
|
||||||
5
tests/wasm/fib.zig
Normal file
5
tests/wasm/fib.zig
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
extern "env" fn log(ptr: [*]u8, len: i32) void;
|
||||||
|
|
||||||
|
export fn init() void {
|
||||||
|
log(@ptrCast(@constCast("Hello world!\n")), 13);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue