initial commit
This commit is contained in:
commit
caec0b8ac9
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…
Reference in a new issue