From 3a7c04686af9e007b094b9d7eddeadc1b70c32e6 Mon Sep 17 00:00:00 2001 From: Alek Ratzloff Date: Tue, 24 Sep 2024 11:34:07 -0700 Subject: [PATCH] Add function yielding and resuming Sometimes, a builtin function may need to call out to another function (user-defined or otherwise). Previously, we were just calling the function and popping the stack frame, leaving no room for the new function to be called. This introduces a `FunctionResult` and `FunctionState` that get passed between these builtin functions. A builtin function will receive a FunctionState that tells it whether it is currently beginning or being resumed and can act accordingly. A builtin function will in turn return a FunctionResult, which can either be to return and push a value to the stack, return without pushing a value (value is already on top of the stack), or yield execution back to the VM (implying that a new stack frame has been pushed with a new function to execute). Having to call a new function and resume is a bit unwieldy and un-ergonomic, and making a macro to help write these would be nice, but it looks like a procedural macro may be required to really enable this. For now, we will write these yields by hand and once it becomes truly too much, we can start looking at writing a macro library to handle this case. Signed-off-by: Alek Ratzloff --- src/builtins.rs | 53 +++++++++++++++++++++++---- src/disassemble.rs | 5 +++ src/obj.rs | 74 +++++++++++++++++++------------------- src/obj/function.rs | 76 ++++++++++++++++++++++++++++----------- src/vm.rs | 87 ++++++++++++++++++++++++++++++++++++++------- 5 files changed, 220 insertions(+), 75 deletions(-) diff --git a/src/builtins.rs b/src/builtins.rs index 6d32d09..51534d0 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -1,17 +1,58 @@ //! Builtin functions. use std::collections::HashMap; +use crate::obj::function::{FunctionResult, FunctionState}; use crate::obj::*; use crate::vm::Vm; -pub(crate) fn println(vm: &mut Vm, args: Vec) -> ObjP { - println!("{}", args[0].borrow()); - vm.create_nil() +// TODO builtins.rs - need a good macro to help reduce this repetition. +// The main problem is that "macros cannot expand to match arms". +// Thus, if we try to do something like this: +// +// ( ($n:expr, $block:block),* ) => { FunctionState::Resume($n) => $block } +// +// Is not allowed. +// +// This would probably be doable in a procedural macro. + +pub(crate) fn println(vm: &mut Vm, state: FunctionState) -> FunctionResult { + match state { + FunctionState::Begin => { + let obj = vm.peek(); + let method_type = vm.builtins().get("Method").unwrap().clone(); + let to_repr = obj + .borrow_mut() + .get_attr_lazy(obj.clone(), method_type, "to_repr") + .expect("no to_repr"); + to_repr.borrow().call(vm, 0); + FunctionResult::Yield(0) + } + FunctionState::Resume(0) => { + println!("{}", vm.frame_stack()[0].borrow()); + vm.create_nil().into() + } + _ => unreachable!(), + } } -pub(crate) fn print(vm: &mut Vm, args: Vec) -> ObjP { - print!("{}", args[0].borrow()); - vm.create_nil() +pub(crate) fn print(vm: &mut Vm, state: FunctionState) -> FunctionResult { + match state { + FunctionState::Begin => { + let obj = vm.peek(); + let method_type = vm.builtins().get("Method").unwrap().clone(); + let to_repr = obj + .borrow_mut() + .get_attr_lazy(obj.clone(), method_type, "to_repr") + .expect("no to_repr"); + to_repr.borrow().call(vm, 0); + FunctionResult::Yield(0) + } + FunctionState::Resume(0) => { + print!("{}", vm.frame_stack()[0].borrow()); + vm.create_nil().into() + } + _ => unreachable!(), + } } pub fn init_builtins(builtins: &mut HashMap) { diff --git a/src/disassemble.rs b/src/disassemble.rs index 76e0984..191a773 100644 --- a/src/disassemble.rs +++ b/src/disassemble.rs @@ -99,6 +99,11 @@ fn disassemble_chunk(chunk: &Chunk, constants: &Vec, globals: &Vec arg = format!("{depth}"); info = format!("slot {slot} (name unknown)"); } + Op::Nop => { + op_str = "NOP"; + arg = String::new(); + info = String::new(); + } Op::Halt => { op_str = "HALT"; arg = String::new(); diff --git a/src/obj.rs b/src/obj.rs index 965db5a..888344d 100644 --- a/src/obj.rs +++ b/src/obj.rs @@ -116,7 +116,8 @@ pub fn init_types(builtins: &mut HashMap) { // type definitions Type { // Method conversion - to_string => builtins.create_builtin_function("to_string", BaseObjInst::to_string, 1), + to_string => builtins.create_builtin_function("to_string", BaseObjInst::to_repr, 1), + to_repr => builtins.create_builtin_function("to_repr", BaseObjInst::to_repr, 1), to_bool => builtins.create_builtin_function("to_bool", BaseObjInst::to_bool, 1), // Operators __add__ => builtins.create_builtin_function("__add__", BaseObjInst::add, 2), @@ -333,73 +334,74 @@ struct BaseObjInst { // impl BaseObjInst { - fn add(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __add__ function (self: {:?}, rhs: {:?})", args[0].borrow(), args[1].borrow()) + fn add(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __add__ function (self: {:?}, rhs: {:?})", vm.frame_stack()[0].borrow(), vm.frame_stack()[1].borrow()) } - fn sub(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __sub__ function (self: {:?}, rhs: {:?})", args[0].borrow(), args[1].borrow()) + fn sub(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __sub__ function (self: {:?}, rhs: {:?})", vm.frame_stack()[0].borrow(), vm.frame_stack()[1].borrow()) } - fn mul(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __mul__ function (self: {:?}, rhs: {:?})", args[0].borrow(), args[1].borrow()) + fn mul(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __mul__ function (self: {:?}, rhs: {:?})", vm.frame_stack()[0].borrow(), vm.frame_stack()[1].borrow()) } - fn div(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __div__ function (self: {:?}, rhs: {:?})", args[0].borrow(), args[1].borrow()) + fn div(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __div__ function (self: {:?}, rhs: {:?})", vm.frame_stack()[0].borrow(), vm.frame_stack()[1].borrow()) } - fn and(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __and__ function (self: {:?}, rhs: {:?})", args[0].borrow(), args[1].borrow()) + fn and(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __and__ function (self: {:?}, rhs: {:?})", vm.frame_stack()[0].borrow(), vm.frame_stack()[1].borrow()) } - fn or(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __or__ function (self: {:?}, rhs: {:?})", args[0].borrow(), args[1].borrow()) + fn or(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __or__ function (self: {:?}, rhs: {:?})", vm.frame_stack()[0].borrow(), vm.frame_stack()[1].borrow()) } - fn ne(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __ne__ function (self: {:?}, rhs: {:?})", args[0].borrow(), args[1].borrow()) + fn ne(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __ne__ function (self: {:?}, rhs: {:?})", vm.frame_stack()[0].borrow(), vm.frame_stack()[1].borrow()) } - fn eq(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __eq__ function (self: {:?}, rhs: {:?})", args[0].borrow(), args[1].borrow()) + fn eq(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __eq__ function (self: {:?}, rhs: {:?})", vm.frame_stack()[0].borrow(), vm.frame_stack()[1].borrow()) } - fn gt(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __gt__ function (self: {:?}, rhs: {:?})", args[0].borrow(), args[1].borrow()) + fn gt(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __gt__ function (self: {:?}, rhs: {:?})", vm.frame_stack()[0].borrow(), vm.frame_stack()[1].borrow()) } - fn ge(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __ge__ function (self: {:?}, rhs: {:?})", args[0].borrow(), args[1].borrow()) + fn ge(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __ge__ function (self: {:?}, rhs: {:?})", vm.frame_stack()[0].borrow(), vm.frame_stack()[1].borrow()) } - fn lt(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __lt__ function (self: {:?}, rhs: {:?})", args[0].borrow(), args[1].borrow()) + fn lt(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __lt__ function (self: {:?}, rhs: {:?})", vm.frame_stack()[0].borrow(), vm.frame_stack()[1].borrow()) } - fn le(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __le__ function (self: {:?}, rhs: {:?})", args[0].borrow(), args[1].borrow()) + fn le(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __le__ function (self: {:?}, rhs: {:?})", vm.frame_stack()[0].borrow(), vm.frame_stack()[1].borrow()) } - fn pos(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __pos__ function (self: {:?})", args[0].borrow()) + fn pos(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __pos__ function (self: {:?})", vm.frame_stack()[0].borrow()) } - fn neg(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __neg__ function (self: {:?})", args[0].borrow()) + fn neg(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __neg__ function (self: {:?})", vm.frame_stack()[0].borrow()) } - fn not(_vm: &mut Vm, args: Vec) -> ObjP { - todo!("Raise some kind of not implemented/not callable error for __not__ function (self: {:?})", args[0].borrow()) + fn not(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + todo!("Raise some kind of not implemented/not callable error for __not__ function (self: {:?})", vm.frame_stack()[0].borrow()) } - fn to_bool(vm: &mut Vm, args: Vec) -> ObjP { - vm.create_bool(args[0].borrow().is_truthy()) + fn to_bool(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + vm.create_bool(vm.frame_stack()[0].borrow().is_truthy()) + .into() } - fn to_string(vm: &mut Vm, args: Vec) -> ObjP { - let str_value = format!("{}", &args[0].borrow()); - vm.create_str(str_value) + fn to_repr(vm: &mut Vm, _state: FunctionState) -> FunctionResult { + let str_value = format!("{}", vm.frame_stack()[0].borrow()); + vm.create_str(str_value).into() } } diff --git a/src/obj/function.rs b/src/obj/function.rs index 92981c6..556a4ec 100644 --- a/src/obj/function.rs +++ b/src/obj/function.rs @@ -6,18 +6,59 @@ use gc::{Finalize, Trace}; use crate::obj::macros::*; use crate::obj::{make_ptr, BaseObjInst, Obj, ObjP}; -use crate::vm::{Argc, Chunk, Frame, Vm}; +use crate::vm::{Argc, Chunk, Frame, Function, Vm}; + +//////////////////////////////////////////////////////////////////////////////// +// FunctionResult, FunctionState +//////////////////////////////////////////////////////////////////////////////// + +/// A result that instructs the VM what to do after a function finishes its execution. +#[derive(Debug)] +pub enum FunctionResult { + /// Take this value and push it on the stack as its return value. + ReturnPush(ObjP), + + /// Return value has already been pushed to the stack. + Return, + + /// Yield control to the VM with a state marker, and resume execution. + /// + /// This means that a new stack frame should have been pushed to the VM and resume execution + /// starting from there. + Yield(usize), +} + +impl From for FunctionResult { + fn from(other: ObjP) -> Self { + FunctionResult::ReturnPush(other) + } +} + +/// A function's resume state. +/// +/// When a builtin function is called, it may need to set up a stack frame for a new function, +/// yielding its control back to the VM. If it does this, then presumably, that function would like +/// to resume its execution where it left off. There's not really a way to capture a native +/// function's execution state in Rust (without tons of unsafe and not-really-worth-it black +/// magic), so instead a builtin function will accept a `FunctionState` value to give it a hint of +/// where it left off. +#[derive(Debug, Clone, Copy)] +pub enum FunctionState { + Begin, + Resume(usize), +} //////////////////////////////////////////////////////////////////////////////// // BuiltinFunctionInst //////////////////////////////////////////////////////////////////////////////// -pub type BuiltinFunctionPtr = fn(vm: &mut Vm, args: Vec) -> ObjP; +pub type BuiltinFunctionPtr = fn(vm: &mut Vm, function_state: FunctionState) -> FunctionResult; #[derive(Debug, Trace)] pub struct BuiltinFunctionInst { base: BaseObjInst, - name: String, + #[unsafe_ignore_trace] + name: Rc, #[unsafe_ignore_trace] function: BuiltinFunctionPtr, arity: Argc, @@ -27,7 +68,7 @@ impl BuiltinFunctionInst { pub fn new(name: impl ToString, function: BuiltinFunctionPtr, arity: Argc) -> Self { Self { base: Default::default(), - name: name.to_string(), + name: Rc::new(name.to_string()), function, arity, } @@ -66,16 +107,12 @@ impl Obj for BuiltinFunctionInst { } fn call(&self, vm: &mut Vm, argc: Argc) { - // args - let mut args = Vec::with_capacity(argc as usize); - for _ in 0..argc { - args.push(vm.pop()); - } - args.reverse(); - // callee (self) - vm.pop(); - let result = (self.function)(vm, args); - vm.push(result); + let new_frame = Frame::new( + Rc::clone(&self.name), + Function::Builtin(self.function, FunctionState::Begin), + vm.stack().len() - (argc as usize), + ); + vm.push_frame(new_frame); } fn equals(&self, other: &dyn Obj) -> bool { @@ -159,12 +196,11 @@ impl Obj for UserFunctionInst { fn call(&self, vm: &mut Vm, argc: Argc) { assert_eq!(argc, self.arity, "argc must match arity"); - let new_frame = Frame { - name: Rc::clone(&self.name), - chunk: Rc::clone(&self.chunk), - ip: 0, - stack_base: vm.stack().len() - (argc as usize), - }; + let new_frame = Frame::new( + Rc::clone(&self.name), + Function::Chunk(Rc::clone(&self.chunk)), + vm.stack().len() - (argc as usize), + ); vm.push_frame(new_frame); for capture in &self.captures { vm.push(capture.clone()); diff --git a/src/vm.rs b/src/vm.rs index 9e0f9b2..43e9a6c 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -31,6 +31,7 @@ pub enum Op { CloseOver { depth: ShortOpArg, slot: ShortOpArg }, // VM control + Nop, Halt, } @@ -61,19 +62,25 @@ pub struct Chunk { pub(crate) locals: Vec, } +#[derive(Debug)] +pub(crate) enum Function { + Chunk(Rc), + Builtin(BuiltinFunctionPtr, FunctionState), +} + #[derive(Debug)] pub struct Frame { pub(crate) name: Rc, - pub(crate) chunk: Rc, + pub(crate) function: Function, pub(crate) ip: usize, pub(crate) stack_base: usize, } impl Frame { - pub fn new(name: Rc, chunk: Rc, stack_base: usize) -> Self { + pub fn new(name: Rc, function: Function, stack_base: usize) -> Self { Self { name, - chunk, + function, ip: 0, stack_base, } @@ -115,7 +122,11 @@ impl Vm { // stack and frames let stack = Vec::new(); - let frames = vec![Frame::new("__main__".to_string().into(), chunk, 0)]; + let frames = vec![Frame::new( + "__main__".to_string().into(), + Function::Chunk(chunk), + 0, + )]; Vm { constants, @@ -137,6 +148,11 @@ impl Vm { &mut self.stack } + /// Gets the current stack, starting at the frame's stack base. + pub fn frame_stack(&self) -> &[ObjP] { + &self.stack()[self.frame().stack_base..] + } + /// Current stack frame. pub fn frame(&self) -> &Frame { self.frames.last().unwrap() @@ -158,8 +174,12 @@ impl Vm { } /// Gets the chunk of the currently executing frame. - pub fn chunk(&self) -> &Chunk { - &self.frame().chunk + pub fn chunk(&self) -> Option<&Chunk> { + if let Function::Chunk(chunk) = &self.frame().function { + Some(chunk) + } else { + None + } } /// Instruction pointer of the current frame. @@ -181,10 +201,48 @@ impl Vm { */ /// Get the current instruction and advance the IP. - fn next(&mut self) -> Op { - let ip = self.ip(); - self.set_ip(ip + 1); - self.chunk().code[ip] + /// + /// This may actually end up calling a function on top of the stack, if it's a builtin function. + fn dispatch(&mut self) -> Op { + let mut ip = self.ip(); + let op = match &self.frame().function { + Function::Chunk(chunk) => { + let op = chunk.code[ip]; + ip += 1; + op + } + Function::Builtin(function, state) => { + // keep track of where the current frame index is in case we need to yield + let frame_index = self.frames.len() - 1; + let result = (function)(self, *state); + match result { + FunctionResult::ReturnPush(value) => { + // push value to the stack and let the VM handle return protocols + self.push(value); + Op::Return + } + // value is already on top of the stack, let the VM handle return protocols + FunctionResult::Return => Op::Return, + // new stack frame has been pushed, yield control while keeping track of the + // old state + FunctionResult::Yield(resume_state) => { + // update the current state + if let Function::Builtin(_function, ref mut state) = + &mut self.frames[frame_index].function + { + *state = FunctionState::Resume(resume_state) + } else { + panic!("function stack got really messed up - function stack was changed under us"); + } + // inject a no-op so the VM can load a new instruction or dispatch a new + // instruction. + Op::Nop + } + } + } + }; + self.set_ip(ip); + op } /// Pop a value from the stack. @@ -207,7 +265,7 @@ impl Vm { let method_type = self.builtins().get("Method").unwrap().clone(); loop { - match self.next() { + match self.dispatch() { Op::Pop => { self.pop(); } @@ -216,14 +274,14 @@ impl Vm { self.push(constant); } Op::GetLocal(local_index) => { - let local = &self.chunk().locals[local_index as usize]; + let local = &self.chunk().expect("no chunk").locals[local_index as usize]; let value = Ptr::clone(&self.stack[self.frame().stack_base + local.slot as usize]); self.push(value); } Op::SetLocal(local_index) => { let value = self.pop(); - let local = &self.chunk().locals[local_index as usize]; + let local = &self.chunk().expect("no chunk").locals[local_index as usize]; let index = self.frame().stack_base + local.slot as usize; self.stack[index] = value; } @@ -340,6 +398,9 @@ impl Vm { fun.push_capture(value); self.push(make_ptr(fun)); } + Op::Nop => { + continue; + } Op::Halt => { break; }