Add internal error handling to VM, plus function arity

* VM is able to handle basic runtime errors although there is no way to
  catch this in executing code currently
* If a function is called with the wrong number of arguments (arity) it
  will invoke a runtime error.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2020-10-13 14:07:22 -07:00
parent c738c52455
commit 902da3f2f3
9 changed files with 98 additions and 50 deletions

View File

@@ -1,4 +1,4 @@
use not_python::{syn::ast, compile::Compile, vm::{Vm, signal::*}}; use not_python::{syn::ast, compile::Compile, vm::Vm};
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use structopt::StructOpt; use structopt::StructOpt;
@@ -57,8 +57,7 @@ fn main() -> Result<()> {
if action == "run" { if action == "run" {
let mut vm = Vm::new(&const_pool); let mut vm = Vm::new(&const_pool);
vm.handle_signal(Signal::Call(main, vec![])); vm.call(main, vec![])?;
vm.resume();
} }
Ok(()) Ok(())

View File

@@ -42,7 +42,7 @@ impl Compile {
sym sym
}) })
.collect(); .collect();
Ok((self.const_data.const_pool, UserFun::new_obj(main, globals))) Ok((self.const_data.const_pool, UserFun::new_obj(main, globals, 0)))
} }
/// Gets the constant data that is interned in this compile session. /// Gets the constant data that is interned in this compile session.

View File

@@ -407,7 +407,7 @@ impl Visit for CompileBody<'_> {
}) })
.collect(); .collect();
let (hdl, _fun) = self.compile.push_const(UserFun::new_obj(code, locals)); let (hdl, _fun) = self.compile.push_const(UserFun::new_obj(code, locals, expr.params.len()));
// TODO(compile) : determine return value at the end of the body (preferably at parse-time) // TODO(compile) : determine return value at the end of the body (preferably at parse-time)

View File

