stack traces in the brush editor

after 35 thousand years it's finally here
good erro message
This commit is contained in:
りき萌 2025-06-25 20:51:34 +02:00
parent c1612b2a94
commit e49885c83a
11 changed files with 710 additions and 150 deletions

View file

@ -2,7 +2,7 @@
extern crate alloc; extern crate alloc;
use core::{alloc::Layout, mem, slice}; use core::{alloc::Layout, mem, ptr, slice};
use alloc::{boxed::Box, string::String, vec::Vec}; use alloc::{boxed::Box, string::String, vec::Vec};
use haku::{ use haku::{
@ -12,7 +12,7 @@ use haku::{
diagnostic::Diagnostic, diagnostic::Diagnostic,
lexer::{lex, Lexer}, lexer::{lex, Lexer},
parser::{self, IntoAstError, Parser}, parser::{self, IntoAstError, Parser},
source::SourceCode, source::{SourceCode, Span},
token::Lexis, token::Lexis,
}; };
use log::{debug, info}; use log::{debug, info};
@ -449,6 +449,22 @@ unsafe extern "C" fn haku_compile_brush(
); );
debug!("compiling: {closure_spec:?}"); debug!("compiling: {closure_spec:?}");
debug!("bytecode: {:?}", chunk.bytecode);
{
let mut cursor = 0_usize;
for info in &chunk.span_info {
let slice = &chunk.bytecode[cursor..cursor + info.len as usize];
debug!(
"{:?} | 0x{:x} {:?} | {:?}",
info.span,
cursor,
slice,
info.span.slice(src.code),
);
cursor += info.len as usize;
}
}
instance.compile_result2 = Some(CompileResult { instance.compile_result2 = Some(CompileResult {
defs_string: instance.defs.serialize_defs(), defs_string: instance.defs.serialize_defs(),
tags_string: instance.defs.serialize_tags(), tags_string: instance.defs.serialize_tags(),
@ -528,6 +544,25 @@ unsafe extern "C" fn haku_bytecode(instance: *const Instance) -> *const u8 {
unwrap_compile_result(instance).chunk.bytecode.as_ptr() unwrap_compile_result(instance).chunk.bytecode.as_ptr()
} }
#[unsafe(no_mangle)]
unsafe extern "C" fn haku_bytecode_find_span(instance: *const Instance, pc: u16) -> *const Span {
let chunk = &unwrap_compile_result(instance).chunk;
match chunk.find_span(pc) {
Some(r) => r,
None => ptr::null(),
}
}
#[unsafe(no_mangle)]
unsafe extern "C" fn haku_span_start(span: *const Span) -> u32 {
(*span).start
}
#[unsafe(no_mangle)]
unsafe extern "C" fn haku_span_end(span: *const Span) -> u32 {
(*span).end
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
unsafe extern "C" fn haku_local_count(instance: *const Instance) -> u8 { unsafe extern "C" fn haku_local_count(instance: *const Instance) -> u8 {
unwrap_compile_result(instance).closure_spec.local_count unwrap_compile_result(instance).closure_spec.local_count

View file

@ -1,10 +1,9 @@
use core::{ use core::fmt::{self, Display};
fmt::{self, Display},
mem::transmute,
};
use alloc::{borrow::ToOwned, string::String, vec::Vec}; use alloc::{borrow::ToOwned, string::String, vec::Vec};
use crate::source::Span;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)] #[repr(u8)]
pub enum Opcode { pub enum Opcode {
@ -56,6 +55,14 @@ pub const CAPTURE_CAPTURE: u8 = 1;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Chunk { pub struct Chunk {
pub bytecode: Vec<u8>, pub bytecode: Vec<u8>,
pub span_info: Vec<SpanRun>,
pub current_span: Span,
}
#[derive(Debug, Clone)]
pub struct SpanRun {
pub span: Span,
pub len: u16,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -66,6 +73,8 @@ impl Chunk {
if capacity <= (1 << 16) { if capacity <= (1 << 16) {
Ok(Chunk { Ok(Chunk {
bytecode: Vec::with_capacity(capacity), bytecode: Vec::with_capacity(capacity),
span_info: Vec::new(),
current_span: Span::new(0, 0),
}) })
} else { } else {
Err(ChunkSizeError) Err(ChunkSizeError)
@ -76,6 +85,16 @@ impl Chunk {
Offset(self.bytecode.len() as u16) Offset(self.bytecode.len() as u16)
} }
fn push_span_info(&mut self, span: Span, len: u16) {
if let Some(info) = self.span_info.last_mut() {
if info.span == span {
info.len += len;
return;
}
}
self.span_info.push(SpanRun { span, len });
}
pub fn emit_bytes(&mut self, bytes: &[u8]) -> Result<Offset, EmitError> { pub fn emit_bytes(&mut self, bytes: &[u8]) -> Result<Offset, EmitError> {
if self.bytecode.len() + bytes.len() > self.bytecode.capacity() { if self.bytecode.len() + bytes.len() > self.bytecode.capacity() {
return Err(EmitError); return Err(EmitError);
@ -83,6 +102,7 @@ impl Chunk {
let offset = Offset(self.bytecode.len() as u16); let offset = Offset(self.bytecode.len() as u16);
self.bytecode.extend_from_slice(bytes); self.bytecode.extend_from_slice(bytes);
self.push_span_info(self.current_span, bytes.len() as u16);
Ok(offset) Ok(offset)
} }
@ -122,40 +142,15 @@ impl Chunk {
self.patch_u16(offset, x.0); self.patch_u16(offset, x.0);
} }
// NOTE: I'm aware these aren't the fastest implementations since they validate quite a lot pub fn find_span(&self, pc: u16) -> Option<&Span> {
// during runtime, but this is just an MVP. It doesn't have to be blazingly fast. let mut cur = 0;
for info in &self.span_info {
pub fn read_u8(&self, pc: &mut usize) -> Result<u8, ReadError> { if pc >= cur && pc < cur + info.len {
let x = self.bytecode.get(*pc).copied(); return Some(&info.span);
*pc += 1;
x.ok_or(ReadError)
} }
cur += info.len;
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)
} }
None
} }
} }
@ -183,15 +178,6 @@ impl Display for EmitError {
} }
} }
#[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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct DefId(u16); pub struct DefId(u16);

View file

@ -1,9 +1,14 @@
use core::{ use core::{
error::Error, error::Error,
fmt::{self, Display}, fmt::{self, Display},
mem::take,
}; };
use alloc::vec::Vec; use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
use crate::{ use crate::{
ast::{Ast, NodeId, NodeKind}, ast::{Ast, NodeId, NodeKind},
@ -42,6 +47,11 @@ pub struct Compiler<'a> {
pub chunk: &'a mut Chunk, pub chunk: &'a mut Chunk,
pub diagnostics: Vec<Diagnostic>, pub diagnostics: Vec<Diagnostic>,
scopes: Vec<Scope<'a>>, scopes: Vec<Scope<'a>>,
// NOTE: This mechanism is kind of non-obvious at first, but keep in mind this is the name of
// any functions emitted within the current scope---not of the current function.
// The convention is to set this to the current def's name, and any functions inside the current
// function get λ as a suffix. The more nesting, the more lambdas---e.g. myFunctionλλ.
function_name: String,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -60,6 +70,8 @@ impl<'a> Compiler<'a> {
captures: Vec::new(), captures: Vec::new(),
let_count: 0, let_count: 0,
}]), }]),
// For unnamed functions within the brush's toplevel.
function_name: "(brush)λ".into(),
} }
} }
@ -86,7 +98,10 @@ impl<'a> Compiler<'a> {
type CompileResult<T = ()> = Result<T, CompileError>; type CompileResult<T = ()> = Result<T, CompileError>;
pub fn compile_expr<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> CompileResult { pub fn compile_expr<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> CompileResult {
match src.ast.kind(node_id) { let previous_span = c.chunk.current_span;
c.chunk.current_span = src.ast.span(node_id);
let result = match src.ast.kind(node_id) {
// The nil node is special, as it inhabits node ID 0. // The nil node is special, as it inhabits node ID 0.
NodeKind::Nil => { NodeKind::Nil => {
unreachable!("Nil node should never be emitted (ParenEmpty is used for nil literals)") unreachable!("Nil node should never be emitted (ParenEmpty is used for nil literals)")
@ -121,7 +136,11 @@ pub fn compile_expr<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId)
// Error nodes are ignored, because for each error node an appropriate parser // Error nodes are ignored, because for each error node an appropriate parser
// diagnostic is emitted anyways. // diagnostic is emitted anyways.
NodeKind::Error => Ok(()), NodeKind::Error => Ok(()),
} };
c.chunk.current_span = previous_span;
result
} }
fn compile_nil(c: &mut Compiler) -> CompileResult { fn compile_nil(c: &mut Compiler) -> CompileResult {
@ -518,7 +537,17 @@ fn compile_lambda<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -
c.chunk.emit_opcode(Opcode::Function)?; c.chunk.emit_opcode(Opcode::Function)?;
c.chunk.emit_u8(param_count)?; c.chunk.emit_u8(param_count)?;
let after_offset = c.chunk.emit_u16(0)?; let then_offset = c.chunk.emit_u16(0)?;
let function_name = &c.function_name[0..c.function_name.len().min(u8::MAX as usize)];
c.chunk.emit_u8(function_name.len() as u8)?;
c.chunk.emit_bytes(function_name.as_bytes())?;
// Swap the current name for a name with a λ at the end.
// This is to make lambdas within the current lambda shown as a different functions in
// stack traces.
let previous_function_name = take(&mut c.function_name);
c.function_name = format!("{previous_function_name}λ");
c.scopes.push(Scope { c.scopes.push(Scope {
locals, locals,
@ -528,8 +557,10 @@ fn compile_lambda<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -
compile_expr(c, src, body)?; compile_expr(c, src, body)?;
c.chunk.emit_opcode(Opcode::Return)?; c.chunk.emit_opcode(Opcode::Return)?;
let after = u16::try_from(c.chunk.bytecode.len()).expect("chunk is too large"); c.function_name = previous_function_name;
c.chunk.patch_u16(after_offset, after);
let then = u16::try_from(c.chunk.bytecode.len()).expect("chunk is too large");
c.chunk.patch_u16(then_offset, then);
let scope = c.scopes.pop().unwrap(); let scope = c.scopes.pop().unwrap();
let let_count = u8::try_from(scope.let_count).unwrap_or_else(|_| { let let_count = u8::try_from(scope.let_count).unwrap_or_else(|_| {
@ -580,6 +611,7 @@ fn compile_toplevel<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId)
)); ));
} }
c.chunk.current_span = src.ast.span(toplevel_expr);
match compile_toplevel_expr(c, src, toplevel_expr)? { match compile_toplevel_expr(c, src, toplevel_expr)? {
ToplevelExpr::Def => (), ToplevelExpr::Def => (),
ToplevelExpr::Result if result_expr.is_none() => result_expr = Some(toplevel_expr), ToplevelExpr::Result if result_expr.is_none() => result_expr = Some(toplevel_expr),
@ -676,10 +708,20 @@ fn compile_def<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> C
// zero def instead. // zero def instead.
let def_id = c.defs.get_def(name).unwrap_or_default(); let def_id = c.defs.get_def(name).unwrap_or_default();
let previous_function_name = take(&mut c.function_name);
if src.ast.kind(right) == NodeKind::Lambda {
c.function_name = name.to_string();
} else {
// Name for lambdas within the current def, which is not a lambda itself.
c.function_name = format!("{name}λ");
}
compile_expr(c, src, right)?; compile_expr(c, src, right)?;
c.chunk.emit_opcode(Opcode::SetDef)?; c.chunk.emit_opcode(Opcode::SetDef)?;
c.chunk.emit_u16(def_id.to_u16())?; c.chunk.emit_u16(def_id.to_u16())?;
c.function_name = previous_function_name;
Ok(()) Ok(())
} }

View file

@ -22,7 +22,10 @@ pub const std_options: std.Options = .{
}; };
pub fn enableLogScope(scope: @TypeOf(.enum_literal)) bool { pub fn enableLogScope(scope: @TypeOf(.enum_literal)) bool {
if (scope == .vm) return false else return true; if (scope == .vm)
return false
else
return true;
} }
// Allocator // Allocator
@ -124,14 +127,21 @@ export fn haku2_vm_run_main(
chunk.* = bytecode.Chunk{ chunk.* = bytecode.Chunk{
.bytecode = code[0..code_len], .bytecode = code[0..code_len],
}; };
const closure = value.Closure{ const closure = scratch.allocator().create(value.Closure) catch {
vm.outOfMemory() catch {};
return false;
};
closure.* = value.Closure{
.name = "(brush)",
.param_count = 0,
.impl = .{ .bytecode = .{
.chunk = chunk, .chunk = chunk,
.start = 0, .start = 0,
.param_count = 0,
.local_count = local_count, .local_count = local_count,
.captures = &[_]value.Value{}, .captures = &[_]value.Value{},
} },
}; };
vm.run(scratch.allocator(), &closure, vm.stack_top) catch return false; vm.run(scratch.allocator(), closure, vm.stack_top) catch return false;
return true; return true;
} }
@ -182,6 +192,33 @@ export fn haku2_vm_exception_render(vm: *const Vm, buffer: [*]u8) void {
_ = exn.format(buffer[0..exn.len], &exn.args); _ = exn.format(buffer[0..exn.len], &exn.args);
} }
export fn haku2_vm_stackframe_count(vm: *const Vm) usize {
return vm.stackFrameCount();
}
export fn haku2_vm_stackframe_is_system(vm: *const Vm, index: usize) bool {
const stack_frame = vm.stackFrame(index);
return stack_frame.closure.impl == .system;
}
export fn haku2_vm_stackframe_pc(vm: *const Vm, index: usize) i32 {
const stack_frame = vm.stackFrame(index);
if (stack_frame.closure.impl == .bytecode)
if (stack_frame.ip) |ip|
return stack_frame.closure.programCounter(ip);
return -1; // no return address
}
export fn haku2_vm_stackframe_function_name_len(vm: *const Vm, index: usize) usize {
const stack_frame = vm.stackFrame(index);
return stack_frame.closure.name.len;
}
export fn haku2_vm_stackframe_function_name(vm: *const Vm, index: usize) [*]const u8 {
const stack_frame = vm.stackFrame(index);
return stack_frame.closure.name.ptr;
}
// Renderer // Renderer
export fn haku2_render(vm: *Vm, canvas: *Canvas, max_depth: usize) bool { export fn haku2_render(vm: *Vm, canvas: *Canvas, max_depth: usize) bool {

View file

@ -62,7 +62,12 @@ pub const Context = struct {
} }
}; };
pub const Fn = *const fn (Context) Vm.Error!Value; pub const Fn = struct {
pub const Impl = *const fn (Context) Vm.Error!Value;
closure: value.Closure,
impl: Impl,
};
pub const FnTable = [256]Fn; pub const FnTable = [256]Fn;
fn invalid(cx: Context) !Value { fn invalid(cx: Context) !Value {
@ -150,13 +155,24 @@ fn intoReturn(cx: Context, any: anytype) Vm.Error!Value {
}; };
} }
fn rawFn(name: []const u8, func: Fn.Impl) Fn {
return .{
.closure = .{
.name = name,
.param_count = 0, // TODO
.impl = .{ .system = void{} },
},
.impl = func,
};
}
/// Erase a well-typed function into a function that operates on raw values. /// Erase a well-typed function into a function that operates on raw values.
/// The erased function performs all the necessary conversions automatically. /// The erased function performs all the necessary conversions automatically.
/// ///
/// Note that the argument order is important---function arguments go first, then context (such as /// Note that the argument order is important---function arguments go first, then context (such as
/// the VM or the allocator.) Otherwise argument indices will not match up. /// the VM or the allocator.) Otherwise argument indices will not match up.
fn erase(comptime name: []const u8, comptime func: anytype) Fn { fn erase(comptime name: []const u8, comptime func: anytype) Fn {
return Erased(name, func).call; return rawFn(name, Erased(name, func).call);
} }
fn countParams(comptime func: anytype) usize { fn countParams(comptime func: anytype) usize {
@ -191,7 +207,7 @@ const SparseFn = struct { u8, Fn };
const SparseFnTable = []const SparseFn; const SparseFnTable = []const SparseFn;
fn makeFnTable(init: SparseFnTable) FnTable { fn makeFnTable(init: SparseFnTable) FnTable {
var table = [_]Fn{invalid} ** @typeInfo(FnTable).array.len; var table = [_]Fn{rawFn("(invalid)", invalid)} ** @typeInfo(FnTable).array.len;
for (init) |entry| { for (init) |entry| {
const index, const func = entry; const index, const func = entry;
table[index] = func; table[index] = func;
@ -249,12 +265,12 @@ pub const fns = makeFnTable(&[_]SparseFn{
.{ 0x44, erase("<=", lessOrEqual) }, .{ 0x44, erase("<=", lessOrEqual) },
.{ 0x45, erase(">", greater) }, .{ 0x45, erase(">", greater) },
.{ 0x46, erase(">=", greaterOrEqual) }, .{ 0x46, erase(">=", greaterOrEqual) },
.{ 0x80, vec }, .{ 0x80, rawFn("vec", vec) },
.{ 0x81, erase("vecX", vecX) }, .{ 0x81, erase("vecX", vecX) },
.{ 0x82, erase("vecY", vecY) }, .{ 0x82, erase("vecY", vecY) },
.{ 0x83, erase("vecZ", vecZ) }, .{ 0x83, erase("vecZ", vecZ) },
.{ 0x84, erase("vecW", vecW) }, .{ 0x84, erase("vecW", vecW) },
.{ 0x85, rgba }, .{ 0x85, rawFn("rgba", rgba) },
.{ 0x86, erase("rgbaR", rgbaR) }, .{ 0x86, erase("rgbaR", rgbaR) },
.{ 0x87, erase("rgbaG", rgbaG) }, .{ 0x87, erase("rgbaG", rgbaG) },
.{ 0x88, erase("rgbaB", rgbaB) }, .{ 0x88, erase("rgbaB", rgbaB) },

View file

@ -77,7 +77,7 @@ pub const Value = union(enum) {
inline .tag, .number => |x| try std.fmt.format(writer, "{d}", .{x}), inline .tag, .number => |x| try std.fmt.format(writer, "{d}", .{x}),
inline .vec4, .rgba => |x| try std.fmt.format(writer, "{s}{d}", .{ @tagName(value), x }), inline .vec4, .rgba => |x| try std.fmt.format(writer, "{s}{d}", .{ @tagName(value), x }),
.ref => |ref| switch (ref.*) { .ref => |ref| switch (ref.*) {
.closure => |c| try std.fmt.format(writer, "function({})", .{c.param_count}), .closure => |c| try std.fmt.format(writer, "function {s}", .{c.name}),
.list => |l| { .list => |l| {
try std.fmt.formatBuf("[", options, writer); try std.fmt.formatBuf("[", options, writer);
for (l, 0..) |elem, i| { for (l, 0..) |elem, i| {
@ -128,11 +128,31 @@ pub const Ref = union(enum) {
}; };
pub const Closure = struct { pub const Closure = struct {
pub const Impl = union(enum) {
bytecode: Bytecode,
// Currently unimplemented; only used as a discriminator for system functions on
// the call stack that do not have bytecode.
system: void,
pub const Bytecode = struct {
chunk: *const bytecode.Chunk, chunk: *const bytecode.Chunk,
start: bytecode.Loc, start: bytecode.Loc,
param_count: u8,
local_count: u8, local_count: u8,
captures: []Value, captures: []Value,
};
};
name: []const u8,
param_count: u8,
impl: Impl,
pub fn bytecodePtr(c: *const Closure, pc: usize) [*]const u8 {
return c.impl.bytecode.chunk.bytecode[c.impl.bytecode.start + pc ..].ptr;
}
pub fn programCounter(c: *const Closure, ip: [*]const u8) u16 {
return @truncate(ip - c.impl.bytecode.chunk.bytecode.ptr);
}
}; };
pub const List = []Value; pub const List = []Value;

View file

@ -19,6 +19,10 @@ call_stack_top: u32 = 0,
defs: []Value = &.{}, defs: []Value = &.{},
fuel: u32 = 0, // NOTE: VM must be refueled via reset() before running code fuel: u32 = 0, // NOTE: VM must be refueled via reset() before running code
exception: ?Exception = null, exception: ?Exception = null,
// closure, ip are for exception reporting. These variables are read-only, system functions
// cannot affect control flow using them.
closure: ?*const value.Closure = null,
ip: ?[*]const u8 = null,
pub const Limits = struct { pub const Limits = struct {
stack_capacity: usize = 256, stack_capacity: usize = 256,
@ -27,7 +31,7 @@ pub const Limits = struct {
pub const CallFrame = struct { pub const CallFrame = struct {
closure: *const value.Closure, closure: *const value.Closure,
ip: [*]const u8, return_addr: ?[*]const u8,
bottom: u32, bottom: u32,
}; };
@ -59,8 +63,10 @@ pub fn reset(vm: *Vm, fuel: u32) void {
vm.exception = null; vm.exception = null;
} }
// NOTE: When the VM throws an exception, it must be reset. There's no resuming from an exception.
pub fn throw(vm: *Vm, comptime fmt: []const u8, args: anytype) Error { pub fn throw(vm: *Vm, comptime fmt: []const u8, args: anytype) Error {
log.info("throw: fmt={s}", .{fmt}); log.info("throw: fmt={s}", .{fmt});
log.debug("implicit stack frame: closure={?any} ip={?*}", .{ vm.closure, vm.ip });
const Args = @TypeOf(args); const Args = @TypeOf(args);
const max_args_size = @sizeOf(@TypeOf(vm.exception.?.args)); const max_args_size = @sizeOf(@TypeOf(vm.exception.?.args));
@ -92,6 +98,45 @@ pub fn outOfMemory(vm: *Vm) Error {
return vm.throw("out of memory", .{}); return vm.throw("out of memory", .{});
} }
// NOTE: Different from CallFrame, this is a helper type for obtaining stack traces.
pub const StackFrame = struct {
closure: *const value.Closure,
ip: ?[*]const u8,
};
pub fn stackFrameCount(vm: *const Vm) usize {
if (vm.closure != null) {
return vm.call_stack_top + 1;
} else {
return vm.call_stack_top;
}
}
pub fn stackFrame(vm: *const Vm, index: usize) StackFrame {
if (vm.closure) |closure| {
if (index == 0) {
return .{
.closure = closure,
.ip = vm.ip,
};
} else {
// NOTE: Minus two, because remember index == 0 is occupied by the current stack frame.
const call_frame_index = vm.call_stack_top - 1 - (index - 1);
const call_frame = vm.call_stack[call_frame_index];
return .{
.closure = call_frame.closure,
.ip = call_frame.return_addr,
};
}
} else {
const call_frame = vm.call_stack[vm.call_stack_top - 1 - index];
return .{
.closure = call_frame.closure,
.ip = call_frame.return_addr,
};
}
}
/// Debug assertion for bytecode validity. /// Debug assertion for bytecode validity.
/// In future versions, this may become disabled in release builds. /// In future versions, this may become disabled in release builds.
fn validateBytecode(vm: *Vm, ok: bool, comptime fmt: []const u8, args: anytype) Error!void { fn validateBytecode(vm: *Vm, ok: bool, comptime fmt: []const u8, args: anytype) Error!void {
@ -162,16 +207,22 @@ pub fn def(vm: *Vm, index: u16) Error!*Value {
// Utility struct for storing and restoring the VM's state variables across FFI boundaries. // Utility struct for storing and restoring the VM's state variables across FFI boundaries.
const Context = struct { const Context = struct {
ip: ?[*]const u8,
closure: ?*const value.Closure,
fuel: u32, fuel: u32,
}; };
fn restoreContext(vm: *Vm) Context { fn restoreContext(vm: *Vm) Context {
return .{ return .{
.ip = vm.ip,
.closure = vm.closure,
.fuel = vm.fuel, .fuel = vm.fuel,
}; };
} }
fn storeContext(vm: *Vm, context: Context) void { fn storeContext(vm: *Vm, context: Context) void {
vm.ip = context.ip;
vm.closure = context.closure;
vm.fuel = context.fuel; vm.fuel = context.fuel;
} }
@ -181,7 +232,8 @@ inline fn read(comptime T: type, ip: *[*]const u8) T {
return result; return result;
} }
inline fn readOpcode(ip: *[*]const u8) bytecode.Opcode { inline fn readOpcode(vm: *Vm, ip: *[*]const u8) bytecode.Opcode {
vm.ip = ip.*;
const opcode: bytecode.Opcode = @enumFromInt(read(u8, ip)); const opcode: bytecode.Opcode = @enumFromInt(read(u8, ip));
log.debug("OP {*} {}", .{ ip.*, opcode }); log.debug("OP {*} {}", .{ ip.*, opcode });
return opcode; return opcode;
@ -200,53 +252,59 @@ pub fn run(
) Error!void { ) Error!void {
log.debug("BEGIN RUN {}", .{init_closure}); log.debug("BEGIN RUN {}", .{init_closure});
// NOTE: This will need swapping out once we implement first-class system functions, because
// system functions should be able to call themselves (like: map (range 1 10) sin)
debug.assert(init_closure.impl == .bytecode);
var closure = init_closure; var closure = init_closure;
var ip: [*]const u8 = closure.chunk.bytecode[closure.start..].ptr; var ip: [*]const u8 = closure.bytecodePtr(0);
var bottom = init_bottom; var bottom = init_bottom;
var fuel = vm.fuel; var fuel = vm.fuel;
for (0..closure.local_count) |_| { vm.closure = closure;
vm.ip = ip;
for (0..closure.impl.bytecode.local_count) |_| {
try vm.push(.nil); try vm.push(.nil);
} }
const call_bottom = vm.call_stack_top;
try vm.pushCall(.{ try vm.pushCall(.{
.closure = closure, .closure = closure,
.ip = ip, .return_addr = null, // nowhere to return to
.bottom = bottom, .bottom = bottom,
}); });
next: switch (readOpcode(&ip)) { next: switch (vm.readOpcode(&ip)) {
.nil => { .nil => {
try vm.consumeFuel(&fuel, 1); try vm.consumeFuel(&fuel, 1);
try vm.push(.nil); try vm.push(.nil);
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.false => { .false => {
try vm.consumeFuel(&fuel, 1); try vm.consumeFuel(&fuel, 1);
try vm.push(.false); try vm.push(.false);
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.true => { .true => {
try vm.consumeFuel(&fuel, 1); try vm.consumeFuel(&fuel, 1);
try vm.push(.true); try vm.push(.true);
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.tag => { .tag => {
try vm.consumeFuel(&fuel, 1); try vm.consumeFuel(&fuel, 1);
const tag_id: value.TagId = @enumFromInt(read(u16, &ip)); const tag_id: value.TagId = @enumFromInt(read(u16, &ip));
try vm.push(.{ .tag = tag_id }); try vm.push(.{ .tag = tag_id });
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.number => { .number => {
try vm.consumeFuel(&fuel, 1); try vm.consumeFuel(&fuel, 1);
const number: f32 = @bitCast(read(u32, &ip)); const number: f32 = @bitCast(read(u32, &ip));
try vm.push(.{ .number = number }); try vm.push(.{ .number = number });
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.rgba => { .rgba => {
@ -256,7 +314,7 @@ pub fn run(
const b = read(u8, &ip); const b = read(u8, &ip);
const a = read(u8, &ip); const a = read(u8, &ip);
try vm.push(.{ .rgba = value.rgbaFrom8(value.Rgba8{ r, g, b, a }) }); try vm.push(.{ .rgba = value.rgbaFrom8(value.Rgba8{ r, g, b, a }) });
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.local => { .local => {
@ -264,7 +322,7 @@ pub fn run(
const index = read(u8, &ip); const index = read(u8, &ip);
const l = try vm.local(bottom, index); const l = try vm.local(bottom, index);
try vm.push(l.*); try vm.push(l.*);
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.set_local => { .set_local => {
@ -273,16 +331,17 @@ pub fn run(
const new = try vm.pop(); const new = try vm.pop();
const l = try vm.local(bottom, index); const l = try vm.local(bottom, index);
l.* = new; l.* = new;
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.capture => { .capture => {
try vm.consumeFuel(&fuel, 1); try vm.consumeFuel(&fuel, 1);
const index = read(u8, &ip); const index = read(u8, &ip);
try vm.validateBytecode(index < closure.captures.len, "capture index out of bounds", .{}); const captures = closure.impl.bytecode.captures;
const capture = closure.captures[index]; try vm.validateBytecode(index < captures.len, "capture index out of bounds", .{});
const capture = captures[index];
try vm.push(capture); try vm.push(capture);
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.def => { .def => {
@ -290,7 +349,7 @@ pub fn run(
const index = read(u16, &ip); const index = read(u16, &ip);
const d = try vm.def(index); const d = try vm.def(index);
try vm.push(d.*); try vm.push(d.*);
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.set_def => { .set_def => {
@ -299,7 +358,7 @@ pub fn run(
const new = try vm.pop(); const new = try vm.pop();
const d = try vm.def(index); const d = try vm.def(index);
d.* = new; d.* = new;
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.list => { .list => {
@ -312,62 +371,70 @@ pub fn run(
const ref = allocator.create(value.Ref) catch return vm.outOfMemory(); const ref = allocator.create(value.Ref) catch return vm.outOfMemory();
ref.* = .{ .list = list }; ref.* = .{ .list = list };
try vm.push(.{ .ref = ref }); try vm.push(.{ .ref = ref });
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.function => { .function => {
try vm.consumeFuel(&fuel, 1);
const param_count = read(u8, &ip); const param_count = read(u8, &ip);
const then = read(u16, &ip); const then = read(u16, &ip);
const name_len = read(u8, &ip);
const name = ip[0..name_len];
ip += name_len;
const body = ip; const body = ip;
ip = closure.chunk.bytecode[then..].ptr; ip = closure.bytecodePtr(then);
const local_count = read(u8, &ip); const local_count = read(u8, &ip);
const capture_count = read(u8, &ip); const capture_count = read(u8, &ip);
try vm.consumeFuel(&fuel, 1 + capture_count);
const captures = allocator.alloc(Value, capture_count) catch return vm.outOfMemory(); const captures = allocator.alloc(Value, capture_count) catch return vm.outOfMemory();
for (captures) |*capture| { for (captures) |*capture| {
const capture_kind = read(u8, &ip); const capture_kind = read(u8, &ip);
const index = read(u8, &ip); const index = read(u8, &ip);
vm.ip = ip;
capture.* = switch (capture_kind) { capture.* = switch (capture_kind) {
bytecode.capture_local => (try vm.local(bottom, index)).*, bytecode.capture_local => (try vm.local(bottom, index)).*,
bytecode.capture_capture => blk: { bytecode.capture_capture => blk: {
try vm.validateBytecode(index < closure.captures.len, "capture index out of bounds", .{}); const closureCaptures = closure.impl.bytecode.captures;
break :blk closure.captures[index]; try vm.validateBytecode(index < closureCaptures.len, "capture index out of bounds", .{});
break :blk closureCaptures[index];
}, },
else => .nil, else => .nil,
}; };
} }
const new_closure = value.Closure{ const new_closure = value.Closure{
.chunk = closure.chunk, .name = name,
.start = @truncate(body - closure.chunk.bytecode.ptr),
.param_count = param_count, .param_count = param_count,
.impl = .{ .bytecode = .{
.chunk = closure.impl.bytecode.chunk,
.start = @truncate(body - closure.bytecodePtr(0)),
.local_count = local_count, .local_count = local_count,
.captures = captures, .captures = captures,
} },
}; };
const ref = allocator.create(value.Ref) catch return vm.outOfMemory(); const ref = allocator.create(value.Ref) catch return vm.outOfMemory();
ref.* = .{ .closure = new_closure }; ref.* = .{ .closure = new_closure };
try vm.push(.{ .ref = ref }); try vm.push(.{ .ref = ref });
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.jump => { .jump => {
try vm.consumeFuel(&fuel, 1);
const offset = read(u16, &ip); const offset = read(u16, &ip);
ip = closure.chunk.bytecode[offset..].ptr; try vm.consumeFuel(&fuel, 1);
continue :next readOpcode(&ip); ip = closure.bytecodePtr(offset);
continue :next vm.readOpcode(&ip);
}, },
.jump_if_not => { .jump_if_not => {
try vm.consumeFuel(&fuel, 1);
const offset = read(u16, &ip); const offset = read(u16, &ip);
try vm.consumeFuel(&fuel, 1);
const condition = try vm.pop(); const condition = try vm.pop();
if (!condition.isTruthy()) { if (!condition.isTruthy()) {
ip = closure.chunk.bytecode[offset..].ptr; ip = closure.bytecodePtr(offset);
} }
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.field => { .field => {
@ -389,27 +456,31 @@ pub fn run(
} }
if (found_index) |index| { if (found_index) |index| {
try vm.validateBytecode(index < closure.captures.len, "field index out of bounds", .{}); const fields = closure.impl.bytecode.captures;
const field = closure.captures[index]; try vm.validateBytecode(index < fields.len, "field index out of bounds", .{});
const field = fields[index];
try vm.push(field); try vm.push(field);
} else { } else {
return vm.throw("field with this name does not exist", .{}); return vm.throw("field with this name does not exist", .{});
} }
continue :next readOpcode(&ip); continue :next vm.readOpcode(&ip);
}, },
.call => { .call => {
try vm.consumeFuel(&fuel, 1);
const arg_count = read(u8, &ip); const arg_count = read(u8, &ip);
try vm.consumeFuel(&fuel, 1);
const function_value = try vm.pop(); const function_value = try vm.pop();
if (function_value != .ref or function_value.ref.* != .closure) { if (function_value != .ref or function_value.ref.* != .closure) {
return vm.throw("attempt to call a value that is not a function", .{}); return vm.throw("attempt to call a value that is not a function", .{});
} }
const called_closure = &function_value.ref.closure; const called_closure = &function_value.ref.closure;
// NOTE: Will need replacing for first-class system functions.
debug.assert(called_closure.impl == .bytecode);
if (arg_count != called_closure.param_count) { if (arg_count != called_closure.param_count) {
return vm.throw( return vm.throw(
"function expects {} arguments, but it received {}", "function expects {} arguments, but it received {}",
@ -419,7 +490,7 @@ pub fn run(
const call_frame = CallFrame{ const call_frame = CallFrame{
.closure = closure, .closure = closure,
.ip = ip, .return_addr = ip,
.bottom = bottom, .bottom = bottom,
}; };
@ -427,29 +498,61 @@ pub fn run(
try vm.validateBytecode(overflow == 0, "not enough values on the stack for arguments", .{}); try vm.validateBytecode(overflow == 0, "not enough values on the stack for arguments", .{});
closure = called_closure; closure = called_closure;
ip = closure.chunk.bytecode[closure.start..].ptr; ip = closure.bytecodePtr(0);
bottom = new_bottom; bottom = new_bottom;
for (0..closure.local_count) |_| { // pushCall before setting vm.closure and vm.ip, such that stack overflow errors are
// reported at the call site rather than within the called function.
try vm.pushCall(call_frame);
// Remember to update the closure used for exception reporting, since readOpcode does
// not do that as it does with ip.
// ip also needs updating, because the topmost stack frame will assume that vm.ip is
// based in the new closure.
// We want errors for no stack space being left for locals to appear at the start of the
// function in source code, not earlier or later. Hence we do this before local slots
// are pushed onto the stack.
vm.closure = closure;
vm.ip = ip;
for (0..closure.impl.bytecode.local_count) |_| {
try vm.push(.nil); try vm.push(.nil);
} }
try vm.pushCall(call_frame); continue :next vm.readOpcode(&ip);
continue :next readOpcode(&ip);
}, },
.system => { .system => {
try vm.consumeFuel(&fuel, 1);
const index = read(u8, &ip); const index = read(u8, &ip);
const arg_count = read(u8, &ip); const arg_count = read(u8, &ip);
const system_fn = system.fns[index]; const system_fn = &system.fns[index];
log.debug("system index={} arg_count={} system_fn={p}", .{ index, arg_count, system_fn }); try vm.consumeFuel(&fuel, 1);
vm.storeContext(.{ .fuel = fuel }); log.debug("system index={} arg_count={} system_fn={s}", .{ index, arg_count, system_fn.closure.name });
const result = try system_fn(.{
// Push the current call frame onto the stack, such that the stack trace displays it
// in addition to the ongoing system call.
try vm.pushCall(.{
.closure = closure,
.return_addr = ip,
.bottom = bottom,
});
// Also now, push the system function onto the stack, because nulling out vm.closure
// and vm.ip removes the extra "current function" stack entry.
try vm.pushCall(.{
.closure = &system_fn.closure,
.return_addr = null,
.bottom = vm.stack_top - arg_count,
});
vm.storeContext(.{
.fuel = fuel,
.closure = null,
.ip = null,
});
const result = try system_fn.impl(.{
.vm = vm, .vm = vm,
.allocator = allocator, .allocator = allocator,
.args = vm.stack[vm.stack_top - arg_count .. vm.stack_top], .args = vm.stack[vm.stack_top - arg_count .. vm.stack_top],
@ -460,7 +563,16 @@ pub fn run(
vm.stack_top -= arg_count; vm.stack_top -= arg_count;
try vm.push(result); try vm.push(result);
continue :next readOpcode(&ip); // Restore the exception handling ip and closure to what it was.
log.debug("system restore", .{});
vm.ip = ip;
vm.closure = closure;
// Remove the temporary call frames.
_ = try vm.popCall(); // systemClosure
_ = try vm.popCall(); // closure
continue :next vm.readOpcode(&ip);
}, },
.ret => { .ret => {
@ -474,26 +586,28 @@ pub fn run(
"called function popped too many values. bottom={} stack_top={}", "called function popped too many values. bottom={} stack_top={}",
.{ bottom, vm.stack_top }, .{ bottom, vm.stack_top },
); );
try vm.validateBytecode(
call_bottom <= vm.call_stack_top,
"called function popped too many call frames. call_bottom={} call_stack_top={}",
.{ call_bottom, vm.call_stack_top },
);
vm.stack_top = bottom; vm.stack_top = bottom;
try vm.push(result); try vm.push(result);
if (vm.call_stack_top == call_bottom) { if (call_frame.return_addr) |addr| {
// If this is the last call frame from this `run` instance, return from the function. closure = call_frame.closure;
vm.storeContext(Context{ .fuel = fuel }); ip = addr;
bottom = call_frame.bottom;
vm.closure = closure;
vm.ip = ip;
} else {
// If there's nowhere to return to, stop interpreting.
vm.storeContext(Context{
.closure = closure,
.ip = ip,
.fuel = fuel,
});
break :next; break :next;
} }
closure = call_frame.closure; continue :next vm.readOpcode(&ip);
ip = call_frame.ip;
bottom = call_frame.bottom;
continue :next readOpcode(&ip);
}, },
_ => |invalid_opcode| { _ => |invalid_opcode| {
@ -531,11 +645,14 @@ pub fn runDotter(
}) catch return vm.outOfMemory(); }) catch return vm.outOfMemory();
const ref = allocator.create(value.Ref) catch return vm.outOfMemory(); const ref = allocator.create(value.Ref) catch return vm.outOfMemory();
ref.* = value.Ref{ .closure = .{ ref.* = value.Ref{ .closure = .{
.name = "dotter",
.param_count = 1,
.impl = .{ .bytecode = .{
.chunk = &system.record_dotter, .chunk = &system.record_dotter,
.start = 0, .start = 0,
.param_count = 1,
.local_count = 0, .local_count = 0,
.captures = data, .captures = data,
} },
} }; } };
const bottom = vm.stack_top; const bottom = vm.stack_top;

View file

@ -2,6 +2,7 @@
:root { :root {
--color-text: #111; --color-text: #111;
--color-text-dim: #777;
--color-error: #db344b; --color-error: #db344b;
--color-brand-blue: #40b1f4; --color-brand-blue: #40b1f4;
@ -190,6 +191,12 @@ pre:has(code) {
overflow: auto; overflow: auto;
} }
/* Abbreviations */
abbr {
cursor: help;
}
/* Icons */ /* Icons */
:root { :root {

View file

@ -1,4 +1,4 @@
import { CodeEditor } from "rkgk/code-editor.js"; import { CodeEditor, Selection } from "rkgk/code-editor.js";
import { SaveData } from "rkgk/framework.js"; import { SaveData } from "rkgk/framework.js";
import { builtInPresets } from "rkgk/brush-box.js"; import { builtInPresets } from "rkgk/brush-box.js";
@ -33,6 +33,10 @@ export class BrushEditor extends HTMLElement {
this.codeEditor = this.appendChild( this.codeEditor = this.appendChild(
new CodeEditor([ new CodeEditor([
{
className: "layer-syntax",
render: (code, element) => this.#renderSyntax(code, element),
},
{ {
className: "layer-error-squiggles", className: "layer-error-squiggles",
render: (code, element) => this.#renderErrorSquiggles(code, element), render: (code, element) => this.#renderErrorSquiggles(code, element),
@ -116,8 +120,32 @@ export class BrushEditor extends HTMLElement {
} else if (result.errorKind == "plain") { } else if (result.errorKind == "plain") {
this.errorHeader.textContent = result.message; this.errorHeader.textContent = result.message;
} else if (result.errorKind == "exception") { } else if (result.errorKind == "exception") {
// TODO: This should show a stack trace. let renderer = new ErrorException(result);
this.errorArea.textContent = `an exception occurred: ${result.message}`; let squiggles = renderer.prepareSquiggles();
this.codeEditor.rebuildLineMap();
this.errorSquiggles = this.#computeErrorSquiggles(
this.codeEditor.lineMap,
squiggles.diagnostics,
);
this.codeEditor.renderLayer("layer-error-squiggles");
renderer.addEventListener(".functionNameMouseEnter", (event) => {
squiggles.highlightSegment(event.frameIndex);
});
renderer.addEventListener(".functionNameMouseLeave", () => {
squiggles.highlightSegment(null);
});
renderer.addEventListener(".functionNameClick", (event) => {
let span = event.frame.span;
this.codeEditor.textArea.focus();
this.codeEditor.setSelection(new Selection(span.start, span.end));
});
this.errorArea.replaceChildren();
this.errorArea.appendChild(renderer);
} else { } else {
console.warn(`unknown error kind: ${result.errorKind}`); console.warn(`unknown error kind: ${result.errorKind}`);
this.errorHeader.textContent = "(unknown error kind)"; this.errorHeader.textContent = "(unknown error kind)";
@ -206,6 +234,10 @@ export class BrushEditor extends HTMLElement {
let spanElement = lineElement.appendChild(document.createElement("span")); let spanElement = lineElement.appendChild(document.createElement("span"));
spanElement.classList.add("squiggle", "squiggle-error"); spanElement.classList.add("squiggle", "squiggle-error");
spanElement.textContent = text; spanElement.textContent = text;
for (let diagnostic of segment.diagnostics) {
diagnostic.customizeSegment?.(diagnostic, spanElement);
}
} }
} }
} else { } else {
@ -213,6 +245,151 @@ export class BrushEditor extends HTMLElement {
} }
} }
} }
#renderSyntax(lines, element) {
// TODO: Syntax highlighting.
// Right now we just render a layer of black text to have the text visible in the code editor.
element.textContent = lines.string;
}
} }
customElements.define("rkgk-brush-editor", BrushEditor); customElements.define("rkgk-brush-editor", BrushEditor);
class ErrorException extends HTMLElement {
constructor(result) {
super();
this.result = result;
}
prepareSquiggles() {
let diagnostics = [];
let segments = [];
const customizeSegment = (diagnostic, segment) => {
// The control flow here is kind of spaghetti, but basically this fills out `segments`
// as the squiggles are being rendered.
// Once squiggles are rendered, you're free to call highlightSegment(i).
segments.push({
frameIndex: diagnostic.frameIndex,
colorIndex: diagnostic.colorIndex,
segment,
});
if (diagnostic.colorIndex != null) {
segment.classList.add("squiggle-colored");
segment.style.setProperty("--color-index", diagnostic.colorIndex);
}
};
for (let i = 0; i < this.result.stackTrace.length; ++i) {
let frame = this.result.stackTrace[i];
if (frame.span != null) {
diagnostics.push({
start: frame.span.start,
end: frame.span.end,
customizeSegment,
frameIndex: i,
colorIndex: i / this.result.stackTrace.length,
});
}
}
return {
diagnostics,
highlightSegment(frameIndex) {
let justHighlighted = new Set();
for (let entry of segments.values()) {
let segment = entry.segment;
if (entry.frameIndex == frameIndex) {
segment.classList.add("squiggle-highlighted");
// This logic exists so that a specific segment can display on top of
// another one if it's highlighted.
segment.style.setProperty("--highlight-color-index", entry.colorIndex);
justHighlighted.add(segment);
} else if (!justHighlighted.has(segment)) {
segment.classList.remove("squiggle-highlighted");
}
}
},
};
}
connectedCallback() {
let message = this.appendChild(document.createElement("p"));
message.classList.add("message");
message.textContent = this.result.message;
let stackTrace = this.appendChild(document.createElement("ol"));
stackTrace.classList.add("stack-trace");
for (let i = 0; i < this.result.stackTrace.length; ++i) {
let frame = this.result.stackTrace[i];
let line = stackTrace.appendChild(document.createElement("li"));
let [_, name, lambdasString] = frame.functionName.match(/([^λ]+)(λ*)/);
let inCaption = line.appendChild(document.createElement("span"));
inCaption.classList.add("in");
inCaption.innerText = "in ";
let functionName = line.appendChild(document.createElement("button"));
functionName.classList.add("function-name");
functionName.disabled = true;
functionName.innerText = name;
if (name == "(brush)") {
functionName.classList.add("function-name-brush");
functionName.title = "Top-level brush code";
}
let lambdas = functionName.appendChild(document.createElement("abbr"));
lambdas.classList.add("lambdas");
lambdas.textContent = lambdasString;
lambdas.title =
"λ - unnamed function\n" +
"Symbolizes functions that were not given a name.\n" +
"Each additional λ is one level of nesting inside another function.";
if (frame.isSystem) {
line.append(" ");
let system = line.appendChild(document.createElement("abbr"));
system.classList.add("system");
system.innerText = "(built-in)";
system.title =
"This function is built into rakugaki and does not exist in your brush's source code, so you can't see it in the editor.";
}
if (frame.span != null) {
functionName.disabled = false;
functionName.classList.add("source-link");
functionName.style.setProperty("--color-index", i / this.result.stackTrace.length);
functionName.addEventListener("mouseenter", () => {
this.dispatchEvent(
Object.assign(new Event(".functionNameMouseEnter"), {
frameIndex: i,
frame,
}),
);
});
functionName.addEventListener("mouseleave", () => {
this.dispatchEvent(
Object.assign(new Event(".functionNameMouseLeave"), {
frameIndex: i,
frame,
}),
);
});
functionName.addEventListener("click", () => {
this.dispatchEvent(
Object.assign(new Event(".functionNameClick"), {
frameIndex: i,
frame,
}),
);
});
}
}
}
}
customElements.define("rkgk-brush-editor-error-exception", ErrorException);

View file

@ -244,7 +244,7 @@ export class Haku {
w2.haku2_vm_destroy(this.#pVm2); w2.haku2_vm_destroy(this.#pVm2);
w2.haku2_defs_destroy(this.#pDefs2); w2.haku2_defs_destroy(this.#pDefs2);
w2.haku2_limits_destroy(this.#pLimits2); w2.haku2_limits_destroy(this.#pLimits2);
w2.haku_dealloc(this.#bytecode2.ptr); freeString2(this.#bytecode2);
} }
setBrush(code) { setBrush(code) {
@ -332,12 +332,49 @@ export class Haku {
return exn; return exn;
} }
#stackTrace() {
let count = w2.haku2_vm_stackframe_count(this.#pVm2);
let trace = [];
for (let i = 0; i < count; ++i) {
let isSystem = w2.haku2_vm_stackframe_is_system(this.#pVm2, i) != 0;
let pc = w2.haku2_vm_stackframe_pc(this.#pVm2, i);
let span = null;
if (!isSystem && pc > 0) {
// NOTE: We find the span at (pc - 1), because stack frames' program counters are
// situated at where control flow _ought_ to return, and not where the function call
// is located. Therefore, to pull the program counter back into the function call,
// we subtract 1.
let pSpan = w.haku_bytecode_find_span(this.#pInstance, Math.max(0, pc - 1));
if (pSpan != 0) {
span = {
start: w.haku_span_start(pSpan),
end: w.haku_span_end(pSpan),
};
}
}
trace.push({
isSystem,
pc,
span,
functionName: readString(
memory2,
w2.haku2_vm_stackframe_function_name_len(this.#pVm2, i),
w2.haku2_vm_stackframe_function_name(this.#pVm2, i),
),
});
}
return trace;
}
#exceptionResult() { #exceptionResult() {
return { return {
status: "error", status: "error",
errorKind: "exception", errorKind: "exception",
description: "Runtime error", description: "Runtime error",
message: this.#exceptionMessage(), message: this.#exceptionMessage(),
stackTrace: this.#stackTrace(),
}; };
} }

View file

@ -1,7 +1,7 @@
/* index.css - styles for index.html and generally main parts of the app /* index.css - styles for index.html and generally main parts of the app
For shared styles (such as color definitions) check out base.css. */ For shared styles (such as color definitions) check out base.css. */
* { html {
/* On the main page, we don't really want to permit selecting things. /* On the main page, we don't really want to permit selecting things.
It comes off as janky-looking. */ It comes off as janky-looking. */
user-select: none; user-select: none;
@ -412,22 +412,47 @@ rkgk-code-editor {
& > .squiggle-error { & > .squiggle-error {
text-decoration-color: var(--color-error); text-decoration-color: var(--color-error);
&.squiggle-colored {
--color: oklch(40% 100% calc(var(--color-index) * 300));
text-decoration-color: var(--color);
background-color: oklch(from var(--color) 60% c h / 0.13);
&.squiggle-highlighted {
--highlight-color-index: var(--color-index);
text-decoration: none;
background-color: oklch(
40% 100% calc(var(--highlight-color-index) * 300)
);
color: white;
font-weight: bold;
} }
} }
} }
}
}
& > .layer-syntax {
white-space: pre-wrap;
}
& > textarea { & > textarea {
display: block; display: block;
width: calc(100% - var(--gutter-width)); width: calc(100% - var(--gutter-width));
margin: 0; margin: 0;
margin-left: var(--gutter-width); margin-left: var(--gutter-width);
padding: 0; padding: 0;
box-sizing: border-box; border: none;
overflow: hidden; overflow: hidden;
resize: none; resize: none;
white-space: pre-wrap; white-space: pre-wrap;
border: none;
background: none; background: none;
color: transparent;
caret-color: var(--color-text);
&:focus { &:focus {
/* The outline is displayed on the parent element to also surround the gutter. */ /* The outline is displayed on the parent element to also surround the gutter. */
@ -490,6 +515,67 @@ rkgk-brush-editor.rkgk-panel {
margin: 0; margin: 0;
color: var(--color-error); color: var(--color-error);
white-space: pre-wrap; white-space: pre-wrap;
user-select: text;
max-height: 20em;
overflow-y: auto;
}
}
rkgk-brush-editor-error-exception {
& > .message {
margin: 4px 0;
}
& > .stack-trace {
margin: 0;
margin-left: 4ch;
color: var(--color-text);
& > li {
&::marker {
color: var(--color-text-dim);
}
& > button.function-name {
border: none;
border-radius: 0;
padding: 0;
background: none;
user-select: text;
font-weight: bold;
&:not(:disabled) {
text-decoration: underline;
cursor: pointer;
}
&:disabled {
opacity: 100%;
}
&.source-link {
--color-index: 0; /* set by JavaScript */
--color: oklch(40% 100% calc(var(--color-index) * 300));
color: var(--color);
&:hover {
text-decoration: none;
background-color: var(--color);
color: white;
}
}
}
& > .system {
color: var(--color-text-dim);
}
}
} }
} }