Files
not-python-rust/src/vm.rs
Alek Ratzloff 0d04090a99 Implement vtables and method resolution
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>
2024-09-23 20:59:00 -07:00

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
}
}