initial commit
This commit is contained in:
		
						commit
						caec0b8ac9
					
				
					 27 changed files with 4786 additions and 0 deletions
				
			
		
							
								
								
									
										15
									
								
								.editorconfig
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.editorconfig
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
/target
 | 
			
		||||
							
								
								
									
										1058
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1058
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										15
									
								
								Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Cargo.toml
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										5
									
								
								Justfile
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										15
									
								
								crates/canvane/Cargo.toml
									
										
									
									
									
										Normal 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"] }
 | 
			
		||||
							
								
								
									
										23
									
								
								crates/canvane/src/live_reload.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								crates/canvane/src/live_reload.rs
									
										
									
									
									
										Normal 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()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								crates/canvane/src/main.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								crates/canvane/src/main.rs
									
										
									
									
									
										Normal 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");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								crates/haku-cli/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								crates/haku-cli/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "haku-cli"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
haku.workspace = true
 | 
			
		||||
							
								
								
									
										91
									
								
								crates/haku-cli/src/main.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								crates/haku-cli/src/main.rs
									
										
									
									
									
										Normal 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(())
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								crates/haku-wasm/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								crates/haku-wasm/Cargo.toml
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										349
									
								
								crates/haku-wasm/src/lib.rs
									
										
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								crates/haku-wasm/src/logging.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								crates/haku-wasm/src/logging.rs
									
										
									
									
									
										Normal 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");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								crates/haku-wasm/src/panicking.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								crates/haku-wasm/src/panicking.rs
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										6
									
								
								crates/haku/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "haku"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
							
								
								
									
										266
									
								
								crates/haku/src/bytecode.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								crates/haku/src/bytecode.rs
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										625
									
								
								crates/haku/src/compiler.rs
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										11
									
								
								crates/haku/src/lib.rs
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										144
									
								
								crates/haku/src/render.rs
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										476
									
								
								crates/haku/src/sexp.rs
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										440
									
								
								crates/haku/src/system.rs
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										161
									
								
								crates/haku/src/value.rs
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										486
									
								
								crates/haku/src/vm.rs
									
										
									
									
									
										Normal 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 {}
 | 
			
		||||
							
								
								
									
										256
									
								
								crates/haku/tests/language.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								crates/haku/tests/language.rs
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										18
									
								
								static/index.html
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										154
									
								
								static/index.js
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										16
									
								
								static/live-reload.js
									
										
									
									
									
										Normal 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));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue