initial commit

This commit is contained in:
liquidex 2024-08-10 23:10:03 +02:00
commit caec0b8ac9
27 changed files with 4786 additions and 0 deletions

15
.editorconfig Normal file
View file

@ -0,0 +1,15 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.js]
max_line_length = 100

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1058
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

15
Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[workspace]
resolver = "2"
members = ["crates/*"]
[workspace.dependencies]
haku.path = "crates/haku"
log = "0.4.22"
[profile.wasm-dev]
inherits = "dev"
panic = "abort"
[profile.wasm-release]
inherits = "release"
panic = "abort"

5
Justfile Normal file
View file

@ -0,0 +1,5 @@
serve wasm_profile="wasm-dev": (wasm wasm_profile)
cargo run -p canvane
wasm profile="wasm-dev":
cargo build -p haku-wasm --target wasm32-unknown-unknown --profile {{profile}}

15
crates/canvane/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "canvane"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7.5"
color-eyre = "0.6.3"
copy_dir = "0.1.3"
eyre = "0.6.12"
haku.workspace = true
tokio = { version = "1.39.2", features = ["full"] }
tower-http = { version = "0.5.2", features = ["fs"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }

View file

@ -0,0 +1,23 @@
use std::time::Duration;
use axum::{routing::get, Router};
use tokio::time::sleep;
pub fn router<S>() -> Router<S> {
Router::new()
.route("/stall", get(stall))
.route("/back-up", get(back_up))
.with_state(())
}
async fn stall() -> String {
loop {
// Sleep for a day, I guess. Just to uphold the connection forever without really using any
// significant resources.
sleep(Duration::from_secs(60 * 60 * 24)).await;
}
}
async fn back_up() -> String {
"".into()
}

View file

@ -0,0 +1,70 @@
use std::{
fs::{copy, create_dir_all, remove_dir_all},
path::Path,
};
use axum::Router;
use copy_dir::copy_dir;
use eyre::Context;
use tokio::net::TcpListener;
use tower_http::services::{ServeDir, ServeFile};
use tracing::{info, info_span};
use tracing_subscriber::fmt::format::FmtSpan;
#[cfg(debug_assertions)]
mod live_reload;
struct Paths<'a> {
target_dir: &'a Path,
}
fn build(paths: &Paths<'_>) -> eyre::Result<()> {
let _span = info_span!("build").entered();
_ = remove_dir_all(paths.target_dir);
create_dir_all(paths.target_dir).context("cannot create target directory")?;
copy_dir("static", paths.target_dir.join("static")).context("cannot copy static directory")?;
create_dir_all(paths.target_dir.join("static/wasm"))
.context("cannot create static/wasm directory")?;
copy(
"target/wasm32-unknown-unknown/wasm-dev/haku_wasm.wasm",
paths.target_dir.join("static/wasm/haku.wasm"),
)
.context("cannot copy haku.wasm file")?;
Ok(())
}
#[tokio::main]
async fn main() {
color_eyre::install().unwrap();
tracing_subscriber::fmt()
.with_span_events(FmtSpan::ACTIVE)
.init();
let paths = Paths {
target_dir: Path::new("target/site"),
};
match build(&paths) {
Ok(()) => (),
Err(error) => eprintln!("{error:?}"),
}
let app = Router::new()
.route_service(
"/",
ServeFile::new(paths.target_dir.join("static/index.html")),
)
.nest_service("/static", ServeDir::new(paths.target_dir.join("static")));
#[cfg(debug_assertions)]
let app = app.nest("/dev/live-reload", live_reload::router());
let listener = TcpListener::bind("0.0.0.0:8080")
.await
.expect("cannot bind to port");
info!("listening on port 8080");
axum::serve(listener, app).await.expect("cannot serve app");
}

View file

@ -0,0 +1,7 @@
[package]
name = "haku-cli"
version = "0.1.0"
edition = "2021"
[dependencies]
haku.workspace = true

View file

@ -0,0 +1,91 @@
// NOTE: This is a very bad CLI.
// Sorry!
use std::{error::Error, fmt::Display, io::BufRead};
use haku::{
bytecode::{Chunk, Defs},
compiler::{compile_expr, Compiler, Source},
sexp::{parse_toplevel, Ast, Parser},
system::System,
value::{BytecodeLoc, Closure, FunctionName, Ref, Value},
vm::{Vm, VmLimits},
};
fn eval(code: &str) -> Result<Value, Box<dyn Error>> {
let mut system = System::new(1);
let ast = Ast::new(1024);
let mut parser = Parser::new(ast, code);
let root = parse_toplevel(&mut parser);
let ast = parser.ast;
let src = Source {
code,
ast: &ast,
system: &system,
};
let mut defs = Defs::new(256);
let mut chunk = Chunk::new(65536).unwrap();
let mut compiler = Compiler::new(&mut defs, &mut chunk);
compile_expr(&mut compiler, &src, root)?;
let diagnostics = compiler.diagnostics;
let defs = compiler.defs;
println!("{chunk:?}");
for diagnostic in &diagnostics {
eprintln!(
"{}..{}: {}",
diagnostic.span.start, diagnostic.span.end, diagnostic.message
);
}
if !diagnostics.is_empty() {
return Err(Box::new(DiagnosticsEmitted));
}
let mut vm = Vm::new(
defs,
&VmLimits {
stack_capacity: 256,
call_stack_capacity: 256,
ref_capacity: 256,
fuel: 32768,
},
);
let chunk_id = system.add_chunk(chunk)?;
let closure = vm.create_ref(Ref::Closure(Closure {
start: BytecodeLoc {
chunk_id,
offset: 0,
},
name: FunctionName::Anonymous,
param_count: 0,
captures: Vec::new(),
}))?;
Ok(vm.run(&system, closure)?)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct DiagnosticsEmitted;
impl Display for DiagnosticsEmitted {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("diagnostics were emitted")
}
}
impl Error for DiagnosticsEmitted {}
fn main() -> Result<(), Box<dyn Error>> {
let stdin = std::io::stdin();
for line in stdin.lock().lines() {
let line = line?;
match eval(&line) {
Ok(value) => println!("{value:?}"),
Err(error) => eprintln!("error: {error}"),
}
}
Ok(())
}

View file

@ -0,0 +1,14 @@
[package]
name = "haku-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
arrayvec = { version = "0.7.4", default-features = false }
dlmalloc = { version = "0.2.6", features = ["global"] }
haku.workspace = true
log.workspace = true

349
crates/haku-wasm/src/lib.rs Normal file
View file

