hotwire haku2 into rkgk

really a bodge job right now and it crashes but it's a start
This commit is contained in:
りき萌 2025-06-04 00:28:21 +02:00
parent 550227da34
commit 5de4f9d7c6
9 changed files with 178 additions and 71 deletions

View file

@ -306,6 +306,24 @@ impl Defs {
panic!("image must be a subset of the current defs")
});
}
pub fn serialize_defs(&self) -> String {
let mut result = String::new();
for def in &self.defs {
result.push_str(def);
result.push('\n');
}
result
}
pub fn serialize_tags(&self) -> String {
let mut result = String::new();
for tag in &self.tags {
result.push_str(tag);
result.push('\n');
}
result
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

View file

@ -47,7 +47,7 @@ pub struct Compiler<'a> {
#[derive(Debug, Clone, Copy)]
pub struct ClosureSpec {
pub(crate) local_count: u8,
pub local_count: u8,
}
impl<'a> Compiler<'a> {

View file

@ -17,6 +17,7 @@ pub fn build(b: *std.Build) void {
.root_module = mod,
});
lib.pie = true;
lib.bundle_compiler_rt = true;
b.installArtifact(lib);
const mod_wasm = b.createModule(.{

View file

@ -9,7 +9,11 @@ const Scratch = @import("scratch.zig");
const value = @import("value.zig");
const Vm = @import("vm.zig");
const allocator = if (builtin.cpu.arch == .wasm32) std.heap.wasm_allocator else @import("allocator.zig").hostAllocator;
const allocator =
if (builtin.cpu.arch == .wasm32)
std.heap.wasm_allocator
else
@import("allocator.zig").hostAllocator;
// Scratch
@ -28,7 +32,9 @@ export fn haku2_scratch_reset(scratch: *Scratch) void {
// Limits
export fn haku2_limits_new() ?*Vm.Limits {
return allocator.create(Vm.Limits) catch null;
const limits = allocator.create(Vm.Limits) catch return null;
limits.* = .{};
return limits;
}
export fn haku2_limits_destroy(limits: *Vm.Limits) void {
@ -102,9 +108,15 @@ export fn haku2_vm_run_main(
return true;
}
export fn haku2_vm_has_cont(vm: *const Vm) bool {
if (vm.stack.len == 0) return false;
const top = vm.top();
return top == .ref and top.ref.* == .reticle;
}
export fn haku2_vm_is_dotter(vm: *const Vm) bool {
if (vm.stack.len == 0) return false;
const top = vm.stack[vm.stack_top];
const top = vm.top();
return top == .ref and top.ref.* == .reticle and top.ref.reticle == .dotter;
}

View file

@ -3,6 +3,7 @@ use std::{
error::Error,
fmt::{self, Display},
marker::{PhantomData, PhantomPinned},
mem::forget,
ptr::{self, NonNull},
};
@ -91,6 +92,7 @@ extern "C" {
code_len: usize,
local_count: u8,
) -> bool;
fn haku2_vm_has_cont(vm: *const VmC) -> bool;
fn haku2_vm_is_dotter(vm: *const VmC) -> bool;
fn haku2_vm_run_dotter(
vm: *mut VmC,
@ -211,10 +213,38 @@ impl Drop for Defs {
}
}
#[derive(Debug)]
pub struct Code {
defs: Defs,
main_chunk: Vec<u8>,
main_local_count: u8,
}
impl Code {
/// Creates a new instance of `Code` from a valid vector of bytes.
///
/// # Safety
///
/// This does not perform any validation, and there is no way to perform such
/// validation before constructing this. The bytecode must simply be valid, which is the case
/// for bytecode emitted directly by the compiler.
///
/// Untrusted bytecode should never ever be loaded under any circumstances.
pub unsafe fn new(defs: Defs, main_chunk: Vec<u8>, main_local_count: u8) -> Self {
Self {
defs,
main_chunk,
main_local_count,
}
}
}
/// A VM that is ready to run and loaded with valid bytecode.
#[derive(Debug)]
pub struct Vm {
scratch: Scratch,
raw: NonNull<VmC>,
code: Code,
inner: VmInner,
}
#[derive(Debug)]
@ -236,17 +266,24 @@ pub struct Dotter {
}
impl Vm {
pub fn new(scratch: Scratch, defs: &Defs, limits: &Limits) -> Self {
pub fn new(scratch: Scratch, code: Code, limits: &Limits) -> Self {
Self {
// SAFETY:
// - Ownership of s is passed to the VM, so the VM cannot outlive the scratch space.
// - 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(), defs.raw.as_ptr(), limits.raw.as_ptr())
haku2_vm_new(
scratch.raw.as_ptr(),
code.defs.raw.as_ptr(),
limits.raw.as_ptr(),
)
})
.expect("out of memory"),
},
scratch,
code,
}
}
@ -255,19 +292,14 @@ 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.
///
/// # Safety
///
/// The bytecode passed in must be valid, because bytecode validation is done on a best-effort
/// basis. Bytecode retrieved out of the compiler is guaranteed to be safe.
pub unsafe fn begin(&mut self, code: &[u8], local_count: u8) -> Result<(), Exception> {
pub fn begin(&mut self) -> Result<(), Exception> {
let ok = unsafe {
haku2_vm_run_main(
self.raw.as_ptr(),
self.inner.raw.as_ptr(),
self.scratch.raw.as_ptr(),
code.as_ptr(),
code.len(),
local_count,
self.code.main_chunk.as_ptr(),
self.code.main_chunk.len(),
self.code.main_local_count,
)
};
if ok {
@ -277,9 +309,14 @@ impl Vm {
}
}
/// Returns whether `cont()` can be called to run the next continuation.
pub fn has_cont(&self) -> bool {
unsafe { haku2_vm_has_cont(self.inner.raw.as_ptr()) }
}
fn is_dotter(&self) -> bool {
// SAFETY: The pointer is valid.
unsafe { haku2_vm_is_dotter(self.raw.as_ptr()) }
unsafe { haku2_vm_is_dotter(self.inner.raw.as_ptr()) }
}
/// Returns how the VM should continue executing after the previous execution.
@ -291,12 +328,12 @@ impl Vm {
}
/// Renders the current scribble on top of the stack.
/// If the value on top is not a scribble, throws an exception (indicated by the return type.)
/// If the value on top is not a scribble, throws an exception.
///
/// The rendering is performed by calling into the [`Canvas`] trait.
pub fn render(&mut self, canvas: &mut dyn Canvas, max_depth: usize) -> Result<(), Exception> {
let mut wrapped = CanvasC { inner: canvas };
let ok = unsafe { haku2_render(self.raw.as_ptr(), &mut wrapped, max_depth) };
let ok = unsafe { haku2_render(self.inner.raw.as_ptr(), &mut wrapped, max_depth) };
if ok {
Ok(())
} else {
@ -308,7 +345,7 @@ impl Vm {
/// Returns `None` if there's no exception.
pub fn exception(&self) -> Option<Exception> {
// SAFETY: The pointer passed to this function is valid.
let len = unsafe { haku2_vm_exception_len(self.raw.as_ptr()) };
let len = unsafe { haku2_vm_exception_len(self.inner.raw.as_ptr()) };
if len == 0 {
return None;
}
@ -316,12 +353,24 @@ impl Vm {
let mut buffer = vec![0; len];
// SAFETY: The length of the buffer is as indicated by haku2_vm_exception_len.
unsafe {
haku2_vm_exception_render(self.raw.as_ptr(), buffer.as_mut_ptr());
haku2_vm_exception_render(self.inner.raw.as_ptr(), buffer.as_mut_ptr());
}
Some(Exception {
message: String::from_utf8_lossy(&buffer).into_owned(),
})
}
/// 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 {
let Vm {
mut scratch,
code: _,
inner: _,
} = self;
scratch.reset();
scratch
}
}
impl ContDotter<'_> {
@ -334,7 +383,7 @@ impl ContDotter<'_> {
let ok = unsafe {
haku2_vm_run_dotter(
self.vm.raw.as_ptr(),
self.vm.inner.raw.as_ptr(),
self.vm.scratch.raw.as_ptr(),
from_x,
from_y,
@ -351,7 +400,12 @@ impl ContDotter<'_> {
}
}
impl Drop for Vm {
#[derive(Debug)]
struct VmInner {
raw: NonNull<VmC>,
}
impl Drop for VmInner {
fn drop(&mut self) {
// SAFETY: The pointer passed is non-null.
unsafe {

View file

@ -54,6 +54,6 @@ fn renderRec(vm: *Vm, canvas: *Canvas, val: Value, depth: usize, max_depth: usiz
}
pub fn render(vm: *Vm, canvas: *Canvas, max_depth: usize) !void {
const val = try vm.pop();
const val = vm.stack[vm.stack_top - 1];
try renderRec(vm, canvas, val, 0, max_depth);
}

View file

@ -113,6 +113,14 @@ pub fn pop(vm: *Vm) Error!Value {
return vm.stack[vm.stack_top];
}
pub fn top(vm: *const Vm) Value {
if (vm.stack_top > 0) {
return vm.stack[vm.stack_top - 1];
} else {
return .nil;
}
}
pub fn pushCall(vm: *Vm, frame: CallFrame) Error!void {
if (vm.call_stack_top >= vm.call_stack.len) {
return vm.throw("too much recursion", .{});

View file

@ -371,47 +371,7 @@ impl SessionLoop {
// TODO: Auto save. This'll need us to compute which chunks will be affected
// by the interactions.
} // wall::EventKind::SetBrush { brush } => {
// // SetBrush is not dropped because it is a very important event.
// _ = self
// .render_commands_tx
// .send(RenderCommand::SetBrush {
// brush: brush.clone(),
// })
// .await;
// }
// wall::EventKind::Plot { points } => {
// let chunks_to_modify: Vec<_> =
// chunks_to_modify(&self.wall, points).into_iter().collect();
// match self.chunk_images.load(chunks_to_modify.clone()).await {
// Ok(_) => {
// // We drop commands if we take too long to render instead of lagging
// // the WebSocket thread.
// // Theoretically this will yield much better responsiveness, but it _will_
// // result in some visual glitches if we're getting bottlenecked.
// let (done_tx, done_rx) = oneshot::channel();
// let send_result =
// self.render_commands_tx.try_send(RenderCommand::Plot {
// points: points.clone(),
// done: done_tx,
// });
// if send_result.is_err() {
// info!(
// ?points,
// "render thread is overloaded, dropping request to draw points"
// );
// }
// let auto_save = Arc::clone(&self.auto_save);
// tokio::spawn(async move {
// _ = done_rx.await;
// auto_save.request(chunks_to_modify).await;
// });
// }
// Err(err) => error!(?err, "while loading chunks for render command"),
// }
// }
}
}
self.wall.event(wall::Event {
@ -516,6 +476,8 @@ impl SessionLoop {
Interaction::Dotter { from, to, num } => {
if brush_ok {
jumpstart_trampoline2(&mut haku);
if let Some(tramp) = jumpstart_trampoline(&mut haku, &mut trampoline) {
let cont = haku.cont(tramp);
if cont == Cont::Dotter {
@ -598,6 +560,7 @@ fn chunks_to_modify(
chunks
}
fn jumpstart_trampoline<'a>(
haku: &mut Haku,
trampoline: &'a mut Option<Trampoline>,
@ -608,6 +571,14 @@ fn jumpstart_trampoline<'a>(
trampoline.as_mut()
}
fn jumpstart_trampoline2(haku: &mut Haku) {
if !haku.has_cont2() {
if let Err(e) = haku.eval_brush2() {
error!("eval_brush2: {e}");
}
}
}
#[instrument(skip(wall, haku, value))]
fn draw_to_chunks(
wall: &Wall,

View file

@ -55,6 +55,8 @@ pub struct Haku {
vm: Vm,
vm_image: VmImage,
vm2: Option<haku2::Vm>,
brush: Option<(ChunkId, ClosureSpec)>,
}
@ -88,6 +90,7 @@ impl Haku {
defs_image,
vm,
vm_image,
vm2: None,
brush: None,
}
}
@ -140,9 +143,31 @@ impl Haku {
bail!("diagnostics were emitted");
}
let chunk_id = self.system.add_chunk(chunk).context("too many chunks")?;
let chunk_id = self
.system
.add_chunk(chunk.clone())
.context("too many chunks")?;
self.brush = Some((chunk_id, closure_spec));
// haku2 setup
{
let scratch = self
.vm2
.take()
.map(|vm| vm.into_scratch())
.unwrap_or_else(|| haku2::Scratch::new(self.limits.memory));
let defs = haku2::Defs::parse(&self.defs.serialize_defs(), &self.defs.serialize_tags());
// SAFETY: The code is fresh out of the compiler oven, so it is guaranteed to be valid.
// Well, more or less. There may lurk bugs.
let code = unsafe { haku2::Code::new(defs, chunk.bytecode, closure_spec.local_count) };
let limits = haku2::Limits::new(haku2::LimitsSpec {
stack_capacity: self.limits.stack_capacity,
call_stack_capacity: self.limits.call_stack_capacity,
fuel: self.limits.fuel as u32,
});
self.vm2 = Some(haku2::Vm::new(scratch, code, &limits))
}
info!("brush set successfully");
Ok(())
@ -170,6 +195,16 @@ impl Haku {
Ok(scribble)
}
#[instrument(skip(self), err(level = Level::INFO))]
pub fn eval_brush2(&mut self) -> eyre::Result<()> {
let vm = self
.vm2
.as_mut()
.ok_or_eyre("brush is not compiled and ready to be used")?;
vm.begin().context("an exception occurred during begin()")?;
Ok(())
}
#[instrument(skip(self, pixmap, value, translation), err(level = Level::INFO))]
pub fn render_value(
&self,
@ -194,6 +229,14 @@ impl Haku {
trampoline.cont(&self.vm)
}
pub fn has_cont2(&mut self) -> bool {
self.vm2.as_mut().expect("VM is not started").has_cont()
}
pub fn cont2(&mut self) -> haku2::Cont<'_> {
self.vm2.as_mut().expect("VM is not started").cont()
}
pub fn dotter(
&mut self,
trampoline: &mut Trampoline,