beginning of haku2: a reimplementation of haku in Zig

the goal is to rewrite haku completely, starting with the VM---because it was the most obvious point of improvement
the reason is because Rust is kinda too verbose for low level stuff like this. compare the line numbers between haku1 and haku2's VM and how verbose those lines are, and it's kind of an insane difference
it also feels like Zig's compilation model can work better for small wasm binary sizes

and of course, I also just wanted an excuse to try out Zig :3
This commit is contained in:
りき萌 2025-06-01 23:13:34 +02:00
parent 598c0348f6
commit 01d4514a65
19 changed files with 1946 additions and 11 deletions

View file

@ -0,0 +1,79 @@
const std = @import("std");
const mem = std.mem;
/// NOTE: This must be mirrored in bytecode.rs.
pub const Opcode = enum(u8) {
// Push literal values onto the stack.
nil,
false,
true,
tag,
number, // (float: f32)
rgba, // (r: u8, g: u8, b: u8, a: u8)
// Duplicate existing values.
local, // (index: u8) push a value relative to the bottom of the current stack window
set_local, // (index: u8) set the value of a value relative to the bottom of the current stack window
capture, // (index: u8) push a value captured by the current closure
def, // (index: u16) get the value of a definition
set_def, // (index: u16) set the value of a definition
// Create lists.
list, // (len: u16)
// Create literal functions.
function, // (params: u8, then: u16), at `then`: (local_count: u8, capture_count: u8, captures: [capture_count](source: u8, index: u8))
// Control flow.
jump, // (offset: u16)
jump_if_not, // (offset: u16)
field, // (count: u8, tags: [count]u16) look up a closure capture named by a tag. this is used to implement records with fields
// Function calls.
call, // (argc: u8)
system, // (index: u8, argc: u8) fast path for system calls
ret, // must be the last opcode; opcodes after .ret are treated as invalid
_, // invalid opcodes
};
// Constants used by the .function opcode to indicate capture sources.
pub const capture_local: u8 = 0;
pub const capture_capture: u8 = 1;
pub const Chunk = struct {
bytecode: []const u8,
};
pub const Loc = u16;
pub const Defs = struct {
num_defs: usize,
num_tags: usize,
// Unlike the Rust version, we currently do not store strings here, because we still don't
// support stack traces.
// The VM therefore never needs the names of Defs for any practical purposes.
pub fn parse(
a: mem.Allocator,
// These strings are expected to contain a list of names, where each name is terminated
// by a newline (LF).
defs_string: []const u8,
tags_string: []const u8,
) !*Defs {
const d = try a.create(Defs);
errdefer a.destroy(d);
d.* = .{
.num_defs = mem.count(u8, defs_string, "\n"),
.num_tags = mem.count(u8, tags_string, "\n"),
};
return d;
}
pub fn destroy(defs: *Defs, a: mem.Allocator) void {
a.destroy(defs);
}
};