@@ -1,45 +1,46 @@
use crate::{obj::{prelude::*, reserved::*}, vm::signal::*}; use crate::{obj::{prelude::*, reserved::*}, vm::{error::*, signal::*}};
use maplit::btreemap; use maplit::btreemap;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::collections::BTreeMap; use std::collections::BTreeMap;
pub static PRINTLN_BUILTIN_FUN: Lazy<NativeFunRef> = Lazy::new(|| NativeFun::new_obj(|_, vm, args| { pub static PRINTLN_BUILTIN_FUN: Lazy<NativeFunRef> = Lazy::new(|| NativeFun::new_obj(1, |_, vm, args| {
// TODO : use __get_attr__ when it gets added // TODO : use __get_attr__ when it gets added
let to_string = { let to_string = {
let obj_ref = &args[0]; let obj_ref = &args[0];
read_obj!(let obj = obj_ref); read_obj!(let obj = obj_ref);
obj.get_attr(STR_MEMBER_NAME.sym) obj.get_attr(STR_MEMBER_NAME.sym)
.or_else(|| obj.get_attr(REPR_MEMBER_NAME.sym)) .ok_or(Error::MissingAttr { attr: STR_MEMBER_NAME.sym })?
.expect("no __str__ or __repr__ member")
}; };
let return_value = vm.call(to_string, vec![]); let return_value = vm.call(to_string, vec![])?;
{ {
read_obj!(let str_obj = return_value); read_obj!(let str_obj = return_value);
let str_obj: &Str = str_obj.as_any().downcast_ref().unwrap(); let str_obj: &Str = str_obj.as_any().downcast_ref()
.ok_or_else(|| Error::ValueError { error: "expected str value".to_string(), value: return_value.clone() })?;
println!("{}", str_obj.value()); println!("{}", str_obj.value());
} }
vm.push(NIL_NAME.sym_ref()); vm.push(NIL_NAME.sym_ref());
Signal::Return Ok(Signal::Return)
})); }));
pub static PRINT_BUILTIN_FUN: Lazy<NativeFunRef> = Lazy::new(|| NativeFun::new_obj(|_, vm, args| { pub static PRINT_BUILTIN_FUN: Lazy<NativeFunRef> = Lazy::new(|| NativeFun::new_obj(1, |_, vm, args| {
// TODO : use __get_attr__ when it gets added // TODO : use __get_attr__ when it gets added
let to_string = { let to_string = {
let obj_ref = &args[0]; let obj_ref = &args[0];
read_obj!(let obj = obj_ref); read_obj!(let obj = obj_ref);
obj.get_attr(STR_MEMBER_NAME.sym) obj.get_attr(STR_MEMBER_NAME.sym)
.or_else(|| obj.get_attr(REPR_MEMBER_NAME.sym)) .ok_or(Error::MissingAttr { attr: STR_MEMBER_NAME.sym })?
.expect("no __str__ or __repr__ member")
}; };
let return_value = vm.call(to_string, vec![]); let return_value = vm.call(to_string, vec![])?;
{ {
read_obj!(let str_obj = return_value); read_obj!(let str_obj = return_value);
let str_obj: &Str = str_obj.as_any().downcast_ref().unwrap(); let str_obj: &Str = str_obj.as_any().downcast_ref()
.ok_or_else(|| Error::ValueError { error: "expected str value".to_string(), value: return_value.clone() })?;
print!("{}", str_obj.value()); print!("{}", str_obj.value());
} }
Signal::Return vm.push(NIL_NAME.sym_ref());
Ok(Signal::Return)
})); }));
pub static BUILTIN_OBJS: Lazy<BTreeMap<Sym, ObjRef>> = Lazy::new(|| btreemap! { pub static BUILTIN_OBJS: Lazy<BTreeMap<Sym, ObjRef>> = Lazy::new(|| btreemap! {

View File

@@ -1,4 +1,4 @@
use crate::{obj::{reserved::*, prelude::*}, vm::{consts::ConstPool, frame::*, inst::Inst, signal::*, Vm}}; use crate::{obj::{reserved::*, prelude::*}, vm::{consts::ConstPool, error::*, frame::*, inst::Inst, signal::*, Vm}};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use shredder::{GcSafeWrapper, Scan}; use shredder::{GcSafeWrapper, Scan};
use std::{fmt::{Debug, Formatter, self}, io::{self, Write}, sync::Arc}; use std::{fmt::{Debug, Formatter, self}, io::{self, Write}, sync::Arc};
@@ -8,7 +8,7 @@ pub type FunLocals = Vec<Sym>;
/// A function object which can create a new stack frame. /// A function object which can create a new stack frame.
pub trait Fun { pub trait Fun {
/// Creates a new `vm::Frame` from this function object. /// Creates a new `vm::Frame` from this function object.
fn create_frame(&self, callee: ObjRef, vm: &Vm, _args: Vec<ObjRef>) -> Frame; fn create_frame(&self, callee: ObjRef, vm: &Vm, _args: Vec<ObjRef>) -> Result<Frame>;
} }
pub type MethodRef = ObjRef<Method>; pub type MethodRef = ObjRef<Method>;
@@ -55,7 +55,7 @@ impl Obj for Method {
} }
impl Fun for Method { impl Fun for Method {
fn create_frame(&self, _callee: ObjRef, vm: &Vm, mut args: Vec<ObjRef>) -> Frame { fn create_frame(&self, _callee: ObjRef, vm: &Vm, mut args: Vec<ObjRef>) -> Result<Frame> {
// insert self argument // insert self argument
args.insert(0, self.get_attr(SELF_MEMBER_NAME.sym).unwrap()); args.insert(0, self.get_attr(SELF_MEMBER_NAME.sym).unwrap());
// get function and use its create_frame method with the updated args // get function and use its create_frame method with the updated args
@@ -82,16 +82,18 @@ pub struct UserFun {
// Safe because this is just an interner that points to symbols, which aren't GC'd // Safe because this is just an interner that points to symbols, which aren't GC'd
#[shredder(unsafe_skip)] #[shredder(unsafe_skip)]
locals: FunLocals, locals: FunLocals,
arity: usize,
} }
impl UserFun { impl UserFun {
pub fn new_obj(code: Vec<Inst>, locals: FunLocals) -> UserFunRef { pub fn new_obj(code: Vec<Inst>, locals: FunLocals, arity: usize) -> UserFunRef {
self_referring_obj! ( self_referring_obj! (
Self { Self {
vtable: Default::default(), // this is a placeholder for the real vtable vtable: Default::default(), // this is a placeholder for the real vtable
attrs: Default::default(), attrs: Default::default(),
code: Arc::new(code), code: Arc::new(code),
locals, locals,
arity,
}, },
vtable: |obj_ref: ObjRef| vtable! { vtable: |obj_ref: ObjRef| vtable! {
TY_MEMBER_NAME.sym => USER_FUN_TY.clone(), TY_MEMBER_NAME.sym => USER_FUN_TY.clone(),
@@ -215,13 +217,19 @@ impl Obj for UserFun {
} }
impl Fun for UserFun { impl Fun for UserFun {
fn create_frame(&self, callee: ObjRef, vm: &Vm, args: Vec<ObjRef>) -> Frame { fn create_frame(&self, callee: ObjRef, vm: &Vm, args: Vec<ObjRef>) -> Result<Frame> {
if args.len() != self.arity {
return Err(Error::ArityError {
expected: self.arity,
got: args.len(),
});
}
let bindings: FrameBindings = args.into_iter() let bindings: FrameBindings = args.into_iter()
.enumerate() .enumerate()
.collect(); .collect();
Frame::User( Ok(Frame::User(
UserFrame::new(callee, bindings, vm.pc(), Arc::clone(&self.code)) UserFrame::new(callee, bindings, vm.pc(), Arc::clone(&self.code))
) ))
} }
} }
@@ -241,7 +249,7 @@ pub static USER_FUN_TY: Lazy<ObjRef<Ty>> = Lazy::new(|| Ty::new_obj(USER_FUN_NAM
// struct NativeFun // struct NativeFun
// //
pub type NativeFunPtr = fn(ObjRef, &mut Vm, Vec<ObjRef>) -> Signal; pub type NativeFunPtr = fn(ObjRef, &mut Vm, Vec<ObjRef>) -> Result<Signal>;
pub type NativeFunRef = ObjRef<NativeFun>; pub type NativeFunRef = ObjRef<NativeFun>;
#[derive(Scan)] #[derive(Scan)]
@@ -250,6 +258,7 @@ pub struct NativeFun {
attrs: Attrs, attrs: Attrs,
#[shredder(skip)] #[shredder(skip)]
fun: GcSafeWrapper<NativeFunPtr>, fun: GcSafeWrapper<NativeFunPtr>,
arity: usize,
} }
// //
@@ -257,12 +266,13 @@ pub struct NativeFun {
// //
impl NativeFun { impl NativeFun {
pub fn new_obj(fun: NativeFunPtr) -> ObjRef<Self> { pub fn new_obj(arity: usize, fun: NativeFunPtr) -> ObjRef<Self> {
self_referring_obj! ( self_referring_obj! (
Self { Self {
vtable: Default::default(), vtable: Default::default(),
attrs: Default::default(), attrs: Default::default(),
fun: GcSafeWrapper::new(fun), fun: GcSafeWrapper::new(fun),
arity,
}, },
vtable: |obj_ref: ObjRef| vtable! { vtable: |obj_ref: ObjRef| vtable! {
TY_MEMBER_NAME.sym => NATIVE_FUN_TY.clone(), TY_MEMBER_NAME.sym => NATIVE_FUN_TY.clone(),
@@ -313,10 +323,16 @@ impl Obj for NativeFun {
} }
impl Fun for NativeFun { impl Fun for NativeFun {
fn create_frame(&self, callee: ObjRef, _vm: &Vm, args: Vec<ObjRef>) -> Frame { fn create_frame(&self, callee: ObjRef, _vm: &Vm, args: Vec<ObjRef>) -> Result<Frame> {
Frame::Native( if args.len() != self.arity {
return Err(Error::ArityError {
expected: self.arity,
got: args.len(),
});
}
Ok(Frame::Native(
NativeFrame::new(callee, *self.fun, args) NativeFrame::new(callee, *self.fun, args)
) ))
} }
} }

View File

@@ -44,13 +44,14 @@ impl Debug for Int {
} }
static INT_STR_FUN: Lazy<NativeFunRef> = Lazy::new(|| { static INT_STR_FUN: Lazy<NativeFunRef> = Lazy::new(|| {
NativeFun::new_obj(|_callee, vm, args| { NativeFun::new_obj(1, |_callee, vm, args| {
read_obj!(let int_obj = &args[0]); read_obj!(let int_obj = &args[0]);
if let Some(int_obj) = std::any::Any::downcast_ref::<Int>(int_obj.as_any()) { if let Some(int_obj) = std::any::Any::downcast_ref::<Int>(int_obj.as_any()) {
vm.push(Str::new_obj(int_obj.value.to_string())); vm.push(Str::new_obj(int_obj.value.to_string()));
} else { } else {
panic!("{:?} is not an int", int_obj) panic!("{:?} is not an int", int_obj)
} }
crate::vm::signal::Signal::Return
Ok(crate::vm::signal::Signal::Return)
}) })
}); });

View File

@@ -20,6 +20,7 @@ pub mod prelude {
use shredder::{Gc, Scan}; use shredder::{Gc, Scan};
use std::{ use std::{
fmt::{self, Debug, Formatter},
marker::Unsize, marker::Unsize,
ops::{CoerceUnsized, Deref, DerefMut}, ops::{CoerceUnsized, Deref, DerefMut},
sync::RwLock, sync::RwLock,
@@ -42,7 +43,7 @@ macro_rules! obj_attr {
} }
} }
pub trait Obj: Scan + std::fmt::Debug { pub trait Obj: Scan + Debug {
fn vtable(&self) -> &Vtable; fn vtable(&self) -> &Vtable;
fn attrs(&self) -> &Attrs; fn attrs(&self) -> &Attrs;
fn attrs_mut(&mut self) -> Option<&mut Attrs>; fn attrs_mut(&mut self) -> Option<&mut Attrs>;
@@ -82,7 +83,7 @@ pub trait Obj: Scan + std::fmt::Debug {
// struct ObjRef // struct ObjRef
// //
#[derive(Debug, Scan)] #[derive(Scan)]
pub struct ObjRef<T = (dyn Obj + Send + Sync + 'static)> pub struct ObjRef<T = (dyn Obj + Send + Sync + 'static)>
where where
T: Obj + ?Sized + Send + Sync, T: Obj + ?Sized + Send + Sync,
@@ -135,6 +136,18 @@ where
{ {
} }
//
// impl Debug for ObjRef
//
impl<T> Debug for ObjRef<T>
where T: Obj + ?Sized + Send + Sync + 'static,
{
fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
read_obj!(let obj: &T = self);
Debug::fmt(obj, fmt)
}
}
// //
// impl Deref for ObjRef // impl Deref for ObjRef
// //

View File

@@ -1,9 +1,24 @@
use crate::obj::prelude::*;
use snafu::Snafu; use snafu::Snafu;
#[derive(Debug, Snafu)] #[derive(Debug, Snafu)]
pub enum Error { pub enum Error {
#[snafu(display("attempted to pop an empty stack"))] #[snafu(display("missing attribute: {}", global_sym_lookup(*attr).unwrap()))]
EmptyStack, MissingAttr {
attr: Sym,
},
#[snafu(display("{}", error))]
ValueError {
error: String,
value: ObjRef,
},
#[snafu(display("incorrect function arity; expected {} but got {} instead", expected, got))]
ArityError {
expected: usize,
got: usize,
},
} }
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;

View File

@@ -6,7 +6,7 @@ pub mod signal;
use crate::{ use crate::{
obj::{builtin::BUILTIN_OBJS, reserved::*, prelude::*}, obj::{builtin::BUILTIN_OBJS, reserved::*, prelude::*},
vm::{consts::ConstPool, frame::*, inst::*, signal::*}, vm::{consts::ConstPool, error::*, frame::*, inst::*, signal::*},
}; };
#[derive(Debug)] #[derive(Debug)]
@@ -92,25 +92,26 @@ impl<'c> Vm<'c> {
} }
/// Calls a function. /// Calls a function.
pub fn call(&mut self, fun: ObjRef, args: Vec<ObjRef>) -> ObjRef { pub fn call(&mut self, fun: ObjRef, args: Vec<ObjRef>) -> Result<ObjRef> {
self.handle_signal(Signal::Call(fun, args)); self.handle_signal(Signal::Call(fun, args))?;
self.resume_until_return(); self.resume_until_return()?;
self.pop().expect("return value") Ok(self.pop().expect("return value"))
} }
/// Resumes execution of the current program. /// Resumes execution of the current program.
pub fn resume(&mut self) { pub fn resume(&mut self) -> Result<()> {
while !self.frames().is_empty() { while !self.frames().is_empty() {
self.resume_until_return(); self.resume_until_return()?;
} }
Ok(())
} }
/// Resume execution until the currently executing function returns. /// Resume execution until the currently executing function returns.
pub fn resume_until_return(&mut self) { pub fn resume_until_return(&mut self) -> Result<()> {
// resume execution until control is returned to the originally calling function // resume execution until control is returned to the originally calling function
let start_frames = self.frames().len(); let start_frames = self.frames().len();
if start_frames == 0 { if start_frames == 0 {
return; return Ok(());
} }
while self.frames().len() >= start_frames { while self.frames().len() >= start_frames {
@@ -119,14 +120,15 @@ impl<'c> Vm<'c> {
Frame::Native(fun) => { Frame::Native(fun) => {
let callee = fun.callee().clone(); let callee = fun.callee().clone();
let args = fun.args().clone(); let args = fun.args().clone();
(*fun.fun_ptr())(callee, self, args) (*fun.fun_ptr())(callee, self, args)?
} }
Frame::User(_) => { Frame::User(_) => {
self.resume_user_fun() self.resume_user_fun()
} }
}; };
self.handle_signal(signal); self.handle_signal(signal)?;
} }
Ok(())
} }
fn resume_user_fun(&mut self) -> Signal { fn resume_user_fun(&mut self) -> Signal {
@@ -142,13 +144,13 @@ impl<'c> Vm<'c> {
/// ///
/// The signal may originate from the VM itself, or from an external location. Signals /// The signal may originate from the VM itself, or from an external location. Signals
/// generally interrupt control flow of a program. /// generally interrupt control flow of a program.
pub fn handle_signal(&mut self, signal: Signal) { pub fn handle_signal(&mut self, signal: Signal) -> Result<()> {
match signal { match signal {
Signal::Call(callee, args) => { Signal::Call(callee, args) => {
read_obj!(let callee_obj = callee); read_obj!(let callee_obj = callee);
let frame = callee_obj.as_fun() let frame = callee_obj.as_fun()
.expect("callable function") .ok_or_else(|| Error::ValueError { error: "cannot call this object".to_string(), value: callee.clone() })?
.create_frame(callee.clone(), self, args); .create_frame(callee.clone(), self, args)?;
// Jump to the first address of the new function call if it's a user function // Jump to the first address of the new function call if it's a user function
if let Frame::User(_) = &frame { if let Frame::User(_) = &frame {
self.set_pc(0); self.set_pc(0);
@@ -171,6 +173,7 @@ impl<'c> Vm<'c> {
self.push(ret_val); self.push(ret_val);
} }
} }
Ok(())
} }
/// Gets the current instruction. /// Gets the current instruction.