haku2: make server use haku2 (and make it work!)

This commit is contained in:
りき萌 2025-06-13 20:11:52 +02:00
parent c5e2892def
commit 48d03699bd
16 changed files with 329 additions and 174 deletions

View file

@ -10,7 +10,7 @@ const value = @import("value.zig");
const Vm = @import("vm.zig");
const log = @import("log.zig");
const vm_trace = true;
const debug_logs = false;
pub const allocator =
if (builtin.cpu.arch == .wasm32)
@ -19,7 +19,7 @@ pub const allocator =
@import("allocator.zig").hostAllocator;
pub const std_options: std.Options = .{
.log_level = if (vm_trace) .debug else .info,
.log_level = if (debug_logs) .debug else .info,
.logFn = log.logFn,
};
@ -57,10 +57,6 @@ export fn haku2_limits_set_call_stack_capacity(limits: *Vm.Limits, new: usize) v
limits.call_stack_capacity = new;
}
export fn haku2_limits_set_fuel(limits: *Vm.Limits, new: u32) void {
limits.fuel = new;
}
// Defs
export fn haku2_defs_parse(
@ -95,6 +91,10 @@ export fn haku2_vm_destroy(vm: *Vm) void {
allocator.destroy(vm);
}
export fn haku2_vm_reset(vm: *Vm, fuel: u32) void {
vm.reset(fuel);
}
export fn haku2_vm_run_main(
vm: *Vm,
scratch: *Scratch,
@ -102,11 +102,15 @@ export fn haku2_vm_run_main(
code_len: usize,
local_count: u8,
) bool {
const chunk = bytecode.Chunk{
const chunk = scratch.allocator().create(bytecode.Chunk) catch {
vm.outOfMemory() catch {};
return false;
};
chunk.* = bytecode.Chunk{
.bytecode = code[0..code_len],
};
const closure = value.Closure{
.chunk = &chunk,
.chunk = chunk,
.start = 0,
.param_count = 0,
.local_count = local_count,

View file

@ -7,6 +7,8 @@ use std::{
slice,
};
use log::{debug, trace};
#[unsafe(no_mangle)]
unsafe extern "C" fn __haku2_alloc(size: usize, align: usize) -> *mut u8 {
if let Ok(layout) = Layout::from_size_align(size, align) {
@ -121,7 +123,6 @@ extern "C" {
fn haku2_limits_destroy(limits: *mut LimitsC);
fn haku2_limits_set_stack_capacity(limits: *mut LimitsC, new: usize);
fn haku2_limits_set_call_stack_capacity(limits: *mut LimitsC, new: usize);
fn haku2_limits_set_fuel(limits: *mut LimitsC, new: u32);
fn haku2_defs_parse(
defs_string: *const u8,
@ -133,6 +134,7 @@ extern "C" {
fn haku2_vm_new(s: *mut ScratchC, defs: *const DefsC, limits: *const LimitsC) -> *mut VmC;
fn haku2_vm_destroy(vm: *mut VmC);
fn haku2_vm_reset(vm: *mut VmC, fuel: u32);
fn haku2_vm_run_main(
vm: *mut VmC,
scratch: *mut ScratchC,
@ -167,13 +169,14 @@ pub struct Scratch {
impl Scratch {
pub fn new(max: usize) -> Scratch {
Scratch {
// SAFETY: haku2_scratch_new does not have any safety invariants.
raw: NonNull::new(unsafe { haku2_scratch_new(max) }).expect("out of memory"),
}
// SAFETY: haku2_scratch_new does not have any safety invariants.
let raw = NonNull::new(unsafe { haku2_scratch_new(max) }).expect("out of memory");
trace!("Scratch::new -> {raw:?}");
Scratch { raw }
}
pub fn reset(&mut self) {
trace!("Scratch::reset({:?})", self.raw);
// SAFETY: The pointer passed is non-null.
unsafe {
haku2_scratch_reset(self.raw.as_ptr());
@ -183,6 +186,7 @@ impl Scratch {
impl Drop for Scratch {
fn drop(&mut self) {
trace!("Scratch::drop({:?})", self.raw);
// SAFETY: The pointer passed is non-null.
unsafe {
haku2_scratch_destroy(self.raw.as_ptr());
@ -194,7 +198,6 @@ impl Drop for Scratch {
pub struct LimitsSpec {
pub stack_capacity: usize,
pub call_stack_capacity: usize,
pub fuel: u32,
}
#[derive(Debug)]
@ -216,7 +219,6 @@ impl Limits {
unsafe {
haku2_limits_set_stack_capacity(limits.as_ptr(), spec.stack_capacity);
haku2_limits_set_call_stack_capacity(limits.as_ptr(), spec.call_stack_capacity);
haku2_limits_set_fuel(limits.as_ptr(), spec.fuel);
}
Self { raw: limits }
@ -315,21 +317,21 @@ pub struct Dotter {
impl Vm {
pub fn new(scratch: Scratch, code: Code, limits: &Limits) -> Self {
let raw = NonNull::new(unsafe {
haku2_vm_new(
scratch.raw.as_ptr(),
code.defs.raw.as_ptr(),
limits.raw.as_ptr(),
)
})
.expect("out of memory");
trace!("Vm::new({scratch:?}, {code:?}, {limits:?}) -> {raw:?}");
Self {
// SAFETY:
// - Ownership of scratch is passed to the VM, so the VM cannot outlive the scratch space.
// - The VM never gives you any references back, so this is safe to do.
// - The other arguments are only borrowed immutably for construction.
inner: VmInner {
raw: NonNull::new(unsafe {
haku2_vm_new(
scratch.raw.as_ptr(),
code.defs.raw.as_ptr(),
limits.raw.as_ptr(),
)
})
.expect("out of memory"),
},
inner: VmInner { raw },
scratch,
code,
}
@ -340,8 +342,10 @@ impl Vm {
///
/// Calling `begin` again during this process will work correctly, and result in another
/// continuation being stack on top of the old one---at the expense of a stack slot.
pub fn begin(&mut self) -> Result<(), Exception> {
pub fn begin(&mut self, fuel: u32) -> Result<(), Exception> {
trace!("Vm::begin({self:?}, {fuel})");
let ok = unsafe {
haku2_vm_reset(self.inner.raw.as_ptr(), fuel);
haku2_vm_run_main(
self.inner.raw.as_ptr(),
self.scratch.raw.as_ptr(),
@ -411,6 +415,7 @@ impl Vm {
/// Take the `Scratch` out of the VM for reuse in another one.
/// The scratch memory will be reset (no bytes will be consumed.)
pub fn into_scratch(self) -> Scratch {
trace!("Vm::into_scratch({self:?})");
let Vm {
mut scratch,
code: _,
@ -423,6 +428,8 @@ impl Vm {
impl ContDotter<'_> {
pub fn run(self, dotter: &Dotter) -> Result<(), Exception> {
trace!("ContDotter::run({self:?}, {dotter:?})");
let Dotter {
from: (from_x, from_y),
to: (to_x, to_y),
@ -455,6 +462,7 @@ struct VmInner {
impl Drop for VmInner {
fn drop(&mut self) {
trace!("VmInner::drop({:?})", self.raw);
// SAFETY: The pointer passed is non-null.
unsafe {
haku2_vm_destroy(self.raw.as_ptr());

View file

@ -2,6 +2,7 @@ const std = @import("std");
const mem = std.mem;
const meta = std.meta;
const math = std.math;
const log = std.log.scoped(.system);
const bytecode = @import("bytecode.zig");
const Opcode = bytecode.Opcode;
@ -112,10 +113,13 @@ fn fromArgument(cx: Context, comptime T: type, i: usize) Vm.Error!T {
if (val != .ref or val.ref.* != .list) return typeError(cx.vm, val, i, "list");
return val.ref.list;
},
*const value.Shape => {
value.Shape => {
const val = cx.args[i];
if (val != .ref or val.ref.* != .shape) return typeError(cx.vm, val, i, "shape");
return &val.ref.shape;
if (toShape(val)) |shape| {
return shape;
} else {
return typeError(cx.vm, val, i, "shape");
}
},
*const value.Closure => {
const val = cx.args[i];
@ -756,9 +760,9 @@ fn circle(center: Vec4, radius: f32) value.Ref {
} } };
}
fn stroke(thickness: f32, color: Rgba, shape: *const value.Shape) value.Ref {
fn stroke(thickness: f32, color: Rgba, shape: value.Shape) value.Ref {
return .{ .scribble = .{
.shape = shape.*,
.shape = shape,
.action = .{ .stroke = .{
.thickness = thickness,
.color = color.value,
@ -766,9 +770,9 @@ fn stroke(thickness: f32, color: Rgba, shape: *const value.Shape) value.Ref {
} };
}
fn fill(color: Rgba, shape: *const value.Shape) value.Ref {
fn fill(color: Rgba, shape: value.Shape) value.Ref {
return .{ .scribble = .{
.shape = shape.*,
.shape = shape,
.action = .{ .fill = .{
.color = color.value,
} },
@ -776,6 +780,7 @@ fn fill(color: Rgba, shape: *const value.Shape) value.Ref {
}
fn withDotter(cont: *const value.Closure, cx: Context) Vm.Error!value.Ref {
log.debug("withDotter({})", .{cont});
if (cont.param_count != 1) {
return cx.vm.throw("function passed to withDotter must have a single parameter (\\d -> _), but it has {}", .{cont.param_count});
}

View file

@ -68,6 +68,28 @@ pub const Value = union(enum) {
},
};
}
pub fn format(value: Value, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
switch (value) {
.nil => try std.fmt.formatBuf("Nil", options, writer),
.false => try std.fmt.formatBuf("False", options, writer),
.true => try std.fmt.formatBuf("True", options, writer),
inline .tag, .number => |x| try std.fmt.format(writer, "{d}", .{x}),
inline .vec4, .rgba => |x| try std.fmt.format(writer, "{s}{d}", .{ @tagName(value), x }),
.ref => |ref| switch (ref.*) {
.closure => |c| try std.fmt.format(writer, "function({})", .{c.param_count}),
.list => |l| {
try std.fmt.formatBuf("[", options, writer);
for (l, 0..) |elem, i| {
if (i != 0) try std.fmt.formatBuf(", ", options, writer);
try elem.format(fmt, options, writer);
}
try std.fmt.formatBuf("]", options, writer);
},
inline .shape, .scribble, .reticle => |x| try std.fmt.format(writer, "{}", .{x}),
},
}
}
};
pub const TagId = enum(u16) {

View file

@ -17,13 +17,12 @@ stack_top: u32 = 0,
call_stack: []CallFrame,
call_stack_top: u32 = 0,
defs: []Value,
fuel: u32,
fuel: u32 = 0, // NOTE: VM must be refueled via reset() before running code
exception: ?Exception = null,
pub const Limits = struct {
stack_capacity: usize = 256,
call_stack_capacity: usize = 256,
fuel: u32 = 63336,
};
pub const CallFrame = struct {
@ -50,10 +49,16 @@ pub fn init(a: mem.Allocator, defs: *const bytecode.Defs, limits: *const Limits)
.stack = try a.alloc(Value, limits.stack_capacity),
.call_stack = try a.alloc(CallFrame, limits.call_stack_capacity),
.defs = try a.alloc(Value, defs.num_defs),
.fuel = limits.fuel,
};
}
pub fn reset(vm: *Vm, fuel: u32) void {
vm.stack_top = 0;
vm.call_stack_top = 0;
vm.fuel = fuel;
vm.exception = null;
}
pub fn throw(vm: *Vm, comptime fmt: []const u8, args: anytype) Error {
log.debug("throw: fmt={s}", .{fmt});