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 <alekratz@gmail.com>
This commit is contained in:
2024-09-24 11:34:07 -07:00
parent d69a60f42c
commit 3a7c04686a
5 changed files with 220 additions and 75 deletions

View File

@@ -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<Local>,
}
#[derive(Debug)]
pub(crate) enum Function {
Chunk(Rc<Chunk>),
Builtin(BuiltinFunctionPtr, FunctionState),
}
#[derive(Debug)]
pub struct Frame {
pub(crate) name: Rc<String>,
pub(crate) chunk: Rc<Chunk>,
pub(crate) function: Function,
pub(crate) ip: usize,
pub(crate) stack_base: usize,
}
impl Frame {
pub fn new(name: Rc<String>, chunk: Rc<Chunk>, stack_base: usize) -> Self {
pub fn new(name: Rc<String>, 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;
}