Change up how function calls are handled

This is a big one.

For a while, builtin functions were a bit cumbersome and not easily
re-entrant. If you needed to call a function from within a builtin
function, the only method of doing so was to take a `FunctionState`
parameter, which would either be "Begin", meaning the function was being
called for the first time, or "Resume", meaning the function was being
re-entered. This meant that if we wanted to call another function within
this function, we'd have to set up a whole `match` statement to figure
out whether we were re-entering the function or starting out. It was a
mess and not very ergonomic, and most importantly, made it very
difficult to implement hashmaps.

Now, builtin functions are handled a little more elegantly. A native
function is pushed to the stack, where it is detected in the
`Vm::dispatch()` function. It is then called, like normal. If the
builtin function then needs to call *another* function, it will push
that function to the stack and call it, and then call `Vm::resume()` to
resume VM execution. `Vm::dispatch()` is then called again, this time
with the current function on top of the stack. If it's another builtin
function, the above is repeated. If it's a user-defined function, then
bytecode is executed in the main `loop` inside of resume. Ultimately, we
are able to compose builtin functions like we would any other internal
function to the program. Overall this should speed things up a little,
make them a whole lot easier to read, and make them a million times
easier to compose with other builtin parts of Rust.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2024-10-11 16:22:19 -07:00
parent 3fd0ba3a0b
commit c6e242c39d
12 changed files with 226 additions and 405 deletions

View File

