hotwire haku2 into rkgk
really a bodge job right now and it crashes but it's a start
This commit is contained in:
parent
550227da34
commit
5de4f9d7c6
9 changed files with 178 additions and 71 deletions
|
@ -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)]
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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(.{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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", .{});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue