├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.zig
├── scripts
├── fibo-bench.zig
└── opcodes.zig
└── src
├── Memory.zig
├── execution.zig
├── func
├── basic.zig
├── global.zig
├── imports.zig
└── logic.zig
├── instance.zig
├── main.zig
├── module.zig
├── module
└── post_process.zig
├── op.zig
├── util.zig
├── wasi.zig
└── wat.zig
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Setup Zig
15 | uses: goto-bus-stop/setup-zig@v1
16 | with:
17 | version: 0.8.1
18 | - name: Run tests
19 | run: zig build test
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .*
2 | /zig-cache
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Benjamin Feng
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # wazm — Web Assembly Zig Machine
2 |
3 | | Feature | Implemented | Tested |
4 | |:------|:------:|:------:|
5 | | Bytecode parser | ✅ | ⚠️ |
6 | | Bytecode output | ❌ | ❌ |
7 | | WAT parser | 🚧 | ⚠️ |
8 | | WAT output | ❌ | ❌ |
9 | | Execution | ✅ | 🐛 |
10 | | WASI | 🚧 | ⚠️ |
11 |
12 | ```bash
13 | $ git clone https://github.com/fengb/wazm.git
14 | $ cd wazm
15 |
16 | $ zig build test
17 | ```
18 |
--------------------------------------------------------------------------------
/build.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | pub fn build(b: *std.build.Builder) void {
4 | const target = b.standardTargetOptions(.{});
5 | const mode = b.standardReleaseOptions();
6 | const exe = b.addExecutable("wazm", "src/main.zig");
7 | exe.setTarget(target);
8 | exe.setBuildMode(mode);
9 | exe.install();
10 |
11 | const run_cmd = exe.run();
12 | run_cmd.step.dependOn(b.getInstallStep());
13 | if (b.args) |args| {
14 | run_cmd.addArgs(args);
15 | }
16 | const run_step = b.step("run", "Run the app");
17 | run_step.dependOn(&run_cmd.step);
18 |
19 | var all_tests = b.addTest("src/main.zig");
20 | all_tests.setBuildMode(mode);
21 | const test_step = b.step("test", "Run library tests");
22 | test_step.dependOn(&all_tests.step);
23 |
24 | addScript(b, "opcodes");
25 | addScript(b, "fibo-bench");
26 | }
27 |
28 | fn addScript(b: *std.build.Builder, name: []const u8) void {
29 | const filename = std.fmt.allocPrint(b.allocator, "scripts/{s}.zig", .{name}) catch unreachable;
30 | const mode = b.standardReleaseOptions();
31 | const exe = b.addExecutable(name, filename);
32 | exe.setBuildMode(mode);
33 | exe.addPackagePath("self", "src/main.zig");
34 |
35 | const run_cmd = exe.run();
36 | if (b.args) |args| {
37 | run_cmd.addArgs(args);
38 | }
39 |
40 | const run_step = b.step(name, filename);
41 | run_step.dependOn(&run_cmd.step);
42 | }
43 |
--------------------------------------------------------------------------------
/scripts/fibo-bench.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Wat = @import("self").Wat;
3 |
4 | pub fn main() !void {
5 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
6 | const allocator = &gpa.allocator;
7 |
8 | const args = try std.process.argsAlloc(allocator);
9 | defer std.process.argsFree(allocator, args);
10 |
11 | const iters: u32 = try std.fmt.parseInt(u8, args[1], 10);
12 |
13 | var fbs = std.io.fixedBufferStream(
14 | \\(module
15 | \\ (func (param i32) (result i32) (local i32)
16 | \\ i32.const 1
17 | \\ local.set 1
18 | \\ block
19 | \\ local.get 0
20 | \\ i32.const 2
21 | \\ i32.lt_s
22 | \\ br_if 0
23 | \\ local.get 0
24 | \\ i32.const -1
25 | \\ i32.add
26 | \\ call 0
27 | \\ local.get 0
28 | \\ i32.const -2
29 | \\ i32.add
30 | \\ call 0
31 | \\ i32.add
32 | \\ local.set 1
33 | \\ end
34 | \\ local.get 1)
35 | \\ (export "fib" (func 0)))
36 | );
37 |
38 | var module = try Wat.parse(allocator, fbs.reader());
39 | defer module.deinit();
40 |
41 | var instance = try module.instantiate(allocator, null, struct {});
42 | defer instance.deinit();
43 |
44 | const result = try instance.call("fib", .{iters});
45 | std.debug.print("{}\n", .{result.?.I32});
46 | }
47 |
--------------------------------------------------------------------------------
/scripts/opcodes.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const self = @import("self");
3 |
4 | pub fn main() !void {
5 | const stdout = std.io.getStdOut();
6 | try outputHtml(stdout.writer());
7 | }
8 |
9 | fn outputConsole(writer: anytype) !void {
10 | for (self.Op.Meta.all) |op, i| {
11 | var buf = [_]u8{' '} ** 13;
12 | if (op) |o| {
13 | std.mem.copy(u8, &buf, o.name);
14 | }
15 | try writer.print("{}", .{buf});
16 |
17 | if (i % 0x10 == 0xF) {
18 | try writer.print("\n", .{});
19 | }
20 | }
21 | }
22 |
23 | fn outputHtml(writer: anytype) !void {
24 | try writer.print("\n
\n\n", .{});
25 |
26 | try writer.print("\n | \n", .{});
27 | for ([_]u8{0} ** 16) |_, i| {
28 | try writer.print("_{X} | \n", .{i});
29 | }
30 | try writer.print("
\n", .{});
31 |
32 | for (self.Op.Meta.all) |op, i| {
33 | if (i % 0x10 == 0x0) {
34 | try writer.print("\n{X}_ | \n", .{i / 16});
35 | }
36 | try writer.print("\n", .{});
37 | if (op) |o| {
38 | try writer.print("{s} \n", .{o.name});
39 | try writer.print("(", .{});
40 | if (o.pop.len > 0) {
41 | for (o.pop) |change| {
42 | try writer.print("{s} ", .{@tagName(change)});
43 | }
44 | }
45 | try writer.print(") ", .{});
46 | try writer.print("→ ({s})\n", .{if (o.push) |change| @tagName(change) else ""});
47 | }
48 | try writer.print(" | ", .{});
49 |
50 | if (i % 0x10 == 0xF) {
51 | try writer.print("
\n", .{});
52 | }
53 | }
54 | try writer.print("
\n\n\n", .{});
55 | }
56 |
--------------------------------------------------------------------------------
/src/Memory.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Memory = @This();
4 |
5 | pages: std.ArrayListUnmanaged(*[65536]u8),
6 | allocator: *std.mem.Allocator,
7 | context: ?*c_void,
8 |
9 | const page_size = 65536;
10 |
11 | pub fn init(allocator: *std.mem.Allocator, context: ?*c_void, initial_pages: u16) !Memory {
12 | var result = Memory{ .allocator = allocator, .pages = .{}, .context = context };
13 | try result.grow(initial_pages);
14 | return result;
15 | }
16 |
17 | pub fn deinit(self: *Memory) void {
18 | for (self.pages.items) |page| {
19 | self.allocator.destroy(page);
20 | }
21 | self.pages.deinit(self.allocator);
22 | self.* = undefined;
23 | }
24 |
25 | pub fn pageCount(self: Memory) u16 {
26 | return @intCast(u16, self.pages.items.len);
27 | }
28 |
29 | pub fn ext(self: Memory, comptime T: type) *T {
30 | return @ptrCast(*T, @alignCast(@alignOf(T), self.context));
31 | }
32 |
33 | pub fn grow(self: *Memory, additional_pages: u16) !void {
34 | const new_page_count = self.pageCount() + additional_pages;
35 | if (new_page_count > 65536) {
36 | return error.OutOfMemory;
37 | }
38 | try self.pages.ensureCapacity(self.allocator, new_page_count);
39 |
40 | var i: u16 = 0;
41 | while (i < additional_pages) : (i += 1) {
42 | const page = try self.allocator.alloc(u8, page_size);
43 | self.pages.appendAssumeCapacity(@ptrCast(*[page_size]u8, page.ptr));
44 | }
45 | }
46 |
47 | pub fn load(self: Memory, comptime T: type, start: u32, offset: u32) !T {
48 | const Int = std.meta.Int(.unsigned, @bitSizeOf(T));
49 | const idx = try std.math.add(u32, start, offset);
50 | const bytes = try self.pageChunk(idx);
51 | // TODO: handle split byte boundary
52 | return @bitCast(T, std.mem.readIntLittle(Int, bytes[0..@sizeOf(T)]));
53 | }
54 |
55 | pub fn store(self: Memory, comptime T: type, start: u32, offset: u32, value: T) !void {
56 | const Int = std.meta.Int(.unsigned, @bitSizeOf(T));
57 | const idx = try std.math.add(u32, start, offset);
58 | const bytes = try self.pageChunk(idx);
59 | // TODO: handle split byte boundary
60 | std.mem.writeIntLittle(Int, bytes[0..@sizeOf(T)], @bitCast(Int, value));
61 | }
62 |
63 | fn pageChunk(self: Memory, idx: u32) ![]u8 {
64 | const page_num = idx / page_size;
65 | const offset = idx % page_size;
66 | if (page_num >= self.pageCount()) {
67 | std.log.info("{} > {}", .{ page_num, self.pageCount() });
68 | return error.OutOfBounds;
69 | }
70 | const page = self.pages.items[page_num];
71 | return page[offset..];
72 | }
73 |
74 | pub fn get(self: Memory, ptr: anytype) !@TypeOf(ptr).Pointee {
75 | return self.load(@TypeOf(ptr).Pointee, @enumToInt(ptr), 0);
76 | }
77 |
78 | pub fn iterBytes(self: Memory, ptr: P(u8), size: u32) ByteIterator {
79 | return .{
80 | .memory = self,
81 | .ptr = ptr,
82 | .remaining = size,
83 | };
84 | }
85 |
86 | const ByteIterator = struct {
87 | memory: Memory,
88 | ptr: P(u8),
89 | remaining: u32,
90 |
91 | pub fn next(iter: *ByteIterator) !?[]u8 {
92 | if (iter.remaining == 0) {
93 | return null;
94 | }
95 |
96 | const bytes = try iter.memory.pageChunk(@enumToInt(iter.ptr));
97 |
98 | const size = @intCast(u17, bytes.len);
99 | if (size >= iter.remaining) {
100 | defer iter.remaining = 0;
101 | return bytes[0..iter.remaining];
102 | } else {
103 | iter.remaining -= size;
104 | iter.ptr = try iter.ptr.add(size);
105 | return bytes;
106 | }
107 | }
108 | };
109 |
110 | pub fn set(self: Memory, ptr: anytype, value: @TypeOf(ptr).Pointee) !void {
111 | return self.store(@TypeOf(ptr).Pointee, @enumToInt(ptr), 0, value);
112 | }
113 |
114 | pub fn setMany(self: Memory, ptr: anytype, values: []const @TypeOf(ptr).Pointee) !void {
115 | for (values) |value, i| {
116 | try self.set(try ptr.add(@intCast(u32, i)), value);
117 | }
118 | }
119 |
120 | pub fn P(comptime T: type) type {
121 | return enum(u32) {
122 | _,
123 |
124 | const Self = @This();
125 | pub const Pointee = T;
126 | pub const stride = @sizeOf(T);
127 |
128 | pub fn init(addr: u32) Self {
129 | return @intToEnum(Self, addr);
130 | }
131 |
132 | pub fn add(self: Self, change: u32) !Self {
133 | return init(try std.math.add(u32, @enumToInt(self), try std.math.mul(u32, change, stride)));
134 | }
135 |
136 | pub fn sub(self: Self, change: u32) !Self {
137 | return init(try std.math.sub(u32, @enumToInt(self), try std.math.mul(u32, change, stride)));
138 | }
139 | };
140 | }
141 |
142 | test "grow" {
143 | var mem = try Memory.init(std.testing.allocator, null, 1);
144 | defer mem.deinit();
145 |
146 | try std.testing.expectEqual(@as(u16, 1), mem.pageCount());
147 |
148 | try mem.grow(1);
149 | try std.testing.expectEqual(@as(u16, 2), mem.pageCount());
150 | }
151 |
152 | test "get/set" {
153 | var mem = try Memory.init(std.testing.allocator, null, 1);
154 | defer mem.deinit();
155 |
156 | try std.testing.expectEqual(@as(u16, 1), mem.pageCount());
157 |
158 | const ptr1 = P(u32).init(1234);
159 | const ptr2 = P(u32).init(4321);
160 | try mem.set(ptr1, 69);
161 | try mem.set(ptr2, 420);
162 |
163 | try std.testing.expectEqual(@as(u32, 69), try mem.get(ptr1));
164 | try std.testing.expectEqual(@as(u32, 420), try mem.get(ptr2));
165 | }
166 |
--------------------------------------------------------------------------------
/src/execution.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Op = @import("op.zig");
3 | const Module = @import("module.zig");
4 | const Instance = @import("instance.zig");
5 | const Memory = @import("Memory.zig");
6 | const util = @import("util.zig");
7 |
8 | const Execution = @This();
9 |
10 | memory: *Memory,
11 | funcs: []const Instance.Func,
12 | allocator: *std.mem.Allocator,
13 | instance: *const Instance,
14 |
15 | stack: []Op.Fixval,
16 | stack_top: usize,
17 | current_frame: Frame = Frame.terminus(),
18 |
19 | result: Op.WasmTrap!?Op.Fixval,
20 |
21 | pub fn run(instance: *Instance, stack: []Op.Fixval, func_id: usize, params: []Op.Fixval) !?Op.Fixval {
22 | var ctx = Execution{
23 | .memory = &instance.memory,
24 | .funcs = instance.funcs,
25 | .allocator = instance.allocator,
26 | .instance = instance,
27 |
28 | .stack = stack,
29 | .stack_top = 0,
30 |
31 | .result = undefined,
32 | };
33 |
34 | // initCall assumes the params are already pushed onto the stack
35 | for (params) |param| {
36 | try ctx.push(Op.Fixval, param);
37 | }
38 |
39 | try ctx.initCall(func_id);
40 | if (ctx.current_frame.isTerminus()) {
41 | return switch (ctx.stack_top) {
42 | 0 => null,
43 | 1 => ctx.stack[0],
44 | else => unreachable,
45 | };
46 | }
47 |
48 | tailDispatch(&ctx, undefined);
49 | return ctx.result;
50 | }
51 |
52 | fn tailDispatch(self: *Execution, arg: Op.Arg) callconv(.C) void {
53 | const func = self.funcs[self.current_frame.func];
54 | if (self.current_frame.instr >= func.kind.instrs.len) {
55 | return @call(.{ .modifier = .always_tail }, tailUnwind, .{ self, arg });
56 | }
57 |
58 | const instr = func.kind.instrs[self.current_frame.instr];
59 | self.current_frame.instr += 1;
60 |
61 | const TailCalls = comptime blk: {
62 | var result: [256]fn (self: *Execution, arg: Op.Arg) callconv(.C) void = undefined;
63 | @setEvalBranchQuota(10000);
64 | for (Op.Meta.sparse) |meta| {
65 | const Tail = TailWrap(meta.code);
66 | result[@enumToInt(meta.code)] = Tail.call;
67 | }
68 | break :blk result;
69 | };
70 |
71 | return @call(.{ .modifier = .always_tail }, TailCalls[@enumToInt(instr.op)], .{ self, instr.arg });
72 | }
73 |
74 | fn tailUnwind(self: *Execution, arg: Op.Arg) callconv(.C) void {
75 | const result = self.unwindCall();
76 |
77 | if (self.current_frame.isTerminus()) {
78 | std.debug.assert(self.stack_top == 0);
79 | self.result = result;
80 | return;
81 | } else {
82 | if (result) |res| {
83 | self.push(Op.Fixval, res) catch unreachable;
84 | }
85 | }
86 | return @call(.{ .modifier = .always_inline }, tailDispatch, .{ self, arg });
87 | }
88 |
89 | fn TailWrap(comptime opcode: std.wasm.Opcode) type {
90 | const meta = Op.Meta.of(opcode);
91 | return struct {
92 | fn call(ctx: *Execution, arg: Op.Arg) callconv(.C) void {
93 | const pops = ctx.popN(meta.pop.len);
94 | const result = @call(
95 | .{ .modifier = .always_inline },
96 | Op.stepName,
97 | .{ meta.func_name, ctx, arg, pops.ptr },
98 | ) catch |err| {
99 | ctx.result = err;
100 | return;
101 | };
102 |
103 | if (result) |res| {
104 | ctx.push(@TypeOf(res), res) catch |err| {
105 | ctx.result = err;
106 | return;
107 | };
108 | }
109 | return @call(.{ .modifier = .always_inline }, tailDispatch, .{ ctx, arg });
110 | }
111 | };
112 | }
113 |
114 | pub fn getLocal(self: Execution, idx: usize) Op.Fixval {
115 | return self.stack[idx + self.current_frame.locals_begin];
116 | }
117 |
118 | pub fn getLocals(self: Execution, idx: usize, len: usize) []Op.Fixval {
119 | return self.stack[idx + self.current_frame.locals_begin ..][0..len];
120 | }
121 |
122 | pub fn setLocal(self: Execution, idx: usize, value: Op.Fixval) void {
123 | self.stack[idx + self.current_frame.locals_begin] = value;
124 | }
125 |
126 | pub fn getGlobal(self: Execution, idx: usize) Op.Fixval {
127 | return self.instance.globals[idx];
128 | }
129 |
130 | pub fn setGlobal(self: Execution, idx: usize, value: Op.Fixval) void {
131 | self.instance.globals[idx] = value;
132 | }
133 |
134 | pub fn initCall(self: *Execution, func_id: usize) !void {
135 | const func = self.funcs[func_id];
136 | if (func.kind == .imported) {
137 | // TODO: investigate imported calling another imported
138 | const params = self.popN(func.params.len);
139 | const result = try func.kind.imported.func(self, params);
140 |
141 | if (result) |res| {
142 | self.push(Op.Fixval, res) catch unreachable;
143 | }
144 | } else {
145 | const locals_begin = self.stack_top - func.params.len;
146 | for (func.locals) |local| {
147 | // TODO: assert params on the callstack are correct
148 | _ = local;
149 | try self.push(u128, 0);
150 | }
151 |
152 | try self.push(Frame, self.current_frame);
153 |
154 | self.current_frame = .{
155 | .func = @intCast(u32, func_id),
156 | .instr = 0,
157 | .stack_begin = @intCast(u32, self.stack_top),
158 | .locals_begin = @intCast(u32, locals_begin),
159 | };
160 | }
161 | }
162 |
163 | pub fn unwindCall(self: *Execution) ?Op.Fixval {
164 | const func = self.funcs[self.current_frame.func];
165 |
166 | const result = if (func.result) |_|
167 | self.pop(Op.Fixval)
168 | else
169 | null;
170 |
171 | self.stack_top = self.current_frame.stack_begin;
172 |
173 | self.current_frame = self.pop(Frame);
174 | _ = self.popN(func.params.len + func.locals.len);
175 |
176 | return result;
177 | }
178 |
179 | pub fn jump(self: *Execution, table_idx: ?u32) void {
180 | const meta = self.instance.module.post_process.?.jumps.get(.{
181 | .func = self.current_frame.func,
182 | .instr = self.current_frame.instr - 1,
183 | }).?;
184 |
185 | const target = if (table_idx) |idx|
186 | meta.many[idx]
187 | else
188 | meta.one;
189 |
190 | const result = if (target.has_value)
191 | self.peek(Op.Fixval)
192 | else
193 | null;
194 |
195 | _ = self.popN(target.stack_unroll);
196 | // Jumps to 1 after the target.
197 | // If target == "end", this skips a noop and is faster.
198 | // If target == "else", this correctly skips over the annoying check.
199 | self.current_frame.instr = target.addr + 1;
200 |
201 | if (result) |value| {
202 | self.push(Op.Fixval, value) catch unreachable;
203 | }
204 | }
205 |
206 | pub fn peek(self: *Execution, comptime T: type) T {
207 | std.debug.assert(@sizeOf(T) == 16);
208 | return @bitCast(T, self.stack[self.stack_top - 1]);
209 | }
210 |
211 | pub fn pop(self: *Execution, comptime T: type) T {
212 | std.debug.assert(@sizeOf(T) == 16);
213 | self.stack_top -= 1;
214 | return @bitCast(T, self.stack[self.stack_top]);
215 | }
216 |
217 | pub fn popN(self: *Execution, size: usize) []Op.Fixval {
218 | std.debug.assert(self.stack_top + size <= self.stack.len);
219 | self.stack_top -= size;
220 | return self.stack[self.stack_top..][0..size];
221 | }
222 |
223 | pub fn push(self: *Execution, comptime T: type, value: T) !void {
224 | std.debug.assert(@sizeOf(T) == 16);
225 | self.stack[self.stack_top] = @bitCast(Op.Fixval, value);
226 | self.stack_top = try std.math.add(usize, self.stack_top, 1);
227 | }
228 |
229 | pub fn pushOpaque(self: *Execution, comptime len: usize) !*[len]Op.Fixval {
230 | const start = self.stack_top;
231 | self.stack_top = try std.math.add(usize, len, 1);
232 | return self.stack[start..][0..len];
233 | }
234 |
235 | const Frame = extern struct {
236 | func: u32,
237 | instr: u32,
238 | stack_begin: u32,
239 | locals_begin: u32,
240 |
241 | pub fn terminus() Frame {
242 | return @bitCast(Frame, @as(u128, 0));
243 | }
244 |
245 | pub fn isTerminus(self: Frame) bool {
246 | return @bitCast(u128, self) == 0;
247 | }
248 | };
249 |
--------------------------------------------------------------------------------
/src/func/basic.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Wat = @import("../wat.zig");
4 | const Instance = @import("../instance.zig");
5 |
6 | test "i32 math" {
7 | var fbs = std.io.fixedBufferStream(
8 | \\(module
9 | \\ (func (result i32)
10 | \\ i32.const 40
11 | \\ i32.const 2
12 | \\ i32.add)
13 | \\ (func (result i32)
14 | \\ i32.const 40
15 | \\ i32.const 2
16 | \\ i32.sub)
17 | \\ (func (result i32)
18 | \\ i32.const 40
19 | \\ i32.const 2
20 | \\ i32.mul)
21 | \\ (func (result i32)
22 | \\ i32.const 40
23 | \\ i32.const 2
24 | \\ i32.div_s)
25 | \\ (export "add" (func 0))
26 | \\ (export "sub" (func 1))
27 | \\ (export "mul" (func 2))
28 | \\ (export "div" (func 3)))
29 | );
30 | var module = try Wat.parse(std.testing.allocator, fbs.reader());
31 | defer module.deinit();
32 |
33 | var instance = try module.instantiate(std.testing.allocator, null, struct {});
34 | defer instance.deinit();
35 |
36 | {
37 | const result = try instance.call("add", .{});
38 | try std.testing.expectEqual(@as(i32, 42), result.?.I32);
39 | }
40 | {
41 | const result = try instance.call("sub", .{});
42 | try std.testing.expectEqual(@as(i32, 38), result.?.I32);
43 | }
44 | {
45 | const result = try instance.call("mul", .{});
46 | try std.testing.expectEqual(@as(i32, 80), result.?.I32);
47 | }
48 | {
49 | const result = try instance.call("div", .{});
50 | try std.testing.expectEqual(@as(i32, 20), result.?.I32);
51 | }
52 | }
53 |
54 | test "i64 math" {
55 | var fbs = std.io.fixedBufferStream(
56 | \\(module
57 | \\ (func (result i64)
58 | \\ i64.const 40
59 | \\ i64.const 2
60 | \\ i64.add)
61 | \\ (func (result i64)
62 | \\ i64.const 40
63 | \\ i64.const 2
64 | \\ i64.sub)
65 | \\ (func (result i64)
66 | \\ i64.const 40
67 | \\ i64.const 2
68 | \\ i64.mul)
69 | \\ (func (result i64)
70 | \\ i64.const 40
71 | \\ i64.const 2
72 | \\ i64.div_s)
73 | \\ (export "add" (func 0))
74 | \\ (export "sub" (func 1))
75 | \\ (export "mul" (func 2))
76 | \\ (export "div" (func 3)))
77 | );
78 | var module = try Wat.parse(std.testing.allocator, fbs.reader());
79 | defer module.deinit();
80 |
81 | var instance = try module.instantiate(std.testing.allocator, null, struct {});
82 | defer instance.deinit();
83 |
84 | {
85 | const result = try instance.call("add", .{});
86 | try std.testing.expectEqual(@as(i64, 42), result.?.I64);
87 | }
88 | {
89 | const result = try instance.call("sub", .{});
90 | try std.testing.expectEqual(@as(i64, 38), result.?.I64);
91 | }
92 | {
93 | const result = try instance.call("mul", .{});
94 | try std.testing.expectEqual(@as(i64, 80), result.?.I64);
95 | }
96 | {
97 | const result = try instance.call("div", .{});
98 | try std.testing.expectEqual(@as(i64, 20), result.?.I64);
99 | }
100 | }
101 |
102 | test "f32 math" {
103 | var fbs = std.io.fixedBufferStream(
104 | \\(module
105 | \\ (func (result f32)
106 | \\ f32.const 40
107 | \\ f32.const 2
108 | \\ f32.add)
109 | \\ (func (result f32)
110 | \\ f32.const 40
111 | \\ f32.const 2
112 | \\ f32.sub)
113 | \\ (func (result f32)
114 | \\ f32.const 40
115 | \\ f32.const 2
116 | \\ f32.mul)
117 | \\ (func (result f32)
118 | \\ f32.const 40
119 | \\ f32.const 2
120 | \\ f32.div)
121 | \\ (export "add" (func 0))
122 | \\ (export "sub" (func 1))
123 | \\ (export "mul" (func 2))
124 | \\ (export "div" (func 3)))
125 | );
126 | var module = try Wat.parse(std.testing.allocator, fbs.reader());
127 | defer module.deinit();
128 |
129 | var instance = try module.instantiate(std.testing.allocator, null, struct {});
130 | defer instance.deinit();
131 |
132 | {
133 | const result = try instance.call("add", .{});
134 | try std.testing.expectEqual(@as(f32, 42), result.?.F32);
135 | }
136 | {
137 | const result = try instance.call("sub", .{});
138 | try std.testing.expectEqual(@as(f32, 38), result.?.F32);
139 | }
140 | {
141 | const result = try instance.call("mul", .{});
142 | try std.testing.expectEqual(@as(f32, 80), result.?.F32);
143 | }
144 | {
145 | const result = try instance.call("div", .{});
146 | try std.testing.expectEqual(@as(f32, 20), result.?.F32);
147 | }
148 | }
149 |
150 | test "f64 math" {
151 | var fbs = std.io.fixedBufferStream(
152 | \\(module
153 | \\ (func (result f64)
154 | \\ f64.const 1
155 | \\ f64.const 2
156 | \\ f64.add)
157 | \\ (func (result f64)
158 | \\ f64.const 1
159 | \\ f64.const 2
160 | \\ f64.sub)
161 | \\ (func (result f64)
162 | \\ f64.const 1
163 | \\ f64.const 2
164 | \\ f64.mul)
165 | \\ (func (result f64)
166 | \\ f64.const 1
167 | \\ f64.const 2
168 | \\ f64.div)
169 | \\ (export "add" (func 0))
170 | \\ (export "sub" (func 1))
171 | \\ (export "mul" (func 2))
172 | \\ (export "div" (func 3)))
173 | );
174 | var module = try Wat.parse(std.testing.allocator, fbs.reader());
175 | defer module.deinit();
176 |
177 | var instance = try module.instantiate(std.testing.allocator, null, struct {});
178 | defer instance.deinit();
179 |
180 | {
181 | const result = try instance.call("add", .{});
182 | try std.testing.expectEqual(@as(f64, 3), result.?.F64);
183 | }
184 | {
185 | const result = try instance.call("sub", .{});
186 | try std.testing.expectEqual(@as(f64, -1), result.?.F64);
187 | }
188 | {
189 | const result = try instance.call("mul", .{});
190 | try std.testing.expectEqual(@as(f64, 2), result.?.F64);
191 | }
192 | {
193 | const result = try instance.call("div", .{});
194 | try std.testing.expectEqual(@as(f64, 0.5), result.?.F64);
195 | }
196 | }
197 |
198 | test "call with args" {
199 | var fbs = std.io.fixedBufferStream(
200 | \\(module
201 | \\ (func (param i32) (param i32) (result i32)
202 | \\ local.get 0
203 | \\ local.get 1
204 | \\ i32.add)
205 | \\ (func (param i32) (param i32) (result i32) (local i32) (local i64) (local f64)
206 | \\ local.get 0
207 | \\ local.get 1
208 | \\ i32.add)
209 | \\ (export "add" (func 0))
210 | \\ (export "addtemp" (func 1)))
211 | );
212 | var module = try Wat.parse(std.testing.allocator, fbs.reader());
213 | defer module.deinit();
214 |
215 | var instance = try module.instantiate(std.testing.allocator, null, struct {});
216 | defer instance.deinit();
217 |
218 | try std.testing.expectError(error.TypeSignatureMismatch, instance.call("add", &[0]Instance.Value{}));
219 |
220 | {
221 | const result = try instance.call("add", .{ @as(i32, 16), @as(i32, 8) });
222 | try std.testing.expectEqual(@as(i32, 24), result.?.I32);
223 | }
224 |
225 | {
226 | const result = try instance.call("addtemp", .{ @as(i32, 16), @as(i32, 8) });
227 | try std.testing.expectEqual(@as(i32, 24), result.?.I32);
228 | }
229 | }
230 |
231 | test "call call call" {
232 | var fbs = std.io.fixedBufferStream(
233 | \\(module
234 | \\ (func (param i32) (param i32) (result i32)
235 | \\ local.get 0
236 | \\ local.get 1
237 | \\ i32.add)
238 | \\ (func (param i32) (param i32) (result i32)
239 | \\ local.get 0
240 | \\ local.get 1
241 | \\ call 0
242 | \\ i32.const 2
243 | \\ i32.mul)
244 | \\ (export "addDouble" (func 1)))
245 | );
246 | var module = try Wat.parse(std.testing.allocator, fbs.reader());
247 | defer module.deinit();
248 |
249 | var instance = try module.instantiate(std.testing.allocator, null, struct {});
250 | defer instance.deinit();
251 |
252 | {
253 | const result = try instance.call("addDouble", .{ @as(i32, 16), @as(i32, 8) });
254 | try std.testing.expectEqual(@as(i32, 48), result.?.I32);
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/src/func/global.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Wat = @import("../wat.zig");
4 | const Instance = @import("../instance.zig");
5 |
6 | test "get global" {
7 | var fbs = std.io.fixedBufferStream(
8 | \\(module
9 | \\ (global (;0;) i32 (i32.const 10))
10 | \\ (func (param i32) (result i32)
11 | \\ local.get 0
12 | \\ global.get 0
13 | \\ i32.add)
14 | \\ (export "add" (func 0)))
15 | );
16 | var module = try Wat.parse(std.testing.allocator, fbs.reader());
17 | defer module.deinit();
18 |
19 | var instance = try module.instantiate(std.testing.allocator, null, struct {});
20 | defer instance.deinit();
21 |
22 | {
23 | const result = try instance.call("add", .{@as(i32, 1)});
24 | try std.testing.expectEqual(@as(i32, 11), result.?.I32);
25 | }
26 | {
27 | const result = try instance.call("add", .{@as(i32, 5)});
28 | try std.testing.expectEqual(@as(i32, 15), result.?.I32);
29 | }
30 | }
31 |
32 | test "set global" {
33 | var fbs = std.io.fixedBufferStream(
34 | \\(module
35 | \\ (global (;0;) i32 (i32.const 0))
36 | \\ (func (param i32)
37 | \\ local.get 0
38 | \\ global.set 0)
39 | \\ (export "get" (func 0)))
40 | );
41 | var module = try Wat.parse(std.testing.allocator, fbs.reader());
42 | defer module.deinit();
43 |
44 | var instance = try module.instantiate(std.testing.allocator, null, struct {});
45 | defer instance.deinit();
46 |
47 | {
48 | const result = try instance.call("get", .{@as(i32, 1)});
49 | try std.testing.expectEqual(Instance.Value{ .I32 = 1 }, instance.getGlobal(0));
50 | }
51 |
52 | {
53 | const result = try instance.call("get", .{@as(i32, 5)});
54 | try std.testing.expectEqual(Instance.Value{ .I32 = 5 }, instance.getGlobal(0));
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/func/imports.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Wat = @import("../wat.zig");
4 | const Instance = @import("../instance.zig");
5 | const Memory = @import("../Memory.zig");
6 |
7 | test "import" {
8 | var fbs = std.io.fixedBufferStream(
9 | \\(module
10 | \\ (type (;0;) (func (param i32) (result i32)))
11 | \\ (import "env" "thing" (func (type 0)))
12 | \\ (export "run" (func 0)))
13 | );
14 | var module = try Wat.parse(std.testing.allocator, fbs.reader());
15 | defer module.deinit();
16 |
17 | var instance = try module.instantiate(std.testing.allocator, null, struct {
18 | pub const env = struct {
19 | pub fn thing(mem: *Memory, arg: i32) i32 {
20 | return arg + 1;
21 | }
22 | };
23 | });
24 | defer instance.deinit();
25 |
26 | {
27 | const result = try instance.call("run", .{@as(i32, 1)});
28 | try std.testing.expectEqual(@as(i32, 2), result.?.I32);
29 | }
30 | {
31 | const result = try instance.call("run", .{@as(i32, 42)});
32 | try std.testing.expectEqual(@as(i32, 43), result.?.I32);
33 | }
34 | }
35 |
36 | test "import multiple" {
37 | var fbs = std.io.fixedBufferStream(
38 | \\(module
39 | \\ (type (;0;) (func (param i32) (param i32) (result i32)))
40 | \\ (import "env" "add" (func (type 0)))
41 | \\ (import "env" "mul" (func (type 0)))
42 | \\ (export "add" (func 0))
43 | \\ (export "mul" (func 1)))
44 | );
45 | var module = try Wat.parse(std.testing.allocator, fbs.reader());
46 | defer module.deinit();
47 |
48 | var instance = try module.instantiate(std.testing.allocator, null, struct {
49 | pub const env = struct {
50 | pub fn add(mem: *Memory, arg0: i32, arg1: i32) i32 {
51 | return arg0 + arg1;
52 | }
53 |
54 | pub fn mul(mem: *Memory, arg0: i32, arg1: i32) i32 {
55 | return arg0 * arg1;
56 | }
57 | };
58 | });
59 | defer instance.deinit();
60 |
61 | {
62 | const result = try instance.call("add", .{ @as(i32, 2), @as(i32, 3) });
63 | try std.testing.expectEqual(@as(i32, 5), result.?.I32);
64 | }
65 |
66 | {
67 | const result = try instance.call("mul", .{ @as(i32, 2), @as(i32, 3) });
68 | try std.testing.expectEqual(@as(i32, 6), result.?.I32);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/func/logic.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Wat = @import("../wat.zig");
4 | const Instance = @import("../instance.zig");
5 |
6 | test "if/else" {
7 | var fbs = std.io.fixedBufferStream(
8 | \\(module
9 | \\ (func (param i32) (result i32)
10 | \\ local.get 0
11 | \\ if (result i32)
12 | \\ i32.const 1
13 | \\ else
14 | \\ i32.const 42
15 | \\ end)
16 | \\ (export "if" (func 0)))
17 | );
18 | var module = try Wat.parse(std.testing.allocator, fbs.reader());
19 | defer module.deinit();
20 |
21 | var instance = try module.instantiate(std.testing.allocator, null, struct {});
22 | defer instance.deinit();
23 |
24 | {
25 | const result = try instance.call("if", .{@as(i32, 1)});
26 | try std.testing.expectEqual(@as(i32, 1), result.?.I32);
27 | }
28 | {
29 | const result = try instance.call("if", .{@as(i32, 0)});
30 | try std.testing.expectEqual(@as(i32, 42), result.?.I32);
31 | }
32 | }
33 |
34 | test "select" {
35 | var fbs = std.io.fixedBufferStream(
36 | \\(module
37 | \\ (func (param i32) (result i32)
38 | \\ i32.const 1
39 | \\ i32.const 42
40 | \\ local.get 0
41 | \\ select)
42 | \\ (export "if" (func 0)))
43 | );
44 | var module = try Wat.parse(std.testing.allocator, fbs.reader());
45 | defer module.deinit();
46 |
47 | var instance = try module.instantiate(std.testing.allocator, null, struct {});
48 | defer instance.deinit();
49 |
50 | {
51 | const result = try instance.call("if", .{@as(i32, 1)});
52 | try std.testing.expectEqual(@as(i32, 1), result.?.I32);
53 | }
54 | {
55 | const result = try instance.call("if", .{@as(i32, 0)});
56 | try std.testing.expectEqual(@as(i32, 42), result.?.I32);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/instance.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Module = @import("module.zig");
4 | const Op = @import("op.zig");
5 | const Execution = @import("execution.zig");
6 | const Memory = @import("Memory.zig");
7 |
8 | const Instance = @This();
9 |
10 | module: *const Module,
11 | allocator: *std.mem.Allocator,
12 | memory: Memory,
13 | exports: std.StringHashMap(Export),
14 | funcs: []const Func,
15 | globals: []Op.Fixval,
16 |
17 | // TODO: revisit if wasm ever becomes multi-threaded
18 | mutex: std.Thread.Mutex,
19 |
20 | pub fn init(module: *const Module, allocator: *std.mem.Allocator, context: ?*c_void, comptime Imports: type) !Instance {
21 | var exports = std.StringHashMap(Export).init(allocator);
22 | errdefer exports.deinit();
23 | for (module.@"export") |exp| {
24 | try exports.putNoClobber(exp.field, .{ .Func = exp.index });
25 | }
26 |
27 | var funcs = std.ArrayList(Func).init(allocator);
28 | errdefer funcs.deinit();
29 |
30 | const karen = ImportManager(Imports);
31 |
32 | for (module.post_process.?.import_funcs) |import_func| {
33 | const type_idx = @enumToInt(import_func.type_idx);
34 | const func_type = module.@"type"[type_idx];
35 | const lookup = karen.get(import_func.module, import_func.field) orelse return error.ImportNotFound;
36 | if (!std.meta.eql(lookup.return_type, func_type.return_type)) {
37 | return error.ImportSignatureMismatch;
38 | }
39 | if (!std.mem.eql(Module.Type.Value, lookup.param_types, func_type.param_types)) {
40 | return error.ImportSignatureMismatch;
41 | }
42 |
43 | try funcs.append(.{
44 | .func_type = type_idx,
45 | .params = func_type.param_types,
46 | .result = func_type.return_type,
47 | .locals = &[0]Module.Type.Value{},
48 | .kind = .{
49 | .imported = .{ .func = lookup.func, .frame_size = lookup.frame_size },
50 | },
51 | });
52 | }
53 |
54 | for (module.code) |code, i| {
55 | const type_idx = @enumToInt(module.function[i].type_idx);
56 | const func_type = module.@"type"[type_idx];
57 | try funcs.append(.{
58 | .func_type = type_idx,
59 | .params = func_type.param_types,
60 | .result = func_type.return_type,
61 | .locals = code.locals,
62 | .kind = .{ .instrs = code.body },
63 | });
64 | }
65 |
66 | var globals = try allocator.alloc(Op.Fixval, module.global.len);
67 | errdefer allocator.free(globals);
68 |
69 | for (module.global) |global, i| {
70 | globals[i] = switch (global.type.content_type) {
71 | .I32 => .{ .I32 = global.init.i32_const },
72 | .I64 => .{ .I64 = global.init.i64_const },
73 | .F32 => .{ .F32 = global.init.f32_const },
74 | .F64 => .{ .F64 = global.init.f64_const },
75 | };
76 | }
77 |
78 | const initial = if (module.memory.len > 0) @intCast(u16, module.memory[0].limits.initial) else 0;
79 | var memory = try Memory.init(allocator, context, initial);
80 | errdefer memory.deinit();
81 |
82 | for (module.data) |data| {
83 | // TODO: add validation in processing
84 | const offset = switch (data.offset) {
85 | .i32_const => |num| num,
86 | .global_get => @panic("I don't know"),
87 | else => unreachable,
88 | };
89 | const addr = Memory.P(u8).init(@enumToInt(data.index) + @intCast(u32, offset));
90 | try memory.setMany(addr, data.data);
91 | }
92 |
93 | return Instance{
94 | .module = module,
95 | .mutex = .{},
96 | .memory = memory,
97 | .exports = exports,
98 | .funcs = funcs.toOwnedSlice(),
99 | .globals = globals,
100 | .allocator = allocator,
101 | };
102 | }
103 |
104 | pub fn deinit(self: *Instance) void {
105 | self.allocator.free(self.funcs);
106 | self.allocator.free(self.globals);
107 | self.memory.deinit();
108 | self.exports.deinit();
109 | self.* = undefined;
110 | }
111 |
112 | pub fn call(self: *Instance, name: []const u8, params: anytype) !?Value {
113 | const lock = self.mutex.acquire();
114 | defer lock.release();
115 |
116 | const exp = self.exports.get(name) orelse return error.ExportNotFound;
117 | if (exp != .Func) {
118 | return error.ExportNotAFunction;
119 | }
120 |
121 | const func_id = exp.Func;
122 | const func = self.funcs[func_id];
123 | if (params.len != func.params.len) {
124 | return error.TypeSignatureMismatch;
125 | }
126 |
127 | var converted_params: [params.len]Op.Fixval = undefined;
128 | inline for ([_]void{{}} ** params.len) |_, i| {
129 | const param_type: Module.Type.Value = switch (@TypeOf(params[i])) {
130 | i32, u32 => .I32,
131 | i64, u64 => .I64,
132 | f32 => .F32,
133 | f64 => .F64,
134 | else => @compileError("Unsupported type"),
135 | };
136 | if (param_type != func.params[i]) return error.TypeSignatureMismatch;
137 |
138 | converted_params[i] = switch (@TypeOf(params[i])) {
139 | i32 => .{ .I32 = params[i] },
140 | i64 => .{ .I64 = params[i] },
141 | u32 => .{ .U32 = params[i] },
142 | u64 => .{ .U64 = params[i] },
143 | f32 => .{ .F32 = params[i] },
144 | f64 => .{ .F64 = params[i] },
145 | else => @compileError("Unsupported type"),
146 | };
147 | }
148 |
149 | var stack: [1 << 10]Op.Fixval = undefined;
150 | const result = try Execution.run(self, &stack, func_id, converted_params[0..params.len]);
151 | if (result) |res| {
152 | return switch (func.result.?) {
153 | .I32 => Value{ .I32 = res.I32 },
154 | .I64 => Value{ .I64 = res.I64 },
155 | .F32 => Value{ .F32 = res.F32 },
156 | .F64 => Value{ .F64 = res.F64 },
157 | };
158 | } else {
159 | return null;
160 | }
161 | }
162 |
163 | pub fn getGlobal(self: Instance, idx: usize) Value {
164 | const raw = self.globals[idx];
165 | return switch (self.module.global[idx].type.content_type) {
166 | .I32 => Value{ .I32 = raw.I32 },
167 | .I64 => Value{ .I64 = raw.I64 },
168 | .F32 => Value{ .F32 = raw.F32 },
169 | .F64 => Value{ .F64 = raw.F64 },
170 | };
171 | }
172 |
173 | pub const Value = union(Module.Type.Value) {
174 | I32: i32,
175 | I64: i64,
176 | F32: f32,
177 | F64: f64,
178 | };
179 |
180 | pub const Export = union(enum) {
181 | Func: usize,
182 | Table: usize,
183 | Memory: usize,
184 | Global: usize,
185 | };
186 |
187 | pub fn ImportManager(comptime Imports: type) type {
188 | const V = struct {
189 | func: ImportFunc,
190 | frame_size: usize,
191 | param_types: []const Module.Type.Value,
192 | return_type: ?Module.Type.Value,
193 | };
194 | const KV = struct {
195 | @"0": []const u8,
196 | @"1": V,
197 | };
198 |
199 | const helpers = struct {
200 | fn Unwrapped(comptime T: type) type {
201 | return switch (@typeInfo(T)) {
202 | .ErrorUnion => |eu_info| Unwrapped(eu_info.payload),
203 | .Enum => |e_info| if (e_info.is_exhaustive) @compileError("Enum must be exhaustive") else e_info.tag_type,
204 | .Struct => {
205 | return std.meta.Int(.unsigned, @bitSizeOf(T));
206 | },
207 | else => T,
208 | };
209 | }
210 |
211 | fn shim(comptime func: anytype) ImportFunc {
212 | return struct {
213 | fn unwrap(raw: anytype) !Unwrapped(@TypeOf(raw)) {
214 | const T = @TypeOf(raw);
215 | return switch (@typeInfo(T)) {
216 | .ErrorUnion => unwrap(try raw),
217 | .Enum => @enumToInt(raw),
218 | .Struct => @bitCast(Unwrapped(T), raw),
219 | else => raw,
220 | };
221 | }
222 |
223 | pub fn shimmed(ctx: *Execution, params: []const Op.Fixval) Op.WasmTrap!?Op.Fixval {
224 | var args: std.meta.ArgsTuple(@TypeOf(func)) = undefined;
225 | args[0] = ctx.memory;
226 | inline for (std.meta.fields(@TypeOf(args))) |f, i| {
227 | if (i == 0) continue;
228 |
229 | const raw_value = switch (Unwrapped(f.field_type)) {
230 | i32 => params[i - 1].I32,
231 | i64 => params[i - 1].I64,
232 | u32 => params[i - 1].U32,
233 | u64 => params[i - 1].U64,
234 | f32 => params[i - 1].F32,
235 | f64 => params[i - 1].F64,
236 | else => @compileError("Signature not supported"),
237 | };
238 | args[i] = switch (@typeInfo(f.field_type)) {
239 | .Enum => @intToEnum(f.field_type, raw_value),
240 | .Struct => @bitCast(f.field_type, raw_value),
241 | else => raw_value,
242 | };
243 | }
244 |
245 | // TODO: move async call to where this is being invoked
246 | // const fixval_len = std.math.divCeil(comptime_int, @sizeOf(@Frame(func)), @sizeOf(Op.Fixval)) catch unreachable;
247 | // const frame_loc = ctx.pushOpaque(fixval_len) catch unreachable;
248 | // const frame = @ptrCast(*@Frame(func), frame_loc);
249 | // comptime const opts = std.builtin.CallOptions{ .modifier = .async_kw };
250 | // frame.* = @call(opts, func, args);
251 | // const result = nosuspend await frame;
252 |
253 | const result = try unwrap(@call(.{}, func, args));
254 | return switch (@TypeOf(result)) {
255 | void => null,
256 | i32 => Op.Fixval{ .I32 = result },
257 | i64 => Op.Fixval{ .I64 = result },
258 | u32 => Op.Fixval{ .U32 = result },
259 | u64 => Op.Fixval{ .U64 = result },
260 | f32 => Op.Fixval{ .F32 = result },
261 | f64 => Op.Fixval{ .F64 = result },
262 | else => @compileError("Signature not supported"),
263 | };
264 | }
265 | }.shimmed;
266 | }
267 |
268 | fn mapType(comptime T: type) ?Module.Type.Value {
269 | return switch (Unwrapped(T)) {
270 | void => null,
271 | i32, u32 => .I32,
272 | i64, u64 => .I64,
273 | f32 => .F32,
274 | f64 => .F64,
275 | else => @compileError("Type '" ++ @typeName(T) ++ "' not supported"),
276 | };
277 | }
278 | };
279 |
280 | const sep = "\x00\x00";
281 |
282 | var kvs: []const KV = &[0]KV{};
283 | inline for (std.meta.declarations(Imports)) |decl| {
284 | if (decl.is_pub) {
285 | inline for (std.meta.declarations(decl.data.Type)) |decl2| {
286 | if (decl2.is_pub) {
287 | const func = @field(decl.data.Type, decl2.name);
288 | const fn_info = @typeInfo(@TypeOf(func)).Fn;
289 | const shimmed = helpers.shim(func);
290 | kvs = kvs ++ [1]KV{.{
291 | .@"0" = decl.name ++ sep ++ decl2.name,
292 | .@"1" = .{
293 | .func = shimmed,
294 | .frame_size = @sizeOf(@Frame(shimmed)),
295 | .param_types = params: {
296 | var param_types: [fn_info.args.len - 1]Module.Type.Value = undefined;
297 | for (param_types) |*param, i| {
298 | param.* = helpers.mapType(fn_info.args[i + 1].arg_type.?).?;
299 | }
300 | break :params ¶m_types;
301 | },
302 | .return_type = helpers.mapType(fn_info.return_type.?),
303 | },
304 | }};
305 | }
306 | }
307 | }
308 | }
309 |
310 | const map = if (kvs.len > 0) std.ComptimeStringMap(V, kvs) else {};
311 |
312 | return struct {
313 | pub fn get(module: []const u8, field: []const u8) ?V {
314 | if (kvs.len == 0) return null;
315 |
316 | var buffer: [1 << 10]u8 = undefined;
317 | var fbs = std.io.fixedBufferStream(&buffer);
318 |
319 | fbs.writer().writeAll(module) catch return null;
320 | fbs.writer().writeAll(sep) catch return null;
321 | fbs.writer().writeAll(field) catch return null;
322 |
323 | return map.get(fbs.getWritten());
324 | }
325 | };
326 | }
327 |
328 | const ImportFunc = fn (ctx: *Execution, params: []const Op.Fixval) Op.WasmTrap!?Op.Fixval;
329 |
330 | pub const Func = struct {
331 | func_type: usize,
332 | params: []const Module.Type.Value,
333 | result: ?Module.Type.Value,
334 | locals: []const Module.Type.Value,
335 | kind: union(enum) {
336 | imported: struct {
337 | func: ImportFunc,
338 | frame_size: usize,
339 | },
340 | instrs: []const Module.Instr,
341 | },
342 | };
343 |
344 | test "" {
345 | _ = call;
346 | }
347 |
--------------------------------------------------------------------------------
/src/main.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | pub const Instance = @import("instance.zig");
4 | pub const Module = @import("module.zig");
5 | pub const Op = @import("op.zig");
6 | pub const Wat = @import("wat.zig");
7 | pub const Wasi = @import("wasi.zig");
8 |
9 | pub fn main() !u8 {
10 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
11 | defer _ = gpa.deinit();
12 | const allocator = &gpa.allocator;
13 |
14 | const args = try std.process.argsAlloc(allocator);
15 | defer std.process.argsFree(allocator, args);
16 |
17 | const file = try std.fs.cwd().openFile(args[1], .{});
18 | defer file.close();
19 |
20 | var wasi = Wasi{ .argv = args[1..] };
21 | const exit_code = @enumToInt(try wasi.run(&gpa.allocator, file.reader()));
22 | if (exit_code > 255) {
23 | std.debug.print("Exit code {} > 255\n", .{exit_code});
24 | return 255;
25 | } else {
26 | return @intCast(u8, exit_code);
27 | }
28 | }
29 |
30 | test "" {
31 | _ = Instance;
32 | _ = Module;
33 | _ = Op;
34 | _ = Wat;
35 | _ = Wasi;
36 |
37 | _ = main;
38 | _ = @import("func/basic.zig");
39 | _ = @import("func/imports.zig");
40 | _ = @import("func/logic.zig");
41 | _ = @import("func/global.zig");
42 | }
43 |
--------------------------------------------------------------------------------
/src/module.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Instance = @import("instance.zig");
3 | const Op = @import("op.zig");
4 | pub const PostProcess = @import("module/post_process.zig");
5 |
6 | const log = std.log.scoped(.wazm);
7 | const magic_number = std.mem.readIntLittle(u32, "\x00asm");
8 |
9 | const Module = @This();
10 |
11 | arena: std.heap.ArenaAllocator,
12 |
13 | /// Code=0
14 | custom: []const struct {
15 | name: []const u8,
16 | payload: []const u8,
17 | } = &.{},
18 |
19 | /// Code=1
20 | @"type": []const struct {
21 | form: Type.Form, // TODO: why is this called form?
22 | param_types: []const Type.Value,
23 | return_type: ?Type.Value,
24 | } = &.{},
25 |
26 | /// Code=2
27 | import: []const struct {
28 | module: []const u8,
29 | field: []const u8,
30 | kind: union(ExternalKind) {
31 | Function: Index.FuncType,
32 | // TODO: add these types
33 | Table: void,
34 | Memory: void,
35 | Global: void,
36 | },
37 | } = &.{},
38 |
39 | /// Code=3
40 | function: []const struct {
41 | type_idx: Index.FuncType,
42 | } = &.{},
43 |
44 | /// Code=4
45 | table: []const struct {
46 | element_type: Type.Elem,
47 | limits: ResizableLimits,
48 | } = &.{},
49 |
50 | /// Code=5
51 | memory: []const struct {
52 | limits: ResizableLimits,
53 | } = &.{},
54 |
55 | /// Code=6
56 | global: []const struct {
57 | @"type": struct {
58 | content_type: Type.Value,
59 | mutability: bool,
60 | },
61 | init: InitExpr,
62 | } = &.{},
63 |
64 | /// Code=7
65 | @"export": []const struct {
66 | field: []const u8,
67 | kind: ExternalKind,
68 | index: u32,
69 | } = &.{},
70 |
71 | /// Code=8
72 | start: ?struct {
73 | index: Index.Function,
74 | } = null,
75 |
76 | /// Code=9
77 | element: []const struct {
78 | index: Index.Table,
79 | offset: InitExpr,
80 | elems: []const Index.Function,
81 | } = &.{},
82 |
83 | /// Code=10
84 | code: []const struct {
85 | locals: []const Type.Value,
86 | body: []const Module.Instr,
87 | } = &.{},
88 |
89 | /// Code=11
90 | data: []const struct {
91 | index: Index.Memory,
92 | offset: InitExpr,
93 | data: []const u8,
94 | } = &.{},
95 |
96 | post_process: ?PostProcess = null,
97 |
98 | pub fn init(arena: std.heap.ArenaAllocator) Module {
99 | return Module{ .arena = arena };
100 | }
101 |
102 | pub fn deinit(self: *Module) void {
103 | self.arena.deinit();
104 | self.* = undefined;
105 | }
106 |
107 | pub fn Section(comptime section: std.wasm.Section) type {
108 | const fields = std.meta.fields(Module);
109 | const num = @enumToInt(section) + 1; // 0 == allocator, 1 == custom, etc.
110 | return std.meta.Child(fields[num].field_type);
111 | }
112 |
113 | test "Section" {
114 | const module: Module = undefined;
115 | try std.testing.expectEqual(std.meta.Child(@TypeOf(module.custom)), Section(.custom));
116 | try std.testing.expectEqual(std.meta.Child(@TypeOf(module.memory)), Section(.memory));
117 | try std.testing.expectEqual(std.meta.Child(@TypeOf(module.data)), Section(.data));
118 | }
119 |
120 | pub const Index = struct {
121 | pub const FuncType = enum(u32) { _ };
122 | pub const Table = enum(u32) { _ };
123 | pub const Function = enum(u32) { _ };
124 | pub const Memory = enum(u32) { _ };
125 | pub const Global = enum(u32) { _ };
126 | };
127 |
128 | pub const Type = struct {
129 | pub const Value = enum(i7) {
130 | I32 = -0x01,
131 | I64 = -0x02,
132 | F32 = -0x03,
133 | F64 = -0x04,
134 | };
135 |
136 | pub const Block = enum(i7) {
137 | I32 = -0x01,
138 | I64 = -0x02,
139 | F32 = -0x03,
140 | F64 = -0x04,
141 | Empty = -0x40,
142 | };
143 |
144 | pub const Elem = enum(i7) {
145 | Anyfunc = -0x10,
146 | };
147 |
148 | pub const Form = enum(i7) {
149 | Func = -0x20,
150 | };
151 | };
152 |
153 | pub const ExternalKind = enum(u7) {
154 | Function = 0,
155 | Table = 1,
156 | Memory = 2,
157 | Global = 3,
158 | };
159 |
160 | pub const ResizableLimits = struct {
161 | initial: u32,
162 | maximum: ?u32,
163 | };
164 |
165 | pub const Instr = struct {
166 | op: std.wasm.Opcode,
167 | pop_len: u8,
168 | arg: Op.Arg,
169 | };
170 |
171 | pub const InitExpr = union(enum) {
172 | i32_const: i32,
173 | i64_const: i64,
174 | f32_const: f32,
175 | f64_const: f64,
176 | global_get: u32,
177 |
178 | fn parse(reader: anytype) !InitExpr {
179 | const opcode = @intToEnum(std.wasm.Opcode, try reader.readByte());
180 | const result: InitExpr = switch (opcode) {
181 | .i32_const => .{ .i32_const = try readVarint(i32, reader) },
182 | .i64_const => .{ .i64_const = try readVarint(i64, reader) },
183 | .f32_const => .{ .f32_const = @bitCast(f32, try reader.readIntLittle(i32)) },
184 | .f64_const => .{ .f64_const = @bitCast(f64, try reader.readIntLittle(i64)) },
185 | .global_get => .{ .global_get = try readVarint(u32, reader) },
186 | else => return error.UnsupportedInitExpr,
187 | };
188 | if (std.wasm.opcode(.end) != try reader.readByte()) {
189 | return error.InitExprTerminationError;
190 | }
191 | return result;
192 | }
193 | };
194 |
195 | pub fn postProcess(self: *Module) !void {
196 | if (self.post_process == null) {
197 | self.post_process = try PostProcess.init(self);
198 | }
199 | }
200 |
201 | fn readVarint(comptime T: type, reader: anytype) !T {
202 | const readFn = switch (@typeInfo(T).Int.signedness) {
203 | .signed => std.leb.readILEB128,
204 | .unsigned => std.leb.readULEB128,
205 | };
206 | return try readFn(T, reader);
207 | }
208 |
209 | test "readVarint" {
210 | {
211 | var ios = std.io.fixedBufferStream("\xE5\x8E\x26");
212 | try std.testing.expectEqual(@as(u32, 624485), try readVarint(u32, ios.reader()));
213 | ios.pos = 0;
214 | try std.testing.expectEqual(@as(u21, 624485), try readVarint(u21, ios.reader()));
215 | }
216 | {
217 | var ios = std.io.fixedBufferStream("\xC0\xBB\x78");
218 | try std.testing.expectEqual(@as(i32, -123456), try readVarint(i32, ios.reader()));
219 | ios.pos = 0;
220 | try std.testing.expectEqual(@as(i21, -123456), try readVarint(i21, ios.reader()));
221 | }
222 | {
223 | var ios = std.io.fixedBufferStream("\x7F");
224 | try std.testing.expectEqual(@as(i7, -1), try readVarint(i7, ios.reader()));
225 | ios.pos = 0;
226 | try std.testing.expectEqual(@as(i21, -1), try readVarint(i21, ios.reader()));
227 | ios.pos = 0;
228 | try std.testing.expectEqual(@as(i32, -1), try readVarint(i32, ios.reader()));
229 | }
230 | {
231 | var ios = std.io.fixedBufferStream("\xa4\x03");
232 | try std.testing.expectEqual(@as(i21, 420), try readVarint(i21, ios.reader()));
233 | ios.pos = 0;
234 | try std.testing.expectEqual(@as(i32, 420), try readVarint(i32, ios.reader()));
235 | }
236 | }
237 |
238 | fn readVarintEnum(comptime E: type, reader: anytype) !E {
239 | const raw = try readVarint(std.meta.TagType(E), reader);
240 | if (@typeInfo(E).Enum.is_exhaustive) {
241 | return try std.meta.intToEnum(E, raw);
242 | } else {
243 | return @intToEnum(E, raw);
244 | }
245 | }
246 |
247 | fn expectEos(reader: anytype) !void {
248 | var tmp: [1]u8 = undefined;
249 | const len = try reader.read(&tmp);
250 | if (len != 0) {
251 | return error.NotEndOfStream;
252 | }
253 | }
254 |
255 | fn Mut(comptime T: type) type {
256 | var ptr_info = @typeInfo(T).Pointer;
257 | ptr_info.is_const = false;
258 | return @Type(.{ .Pointer = ptr_info });
259 | }
260 |
261 | // --- Before ---
262 | // const count = try readVarint(u32, section.reader());
263 | // result.field = arena.allocator.alloc(@TypeOf(result.field), count);
264 | // for (result.field) |*item| {
265 | //
266 | // --- After ---
267 | // const count = try readVarint(u32, section.reader());
268 | // for (self.allocInto(&result.field, count)) |*item| {
269 | fn allocInto(self: *Module, ptr_to_slice: anytype, count: usize) !Mut(std.meta.Child(@TypeOf(ptr_to_slice))) {
270 | const Slice = Mut(std.meta.Child(@TypeOf(ptr_to_slice)));
271 | std.debug.assert(@typeInfo(Slice).Pointer.size == .Slice);
272 |
273 | var result = try self.arena.allocator.alloc(std.meta.Child(Slice), count);
274 | ptr_to_slice.* = result;
275 | return result;
276 | }
277 |
278 | pub fn parse(allocator: *std.mem.Allocator, reader: anytype) !Module {
279 | const signature = try reader.readIntLittle(u32);
280 | if (signature != magic_number) {
281 | return error.InvalidFormat;
282 | }
283 |
284 | const version = try reader.readIntLittle(u32);
285 | if (version != 1) {
286 | return error.InvalidFormat;
287 | }
288 |
289 | var result = Module.init(std.heap.ArenaAllocator.init(allocator));
290 | errdefer result.arena.deinit();
291 |
292 | var customs = std.ArrayList(Module.Section(.custom)).init(&result.arena.allocator);
293 | errdefer customs.deinit();
294 |
295 | while (true) {
296 | const id = reader.readByte() catch |err| switch (err) {
297 | error.EndOfStream => break,
298 | else => return err,
299 | };
300 | const section_len = try readVarint(u32, reader);
301 | var section = std.io.limitedReader(reader, section_len);
302 |
303 | switch (try std.meta.intToEnum(std.wasm.Section, id)) {
304 | .type => {
305 | const count = try readVarint(u32, section.reader());
306 | for (try result.allocInto(&result.@"type", count)) |*t| {
307 | t.form = try readVarintEnum(Type.Form, section.reader());
308 |
309 | const param_count = try readVarint(u32, section.reader());
310 | for (try result.allocInto(&t.param_types, param_count)) |*param_type| {
311 | param_type.* = try readVarintEnum(Type.Value, section.reader());
312 | }
313 |
314 | const return_count = try readVarint(u1, section.reader());
315 | t.return_type = if (return_count == 0) null else try readVarintEnum(Type.Value, section.reader());
316 | }
317 | try expectEos(section.reader());
318 | },
319 | .import => {
320 | const count = try readVarint(u32, section.reader());
321 | for (try result.allocInto(&result.import, count)) |*i| {
322 | const module_len = try readVarint(u32, section.reader());
323 | const module_data = try result.arena.allocator.alloc(u8, module_len);
324 | try section.reader().readNoEof(module_data);
325 | i.module = module_data;
326 |
327 | const field_len = try readVarint(u32, section.reader());
328 | const field_data = try result.arena.allocator.alloc(u8, field_len);
329 | try section.reader().readNoEof(field_data);
330 | i.field = field_data;
331 |
332 | // TODO: actually test import parsing
333 | const kind = try readVarintEnum(ExternalKind, section.reader());
334 | i.kind = switch (kind) {
335 | .Function => .{ .Function = try readVarintEnum(Index.FuncType, section.reader()) },
336 | .Table => @panic("TODO"),
337 | .Memory => @panic("TODO"),
338 | .Global => @panic("TODO"),
339 | };
340 | }
341 | try expectEos(section.reader());
342 | },
343 | .function => {
344 | const count = try readVarint(u32, section.reader());
345 | for (try result.allocInto(&result.function, count)) |*f| {
346 | f.type_idx = try readVarintEnum(Index.FuncType, section.reader());
347 | }
348 | try expectEos(section.reader());
349 | },
350 | .table => {
351 | const count = try readVarint(u32, section.reader());
352 | for (try result.allocInto(&result.table, count)) |*t| {
353 | t.element_type = try readVarintEnum(Type.Elem, section.reader());
354 |
355 | const flags = try readVarint(u1, section.reader());
356 | t.limits.initial = try readVarint(u32, section.reader());
357 | t.limits.maximum = if (flags == 0) null else try readVarint(u32, section.reader());
358 | }
359 | try expectEos(section.reader());
360 | },
361 | .memory => {
362 | const count = try readVarint(u32, section.reader());
363 | for (try result.allocInto(&result.memory, count)) |*m| {
364 | const flags = try readVarint(u1, section.reader());
365 | m.limits.initial = try readVarint(u32, section.reader());
366 | m.limits.maximum = if (flags == 0) null else try readVarint(u32, section.reader());
367 | }
368 | try expectEos(section.reader());
369 | },
370 | .global => {
371 | const count = try readVarint(u32, section.reader());
372 | for (try result.allocInto(&result.global, count)) |*g| {
373 | g.@"type".content_type = try readVarintEnum(Type.Value, section.reader());
374 | g.@"type".mutability = (try readVarint(u1, section.reader())) == 1;
375 | g.init = try InitExpr.parse(section.reader());
376 | }
377 | try expectEos(section.reader());
378 | },
379 | .@"export" => {
380 | const count = try readVarint(u32, section.reader());
381 | for (try result.allocInto(&result.@"export", count)) |*e| {
382 | const field_len = try readVarint(u32, section.reader());
383 | const field_data = try result.arena.allocator.alloc(u8, field_len);
384 | try section.reader().readNoEof(field_data);
385 | e.field = field_data;
386 | e.kind = try readVarintEnum(ExternalKind, section.reader());
387 | e.index = try readVarint(u32, section.reader());
388 | }
389 | try expectEos(section.reader());
390 | },
391 | .start => {
392 | const index = try readVarint(u32, section.reader());
393 | result.start = .{
394 | .index = try readVarintEnum(Index.Function, section.reader()),
395 | };
396 | try expectEos(section.reader());
397 | },
398 | .element => {
399 | const count = try readVarint(u32, section.reader());
400 | for (try result.allocInto(&result.element, count)) |*e| {
401 | e.index = try readVarintEnum(Index.Table, section.reader());
402 | e.offset = try InitExpr.parse(section.reader());
403 |
404 | const num_elem = try readVarint(u32, section.reader());
405 | for (try result.allocInto(&e.elems, count)) |*func| {
406 | func.* = try readVarintEnum(Index.Function, section.reader());
407 | }
408 | }
409 | try expectEos(section.reader());
410 | },
411 | .code => {
412 | const count = try readVarint(u32, section.reader());
413 | for (try result.allocInto(&result.code, count)) |*c| {
414 | const body_size = try readVarint(u32, section.reader());
415 | var body = std.io.limitedReader(section.reader(), body_size);
416 |
417 | c.locals = blk: {
418 | // TODO: double pass here to preallocate the exact array size
419 | var list = std.ArrayList(Type.Value).init(&result.arena.allocator);
420 | var local_count = try readVarint(u32, body.reader());
421 | while (local_count > 0) : (local_count -= 1) {
422 | var current_count = try readVarint(u32, body.reader());
423 | const typ = try readVarintEnum(Type.Value, body.reader());
424 | while (current_count > 0) : (current_count -= 1) {
425 | try list.append(typ);
426 | }
427 | }
428 | break :blk list.items;
429 | };
430 |
431 | c.body = body: {
432 | var list = std.ArrayList(Module.Instr).init(&result.arena.allocator);
433 | while (true) {
434 | const opcode = body.reader().readByte() catch |err| switch (err) {
435 | error.EndOfStream => {
436 | const last = list.popOrNull() orelse return error.MissingFunctionEnd;
437 | if (last.op != .end) {
438 | return error.MissingFunctionEnd;
439 | }
440 | break :body list.items;
441 | },
442 | else => return err,
443 | };
444 |
445 | const op_meta = Op.Meta.all[opcode] orelse return error.InvalidOpCode;
446 |
447 | try list.append(.{
448 | .op = @intToEnum(std.wasm.Opcode, opcode),
449 | .pop_len = @intCast(u8, op_meta.pop.len),
450 | .arg = switch (op_meta.arg_kind) {
451 | .Void => undefined,
452 | .I32 => .{ .I32 = try readVarint(i32, body.reader()) },
453 | .U32 => .{ .U32 = try readVarint(u32, body.reader()) },
454 | .I64 => .{ .I64 = try readVarint(i64, body.reader()) },
455 | .U64 => .{ .U64 = try readVarint(u64, body.reader()) },
456 | .F32 => .{ .F64 = @bitCast(f32, try body.reader().readIntLittle(i32)) },
457 | .F64 => .{ .F64 = @bitCast(f64, try body.reader().readIntLittle(i64)) },
458 | .Type => .{ .Type = @intToEnum(Op.Arg.Type, try readVarint(u7, body.reader())) },
459 | .U32z => .{
460 | .U32z = .{
461 | .data = try readVarint(u32, body.reader()),
462 | .reserved = try body.reader().readByte(),
463 | },
464 | },
465 | .Mem => .{
466 | .Mem = .{
467 | .align_ = try readVarint(u32, body.reader()),
468 | .offset = try readVarint(u32, body.reader()),
469 | },
470 | },
471 | .Array => blk: {
472 | const target_count = try readVarint(u32, body.reader());
473 | const size = target_count + 1; // Implementation detail: we shove the default into the last element of the array
474 |
475 | const data = try result.arena.allocator.alloc(u32, size);
476 | for (data) |*item| {
477 | item.* = try readVarint(u32, body.reader());
478 | }
479 | break :blk .{
480 | .Array = .{
481 | .ptr = data.ptr,
482 | .len = data.len,
483 | },
484 | };
485 | },
486 | },
487 | });
488 | }
489 | };
490 |
491 | try expectEos(body.reader());
492 | }
493 | try expectEos(section.reader());
494 | },
495 | .data => {
496 | const count = try readVarint(u32, section.reader());
497 | for (try result.allocInto(&result.data, count)) |*d| {
498 | d.index = try readVarintEnum(Index.Memory, section.reader());
499 | d.offset = try InitExpr.parse(section.reader());
500 |
501 | const size = try readVarint(u32, section.reader());
502 | const data = try result.arena.allocator.alloc(u8, size);
503 | try section.reader().readNoEof(data);
504 | d.data = data;
505 | }
506 | try expectEos(section.reader());
507 | },
508 | .custom => {
509 | const custom_section = try customs.addOne();
510 |
511 | const name_len = try readVarint(u32, section.reader());
512 | const name = try result.arena.allocator.alloc(u8, name_len);
513 | try section.reader().readNoEof(name);
514 | custom_section.name = name;
515 |
516 | const payload = try result.arena.allocator.alloc(u8, section.bytes_left);
517 | try section.reader().readNoEof(payload);
518 | custom_section.payload = payload;
519 |
520 | try expectEos(section.reader());
521 | },
522 | }
523 |
524 | // Putting this in all the switch paths makes debugging much easier
525 | // Leaving an extra one here in case one of the paths is missing
526 | if (std.builtin.mode == .Debug) {
527 | try expectEos(section.reader());
528 | }
529 | }
530 |
531 | result.custom = customs.toOwnedSlice();
532 |
533 | result.post_process = try PostProcess.init(&result);
534 |
535 | return result;
536 | }
537 |
538 | pub const instantiate = Instance.init;
539 |
540 | const empty_raw_bytes = &[_]u8{ 0, 'a', 's', 'm', 1, 0, 0, 0 };
541 |
542 | test "empty module" {
543 | var ios = std.io.fixedBufferStream(empty_raw_bytes);
544 | var module = try Module.parse(std.testing.allocator, ios.reader());
545 | defer module.deinit();
546 |
547 | try std.testing.expectEqual(@as(usize, 0), module.memory.len);
548 | try std.testing.expectEqual(@as(usize, 0), module.@"type".len);
549 | try std.testing.expectEqual(@as(usize, 0), module.function.len);
550 | try std.testing.expectEqual(@as(usize, 0), module.@"export".len);
551 | }
552 |
553 | test "module with only type" {
554 | const raw_bytes = empty_raw_bytes ++ // (module
555 | "\x01\x04\x01\x60\x00\x00" ++ // (type (func)))
556 | "";
557 | var ios = std.io.fixedBufferStream(raw_bytes);
558 | var module = try Module.parse(std.testing.allocator, ios.reader());
559 | defer module.deinit();
560 |
561 | try std.testing.expectEqual(@as(usize, 1), module.@"type".len);
562 | try std.testing.expectEqual(@as(usize, 0), module.@"type"[0].param_types.len);
563 | try std.testing.expectEqual(@as(?Type.Value, null), module.@"type"[0].return_type);
564 | }
565 |
566 | test "module with function body" {
567 | const raw_bytes = empty_raw_bytes ++ // (module
568 | "\x01\x05\x01\x60\x00\x01\x7f" ++ // (type (;0;) (func (result i32)))
569 | "\x03\x02\x01\x00" ++ // (func (;0;) (type 0)
570 | "\x07\x05\x01\x01\x61\x00\x00" ++ // (export "a" (func 0))
571 | "\x0a\x07\x01\x05\x00\x41\xa4\x03\x0b" ++ // i32.const 420
572 | "";
573 |
574 | var ios = std.io.fixedBufferStream(raw_bytes);
575 | var module = try Module.parse(std.testing.allocator, ios.reader());
576 | defer module.deinit();
577 |
578 | try std.testing.expectEqual(@as(usize, 1), module.@"type".len);
579 | try std.testing.expectEqual(@as(usize, 0), module.@"type"[0].param_types.len);
580 | try std.testing.expectEqual(Type.Value.I32, module.@"type"[0].return_type.?);
581 |
582 | try std.testing.expectEqual(@as(usize, 1), module.@"export".len);
583 |
584 | try std.testing.expectEqual(@as(usize, 1), module.function.len);
585 | try std.testing.expectEqual(@as(usize, 1), module.code.len);
586 | try std.testing.expectEqual(@as(usize, 1), module.code[0].body.len);
587 | try std.testing.expectEqual(std.wasm.Opcode.i32_const, module.code[0].body[0].op);
588 | }
589 |
590 | test "global definitions" {
591 | const raw_bytes = empty_raw_bytes ++ // (module
592 | "\x06\x09\x01\x7f\x01\x41\x80\x80\xc0\x00\x0b" ++ // (global (mut i32) (i32.const 1048576)))
593 | "";
594 | var ios = std.io.fixedBufferStream(raw_bytes);
595 | var module = try Module.parse(std.testing.allocator, ios.reader());
596 | defer module.deinit();
597 |
598 | try std.testing.expectEqual(@as(usize, 1), module.global.len);
599 | try std.testing.expectEqual(Type.Value.I32, module.global[0].type.content_type);
600 | try std.testing.expectEqual(true, module.global[0].type.mutability);
601 | }
602 |
--------------------------------------------------------------------------------
/src/module/post_process.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Module = @import("../module.zig");
4 | const Op = @import("../op.zig");
5 | const Wat = @import("../wat.zig");
6 |
7 | const PostProcess = @This();
8 |
9 | import_funcs: []const ImportFunc,
10 | jumps: InstrJumps,
11 |
12 | pub const ImportFunc = struct {
13 | module: []const u8,
14 | field: []const u8,
15 | type_idx: Module.Index.FuncType,
16 | };
17 |
18 | pub const InstrJumps = std.AutoHashMap(struct { func: u32, instr: u32 }, union {
19 | one: JumpTarget,
20 | many: [*]const JumpTarget, // len = args.len
21 | });
22 |
23 | pub const JumpTarget = struct {
24 | has_value: bool,
25 | addr: u32,
26 | stack_unroll: u32,
27 | };
28 |
29 | pub fn init(module: *Module) !PostProcess {
30 | var temp_arena = std.heap.ArenaAllocator.init(module.arena.child_allocator);
31 | defer temp_arena.deinit();
32 |
33 | var import_funcs = std.ArrayList(ImportFunc).init(&module.arena.allocator);
34 | for (module.import) |import| {
35 | switch (import.kind) {
36 | .Function => |type_idx| {
37 | try import_funcs.append(.{
38 | .module = import.module,
39 | .field = import.field,
40 | .type_idx = type_idx,
41 | });
42 | },
43 | else => @panic("TODO"),
44 | }
45 | }
46 |
47 | var stack_validator = StackValidator.init(&temp_arena.allocator);
48 | var jumps = InstrJumps.init(&module.arena.allocator);
49 |
50 | for (module.code) |code, f| {
51 | try stack_validator.process(import_funcs.items, module, f);
52 |
53 | // Fill in jump targets
54 | const jump_targeter = JumpTargeter{ .jumps = &jumps, .func_idx = f + import_funcs.items.len, .types = stack_validator.types };
55 |
56 | for (code.body) |instr, instr_idx| {
57 | switch (instr.op) {
58 | .br, .br_if => {
59 | const block = stack_validator.blocks.upFrom(instr_idx, instr.arg.U32) orelse return error.JumpExceedsBlock;
60 | const block_instr = code.body[block.start_idx];
61 | try jump_targeter.add(block.data, .{
62 | .from = instr_idx,
63 | .target = if (block_instr.op == .loop) block.start_idx else block.end_idx,
64 | });
65 | },
66 | .br_table => {
67 | const targets = try module.arena.allocator.alloc(JumpTarget, instr.arg.Array.len);
68 | for (targets) |*target, t| {
69 | const block_level = instr.arg.Array.ptr[t];
70 | const block = stack_validator.blocks.upFrom(instr_idx, block_level) orelse return error.JumpExceedsBlock;
71 | const block_instr = code.body[block.start_idx];
72 | const target_idx = if (block_instr.op == .loop) block.start_idx else block.end_idx;
73 | target.addr = @intCast(u32, if (block_instr.op == .loop) block.start_idx else block.end_idx);
74 | target.has_value = block.data != .Empty;
75 | }
76 | try jump_targeter.addMany(instr_idx, targets);
77 | },
78 | .@"else" => {
79 | const block = stack_validator.blocks.list.items[instr_idx].?;
80 | try jump_targeter.add(block.data, .{
81 | .from = instr_idx,
82 | .target = block.end_idx,
83 | // When the "if" block has a value, it is left on the stack at
84 | // the "else", which needs to carry it forward
85 | // This is either off-by-one during stack analysis or jumping... :(
86 | .stack_adjust = @boolToInt(block.data != .Empty),
87 | });
88 | },
89 | .@"if" => {
90 | const block = stack_validator.blocks.list.items[instr_idx].?;
91 | try jump_targeter.add(block.data, .{
92 | .from = instr_idx,
93 | .target = block.end_idx,
94 | });
95 | },
96 | else => {},
97 | }
98 | }
99 | }
100 |
101 | return PostProcess{
102 | .jumps = jumps,
103 | .import_funcs = import_funcs.toOwnedSlice(),
104 | };
105 | }
106 |
107 | const JumpTargeter = struct {
108 | jumps: *InstrJumps,
109 | func_idx: usize,
110 | types: StackLedger(Module.Type.Value),
111 |
112 | fn add(self: JumpTargeter, block_type: Module.Type.Block, args: struct {
113 | from: usize,
114 | target: usize,
115 | stack_adjust: u32 = 0,
116 | }) !void {
117 | const target_depth = self.types.depthOf(args.target);
118 | try self.jumps.putNoClobber(
119 | .{ .func = @intCast(u32, self.func_idx), .instr = @intCast(u32, args.from) },
120 | .{
121 | .one = .{
122 | .has_value = block_type != .Empty,
123 | .addr = @intCast(u32, args.target),
124 | .stack_unroll = self.types.depthOf(args.from) + args.stack_adjust - target_depth,
125 | },
126 | },
127 | );
128 | }
129 |
130 | fn addMany(self: JumpTargeter, from_idx: usize, targets: []JumpTarget) !void {
131 | for (targets) |*target| {
132 | target.stack_unroll = self.types.depthOf(from_idx) - self.types.depthOf(target.addr);
133 | }
134 | try self.jumps.putNoClobber(
135 | .{ .func = @intCast(u32, self.func_idx), .instr = @intCast(u32, from_idx) },
136 | .{ .many = targets.ptr },
137 | );
138 | }
139 | };
140 |
141 | pub fn StackLedger(comptime T: type) type {
142 | return struct {
143 | const Self = @This();
144 | const Node = struct {
145 | data: T,
146 | start_idx: usize,
147 | end_idx: usize,
148 | prev: ?*Node,
149 | };
150 |
151 | top: ?*Node,
152 | list: std.ArrayList(?Node),
153 |
154 | pub fn init(allocator: *std.mem.Allocator) Self {
155 | return .{
156 | .top = null,
157 | .list = std.ArrayList(?Node).init(allocator),
158 | };
159 | }
160 |
161 | pub fn depthOf(self: Self, idx: usize) u32 {
162 | var iter = &(self.list.items[idx] orelse return 0);
163 | var result: u32 = 1;
164 | while (iter.prev) |prev| {
165 | result += 1;
166 | iter = prev;
167 | }
168 | return result;
169 | }
170 |
171 | pub fn format(self: Self, comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
172 | try writer.writeAll("StackLedger(");
173 | var iter = self.top;
174 | while (iter) |node| {
175 | try writer.print(", {}", .{node.data});
176 | iter = node.prev;
177 | }
178 | try writer.writeAll(")");
179 | }
180 |
181 | pub fn reset(self: *Self, size: usize) !void {
182 | self.top = null;
183 | self.list.shrinkRetainingCapacity(0);
184 | try self.list.ensureCapacity(size);
185 | }
186 |
187 | pub fn upFrom(self: Self, start_idx: usize, levels: usize) ?*const Node {
188 | var node = &(self.list.items[start_idx] orelse return null);
189 | if (levels == 0) {
190 | return &(self.list.items[node.start_idx].?);
191 | }
192 |
193 | var l = levels;
194 | while (l > 0) {
195 | l -= 1;
196 | node = node.prev orelse return null;
197 | }
198 | return node;
199 | }
200 |
201 | pub fn pushAt(self: *Self, idx: usize, data: T) void {
202 | std.debug.assert(idx == self.list.items.len);
203 | self.list.appendAssumeCapacity(Node{ .data = data, .start_idx = idx, .end_idx = undefined, .prev = self.top });
204 | self.top = &self.list.items[idx].?;
205 | }
206 |
207 | pub fn seal(self: *Self, idx: usize) void {
208 | if (self.list.items.len == idx) {
209 | self.list.appendAssumeCapacity(if (self.top) |top| top.* else null);
210 | }
211 | }
212 |
213 | pub fn pop(self: *Self, idx: usize) !T {
214 | const top = self.top orelse return error.StackMismatch;
215 | self.top = top.prev;
216 | top.end_idx = idx;
217 | return top.data;
218 | }
219 |
220 | pub fn checkPops(self: *Self, idx: usize, datas: []const T) !void {
221 | var i: usize = datas.len;
222 | while (i > 0) {
223 | i -= 1;
224 | if (datas[i] != try self.pop(idx)) {
225 | return error.StackMismatch;
226 | }
227 | }
228 | }
229 | };
230 | }
231 |
232 | const StackValidator = struct {
233 | types: StackLedger(Module.Type.Value),
234 | blocks: StackLedger(Module.Type.Block),
235 |
236 | pub fn init(allocator: *std.mem.Allocator) StackValidator {
237 | return .{
238 | .types = StackLedger(Module.Type.Value).init(allocator),
239 | .blocks = StackLedger(Module.Type.Block).init(allocator),
240 | };
241 | }
242 |
243 | pub fn process(self: *StackValidator, import_funcs: []const ImportFunc, module: *const Module, code_idx: usize) !void {
244 | const func = module.function[code_idx];
245 | const func_type = module.@"type"[@enumToInt(func.type_idx)];
246 | const code = module.code[code_idx];
247 |
248 | try self.blocks.reset(code.body.len);
249 | for (code.body) |instr, instr_idx| {
250 | defer self.blocks.seal(instr_idx);
251 |
252 | switch (instr.op) {
253 | // Block operations
254 | .block, .loop, .@"if" => {
255 | const result_type = instr.arg.Type;
256 | self.blocks.pushAt(instr_idx, switch (result_type) {
257 | .Void => .Empty,
258 | .I32 => .I32,
259 | .I64 => .I64,
260 | .F32 => .F32,
261 | .F64 => .F64,
262 | });
263 | },
264 | .@"else" => {
265 | const block_idx = (self.blocks.top orelse return error.StackMismatch).start_idx;
266 | const top = try self.blocks.pop(instr_idx);
267 | if (code.body[block_idx].op != .@"if") {
268 | return error.MismatchElseWithoutIf;
269 | }
270 | self.blocks.pushAt(instr_idx, top);
271 | },
272 | .end => _ = try self.blocks.pop(instr_idx),
273 | // TODO: catch invalid br/br_table/br_if
274 | else => {},
275 | }
276 | }
277 | if (self.blocks.top != null) {
278 | return error.BlockMismatch;
279 | }
280 |
281 | try self.types.reset(code.body.len);
282 | var terminating_block_idx: ?usize = null;
283 | for (code.body) |instr, instr_idx| {
284 | defer self.types.seal(instr_idx);
285 |
286 | if (terminating_block_idx) |block_idx| {
287 | if (instr.op == .end) {
288 | terminating_block_idx = null;
289 | const unroll_amount = self.types.depthOf(instr_idx - 1) - self.types.depthOf(block_idx);
290 |
291 | var i: usize = 0;
292 | while (i < unroll_amount) : (i += 1) {
293 | _ = self.types.pop(instr_idx) catch unreachable;
294 | }
295 | }
296 | // TODO: do I need to detect valid instruction
297 | continue;
298 | }
299 |
300 | switch (instr.op) {
301 | .@"return", .br, .br_table, .@"unreachable" => {
302 | if (instr.op == .br_table) {
303 | try self.types.checkPops(instr_idx, &.{Module.Type.Value.I32});
304 | }
305 | terminating_block_idx = if (self.blocks.list.items[instr_idx]) |block| block.start_idx else std.math.maxInt(usize);
306 | },
307 |
308 | .@"else" => {
309 | const if_block = self.blocks.list.items[instr_idx - 1].?;
310 | if (if_block.data != .Empty) {
311 | _ = try self.types.pop(instr_idx);
312 | }
313 | },
314 |
315 | .end => {
316 | const block = self.blocks.list.items[instr_idx - 1].?;
317 | const extra = @boolToInt(block.data != .Empty);
318 | if (self.types.depthOf(block.start_idx) != self.types.depthOf(instr_idx - 1) - extra) {
319 | return error.StackMismatch;
320 | }
321 | },
322 |
323 | .call => {
324 | // TODO: validate these indexes
325 | const func_idx = instr.arg.U32;
326 | if (func_idx < import_funcs.len) {
327 | // import
328 | const call_func = import_funcs[func_idx];
329 | const call_type = module.@"type"[@enumToInt(call_func.type_idx)];
330 | try self.types.checkPops(instr_idx, call_type.param_types);
331 | if (call_type.return_type) |typ| {
332 | self.types.pushAt(instr_idx, typ);
333 | }
334 | } else {
335 | const call_func = module.function[func_idx - import_funcs.len];
336 | const call_type = module.@"type"[@enumToInt(call_func.type_idx)];
337 | try self.types.checkPops(instr_idx, call_type.param_types);
338 | if (call_type.return_type) |typ| {
339 | self.types.pushAt(instr_idx, typ);
340 | }
341 | }
342 | },
343 | .call_indirect => {
344 | const call_type = module.@"type"[instr.arg.U32];
345 | try self.types.checkPops(instr_idx, call_type.param_types);
346 | if (call_type.return_type) |typ| {
347 | self.types.pushAt(instr_idx, typ);
348 | }
349 | },
350 |
351 | .local_set => try self.types.checkPops(instr_idx, &.{try localType(instr.arg.U32, func_type.param_types, code.locals)}),
352 | .local_get => self.types.pushAt(instr_idx, try localType(instr.arg.U32, func_type.param_types, code.locals)),
353 | .local_tee => {
354 | const typ = try localType(instr.arg.U32, func_type.param_types, code.locals);
355 | try self.types.checkPops(instr_idx, &.{typ});
356 | self.types.pushAt(instr_idx, typ);
357 | },
358 | .global_set => {
359 | const idx = instr.arg.U32;
360 | if (idx >= module.global.len) return error.GlobalIndexOutOfBounds;
361 | try self.types.checkPops(instr_idx, &.{module.global[idx].@"type".content_type});
362 | },
363 | .global_get => {
364 | const idx = instr.arg.U32;
365 | if (idx >= module.global.len) return error.GlobalIndexOutOfBounds;
366 | self.types.pushAt(instr_idx, module.global[idx].@"type".content_type);
367 | },
368 |
369 | .select => {
370 | try self.types.checkPops(instr_idx, &.{.I32});
371 | const top1 = try self.types.pop(instr_idx);
372 | const top2 = try self.types.pop(instr_idx);
373 | if (top1 != top2) {
374 | return error.StackMismatch;
375 | }
376 | self.types.pushAt(instr_idx, top1);
377 | },
378 |
379 | // Drops *any* value, no check needed
380 | .drop => _ = try self.types.pop(instr_idx),
381 |
382 | else => {
383 | const op_meta = Op.Meta.of(instr.op);
384 | for (op_meta.pop) |pop| {
385 | try self.types.checkPops(instr_idx, &.{asValue(pop)});
386 | }
387 |
388 | if (op_meta.push) |push| {
389 | self.types.pushAt(instr_idx, asValue(push));
390 | }
391 | },
392 | }
393 | }
394 |
395 | if (terminating_block_idx == null) {
396 | if (func_type.return_type) |return_type| {
397 | try self.types.checkPops(code.body.len, &.{return_type});
398 | }
399 |
400 | if (self.types.top != null) {
401 | return error.StackMismatch;
402 | }
403 | }
404 | }
405 |
406 | fn asValue(change: Op.Stack.Change) Module.Type.Value {
407 | return switch (change) {
408 | .I32 => .I32,
409 | .I64 => .I64,
410 | .F32 => .F32,
411 | .F64 => .F64,
412 | .Poly => unreachable,
413 | };
414 | }
415 |
416 | fn localType(local_idx: u32, params: []const Module.Type.Value, locals: []const Module.Type.Value) !Module.Type.Value {
417 | if (local_idx >= params.len + locals.len) return error.LocalIndexOutOfBounds;
418 | if (local_idx < params.len) {
419 | return params[local_idx];
420 | } else {
421 | return locals[local_idx - params.len];
422 | }
423 | }
424 | };
425 |
426 | test "smoke" {
427 | var fbs = std.io.fixedBufferStream(
428 | \\(module
429 | \\ (func (result i32)
430 | \\ i32.const 40
431 | \\ i32.const 2
432 | \\ i32.add))
433 | );
434 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
435 | defer module.deinit();
436 |
437 | _ = try PostProcess.init(&module);
438 | }
439 |
440 | test "add nothing" {
441 | var fbs = std.io.fixedBufferStream(
442 | \\(module
443 | \\ (func (result i32)
444 | \\ i32.add))
445 | );
446 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
447 | defer module.deinit();
448 |
449 | try std.testing.expectError(error.StackMismatch, PostProcess.init(&module));
450 | }
451 |
452 | test "add wrong types" {
453 | var fbs = std.io.fixedBufferStream(
454 | \\(module
455 | \\ (func (result i32)
456 | \\ i32.const 40
457 | \\ i64.const 2
458 | \\ i32.add))
459 | );
460 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
461 | defer module.deinit();
462 |
463 | try std.testing.expectError(error.StackMismatch, PostProcess.init(&module));
464 | }
465 |
466 | test "return nothing" {
467 | var fbs = std.io.fixedBufferStream(
468 | \\(module
469 | \\ (func (result i32)))
470 | );
471 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
472 | defer module.deinit();
473 |
474 | try std.testing.expectError(error.StackMismatch, PostProcess.init(&module));
475 | }
476 |
477 | test "return wrong type" {
478 | var fbs = std.io.fixedBufferStream(
479 | \\(module
480 | \\ (func (result i32)
481 | \\ i64.const 40))
482 | );
483 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
484 | defer module.deinit();
485 |
486 | try std.testing.expectError(error.StackMismatch, PostProcess.init(&module));
487 | }
488 |
489 | test "jump locations" {
490 | var fbs = std.io.fixedBufferStream(
491 | \\(module
492 | \\ (func
493 | \\ block ;; 0
494 | \\ loop ;; 1
495 | \\ br 0 ;; 2
496 | \\ br 1 ;; 3
497 | \\ end ;; 4
498 | \\ end ;; 5
499 | \\ ))
500 | );
501 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
502 | defer module.deinit();
503 |
504 | const process = try PostProcess.init(&module);
505 |
506 | const br_0 = process.jumps.get(.{ .func = 0, .instr = 2 }) orelse return error.JumpNotFound;
507 | try std.testing.expectEqual(@as(usize, 1), br_0.one.addr);
508 |
509 | const br_1 = process.jumps.get(.{ .func = 0, .instr = 3 }) orelse return error.JumpNotFound;
510 | try std.testing.expectEqual(@as(usize, 5), br_1.one.addr);
511 | }
512 |
513 | test "if/else locations" {
514 | var fbs = std.io.fixedBufferStream(
515 | \\(module
516 | \\ (func (result i32)
517 | \\ i32.const 0 ;; 0
518 | \\ if (result i32) ;; 1
519 | \\ i32.const 1 ;; 2
520 | \\ else ;; 3
521 | \\ i32.const 0 ;; 4
522 | \\ end ;; 5
523 | \\ ))
524 | );
525 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
526 | defer module.deinit();
527 |
528 | const process = try PostProcess.init(&module);
529 |
530 | const jump_if = process.jumps.get(.{ .func = 0, .instr = 1 }) orelse return error.JumpNotFound;
531 | try std.testing.expectEqual(@as(usize, 3), jump_if.one.addr);
532 | try std.testing.expectEqual(@as(usize, 0), jump_if.one.stack_unroll);
533 |
534 | const jump_else = process.jumps.get(.{ .func = 0, .instr = 3 }) orelse return error.JumpNotFound;
535 | try std.testing.expectEqual(@as(usize, 5), jump_else.one.addr);
536 | try std.testing.expectEqual(@as(usize, 0), jump_else.one.stack_unroll);
537 | }
538 |
539 | test "invalid global idx" {
540 | var fbs = std.io.fixedBufferStream(
541 | \\(module
542 | \\ (global $x i32 (i32.const -5))
543 | \\ (func (result i32)
544 | \\ global.get 1))
545 | );
546 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
547 | defer module.deinit();
548 | try std.testing.expectError(error.GlobalIndexOutOfBounds, PostProcess.init(&module));
549 | }
550 |
551 | test "valid global idx" {
552 | var fbs = std.io.fixedBufferStream(
553 | \\(module
554 | \\ (global $x i32 (i32.const -5))
555 | \\ (func (result i32)
556 | \\ global.get 0))
557 | );
558 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
559 | defer module.deinit();
560 | _ = try PostProcess.init(&module);
561 | }
562 |
563 | test "invalid local idx" {
564 | var fbs = std.io.fixedBufferStream(
565 | \\(module
566 | \\ (func (result i32)
567 | \\ local.get 0))
568 | );
569 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
570 | defer module.deinit();
571 | try std.testing.expectError(error.LocalIndexOutOfBounds, PostProcess.init(&module));
572 | }
573 |
574 | test "valid local idx" {
575 | var fbs = std.io.fixedBufferStream(
576 | \\(module
577 | \\ (func (param i32) (result i32)
578 | \\ local.get 0))
579 | );
580 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
581 | defer module.deinit();
582 | _ = try PostProcess.init(&module);
583 | }
584 |
585 | test "valid br flushing the stack" {
586 | var fbs = std.io.fixedBufferStream(
587 | \\(module
588 | \\ (func
589 | \\ block ;; 0
590 | \\ i32.const 1 ;; 1
591 | \\ br 0 ;; 2
592 | \\ i32.const 2 ;; 3
593 | \\ end)) ;; 4
594 | );
595 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
596 | defer module.deinit();
597 | _ = try PostProcess.init(&module);
598 | }
599 |
600 | test "valid return flushing the stack" {
601 | var fbs = std.io.fixedBufferStream(
602 | \\(module
603 | \\ (func
604 | \\ i32.const 1 ;; 0
605 | \\ return ;; 1
606 | \\ i32.const 2)) ;; 2
607 | );
608 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
609 | defer module.deinit();
610 | _ = try PostProcess.init(&module);
611 | }
612 |
613 | test "return a value from block" {
614 | var fbs = std.io.fixedBufferStream(
615 | \\(module
616 | \\ (func (result i32)
617 | \\ block ;; 0
618 | \\ i32.const 1 ;; 1
619 | \\ return ;; 2
620 | \\ end ;; 3
621 | \\ i32.const 42)) ;; 4
622 | );
623 | var module = try Wat.parseNoValidate(std.testing.allocator, fbs.reader());
624 | defer module.deinit();
625 | _ = try PostProcess.init(&module);
626 | }
627 |
--------------------------------------------------------------------------------
/src/op.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Execution = @import("execution.zig");
3 |
4 | pub const Meta = struct {
5 | code: std.wasm.Opcode,
6 | func_name: []const u8,
7 | arg_kind: Arg.Kind,
8 | push: ?Stack.Change,
9 | pop: []const Stack.Change,
10 |
11 | pub fn name(self: Meta) []const u8 {
12 | return self.func_name[5..];
13 | }
14 |
15 | pub const sparse = sparse: {
16 | @setEvalBranchQuota(10000);
17 | const decls = publicFunctions(Impl);
18 | var result: [decls.len]Meta = undefined;
19 | for (decls) |decl, i| {
20 | const args = @typeInfo(decl.data.Fn.fn_type).Fn.args;
21 | const ctx_type = args[0].arg_type.?;
22 | const arg_type = args[1].arg_type.?;
23 | const pop_type = args[2].arg_type.?;
24 | if (@typeInfo(pop_type) != .Pointer) @compileError("Pop must be a pointer: " ++ @typeName(pop_type));
25 | const pop_ref_type = std.meta.Child(pop_type);
26 |
27 | const return_type = decl.data.Fn.return_type;
28 | const push_type = switch (@typeInfo(decl.data.Fn.return_type)) {
29 | .ErrorUnion => |eu_info| blk: {
30 | for (std.meta.fields(eu_info.error_set)) |err| {
31 | if (!errContains(WasmTrap, err.name)) {
32 | @compileError("Unhandleable error: " ++ err.name);
33 | }
34 | }
35 | break :blk eu_info.payload;
36 | },
37 | else => return_type,
38 | };
39 |
40 | result[i] = .{
41 | .code = parseOpcode(decl.name) catch @compileError("Not a known hex: " ++ decl.name[0..4]),
42 | .func_name = decl.name,
43 | .arg_kind = Arg.Kind.init(arg_type),
44 | .push = Stack.Change.initPush(push_type),
45 | .pop = switch (pop_ref_type) {
46 | Fixval.Void => &[0]Stack.Change{},
47 | else => switch (@typeInfo(pop_ref_type)) {
48 | .Union => &[1]Stack.Change{Stack.Change.initPop(pop_ref_type)},
49 | .Struct => |s_info| blk: {
50 | var pop_changes: [s_info.fields.len]Stack.Change = undefined;
51 | for (s_info.fields) |field, f| {
52 | pop_changes[f] = Stack.Change.initPop(field.field_type);
53 | }
54 | break :blk &pop_changes;
55 | },
56 | else => @compileError("Unsupported pop type: " ++ @typeName(pop_type)),
57 | },
58 | },
59 | };
60 | }
61 |
62 | break :sparse result;
63 | };
64 |
65 | pub fn of(code: std.wasm.Opcode) Meta {
66 | return all[@enumToInt(code)].?;
67 | }
68 |
69 | pub const all = blk: {
70 | var result = [_]?Meta{null} ** 256;
71 |
72 | for (sparse) |meta| {
73 | const raw_code = @enumToInt(meta.code);
74 | if (result[raw_code] != null) {
75 | var buf: [100]u8 = undefined;
76 | @compileError(try std.fmt.bufPrint(&buf, "Collision: '0x{X} {}'", .{ code, meta.name }));
77 | }
78 | result[raw_code] = meta;
79 | }
80 | break :blk result;
81 | };
82 | };
83 |
84 | /// Generic memory chunk capable of representing any wasm type.
85 | /// Useful for storing stack variables, locals, and globals.
86 | pub const Fixval = extern union {
87 | I32: i32,
88 | U32: u32,
89 | I64: i64,
90 | U64: u64,
91 | F32: f32,
92 | F64: f64,
93 | V128: i128, // TODO: make this a real vector
94 |
95 | pub fn format(self: Fixval, comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
96 | try writer.print("Fixval(0x{x})", .{@bitCast(u128, self)});
97 | }
98 |
99 | pub const Void = extern struct {
100 | _pad: u128,
101 | };
102 |
103 | const I32 = extern union {
104 | data: i32,
105 | _pad: u128,
106 | };
107 |
108 | const U32 = extern union {
109 | data: u32,
110 | _pad: u128,
111 | };
112 |
113 | const I64 = extern union {
114 | data: i64,
115 | _pad: u128,
116 | };
117 |
118 | const U64 = extern union {
119 | data: u64,
120 | _pad: u128,
121 | };
122 |
123 | const F32 = extern union {
124 | data: f32,
125 | _pad: u128,
126 | };
127 |
128 | const F64 = extern union {
129 | data: f64,
130 | _pad: u128,
131 | };
132 | };
133 |
134 | test "Fixval subtype sizes" {
135 | inline for (std.meta.declarations(Fixval)) |decl| {
136 | if (decl.data == .Type) {
137 | try std.testing.expectEqual(@sizeOf(Fixval), @sizeOf(decl.data.Type));
138 | }
139 | }
140 | }
141 |
142 | pub const Arg = extern union {
143 | I32: i32,
144 | U32: u32,
145 | I64: i64,
146 | U64: u64,
147 | F32: f32,
148 | F64: f64,
149 | Type: Type,
150 | U32z: U32z,
151 | Mem: Mem,
152 | Array: Array,
153 |
154 | pub fn format(self: Arg, comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void {
155 | try writer.print("Arg(0x{x})", .{@bitCast(u128, self)});
156 | }
157 |
158 | pub const Kind = enum {
159 | Void,
160 | I32,
161 | U32,
162 | I64,
163 | U64,
164 | F32,
165 | F64,
166 | Type,
167 | U32z,
168 | Mem,
169 | Array,
170 |
171 | fn init(comptime T: type) Kind {
172 | return switch (T) {
173 | Fixval.Void => .Void,
174 | Fixval.I32 => .I32,
175 | Fixval.U32 => .U32,
176 | Fixval.I64 => .I64,
177 | Fixval.U64 => .U64,
178 | Fixval.F32 => .F32,
179 | Fixval.F64 => .F64,
180 | Type => .Type,
181 | U32z => .U32z,
182 | Mem => .Mem,
183 | Array => .Array,
184 | else => @compileError("Unsupported arg type: " ++ @typeName(T)),
185 | };
186 | }
187 | };
188 |
189 | pub const Type = enum(u128) {
190 | Void = 0x40,
191 | I32 = 0x7F,
192 | I64 = 0x7E,
193 | F32 = 0x7D,
194 | F64 = 0x7C,
195 | };
196 |
197 | pub const U32z = extern struct {
198 | data: u32,
199 | reserved: u8,
200 | // Zig bug -- won't pack correctly without manually splitting this
201 | _pad0: u8 = 0,
202 | _pad1: u16 = 0,
203 | _pad2: u64 = 0,
204 | };
205 |
206 | pub const Mem = extern struct {
207 | offset: u32,
208 | align_: u32,
209 | _pad: u64 = 0,
210 | };
211 |
212 | // TODO: make this extern
213 | pub const Array = packed struct {
214 | ptr: [*]u32,
215 | len: usize,
216 | _pad: std.meta.Int(.unsigned, 128 - 2 * @bitSizeOf(usize)) = 0,
217 | };
218 | };
219 |
220 | pub const Stack = struct {
221 | pub const Change = enum {
222 | I32,
223 | I64,
224 | F32,
225 | F64,
226 | Poly,
227 |
228 | fn initPush(comptime T: type) ?Change {
229 | return switch (T) {
230 | void => null,
231 | i32, u32 => Change.I32,
232 | i64, u64 => Change.I64,
233 | f32 => Change.F32,
234 | f64 => Change.F64,
235 | Fixval => Change.Poly,
236 | else => @compileError("Unsupported type: " ++ @typeName(T)),
237 | };
238 | }
239 |
240 | fn initPop(comptime T: type) Change {
241 | return switch (T) {
242 | Fixval.I32, Fixval.U32 => .I32,
243 | Fixval.I64, Fixval.U64 => .I64,
244 | Fixval.F32 => .F32,
245 | Fixval.F64 => .F64,
246 | Fixval => .Poly,
247 | else => @compileError("Unsupported type: " ++ @typeName(T)),
248 | };
249 | }
250 | };
251 | };
252 |
253 | fn errContains(comptime err_set: type, comptime name: []const u8) bool {
254 | std.debug.assert(@typeInfo(err_set) == .ErrorSet);
255 | for (std.meta.fields(err_set)) |err| {
256 | if (std.mem.eql(u8, err.name, name)) {
257 | return true;
258 | }
259 | }
260 | return false;
261 | }
262 |
263 | fn publicFunctions(comptime T: type) []std.builtin.TypeInfo.Declaration {
264 | const decls = std.meta.declarations(T);
265 | var result: [decls.len]std.builtin.TypeInfo.Declaration = undefined;
266 | var cursor: usize = 0;
267 | for (decls) |decl| {
268 | if (decl.is_pub and decl.data == .Fn) {
269 | result[cursor] = decl;
270 | cursor += 1;
271 | }
272 | }
273 |
274 | return result[0..cursor];
275 | }
276 |
277 | test "ops" {
278 | const nop = Meta.of(.nop);
279 | try std.testing.expectEqual(nop.arg_kind, .Void);
280 | try std.testing.expectEqual(nop.push, null);
281 | try std.testing.expectEqual(nop.pop.len, 0);
282 |
283 | const i32_load = Meta.of(.i32_load);
284 | try std.testing.expectEqual(i32_load.arg_kind, .Mem);
285 | try std.testing.expectEqual(i32_load.push, .I32);
286 |
287 | try std.testing.expectEqual(i32_load.pop.len, 1);
288 | try std.testing.expectEqual(i32_load.pop[0], .I32);
289 |
290 | const select = Meta.of(.select);
291 | try std.testing.expectEqual(select.arg_kind, .Void);
292 | try std.testing.expectEqual(select.push, .Poly);
293 |
294 | try std.testing.expectEqual(select.pop.len, 3);
295 | try std.testing.expectEqual(select.pop[0], .Poly);
296 | try std.testing.expectEqual(select.pop[1], .Poly);
297 | try std.testing.expectEqual(select.pop[2], .I32);
298 | }
299 |
300 | pub const WasmTrap = error{
301 | Unreachable,
302 | Overflow,
303 | OutOfBounds,
304 | DivisionByZero,
305 | InvalidConversionToInteger,
306 | IndirectCalleeAbsent,
307 | IndirectCallTypeMismatch,
308 | };
309 |
310 | pub fn step(op: std.wasm.Opcode, ctx: *Execution, arg: Arg, pop: [*]Fixval) WasmTrap!?Fixval {
311 | // TODO: test out function pointers for performance comparison
312 | // LLVM optimizes this inline for / mem.eql as a jump table
313 | // Please benchmark if we try to to optimize this.
314 | inline for (Meta.sparse) |meta| {
315 | if (meta.code == op) {
316 | return stepName(meta.func_name, ctx, arg, pop);
317 | }
318 | }
319 |
320 | unreachable; // Op parse error
321 | }
322 |
323 | pub inline fn stepName(comptime func_name: []const u8, ctx: *Execution, arg: Arg, pop: [*]Fixval) WasmTrap!?Fixval {
324 | const func = @field(Impl, func_name);
325 | const args = @typeInfo(@TypeOf(func)).Fn.args;
326 | const result = func(
327 | ctx,
328 | switch (args[1].arg_type.?) {
329 | Arg.Type => arg.Type,
330 | else => @bitCast(args[1].arg_type.?, arg),
331 | },
332 | @ptrCast(args[2].arg_type.?, pop),
333 | );
334 |
335 | const result_value = if (@typeInfo(@TypeOf(result)) == .ErrorUnion) try result else result;
336 |
337 | return switch (@TypeOf(result_value)) {
338 | void => null,
339 | i32 => Fixval{ .I32 = result_value },
340 | u32 => Fixval{ .U32 = result_value },
341 | i64 => Fixval{ .I64 = result_value },
342 | u64 => Fixval{ .U64 = result_value },
343 | f32 => Fixval{ .F32 = result_value },
344 | f64 => Fixval{ .F64 = result_value },
345 | Fixval => result_value,
346 | else => @compileError("Op return unimplemented: " ++ @typeName(@TypeOf(result_value))),
347 | };
348 | }
349 |
350 | fn parseOpcode(name: []const u8) !std.wasm.Opcode {
351 | if (name[0] != '0' or name[1] != 'x' or name[4] != ' ') {
352 | return error.InvalidCharacter;
353 | }
354 |
355 | return @intToEnum(std.wasm.Opcode, try std.fmt.parseInt(u8, name[2..4], 16));
356 | }
357 |
358 | const Impl = struct {
359 | const Void = Fixval.Void;
360 | const I32 = Fixval.I32;
361 | const I64 = Fixval.I64;
362 | const U32 = Fixval.U32;
363 | const U64 = Fixval.U64;
364 | const F32 = Fixval.F32;
365 | const F64 = Fixval.F64;
366 |
367 | // TODO: replace once Zig can define tuple types
368 | fn Pair(comptime T0: type, comptime T1: type) type {
369 | return extern struct {
370 | _0: T0,
371 | _1: T1,
372 | };
373 | }
374 |
375 | // TODO: replace once Zig can define tuple types
376 | fn Triple(comptime T0: type, comptime T1: type, comptime T2: type) type {
377 | return extern struct {
378 | _0: T0,
379 | _1: T1,
380 | _2: T2,
381 | };
382 | }
383 |
384 | pub fn @"0x00 unreachable"(ctx: *Execution, arg: Void, pop: *Void) !void {
385 | return error.Unreachable;
386 | }
387 |
388 | pub fn @"0x01 nop"(ctx: *Execution, arg: Void, pop: *Void) void {}
389 |
390 | pub fn @"0x02 block"(ctx: *Execution, arg: Arg.Type, pop: *Void) void {
391 | // noop, setup metadata only
392 | }
393 |
394 | pub fn @"0x03 loop"(ctx: *Execution, arg: Arg.Type, pop: *Void) void {
395 | // noop, setup metadata only
396 | }
397 |
398 | pub fn @"0x04 if"(ctx: *Execution, arg: Arg.Type, pop: *I32) void {
399 | if (pop.data == 0) {
400 | ctx.jump(null);
401 | }
402 | }
403 |
404 | pub fn @"0x05 else"(ctx: *Execution, arg: Void, pop: *Void) void {
405 | // If we are executing this instruction, it means the `if` block was executed, so we should skip until the end
406 | ctx.jump(null);
407 | }
408 |
409 | pub fn @"0x0B end"(ctx: *Execution, arg: Void, pop: *Void) void {
410 | // noop, setup metadata only
411 | // Technically this can return the top value from the stack,
412 | // but it would be immediately pushed on
413 | }
414 |
415 | pub fn @"0x0C br"(ctx: *Execution, arg: U32, pop: *Void) void {
416 | ctx.jump(null);
417 | }
418 | pub fn @"0x0D br_if"(ctx: *Execution, arg: U32, pop: *I32) void {
419 | if (pop.data != 0) {
420 | ctx.jump(null);
421 | }
422 | }
423 | pub fn @"0x0E br_table"(ctx: *Execution, arg: Arg.Array, pop: *U32) void {
424 | const idx = std.math.min(pop.data, arg.len - 1); // default to last item. Pretty handy!
425 | ctx.jump(arg.ptr[idx]);
426 | }
427 | pub fn @"0x0F return"(ctx: *Execution, arg: Void, pop: *Void) void {
428 | // Forces unwindCall()
429 | ctx.current_frame.instr = std.math.maxInt(u32);
430 | }
431 |
432 | pub fn @"0x10 call"(ctx: *Execution, arg: U32, pop: *Void) !void {
433 | try ctx.initCall(arg.data);
434 | }
435 | pub fn @"0x11 call_indirect"(ctx: *Execution, arg: Arg.U32z, pop: *U32) !void {
436 | const func_id = pop.data;
437 | if (func_id >= ctx.funcs.len) {
438 | return error.IndirectCalleeAbsent;
439 | }
440 | const func = ctx.funcs[func_id];
441 | if (func.func_type != arg.data) {
442 | return error.IndirectCallTypeMismatch;
443 | }
444 | try ctx.initCall(func_id);
445 | }
446 | pub fn @"0x1A drop"(ctx: *Execution, arg: Void, pop: *Fixval) void {
447 | // Do nothing with the popped value
448 | }
449 | pub fn @"0x1B select"(ctx: *Execution, arg: Void, pop: *Triple(Fixval, Fixval, I32)) Fixval {
450 | return if (pop._2.data != 0) pop._0 else pop._1;
451 | }
452 |
453 | pub fn @"0x20 local.get"(ctx: *Execution, arg: U32, pop: *Void) Fixval {
454 | return ctx.getLocal(arg.data);
455 | }
456 | pub fn @"0x21 local.set"(ctx: *Execution, arg: U32, pop: *Fixval) void {
457 | ctx.setLocal(arg.data, pop.*);
458 | }
459 | pub fn @"0x22 local.tee"(ctx: *Execution, arg: U32, pop: *Fixval) Fixval {
460 | ctx.setLocal(arg.data, pop.*);
461 | return pop.*;
462 | }
463 | pub fn @"0x23 global.get"(ctx: *Execution, arg: U32, pop: *Void) Fixval {
464 | return ctx.getGlobal(arg.data);
465 | }
466 | pub fn @"0x24 global.set"(ctx: *Execution, arg: U32, pop: *Fixval) void {
467 | ctx.setGlobal(arg.data, pop.*);
468 | }
469 | pub fn @"0x28 i32.load"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !i32 {
470 | return try ctx.memory.load(i32, pop.data, mem.offset);
471 | }
472 | pub fn @"0x29 i64.load"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !i64 {
473 | return try ctx.memory.load(i64, pop.data, mem.offset);
474 | }
475 | pub fn @"0x2A f32.load"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !f32 {
476 | return try ctx.memory.load(f32, pop.data, mem.offset);
477 | }
478 | pub fn @"0x2B f64.load"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !f64 {
479 | return try ctx.memory.load(f64, pop.data, mem.offset);
480 | }
481 | pub fn @"0x2C i32.load8_s"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !i32 {
482 | return try ctx.memory.load(i8, pop.data, mem.offset);
483 | }
484 | pub fn @"0x2D i32.load8_u"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !u32 {
485 | return try ctx.memory.load(u8, pop.data, mem.offset);
486 | }
487 | pub fn @"0x2E i32.load16_s"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !i32 {
488 | return try ctx.memory.load(i16, pop.data, mem.offset);
489 | }
490 | pub fn @"0x2F i32.load16_u"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !u32 {
491 | return try ctx.memory.load(u16, pop.data, mem.offset);
492 | }
493 |
494 | pub fn @"0x30 i64.load8_s"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !i64 {
495 | return try ctx.memory.load(i8, pop.data, mem.offset);
496 | }
497 | pub fn @"0x31 i64.load8_u"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !i64 {
498 | return try ctx.memory.load(u8, pop.data, mem.offset);
499 | }
500 | pub fn @"0x32 i64.load16_s"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !i64 {
501 | return try ctx.memory.load(i16, pop.data, mem.offset);
502 | }
503 | pub fn @"0x33 i64.load16_u"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !i64 {
504 | return try ctx.memory.load(u16, pop.data, mem.offset);
505 | }
506 | pub fn @"0x34 i64.load32_s"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !i64 {
507 | return try ctx.memory.load(i32, pop.data, mem.offset);
508 | }
509 | pub fn @"0x35 i64.load32_u"(ctx: *Execution, mem: Arg.Mem, pop: *U32) !i64 {
510 | return try ctx.memory.load(u32, pop.data, mem.offset);
511 | }
512 | pub fn @"0x36 i32.store"(ctx: *Execution, mem: Arg.Mem, pop: *Pair(U32, I32)) !void {
513 | return try ctx.memory.store(i32, pop._0.data, mem.offset, pop._1.data);
514 | }
515 | pub fn @"0x37 i64.store"(ctx: *Execution, mem: Arg.Mem, pop: *Pair(U32, I64)) !void {
516 | return try ctx.memory.store(i64, pop._0.data, mem.offset, pop._1.data);
517 | }
518 | pub fn @"0x38 f32.store"(ctx: *Execution, mem: Arg.Mem, pop: *Pair(U32, F32)) !void {
519 | return try ctx.memory.store(f32, pop._0.data, mem.offset, pop._1.data);
520 | }
521 | pub fn @"0x39 f64.store"(ctx: *Execution, mem: Arg.Mem, pop: *Pair(U32, F64)) !void {
522 | return try ctx.memory.store(f64, pop._0.data, mem.offset, pop._1.data);
523 | }
524 | pub fn @"0x3A i32.store8"(ctx: *Execution, mem: Arg.Mem, pop: *Pair(U32, I32)) !void {
525 | return try ctx.memory.store(i8, pop._0.data, mem.offset, @truncate(i8, pop._1.data));
526 | }
527 | pub fn @"0x3B i32.store16"(ctx: *Execution, mem: Arg.Mem, pop: *Pair(U32, I32)) !void {
528 | return try ctx.memory.store(i16, pop._0.data, mem.offset, @truncate(i16, pop._1.data));
529 | }
530 | pub fn @"0x3C i64.store8"(ctx: *Execution, mem: Arg.Mem, pop: *Pair(U32, I64)) !void {
531 | return try ctx.memory.store(i8, pop._0.data, mem.offset, @truncate(i8, pop._1.data));
532 | }
533 | pub fn @"0x3D i64.store16"(ctx: *Execution, mem: Arg.Mem, pop: *Pair(U32, I64)) !void {
534 | return try ctx.memory.store(i16, pop._0.data, mem.offset, @truncate(i16, pop._1.data));
535 | }
536 | pub fn @"0x3E i64.store32"(ctx: *Execution, mem: Arg.Mem, pop: *Pair(U32, I64)) !void {
537 | return try ctx.memory.store(i32, pop._0.data, mem.offset, @truncate(i32, pop._1.data));
538 | }
539 | pub fn @"0x3F memory.size"(ctx: *Execution, arg: Void, pop: *Void) u32 {
540 | return ctx.memory.pageCount();
541 | }
542 |
543 | pub fn @"0x40 memory.grow"(ctx: *Execution, arg: Void, pop: *U32) i32 {
544 | ctx.memory.grow(@intCast(u16, pop.data)) catch |err| switch (err) {
545 | error.OutOfMemory => return @as(i32, -1),
546 | };
547 | return ctx.memory.pageCount();
548 | }
549 | pub fn @"0x41 i32.const"(ctx: *Execution, arg: I32, pop: *Void) i32 {
550 | return arg.data;
551 | }
552 | pub fn @"0x42 i64.const"(ctx: *Execution, arg: I64, pop: *Void) i64 {
553 | return arg.data;
554 | }
555 | pub fn @"0x43 f32.const"(ctx: *Execution, arg: F32, pop: *Void) f32 {
556 | return arg.data;
557 | }
558 | pub fn @"0x44 f64.const"(ctx: *Execution, arg: F64, pop: *Void) f64 {
559 | return arg.data;
560 | }
561 | pub fn @"0x45 i32.eqz"(ctx: *Execution, arg: Void, pop: *I32) i32 {
562 | return @boolToInt(pop.data == 0);
563 | }
564 | pub fn @"0x46 i32.eq"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) i32 {
565 | return @boolToInt(pop._0.data == pop._1.data);
566 | }
567 | pub fn @"0x47 i32.ne"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) i32 {
568 | return @boolToInt(pop._0.data != pop._1.data);
569 | }
570 | pub fn @"0x48 i32.lt_s"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) i32 {
571 | return @boolToInt(pop._0.data < pop._1.data);
572 | }
573 | pub fn @"0x49 i32.lt_u"(ctx: *Execution, arg: Void, pop: *Pair(U32, U32)) i32 {
574 | return @boolToInt(pop._0.data < pop._1.data);
575 | }
576 | pub fn @"0x4A i32.gt_s"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) i32 {
577 | return @boolToInt(pop._0.data > pop._1.data);
578 | }
579 | pub fn @"0x4B i32.gt_u"(ctx: *Execution, arg: Void, pop: *Pair(U32, U32)) i32 {
580 | return @boolToInt(pop._0.data > pop._1.data);
581 | }
582 | pub fn @"0x4C i32.le_s"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) i32 {
583 | return @boolToInt(pop._0.data <= pop._1.data);
584 | }
585 | pub fn @"0x4D i32.le_u"(ctx: *Execution, arg: Void, pop: *Pair(U32, U32)) i32 {
586 | return @boolToInt(pop._0.data <= pop._1.data);
587 | }
588 | pub fn @"0x4E i32.ge_s"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) i32 {
589 | return @boolToInt(pop._0.data >= pop._1.data);
590 | }
591 | pub fn @"0x4F i32.ge_u"(ctx: *Execution, arg: Void, pop: *Pair(U32, U32)) i32 {
592 | return @boolToInt(pop._0.data >= pop._1.data);
593 | }
594 |
595 | pub fn @"0x50 i64.eqz"(ctx: *Execution, arg: Void, pop: *I64) i32 {
596 | return @boolToInt(pop.data == 0);
597 | }
598 | pub fn @"0x51 i64.eq"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) i32 {
599 | return @boolToInt(pop._0.data == pop._1.data);
600 | }
601 | pub fn @"0x52 i64.ne"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) i32 {
602 | return @boolToInt(pop._0.data != pop._1.data);
603 | }
604 | pub fn @"0x53 i64.lt_s"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) i32 {
605 | return @boolToInt(pop._0.data < pop._1.data);
606 | }
607 | pub fn @"0x54 i64.lt_u"(ctx: *Execution, arg: Void, pop: *Pair(U64, U64)) i32 {
608 | return @boolToInt(pop._0.data < pop._1.data);
609 | }
610 | pub fn @"0x55 i64.gt_s"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) i32 {
611 | return @boolToInt(pop._0.data > pop._1.data);
612 | }
613 | pub fn @"0x56 i64.gt_u"(ctx: *Execution, arg: Void, pop: *Pair(U64, U64)) i32 {
614 | return @boolToInt(pop._0.data > pop._1.data);
615 | }
616 | pub fn @"0x57 i64.le_s"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) i32 {
617 | return @boolToInt(pop._0.data <= pop._1.data);
618 | }
619 | pub fn @"0x58 i64.le_u"(ctx: *Execution, arg: Void, pop: *Pair(U64, U64)) i32 {
620 | return @boolToInt(pop._0.data <= pop._1.data);
621 | }
622 | pub fn @"0x59 i64.ge_s"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) i32 {
623 | return @boolToInt(pop._0.data >= pop._1.data);
624 | }
625 | pub fn @"0x5A i64.ge_u"(ctx: *Execution, arg: Void, pop: *Pair(U64, U64)) i32 {
626 | return @boolToInt(pop._0.data >= pop._1.data);
627 | }
628 | pub fn @"0x5B f32.eq"(ctx: *Execution, arg: Void, pop: *Pair(F32, F32)) i32 {
629 | return @boolToInt(pop._0.data == pop._1.data);
630 | }
631 | pub fn @"0x5C f32.ne"(ctx: *Execution, arg: Void, pop: *Pair(F32, F32)) i32 {
632 | return @boolToInt(pop._0.data != pop._1.data);
633 | }
634 | pub fn @"0x5D f32.lt"(ctx: *Execution, arg: Void, pop: *Pair(F32, F32)) i32 {
635 | return @boolToInt(pop._0.data < pop._1.data);
636 | }
637 | pub fn @"0x5E f32.gt"(ctx: *Execution, arg: Void, pop: *Pair(F32, F32)) i32 {
638 | return @boolToInt(pop._0.data > pop._1.data);
639 | }
640 | pub fn @"0x5F f32.le"(ctx: *Execution, arg: Void, pop: *Pair(F32, F32)) i32 {
641 | return @boolToInt(pop._0.data <= pop._1.data);
642 | }
643 |
644 | pub fn @"0x60 f32.ge"(ctx: *Execution, arg: Void, pop: *Pair(F32, F32)) i32 {
645 | return @boolToInt(pop._0.data >= pop._1.data);
646 | }
647 | pub fn @"0x61 f64.eq"(ctx: *Execution, arg: Void, pop: *Pair(F64, F64)) i32 {
648 | return @boolToInt(pop._0.data == pop._1.data);
649 | }
650 | pub fn @"0x62 f64.ne"(ctx: *Execution, arg: Void, pop: *Pair(F64, F64)) i32 {
651 | return @boolToInt(pop._0.data != pop._1.data);
652 | }
653 | pub fn @"0x63 f64.lt"(ctx: *Execution, arg: Void, pop: *Pair(F64, F64)) i32 {
654 | return @boolToInt(pop._0.data < pop._1.data);
655 | }
656 | pub fn @"0x64 f64.gt"(ctx: *Execution, arg: Void, pop: *Pair(F64, F64)) i32 {
657 | return @boolToInt(pop._0.data > pop._1.data);
658 | }
659 | pub fn @"0x65 f64.le"(ctx: *Execution, arg: Void, pop: *Pair(F64, F64)) i32 {
660 | return @boolToInt(pop._0.data <= pop._1.data);
661 | }
662 | pub fn @"0x66 f64.ge"(ctx: *Execution, arg: Void, pop: *Pair(F64, F64)) i32 {
663 | return @boolToInt(pop._0.data >= pop._1.data);
664 | }
665 | pub fn @"0x67 i32.clz"(ctx: *Execution, arg: Void, pop: *I32) i32 {
666 | return @clz(i32, pop.data);
667 | }
668 | pub fn @"0x68 i32.ctz"(ctx: *Execution, arg: Void, pop: *I32) i32 {
669 | return @ctz(i32, pop.data);
670 | }
671 | pub fn @"0x69 i32.popcnt"(ctx: *Execution, arg: Void, pop: *I32) i32 {
672 | return @popCount(i32, pop.data);
673 | }
674 | pub fn @"0x6A i32.add"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) i32 {
675 | return pop._0.data +% pop._1.data;
676 | }
677 | pub fn @"0x6B i32.sub"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) i32 {
678 | return pop._0.data -% pop._1.data;
679 | }
680 | pub fn @"0x6C i32.mul"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) i32 {
681 | return pop._0.data *% pop._1.data;
682 | }
683 | pub fn @"0x6D i32.div_s"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) !i32 {
684 | if (pop._1.data == 0) return error.DivisionByZero;
685 | if (pop._0.data == std.math.minInt(i32) and pop._1.data == -1) return error.Overflow;
686 | return @divTrunc(pop._0.data, pop._1.data);
687 | }
688 | pub fn @"0x6E i32.div_u"(ctx: *Execution, arg: Void, pop: *Pair(U32, U32)) !u32 {
689 | if (pop._1.data == 0) return error.DivisionByZero;
690 | return @divFloor(pop._0.data, pop._1.data);
691 | }
692 | pub fn @"0x6F i32.rem_s"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) !i32 {
693 | if (pop._1.data == 0) return error.DivisionByZero;
694 | const abs_0 = std.math.absCast(pop._0.data);
695 | const abs_1 = std.math.absCast(pop._1.data);
696 | const val = @intCast(i32, @rem(abs_0, abs_1));
697 | return if (pop._0.data < 0) -val else val;
698 | }
699 |
700 | pub fn @"0x70 i32.rem_u"(ctx: *Execution, arg: Void, pop: *Pair(U32, U32)) !u32 {
701 | if (pop._1.data == 0) return error.DivisionByZero;
702 | return @mod(pop._0.data, pop._1.data);
703 | }
704 | pub fn @"0x71 i32.and"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) i32 {
705 | return pop._0.data & pop._1.data;
706 | }
707 | pub fn @"0x72 i32.or"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) i32 {
708 | return pop._0.data | pop._1.data;
709 | }
710 | pub fn @"0x73 i32.xor"(ctx: *Execution, arg: Void, pop: *Pair(I32, I32)) i32 {
711 | return pop._0.data ^ pop._1.data;
712 | }
713 | pub fn @"0x74 i32.shl"(ctx: *Execution, arg: Void, pop: *Pair(I32, U32)) i32 {
714 | return pop._0.data << @truncate(u5, pop._1.data);
715 | }
716 | pub fn @"0x75 i32.shr_s"(ctx: *Execution, arg: Void, pop: *Pair(I32, U32)) i32 {
717 | return pop._0.data >> @truncate(u5, pop._1.data);
718 | }
719 | pub fn @"0x76 i32.shr_u"(ctx: *Execution, arg: Void, pop: *Pair(U32, U32)) u32 {
720 | return pop._0.data >> @truncate(u5, pop._1.data);
721 | }
722 | pub fn @"0x77 i32.rotl"(ctx: *Execution, arg: Void, pop: *Pair(U32, U32)) u32 {
723 | return std.math.rotl(u32, pop._0.data, @truncate(u6, pop._1.data));
724 | }
725 | pub fn @"0x78 i32.rotr"(ctx: *Execution, arg: Void, pop: *Pair(U32, U32)) u32 {
726 | return std.math.rotr(u32, pop._0.data, @truncate(u6, pop._1.data));
727 | }
728 | pub fn @"0x79 i64.clz"(ctx: *Execution, arg: Void, pop: *I64) i64 {
729 | return @clz(i64, pop.data);
730 | }
731 | pub fn @"0x7A i64.ctz"(ctx: *Execution, arg: Void, pop: *I64) i64 {
732 | return @ctz(i64, pop.data);
733 | }
734 | pub fn @"0x7B i64.popcnt"(ctx: *Execution, arg: Void, pop: *I64) i64 {
735 | return @popCount(i64, pop.data);
736 | }
737 | pub fn @"0x7C i64.add"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) i64 {
738 | return pop._0.data +% pop._1.data;
739 | }
740 | pub fn @"0x7D i64.sub"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) i64 {
741 | return pop._0.data -% pop._1.data;
742 | }
743 | pub fn @"0x7E i64.mul"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) i64 {
744 | return pop._0.data *% pop._1.data;
745 | }
746 | pub fn @"0x7F i64.div_s"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) !i64 {
747 | if (pop._1.data == 0) return error.DivisionByZero;
748 | if (pop._0.data == std.math.minInt(i64) and pop._1.data == -1) return error.Overflow;
749 | return @divTrunc(pop._0.data, pop._1.data);
750 | }
751 |
752 | pub fn @"0x80 i64.div_u"(ctx: *Execution, arg: Void, pop: *Pair(U64, U64)) !u64 {
753 | if (pop._1.data == 0) return error.DivisionByZero;
754 | return @divFloor(pop._0.data, pop._1.data);
755 | }
756 | pub fn @"0x81 i64.rem_s"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) !i64 {
757 | if (pop._1.data == 0) return error.DivisionByZero;
758 | const abs_0 = std.math.absCast(pop._0.data);
759 | const abs_1 = std.math.absCast(pop._1.data);
760 | const val = @intCast(i64, @rem(abs_0, abs_1));
761 | return if (pop._0.data < 0) -val else val;
762 | }
763 | pub fn @"0x82 i64.rem_u"(ctx: *Execution, arg: Void, pop: *Pair(U64, U64)) !u64 {
764 | if (pop._1.data == 0) return error.DivisionByZero;
765 | return @mod(pop._0.data, pop._1.data);
766 | }
767 | pub fn @"0x83 i64.and"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) i64 {
768 | return pop._0.data & pop._1.data;
769 | }
770 | pub fn @"0x84 i64.or"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) i64 {
771 | return pop._0.data | pop._1.data;
772 | }
773 | pub fn @"0x85 i64.xor"(ctx: *Execution, arg: Void, pop: *Pair(I64, I64)) i64 {
774 | return pop._0.data ^ pop._1.data;
775 | }
776 | pub fn @"0x86 i64.shl"(ctx: *Execution, arg: Void, pop: *Pair(I64, U64)) i64 {
777 | return pop._0.data << @truncate(u6, pop._1.data);
778 | }
779 | pub fn @"0x87 i64.shr_s"(ctx: *Execution, arg: Void, pop: *Pair(I64, U64)) i64 {
780 | return pop._0.data >> @truncate(u6, pop._1.data);
781 | }
782 | pub fn @"0x88 i64.shr_u"(ctx: *Execution, arg: Void, pop: *Pair(U64, U64)) u64 {
783 | return pop._0.data >> @truncate(u6, pop._1.data);
784 | }
785 | pub fn @"0x89 i64.rotl"(ctx: *Execution, arg: Void, pop: *Pair(U64, U64)) u64 {
786 | return std.math.rotl(u64, pop._0.data, @truncate(u7, pop._1.data));
787 | }
788 | pub fn @"0x8A i64.rotr"(ctx: *Execution, arg: Void, pop: *Pair(U64, U64)) u64 {
789 | return std.math.rotr(u64, pop._0.data, @truncate(u7, pop._1.data));
790 | }
791 | pub fn @"0x8B f32.abs"(ctx: *Execution, arg: Void, pop: *F32) f32 {
792 | return @fabs(pop.data);
793 | }
794 | pub fn @"0x8C f32.neg"(ctx: *Execution, arg: Void, pop: *F32) f32 {
795 | return -pop.data;
796 | }
797 | pub fn @"0x8D f32.ceil"(ctx: *Execution, arg: Void, pop: *F32) f32 {
798 | return @ceil(pop.data);
799 | }
800 | pub fn @"0x8E f32.floor"(ctx: *Execution, arg: Void, pop: *F32) f32 {
801 | return @floor(pop.data);
802 | }
803 | pub fn @"0x8F f32.trunc"(ctx: *Execution, arg: Void, pop: *F32) f32 {
804 | return @trunc(pop.data);
805 | }
806 |
807 | pub fn @"0x90 f32.nearest"(ctx: *Execution, arg: Void, pop: *F32) f32 {
808 | return @round(pop.data);
809 | }
810 | pub fn @"0x91 f32.sqrt"(ctx: *Execution, arg: Void, pop: *F32) f32 {
811 | return @sqrt(pop.data);
812 | }
813 | pub fn @"0x92 f32.add"(ctx: *Execution, arg: Void, pop: *Pair(F32, F32)) f32 {
814 | return pop._0.data + pop._1.data;
815 | }
816 | pub fn @"0x93 f32.sub"(ctx: *Execution, arg: Void, pop: *Pair(F32, F32)) f32 {
817 | return pop._0.data - pop._1.data;
818 | }
819 | pub fn @"0x94 f32.mul"(ctx: *Execution, arg: Void, pop: *Pair(F32, F32)) f32 {
820 | return pop._0.data * pop._1.data;
821 | }
822 | pub fn @"0x95 f32.div"(ctx: *Execution, arg: Void, pop: *Pair(F32, F32)) f32 {
823 | return pop._0.data / pop._1.data;
824 | }
825 | pub fn @"0x96 f32.min"(ctx: *Execution, arg: Void, pop: *Pair(F32, F32)) f32 {
826 | return std.math.min(pop._0.data, pop._1.data);
827 | }
828 | pub fn @"0x97 f32.max"(ctx: *Execution, arg: Void, pop: *Pair(F32, F32)) f32 {
829 | return std.math.max(pop._0.data, pop._1.data);
830 | }
831 | pub fn @"0x98 f32.copysign"(ctx: *Execution, arg: Void, pop: *Pair(F32, F32)) f32 {
832 | return std.math.copysign(f32, pop._0.data, pop._1.data);
833 | }
834 | pub fn @"0x99 f64.abs"(ctx: *Execution, arg: Void, pop: *F64) f64 {
835 | return @fabs(pop.data);
836 | }
837 | pub fn @"0x9A f64.neg"(ctx: *Execution, arg: Void, pop: *F64) f64 {
838 | return -pop.data;
839 | }
840 | pub fn @"0x9B f64.ceil"(ctx: *Execution, arg: Void, pop: *F64) f64 {
841 | return @ceil(pop.data);
842 | }
843 | pub fn @"0x9C f64.floor"(ctx: *Execution, arg: Void, pop: *F64) f64 {
844 | return @floor(pop.data);
845 | }
846 | pub fn @"0x9D f64.trunc"(ctx: *Execution, arg: Void, pop: *F64) f64 {
847 | return @trunc(pop.data);
848 | }
849 | pub fn @"0x9E f64.nearest"(ctx: *Execution, arg: Void, pop: *F64) f64 {
850 | return @round(pop.data);
851 | }
852 | pub fn @"0x9F f64.sqrt"(ctx: *Execution, arg: Void, pop: *F64) f64 {
853 | return @sqrt(pop.data);
854 | }
855 | pub fn @"0xA0 f64.add"(ctx: *Execution, arg: Void, pop: *Pair(F64, F64)) f64 {
856 | return pop._0.data + pop._1.data;
857 | }
858 | pub fn @"0xA1 f64.sub"(ctx: *Execution, arg: Void, pop: *Pair(F64, F64)) f64 {
859 | return pop._0.data - pop._1.data;
860 | }
861 | pub fn @"0xA2 f64.mul"(ctx: *Execution, arg: Void, pop: *Pair(F64, F64)) f64 {
862 | return pop._0.data * pop._1.data;
863 | }
864 | pub fn @"0xA3 f64.div"(ctx: *Execution, arg: Void, pop: *Pair(F64, F64)) f64 {
865 | return pop._0.data / pop._1.data;
866 | }
867 | pub fn @"0xA4 f64.min"(ctx: *Execution, arg: Void, pop: *Pair(F64, F64)) f64 {
868 | return std.math.min(pop._0.data, pop._1.data);
869 | }
870 | pub fn @"0xA5 f64.max"(ctx: *Execution, arg: Void, pop: *Pair(F64, F64)) f64 {
871 | return std.math.max(pop._0.data, pop._1.data);
872 | }
873 | pub fn @"0xA6 f64.copysign"(ctx: *Execution, arg: Void, pop: *Pair(F64, F64)) f64 {
874 | return std.math.copysign(f64, pop._0.data, pop._1.data);
875 | }
876 | pub fn @"0xA7 i32.wrap_i64"(ctx: *Execution, arg: Void, pop: *U64) u32 {
877 | return @truncate(u32, std.math.maxInt(u32) & pop.data);
878 | }
879 | pub fn @"0xA8 i32.trunc_f32_s"(ctx: *Execution, arg: Void, pop: *F32) !i32 {
880 | return floatToInt(i32, f32, pop.data);
881 | }
882 | pub fn @"0xA9 i32.trunc_f32_u"(ctx: *Execution, arg: Void, pop: *F32) !u32 {
883 | return floatToInt(u32, f32, pop.data);
884 | }
885 | pub fn @"0xAA i32.trunc_f64_s"(ctx: *Execution, arg: Void, pop: *F64) !i32 {
886 | return floatToInt(i32, f64, pop.data);
887 | }
888 | pub fn @"0xAB i32.trunc_f64_u"(ctx: *Execution, arg: Void, pop: *F64) !u32 {
889 | return floatToInt(u32, f64, pop.data);
890 | }
891 | pub fn @"0xAC i64.extend_i32_s"(ctx: *Execution, arg: Void, pop: *I64) i64 {
892 | return pop.data;
893 | }
894 | pub fn @"0xAD i64.extend_i32_u"(ctx: *Execution, arg: Void, pop: *U32) u64 {
895 | return pop.data;
896 | }
897 | pub fn @"0xAE i64.trunc_f32_s"(ctx: *Execution, arg: Void, pop: *F32) !i64 {
898 | return floatToInt(i64, f32, pop.data);
899 | }
900 | pub fn @"0xAF i64.trunc_f32_u"(ctx: *Execution, arg: Void, pop: *F32) !u64 {
901 | return floatToInt(u64, f32, pop.data);
902 | }
903 |
904 | pub fn @"0xB0 i64.trunc_f64_s"(ctx: *Execution, arg: Void, pop: *F64) !i64 {
905 | return floatToInt(i64, f64, pop.data);
906 | }
907 | pub fn @"0xB1 i64.trunc_f64_u"(ctx: *Execution, arg: Void, pop: *F64) !u64 {
908 | return floatToInt(u64, f64, pop.data);
909 | }
910 | pub fn @"0xB2 f32.convert_i32_s"(ctx: *Execution, arg: Void, pop: *I32) f32 {
911 | return @intToFloat(f32, pop.data);
912 | }
913 | pub fn @"0xB3 f32.convert_i32_u"(ctx: *Execution, arg: Void, pop: *U32) f32 {
914 | return @intToFloat(f32, pop.data);
915 | }
916 | pub fn @"0xB4 f32.convert_i64_s"(ctx: *Execution, arg: Void, pop: *I64) f32 {
917 | return @intToFloat(f32, pop.data);
918 | }
919 | pub fn @"0xB5 f32.convert_i64_u"(ctx: *Execution, arg: Void, pop: *U64) f32 {
920 | return @intToFloat(f32, pop.data);
921 | }
922 | pub fn @"0xB6 f32.demote_f64"(ctx: *Execution, arg: Void, pop: *F64) f32 {
923 | return @floatCast(f32, pop.data);
924 | }
925 | pub fn @"0xB7 f64.convert_i32_s"(ctx: *Execution, arg: Void, pop: *I32) f64 {
926 | return @intToFloat(f64, pop.data);
927 | }
928 | pub fn @"0xB8 f64.convert_i32_u"(ctx: *Execution, arg: Void, pop: *U32) f64 {
929 | return @intToFloat(f64, pop.data);
930 | }
931 | pub fn @"0xB9 f64.convert_i64_s"(ctx: *Execution, arg: Void, pop: *I64) f64 {
932 | return @intToFloat(f64, pop.data);
933 | }
934 | pub fn @"0xBA f64.convert_i64_u"(ctx: *Execution, arg: Void, pop: *U64) f64 {
935 | return @intToFloat(f64, pop.data);
936 | }
937 | pub fn @"0xBB f64.promote_f32"(ctx: *Execution, arg: Void, pop: *F32) f64 {
938 | return @floatCast(f64, pop.data);
939 | }
940 | pub fn @"0xBC i32.reinterpret_f32"(ctx: *Execution, arg: Void, pop: *F32) i32 {
941 | return @bitCast(i32, pop.data);
942 | }
943 | pub fn @"0xBD i64.reinterpret_f64"(ctx: *Execution, arg: Void, pop: *F64) i64 {
944 | return @bitCast(i64, pop.data);
945 | }
946 | pub fn @"0xBE f32.reinterpret_i32"(ctx: *Execution, arg: Void, pop: *I32) f32 {
947 | return @bitCast(f32, pop.data);
948 | }
949 | pub fn @"0xBF f64.reinterpret_i64"(ctx: *Execution, arg: Void, pop: *I64) f64 {
950 | return @bitCast(f64, pop.data);
951 | }
952 |
953 | fn floatToInt(comptime Dst: type, comptime Src: type, val: Src) !Dst {
954 | if (!std.math.isFinite(val) or val > std.math.maxInt(Dst) or val < std.math.minInt(Dst)) {
955 | return error.InvalidConversionToInteger;
956 | }
957 | return @floatToInt(Dst, val);
958 | }
959 | };
960 |
--------------------------------------------------------------------------------
/src/util.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | /// Super simple "perfect hash" algorithm
3 | /// Only really useful for switching on strings
4 | // TODO: can we auto detect and promote the underlying type?
5 | pub fn Swhash(comptime max_bytes: comptime_int) type {
6 | const T = std.meta.Int(.unsigned, max_bytes * 8);
7 |
8 | return struct {
9 | pub fn match(string: []const u8) T {
10 | return hash(string) orelse std.math.maxInt(T);
11 | }
12 |
13 | pub fn case(comptime string: []const u8) T {
14 | return hash(string) orelse @compileError("Cannot hash '" ++ string ++ "'");
15 | }
16 |
17 | fn hash(string: []const u8) ?T {
18 | if (string.len > max_bytes) return null;
19 | var tmp = [_]u8{0} ** max_bytes;
20 | std.mem.copy(u8, &tmp, string);
21 | return std.mem.readIntNative(T, &tmp);
22 | }
23 | };
24 | }
25 |
26 | pub const RingAllocator = struct {
27 | buffer: []u8,
28 | alignment: u29,
29 | max_alloc_size: usize,
30 | curr_index: usize = 0,
31 | allocator: std.mem.Allocator = .{
32 | .allocFn = alloc,
33 | .resizeFn = resize,
34 | },
35 |
36 | pub fn init(buffer: []u8, max_alloc_size: usize) RingAllocator {
37 | std.debug.assert(@popCount(usize, max_alloc_size) == 1);
38 | std.debug.assert(buffer.len % max_alloc_size == 0);
39 | return .{
40 | .buffer = buffer,
41 | .alignment = @as(u29, 1) << @intCast(std.math.Log2Int(u29), @ctz(usize, max_alloc_size | @ptrToInt(buffer.ptr))),
42 | .max_alloc_size = max_alloc_size,
43 | };
44 | }
45 |
46 | const ShiftSize = std.math.Log2Int(usize);
47 | fn shiftSize(self: RingAllocator) ShiftSize {
48 | return @intCast(ShiftSize, @ctz(usize, self.max_alloc_size));
49 | }
50 |
51 | fn totalSlots(self: RingAllocator) usize {
52 | return self.buffer.len >> self.shiftSize();
53 | }
54 |
55 | pub fn ownsSlice(self: *const RingAllocator, slice: []u8) bool {
56 | return @ptrToInt(slice.ptr) >= @ptrToInt(self.buffer.ptr) and
57 | (@ptrToInt(slice.ptr) + slice.len) <= (@ptrToInt(self.buffer.ptr) + self.buffer.len);
58 | }
59 |
60 | fn alloc(allocator: *std.mem.Allocator, n: usize, ptr_align: u29, len_align: u29, return_address: usize) error{OutOfMemory}![]u8 {
61 | const self = @fieldParentPtr(RingAllocator, "allocator", allocator);
62 | std.debug.assert(ptr_align <= self.alignment);
63 | if (n >= self.max_alloc_size) {
64 | return error.OutOfMemory;
65 | }
66 |
67 | const start = self.curr_index << self.shiftSize();
68 | self.curr_index += 1;
69 | if (self.curr_index >= self.totalSlots()) {
70 | // Wrap around the ring
71 | self.curr_index = 0;
72 | }
73 |
74 | return self.buffer[start..][0..self.max_alloc_size];
75 | }
76 |
77 | fn resize(allocator: *std.mem.Allocator, buf: []u8, buf_align: u29, new_size: usize, len_align: u29, return_address: usize) error{OutOfMemory}!usize {
78 | const self = @fieldParentPtr(RingAllocator, "allocator", allocator);
79 | std.debug.assert(self.ownsSlice(buf)); // sanity check
80 | std.debug.assert(buf_align == 1);
81 |
82 | if (new_size >= self.max_alloc_size) {
83 | return error.OutOfMemory;
84 | }
85 |
86 | return new_size;
87 | }
88 | };
89 |
--------------------------------------------------------------------------------
/src/wasi.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Module = @import("module.zig");
3 | const Instance = @import("instance.zig");
4 | const Memory = @import("Memory.zig");
5 |
6 | const Wasi = @This();
7 |
8 | argv: [][]u8,
9 | environ: [][]u8 = &.{},
10 | exit_code: ?Exitcode = null,
11 |
12 | const P = Memory.P;
13 |
14 | pub fn run(self: *Wasi, allocator: *std.mem.Allocator, reader: anytype) !Exitcode {
15 | var module = try Module.parse(allocator, reader);
16 | defer module.deinit();
17 |
18 | var instance = try module.instantiate(allocator, self, struct {
19 | pub const wasi = imports;
20 | pub const wasi_snapshot_preview1 = imports;
21 | });
22 | defer instance.deinit();
23 |
24 | _ = instance.call("_start", .{}) catch |err| switch (err) {
25 | error.Unreachable => {
26 | if (self.exit_code) |code| return code;
27 | return err;
28 | },
29 | else => return err,
30 | };
31 | // TODO: what should we do if there is no explicit exit?
32 | return @intToEnum(Exitcode, 0);
33 | }
34 |
35 | pub const Size = u32;
36 |
37 | /// Non-negative file size or length of a region within a file.
38 | pub const Filesize: u64;
39 |
40 | /// Timestamp in nanoseconds.
41 | pub const Timestamp = u64;
42 |
43 | /// Identifiers for clocks.
44 | pub const ClockId = enum(u32) {
45 | /// The clock measuring real time. Time value zero corresponds with 1970-01-01T00:00:00Z.
46 | realtime = 0,
47 |
48 | /// The store-wide monotonic clock, which is defined as a clock measuring real time, whose value cannot be adjusted and which cannot have negative clock jumps. The epoch of this clock is undefined. The absolute time value of this clock therefore has no meaning.
49 | monotonic = 1,
50 |
51 | /// The CPU-time clock associated with the current process.
52 | process_cputime_id = 2,
53 |
54 | /// The CPU-time clock associated with the current thread.
55 | thread_cputime_id = 3,
56 | _,
57 | };
58 |
59 | /// Exit code generated by a process when exiting.
60 | pub const Exitcode = enum(u32) { _ };
61 |
62 | /// A file descriptor handle.
63 | const Fd = enum(u32) {
64 | stdin = 0,
65 | stdout = 1,
66 | stderr = 2,
67 | _,
68 | };
69 |
70 | /// A region of memory for scatter/gather reads.
71 | const Iovec = extern struct {
72 | buf: P(u8),
73 | len: Size,
74 | };
75 |
76 | /// Error codes returned by functions. Not all of these error codes are returned by the functions provided by this API; some are used in higher-level library layers, and others are provided merely for alignment with POSIX.
77 | pub const Errno = enum(u32) {
78 | /// No error occurred. System call completed successfully.
79 | success = 0,
80 |
81 | /// Permission denied.
82 | acces = 2,
83 |
84 | /// Resource unavailable, or operation would block.
85 | again = 6,
86 |
87 | /// Bad file descriptor.
88 | badf = 8,
89 |
90 | /// Connection aborted.
91 | connaborted = 13,
92 |
93 | /// Connection refused.
94 | connrefused = 14,
95 |
96 | /// Connection reset.
97 | connreset = 15,
98 |
99 | /// Reserved.
100 | dquot = 19,
101 |
102 | /// File too large.
103 | fbig = 22,
104 |
105 | /// Invalid argument.
106 | inval = 28,
107 |
108 | /// I/O error.
109 | io = 29,
110 |
111 | /// No buffer space available.
112 | nobufs = 42,
113 |
114 | /// No space left on device.
115 | nospc = 51,
116 |
117 | /// Function not supported.
118 | nosys = 52,
119 |
120 | /// Broken pipe.
121 | pipe = 64,
122 |
123 | unexpected = 0xAAAA,
124 | _,
125 | };
126 |
127 | /// File descriptor rights, determining which actions may be performed.
128 | pub const Rights = packed struct {
129 | fd_datasync: bool,
130 | fd_read: bool,
131 | fd_seek: bool,
132 | fd_fdstat_set_flags: bool,
133 | fd_sync: bool,
134 | fd_tell: bool,
135 | fd_write: bool,
136 | fd_advise: bool,
137 | fd_allocate: bool,
138 | path_create_directory: bool,
139 | path_create_file: bool,
140 | path_link_source: bool,
141 | path_link_target: bool,
142 | path_open: bool,
143 | fd_readdir: bool,
144 | path_readlink: bool,
145 | path_rename_source: bool,
146 | path_rename_target: bool,
147 | path_filestat_get: bool,
148 | path_filestat_set_size: bool,
149 | path_filestat_set_times: bool,
150 | fd_filestat_get: bool,
151 | fd_filestat_set_size: bool,
152 | fd_filestat_set_times: bool,
153 | path_symlink: bool,
154 | path_remove_directory: bool,
155 | path_unlink_file: bool,
156 | poll_fd_readwrite: bool,
157 | sock_shutdown: bool,
158 | _pad: u3,
159 | _pad1: u32,
160 | };
161 |
162 | const imports = struct {
163 | pub fn args_get(mem: *Memory, argv: P(P(u8)), argv_buf: P(u8)) !Errno {
164 | const wasi = mem.ext(Wasi);
165 | return strings_get(mem, wasi.argv, argv, argv_buf);
166 | }
167 |
168 | pub fn args_sizes_get(mem: *Memory, argc: P(Size), argv_buf_size: P(Size)) !Errno {
169 | const wasi = mem.ext(Wasi);
170 | return strings_sizes_get(mem, wasi.argv, argc, argv_buf_size);
171 | }
172 |
173 | pub fn environ_get(mem: *Memory, environ: P(P(u8)), environ_buf: P(u8)) !Errno {
174 | const wasi = mem.ext(Wasi);
175 | return strings_get(mem, wasi.environ, environ, environ_buf);
176 | }
177 |
178 | pub fn environ_sizes_get(mem: *Memory, environc: P(Size), environ_buf_size: P(Size)) !Errno {
179 | const wasi = mem.ext(Wasi);
180 | return strings_sizes_get(mem, wasi.environ, environc, environ_buf_size);
181 | }
182 |
183 | pub fn fd_write(mem: *Memory, fd: Fd, iovs: P(Iovec), iovs_len: Size, nread: P(Size)) !Errno {
184 | var os_vec: [128]std.os.iovec_const = undefined;
185 | var i: u32 = 0;
186 | var osi: u32 = 0;
187 | while (i < iovs_len) : (i += 1) {
188 | const iov = try mem.get(try iovs.add(i));
189 | var iter = mem.iterBytes(iov.buf, iov.len);
190 | while (try iter.next()) |bytes| {
191 | os_vec[osi] = .{ .iov_base = bytes.ptr, .iov_len = bytes.len };
192 | osi += 1;
193 | }
194 | }
195 |
196 | const handle = switch (fd) {
197 | .stdin => unreachable,
198 | .stdout => std.io.getStdOut().handle,
199 | .stderr => std.io.getStdErr().handle,
200 | else => unreachable,
201 | };
202 | var written = std.os.writev(handle, os_vec[0..osi]) catch |err| return switch (err) {
203 | error.DiskQuota => Errno.dquot,
204 | error.FileTooBig => Errno.fbig,
205 | error.InputOutput => Errno.io,
206 | error.NoSpaceLeft => Errno.nospc,
207 | error.AccessDenied => Errno.acces,
208 | error.BrokenPipe => Errno.pipe,
209 | error.SystemResources => Errno.nobufs,
210 | error.OperationAborted => unreachable,
211 | error.NotOpenForWriting => Errno.badf,
212 | error.WouldBlock => Errno.again,
213 | error.ConnectionResetByPeer => Errno.connreset,
214 | error.Unexpected => Errno.unexpected,
215 | };
216 | try mem.set(nread, @intCast(u32, written));
217 | return Errno.success;
218 | }
219 |
220 | fn strings_get(mem: *Memory, strings: [][]u8, target: P(P(u8)), target_buf: P(u8)) !Errno {
221 | var target_ = target;
222 | var target_buf_ = target_buf;
223 |
224 | for (strings) |string| {
225 | try mem.set(target_, target_buf_);
226 | target_ = try target_.add(1);
227 |
228 | try mem.setMany(target_buf_, string);
229 | target_buf_ = try target_buf_.add(@intCast(u32, string.len));
230 | try mem.set(target_buf_, 0);
231 | target_buf_ = try target_buf_.add(1);
232 | }
233 | return Errno.success;
234 | }
235 |
236 | fn strings_sizes_get(mem: *Memory, strings: [][]u8, targetc: P(Size), target_buf_size: P(Size)) !Errno {
237 | try mem.set(targetc, @intCast(Size, strings.len));
238 |
239 | var buf_size: usize = 0;
240 | for (strings) |string| {
241 | buf_size += string.len + 1;
242 | }
243 | try mem.set(target_buf_size, @intCast(Size, buf_size));
244 | return Errno.success;
245 | }
246 |
247 | pub fn clock_res_get(mem: *Memory, clock_id: ClockId, resolution: P(Timestamp)) !Errno {
248 | const clk: i32 = switch (clock_id) {
249 | .realtime => std.os.CLOCK_REALTIME,
250 | .monotonic => std.os.CLOCK_MONOTONIC,
251 | .process_cputime_id => std.os.CLOCK_PROCESS_CPUTIME_ID,
252 | .thread_cputime_id => std.os.CLOCK_THREAD_CPUTIME_ID,
253 | else => return Errno.inval,
254 | };
255 |
256 | var result: std.os.timespec = undefined;
257 | std.os.clock_getres(clk, &result) catch |err| switch (err) {
258 | error.UnsupportedClock => return Errno.inval,
259 | error.Unexpected => return Errno.unexpected,
260 | };
261 | try mem.set(resolution, @intCast(Timestamp, std.time.ns_per_s * result.tv_sec + result.tv_nsec));
262 | return Errno.success;
263 | }
264 |
265 | pub fn clock_time_get(mem: *Memory, clock_id: ClockId, precision: Timestamp, time: P(Timestamp)) !Errno {
266 | const clk: i32 = switch (clock_id) {
267 | .realtime => std.os.CLOCK_REALTIME,
268 | .monotonic => std.os.CLOCK_MONOTONIC,
269 | .process_cputime_id => std.os.CLOCK_PROCESS_CPUTIME_ID,
270 | .thread_cputime_id => std.os.CLOCK_THREAD_CPUTIME_ID,
271 | else => return Errno.inval,
272 | };
273 |
274 | var result: std.os.timespec = undefined;
275 | std.os.clock_gettime(clk, &result) catch |err| switch (err) {
276 | error.UnsupportedClock => return Errno.inval,
277 | error.Unexpected => return Errno.unexpected,
278 | };
279 | try mem.set(time, @intCast(Timestamp, std.time.ns_per_s * result.tv_sec + result.tv_nsec));
280 | return Errno.success;
281 | }
282 |
283 | pub fn proc_exit(mem: *Memory, rval: Exitcode) !void {
284 | const wasi = mem.ext(Wasi);
285 | wasi.exit_code = rval;
286 | return error.Unreachable;
287 | }
288 |
289 | pub fn sched_yield(mem: *Memory) Errno {
290 | std.os.sched_yield() catch |err| switch (err) {
291 | error.SystemCannotYield => return Errno.nosys,
292 | };
293 | return Errno.success;
294 | }
295 | };
296 |
297 | test "smoke" {
298 | _ = Instance.ImportManager(struct {
299 | pub const wasi = imports;
300 | });
301 | }
302 |
--------------------------------------------------------------------------------
/src/wat.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const Module = @import("module.zig");
3 | const Op = @import("op.zig");
4 | const util = @import("util.zig");
5 |
6 | const debug_buffer = std.builtin.mode == .Debug;
7 |
8 | fn sexpr(reader: anytype) Sexpr(@TypeOf(reader)) {
9 | return .{
10 | .reader = reader,
11 | ._debug_buffer = if (debug_buffer)
12 | std.fifo.LinearFifo(u8, .{ .Static = 0x100 }).init()
13 | else {},
14 | };
15 | }
16 |
17 | fn Sexpr(comptime Reader: type) type {
18 | return struct {
19 | const Self = @This();
20 |
21 | reader: Reader,
22 | current_stack: isize = 0,
23 | _peek: ?u8 = null,
24 | _debug_buffer: if (debug_buffer)
25 | std.fifo.LinearFifo(u8, .{ .Static = 0x100 })
26 | else
27 | void,
28 |
29 | const Token = enum {
30 | OpenParen,
31 | CloseParen,
32 | Atom,
33 | };
34 |
35 | pub const List = struct {
36 | ctx: *Self,
37 | stack_level: isize,
38 |
39 | const Next = union(enum) { Atom: []const u8, List: List };
40 |
41 | pub fn next(self: List, allocator: *std.mem.Allocator) !?Next {
42 | if (self.isAtEnd()) return null;
43 |
44 | return switch (try self.ctx.scan()) {
45 | .OpenParen => Next{ .List = .{ .ctx = self.ctx, .stack_level = self.ctx.current_stack } },
46 | .CloseParen => null,
47 | .Atom => Next{ .Atom = try self.loadIntoAllocator(allocator) },
48 | };
49 | }
50 |
51 | pub fn obtainAtom(self: List, allocator: *std.mem.Allocator) ![]u8 {
52 | return (try self.nextAtom(allocator)) orelse error.ExpectedAtomGotNull;
53 | }
54 |
55 | pub fn obtainList(self: List) !List {
56 | return (try self.nextList()) orelse error.ExpectedListGotNull;
57 | }
58 |
59 | pub fn nextAtom(self: List, allocator: *std.mem.Allocator) !?[]u8 {
60 | if (self.isAtEnd()) return null;
61 |
62 | return switch (try self.ctx.scan()) {
63 | .OpenParen => error.ExpectedAtomGotList,
64 | .CloseParen => null,
65 | .Atom => try self.loadIntoAllocator(allocator),
66 | };
67 | }
68 |
69 | pub fn nextList(self: List) !?List {
70 | if (self.isAtEnd()) return null;
71 |
72 | return switch (try self.ctx.scan()) {
73 | .OpenParen => List{ .ctx = self.ctx, .stack_level = self.ctx.current_stack },
74 | .CloseParen => null,
75 | .Atom => error.ExpectedListGotAtom,
76 | };
77 | }
78 |
79 | pub fn expectEnd(self: List) !void {
80 | if (self.isAtEnd()) return;
81 |
82 | switch (try self.ctx.scan()) {
83 | .CloseParen => {},
84 | else => return error.ExpectedEndOfList,
85 | }
86 | }
87 |
88 | fn isAtEnd(self: List) bool {
89 | switch (self.stack_level - self.ctx.current_stack) {
90 | 0 => return false,
91 | 1 => {
92 | if (debug_buffer and self.ctx._debug_buffer.peekItem(self.ctx._debug_buffer.count - 1) != ')') {
93 | self.ctx.debugDump(std.io.getStdOut().writer()) catch {};
94 | unreachable;
95 | }
96 | return true;
97 | },
98 | else => {
99 | if (debug_buffer) {
100 | self.ctx.debugDump(std.io.getStdOut().writer()) catch {};
101 | std.debug.print("Unexpected list depth -- Current {} != List {}\n", .{ self.ctx.current_stack, self.stack_level });
102 | }
103 | unreachable;
104 | },
105 | }
106 | }
107 |
108 | fn loadIntoAllocator(self: List, allocator: *std.mem.Allocator) ![]u8 {
109 | var list = std.ArrayList(u8).init(allocator);
110 | const writer = list.writer();
111 |
112 | const first = try self.ctx.readByte();
113 | try writer.writeByte(first);
114 | const is_string = first == '"';
115 |
116 | while (true) {
117 | const byte = try self.ctx.readByte();
118 | if (is_string) {
119 | try writer.writeByte(byte);
120 |
121 | // TODO: handle escape sequences?
122 | if (byte == '"') {
123 | return list.toOwnedSlice();
124 | }
125 | } else {
126 | switch (byte) {
127 | 0, ' ', '\t', '\n', '(', ')' => {
128 | self.ctx.putBack(byte);
129 | return list.toOwnedSlice();
130 | },
131 | else => try writer.writeByte(byte),
132 | }
133 | }
134 | }
135 | }
136 | };
137 |
138 | pub fn root(self: *Self) !List {
139 | const token = try self.scan();
140 | std.debug.assert(token == .OpenParen);
141 | return List{ .ctx = self, .stack_level = self.current_stack };
142 | }
143 |
144 | pub fn debugDump(self: Self, writer: anytype) !void {
145 | var tmp = self._debug_buffer;
146 | const reader = tmp.reader();
147 |
148 | var buf: [0x100]u8 = undefined;
149 | const size = try reader.read(&buf);
150 | try writer.writeAll(buf[0..size]);
151 | try writer.writeByte('\n');
152 | }
153 |
154 | fn skipPast(self: *Self, seq: []const u8) !void {
155 | std.debug.assert(seq.len > 0);
156 |
157 | var matched: usize = 0;
158 | while (true) {
159 | const byte = try self.readByte();
160 | if (byte == seq[matched]) {
161 | matched += 1;
162 | if (matched >= seq.len) {
163 | return;
164 | }
165 | } else {
166 | matched = 0;
167 | }
168 | }
169 | }
170 |
171 | pub fn expectEos(self: *Self) !void {
172 | const value = self.scan() catch |err| switch (err) {
173 | error.EndOfStream => return,
174 | else => return err,
175 | };
176 |
177 | return error.ExpectEos;
178 | }
179 |
180 | fn readByte(self: *Self) !u8 {
181 | if (self._peek) |p| {
182 | self._peek = null;
183 | return p;
184 | } else {
185 | const byte = try self.reader.readByte();
186 | if (debug_buffer) {
187 | if (self._debug_buffer.writableLength() == 0) {
188 | self._debug_buffer.discard(1);
189 | std.debug.assert(self._debug_buffer.writableLength() == 1);
190 | }
191 | self._debug_buffer.writeAssumeCapacity(&[_]u8{byte});
192 | }
193 | return byte;
194 | }
195 | }
196 |
197 | fn peek(self: *Self) !u8 {
198 | return self._peek orelse {
199 | const byte = try self.readByte();
200 | self._peek = byte;
201 | return byte;
202 | };
203 | }
204 |
205 | fn putBack(self: *Self, byte: u8) void {
206 | std.debug.assert(self._peek == null);
207 | self._peek = byte;
208 | }
209 |
210 | fn scan(self: *Self) !Token {
211 | while (true) {
212 | const byte = try self.readByte();
213 | switch (byte) {
214 | 0, ' ', '\t', '\n' => {},
215 | '(' => {
216 | const next = self.peek() catch |err| switch (err) {
217 | error.EndOfStream => 0,
218 | else => return err,
219 | };
220 |
221 | if (next == ';') {
222 | // Comment block -- skip to next segment
223 | try self.skipPast(";)");
224 | } else {
225 | self.current_stack += 1;
226 | return Token.OpenParen;
227 | }
228 | },
229 | ')' => {
230 | if (self.current_stack <= 0) {
231 | return error.UnmatchedParens;
232 | }
233 | self.current_stack -= 1;
234 | return Token.CloseParen;
235 | },
236 | ';' => try self.skipPast("\n"),
237 | else => {
238 | self.putBack(byte);
239 | if (self.current_stack <= 0) {
240 | return error.TrailingLiteral;
241 | }
242 | return Token.Atom;
243 | },
244 | }
245 | }
246 | }
247 | };
248 | }
249 |
250 | test "sexpr" {
251 | var buf: [256]u8 align(32) = undefined;
252 | var ring = util.RingAllocator.init(&buf, 32);
253 | {
254 | var fbs = std.io.fixedBufferStream("(a bc 42)");
255 | var s = sexpr(fbs.reader());
256 |
257 | const root = try s.root();
258 |
259 | try std.testing.expectEqualSlices(u8, "a", try root.obtainAtom(&ring.allocator));
260 | try std.testing.expectEqualSlices(u8, "bc", try root.obtainAtom(&ring.allocator));
261 | try std.testing.expectEqualSlices(u8, "42", try root.obtainAtom(&ring.allocator));
262 | try root.expectEnd();
263 | try root.expectEnd();
264 | try root.expectEnd();
265 | }
266 | {
267 | var fbs = std.io.fixedBufferStream("(() ())");
268 | var s = sexpr(fbs.reader());
269 |
270 | const root = try s.root();
271 |
272 | const first = try root.obtainList();
273 | try std.testing.expectEqual(@as(?@TypeOf(root), null), try first.nextList());
274 |
275 | const second = try root.obtainList();
276 | try std.testing.expectEqual(@as(?@TypeOf(root), null), try second.nextList());
277 | }
278 | {
279 | var fbs = std.io.fixedBufferStream("( ( ( ())))");
280 | var s = sexpr(fbs.reader());
281 |
282 | const root = try s.root();
283 |
284 | const first = try root.obtainList();
285 | const second = try first.obtainList();
286 | const third = try second.obtainList();
287 | try third.expectEnd();
288 | try second.expectEnd();
289 | try first.expectEnd();
290 | }
291 | {
292 | var fbs = std.io.fixedBufferStream("(block (; ; ; ;) ;; label = @1\n local.get 4)");
293 | var s = sexpr(fbs.reader());
294 |
295 | const root = try s.root();
296 |
297 | try std.testing.expectEqualSlices(u8, "block", try root.obtainAtom(&ring.allocator));
298 | try std.testing.expectEqualSlices(u8, "local.get", try root.obtainAtom(&ring.allocator));
299 | try std.testing.expectEqualSlices(u8, "4", try root.obtainAtom(&ring.allocator));
300 | try root.expectEnd();
301 | }
302 | }
303 |
304 | pub fn parse(allocator: *std.mem.Allocator, reader: anytype) !Module {
305 | var result = try parseNoValidate(allocator, reader);
306 | errdefer result.deinit();
307 |
308 | try result.postProcess();
309 | return result;
310 | }
311 |
312 | pub fn parseNoValidate(allocator: *std.mem.Allocator, reader: anytype) !Module {
313 | var ctx = sexpr(reader);
314 | const root = try ctx.root();
315 |
316 | errdefer if (debug_buffer) ctx.debugDump(std.io.getStdOut().writer()) catch {};
317 |
318 | var ring_buf: [256]u8 align(32) = undefined;
319 | var ring = util.RingAllocator.init(&ring_buf, 32);
320 |
321 | if (!std.mem.eql(u8, try root.obtainAtom(&ring.allocator), "module")) {
322 | return error.ExpectModule;
323 | }
324 |
325 | var arena = std.heap.ArenaAllocator.init(allocator);
326 | errdefer arena.deinit();
327 |
328 | var customs = std.ArrayList(Module.Section(.custom)).init(&arena.allocator);
329 | var types = std.ArrayList(Module.Section(.type)).init(&arena.allocator);
330 | var imports = std.ArrayList(Module.Section(.import)).init(&arena.allocator);
331 | var functions = std.ArrayList(Module.Section(.function)).init(&arena.allocator);
332 | var tables = std.ArrayList(Module.Section(.table)).init(&arena.allocator);
333 | var memories = std.ArrayList(Module.Section(.memory)).init(&arena.allocator);
334 | var globals = std.ArrayList(Module.Section(.global)).init(&arena.allocator);
335 | var exports = std.ArrayList(Module.Section(.@"export")).init(&arena.allocator);
336 | var start: ?Module.Section(.start) = null;
337 | var elements = std.ArrayList(Module.Section(.element)).init(&arena.allocator);
338 | var codes = std.ArrayList(Module.Section(.code)).init(&arena.allocator);
339 | var data = std.ArrayList(Module.Section(.data)).init(&arena.allocator);
340 |
341 | while (try root.nextList()) |command| {
342 | const swhash = util.Swhash(8);
343 |
344 | switch (swhash.match(try command.obtainAtom(&ring.allocator))) {
345 | swhash.case("memory") => {
346 | try memories.append(.{
347 | .limits = .{
348 | .initial = try std.fmt.parseInt(u32, try command.obtainAtom(&ring.allocator), 10),
349 | .maximum = if (try command.nextAtom(&ring.allocator)) |value|
350 | try std.fmt.parseInt(u32, value, 10)
351 | else
352 | null,
353 | },
354 | });
355 | // TODO: this is broken
356 | // try command.expectEnd();
357 | },
358 | swhash.case("type") => {
359 | while (try command.nextList()) |args| {
360 | switch (swhash.match(try args.obtainAtom(&ring.allocator))) {
361 | swhash.case("func") => {
362 | var params = std.ArrayList(Module.Type.Value).init(&arena.allocator);
363 | var result: ?Module.Type.Value = null;
364 |
365 | while (try args.nextList()) |pair| {
366 | const loc = try pair.obtainAtom(&ring.allocator);
367 |
368 | const typ: Module.Type.Value = switch (swhash.match(try pair.obtainAtom(&ring.allocator))) {
369 | swhash.case("i32") => .I32,
370 | swhash.case("i64") => .I64,
371 | swhash.case("f32") => .F32,
372 | swhash.case("f64") => .F64,
373 | else => return error.ExpectedType,
374 | };
375 |
376 | switch (swhash.match(loc)) {
377 | swhash.case("param") => try params.append(typ),
378 | swhash.case("result") => result = typ,
379 | else => return error.ExpectedLoc,
380 | }
381 |
382 | try pair.expectEnd();
383 | }
384 |
385 | try types.append(.{
386 | .form = .Func,
387 | .param_types = params.items,
388 | .return_type = result,
389 | });
390 | },
391 | else => return error.TypeNotRecognized,
392 | }
393 | }
394 | },
395 | swhash.case("import") => {
396 | const module = try command.obtainAtom(&arena.allocator);
397 | if (module[0] != '"') {
398 | return error.ExpectString;
399 | }
400 |
401 | const field = try command.obtainAtom(&arena.allocator);
402 | if (field[0] != '"') {
403 | return error.ExpectString;
404 | }
405 |
406 | var result = Module.Section(.import){
407 | .module = module[1 .. module.len - 1],
408 | .field = field[1 .. field.len - 1],
409 | .kind = undefined,
410 | };
411 |
412 | const kind_list = try command.obtainList();
413 | switch (swhash.match(try kind_list.obtainAtom(&ring.allocator))) {
414 | swhash.case("func") => {
415 | const type_pair = try kind_list.obtainList();
416 |
417 | if (!std.mem.eql(u8, "type", try type_pair.obtainAtom(&ring.allocator))) {
418 | @panic("TODO inline function prototypes");
419 | }
420 |
421 | const index = try type_pair.obtainAtom(&ring.allocator);
422 | try type_pair.expectEnd();
423 | result.kind = .{
424 | .Function = @intToEnum(Module.Index.FuncType, try std.fmt.parseInt(u32, index, 10)),
425 | };
426 | },
427 | swhash.case("table") => @panic("TODO"),
428 | swhash.case("memory") => @panic("TODO"),
429 | swhash.case("global") => @panic("TODO"),
430 | else => return error.ImportNotSupported,
431 | }
432 |
433 | try kind_list.expectEnd();
434 | try command.expectEnd();
435 |
436 | try imports.append(result);
437 | },
438 | swhash.case("func") => {
439 | var params = std.ArrayList(Module.Type.Value).init(&arena.allocator);
440 | var locals = std.ArrayList(Module.Type.Value).init(&arena.allocator);
441 | var result: ?Module.Type.Value = null;
442 |
443 | while (command.obtainList()) |pair| {
444 | const loc = try pair.obtainAtom(&ring.allocator);
445 |
446 | const typ: Module.Type.Value = switch (swhash.match(try pair.obtainAtom(&ring.allocator))) {
447 | swhash.case("i32") => .I32,
448 | swhash.case("i64") => .I64,
449 | swhash.case("f32") => .F32,
450 | swhash.case("f64") => .F64,
451 | else => return error.ExpectedType,
452 | };
453 |
454 | switch (swhash.match(loc)) {
455 | swhash.case("param") => try params.append(typ),
456 | swhash.case("local") => try locals.append(typ),
457 | swhash.case("result") => result = typ,
458 | else => return error.ExpectedLoc,
459 | }
460 |
461 | try pair.expectEnd();
462 | } else |err| switch (err) {
463 | error.ExpectedListGotNull, error.ExpectedListGotAtom => {},
464 | else => return err,
465 | }
466 |
467 | var body = std.ArrayList(Module.Instr).init(&arena.allocator);
468 | while (try command.nextAtom(&ring.allocator)) |op_string| {
469 | for (op_string) |*letter| {
470 | if (letter.* == '.') {
471 | letter.* = '_';
472 | }
473 | }
474 | const op = std.meta.stringToEnum(std.wasm.Opcode, op_string) orelse return error.OpNotFound;
475 | const op_meta = Op.Meta.of(op);
476 |
477 | try body.append(.{
478 | .op = op,
479 | .pop_len = @intCast(u8, op_meta.pop.len),
480 | .arg = switch (op_meta.arg_kind) {
481 | .Void => undefined,
482 | .Type => blk: {
483 | const pair = command.obtainList() catch |err| switch (err) {
484 | error.ExpectedListGotAtom => break :blk Op.Arg{ .Type = .Void },
485 | else => |e| return e,
486 | };
487 | if (!std.mem.eql(u8, try pair.obtainAtom(&ring.allocator), "result")) {
488 | return error.ExpectedResult;
489 | }
490 |
491 | const typ: Op.Arg.Type = switch (swhash.match(try pair.obtainAtom(&ring.allocator))) {
492 | swhash.case("void") => .Void,
493 | swhash.case("i32") => .I32,
494 | swhash.case("i64") => .I64,
495 | swhash.case("f32") => .F32,
496 | swhash.case("f64") => .F64,
497 | else => return error.ExpectedType,
498 | };
499 |
500 | try pair.expectEnd();
501 | break :blk Op.Arg{ .Type = typ };
502 | },
503 | .U32z, .Mem, .Array => @panic("TODO"),
504 | else => blk: {
505 | const arg = try command.obtainAtom(&ring.allocator);
506 | break :blk @as(Op.Arg, switch (op_meta.arg_kind) {
507 | .Void, .Type, .U32z, .Mem, .Array => unreachable,
508 | .I32 => .{ .I32 = try std.fmt.parseInt(i32, arg, 10) },
509 | .U32 => .{ .U32 = try std.fmt.parseInt(u32, arg, 10) },
510 | .I64 => .{ .I64 = try std.fmt.parseInt(i64, arg, 10) },
511 | .U64 => .{ .U64 = try std.fmt.parseInt(u64, arg, 10) },
512 | .F32 => .{ .F32 = try std.fmt.parseFloat(f32, arg) },
513 | .F64 => .{ .F64 = try std.fmt.parseFloat(f64, arg) },
514 | });
515 | },
516 | },
517 | });
518 | }
519 |
520 | try functions.append(.{
521 | .type_idx = @intToEnum(Module.Index.FuncType, @intCast(u32, types.items.len)),
522 | });
523 |
524 | try codes.append(.{
525 | .locals = locals.items,
526 | .body = body.items,
527 | });
528 |
529 | try types.append(.{
530 | .form = .Func,
531 | .param_types = params.items,
532 | .return_type = result,
533 | });
534 | },
535 | swhash.case("global") => {
536 | // 'skip' the id
537 | const id = (try command.next(&ring.allocator)) orelse return error.ExpectedNext;
538 |
539 | const next = blk: {
540 | // a comment was skipped so 'id' is the actual Atom/List we want
541 | if (id != .Atom or id.Atom[0] != '$') break :blk id;
542 |
543 | // if it was an id get next list/atom
544 | break :blk (try command.next(&ring.allocator)) orelse return error.ExpectedNext;
545 | };
546 |
547 | const mutable = blk: {
548 | if (next == .Atom) break :blk false;
549 |
550 | const mut = try next.List.obtainAtom(&ring.allocator);
551 | break :blk std.mem.eql(u8, mut, "mut");
552 | };
553 |
554 | const valtype = blk: {
555 | const type_atom = switch (next) {
556 | .List => |list| list_blk: {
557 | const res = try list.obtainAtom(&ring.allocator);
558 | try list.expectEnd();
559 | break :list_blk res;
560 | },
561 | .Atom => |atom| atom,
562 | };
563 | break :blk @as(Module.Type.Value, switch (swhash.match(type_atom)) {
564 | swhash.case("i32") => .I32,
565 | swhash.case("i64") => .I64,
566 | swhash.case("f32") => .F32,
567 | swhash.case("f64") => .F64,
568 | else => return error.ExpectedType,
569 | });
570 | };
571 |
572 | const init_pair = try command.obtainList();
573 | var op_string = try init_pair.obtainAtom(&ring.allocator);
574 | for (op_string) |*letter| {
575 | if (letter.* == '.') {
576 | letter.* = '_';
577 | }
578 | }
579 |
580 | const op = std.meta.stringToEnum(std.wasm.Opcode, op_string) orelse return error.OpNotFound;
581 |
582 | const value = try init_pair.obtainAtom(&ring.allocator);
583 |
584 | const result: Module.InitExpr = switch (op) {
585 | .i32_const => .{ .i32_const = try std.fmt.parseInt(i32, value, 10) },
586 | .i64_const => .{ .i64_const = try std.fmt.parseInt(i64, value, 10) },
587 | .f32_const => .{ .f32_const = try std.fmt.parseFloat(f32, value) },
588 | .f64_const => .{ .f64_const = try std.fmt.parseFloat(f64, value) },
589 | else => return error.UnsupportedInitExpr,
590 | };
591 |
592 | try init_pair.expectEnd();
593 |
594 | try globals.append(.{
595 | .@"type" = .{
596 | .content_type = valtype,
597 | .mutability = mutable,
598 | },
599 | .init = result,
600 | });
601 | },
602 | swhash.case("export") => {
603 | const export_name = try command.obtainAtom(&arena.allocator);
604 | if (export_name[0] != '"') {
605 | return error.ExpectString;
606 | }
607 | std.debug.assert(export_name[export_name.len - 1] == '"');
608 |
609 | const pair = try command.obtainList();
610 |
611 | const kind = try pair.obtainAtom(&ring.allocator);
612 | const index = try pair.obtainAtom(&ring.allocator);
613 |
614 | try exports.append(.{
615 | .field = export_name[1 .. export_name.len - 1],
616 | .kind = switch (swhash.match(kind)) {
617 | swhash.case("func") => .Function,
618 | swhash.case("table") => .Table,
619 | swhash.case("memory") => .Memory,
620 | swhash.case("global") => .Global,
621 | else => return error.ExpectExternalKind,
622 | },
623 | .index = try std.fmt.parseInt(u32, index, 10),
624 | });
625 |
626 | try pair.expectEnd();
627 | },
628 | else => return error.CommandNotRecognized,
629 | }
630 | try command.expectEnd();
631 | }
632 |
633 | try root.expectEnd();
634 | try ctx.expectEos();
635 |
636 | return Module{
637 | .custom = customs.items,
638 | .@"type" = types.items,
639 | .import = imports.items,
640 | .function = functions.items,
641 | .table = tables.items,
642 | .memory = memories.items,
643 | .global = globals.items,
644 | .@"export" = exports.items,
645 | .start = start,
646 | .element = elements.items,
647 | .code = codes.items,
648 | .data = data.items,
649 |
650 | .arena = arena,
651 | };
652 | }
653 |
654 | test "parseNoValidate" {
655 | {
656 | var fbs = std.io.fixedBufferStream("(module)");
657 | var module = try parseNoValidate(std.testing.allocator, fbs.reader());
658 | defer module.deinit();
659 |
660 | try std.testing.expectEqual(@as(usize, 0), module.memory.len);
661 | try std.testing.expectEqual(@as(usize, 0), module.function.len);
662 | try std.testing.expectEqual(@as(usize, 0), module.@"export".len);
663 | }
664 | {
665 | var fbs = std.io.fixedBufferStream("(module (memory 42))");
666 | var module = try parseNoValidate(std.testing.allocator, fbs.reader());
667 | defer module.deinit();
668 |
669 | try std.testing.expectEqual(@as(usize, 1), module.memory.len);
670 | try std.testing.expectEqual(@as(u32, 42), module.memory[0].limits.initial);
671 | }
672 | {
673 | var fbs = std.io.fixedBufferStream(
674 | \\(module
675 | \\ (func (param i64) (param f32) (result i64) (local f64)
676 | \\ local.get 0
677 | \\ drop
678 | \\ local.get 0))
679 | );
680 | var module = try parseNoValidate(std.testing.allocator, fbs.reader());
681 | defer module.deinit();
682 |
683 | try std.testing.expectEqual(@as(usize, 1), module.function.len);
684 |
685 | const func_type = module.@"type"[0];
686 | try std.testing.expectEqual(@as(usize, 2), func_type.param_types.len);
687 | try std.testing.expectEqual(Module.Type.Value.I64, func_type.param_types[0]);
688 | try std.testing.expectEqual(Module.Type.Value.F32, func_type.param_types[1]);
689 | try std.testing.expectEqual(Module.Type.Value.I64, func_type.return_type.?);
690 |
691 | const code = module.code[0];
692 |
693 | try std.testing.expectEqual(@as(usize, 1), code.locals.len);
694 | try std.testing.expectEqual(Module.Type.Value.F64, code.locals[0]);
695 |
696 | try std.testing.expectEqual(@as(usize, 3), code.body.len);
697 | }
698 | {
699 | var fbs = std.io.fixedBufferStream(
700 | \\(module
701 | \\ (func (param i32) (result i32) local.get 0)
702 | \\ (export "foo" (func 0)))
703 | );
704 | var module = try parseNoValidate(std.testing.allocator, fbs.reader());
705 | defer module.deinit();
706 |
707 | try std.testing.expectEqual(@as(usize, 1), module.function.len);
708 |
709 | try std.testing.expectEqual(@as(usize, 1), module.@"export".len);
710 | try std.testing.expectEqualSlices(u8, "foo", module.@"export"[0].field);
711 | try std.testing.expectEqual(Module.ExternalKind.Function, module.@"export"[0].kind);
712 | try std.testing.expectEqual(@as(u32, 0), module.@"export"[0].index);
713 | }
714 | {
715 | var fbs = std.io.fixedBufferStream(
716 | \\(module
717 | \\ (type (;0;) (func (param i32) (result i32)))
718 | \\ (import "env" "fibonacci" (func (type 0))))
719 | );
720 | var module = try parseNoValidate(std.testing.allocator, fbs.reader());
721 | defer module.deinit();
722 |
723 | try std.testing.expectEqual(@as(usize, 1), module.@"type".len);
724 | try std.testing.expectEqual(Module.Type.Form.Func, module.@"type"[0].form);
725 | try std.testing.expectEqual(@as(usize, 1), module.@"type"[0].param_types.len);
726 | try std.testing.expectEqual(Module.Type.Value.I32, module.@"type"[0].param_types[0]);
727 | try std.testing.expectEqual(Module.Type.Value.I32, module.@"type"[0].return_type.?);
728 |
729 | try std.testing.expectEqual(@as(usize, 1), module.import.len);
730 | try std.testing.expectEqualSlices(u8, "env", module.import[0].module);
731 | try std.testing.expectEqualSlices(u8, "fibonacci", module.import[0].field);
732 | try std.testing.expectEqual(Module.ExternalKind.Function, module.import[0].kind);
733 | try std.testing.expectEqual(@intToEnum(Module.Index.FuncType, 0), module.import[0].kind.Function);
734 |
735 | try std.testing.expectEqual(@as(usize, 0), module.function.len);
736 | }
737 | {
738 | var fbs = std.io.fixedBufferStream(
739 | \\(module
740 | \\ (global $x (mut i32) (i32.const -12))
741 | \\ (global $x i64 (i64.const 12))
742 | \\ (global (;1;) i32 (i32.const 10)))
743 | );
744 | var module = try parseNoValidate(std.testing.allocator, fbs.reader());
745 | defer module.deinit();
746 |
747 | try std.testing.expectEqual(@as(usize, 3), module.global.len);
748 | try std.testing.expectEqual(Module.Type.Value.I32, module.global[0].@"type".content_type);
749 | try std.testing.expectEqual(true, module.global[0].@"type".mutability);
750 | try std.testing.expectEqual(@as(i32, -12), module.global[0].init.i32_const);
751 |
752 | try std.testing.expectEqual(Module.Type.Value.I64, module.global[1].@"type".content_type);
753 | try std.testing.expectEqual(false, module.global[1].@"type".mutability);
754 | try std.testing.expectEqual(@as(i64, 12), module.global[1].init.i64_const);
755 |
756 | try std.testing.expectEqual(Module.Type.Value.I32, module.global[2].@"type".content_type);
757 | try std.testing.expectEqual(false, module.global[2].@"type".mutability);
758 | try std.testing.expectEqual(@as(i32, 10), module.global[2].init.i32_const);
759 | }
760 | }
761 |
762 | test "parse blocks" {
763 | var fbs = std.io.fixedBufferStream(
764 | \\(module
765 | \\ (func (result i32)
766 | \\ block (result i32)
767 | \\ loop
768 | \\ br 0
769 | \\ br 1
770 | \\ end
771 | \\ end))
772 | );
773 | var module = try parseNoValidate(std.testing.allocator, fbs.reader());
774 | defer module.deinit();
775 |
776 | const body = module.code[0].body;
777 | try std.testing.expectEqual(@as(usize, 6), body.len);
778 |
779 | try std.testing.expectEqual(std.wasm.Opcode.block, body[0].op);
780 | try std.testing.expectEqual(Op.Arg.Type.I32, body[0].arg.Type);
781 |
782 | try std.testing.expectEqual(std.wasm.Opcode.loop, body[1].op);
783 | try std.testing.expectEqual(Op.Arg.Type.Void, body[1].arg.Type);
784 |
785 | try std.testing.expectEqual(std.wasm.Opcode.br, body[2].op);
786 | try std.testing.expectEqual(@as(u32, 0), body[2].arg.U32);
787 |
788 | try std.testing.expectEqual(std.wasm.Opcode.br, body[3].op);
789 | try std.testing.expectEqual(@as(u32, 1), body[3].arg.U32);
790 |
791 | try std.testing.expectEqual(std.wasm.Opcode.end, body[4].op);
792 |
793 | try std.testing.expectEqual(std.wasm.Opcode.end, body[5].op);
794 | }
795 |
--------------------------------------------------------------------------------