@@ -22,56 +22,28 @@ pub fn init_global_builtins() {
}
}
// 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.
////////////////////////////////////////////////////////////////////////////////
// Global functions
////////////////////////////////////////////////////////////////////////////////
pub(crate) fn println(vm: &mut Vm, state: FunctionState) -> FunctionResult {
match state {
FunctionState::Begin => {
let obj = vm.peek();
pub(crate) fn println(vm: &mut Vm) -> ObjP {
let obj = vm.frame_stack()[0].clone();
let method = obj
.borrow()
.get_vtable_attr(obj.clone(), "to_str")
.expect("no to_str");
vm.push(method.clone());
method.borrow().call(vm, 0);
FunctionResult::Yield(0)
}
FunctionState::Resume(0) => {
println!("{}", vm.frame_stack().last().unwrap().borrow());
Nil::create().into()
}
_ => unreachable!(),
}
let result = vm.call(method, &[]);
println!("{}", result.borrow());
Nil::create()
}
pub(crate) fn print(vm: &mut Vm, state: FunctionState) -> FunctionResult {
match state {
FunctionState::Begin => {
let obj = vm.peek();
pub(crate) fn print(vm: &mut Vm) -> ObjP {
let obj = vm.frame_stack()[0].clone();
let method = obj
.borrow()
.get_vtable_attr(obj.clone(), "to_str")
.expect("no to_str");
vm.push(method.clone());
method.borrow().call(vm, 0);
FunctionResult::Yield(0)
}
FunctionState::Resume(0) => {
print!("{}", vm.frame_stack().last().unwrap().borrow());
Nil::create().into()
}
_ => unreachable!(),
}
let result = vm.call(method, &[]);
print!("{}", result.borrow());
Nil::create()
}

View File

@@ -56,7 +56,7 @@ fn main() {
// VM needs the module to be on top of the stack when it executes
vm.push(module.clone());
vm.enter_module(module);
vm.run();
vm.resume();
Ok(())
}

View File

@@ -48,7 +48,7 @@ pub mod prelude {
pub use crate::obj::{Nil, Obj};
// Other auxiliary types and functions
pub use crate::obj::function::{BuiltinFunctionPtr, FunctionResult, FunctionState};
pub use crate::obj::function::BuiltinFunctionPtr;
}
// TODO obj::with_obj_downcast - optimize downcasts of "known" types with an unchecked downcast
@@ -308,107 +308,77 @@ impl Obj {
// Common functions
//
pub(crate) fn to_str(vm: &mut Vm, state: FunctionState) -> FunctionResult {
match state {
FunctionState::Begin => {
pub(crate) fn to_str(vm: &mut Vm) -> ObjP {
let this = vm.frame_stack()[0].clone();
let method = this
.borrow()
.get_vtable_attr(this.clone(), "to_repr")
.clone()
.expect("no to_repr");
vm.push(method.clone());
method.borrow().call(vm, 0);
FunctionResult::Yield(0)
}
FunctionState::Resume(0) => FunctionResult::Return,
_ => unreachable!(),
}
vm.call(method, &[])
}
pub(crate) fn to_repr(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
Str::create(format!("{:?}", vm.frame_stack()[0].borrow())).into()
pub(crate) fn to_repr(vm: &mut Vm) -> ObjP {
Str::create(format!("{:?}", vm.frame_stack()[0].borrow()))
}
pub(crate) fn to_bool(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
Bool::create(vm.frame_stack()[0].borrow().is_truthy()).into()
pub(crate) fn to_bool(vm: &mut Vm) -> ObjP {
Bool::create(vm.frame_stack()[0].borrow().is_truthy())
}
//
// Operators
//
pub(crate) fn and(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn and(vm: &mut Vm) -> ObjP {
let lhs = vm.frame_stack()[0].borrow();
let rhs = vm.frame_stack()[1].borrow();
let result = lhs.is_truthy() && rhs.is_truthy();
Bool::create(result).into()
Bool::create(result)
}
pub(crate) fn or(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn or(vm: &mut Vm) -> ObjP {
let lhs = vm.frame_stack()[0].borrow();
let rhs = vm.frame_stack()[1].borrow();
let result = lhs.is_truthy() || rhs.is_truthy();
Bool::create(result).into()
Bool::create(result)
}
pub(crate) fn ne(vm: &mut Vm, state: FunctionState) -> FunctionResult {
match state {
FunctionState::Begin => {
// we actually want to be calling the lhs's eq function and then negate it
let lhs = vm.frame_stack()[0].clone();
let method = lhs
pub(crate) fn ne(vm: &mut Vm) -> ObjP {
let this = vm.frame_stack()[0].clone();
let method = this
.borrow()
.get_vtable_attr(lhs.clone(), "__eq__")
.get_vtable_attr(this.clone(), "__eq__")
.expect("no __eq__");
let rhs = vm.frame_stack()[1].clone();
vm.push(rhs);
method.borrow().call(vm, 1);
FunctionResult::Yield(0)
}
FunctionState::Resume(0) => {
let result = !vm.peek().borrow().is_truthy();
Bool::create(result).into()
}
_ => {
unreachable!()
}
}
let result = vm.call(method, &[rhs]);
let result_borrowed = result.borrow();
Bool::create(!result_borrowed.is_truthy())
}
pub(crate) fn eq(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn eq(vm: &mut Vm) -> ObjP {
let lhs = vm.frame_stack()[0].borrow();
let rhs = vm.frame_stack()[1].borrow();
let equals = lhs.equals(&*rhs);
Bool::create(equals).into()
Bool::create(equals)
}
pub(crate) fn not(vm: &mut Vm, state: FunctionState) -> FunctionResult {
match state {
FunctionState::Begin => {
pub(crate) fn not(vm: &mut Vm) -> ObjP {
let obj = vm.peek();
let method = obj
.borrow()
.get_vtable_attr(obj.clone(), "to_bool")
.expect("no to_bool");
vm.push(method.clone());
method.borrow().call(vm, 0);
FunctionResult::Yield(0)
}
FunctionState::Resume(0) => {
let value = vm.peek().borrow().is_truthy();
Bool::create(!value).into()
}
_ => unreachable!(),
}
let result = vm.call(method, &[]);
let result_borrowed = result.borrow();
Bool::create(!result_borrowed.is_truthy())
}
//
// Not implemented
//
pub(crate) fn not_implemented_un(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn not_implemented_un(vm: &mut Vm) -> ObjP {
let fname = &vm.frame().name;
// TODO Obj::not_implemented_un - throw an exception of some kind for not
// implemented/not available errors on unary operators
@@ -420,7 +390,7 @@ impl Obj {
)
}
pub(crate) fn not_implemented_bin(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn not_implemented_bin(vm: &mut Vm) -> ObjP {
// TODO Obj::not_implemented_un - throw an exception of some kind for not
// implemented/not available errors on unary operators
// BLOCKED-ON: exceptions
@@ -432,29 +402,19 @@ impl Obj {
// Do call
//
pub(crate) fn do_call(vm: &mut Vm, state: FunctionState) -> FunctionResult {
pub(crate) fn do_call(vm: &mut Vm) -> ObjP {
let ty = vm.frame_stack()[0].clone();
match state {
FunctionState::Begin => {
let argc = vm.frame_stack().len() - 1;
let mut obj = Obj::new();
obj.set_attr("__ty__", ty.clone());
obj.instantiate();
let obj_ptr = make_ptr(obj);
let init = obj_ptr
.borrow()
.get_vtable_attr(obj_ptr.clone(), "__init__")
.expect("no __init__");
// check arity against __init__ function
// TODO Obj::do_call - need to throw an exception when __init__ arity does not
// match what was passed to __call__
// Also need to throw an exception when __init__ is not a
// function or doesn't have an arity
// BLOCKED-ON: exceptions
if let Some(arity) = init.borrow().arity() {
if argc != arity as usize {
todo!(
@@ -466,29 +426,14 @@ impl Obj {
todo!("TODO - throw an exception when __init__ member does not have an arity or is not a function");
}
vm.push(obj_ptr);
vm.push(init.clone());
// duplicate arguments that were pushed to the frame
for i in 0..argc {
let arg = vm.frame_stack()[i + 1].clone();
vm.push(arg);
let argv = Vec::from(&vm.frame_stack()[1..]);
vm.call(init, &argv);
obj_ptr
}
init.borrow().call(vm, argc as Argc);
FunctionResult::Yield(0)
}
FunctionState::Resume(0) => {
vm.pop();
FunctionResult::Return
}
_ => unreachable!(),
}
}
pub(crate) fn init(_vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn init(_vm: &mut Vm) -> ObjP {
// no-op
Nil::create().into()
Nil::create()
}
}
@@ -538,12 +483,12 @@ impl Object for Nil {
//
impl Nil {
pub(crate) fn do_call(_vm: &mut Vm, _state: FunctionState) -> FunctionResult {
FunctionResult::ReturnPush(Nil::create())
pub(crate) fn do_call(_vm: &mut Vm) -> ObjP {
Nil::create()
}
pub(crate) fn init(_vm: &mut Vm, _state: FunctionState) -> FunctionResult {
FunctionResult::ReturnPush(Nil::create())
pub(crate) fn init(_vm: &mut Vm) -> ObjP {
Nil::create()
}
}

View File

@@ -58,19 +58,19 @@ impl Object for Bool {
impl Bool {
impl_do_call!(to_bool);
pub(crate) fn init(_vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn init(_vm: &mut Vm) -> ObjP {
// This is a no-op. We don't want the user-exposed `__init__` function to do anything,
// instantiation is done in the `__call__` function.
FunctionResult::ReturnPush(Nil::create())
Nil::create()
}
pub(crate) fn to_int(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn to_int(vm: &mut Vm) -> ObjP {
let bool_value = with_obj_downcast(vm.frame_stack()[0].clone(), Bool::bool_value);
Int::create(bool_value as i64).into()
Int::create(bool_value as i64)
}
pub(crate) fn to_float(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn to_float(vm: &mut Vm) -> ObjP {
let bool_value = with_obj_downcast(vm.frame_stack()[0].clone(), Bool::bool_value);
Float::create(bool_value as i64 as f64).into()
Float::create(bool_value as i64 as f64)
}
}

View File

@@ -68,7 +68,7 @@ impl Object for Float {
macro_rules! float_bin_op_math {
($function:ident, $op:tt) => {
pub(crate) fn $function(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn $function(vm: &mut Vm) -> ObjP {
let lhs = vm.frame_stack()[0].clone();
let rhs = vm.frame_stack()[1].clone();
@@ -86,14 +86,14 @@ macro_rules! float_bin_op_math {
rhs.borrow().ty_name()
)
};
result.into()
result
}
}
}
macro_rules! float_bin_op_logical {
($function:ident, $op:tt) => {
pub(crate) fn $function(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn $function(vm: &mut Vm) -> ObjP {
let lhs = vm.frame_stack()[0].clone();
let rhs = vm.frame_stack()[1].clone();
@@ -111,7 +111,7 @@ macro_rules! float_bin_op_logical {
rhs.borrow().ty_name()
)
};
result.into()
result
}
}
}
@@ -119,19 +119,19 @@ macro_rules! float_bin_op_logical {
impl Float {
impl_do_call!(to_float);
pub(crate) fn init(_vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn init(_vm: &mut Vm) -> ObjP {
// This is a no-op. We don't want the user-exposed `__init__` function to do anything,
// instantiation is done in the `__call__` function.
FunctionResult::ReturnPush(Nil::create())
Nil::create()
}
pub(crate) fn to_int(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn to_int(vm: &mut Vm) -> ObjP {
let float_value = with_obj_downcast(vm.frame_stack()[0].clone(), Float::float_value);
Int::create(float_value as i64).into()
Int::create(float_value as i64)
}
pub(crate) fn to_float(_vm: &mut Vm, _state: FunctionState) -> FunctionResult {
FunctionResult::Return
pub(crate) fn to_float(vm: &mut Vm) -> ObjP {
vm.peek()
}
float_bin_op_math!(add, +);
@@ -147,15 +147,15 @@ impl Float {
float_bin_op_logical!(lt, <);
float_bin_op_logical!(le, <=);
pub(crate) fn pos(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn pos(vm: &mut Vm) -> ObjP {
let lhs = vm.frame_stack()[0].clone();
let value = with_obj_downcast(lhs, Float::float_value);
Float::create(value.abs()).into()
Float::create(value.abs())
}
pub(crate) fn neg(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn neg(vm: &mut Vm) -> ObjP {
let lhs = vm.frame_stack()[0].clone();
let value = with_obj_downcast(lhs, Float::float_value);
Float::create(-value).into()
Float::create(-value)
}
}

View File

@@ -7,51 +7,11 @@ use crate::obj::macros::*;
use crate::obj::prelude::*;
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<ObjP> 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),
}
////////////////////////////////////////////////////////////////////////////////
// BuiltinFunction
////////////////////////////////////////////////////////////////////////////////
pub type BuiltinFunctionPtr = fn(vm: &mut Vm, function_state: FunctionState) -> FunctionResult;
pub type BuiltinFunctionPtr = fn(vm: &mut Vm) -> ObjP;
#[derive(Trace, Finalize)]
pub struct BuiltinFunction {
@@ -94,7 +54,7 @@ impl Debug for BuiltinFunction {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"<BuiltinFunction {}/{} at 0x{:x}>",
"<BuiltinFunction {}/{} at {:#x}>",
self.name(),
self.arity().unwrap(),
self.function as *const BuiltinFunctionPtr as usize
@@ -110,7 +70,7 @@ impl Object for BuiltinFunction {
fn call(&self, vm: &mut Vm, argc: Argc) {
let new_frame = Frame::new(
Rc::clone(&self.name),
Function::Builtin(self.function, FunctionState::Begin),
Function::Builtin(self.function),
vm.stack().len() - (argc as usize),
);
vm.push_frame(new_frame);
@@ -268,7 +228,7 @@ impl Debug for Method {
};
write!(
fmt,
"<Method {}.{}/{} at {}>",
"<Method {}.{}/{} at {:#x}>",
self.self_binding().borrow().ty_name(),
function_name,
self.function.borrow().arity().unwrap(),

View File

@@ -63,7 +63,7 @@ impl Object for Int {
macro_rules! int_bin_op_math {
($function:ident, $op:tt) => {
pub(crate) fn $function(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn $function(vm: &mut Vm) -> ObjP {
let lhs = vm.frame_stack()[0].clone();
let rhs = vm.frame_stack()[1].clone();
@@ -81,14 +81,14 @@ macro_rules! int_bin_op_math {
rhs.borrow().ty_name()
)
};
result.into()
result
}
}
}
macro_rules! int_bin_op_logical {
($function:ident, $op:tt) => {
pub(crate) fn $function(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn $function(vm: &mut Vm) -> ObjP {
let lhs = vm.frame_stack()[0].clone();
let rhs = vm.frame_stack()[1].clone();
@@ -106,33 +106,33 @@ macro_rules! int_bin_op_logical {
rhs.borrow().ty_name()
)
};
result.into()
result
}
}
}
impl Int {
pub(crate) fn to_int(_vm: &mut Vm, _state: FunctionState) -> FunctionResult {
FunctionResult::Return
pub(crate) fn to_int(vm: &mut Vm) -> ObjP {
vm.peek()
}
pub(crate) fn to_float(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn to_float(vm: &mut Vm) -> ObjP {
let int_value = with_obj_downcast(vm.frame_stack()[0].clone(), Int::int_value);
Float::create(int_value as f64).into()
Float::create(int_value as f64)
}
impl_do_call!(to_int);
pub(crate) fn init(_vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn init(_vm: &mut Vm) -> ObjP {
// This is a no-op. We don't want the user-exposed `__init__` function to do anything,
// instantiation is done in the `__call__` function.
FunctionResult::ReturnPush(Nil::create())
Nil::create()
}
int_bin_op_math!(add, +);
int_bin_op_math!(sub, -);
pub(crate) fn mul(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn mul(vm: &mut Vm) -> ObjP {
// can't bin_op_math this one because it needs the string case
let lhs = vm.frame_stack()[0].clone();
let rhs = vm.frame_stack()[1].clone();
@@ -155,7 +155,7 @@ impl Int {
rhs.borrow().ty_name()
)
};
result.into()
result
}
// TODO Int::div - handle divide by zero
@@ -171,15 +171,15 @@ impl Int {
int_bin_op_logical!(lt, <);
int_bin_op_logical!(le, <=);
pub(crate) fn pos(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn pos(vm: &mut Vm) -> ObjP {
let lhs = vm.frame_stack()[0].clone();
let value = with_obj_downcast(lhs, Int::int_value);
Int::create(value.abs()).into()
Int::create(value.abs())
}
pub(crate) fn neg(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn neg(vm: &mut Vm) -> ObjP {
let lhs = vm.frame_stack()[0].clone();
let value = with_obj_downcast(lhs, Int::int_value);
Int::create(-value).into()
Int::create(-value)
}
}

View File

@@ -61,77 +61,44 @@ impl Object for List {
// List function implementations
////////////////////////////////////////////////////////////////////////////////
impl List {
pub(crate) fn to_repr(vm: &mut Vm, state: FunctionState) -> FunctionResult {
// This function is a bit more complicated than the rest because it needs to effectively
// loop over the elements and call them, without using a loop. Thus, we use a sort-of state
// machine with the function state.
//
// When we begin, we check if the list is empty. If that's the case, then we just return
// the "empty list" string. Otherwise, we push two "locals", the string we're building, and
// the current list index, to the stack. Then, we call `to_repr` on the first item in the
// list, and yield execution to the VM with state 0.
//
// When function resumes, we get the return value off the top of the stack, append it to
// the current string, increment the index, and continue, until the string is fully built.
// This function needs to keep track of the string that we're building, plus the current
// index, on the VM stack.
let this_ptr = vm.frame_stack()[0].clone();
let this = this_ptr.borrow();
let this = this.as_any().downcast_ref::<List>().unwrap();
match state {
FunctionState::Begin => {
// empty list, exit early
if this.list().len() == 0 {
return FunctionResult::ReturnPush(Str::create("[]"));
pub(crate) fn to_repr(vm: &mut Vm) -> ObjP {
let this = vm.frame_stack()[0].clone();
let this_borrowed = this.borrow();
let list_obj = this_borrowed.as_any().downcast_ref::<List>().unwrap();
if list_obj.list().len() == 0 {
return Str::create("[]");
}
let string = Str::create("[");
vm.push(string);
let index = Int::create(0);
vm.push(index);
let mut repr = "[".to_string();
let item = this.list()[0].clone();
let method = item
// first item
{
let first = list_obj.list()[0].clone();
let method = first
.borrow()
.get_vtable_attr(item.clone(), "to_repr")
.get_vtable_attr(first.clone(), "to_repr")
.expect("no to_repr");
vm.push(method.clone());
method.borrow().call(vm, 0);
FunctionResult::Yield(0)
let result = vm.call(method, &[]);
repr += &format!("{}", result.borrow());
}
FunctionState::Resume(0) => {
let build_str = vm.frame_stack()[1].clone();
// putting the "1 +" in front so we don't forget that it's there
let index =
1 + with_obj_downcast(vm.frame_stack()[2].clone(), Int::int_value) as usize;
let repr_str = vm.pop();
if index == this.list().len() {
// if this is the last item in the list, then we're done
let new_str = format!("{}{}]", build_str.borrow(), repr_str.borrow());
Str::create(new_str).into()
} else {
// otherwise, continue building the string and calling to_repr
let new_str = format!("{}{}, ", build_str.borrow(), repr_str.borrow());
vm.frame_stack_mut()[1] = Str::create(new_str);
vm.frame_stack_mut()[2] = Int::create(index as i64);
let item = this.list()[index].clone();
let method = item
// remaining items
for obj in &list_obj.list()[1..] {
repr += ", ";
let method = obj
.borrow()
.get_vtable_attr(item.clone(), "to_repr")
.get_vtable_attr(obj.clone(), "to_repr")
.expect("no to_repr");
vm.push(method.clone());
method.borrow().call(vm, 0);
FunctionResult::Yield(0)
}
}
_ => unreachable!(),
}
let result = vm.call(method, &[]);
repr += &format!("{}", result.borrow());
}
pub(crate) fn to_list(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
repr += "]";
return Str::create(repr);
}
pub(crate) fn to_list(vm: &mut Vm) -> ObjP {
// create a clone of this list
let this = vm.frame_stack()[0].clone();
let list_items = with_obj_downcast(this, |list: &List| list.list().clone());
@@ -140,13 +107,13 @@ impl List {
impl_do_call!(to_list);
pub(crate) fn init(_vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn init(_vm: &mut Vm) -> ObjP {
// This is a no-op. We don't want the user-exposed `__init__` function to do anything,
// instantiation is done in the `__call__` function.
FunctionResult::ReturnPush(Nil::create())
Nil::create()
}
pub(crate) fn index(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn index(vm: &mut Vm) -> ObjP {
let this = vm.frame_stack()[0].clone();
let index_obj = vm.frame_stack()[1].clone();
@@ -175,23 +142,23 @@ impl List {
}
});
item.into()
item
}
pub(crate) fn len(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn len(vm: &mut Vm) -> ObjP {
let this = vm.frame_stack()[0].clone();
let len = with_obj_downcast(this, |list: &List| list.list().len());
Int::create(len as i64).into()
Int::create(len as i64)
}
pub(crate) fn push(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn push(vm: &mut Vm) -> ObjP {
let this = vm.frame_stack()[0].clone();
let arg = vm.frame_stack()[1].clone();
with_obj_downcast_mut(this, |list: &mut List| list.list_mut().push(arg));
Nil::create().into()
Nil::create()
}
pub(crate) fn pop(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn pop(vm: &mut Vm) -> ObjP {
let this = vm.frame_stack()[0].clone();
let last = if let Some(last) =
with_obj_downcast_mut(this, |list: &mut List| list.list_mut().pop())
@@ -203,10 +170,10 @@ impl List {
todo!("throw an exception when the list is empty and there is nothing to pop")
};
last.into()
last
}
pub(crate) fn extend(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn extend(vm: &mut Vm) -> ObjP {
let this = vm.frame_stack()[0].clone();
let arg = vm.frame_stack()[1].clone();
@@ -224,6 +191,6 @@ impl List {
list.list_mut().extend(list_extension);
});
Nil::create().into()
Nil::create()
}
}

View File

@@ -52,14 +52,8 @@ macro_rules! impl_create {
macro_rules! impl_do_call {
($name:ident) => {
pub(crate) fn do_call(
vm: &mut $crate::vm::Vm,
state: $crate::obj::function::FunctionState,
) -> $crate::obj::function::FunctionResult {
match state {
$crate::obj::function::FunctionState::Begin => {
// get the top item off the stack and call to_float on it
let arg = vm.peek();
pub(crate) fn do_call(vm: &mut $crate::vm::Vm) -> ObjP {
let arg = vm.frame_stack().last().unwrap().clone();
let method = if let Some(method) =
arg.borrow().get_vtable_attr(arg.clone(), stringify!($name))
{
@@ -73,17 +67,8 @@ macro_rules! impl_do_call {
arg.borrow().ty_name()
);
};
vm.push(method.clone());
method.borrow().call(vm, 0);
// resume execution
$crate::obj::function::FunctionResult::Yield(0)
}
$crate::obj::function::FunctionState::Resume(0) => {
$crate::obj::function::FunctionResult::Return
}
_ => unreachable!(),
}
vm.call(method, &[])
}
};
}

View File

@@ -63,59 +63,59 @@ impl Object for Str {
impl Str {
impl_do_call!(to_str);
pub(crate) fn init(_vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn init(_vm: &mut Vm) -> ObjP {
// This is a no-op. We don't want the user-exposed `__init__` function to do anything,
// instantiation is done in the `__call__` function.
FunctionResult::ReturnPush(Nil::create())
Nil::create()
}
pub(crate) fn to_str(_vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn to_str(vm: &mut Vm) -> ObjP {
// top item of the stack should just be ourselves, so return immediately
FunctionResult::Return
vm.peek()
}
pub(crate) fn to_int(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn to_int(vm: &mut Vm) -> ObjP {
let parsed: Result<i64, _> =
with_obj_downcast(vm.frame_stack()[0].clone(), |str_inst: &Str| {
str_inst.str_value().parse()
});
match parsed {
Ok(int) => Int::create(int).into(),
Ok(int) => Int::create(int),
// TODO Str::to_int - throw an exception when we fail to parse an integer
// BLOCKED-ON - exceptions
Err(e) => todo!("error parsing string to an integer: {}", e),
}
}
pub(crate) fn to_float(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn to_float(vm: &mut Vm) -> ObjP {
let parsed: Result<f64, _> =
with_obj_downcast(vm.frame_stack()[0].clone(), |str_inst: &Str| {
str_inst.str_value().parse()
});
match parsed {
Ok(float) => Float::create(float).into(),
Ok(float) => Float::create(float),
// TODO Str::to_int - throw an exception when we fail to parse an integer
// BLOCKED-ON - exceptions
Err(e) => todo!("error parsing string to a float: {}", e),
}
}
pub(crate) fn to_list(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn to_list(vm: &mut Vm) -> ObjP {
let this = vm.frame_stack()[0].clone();
let list_items = with_obj_downcast(this, |str: &Str| {
str.str_value().chars().map(|c| Str::create(c)).collect()
});
List::create(list_items).into()
List::create(list_items)
}
pub(crate) fn len(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn len(vm: &mut Vm) -> ObjP {
let len = with_obj_downcast(vm.frame_stack()[0].clone(), |str_inst: &Str| {
str_inst.str_value().len() as i64
});
Int::create(len).into()
Int::create(len)
}
pub(crate) fn add(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn add(vm: &mut Vm) -> ObjP {
let lhs = vm.frame_stack()[0].clone();
let rhs = vm.frame_stack()[1].clone();
if !obj_is_inst::<Str>(&rhs) {
@@ -128,10 +128,10 @@ impl Str {
}
let new = format!("{}{}", lhs.borrow(), rhs.borrow());
Str::create(new).into()
Str::create(new)
}
pub(crate) fn mul(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn mul(vm: &mut Vm) -> ObjP {
let lhs = vm.frame_stack()[0].clone();
let rhs = vm.frame_stack()[1].clone();
let repeat_count = if let Some(int_inst) = rhs.borrow().as_any().downcast_ref::<Int>() {
@@ -146,10 +146,10 @@ impl Str {
};
let repeat_count = repeat_count.max(0) as usize;
let new = format!("{}", lhs.borrow()).repeat(repeat_count);
Str::create(new).into()
Str::create(new)
}
pub(crate) fn index(vm: &mut Vm, _state: FunctionState) -> FunctionResult {
pub(crate) fn index(vm: &mut Vm) -> ObjP {
let this = vm.frame_stack()[0].clone();
let index_obj = vm.frame_stack()[1].clone();

View File

@@ -91,6 +91,7 @@ impl Object for Ty {
.vtable
.get("__call__")
.expect("Why does a type not have a __call__ member?");
function.borrow().call(vm, argc + 1);
}

View File

@@ -71,7 +71,7 @@ pub struct Chunk {
pub(crate) enum Function {
Chunk(Rc<Chunk>),
Module(Rc<Chunk>),
Builtin(BuiltinFunctionPtr, FunctionState),
Builtin(BuiltinFunctionPtr),
}
#[derive(Debug)]
@@ -158,7 +158,7 @@ impl<'c> Vm<'c> {
pub fn chunk(&self) -> Option<&Chunk> {
match &self.frame().function {
Function::Chunk(chunk) | Function::Module(chunk) => Some(chunk),
Function::Builtin(_, _) => None,
Function::Builtin(_) => None,
}
}
@@ -278,35 +278,12 @@ impl<'c> Vm<'c> {
ip += 1;
op
}
Function::Builtin(function, state) => {
Function::Builtin(function) => {
// 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);
let result = (function)(self);
self.push(result);
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
@@ -327,7 +304,17 @@ impl<'c> Vm<'c> {
self.stack.push(value);
}
pub fn run(&mut self) {
pub fn call(&mut self, fun: ObjP, argv: &[ObjP]) -> ObjP {
self.push(fun.clone());
for arg in argv {
self.push(arg.clone());
}
fun.borrow().call(self, argv.len() as Argc);
self.resume();
self.pop()
}
pub fn resume(&mut self) {
loop {
let op = self.dispatch();
@@ -450,6 +437,10 @@ impl<'c> Vm<'c> {
// also pop the function object off of the stack
self.stack.pop();
self.push(return_value);
if matches!(self.frame().function, Function::Builtin(_)) {
return;
}
}
Op::CloseOver { depth, slot } => {
// since we're closing over a value, and functions ultimately come from