@ -0,0 +1,349 @@
#![no_std]
extern crate alloc;
use core::{alloc::Layout, ffi::CStr, slice, str};
use alloc::{boxed::Box, vec::Vec};
use haku::{
bytecode::{Chunk, Defs, DefsImage},
compiler::{compile_expr, CompileError, Compiler, Diagnostic, Source},
render::{Bitmap, Renderer, RendererLimits},
sexp::{self, parse_toplevel, Ast, Parser},
system::{ChunkId, System, SystemImage},
value::{BytecodeLoc, Closure, FunctionName, Ref, Value},
vm::{Exception, Vm, VmImage, VmLimits},
};
use log::info;
pub mod logging;
mod panicking;
#[global_allocator]
static ALLOCATOR: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc;
#[no_mangle]
unsafe extern "C" fn haku_alloc(size: usize, align: usize) -> *mut u8 {
alloc::alloc::alloc(Layout::from_size_align(size, align).unwrap())
}
#[no_mangle]
unsafe extern "C" fn haku_free(ptr: *mut u8, size: usize, align: usize) {
alloc::alloc::dealloc(ptr, Layout::from_size_align(size, align).unwrap())
}
#[derive(Debug, Clone, Copy)]
struct Limits {
max_chunks: usize,
max_defs: usize,
ast_capacity: usize,
chunk_capacity: usize,
stack_capacity: usize,
call_stack_capacity: usize,
ref_capacity: usize,
fuel: usize,
bitmap_stack_capacity: usize,
transform_stack_capacity: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
max_chunks: 2,
max_defs: 256,
ast_capacity: 1024,
chunk_capacity: 65536,
stack_capacity: 1024,
call_stack_capacity: 256,
ref_capacity: 2048,
fuel: 65536,
bitmap_stack_capacity: 4,
transform_stack_capacity: 16,
}
}
}
#[derive(Debug, Clone)]
struct Instance {
limits: Limits,
system: System,
system_image: SystemImage,
defs: Defs,
defs_image: DefsImage,
vm: Vm,
vm_image: VmImage,
exception: Option<Exception>,
}
#[no_mangle]
unsafe extern "C" fn haku_instance_new() -> *mut Instance {
// TODO: This should be a parameter.
let limits = Limits::default();
let system = System::new(limits.max_chunks);
let defs = Defs::new(limits.max_defs);
let vm = Vm::new(
&defs,
&VmLimits {
stack_capacity: limits.stack_capacity,
call_stack_capacity: limits.call_stack_capacity,
ref_capacity: limits.ref_capacity,
fuel: limits.fuel,
},
);
let system_image = system.image();
let defs_image = defs.image();
let vm_image = vm.image();
let instance = Box::new(Instance {
limits,
system,
system_image,
defs,
defs_image,
vm,
vm_image,
exception: None,
});
Box::leak(instance)
}
#[no_mangle]
unsafe extern "C" fn haku_instance_destroy(instance: *mut Instance) {
drop(Box::from_raw(instance));
}
#[no_mangle]
unsafe extern "C" fn haku_has_exception(instance: *mut Instance) -> bool {
(*instance).exception.is_some()
}
#[no_mangle]
unsafe extern "C" fn haku_exception_message(instance: *const Instance) -> *const u8 {
(*instance).exception.as_ref().unwrap().message.as_ptr()
}
#[no_mangle]
unsafe extern "C" fn haku_exception_message_len(instance: *const Instance) -> u32 {
(*instance).exception.as_ref().unwrap().message.len() as u32
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
enum StatusCode {
Ok,
ChunkTooBig,
DiagnosticsEmitted,
TooManyChunks,
OutOfRefSlots,
EvalException,
RenderException,
}
#[no_mangle]
extern "C" fn haku_is_ok(code: StatusCode) -> bool {
code == StatusCode::Ok
}
#[no_mangle]
extern "C" fn haku_status_string(code: StatusCode) -> *const i8 {
match code {
StatusCode::Ok => c"ok",
StatusCode::ChunkTooBig => c"compiled bytecode is too large",
StatusCode::DiagnosticsEmitted => c"diagnostics were emitted",
StatusCode::TooManyChunks => c"too many registered bytecode chunks",
StatusCode::OutOfRefSlots => c"out of ref slots (did you forget to restore the VM image?)",
StatusCode::EvalException => c"an exception occurred while evaluating your code",
StatusCode::RenderException => c"an exception occurred while rendering your brush",
}
.as_ptr()
}
#[derive(Debug, Default)]
enum BrushState {
#[default]
Default,
Ready(ChunkId),
}
#[derive(Debug, Default)]
struct Brush {
diagnostics: Vec<Diagnostic>,
state: BrushState,
}
#[no_mangle]
extern "C" fn haku_brush_new() -> *mut Brush {
Box::leak(Box::new(Brush::default()))
}
#[no_mangle]
unsafe extern "C" fn haku_brush_destroy(brush: *mut Brush) {
drop(Box::from_raw(brush))
}
#[no_mangle]
unsafe extern "C" fn haku_num_diagnostics(brush: *const Brush) -> u32 {
(*brush).diagnostics.len() as u32
}
#[no_mangle]
unsafe extern "C" fn haku_diagnostic_start(brush: *const Brush, index: u32) -> u32 {
(*brush).diagnostics[index as usize].span.start as u32
}
#[no_mangle]
unsafe extern "C" fn haku_diagnostic_end(brush: *const Brush, index: u32) -> u32 {
(*brush).diagnostics[index as usize].span.end as u32
}
#[no_mangle]
unsafe extern "C" fn haku_diagnostic_message(brush: *const Brush, index: u32) -> *const u8 {
(*brush).diagnostics[index as usize].message.as_ptr()
}
#[no_mangle]
unsafe extern "C" fn haku_diagnostic_message_len(brush: *const Brush, index: u32) -> u32 {
(*brush).diagnostics[index as usize].message.len() as u32
}
#[no_mangle]
unsafe extern "C" fn haku_compile_brush(
instance: *mut Instance,
out_brush: *mut Brush,
code_len: u32,
code: *const u8,
) -> StatusCode {
info!("compiling brush");
let instance = &mut *instance;
let brush = &mut *out_brush;
*brush = Brush::default();
let code = core::str::from_utf8(slice::from_raw_parts(code, code_len as usize))
.expect("invalid UTF-8");
let ast = Ast::new(instance.limits.ast_capacity);
let mut parser = Parser::new(ast, code);
let root = parse_toplevel(&mut parser);
let ast = parser.ast;
let src = Source {
code,
ast: &ast,
system: &instance.system,
};
let mut chunk = Chunk::new(instance.limits.chunk_capacity).unwrap();
let mut compiler = Compiler::new(&mut instance.defs, &mut chunk);
if let Err(error) = compile_expr(&mut compiler, &src, root) {
match error {
CompileError::Emit => return StatusCode::ChunkTooBig,
}
}
if !compiler.diagnostics.is_empty() {
brush.diagnostics = compiler.diagnostics;
return StatusCode::DiagnosticsEmitted;
}
let chunk_id = match instance.system.add_chunk(chunk) {
Ok(chunk_id) => chunk_id,
Err(_) => return StatusCode::TooManyChunks,
};
brush.state = BrushState::Ready(chunk_id);
info!("brush compiled into {chunk_id:?}");
StatusCode::Ok
}
struct BitmapLock {
bitmap: Option<Bitmap>,
}
#[no_mangle]
extern "C" fn haku_bitmap_new(width: u32, height: u32) -> *mut BitmapLock {
Box::leak(Box::new(BitmapLock {
bitmap: Some(Bitmap::new(width, height)),
}))
}
#[no_mangle]
unsafe extern "C" fn haku_bitmap_destroy(bitmap: *mut BitmapLock) {
drop(Box::from_raw(bitmap))
}
#[no_mangle]
unsafe extern "C" fn haku_bitmap_data(bitmap: *mut BitmapLock) -> *mut u8 {
let bitmap = (*bitmap)
.bitmap
.as_mut()
.expect("bitmap is already being rendered to");
bitmap.pixels[..].as_mut_ptr() as *mut u8
}
#[no_mangle]
unsafe extern "C" fn haku_render_brush(
instance: *mut Instance,
brush: *const Brush,
bitmap: *mut BitmapLock,
) -> StatusCode {
let instance = &mut *instance;
let brush = &*brush;
let BrushState::Ready(chunk_id) = brush.state else {
panic!("brush is not compiled and ready to be used");
};
let Ok(closure_id) = instance.vm.create_ref(Ref::Closure(Closure {
start: BytecodeLoc {
chunk_id,
offset: 0,
},
name: FunctionName::Anonymous,
param_count: 0,
captures: Vec::new(),
})) else {
return StatusCode::OutOfRefSlots;
};
let scribble = match instance.vm.run(&instance.system, closure_id) {
Ok(value) => value,
Err(exn) => {
instance.exception = Some(exn);
return StatusCode::EvalException;
}
};
let bitmap_locked = (*bitmap)
.bitmap
.take()
.expect("bitmap is already being rendered to");
let mut renderer = Renderer::new(
bitmap_locked,
&RendererLimits {
bitmap_stack_capacity: instance.limits.bitmap_stack_capacity,
transform_stack_capacity: instance.limits.transform_stack_capacity,
},
);
match renderer.render(&instance.vm, scribble) {
Ok(()) => (),
Err(exn) => {
instance.exception = Some(exn);
return StatusCode::RenderException;
}
}
let bitmap_locked = renderer.finish();
(*bitmap).bitmap = Some(bitmap_locked);
instance.vm.restore_image(&instance.vm_image);
StatusCode::Ok
}

View file

@ -0,0 +1,44 @@
use alloc::format;
use log::{info, Log};
extern "C" {
fn trace(message_len: u32, message: *const u8);
fn debug(message_len: u32, message: *const u8);
fn info(message_len: u32, message: *const u8);
fn warn(message_len: u32, message: *const u8);
fn error(message_len: u32, message: *const u8);
}
struct ConsoleLogger;
impl Log for ConsoleLogger {
fn enabled(&self, _: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
let s = record
.module_path()
.map(|module_path| format!("{module_path}: {}", record.args()))
.unwrap_or_else(|| format!("{}", record.args()));
unsafe {
match record.level() {
log::Level::Error => error(s.len() as u32, s.as_ptr()),
log::Level::Warn => warn(s.len() as u32, s.as_ptr()),
log::Level::Info => info(s.len() as u32, s.as_ptr()),
log::Level::Debug => debug(s.len() as u32, s.as_ptr()),
log::Level::Trace => trace(s.len() as u32, s.as_ptr()),
}
}
}
fn flush(&self) {}
}
#[no_mangle]
extern "C" fn haku_init_logging() {
log::set_logger(&ConsoleLogger).unwrap();
log::set_max_level(log::LevelFilter::Trace);
info!("enabled logging");
}

View file

@ -0,0 +1,20 @@
use core::fmt::Write;
use alloc::string::String;
extern "C" {
fn panic(message_len: u32, message: *const u8) -> !;
}
fn panic_impl(info: &core::panic::PanicInfo) -> ! {
let mut message = String::new();
_ = write!(&mut message, "{info}");
unsafe { panic(message.len() as u32, message.as_ptr()) };
}
#[cfg(not(test))]
#[panic_handler]
fn panic_handler(info: &core::panic::PanicInfo) -> ! {
panic_impl(info)
}

6
crates/haku/Cargo.toml Normal file
View file

@ -0,0 +1,6 @@
[package]
name = "haku"
version = "0.1.0"
edition = "2021"
[dependencies]

266
crates/haku/src/bytecode.rs Normal file
View file

@ -0,0 +1,266 @@
use core::{
fmt::{self, Display},
mem::transmute,
};
use alloc::{borrow::ToOwned, string::String, vec::Vec};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Opcode {
// Push literal values onto the stack.
Nil,
False,
True,
Number, // (float: f32)
// Duplicate existing values.
/// Push a value relative to the bottom of the current stack window.
Local, // (index: u8)
/// Push a captured value.
Capture, // (index: u8)
/// Get the value of a definition.
Def, // (index: u16)
/// Set the value of a definition.
SetDef, // (index: u16)
/// Drop `number` values from the stack.
/// <!-- OwO -->
DropLet, // (number: u8)
// Create literal functions.
Function, // (params: u8, then: u16), at `then`: (capture_count: u8, captures: [(source: u8, index: u8); capture_count])
// Control flow.
Jump, // (offset: u16)
JumpIfNot, // (offset: u16)
// Function calls.
Call, // (argc: u8)
/// This is a fast path for system calls, which are quite common (e.g. basic arithmetic.)
System, // (index: u8, argc: u8)
Return,
// NOTE: There must be no more opcodes after this.
// They will get treated as invalid.
}
// Constants used by the Function opcode to indicate capture sources.
pub const CAPTURE_LOCAL: u8 = 0;
pub const CAPTURE_CAPTURE: u8 = 1;
#[derive(Debug, Clone)]
pub struct Chunk {
pub bytecode: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Offset(u16);
impl Chunk {
pub fn new(capacity: usize) -> Result<Chunk, ChunkSizeError> {
if capacity <= (1 << 16) {
Ok(Chunk {
bytecode: Vec::with_capacity(capacity),
})
} else {
Err(ChunkSizeError)
}
}
pub fn offset(&self) -> Offset {
Offset(self.bytecode.len() as u16)
}
pub fn emit_bytes(&mut self, bytes: &[u8]) -> Result<Offset, EmitError> {
if self.bytecode.len() + bytes.len() > self.bytecode.capacity() {
return Err(EmitError);
}
let offset = Offset(self.bytecode.len() as u16);
self.bytecode.extend_from_slice(bytes);
Ok(offset)
}
pub fn emit_opcode(&mut self, opcode: Opcode) -> Result<Offset, EmitError> {
self.emit_bytes(&[opcode as u8])
}
pub fn emit_u8(&mut self, x: u8) -> Result<Offset, EmitError> {
self.emit_bytes(&[x])
}
pub fn emit_u16(&mut self, x: u16) -> Result<Offset, EmitError> {
self.emit_bytes(&x.to_le_bytes())
}
pub fn emit_u32(&mut self, x: u32) -> Result<Offset, EmitError> {
self.emit_bytes(&x.to_le_bytes())
}
pub fn emit_f32(&mut self, x: f32) -> Result<Offset, EmitError> {
self.emit_bytes(&x.to_le_bytes())
}
pub fn patch_u8(&mut self, offset: Offset, x: u8) {
self.bytecode[offset.0 as usize] = x;
}
pub fn patch_u16(&mut self, offset: Offset, x: u16) {
let b = x.to_le_bytes();
let i = offset.0 as usize;
self.bytecode[i] = b[0];
self.bytecode[i + 1] = b[1];
}
pub fn patch_offset(&mut self, offset: Offset, x: Offset) {
self.patch_u16(offset, x.0);
}
// NOTE: I'm aware these aren't the fastest implementations since they validate quite a lot
// during runtime, but this is just an MVP. It doesn't have to be blazingly fast.
pub fn read_u8(&self, pc: &mut usize) -> Result<u8, ReadError> {
let x = self.bytecode.get(*pc).copied();
*pc += 1;
x.ok_or(ReadError)
}
pub fn read_u16(&self, pc: &mut usize) -> Result<u16, ReadError> {
let xs = &self.bytecode[*pc..*pc + 2];
*pc += 2;
Ok(u16::from_le_bytes(xs.try_into().map_err(|_| ReadError)?))
}
pub fn read_u32(&self, pc: &mut usize) -> Result<u32, ReadError> {
let xs = &self.bytecode[*pc..*pc + 4];
*pc += 4;
Ok(u32::from_le_bytes(xs.try_into().map_err(|_| ReadError)?))
}
pub fn read_f32(&self, pc: &mut usize) -> Result<f32, ReadError> {
let xs = &self.bytecode[*pc..*pc + 4];
*pc += 4;
Ok(f32::from_le_bytes(xs.try_into().map_err(|_| ReadError)?))
}
pub fn read_opcode(&self, pc: &mut usize) -> Result<Opcode, ReadError> {
let x = self.read_u8(pc)?;
if x <= Opcode::Return as u8 {
Ok(unsafe { transmute::<u8, Opcode>(x) })
} else {
Err(ReadError)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChunkSizeError;
impl Display for ChunkSizeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "chunk size must be less than 64 KiB")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EmitError;
impl Display for EmitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "out of space in chunk")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadError;
impl Display for ReadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid bytecode: out of bounds read or invalid opcode")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct DefId(u16);
impl DefId {
pub fn to_u16(self) -> u16 {
self.0
}
}
#[derive(Debug, Clone)]
pub struct Defs {
defs: Vec<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct DefsImage {
defs: usize,
}
impl Defs {
pub fn new(capacity: usize) -> Self {
assert!(capacity < u16::MAX as usize + 1);
Self {
defs: Vec::with_capacity(capacity),
}
}
pub fn len(&self) -> u16 {
self.defs.len() as u16
}
pub fn is_empty(&self) -> bool {
self.len() != 0
}
pub fn get(&mut self, name: &str) -> Option<DefId> {
self.defs
.iter()
.position(|n| *n == name)
.map(|index| DefId(index as u16))
}
pub fn add(&mut self, name: &str) -> Result<DefId, DefError> {
if self.defs.iter().any(|n| n == name) {
Err(DefError::Exists)
} else {
if self.defs.len() >= self.defs.capacity() {
return Err(DefError::OutOfSpace);
}
let id = DefId(self.defs.len() as u16);
self.defs.push(name.to_owned());
Ok(id)
}
}
pub fn image(&self) -> DefsImage {
DefsImage {
defs: self.defs.len(),
}
}
pub fn restore_image(&mut self, image: &DefsImage) {
self.defs.resize_with(image.defs, || {
panic!("image must be a subset of the current defs")
});
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DefError {
Exists,
OutOfSpace,
}
impl Display for DefError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
DefError::Exists => "definition already exists",
DefError::OutOfSpace => "too many definitions",
})
}
}

625
crates/haku/src/compiler.rs Normal file
View file

@ -0,0 +1,625 @@
use core::{
error::Error,
fmt::{self, Display},
};
use alloc::vec::Vec;
use crate::{
bytecode::{Chunk, DefError, DefId, Defs, EmitError, Opcode, CAPTURE_CAPTURE, CAPTURE_LOCAL},
sexp::{Ast, NodeId, NodeKind, Span},
system::System,
};
pub struct Source<'a> {
pub code: &'a str,
pub ast: &'a Ast,
pub system: &'a System,
}
#[derive(Debug, Clone, Copy)]
pub struct Diagnostic {
pub span: Span,
pub message: &'static str,
}
#[derive(Debug, Clone, Copy)]
struct Local<'a> {
name: &'a str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Variable {
Local(u8),
Captured(u8),
}
struct Scope<'a> {
locals: Vec<Local<'a>>,
captures: Vec<Variable>,
}
pub struct Compiler<'a, 'b> {
pub defs: &'a mut Defs,
pub chunk: &'b mut Chunk,
pub diagnostics: Vec<Diagnostic>,
scopes: Vec<Scope<'a>>,
}
impl<'a, 'b> Compiler<'a, 'b> {
pub fn new(defs: &'a mut Defs, chunk: &'b mut Chunk) -> Self {
Self {
defs,
chunk,
diagnostics: Vec::with_capacity(16),
scopes: Vec::from_iter([Scope {
locals: Vec::new(),
captures: Vec::new(),
}]),
}
}
pub fn diagnose(&mut self, diagnostic: Diagnostic) {
if self.diagnostics.len() >= self.diagnostics.capacity() {
return;
}
if self.diagnostics.len() == self.diagnostics.capacity() - 1 {
self.diagnostics.push(Diagnostic {
span: Span::new(0, 0),
message: "too many diagnostics emitted, stopping", // hello clangd!
})
} else {
self.diagnostics.push(diagnostic);
}
}
}
type CompileResult<T = ()> = Result<T, CompileError>;
pub fn compile_expr<'a>(
c: &mut Compiler<'a, '_>,
src: &Source<'a>,
node_id: NodeId,
) -> CompileResult {
let node = src.ast.get(node_id);
match node.kind {
NodeKind::Eof => unreachable!("eof node should never be emitted"),
NodeKind::Nil => compile_nil(c),
NodeKind::Ident => compile_ident(c, src, node_id),
NodeKind::Number => compile_number(c, src, node_id),
NodeKind::List(_, _) => compile_list(c, src, node_id),
NodeKind::Toplevel(_) => compile_toplevel(c, src, node_id),
NodeKind::Error(message) => {
c.diagnose(Diagnostic {
span: node.span,
message,
});
Ok(())
}
}
}
fn compile_nil(c: &mut Compiler<'_, '_>) -> CompileResult {
c.chunk.emit_opcode(Opcode::Nil)?;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CaptureError;
fn find_variable(
c: &mut Compiler<'_, '_>,
name: &str,
scope_index: usize,
) -> Result<Option<Variable>, CaptureError> {
let scope = &c.scopes[scope_index];
if let Some(index) = scope.locals.iter().rposition(|l| l.name == name) {
let index = u8::try_from(index).expect("a function must not declare more than 256 locals");
Ok(Some(Variable::Local(index)))
} else if scope_index > 0 {
// Search upper scope if not found.
if let Some(variable) = find_variable(c, name, scope_index - 1)? {
let scope = &mut c.scopes[scope_index];
let capture_index = scope
.captures
.iter()
.position(|c| c == &variable)
.unwrap_or_else(|| {
let new_index = scope.captures.len();
scope.captures.push(variable);
new_index
});
let capture_index = u8::try_from(capture_index).map_err(|_| CaptureError)?;
Ok(Some(Variable::Captured(capture_index)))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
fn compile_ident<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, node_id: NodeId) -> CompileResult {
let ident = src.ast.get(node_id);
let name = ident.span.slice(src.code);
match name {
"false" => _ = c.chunk.emit_opcode(Opcode::False)?,
"true" => _ = c.chunk.emit_opcode(Opcode::True)?,
_ => match find_variable(c, name, c.scopes.len() - 1) {
Ok(Some(Variable::Local(index))) => {
c.chunk.emit_opcode(Opcode::Local)?;
c.chunk.emit_u8(index)?;
}
Ok(Some(Variable::Captured(index))) => {
c.chunk.emit_opcode(Opcode::Capture)?;
c.chunk.emit_u8(index)?;
}
Ok(None) => {
if let Some(def_id) = c.defs.get(name) {
c.chunk.emit_opcode(Opcode::Def)?;
c.chunk.emit_u16(def_id.to_u16())?;
} else {
c.diagnose(Diagnostic {
span: ident.span,
message: "undefined variable",
});
}
}
Err(CaptureError) => {
c.diagnose(Diagnostic {
span: ident.span,
message: "too many variables captured from outer functions in this scope",
});
}
},
}
Ok(())
}
fn compile_number(c: &mut Compiler<'_, '_>, src: &Source<'_>, node_id: NodeId) -> CompileResult {
let node = src.ast.get(node_id);
let literal = node.span.slice(src.code);
let float: f32 = literal
.parse()
.expect("the parser should've gotten us a string parsable by the stdlib");
c.chunk.emit_opcode(Opcode::Number)?;
c.chunk.emit_f32(float)?;
Ok(())
}
fn compile_list<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, node_id: NodeId) -> CompileResult {
let NodeKind::List(function_id, args) = src.ast.get(node_id).kind else {
unreachable!("compile_list expects a List");
};
let function = src.ast.get(function_id);
let name = function.span.slice(src.code);
if function.kind == NodeKind::Ident {
match name {
"fn" => return compile_fn(c, src, args),
"if" => return compile_if(c, src, args),
"let" => return compile_let(c, src, args),
_ => (),
};
}
let mut argument_count = 0;
let mut args = args;
while let NodeKind::List(head, tail) = src.ast.get(args).kind {
compile_expr(c, src, head)?;
argument_count += 1;
args = tail;
}
let argument_count = u8::try_from(argument_count).unwrap_or_else(|_| {
c.diagnose(Diagnostic {
span: src.ast.get(args).span,
message: "function call has too many arguments",
});
0
});
if let (NodeKind::Ident, Some(index)) = (function.kind, (src.system.resolve_fn)(name)) {
c.chunk.emit_opcode(Opcode::System)?;
c.chunk.emit_u8(index)?;
c.chunk.emit_u8(argument_count)?;
} else {
// This is a bit of an oddity: we only emit the function expression _after_ the arguments,
// but since the language is effectless this doesn't matter in practice.
// It makes for less code in the compiler and the VM.
compile_expr(c, src, function_id)?;
c.chunk.emit_opcode(Opcode::Call)?;
c.chunk.emit_u8(argument_count)?;
}
Ok(())
}
struct WalkList {
current: NodeId,
ok: bool,
}
impl WalkList {
fn new(start: NodeId) -> Self {
Self {
current: start,
ok: true,
}
}
fn expect_arg(
&mut self,
c: &mut Compiler<'_, '_>,
src: &Source<'_>,
message: &'static str,
) -> NodeId {
if !self.ok {
return NodeId::NIL;
}
if let NodeKind::List(expr, tail) = src.ast.get(self.current).kind {
self.current = tail;
expr
} else {
c.diagnose(Diagnostic {
span: src.ast.get(self.current).span,
message,
});
self.ok = false;
NodeId::NIL
}
}
fn expect_nil(&mut self, c: &mut Compiler<'_, '_>, src: &Source<'_>, message: &'static str) {
if src.ast.get(self.current).kind != NodeKind::Nil {
c.diagnose(Diagnostic {
span: src.ast.get(self.current).span,
message,
});
// NOTE: Don't set self.ok to false, since this is not a fatal error.
// The nodes returned previously are valid and therefore it's safe to operate on them.
// Just having extra arguments shouldn't inhibit emitting additional diagnostics in
// the expression.
}
}
}
fn compile_if<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, args: NodeId) -> CompileResult {
let mut list = WalkList::new(args);
let condition = list.expect_arg(c, src, "missing `if` condition");
let if_true = list.expect_arg(c, src, "missing `if` true branch");
let if_false = list.expect_arg(c, src, "missing `if` false branch");
list.expect_nil(c, src, "extra arguments after `if` false branch");
if !list.ok {
return Ok(());
}
compile_expr(c, src, condition)?;
c.chunk.emit_opcode(Opcode::JumpIfNot)?;
let false_jump_offset_offset = c.chunk.emit_u16(0)?;
compile_expr(c, src, if_true)?;
c.chunk.emit_opcode(Opcode::Jump)?;
let true_jump_offset_offset = c.chunk.emit_u16(0)?;
let false_jump_offset = c.chunk.offset();
c.chunk
.patch_offset(false_jump_offset_offset, false_jump_offset);
compile_expr(c, src, if_false)?;
let true_jump_offset = c.chunk.offset();
c.chunk
.patch_offset(true_jump_offset_offset, true_jump_offset);
Ok(())
}
fn compile_let<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, args: NodeId) -> CompileResult {
let mut list = WalkList::new(args);
let binding_list = list.expect_arg(c, src, "missing `let` binding list ((x 1) (y 2) ...)");
let expr = list.expect_arg(c, src, "missing expression to `let` names into");
list.expect_nil(c, src, "extra arguments after `let` expression");
if !list.ok {
return Ok(());
}
// NOTE: Our `let` behaves like `let*` from Lisps.
// This is because this is generally the more intuitive behaviour with how variable declarations
// work in traditional imperative languages.
// We do not offer an alternative to Lisp `let` to be as minimal as possible.
let mut current = binding_list;
let mut local_count: usize = 0;
while let NodeKind::List(head, tail) = src.ast.get(current).kind {
if !matches!(src.ast.get(head).kind, NodeKind::List(_, _)) {
c.diagnose(Diagnostic {
span: src.ast.get(head).span,
message: "`let` binding expected, like (x 1)",
});
current = tail;
continue;
}
let mut list = WalkList::new(head);
let ident = list.expect_arg(c, src, "binding name expected");
let value = list.expect_arg(c, src, "binding value expected");
list.expect_nil(c, src, "extra expressions after `let` binding value");
if src.ast.get(ident).kind != NodeKind::Ident {
c.diagnose(Diagnostic {
span: src.ast.get(ident).span,
message: "binding name must be an identifier",
});
}
// NOTE: Compile expression _before_ putting the value into scope.
// This is so that the variable cannot refer to itself, as it is yet to be declared.
compile_expr(c, src, value)?;
let name = src.ast.get(ident).span.slice(src.code);
let scope = c.scopes.last_mut().unwrap();
if scope.locals.len() >= u8::MAX as usize {
c.diagnose(Diagnostic {
span: src.ast.get(ident).span,
message: "too many names bound in this function at a single time",
});
} else {
scope.locals.push(Local { name });
}
local_count += 1;
current = tail;
}
compile_expr(c, src, expr)?;
let scope = c.scopes.last_mut().unwrap();
scope
.locals
.resize_with(scope.locals.len() - local_count, || unreachable!());
// NOTE: If we reach more than 255 locals declared in our `let`, we should've gotten
// a diagnostic emitted in the `while` loop beforehand.
let local_count = u8::try_from(local_count).unwrap_or(0);
c.chunk.emit_opcode(Opcode::DropLet)?;
c.chunk.emit_u8(local_count)?;
Ok(())
}
fn compile_fn<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, args: NodeId) -> CompileResult {
let mut list = WalkList::new(args);
let param_list = list.expect_arg(c, src, "missing function parameters");
let body = list.expect_arg(c, src, "missing function body");
list.expect_nil(c, src, "extra arguments after function body");
if !list.ok {
return Ok(());
}
let mut locals = Vec::new();
let mut current = param_list;
while let NodeKind::List(ident, tail) = src.ast.get(current).kind {
if let NodeKind::Ident = src.ast.get(ident).kind {
locals.push(Local {
name: src.ast.get(ident).span.slice(src.code),
})
} else {
c.diagnose(Diagnostic {
span: src.ast.get(ident).span,
message: "function parameters must be identifiers",
})
}
current = tail;
}
let param_count = u8::try_from(locals.len()).unwrap_or_else(|_| {
c.diagnose(Diagnostic {
span: src.ast.get(param_list).span,
message: "too many function parameters",
});
0
});
c.chunk.emit_opcode(Opcode::Function)?;
c.chunk.emit_u8(param_count)?;
let after_offset = c.chunk.emit_u16(0)?;
c.scopes.push(Scope {
locals,
captures: Vec::new(),
});
compile_expr(c, src, body)?;
c.chunk.emit_opcode(Opcode::Return)?;
let after = u16::try_from(c.chunk.bytecode.len()).expect("chunk is too large");
c.chunk.patch_u16(after_offset, after);
let scope = c.scopes.pop().unwrap();
let capture_count = u8::try_from(scope.captures.len()).unwrap_or_else(|_| {
c.diagnose(Diagnostic {
span: src.ast.get(body).span,
message: "function refers to too many variables from the outer function",
});
0
});
c.chunk.emit_u8(capture_count)?;
for capture in scope.captures {
match capture {
// TODO: There's probably a more clever way to encode these than wasting an entire byte
// on what's effectively just a bool per each capture.
Variable::Local(index) => {
c.chunk.emit_u8(CAPTURE_LOCAL)?;
c.chunk.emit_u8(index)?;
}
Variable::Captured(index) => {
c.chunk.emit_u8(CAPTURE_CAPTURE)?;
c.chunk.emit_u8(index)?;
}
}
}
Ok(())
}
fn compile_toplevel<'a>(
c: &mut Compiler<'a, '_>,
src: &Source<'a>,
node_id: NodeId,
) -> CompileResult {
let NodeKind::Toplevel(mut current) = src.ast.get(node_id).kind else {
unreachable!("compile_toplevel expects a Toplevel");
};
def_prepass(c, src, current)?;
let mut had_result = false;
while let NodeKind::List(expr, tail) = src.ast.get(current).kind {
match compile_toplevel_expr(c, src, expr)? {
ToplevelExpr::Def => (),
ToplevelExpr::Result => had_result = true,
}
if had_result && src.ast.get(tail).kind != NodeKind::Nil {
c.diagnose(Diagnostic {
span: src.ast.get(tail).span,
message: "result value may not be followed by anything else",
});
break;
}
current = tail;
}
if !had_result {
c.chunk.emit_opcode(Opcode::Nil)?;
}
c.chunk.emit_opcode(Opcode::Return)?;
Ok(())
}
fn def_prepass<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, node_id: NodeId) -> CompileResult {
// This is a bit of a pattern matching tapeworm, but Rust unfortunately doesn't have `if let`
// chains yet to make this more readable.
let mut current = node_id;
while let NodeKind::List(expr, tail) = src.ast.get(current).kind {
if let NodeKind::List(head_id, tail_id) = src.ast.get(expr).kind {
let head = src.ast.get(head_id);
let name = head.span.slice(src.code);
if head.kind == NodeKind::Ident && name == "def" {
if let NodeKind::List(ident_id, _) = src.ast.get(tail_id).kind {
let ident = src.ast.get(ident_id);
if ident.kind == NodeKind::Ident {
let name = ident.span.slice(src.code);
match c.defs.add(name) {
Ok(_) => (),
Err(DefError::Exists) => c.diagnose(Diagnostic {
span: ident.span,
message: "redefinitions of defs are not allowed",
}),
Err(DefError::OutOfSpace) => c.diagnose(Diagnostic {
span: ident.span,
message: "too many defs",
}),
}
}
}
}
}
current = tail;
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ToplevelExpr {
Def,
Result,
}
fn compile_toplevel_expr<'a>(
c: &mut Compiler<'a, '_>,
src: &Source<'a>,
node_id: NodeId,
) -> CompileResult<ToplevelExpr> {
let node = src.ast.get(node_id);
if let NodeKind::List(head_id, tail_id) = node.kind {
let head = src.ast.get(head_id);
if head.kind == NodeKind::Ident {
let name = head.span.slice(src.code);
if name == "def" {
compile_def(c, src, tail_id)?;
return Ok(ToplevelExpr::Def);
}
}
}
compile_expr(c, src, node_id)?;
Ok(ToplevelExpr::Result)
}
fn compile_def<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, args: NodeId) -> CompileResult {
let mut list = WalkList::new(args);
let ident = list.expect_arg(c, src, "missing definition name");
let value = list.expect_arg(c, src, "missing definition value");
list.expect_nil(c, src, "extra arguments after definition");
if !list.ok {
return Ok(());
}
let name = src.ast.get(ident).span.slice(src.code);
// NOTE: def_prepass collects all definitions beforehand.
// In case a def ends up not existing, that means we ran out of space for defs - so emit a
// zero def instead.
let def_id = c.defs.get(name).unwrap_or_default();
compile_expr(c, src, value)?;
c.chunk.emit_opcode(Opcode::SetDef)?;
c.chunk.emit_u16(def_id.to_u16())?;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompileError {
Emit,
}
impl From<EmitError> for CompileError {
fn from(_: EmitError) -> Self {
Self::Emit
}
}
impl Display for CompileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
CompileError::Emit => "bytecode is too big",
})
}
}
impl Error for CompileError {}

