refactor: VM-only architecture — remove tree-walking interpreter

Eliminate dual-engine architecture (Interpreter + Vm) in favor of
a single bytecode VM executor.

- Extract shared types (Value, FunctionProto, Closure, UpvalueObj,
  Runtime trait, NativeFn) into new  module
- Consolidate builtin registration in  — single
   used by VM, eliminates 40-line duplicate
- Delete tree-walker: exec.rs, eval.rs, interpreter.rs, env.rs, module.rs
- Change Value::Function(Rc<Function>) → Value::Function(Rc<Closure>)
  eliminating the closures HashMap pointer-key hack
- Fix VM semantic gaps found during migration:
  * Structural equality for Object/Array in is_equal
  * CompoundAssignGlobal opcode (global compound assigns were broken)
  * 9 string methods added to VM get_property
  * ForInInit error on non-iterable values
- Switch run_file/run_repl to VM; remove --vm CLI flag
- Move 327 tests from interpreter/ to vm/ — all pass

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
0264408
2026-06-26 10:31:32 +08:00
parent abe0844816
commit fa6ea512b3
19 changed files with 432 additions and 1471 deletions
+4 -64
View File
@@ -7,67 +7,12 @@
use crate::ast::*;
use crate::ast::expr::{Literal, UnaryOp, BinaryOp, LogicalOp, AssignOp};
use crate::error::RuntimeError;
use crate::interpreter::Value;
use crate::runtime::{Value, FunctionProto};
use super::opcode::*;
use std::rc::Rc;
use std::cell::RefCell;
// ============================================================================
// FunctionProto — compiled function blueprint
// ============================================================================
#[derive(Debug, Clone)]
pub struct FunctionProto {
pub name: Option<String>,
pub arity: u8,
pub code: Vec<u8>,
pub constants: Vec<Value>,
/// Nested function protos (for closures and function declarations)
pub protos: Vec<Rc<FunctionProto>>,
pub upvalue_count: u8,
/// Upvalue descriptors for this function (for Closure opcode emission)
pub upvalues: Vec<(bool, u8)>, // (is_local, index)
pub lines: Vec<(usize, usize)>, // (bytecode_offset, source_line)
}
impl FunctionProto {
pub fn new(name: Option<String>) -> Self {
Self {
name,
arity: 0,
code: Vec::new(),
constants: Vec::new(),
protos: Vec::new(),
upvalue_count: 0,
upvalues: Vec::new(),
lines: Vec::new(),
}
}
fn add_constant(&mut self, val: Value) -> u16 {
// Check for existing identical constant
for (i, c) in self.constants.iter().enumerate() {
if values_eq(c, &val) {
return i as u16;
}
}
let idx = self.constants.len();
self.constants.push(val);
idx as u16
}
}
fn values_eq(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Number(x), Value::Number(y)) => (x - y).abs() < f64::EPSILON,
(Value::String(x), Value::String(y)) => x == y,
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Nil, Value::Nil) => true,
_ => false,
}
}
// ============================================================================
// Compiler
// ============================================================================
@@ -536,7 +481,7 @@ impl Compiler {
self.function.code.push(compound_op as u8);
} else {
let name_idx = self.add_string_constant(name);
emit_u16(&mut self.function.code, OpCode::CompoundAssignProp, name_idx);
emit_u16(&mut self.function.code, OpCode::CompoundAssignGlobal, name_idx);
self.function.code.push(compound_op as u8);
}
}
@@ -558,16 +503,11 @@ impl Compiler {
emit_u16(&mut self.function.code, OpCode::SetProperty, name_idx);
} else {
// Compound property set: object.name op= value
// Strategy: load object, dup, get property as current value,
// load rhs, apply op, set property
// CompoundAssignProp handler does get_property internally
self.compile_expr(object)?;
self.emit_op(OpCode::Dup);
let name_idx = self.add_string_constant(name);
emit_u16(&mut self.function.code, OpCode::GetProperty, name_idx); // current value
self.compile_expr(value)?; // rhs
let compound_op = assign_op_to_compound(op);
self.function.code.push(compound_op as u8);
// Now stack: object, current_val, rhs → set property
let name_idx = self.add_string_constant(name);
emit_u16(&mut self.function.code, OpCode::CompoundAssignProp, name_idx);
self.function.code.push(compound_op as u8);
}
+5 -2
View File
@@ -79,12 +79,13 @@ pub enum OpCode {
CompoundAssignProp = 40, // operand: u16 name idx + u8 compound-op tag
CompoundAssignIndex = 41, // operand: u8 compound-op tag
CompoundAssignUpvalue = 44, // operand: u8 upvalue idx + u8 compound-op tag
CompoundAssignGlobal = 45, // operand: u16 name idx + u8 compound-op tag
}
impl OpCode {
pub fn from_u8(byte: u8) -> Option<Self> {
match byte {
0..=41 | 44 => Some(unsafe { std::mem::transmute::<u8, OpCode>(byte) }),
0..=41 | 44..=45 => Some(unsafe { std::mem::transmute::<u8, OpCode>(byte) }),
42 => Some(OpCode::LoadUpvalue),
43 => Some(OpCode::StoreUpvalue),
_ => None,
@@ -92,7 +93,7 @@ impl OpCode {
}
/// Total number of distinct opcodes
pub const COUNT: usize = 45;
pub const COUNT: usize = 46;
}
/// Compound assignment operator tags (used as operand byte after
@@ -171,3 +172,5 @@ pub const SIZE_OP: usize = 1;
pub const SIZE_U8: usize = 2;
/// Size in bytes of an opcode with a u16/i16 operand.
pub const SIZE_U16: usize = 3;
/// Size in bytes of an opcode with a u16 + u8 operand.
pub const SIZE_U16_PLUS1: usize = 4;
File diff suppressed because it is too large Load Diff
+146 -94
View File
@@ -4,11 +4,11 @@
//! call frames. Implements the `Runtime` trait for native function support.
use crate::error::RuntimeError;
use crate::interpreter::{Value, Runtime};
use crate::runtime::{Value, Runtime, FunctionProto, Closure, UpvalueObj};
use crate::lexer::Lexer;
use crate::parser::Parser;
use super::opcode::*;
use super::compiler::{Compiler, FunctionProto};
use super::compiler::Compiler;
use std::cell::RefCell;
use std::collections::HashMap;
@@ -18,22 +18,6 @@ use std::rc::Rc;
// VM data structures
// ============================================================================
/// An upvalue — a reference to a local variable in an enclosing function.
#[derive(Debug, Clone)]
struct UpvalueObj {
/// Stack index where the value lives, or usize::MAX if closed
location: usize,
/// The value, if it has been moved off the stack (closed)
closed: Option<Value>,
}
/// A runtime closure: compiled function proto + captured upvalues.
#[derive(Debug, Clone)]
struct Closure {
proto: Rc<FunctionProto>,
upvalues: Vec<Rc<RefCell<UpvalueObj>>>,
}
/// A call frame on the VM stack.
struct CallFrame {
closure: Rc<Closure>,
@@ -62,9 +46,6 @@ pub struct Vm {
/// Open upvalues (tracked so closures share the same upvalue object)
open_upvalues: Vec<Rc<RefCell<UpvalueObj>>>,
/// VM-compiled closures: maps Function Rc pointer → Closure data
closures: RefCell<HashMap<*const crate::interpreter::Function, Rc<Closure>>>,
}
impl Vm {
@@ -85,7 +66,6 @@ impl Vm {
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| ".".to_string()),
open_upvalues: Vec::new(),
closures: RefCell::new(HashMap::new()),
}
}
@@ -101,6 +81,12 @@ impl Vm {
self.run(Rc::new(proto))
}
/// Look up a global variable by name (for test inspection).
pub fn get_global(&self, name: &str) -> Option<Value> {
self.globals.borrow().get(name).map(|(v, _)| v.clone())
.or_else(|| self.builtins.borrow().get(name).cloned())
}
/// Execute a compiled FunctionProto.
pub fn run(&mut self, proto: Rc<FunctionProto>) -> Result<(), RuntimeError> {
let closure = Rc::new(Closure {
@@ -496,15 +482,7 @@ impl Vm {
}
let closure = Rc::new(Closure { proto, upvalues });
let func = Rc::new(crate::interpreter::Function {
params: Vec::new(),
body: Vec::new(),
env: Rc::new(RefCell::new(crate::interpreter::Env::new(None))),
name: None,
});
let key = Rc::as_ptr(&func) as *const crate::interpreter::Function;
self.closures.borrow_mut().insert(key, closure);
self.stack.push(Value::Function(func));
self.stack.push(Value::Function(closure));
self.advance_ip_to(off);
}
@@ -527,7 +505,15 @@ impl Vm {
// --- For-in ---
OpCode::ForInInit => {
let iterable = self.stack.pop().unwrap();
let items = self.for_in_items(&iterable);
let items = match &iterable {
Value::Array(arr) => arr.borrow().clone(),
Value::Object(obj) => obj.borrow().keys().map(|k| Value::String(k.clone())).collect(),
Value::String(s) => s.chars().map(|c| Value::String(c.to_string())).collect(),
_ => return Err(RuntimeError::RuntimeError {
message: format!("for-in requires an array, object, or string, got {}", iterable),
token: None,
}),
};
self.stack.push(Value::Array(Rc::new(RefCell::new(items))));
self.advance_ip(SIZE_OP);
}
@@ -632,6 +618,33 @@ impl Vm {
self.stack.push(result);
self.advance_ip(SIZE_U8 + 1);
}
OpCode::CompoundAssignGlobal => {
let name_idx = read_u16(code, ip) as usize;
let op_tag = code[ip + 3];
let compound_op = CompoundOp::from_u8(op_tag).unwrap();
let rhs = self.stack.pop().unwrap();
let name = proto_string(&proto, name_idx)?;
let lhs = self.globals.borrow().get(&name)
.map(|(v, _)| v.clone())
.or_else(|| self.builtins.borrow().get(&name).cloned())
.ok_or_else(|| RuntimeError::RuntimeError {
message: format!("Undefined variable '{}'", name),
token: None,
})?;
let result = self.apply_compound_op(lhs, rhs, compound_op)?;
// Check const flag (globals tuple stores (value, mutable))
if let Some((_, mutable)) = self.globals.borrow().get(&name) {
if !*mutable {
return Err(RuntimeError::RuntimeError {
message: format!("Cannot reassign constant '{}'", name),
token: None,
});
}
}
self.globals.borrow_mut().insert(name, (result.clone(), true));
self.stack.push(result);
self.advance_ip(SIZE_U16_PLUS1);
}
}
}
}
@@ -678,22 +691,13 @@ impl Vm {
self.stack.push(result);
self.advance_ip(SIZE_U8);
}
Value::Function(_) => {
// Look up the VM-compiled closure
let func_ptr = match &self.stack[callee_idx] {
Value::Function(f) => Rc::as_ptr(f) as *const crate::interpreter::Function,
_ => unreachable!(),
};
let closure = self.closures.borrow().get(&func_ptr).cloned().ok_or_else(|| RuntimeError::RuntimeError {
message: "VM: call to non-VM function (tree-walker function not supported in VM)".into(),
token: None,
})?;
Value::Function(closure) => {
let closure = Rc::clone(closure);
// Advance caller's IP past the Call instruction before pushing new frame
self.frame_mut().ip += SIZE_U8;
// Callee is at callee_idx, args start at callee_idx+1
let base = callee_idx + 1; // first param is here
let base = callee_idx + 1;
self.frames.push(CallFrame {
closure,
ip: 0,
@@ -748,6 +752,78 @@ impl Vm {
},
Value::String(s) => match name {
"length" => Ok(Value::Number(s.chars().count() as f64)),
"upper" => {
let s = s.clone();
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
let mut all_args = vec![Value::String(s.clone())];
all_args.extend(args);
crate::runtime::builtins::string::upper(runtime, all_args)
})))
}
"lower" => {
let s = s.clone();
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
let mut all_args = vec![Value::String(s.clone())];
all_args.extend(args);
crate::runtime::builtins::string::lower(runtime, all_args)
})))
}
"trim" => {
let s = s.clone();
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
let mut all_args = vec![Value::String(s.clone())];
all_args.extend(args);
crate::runtime::builtins::string::trim(runtime, all_args)
})))
}
"substring" => {
let s = s.clone();
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
let mut all_args = vec![Value::String(s.clone())];
all_args.extend(args);
crate::runtime::builtins::string::substring(runtime, all_args)
})))
}
"replace" => {
let s = s.clone();
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
let mut all_args = vec![Value::String(s.clone())];
all_args.extend(args);
crate::runtime::builtins::string::replace(runtime, all_args)
})))
}
"contains" => {
let s = s.clone();
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
let mut all_args = vec![Value::String(s.clone())];
all_args.extend(args);
crate::runtime::builtins::string::contains(runtime, all_args)
})))
}
"starts_with" => {
let s = s.clone();
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
let mut all_args = vec![Value::String(s.clone())];
all_args.extend(args);
crate::runtime::builtins::string::starts_with(runtime, all_args)
})))
}
"ends_with" => {
let s = s.clone();
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
let mut all_args = vec![Value::String(s.clone())];
all_args.extend(args);
crate::runtime::builtins::string::ends_with(runtime, all_args)
})))
}
"split" => {
let s = s.clone();
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
let mut all_args = vec![Value::String(s.clone())];
all_args.extend(args);
crate::runtime::builtins::string::split(runtime, all_args)
})))
}
_ => Err(RuntimeError::RuntimeError {
message: format!("String has no property '{}'", name),
token: None,
@@ -922,7 +998,23 @@ impl Vm {
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Number(x), Value::Number(y)) => x == y,
(Value::String(x), Value::String(y)) => x == y,
_ => false, // Simplified — full structural equality omitted for now
(Value::Array(x), Value::Array(y)) => {
let x = x.borrow();
let y = y.borrow();
if x.len() != y.len() { return false; }
x.iter().zip(y.iter()).all(|(a, b)| self.is_equal(a, b))
}
(Value::Object(x), Value::Object(y)) => {
let x = x.borrow();
let y = y.borrow();
if x.len() != y.len() { return false; }
x.iter().all(|(k, v)| {
y.get(k).map_or(false, |yv| self.is_equal(v, yv))
})
}
(Value::Function(x), Value::Function(y)) => Rc::ptr_eq(x, y),
(Value::NativeFunction(x), Value::NativeFunction(y)) => Rc::ptr_eq(x, y),
_ => false,
}
}
@@ -988,14 +1080,6 @@ impl Vm {
// For-in helper
// ========================================================================
fn for_in_items(&self, collection: &Value) -> Vec<Value> {
match collection {
Value::Array(arr) => arr.borrow().clone(),
Value::Object(obj) => obj.borrow().keys().map(|k| Value::String(k.clone())).collect(),
Value::String(s) => s.chars().map(|c| Value::String(c.to_string())).collect(),
_ => Vec::new(),
}
}
}
impl Runtime for Vm {
@@ -1080,12 +1164,7 @@ impl Runtime for Vm {
}
}
// 11. Transfer closures from module VM to parent (so exported fns are callable)
for (key, closure) in module_vm.closures.borrow().iter() {
self.closures.borrow_mut().insert(*key, Rc::clone(closure));
}
// 12. Cache in parent
// 11. Cache in parent
self.module_cache.borrow_mut().insert(resolved, exports_obj.clone());
Ok(exports_obj)
@@ -1107,43 +1186,16 @@ fn proto_string(proto: &FunctionProto, idx: usize) -> Result<String, RuntimeErro
}
// ============================================================================
// Builtin registration (reuses tree-walker's builtins)
// Builtin registration (delegates to runtime::builtins)
// ============================================================================
fn register_builtins(map: &Rc<RefCell<HashMap<String, Value>>>) {
let mut m = map.borrow_mut();
// io
let mut io = HashMap::new();
io.insert("print".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::io::print)));
io.insert("input".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::io::input)));
m.insert("io".into(), Value::Object(Rc::new(RefCell::new(io))));
m.insert("print".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::io::print)));
m.insert("input".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::io::input)));
// os
let mut os = HashMap::new();
os.insert("clock".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::os::clock)));
m.insert("os".into(), Value::Object(Rc::new(RefCell::new(os))));
m.insert("clock".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::os::clock)));
// core
m.insert("len".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::core::len)));
m.insert("typeof".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::core::typeof_fn)));
m.insert("push".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::core::push)));
m.insert("pop".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::core::pop)));
// require (VM version)
m.insert("require".into(), Value::NativeFunction(Rc::new(crate::interpreter::module::require_fn)));
// string
m.insert("split".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::split)));
m.insert("trim".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::trim)));
m.insert("substring".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::substring)));
m.insert("replace".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::replace)));
m.insert("contains".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::contains)));
m.insert("upper".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::upper)));
m.insert("lower".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::lower)));
m.insert("starts_with".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::starts_with)));
m.insert("ends_with".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::ends_with)));
crate::runtime::builtins::register_all(&mut *m);
// require is VM-specific
m.insert("require".into(), Value::NativeFunction(Rc::new(crate::runtime::require_fn)));
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;