first commit

This commit is contained in:
Lorenzo Torres 2026-02-24 14:28:56 +01:00
commit b574d39a39
23 changed files with 8604 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
zig-out/
.zig-cache/
**/*~

102
build.zig Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

11
src/wasm/jit_tests.zig Normal file
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

35
tests/wasm/fib.wat Normal file
View 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
View 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);
}