11
crates/haku/src/lib.rs Normal file
View file

@ -0,0 +1,11 @@
#![no_std]
extern crate alloc;
pub mod bytecode;
pub mod compiler;
pub mod render;
pub mod sexp;
pub mod system;
pub mod value;
pub mod vm;

144
crates/haku/src/render.rs Normal file
View file

@ -0,0 +1,144 @@
use core::iter;
use alloc::vec::Vec;
use crate::{
value::{Ref, Rgba, Scribble, Shape, Stroke, Value, Vec4},
vm::{Exception, Vm},
};
pub struct Bitmap {
pub width: u32,
pub height: u32,
pub pixels: Vec<Rgba>,
}
impl Bitmap {
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
pixels: Vec::from_iter(
iter::repeat(Rgba::default()).take(width as usize * height as usize),
),
}
}
pub fn pixel_index(&self, x: u32, y: u32) -> usize {
x as usize + y as usize * self.width as usize
}
pub fn get(&self, x: u32, y: u32) -> Rgba {
self.pixels[self.pixel_index(x, y)]
}
pub fn set(&mut self, x: u32, y: u32, rgba: Rgba) {
let index = self.pixel_index(x, y);
self.pixels[index] = rgba;
}
}
pub struct RendererLimits {
pub bitmap_stack_capacity: usize,
pub transform_stack_capacity: usize,
}
pub struct Renderer {
bitmap_stack: Vec<Bitmap>,
transform_stack: Vec<Vec4>,
}
impl Renderer {
pub fn new(bitmap: Bitmap, limits: &RendererLimits) -> Self {
assert!(limits.bitmap_stack_capacity > 0);
assert!(limits.transform_stack_capacity > 0);
let mut blend_stack = Vec::with_capacity(limits.bitmap_stack_capacity);
blend_stack.push(bitmap);
let mut transform_stack = Vec::with_capacity(limits.transform_stack_capacity);
transform_stack.push(Vec4::default());
Self {
bitmap_stack: blend_stack,
transform_stack,
}
}
fn create_exception(_vm: &Vm, _at: Value, message: &'static str) -> Exception {
Exception { message }
}
fn transform(&self) -> &Vec4 {
self.transform_stack.last().unwrap()
}
fn transform_mut(&mut self) -> &mut Vec4 {
self.transform_stack.last_mut().unwrap()
}
fn bitmap(&self) -> &Bitmap {
self.bitmap_stack.last().unwrap()
}
fn bitmap_mut(&mut self) -> &mut Bitmap {
self.bitmap_stack.last_mut().unwrap()
}
pub fn translate(&mut self, translation: Vec4) {
let transform = self.transform_mut();
transform.x += translation.x;
transform.y += translation.y;
transform.z += translation.z;
transform.w += translation.w;
}
pub fn to_bitmap_coords(&self, point: Vec4) -> Option<(u32, u32)> {
let transform = self.transform();
let x = point.x + transform.x;
let y = point.y + transform.y;
if x >= 0.0 && y >= 0.0 {
let (x, y) = (x as u32, y as u32);
if x < self.bitmap().width && y < self.bitmap().height {
Some((x, y))
} else {
None
}
} else {
None
}
}
pub fn render(&mut self, vm: &Vm, value: Value) -> Result<(), Exception> {
static NOT_A_SCRIBBLE: &str = "cannot draw something that is not a scribble";
let (_id, scribble) = vm
.get_ref_value(value)
.ok_or_else(|| Self::create_exception(vm, value, NOT_A_SCRIBBLE))?;
let Ref::Scribble(scribble) = scribble else {
return Err(Self::create_exception(vm, value, NOT_A_SCRIBBLE));
};
match scribble {
Scribble::Stroke(stroke) => self.render_stroke(vm, value, stroke)?,
}
Ok(())
}
fn render_stroke(&mut self, _vm: &Vm, _value: Value, stroke: &Stroke) -> Result<(), Exception> {
match stroke.shape {
Shape::Point(vec) => {
if let Some((x, y)) = self.to_bitmap_coords(vec) {
// TODO: thickness
self.bitmap_mut().set(x, y, stroke.color);
}
}
}
Ok(())
}
pub fn finish(mut self) -> Bitmap {
self.bitmap_stack.drain(..).next().unwrap()
}
}

