Types now can have vtable elements which are used by instances to bind themselves to methods. When Op::GetAttr is executed, it calls a new function, Obj::get_attr_lazy. This will search: * attributes on the object * vtable on the object's type * vtable on the object's type's type, * etc. This searches up the type tree for a named value. If it exists as an attribute, it will be returned immediately. If it exists in the type's vtable, then it will be inserted as an attribute. If the vtable value is a function, the object that it is being called on will be bound to that method as the `self` parameter. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
363 lines
12 KiB
Rust
363 lines
12 KiB
Rust
use std::collections::HashMap;
|
|
use std::rc::Rc;
|
|
|
|
use crate::obj::*;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum Op {
|
|
// Stack functions
|
|
Pop,
|
|
PushConstant(LongOpArg),
|
|
|
|
// Variables
|
|
GetLocal(LocalIndex),
|
|
SetLocal(LocalIndex),
|
|
GetGlobal(GlobalId),
|
|
SetGlobal(GlobalId),
|
|
|
|
// Attributes
|
|
GetAttr(ConstantId),
|
|
SetAttr(ConstantId),
|
|
|
|
// Jumps
|
|
Jump(JumpOpArg),
|
|
JumpFalse(JumpOpArg),
|
|
JumpTrue(JumpOpArg),
|
|
|
|
// Functions
|
|
Call(Argc),
|
|
Return,
|
|
CloseOver { depth: ShortOpArg, slot: ShortOpArg },
|
|
|
|
// VM control
|
|
Halt,
|
|
}
|
|
|
|
pub type LineRange = (usize, usize);
|
|
|
|
type ShortOpArg = u16;
|
|
type LongOpArg = u32;
|
|
|
|
pub type JumpOpArg = i32;
|
|
pub type LocalIndex = LongOpArg;
|
|
pub type LocalSlot = ShortOpArg;
|
|
pub type ConstantId = LongOpArg;
|
|
pub type GlobalId = LongOpArg;
|
|
pub type Argc = LongOpArg;
|
|
pub type FrameDepth = ShortOpArg;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Local {
|
|
pub(crate) slot: LocalSlot,
|
|
pub(crate) index: LocalIndex,
|
|
pub(crate) name: String,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct Chunk {
|
|
pub(crate) code: Vec<Op>,
|
|
pub(crate) lines: Vec<LineRange>,
|
|
pub(crate) locals: Vec<Local>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Frame {
|
|
pub(crate) name: Rc<String>,
|
|
pub(crate) chunk: Rc<Chunk>,
|
|
pub(crate) ip: usize,
|
|
pub(crate) stack_base: usize,
|
|
}
|
|
|
|
impl Frame {
|
|
pub fn new(name: Rc<String>, chunk: Rc<Chunk>, stack_base: usize) -> Self {
|
|
Self {
|
|
name,
|
|
chunk,
|
|
ip: 0,
|
|
stack_base,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Vm {
|
|
constants: Vec<ObjP>,
|
|
//global_names: Vec<String>,
|
|
globals: Vec<ObjP>,
|
|
stack: Vec<ObjP>,
|
|
frames: Vec<Frame>,
|
|
builtins: HashMap<String, ObjP>,
|
|
}
|
|
|
|
impl Vm {
|
|
/// Create a new virtual machine with the given chunk, constants, and global names.
|
|
pub fn new(
|
|
chunk: Rc<Chunk>,
|
|
constants: Vec<ObjP>,
|
|
global_names: Vec<String>,
|
|
builtins: HashMap<String, ObjP>,
|
|
) -> Self {
|
|
// set up globals
|
|
let nil = builtins.create_nil();
|
|
let mut globals: Vec<_> = global_names.iter().map(|_| ObjP::clone(&nil)).collect();
|
|
|
|
let mut register_global = |name: &str, value: ObjP| {
|
|
let index = global_names
|
|
.iter()
|
|
.position(|global| global == name)
|
|
.expect("could not find global");
|
|
globals[index] = value;
|
|
};
|
|
|
|
for (name, builtin) in builtins.iter() {
|
|
register_global(&name, ObjP::clone(&builtin));
|
|
}
|
|
|
|
// stack and frames
|
|
let stack = Vec::new();
|
|
let frames = vec![Frame::new("__main__".to_string().into(), chunk, 0)];
|
|
|
|
Vm {
|
|
constants,
|
|
//global_names,
|
|
globals,
|
|
stack,
|
|
frames,
|
|
builtins,
|
|
}
|
|
}
|
|
|
|
/// Get the stack.
|
|
pub fn stack(&self) -> &Vec<ObjP> {
|
|
&self.stack
|
|
}
|
|
|
|
/// Current stack frame.
|
|
pub fn frame(&self) -> &Frame {
|
|
self.frames.last().unwrap()
|
|
}
|
|
|
|
/// Current stack frame, mutably.
|
|
pub fn frame_mut(&mut self) -> &mut Frame {
|
|
self.frames.last_mut().unwrap()
|
|
}
|
|
|
|
/// Push a new stack frame.
|
|
pub fn push_frame(&mut self, frame: Frame) {
|
|
self.frames.push(frame);
|
|
}
|
|
|
|
/// Pop the current stack frame.
|
|
pub fn pop_frame(&mut self) -> Frame {
|
|
self.frames.pop().expect("no frame")
|
|
}
|
|
|
|
/// Gets the chunk of the currently executing frame.
|
|
pub fn chunk(&self) -> &Chunk {
|
|
&self.frame().chunk
|
|
}
|
|
|
|
/// Instruction pointer of the current frame.
|
|
pub fn ip(&self) -> usize {
|
|
self.frame().ip
|
|
}
|
|
|
|
/// Update the current instruction pointer.
|
|
pub fn set_ip(&mut self, ip: usize) {
|
|
self.frame_mut().ip = ip;
|
|
}
|
|
|
|
/*
|
|
/// Gets the line of the current instruction.
|
|
fn line(&self, offset: isize) -> LineRange {
|
|
let index = (((self.ip() as isize) + offset).max(0) as usize).min(self.chunk().lines.len());
|
|
self.chunk().lines[index]
|
|
}
|
|
*/
|
|
|
|
/// 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]
|
|
}
|
|
|
|
/// Pop a value from the stack.
|
|
pub fn pop(&mut self) -> ObjP {
|
|
self.stack.pop().expect("stack empty")
|
|
}
|
|
|
|
/// Peek the top value of the stack.
|
|
pub fn peek(&self) -> ObjP {
|
|
self.stack.last().map(Ptr::clone).expect("stack empty")
|
|
}
|
|
|
|
/// Push a value to the stack.
|
|
pub fn push(&mut self, value: ObjP) {
|
|
self.stack.push(value);
|
|
}
|
|
|
|
pub fn run(&mut self) {
|
|
// Cached pointers that we just always want to have on hand
|
|
let method_type = self.builtins().get("Method").unwrap().clone();
|
|
|
|
loop {
|
|
match self.next() {
|
|
Op::Pop => {
|
|
self.pop();
|
|
}
|
|
Op::PushConstant(constant_id) => {
|
|
let constant = Ptr::clone(&self.constants[constant_id as usize]);
|
|
self.push(constant);
|
|
}
|
|
Op::GetLocal(local_index) => {
|
|
let local = &self.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 index = self.frame().stack_base + local.slot as usize;
|
|
self.stack[index] = value;
|
|
}
|
|
Op::GetGlobal(global_index) => {
|
|
let value = Ptr::clone(&self.globals[global_index as usize]);
|
|
self.push(value);
|
|
}
|
|
Op::SetGlobal(global_index) => {
|
|
let value = self.pop();
|
|
self.globals[global_index as usize] = value;
|
|
}
|
|
Op::GetAttr(constant_id) => {
|
|
// need both declarations to borrow cell value
|
|
let name_obj = Ptr::clone(&self.constants[constant_id as usize]);
|
|
let name =
|
|
with_obj_downcast(name_obj, |name: &StrInst| Rc::clone(&name.str_value()));
|
|
let owner = self.pop();
|
|
let value =
|
|
owner
|
|
.borrow_mut()
|
|
.get_attr_lazy(owner.clone(), method_type.clone(), &name);
|
|
if let Some(value) = value {
|
|
self.push(value);
|
|
} else {
|
|
// TODO Vm::run, Op::GetAttr - throw an exception when the attribute
|
|
// doesn't exist
|
|
// BLOCKED-ON: exceptions
|
|
todo!(
|
|
"throw an error because we couldn't read attr '{}' on '{}'",
|
|
name,
|
|
owner.borrow(),
|
|
);
|
|
}
|
|
}
|
|
Op::SetAttr(constant_id) => {
|
|
let name_obj = Ptr::clone(&self.constants[constant_id as usize]);
|
|
let name =
|
|
with_obj_downcast(name_obj, |name: &StrInst| Rc::clone(&name.str_value()));
|
|
let value = self.pop();
|
|
let target = self.pop();
|
|
|
|
target.borrow_mut().set_attr(&name, value);
|
|
}
|
|
Op::Jump(offset) => {
|
|
let base = (self.ip() - 1) as JumpOpArg;
|
|
assert!(base + offset > 0, "tried to jump to negative IP");
|
|
self.set_ip((base + offset) as usize);
|
|
}
|
|
Op::JumpFalse(offset) => {
|
|
let base = (self.ip() - 1) as JumpOpArg;
|
|
let value = self.peek();
|
|
if !value.borrow().is_truthy() {
|
|
self.set_ip((base + offset) as usize);
|
|
}
|
|
}
|
|
Op::JumpTrue(offset) => {
|
|
let base = (self.ip() - 1) as JumpOpArg;
|
|
let value = self.peek();
|
|
if value.borrow().is_truthy() {
|
|
self.set_ip((base + offset) as usize);
|
|
}
|
|
}
|
|
Op::Call(argc) => {
|
|
let argc = argc as usize;
|
|
let index = self.stack.len() - argc - 1;
|
|
let fun_ptr = Ptr::clone(&self.stack[index]);
|
|
|
|
let arity = if let Some(arity) = fun_ptr.borrow().arity() {
|
|
arity as usize
|
|
} else {
|
|
// TODO Vm::run, Op::Call - throw an exception when the value isn't
|
|
// callable
|
|
// BLOCKED-ON: exceptions
|
|
todo!(
|
|
"throw an error because we couldn't call {}",
|
|
fun_ptr.borrow()
|
|
);
|
|
};
|
|
|
|
// Methods with bound "self" parameter
|
|
// argc may be mutated
|
|
let mut argc = argc;
|
|
if let Some(method) = fun_ptr.borrow().as_any().downcast_ref::<MethodInst>() {
|
|
// shift all of the arguments over by one
|
|
// (duplicate the last item on the stack and then shift everyone else over)
|
|
self.stack
|
|
.insert(self.stack.len() - argc, Ptr::clone(method.self_binding()));
|
|
// also increment argc since we're specifying another arg
|
|
argc += 1;
|
|
}
|
|
// remove mutability
|
|
let argc = argc;
|
|
|
|
if arity != argc {
|
|
// TODO Vm::run, Op::Call - throw an exception when the number of arguments
|
|
// does not match the function's arity
|
|
// BLOCKED-ON: exceptions
|
|
todo!(
|
|
"throw an error because we passed the wrong number of arguments to {}",
|
|
fun_ptr.borrow()
|
|
);
|
|
}
|
|
fun_ptr.borrow().call(self, argc as Argc);
|
|
}
|
|
Op::Return => {
|
|
let return_value = self.pop();
|
|
let old_frame = self.frames.pop().unwrap();
|
|
// stack_base is always going to be <= current stack size
|
|
self.stack
|
|
.resize_with(old_frame.stack_base, || unreachable!());
|
|
// also pop the function object off of the stack
|
|
self.stack.pop();
|
|
self.push(return_value);
|
|
}
|
|
Op::CloseOver { depth, slot } => {
|
|
// since we're closing over a value, and functions ultimately come from
|
|
// constants, we want to deep-clone this object so we don't alter any live
|
|
// objects.
|
|
// there is some room for optimization here so we aren't cloning the entire
|
|
// UserFunctionInst for every individual capture in a function.
|
|
let fun_ptr = self.pop();
|
|
let mut fun: UserFunctionInst =
|
|
with_obj_downcast(fun_ptr, UserFunctionInst::clone);
|
|
let frame_index = self.frames.len() - (depth as usize) - 1;
|
|
let stack_base = self.frames[frame_index].stack_base;
|
|
let value = Ptr::clone(&self.stack[stack_base + (slot as usize)]);
|
|
fun.push_capture(value);
|
|
self.push(make_ptr(fun));
|
|
}
|
|
Op::Halt => {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ObjFactory for Vm {
|
|
fn builtins(&self) -> &HashMap<String, ObjP> {
|
|
&self.builtins
|
|
}
|
|
}
|