476
crates/haku/src/sexp.rs Normal file
View file

@ -0,0 +1,476 @@
use core::{cell::Cell, fmt};
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub start: usize,
pub end: usize,
}
impl Span {
pub fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
pub fn slice<'a>(&self, source: &'a str) -> &'a str {
&source[self.start..self.end]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NodeId(usize);
impl NodeId {
pub const NIL: NodeId = NodeId(0);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeKind {
Nil,
Eof,
// Atoms
Ident,
Number,
List(NodeId, NodeId),
Toplevel(NodeId),
Error(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Node {
pub span: Span,
pub kind: NodeKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ast {
pub nodes: Vec<Node>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AstWriteMode {
Compact,
Spans,
}
impl Ast {
pub fn new(capacity: usize) -> Self {
assert!(capacity >= 1, "there must be space for at least a nil node");
let mut ast = Self {
nodes: Vec::with_capacity(capacity),
};
ast.alloc(Node {
span: Span::new(0, 0),
kind: NodeKind::Nil,
})
.unwrap();
ast
}
pub fn alloc(&mut self, node: Node) -> Result<NodeId, NodeAllocError> {
if self.nodes.len() >= self.nodes.capacity() {
return Err(NodeAllocError);
}
let index = self.nodes.len();
self.nodes.push(node);
Ok(NodeId(index))
}
pub fn get(&self, node_id: NodeId) -> &Node {
&self.nodes[node_id.0]
}
pub fn get_mut(&mut self, node_id: NodeId) -> &mut Node {
&mut self.nodes[node_id.0]
}
pub fn write(
&self,
source: &str,
node_id: NodeId,
w: &mut dyn fmt::Write,
mode: AstWriteMode,
) -> fmt::Result {
#[allow(clippy::too_many_arguments)]
fn write_list(
ast: &Ast,
source: &str,
w: &mut dyn fmt::Write,
mode: AstWriteMode,
mut head: NodeId,
mut tail: NodeId,
sep_element: &str,
sep_tail: &str,
) -> fmt::Result {
loop {
write_rec(ast, source, w, mode, head)?;
match ast.get(tail).kind {
NodeKind::Nil => break,
NodeKind::List(head2, tail2) => {
w.write_str(sep_element)?;
(head, tail) = (head2, tail2);
}
_ => {
w.write_str(sep_tail)?;
write_rec(ast, source, w, mode, tail)?;
break;
}
}
}
Ok(())
}
// NOTE: Separated out to a separate function in case we ever want to introduce auto-indentation.
fn write_rec(
ast: &Ast,
source: &str,
w: &mut dyn fmt::Write,
mode: AstWriteMode,
node_id: NodeId,
) -> fmt::Result {
let node = ast.get(node_id);
match &node.kind {
NodeKind::Nil => write!(w, "()")?,
NodeKind::Eof => write!(w, "<eof>")?,
NodeKind::Ident | NodeKind::Number => write!(w, "{}", node.span.slice(source))?,
NodeKind::List(head, tail) => {
w.write_char('(')?;
write_list(ast, source, w, mode, *head, *tail, " ", " . ")?;
w.write_char(')')?;
}
NodeKind::Toplevel(list) => {
let NodeKind::List(head, tail) = ast.get(*list).kind else {
unreachable!("child of Toplevel must be a List");
};
write_list(ast, source, w, mode, head, tail, "\n", " . ")?;
}
NodeKind::Error(message) => write!(w, "#error({message})")?,
}
if mode == AstWriteMode::Spans {
write!(w, "@{}..{}", node.span.start, node.span.end)?;
}
Ok(())
}
write_rec(self, source, w, mode, node_id)?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NodeAllocError;
pub struct Parser<'a> {
pub ast: Ast,
input: &'a str,
position: usize,
fuel: Cell<usize>,
alloc_error: NodeId,
}
impl<'a> Parser<'a> {
const FUEL: usize = 256;
pub fn new(mut ast: Ast, input: &'a str) -> Self {
let alloc_error = ast
.alloc(Node {
span: Span::new(0, 0),
kind: NodeKind::Error("program is too big"),
})
.expect("there is not enough space in the arena for an error node");
Self {
ast,
input,
position: 0,
fuel: Cell::new(Self::FUEL),
alloc_error,
}
}
pub fn current(&self) -> char {
assert_ne!(self.fuel.get(), 0, "parser is stuck");
self.fuel.set(self.fuel.get() - 1);
self.input[self.position..].chars().next().unwrap_or('\0')
}
pub fn advance(&mut self) {
self.position += self.current().len_utf8();
self.fuel.set(Self::FUEL);
}
pub fn alloc(&mut self, expr: Node) -> NodeId {
self.ast.alloc(expr).unwrap_or(self.alloc_error)
}
}
pub fn skip_whitespace_and_comments(p: &mut Parser<'_>) {
loop {
match p.current() {
' ' | '\t' | '\n' => {
p.advance();
continue;
}
';' => {
while p.current() != '\n' {
p.advance();
}
}
_ => break,
}
}
}
fn is_decimal_digit(c: char) -> bool {
c.is_ascii_digit()
}
pub fn parse_number(p: &mut Parser<'_>) -> NodeKind {
while is_decimal_digit(p.current()) {
p.advance();
}
if p.current() == '.' {
p.advance();
if !is_decimal_digit(p.current()) {
return NodeKind::Error("missing digits after decimal point '.' in number literal");
}
while is_decimal_digit(p.current()) {
p.advance();
}
}
NodeKind::Number
}
fn is_ident(c: char) -> bool {
// The identifier character set is quite limited to help with easy expansion in the future.
// Rationale:
// - alphabet and digits are pretty obvious
// - '-' and '_' can be used for identifier separators, whichever you prefer.
// - '+', '-', '*', '/', '^' are for arithmetic.
// - '=', '!', '<', '>' are fore comparison.
// - '\' is for builtin string constants, such as \n.
// For other operators, it's generally clearer to use words (such as `and` and `or`.)
matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '+' | '*' | '/' | '\\' | '^' | '!' | '=' | '<' | '>')
}
pub fn parse_ident(p: &mut Parser<'_>) -> NodeKind {
while is_ident(p.current()) {
p.advance();
}
NodeKind::Ident
}
struct List {
head: NodeId,
tail: NodeId,
}
impl List {
fn new() -> Self {
Self {
head: NodeId::NIL,
tail: NodeId::NIL,
}
}
fn append(&mut self, p: &mut Parser<'_>, node: NodeId) {
let node_span = p.ast.get(node).span;
let new_tail = p.alloc(Node {
span: node_span,
kind: NodeKind::List(node, NodeId::NIL),
});
if self.head == NodeId::NIL {
self.head = new_tail;
self.tail = new_tail;
} else {
let old_tail = p.ast.get_mut(self.tail);
let NodeKind::List(expr_before, _) = old_tail.kind else {
return;
};
*old_tail = Node {
span: Span::new(old_tail.span.start, node_span.end),
kind: NodeKind::List(expr_before, new_tail),
};
self.tail = new_tail;
}
}
}
pub fn parse_list(p: &mut Parser<'_>) -> NodeId {
// This could've been a lot simpler if Rust supported tail recursion.
let start = p.position;
p.advance(); // skip past opening parenthesis
skip_whitespace_and_comments(p);
let mut list = List::new();
while p.current() != ')' {
if p.current() == '\0' {
return p.alloc(Node {
span: Span::new(start, p.position),
kind: NodeKind::Error("missing ')' to close '('"),
});
}
let expr = parse_expr(p);
skip_whitespace_and_comments(p);
list.append(p, expr);
}
p.advance(); // skip past closing parenthesis
// If we didn't have any elements, we must not modify the initial Nil with ID 0.
if list.head == NodeId::NIL {
list.head = p.alloc(Node {
span: Span::new(0, 0),
kind: NodeKind::Nil,
});
}
let end = p.position;
p.ast.get_mut(list.head).span = Span::new(start, end);
list.head
}
pub fn parse_expr(p: &mut Parser<'_>) -> NodeId {
let start = p.position;
let kind = match p.current() {
'\0' => NodeKind::Eof,
c if is_decimal_digit(c) => parse_number(p),
// NOTE: Because of the `match` order, this prevents identifiers from starting with a digit.
c if is_ident(c) => parse_ident(p),
'(' => return parse_list(p),
_ => {
p.advance();
NodeKind::Error("unexpected character")
}
};
let end = p.position;
p.alloc(Node {
span: Span::new(start, end),
kind,
})
}
pub fn parse_toplevel(p: &mut Parser<'_>) -> NodeId {
let start = p.position;
let mut nodes = List::new();
skip_whitespace_and_comments(p);
while p.current() != '\0' {
let expr = parse_expr(p);
skip_whitespace_and_comments(p);
nodes.append(p, expr);
}
let end = p.position;
p.alloc(Node {
span: Span::new(start, end),
kind: NodeKind::Toplevel(nodes.head),
})
}
#[cfg(test)]
mod tests {
use core::error::Error;
use alloc::{boxed::Box, string::String};
use super::*;
#[track_caller]
fn parse(
f: fn(&mut Parser<'_>) -> NodeId,
source: &str,
expected: &str,
) -> Result<(), Box<dyn Error>> {
let ast = Ast::new(16);
let mut p = Parser::new(ast, source);
let node = f(&mut p);
let ast = p.ast;
let mut s = String::new();
ast.write(source, node, &mut s, AstWriteMode::Spans)?;
assert_eq!(s, expected);
Ok(())
}
#[test]
fn parse_number() -> Result<(), Box<dyn Error>> {
parse(parse_expr, "123", "123@0..3")?;
parse(parse_expr, "123.456", "123.456@0..7")?;
Ok(())
}
#[test]
fn parse_ident() -> Result<(), Box<dyn Error>> {
parse(parse_expr, "abc", "abc@0..3")?;
parse(parse_expr, "abcABC_01234", "abcABC_01234@0..12")?;
parse(parse_expr, "+-*/\\^!=<>", "+-*/\\^!=<>@0..10")?;
Ok(())
}
#[test]
fn parse_list() -> Result<(), Box<dyn Error>> {
parse(parse_expr, "()", "()@0..2")?;
parse(parse_expr, "(a a)", "(a@1..2 a@3..4)@0..5")?;
parse(parse_expr, "(a a a)", "(a@1..2 a@3..4 a@5..6)@0..7")?;
parse(parse_expr, "(() ())", "(()@1..3 ()@4..6)@0..7")?;
parse(
parse_expr,
"(nestedy (nest OwO))",
"(nestedy@1..8 (nest@10..14 OwO@15..18)@9..19)@0..20",
)?;
Ok(())
}
#[test]
fn oom() -> Result<(), Box<dyn Error>> {
parse(parse_expr, "(a a a a a a a a)", "(a@1..2 a@3..4 a@5..6 a@7..8 a@9..10 a@11..12 a@13..14 . #error(program is too big)@0..0)@0..17")?;
parse(parse_expr, "(a a a a a a a a a)", "(a@1..2 a@3..4 a@5..6 a@7..8 a@9..10 a@11..12 a@13..14 . #error(program is too big)@0..0)@0..19")?;
parse(parse_expr, "(a a a a a a a a a a)", "(a@1..2 a@3..4 a@5..6 a@7..8 a@9..10 a@11..12 a@13..14 . #error(program is too big)@0..0)@0..21")?;
parse(parse_expr, "(a a a a a a a a a a a)", "(a@1..2 a@3..4 a@5..6 a@7..8 a@9..10 a@11..12 a@13..14 . #error(program is too big)@0..0)@0..23")?;
Ok(())
}
#[test]
fn toplevel() -> Result<(), Box<dyn Error>> {
parse(
parse_toplevel,
r#"
(hello world)
(abc)
"#,
"(hello@18..23 world@24..29)@17..30\n(abc@48..51)@47..52@0..65",
)?;
Ok(())
}
}

440
crates/haku/src/system.rs Normal file
View file

@ -0,0 +1,440 @@
use core::{
error::Error,
fmt::{self, Display},
};
use alloc::vec::Vec;
use crate::{
bytecode::Chunk,
value::Value,
vm::{Exception, FnArgs, Vm},
};
pub type SystemFn = fn(&mut Vm, FnArgs) -> Result<Value, Exception>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChunkId(u32);
#[derive(Debug, Clone)]
pub struct System {
/// Resolves a system function name to an index into `fn`s.
pub resolve_fn: fn(&str) -> Option<u8>,
pub fns: [Option<SystemFn>; 256],
pub chunks: Vec<Chunk>,
}
#[derive(Debug, Clone, Copy)]
pub struct SystemImage {
chunks: usize,
}
macro_rules! def_fns {
($($index:tt $name:tt => $fnref:expr),* $(,)?) => {
pub(crate) fn init_fns(system: &mut System) {
$(
debug_assert!(system.fns[$index].is_none());
system.fns[$index] = Some($fnref);
)*
}
pub(crate) fn resolve(name: &str) -> Option<u8> {
match name {
$($name => Some($index),)*
_ => None,
}
}
};
}
impl System {
pub fn new(max_chunks: usize) -> Self {
assert!(max_chunks < u32::MAX as usize);
let mut system = Self {
resolve_fn: Self::resolve,
fns: [None; 256],
chunks: Vec::with_capacity(max_chunks),
};
Self::init_fns(&mut system);
system
}
pub fn add_chunk(&mut self, chunk: Chunk) -> Result<ChunkId, ChunkError> {
if self.chunks.len() >= self.chunks.capacity() {
return Err(ChunkError);
}
let id = ChunkId(self.chunks.len() as u32);
self.chunks.push(chunk);
Ok(id)
}
pub fn chunk(&self, id: ChunkId) -> &Chunk {
&self.chunks[id.0 as usize]
}
pub fn image(&self) -> SystemImage {
SystemImage {
chunks: self.chunks.len(),
}
}
pub fn restore_image(&mut self, image: &SystemImage) {
self.chunks.resize_with(image.chunks, || {
panic!("image must be a subset of the current system")
});
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChunkError;
impl Display for ChunkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("too many chunks")
}
}
impl Error for ChunkError {}
pub mod fns {
use crate::{
value::{Ref, Rgba, Scribble, Shape, Stroke, Value, Vec4},
vm::{Exception, FnArgs, Vm},
};
use super::System;
impl System {
def_fns! {
0x00 "+" => add,
0x01 "-" => sub,
0x02 "*" => mul,
0x03 "/" => div,
0x40 "not" => not,
0x41 "=" => eq,
0x42 "<>" => neq,
0x43 "<" => lt,
0x44 "<=" => leq,
0x45 ">" => gt,
0x46 ">=" => geq,
0x80 "vec" => vec,
0x81 ".x" => vec_x,
0x82 ".y" => vec_y,
0x83 ".z" => vec_z,
0x84 ".w" => vec_w,
0x85 "rgba" => rgba,
0x86 ".r" => rgba_r,
0x87 ".g" => rgba_g,
0x88 ".b" => rgba_b,
0x89 ".a" => rgba_a,
0xc0 "to-shape" => to_shape_f,
0xc1 "stroke" => stroke,
}
}
pub fn add(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
let mut result = 0.0;
for i in 0..args.num() {
result += args.get_number(vm, i, "arguments to (+) must be numbers")?;
}
Ok(Value::Number(result))
}
pub fn sub(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() < 1 {
return Err(vm.create_exception("(-) requires at least one argument to subtract from"));
}
static ERROR: &str = "arguments to (-) must be numbers";
if args.num() == 1 {
Ok(Value::Number(-args.get_number(vm, 0, ERROR)?))
} else {
let mut result = args.get_number(vm, 0, ERROR)?;
for i in 1..args.num() {
result -= args.get_number(vm, i, ERROR)?;
}
Ok(Value::Number(result))
}
}
pub fn mul(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
let mut result = 1.0;
for i in 0..args.num() {
result *= args.get_number(vm, i, "arguments to (*) must be numbers")?;
}
Ok(Value::Number(result))
}
pub fn div(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() < 1 {
return Err(vm.create_exception("(/) requires at least one argument to divide"));
}
static ERROR: &str = "arguments to (/) must be numbers";
let mut result = args.get_number(vm, 0, ERROR)?;
for i in 1..args.num() {
result /= args.get_number(vm, i, ERROR)?;
}
Ok(Value::Number(result))
}
pub fn not(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 1 {
return Err(vm.create_exception("(not) expects a single argument to negate"));
}
let value = args.get(vm, 0);
Ok(Value::from(value.is_falsy()))
}
pub fn eq(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 2 {
return Err(vm.create_exception("(=) expects two arguments to compare"));
}
let a = args.get(vm, 0);
let b = args.get(vm, 1);
Ok(Value::from(a == b))
}
pub fn neq(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 2 {
return Err(vm.create_exception("(<>) expects two arguments to compare"));
}
let a = args.get(vm, 0);
let b = args.get(vm, 1);
Ok(Value::from(a != b))
}
pub fn lt(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 2 {
return Err(vm.create_exception("(<) expects two arguments to compare"));
}
let a = args.get(vm, 0);
let b = args.get(vm, 1);
Ok(Value::from(a < b))
}
pub fn leq(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 2 {
return Err(vm.create_exception("(<=) expects two arguments to compare"));
}
let a = args.get(vm, 0);
let b = args.get(vm, 1);
Ok(Value::from(a <= b))
}
pub fn gt(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 2 {
return Err(vm.create_exception("(>) expects two arguments to compare"));
}
let a = args.get(vm, 0);
let b = args.get(vm, 1);
Ok(Value::from(a > b))
}
pub fn geq(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 2 {
return Err(vm.create_exception("(>=) expects two arguments to compare"));
}
let a = args.get(vm, 0);
let b = args.get(vm, 1);
Ok(Value::from(a >= b))
}
pub fn vec(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
static ERROR: &str = "arguments to (vec) must be numbers (vec x y z w)";
match args.num() {
0 => Ok(Value::Vec4(Vec4 {
x: 0.0,
y: 0.0,
z: 0.0,
w: 0.0,
})),
1 => {
let x = args.get_number(vm, 0, ERROR)?;
Ok(Value::Vec4(Vec4 {
x,
y: 0.0,
z: 0.0,
w: 0.0,
}))
}
2 => {
let x = args.get_number(vm, 0, ERROR)?;
let y = args.get_number(vm, 1, ERROR)?;
Ok(Value::Vec4(Vec4 {
x,
y,
z: 0.0,
w: 0.0,
}))
}
3 => {
let x = args.get_number(vm, 0, ERROR)?;
let y = args.get_number(vm, 1, ERROR)?;
let z = args.get_number(vm, 2, ERROR)?;
Ok(Value::Vec4(Vec4 { x, y, z, w: 0.0 }))
}
4 => {
let x = args.get_number(vm, 0, ERROR)?;
let y = args.get_number(vm, 1, ERROR)?;
let z = args.get_number(vm, 2, ERROR)?;
let w = args.get_number(vm, 3, ERROR)?;
Ok(Value::Vec4(Vec4 { x, y, z, w }))
}
_ => Err(vm.create_exception("(vec) expects 0-4 arguments (vec x y z w)")),
}
}
pub fn vec_x(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 1 {
return Err(vm.create_exception("(.x) expects a single argument (.x vec)"));
}
let vec = args.get_vec4(vm, 0, "argument to (.x vec) must be a (vec)")?;
Ok(Value::Number(vec.x))
}
pub fn vec_y(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 1 {
return Err(vm.create_exception("(.y) expects a single argument (.y vec)"));
}
let vec = args.get_vec4(vm, 0, "argument to (.y vec) must be a (vec)")?;
Ok(Value::Number(vec.y))
}
pub fn vec_z(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 1 {
return Err(vm.create_exception("(.z) expects a single argument (.z vec)"));
}
let vec = args.get_vec4(vm, 0, "argument to (.z vec) must be a (vec)")?;
Ok(Value::Number(vec.z))
}
pub fn vec_w(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 1 {
return Err(vm.create_exception("(.w) expects a single argument (.w vec)"));
}
let vec = args.get_vec4(vm, 0, "argument to (.w vec) must be a (vec)")?;
Ok(Value::Number(vec.w))
}
pub fn rgba(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 4 {
return Err(vm.create_exception("(rgba) expects four arguments (rgba r g b a)"));
}
static ERROR: &str = "arguments to (rgba r g b a) must be numbers";
let r = args.get_number(vm, 0, ERROR)?;
let g = args.get_number(vm, 1, ERROR)?;
let b = args.get_number(vm, 2, ERROR)?;
let a = args.get_number(vm, 3, ERROR)?;
Ok(Value::Rgba(Rgba { r, g, b, a }))
}
pub fn rgba_r(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 1 {
return Err(vm.create_exception("(.r) expects a single argument (.r rgba)"));
}
let rgba = args.get_rgba(vm, 0, "argument to (.r rgba) must be an (rgba)")?;
Ok(Value::Number(rgba.r))
}
pub fn rgba_g(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 1 {
return Err(vm.create_exception("(.g) expects a single argument (.g rgba)"));
}
let rgba = args.get_rgba(vm, 0, "argument to (.g rgba) must be an (rgba)")?;
Ok(Value::Number(rgba.g))
}
pub fn rgba_b(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 1 {
return Err(vm.create_exception("(.b) expects a single argument (.b rgba)"));
}
let rgba = args.get_rgba(vm, 0, "argument to (.b rgba) must be an (rgba)")?;
Ok(Value::Number(rgba.r))
}
pub fn rgba_a(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 1 {
return Err(vm.create_exception("(.a) expects a single argument (.a rgba)"));
}
let rgba = args.get_rgba(vm, 0, "argument to (.a rgba) must be an (rgba)")?;
Ok(Value::Number(rgba.r))
}
fn to_shape(value: Value, _vm: &Vm) -> Option<Shape> {
match value {
Value::Nil
| Value::False
| Value::True
| Value::Number(_)
| Value::Rgba(_)
| Value::Ref(_) => None,
Value::Vec4(vec) => Some(Shape::Point(vec)),
}
}
pub fn to_shape_f(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 1 {
return Err(vm.create_exception("(shape) expects 1 argument (shape value)"));
}
if let Some(shape) = to_shape(args.get(vm, 0), vm) {
let id = vm.create_ref(Ref::Shape(shape))?;
Ok(Value::Ref(id))
} else {
Ok(Value::Nil)
}
}
pub fn stroke(vm: &mut Vm, args: FnArgs) -> Result<Value, Exception> {
if args.num() != 3 {
return Err(
vm.create_exception("(stroke) expects 3 arguments (stroke thickness color shape)")
);
}
let thickness = args.get_number(
vm,
0,
"1st argument to (stroke) must be a thickness in pixels (number)",
)?;
let color = args.get_rgba(vm, 1, "2nd argument to (stroke) must be a color (rgba)")?;
if let Some(shape) = to_shape(args.get(vm, 2), vm) {
let id = vm.create_ref(Ref::Scribble(Scribble::Stroke(Stroke {
thickness,
color,
shape,
})))?;
Ok(Value::Ref(id))
} else {
Ok(Value::Nil)
}
}
}

161
crates/haku/src/value.rs Normal file
View file

@ -0,0 +1,161 @@
use alloc::vec::Vec;
use crate::system::ChunkId;
// TODO: Probably needs some pretty hardcore space optimization.
// Maybe when we have static typing.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum Value {
Nil,
False,
True,
Number(f32),
Vec4(Vec4),
Rgba(Rgba),
Ref(RefId),
}
impl Value {
pub fn is_falsy(&self) -> bool {
matches!(self, Self::Nil | Self::False)
}
pub fn is_truthy(&self) -> bool {
!self.is_falsy()
}
pub fn to_number(&self) -> Option<f32> {
match self {
Self::Number(v) => Some(*v),
_ => None,
}
}
pub fn to_vec4(&self) -> Option<Vec4> {
match self {
Self::Vec4(v) => Some(*v),
_ => None,
}
}
pub fn to_rgba(&self) -> Option<Rgba> {
match self {
Self::Rgba(v) => Some(*v),
_ => None,
}
}
}
impl From<()> for Value {
fn from(_: ()) -> Self {
Self::Nil
}
}
impl From<bool> for Value {
fn from(value: bool) -> Self {
match value {
true => Self::True,
false => Self::False,
}
}
}
impl From<f32> for Value {
fn from(value: f32) -> Self {
Self::Number(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
pub struct Vec4 {
pub x: f32,
pub y: f32,
pub z: f32,
pub w: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
#[repr(C)]
pub struct Rgba {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
// NOTE: This is not a pointer, because IDs are safer and easier to clone.
//
// Since this only ever refers to refs inside the current VM, there is no need to walk through all
// the values and update pointers when a VM is cloned.
//
// This ensures it's quick and easy to spin up a new VM from an existing image, as well as being
// extremely easy to serialize a VM image into a file for quick loading back later.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RefId(pub(crate) u32);
impl RefId {
// DO NOT USE outside tests!
#[doc(hidden)]
pub fn from_u32(x: u32) -> Self {
Self(x)
}
}
#[derive(Debug, Clone)]
pub enum Ref {
Closure(Closure),
Shape(Shape),
Scribble(Scribble),
}
impl Ref {
pub fn as_closure(&self) -> Option<&Closure> {
match self {
Self::Closure(v) => Some(v),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct BytecodeLoc {
pub chunk_id: ChunkId,
pub offset: u16,
}
#[derive(Debug, Clone, Copy)]
pub struct BytecodeSpan {
pub loc: BytecodeLoc,
pub len: u16,
}
#[derive(Debug, Clone, Copy)]
pub enum FunctionName {
Anonymous,
}
#[derive(Debug, Clone)]
pub struct Closure {
pub start: BytecodeLoc,
pub name: FunctionName,
pub param_count: u8,
pub captures: Vec<Value>,
}
#[derive(Debug, Clone)]
pub enum Shape {
Point(Vec4),
}
#[derive(Debug, Clone)]
pub struct Stroke {
pub thickness: f32,
pub color: Rgba,
pub shape: Shape,
}
#[derive(Debug, Clone)]
pub enum Scribble {
Stroke(Stroke),
}

486
crates/haku/src/vm.rs Normal file
View file

@ -0,0 +1,486 @@
use core::{
error::Error,
fmt::{self, Display},
iter,
};
use alloc::vec::Vec;
use crate::{
bytecode::{self, Defs, Opcode, CAPTURE_CAPTURE, CAPTURE_LOCAL},
system::{ChunkId, System},
value::{BytecodeLoc, Closure, FunctionName, Ref, RefId, Rgba, Value, Vec4},
};
pub struct VmLimits {
pub stack_capacity: usize,
pub call_stack_capacity: usize,
pub ref_capacity: usize,
pub fuel: usize,
}
#[derive(Debug, Clone)]
pub struct Vm {
stack: Vec<Value>,
call_stack: Vec<CallFrame>,
refs: Vec<Ref>,
defs: Vec<Value>,
fuel: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct VmImage {
refs: usize,
defs: usize,
fuel: usize,
}
#[derive(Debug, Clone)]
struct CallFrame {
closure_id: RefId,
chunk_id: ChunkId,
pc: usize,
bottom: usize,
}
struct Context {
fuel: usize,
}
impl Vm {
pub fn new(defs: &Defs, limits: &VmLimits) -> Self {
Self {
stack: Vec::with_capacity(limits.stack_capacity),
call_stack: Vec::with_capacity(limits.call_stack_capacity),
refs: Vec::with_capacity(limits.ref_capacity),
defs: Vec::from_iter(iter::repeat(Value::Nil).take(defs.len() as usize)),
fuel: limits.fuel,
}
}
pub fn remaining_fuel(&self) -> usize {
self.fuel
}
pub fn set_fuel(&mut self, fuel: usize) {
self.fuel = fuel;
}
pub fn image(&self) -> VmImage {
assert!(
self.stack.is_empty() && self.call_stack.is_empty(),
"cannot image VM while running code"
);
VmImage {
refs: self.refs.len(),
defs: self.defs.len(),
fuel: self.fuel,
}
}
pub fn restore_image(&mut self, image: &VmImage) {
assert!(
self.stack.is_empty() && self.call_stack.is_empty(),
"cannot restore VM image while running code"
);
self.refs.resize_with(image.refs, || {
panic!("image must be a subset of the current VM")
});
self.defs.resize_with(image.defs, || {
panic!("image must be a subset of the current VM")
});
self.fuel = image.fuel;
}
pub fn apply_defs(&mut self, defs: &Defs) {
assert!(
defs.len() as usize >= self.defs.len(),
"defs must be a superset of the current VM"
);
self.defs.resize(defs.len() as usize, Value::Nil);
}
fn push(&mut self, value: Value) -> Result<(), Exception> {
if self.stack.len() >= self.stack.capacity() {
// TODO: can this error message be made clearer?
return Err(self.create_exception("too many local variables"));
}
self.stack.push(value);
Ok(())
}
fn get(&mut self, index: usize) -> Result<Value, Exception> {
self.stack.get(index).copied().ok_or_else(|| {
self.create_exception("corrupted bytecode (local variable out of bounds)")
})
}
fn pop(&mut self) -> Result<Value, Exception> {
self.stack
.pop()
.ok_or_else(|| self.create_exception("corrupted bytecode (value stack underflow)"))
}
fn push_call(&mut self, frame: CallFrame) -> Result<(), Exception> {
if self.call_stack.len() >= self.call_stack.capacity() {
return Err(self.create_exception("too much recursion"));
}
self.call_stack.push(frame);
Ok(())
}
fn pop_call(&mut self) -> Result<CallFrame, Exception> {
self.call_stack
.pop()
.ok_or_else(|| self.create_exception("corrupted bytecode (call stack underflow)"))
}
pub fn run(&mut self, system: &System, mut closure_id: RefId) -> Result<Value, Exception> {
let closure = self
.get_ref(closure_id)
.as_closure()
.expect("a Closure-type Ref must be passed to `run`");
let mut chunk_id = closure.start.chunk_id;
let mut chunk = system.chunk(chunk_id);
let mut pc = closure.start.offset as usize;
let mut bottom = self.stack.len();
let mut fuel = self.fuel;
#[allow(unused)]
let closure = (); // Do not use `closure` after this! Use `get_ref` on `closure_id` instead.
self.push_call(CallFrame {
closure_id,
chunk_id,
pc,
bottom,
})?;
loop {
fuel = fuel
.checked_sub(1)
.ok_or_else(|| self.create_exception("code ran for too long"))?;
let opcode = chunk.read_opcode(&mut pc)?;
match opcode {
Opcode::Nil => self.push(Value::Nil)?,
Opcode::False => self.push(Value::False)?,
Opcode::True => self.push(Value::True)?,
Opcode::Number => {
let x = chunk.read_f32(&mut pc)?;
self.push(Value::Number(x))?;
}
Opcode::Local => {
let index = chunk.read_u8(&mut pc)? as usize;
let value = self.get(bottom + index)?;
self.push(value)?;
}
Opcode::Capture => {
let index = chunk.read_u8(&mut pc)? as usize;
let closure = self.get_ref(closure_id).as_closure().unwrap();
self.push(closure.captures.get(index).copied().ok_or_else(|| {
self.create_exception("corrupted bytecode (capture index out of bounds)")
})?)?;
}
Opcode::Def => {
let index = chunk.read_u16(&mut pc)? as usize;
self.push(self.defs.get(index).copied().ok_or_else(|| {
self.create_exception("corrupted bytecode (def index out of bounds)")
})?)?
}
Opcode::SetDef => {
let index = chunk.read_u16(&mut pc)? as usize;
let value = self.pop()?;
if let Some(def) = self.defs.get_mut(index) {
*def = value;
} else {
return Err(self
.create_exception("corrupted bytecode (set def index out of bounds)"));
}
}
Opcode::DropLet => {
let count = chunk.read_u8(&mut pc)? as usize;
if count != 0 {
let new_len = self.stack.len().checked_sub(count).ok_or_else(|| {
self.create_exception(
"corrupted bytecode (Drop tried to drop too many values off the stack)",
)
})?;
let value = self.pop()?;
self.stack.resize_with(new_len, || unreachable!());
self.push(value)?;
}
}
Opcode::Function => {
let param_count = chunk.read_u8(&mut pc)?;
let then = chunk.read_u16(&mut pc)? as usize;
let body = pc;
pc = then;
let capture_count = chunk.read_u8(&mut pc)? as usize;
let mut captures = Vec::with_capacity(capture_count);
for _ in 0..capture_count {
let capture_kind = chunk.read_u8(&mut pc)?;
let index = chunk.read_u8(&mut pc)? as usize;
captures.push(match capture_kind {
CAPTURE_LOCAL => self.get(bottom + index)?,
CAPTURE_CAPTURE => {
let closure = self.get_ref(closure_id).as_closure().unwrap();
closure.captures.get(index).copied().ok_or_else(|| {
self.create_exception(
"corrupted bytecode (captured capture index out of bounds)",
)
})?
}
_ => Value::Nil,
})
}
let id = self.create_ref(Ref::Closure(Closure {
start: BytecodeLoc {
chunk_id,
offset: body as u16,
},
name: FunctionName::Anonymous,
param_count,
captures,
}))?;
self.push(Value::Ref(id))?;
}
Opcode::Jump => {
let offset = chunk.read_u16(&mut pc)? as usize;
pc = offset;
}
Opcode::JumpIfNot => {
let offset = chunk.read_u16(&mut pc)? as usize;
let value = self.pop()?;
if !value.is_truthy() {
pc = offset;
}
}
Opcode::Call => {
let argument_count = chunk.read_u8(&mut pc)? as usize;
let function_value = self.pop()?;
let Some((called_closure_id, Ref::Closure(closure))) =
self.get_ref_value(function_value)
else {
return Err(self.create_exception("attempt to call non-function value"));
};
// TODO: Varargs?
if argument_count != closure.param_count as usize {
// Would be nice if we told the user the exact counts.
return Err(self.create_exception("function parameter count mismatch"));
}
let frame = CallFrame {
closure_id,
chunk_id,
pc,
bottom,
};
closure_id = called_closure_id;
chunk_id = closure.start.chunk_id;
chunk = system.chunk(chunk_id);
pc = closure.start.offset as usize;
bottom = self
.stack
.len()
.checked_sub(argument_count)
.ok_or_else(|| {
self.create_exception(
"corrupted bytecode (not enough values on the stack for arguments)",
)
})?;
self.push_call(frame)?;
}
Opcode::System => {
let index = chunk.read_u8(&mut pc)? as usize;
let argument_count = chunk.read_u8(&mut pc)? as usize;
let system_fn = system.fns.get(index).copied().flatten().ok_or_else(|| {
self.create_exception("corrupted bytecode (invalid system function index)")
})?;
self.store_context(Context { fuel });
let result = system_fn(
self,
FnArgs {
base: self
.stack
.len()
.checked_sub(argument_count)
.ok_or_else(|| self.create_exception("corrupted bytecode (not enough values on the stack for arguments)"))?,
len: argument_count,
},
)?;
Context { fuel } = self.restore_context();
self.stack
.resize_with(self.stack.len() - argument_count, || unreachable!());
self.push(result)?;
}
Opcode::Return => {
let value = self.pop()?;
let frame = self.pop_call()?;
debug_assert!(bottom <= self.stack.len());
self.stack.resize_with(bottom, || unreachable!());
self.push(value)?;
// Once the initial frame is popped, halt the VM.
if self.call_stack.is_empty() {
self.store_context(Context { fuel });
break;
}
CallFrame {
closure_id,
chunk_id,
pc,
bottom,
} = frame;
chunk = system.chunk(chunk_id);
}
}
}
Ok(self
.stack
.pop()
.expect("there should be a result at the top of the stack"))
}
fn store_context(&mut self, context: Context) {
self.fuel = context.fuel;
}
fn restore_context(&mut self) -> Context {
Context { fuel: self.fuel }
}
pub fn create_ref(&mut self, r: Ref) -> Result<RefId, Exception> {
if self.refs.len() >= self.refs.capacity() {
return Err(self.create_exception("too many value allocations"));
}
let id = RefId(self.refs.len() as u32);
self.refs.push(r);
Ok(id)
}
pub fn get_ref(&self, id: RefId) -> &Ref {
&self.refs[id.0 as usize]
}
pub fn get_ref_value(&self, value: Value) -> Option<(RefId, &Ref)> {
match value {
Value::Ref(id) => Some((id, self.get_ref(id))),
_ => None,
}
}
pub fn create_exception(&self, message: &'static str) -> Exception {
Exception { message }
}
}
pub struct FnArgs {
base: usize,
len: usize,
}
impl FnArgs {
pub fn num(&self) -> usize {
self.len
}
pub fn try_get(&self, vm: &Vm, index: usize) -> Option<Value> {
if index < self.len {
Some(vm.stack[self.base + index])
} else {
None
}
}
// The following are #[inline(never)] wrappers for common operations to reduce code size.
#[inline(never)]
pub fn get(&self, vm: &Vm, index: usize) -> Value {
self.try_get(vm, index)
.expect("argument was expected, but got None")
}
#[inline(never)]
pub fn get_number(
&self,
vm: &Vm,
index: usize,
message: &'static str,
) -> Result<f32, Exception> {
self.get(vm, index)
.to_number()
.ok_or_else(|| vm.create_exception(message))
}
#[inline(never)]
pub fn get_vec4(
&self,
vm: &Vm,
index: usize,
message: &'static str,
) -> Result<Vec4, Exception> {
self.get(vm, index)
.to_vec4()
.ok_or_else(|| vm.create_exception(message))
}
#[inline(never)]
pub fn get_rgba(
&self,
vm: &Vm,
index: usize,
message: &'static str,
) -> Result<Rgba, Exception> {
self.get(vm, index)
.to_rgba()
.ok_or_else(|| vm.create_exception(message))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Exception {
pub message: &'static str,
}
impl From<bytecode::ReadError> for Exception {
fn from(_: bytecode::ReadError) -> Self {
Self {
message: "corrupted bytecode",
}
}
}
impl Display for Exception {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// NOTE: This is not a user-friendly representation!
write!(f, "{self:#?}")
}
}
impl Error for Exception {}

View file

@ -0,0 +1,256 @@
use std::error::Error;
use haku::{
bytecode::{Chunk, Defs},
compiler::{compile_expr, Compiler, Source},
sexp::{self, Ast, Parser},
system::System,
value::{BytecodeLoc, Closure, FunctionName, Ref, RefId, Value},
vm::{Vm, VmLimits},
};
fn eval(code: &str) -> Result<Value, Box<dyn Error>> {
let mut system = System::new(1);
let ast = Ast::new(1024);
let mut parser = Parser::new(ast, code);
let root = sexp::parse_toplevel(&mut parser);
let ast = parser.ast;
let src = Source {
code,
ast: &ast,
system: &system,
};
let mut defs = Defs::new(256);
let mut chunk = Chunk::new(65536).unwrap();
let mut compiler = Compiler::new(&mut defs, &mut chunk);
compile_expr(&mut compiler, &src, root)?;
let defs = compiler.defs;
for diagnostic in &compiler.diagnostics {
println!(
"{}..{}: {}",
diagnostic.span.start, diagnostic.span.end, diagnostic.message
);
}
if !compiler.diagnostics.is_empty() {
panic!("compiler diagnostics were emitted")
}
let limits = VmLimits {
stack_capacity: 256,
call_stack_capacity: 256,
ref_capacity: 256,
fuel: 32768,
};
let mut vm = Vm::new(defs, &limits);
let chunk_id = system.add_chunk(chunk)?;
println!("bytecode: {:?}", system.chunk(chunk_id));
let closure = vm.create_ref(Ref::Closure(Closure {
start: BytecodeLoc {
chunk_id,
offset: 0,
},
name: FunctionName::Anonymous,
param_count: 0,
captures: Vec::new(),
}))?;
let result = vm.run(&system, closure)?;
println!("used fuel: {}", limits.fuel - vm.remaining_fuel());
Ok(result)
}
#[track_caller]
fn expect_number(code: &str, number: f32, epsilon: f32) {
match eval(code) {
Ok(Value::Number(n)) => assert!((n - number).abs() < epsilon, "expected {number}, got {n}"),
other => panic!("expected ok/numeric result, got {other:?}"),
}
}
#[test]
fn literal_nil() {
assert_eq!(eval("()").unwrap(), Value::Nil);
}
#[test]
fn literal_number() {
expect_number("123", 123.0, 0.0001);
}
#[test]
fn literal_bool() {
assert_eq!(eval("false").unwrap(), Value::False);
assert_eq!(eval("true").unwrap(), Value::True);
}
#[test]
fn function_nil() {
assert_eq!(eval("(fn () ())").unwrap(), Value::Ref(RefId::from_u32(1)));
}
#[test]
fn function_nil_call() {
assert_eq!(eval("((fn () ()))").unwrap(), Value::Nil);
}
#[test]
fn function_arithmetic() {
expect_number("((fn (x) (+ x 2)) 2)", 4.0, 0.0001);
}
#[test]
fn function_let() {
expect_number("((fn (add-two) (add-two 2)) (fn (x) (+ x 2)))", 4.0, 0.0001);
}
#[test]
fn function_closure() {
expect_number("(((fn (x) (fn (y) (+ x y))) 2) 2)", 4.0, 0.0001);
}
#[test]
fn if_literal() {
expect_number("(if 1 1 2)", 1.0, 0.0001);
expect_number("(if () 1 2)", 2.0, 0.0001);
expect_number("(if false 1 2)", 2.0, 0.0001);
expect_number("(if true 1 2)", 1.0, 0.0001);
}
#[test]
fn def_simple() {
let code = r#"
(def x 1)
(def y 2)
(+ x y)
"#;
expect_number(code, 3.0, 0.0001);
}
#[test]
fn def_fib_recursive() {
let code = r#"
(def fib
(fn (n)
(if (< n 2)
n
(+ (fib (- n 1)) (fib (- n 2))))))
(fib 10)
"#;
expect_number(code, 55.0, 0.0001);
}
#[test]
fn def_mutually_recursive() {
let code = r#"
(def f
(fn (x)
(if (< x 10)
(g (+ x 1))
x)))
(def g
(fn (x)
(if (< x 10)
(f (* x 2))
x)))
(f 0)
"#;
expect_number(code, 14.0, 0.0001);
}
#[test]
fn let_single() {
let code = r#"
(let ((x 1))
(+ x 1))
"#;
expect_number(code, 2.0, 0.0001);
}
#[test]
fn let_many() {
let code = r#"
(let ((x 1)
(y 2))
(+ x y))
"#;
expect_number(code, 3.0, 0.0001);
}
#[test]
fn let_sequence() {
let code = r#"
(let ((x 1)
(y (+ x 1)))
(+ x y))
"#;
expect_number(code, 3.0, 0.0001);
}
#[test]
fn let_subexpr() {
let code = r#"
(+
(let ((x 1)
(y 2))
(* x y)))
"#;
expect_number(code, 2.0, 0.0001);
}
#[test]
fn let_empty() {
let code = r#"
(let () 1)
"#;
expect_number(code, 1.0, 0.0001);
}
#[test]
fn let_subexpr_empty() {
let code = r#"
(+ (let () 1) (let () 1))
"#;
expect_number(code, 2.0, 0.0001);
}
#[test]
fn let_subexpr_many() {
let code = r#"
(+
(let ((x 1)
(y 2))
(* x y))
(let () 1)
(let ((x 1)) x))
"#;
expect_number(code, 3.0, 0.0001);
}
#[test]
fn system_arithmetic() {
expect_number("(+ 1 2 3 4)", 10.0, 0.0001);
expect_number("(+ (* 2 1) 1 (/ 6 2) (- 10 3))", 13.0, 0.0001);
}
#[test]
fn practical_fib_recursive() {
let code = r#"
((fn (fib)
(fib fib 10))
(fn (fib n)
(if (< n 2)
n
(+ (fib fib (- n 1)) (fib fib (- n 2))))))
"#;
expect_number(code, 55.0, 0.0001);
}

18
static/index.html Normal file
View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>canvane</title>
<script src="static/index.js" type="module"></script>
<script src="static/live-reload.js" type="module"></script>
</head>
<body>
<main>
<canvas id="render" width="256" height="256">Please enable JavaScript</canvas>
<br>
<textarea id="code" cols="80" rows="25">(stroke 1 (rgba 0 0 0 255) (vec 32 32))</textarea>
<p id="output" style="white-space: pre-wrap;"></p>
</main>
</body>
</html>

154
static/index.js Normal file
View file

@ -0,0 +1,154 @@
let panicImpl;
let logImpl;
function makeLogFunction(level) {
return (length, pMessage) => {
logImpl(level, length, pMessage);
};
}
let { instance: hakuInstance, module: hakuModule } = await WebAssembly.instantiateStreaming(
fetch(import.meta.resolve("./wasm/haku.wasm")),
{
env: {
panic(length, pMessage) {
panicImpl(length, pMessage);
},
trace: makeLogFunction("trace"),
debug: makeLogFunction("debug"),
info: makeLogFunction("info"),
warn: makeLogFunction("warn"),
error: makeLogFunction("error"),
},
},
);
let memory = hakuInstance.exports.memory;
let w = hakuInstance.exports;
let textEncoder = new TextEncoder();
function allocString(string) {
let size = string.length * 3;
let align = 1;
let pString = w.haku_alloc(size, align);
let buffer = new Uint8Array(memory.buffer, pString, size);
let result = textEncoder.encodeInto(string, buffer);
return {
ptr: pString,
length: result.written,
size,
align,
};
}
function freeString(alloc) {
w.haku_free(alloc.ptr, alloc.size, alloc.align);
}
let textDecoder = new TextDecoder();
function readString(size, pString) {
let buffer = new Uint8Array(memory.buffer, pString, size);
return textDecoder.decode(buffer);
}
function readCString(pCString) {
let memoryBuffer = new Uint8Array(memory.buffer);
let pCursor = pCString;
while (memoryBuffer[pCursor] != 0 && memoryBuffer[pCursor] != null) {
pCursor++;
}
let size = pCursor - pCString;
return readString(size, pCString);
}
class Panic extends Error {
name = "Panic";
}
panicImpl = (length, pMessage) => {
throw new Panic(readString(length, pMessage));
};
logImpl = (level, length, pMessage) => {
console[level](readString(length, pMessage));
};
w.haku_init_logging();
/* ------ */
let renderCanvas = document.getElementById("render");
let codeTextArea = document.getElementById("code");
let outputP = document.getElementById("output");
let ctx = renderCanvas.getContext("2d");
function rerender() {
console.log("rerender");
let width = renderCanvas.width;
let height = renderCanvas.height;
let logs = [];
let pInstance = w.haku_instance_new();
let pBrush = w.haku_brush_new();
let pBitmap = w.haku_bitmap_new(width, height);
let code = allocString(codeTextArea.value);
let deallocEverything = () => {
freeString(code);
w.haku_bitmap_destroy(pBitmap);
w.haku_brush_destroy(pBrush);
w.haku_instance_destroy(pInstance);
outputP.textContent = logs.join("\n");
};
let compileStatusCode = w.haku_compile_brush(pInstance, pBrush, code.length, code.ptr);
let pCompileStatusString = w.haku_status_string(compileStatusCode);
logs.push(`compile: ${readCString(pCompileStatusString)}`);
for (let i = 0; i < w.haku_num_diagnostics(pBrush); ++i) {
let start = w.haku_diagnostic_start(pBrush, i);
let end = w.haku_diagnostic_end(pBrush, i);
let length = w.haku_diagnostic_message_len(pBrush, i);
let pMessage = w.haku_diagnostic_message(pBrush, i);
let message = readString(length, pMessage);
logs.push(`${start}..${end}: ${message}`);
}
if (w.haku_num_diagnostics(pBrush) > 0) {
deallocEverything();
return;
}
let renderStatusCode = w.haku_render_brush(pInstance, pBrush, pBitmap);
let pRenderStatusString = w.haku_status_string(renderStatusCode);
logs.push(`render: ${readCString(pRenderStatusString)}`);
if (w.haku_has_exception(pInstance)) {
let length = w.haku_exception_message_len(pInstance);
let pMessage = w.haku_exception_message(pInstance);
let message = readString(length, pMessage);
logs.push(`exception: ${message}`);
deallocEverything();
return;
}
let pBitmapData = w.haku_bitmap_data(pBitmap);
let bitmapDataBuffer = new Float32Array(memory.buffer, pBitmapData, width * height * 4);
let imageData = new ImageData(width, height);
for (let i = 0; i < bitmapDataBuffer.length; ++i) {
imageData.data[i] = bitmapDataBuffer[i] * 255;
}
ctx.putImageData(imageData, 0, 0);
deallocEverything();
}
rerender();
codeTextArea.addEventListener("input", rerender);

16
static/live-reload.js Normal file
View file

@ -0,0 +1,16 @@
// NOTE: The server never fulfills this request, it stalls forever.
// Once the connection is closed, we try to connect with the server until we establish a successful
// connection. Then we reload the page.
await fetch("/dev/live-reload/stall").catch(async () => {
while (true) {
try {
let response = await fetch("/dev/live-reload/back-up");
if (response.status == 200) {
window.location.reload();
break;
}
} catch (e) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
});