diff --git a/aster-core/src/ast/expr.rs b/aster-core/src/ast/expr.rs index b2e79c4..e0bd76f 100644 --- a/aster-core/src/ast/expr.rs +++ b/aster-core/src/ast/expr.rs @@ -130,7 +130,7 @@ pub enum LogicalOp { Or, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AssignOp { Equal, // = PlusEqual, // += diff --git a/aster-core/src/interpreter/builtins/core.rs b/aster-core/src/interpreter/builtins/core.rs index fe42a52..f7283e7 100644 --- a/aster-core/src/interpreter/builtins/core.rs +++ b/aster-core/src/interpreter/builtins/core.rs @@ -1,9 +1,9 @@ //! 标准库:core(len, typeof, push, pop) use crate::error::RuntimeError; -use crate::interpreter::{Interpreter, Value}; +use crate::interpreter::{Runtime, Value}; -pub fn len(_interp: &mut Interpreter, args: Vec) -> Result { +pub fn len(_runtime: &mut dyn Runtime, args: Vec) -> Result { if args.is_empty() { return Err(RuntimeError::RuntimeError { message: "len() expects 1 argument".into(), @@ -21,7 +21,7 @@ pub fn len(_interp: &mut Interpreter, args: Vec) -> Result) -> Result { +pub fn typeof_fn(_interp: &mut dyn Runtime, args: Vec) -> Result { if args.is_empty() { return Err(RuntimeError::RuntimeError { message: "typeof() expects 1 argument".into(), @@ -41,7 +41,7 @@ pub fn typeof_fn(_interp: &mut Interpreter, args: Vec) -> Result) -> Result { +pub fn push(_interp: &mut dyn Runtime, args: Vec) -> Result { if args.len() < 2 { return Err(RuntimeError::RuntimeError { message: "push() expects 2 arguments (array, value)".into(), @@ -61,7 +61,7 @@ pub fn push(_interp: &mut Interpreter, args: Vec) -> Result) -> Result { +pub fn pop(_interp: &mut dyn Runtime, args: Vec) -> Result { if args.is_empty() { return Err(RuntimeError::RuntimeError { message: "pop() expects 1 argument (array)".into(), diff --git a/aster-core/src/interpreter/builtins/io.rs b/aster-core/src/interpreter/builtins/io.rs index 3bc1540..b90626f 100644 --- a/aster-core/src/interpreter/builtins/io.rs +++ b/aster-core/src/interpreter/builtins/io.rs @@ -1,9 +1,9 @@ //! 标准库:io(print, input) use crate::error::RuntimeError; -use crate::interpreter::{Interpreter, Value}; +use crate::interpreter::{Runtime, Value}; -pub fn print(_interp: &mut Interpreter, args: Vec) -> Result { +pub fn print(_interp: &mut dyn Runtime, args: Vec) -> Result { for arg in args.iter() { print!("{}", arg); } @@ -11,7 +11,7 @@ pub fn print(_interp: &mut Interpreter, args: Vec) -> Result) -> Result { +pub fn input(_interp: &mut dyn Runtime, args: Vec) -> Result { if let Some(prompt) = args.get(0) { print!("{}", prompt); std::io::Write::flush(&mut std::io::stdout()).unwrap(); diff --git a/aster-core/src/interpreter/builtins/mod.rs b/aster-core/src/interpreter/builtins/mod.rs index 6727e94..a429f6c 100644 --- a/aster-core/src/interpreter/builtins/mod.rs +++ b/aster-core/src/interpreter/builtins/mod.rs @@ -1,8 +1,8 @@ //! 内置函数模块:按功能分组注册,便于扩展和维护。 -mod core; -mod io; -mod os; +pub mod core; +pub mod io; +pub mod os; pub mod string; use crate::interpreter::{Env, Value}; diff --git a/aster-core/src/interpreter/builtins/os.rs b/aster-core/src/interpreter/builtins/os.rs index 258d033..d53e863 100644 --- a/aster-core/src/interpreter/builtins/os.rs +++ b/aster-core/src/interpreter/builtins/os.rs @@ -1,9 +1,9 @@ //! 标准库:os(clock) use crate::error::RuntimeError; -use crate::interpreter::{Interpreter, Value}; +use crate::interpreter::{Runtime, Value}; -pub fn clock(_interp: &mut Interpreter, _args: Vec) -> Result { +pub fn clock(_interp: &mut dyn Runtime, _args: Vec) -> Result { Ok(Value::Number( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/aster-core/src/interpreter/builtins/string.rs b/aster-core/src/interpreter/builtins/string.rs index c4ef2b8..484eed2 100644 --- a/aster-core/src/interpreter/builtins/string.rs +++ b/aster-core/src/interpreter/builtins/string.rs @@ -1,7 +1,7 @@ //! 标准库:string(split, trim, substring, replace, contains, upper, lower, starts_with, ends_with) use crate::error::RuntimeError; -use crate::interpreter::{Interpreter, Value}; +use crate::interpreter::{Runtime, Value}; use std::cell::RefCell; use std::rc::Rc; @@ -19,7 +19,7 @@ fn require_string(val: &Value, arg_name: &str) -> Result { // split(str, delim) → array of substrings // ============================================================================ -pub fn split(_interp: &mut Interpreter, args: Vec) -> Result { +pub fn split(_interp: &mut dyn Runtime, args: Vec) -> Result { if args.len() < 2 { return Err(RuntimeError::RuntimeError { message: "split() expects 2 arguments (string, delimiter)".into(), @@ -43,7 +43,7 @@ pub fn split(_interp: &mut Interpreter, args: Vec) -> Result) -> Result { +pub fn trim(_interp: &mut dyn Runtime, args: Vec) -> Result { if args.is_empty() { return Err(RuntimeError::RuntimeError { message: "trim() expects 1 argument (string)".into(), @@ -58,7 +58,7 @@ pub fn trim(_interp: &mut Interpreter, args: Vec) -> Result) -> Result { +pub fn substring(_interp: &mut dyn Runtime, args: Vec) -> Result { if args.is_empty() { return Err(RuntimeError::RuntimeError { message: "substring() expects at least 2 arguments (string, start, len?)".into(), @@ -81,7 +81,7 @@ pub fn substring(_interp: &mut Interpreter, args: Vec) -> Result) -> Result { +pub fn replace(_interp: &mut dyn Runtime, args: Vec) -> Result { if args.len() < 3 { return Err(RuntimeError::RuntimeError { message: "replace() expects 3 arguments (string, old, new)".into(), @@ -106,7 +106,7 @@ pub fn replace(_interp: &mut Interpreter, args: Vec) -> Result) -> Result { +pub fn contains(_interp: &mut dyn Runtime, args: Vec) -> Result { if args.len() < 2 { return Err(RuntimeError::RuntimeError { message: "contains() expects 2 arguments (string, needle)".into(), @@ -122,7 +122,7 @@ pub fn contains(_interp: &mut Interpreter, args: Vec) -> Result) -> Result { +pub fn upper(_interp: &mut dyn Runtime, args: Vec) -> Result { if args.is_empty() { return Err(RuntimeError::RuntimeError { message: "upper() expects 1 argument (string)".into(), @@ -137,7 +137,7 @@ pub fn upper(_interp: &mut Interpreter, args: Vec) -> Result) -> Result { +pub fn lower(_interp: &mut dyn Runtime, args: Vec) -> Result { if args.is_empty() { return Err(RuntimeError::RuntimeError { message: "lower() expects 1 argument (string)".into(), @@ -152,7 +152,7 @@ pub fn lower(_interp: &mut Interpreter, args: Vec) -> Result) -> Result { +pub fn starts_with(_interp: &mut dyn Runtime, args: Vec) -> Result { if args.len() < 2 { return Err(RuntimeError::RuntimeError { message: "starts_with() expects 2 arguments (string, prefix)".into(), @@ -168,7 +168,7 @@ pub fn starts_with(_interp: &mut Interpreter, args: Vec) -> Result) -> Result { +pub fn ends_with(_interp: &mut dyn Runtime, args: Vec) -> Result { if args.len() < 2 { return Err(RuntimeError::RuntimeError { message: "ends_with() expects 2 arguments (string, suffix)".into(), diff --git a/aster-core/src/interpreter/eval.rs b/aster-core/src/interpreter/eval.rs index fd9a85e..c159214 100644 --- a/aster-core/src/interpreter/eval.rs +++ b/aster-core/src/interpreter/eval.rs @@ -103,7 +103,7 @@ impl super::Interpreter { "length" => Ok(Value::Number(arr.borrow().len() as f64)), "push" => { let arr = Rc::clone(&arr); - Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, mut args: Vec| { + Ok(Value::NativeFunction(Rc::new(move |_runtime: &mut dyn super::Runtime, mut args: Vec| { let val = args.pop().unwrap_or(Value::Nil); arr.borrow_mut().push(val.clone()); Ok(val) @@ -111,7 +111,7 @@ impl super::Interpreter { } "pop" => { let arr = Rc::clone(&arr); - Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, _args: Vec| { + Ok(Value::NativeFunction(Rc::new(move |_runtime: &mut dyn super::Runtime, _args: Vec| { arr.borrow_mut() .pop() .ok_or_else(|| RuntimeError::RuntimeError { @@ -129,74 +129,74 @@ impl super::Interpreter { "length" => Ok(Value::Number(s.chars().count() as f64)), "upper" => { let s = s.clone(); - Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec| { + Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn super::Runtime, args: Vec| { let mut all_args = vec![Value::String(s.clone())]; all_args.extend(args); - super::builtins::string::upper(_interp, all_args) + super::builtins::string::upper(runtime, all_args) }))) } "lower" => { let s = s.clone(); - Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec| { + Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn super::Runtime, args: Vec| { let mut all_args = vec![Value::String(s.clone())]; all_args.extend(args); - super::builtins::string::lower(_interp, all_args) + super::builtins::string::lower(runtime, all_args) }))) } "trim" => { let s = s.clone(); - Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec| { + Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn super::Runtime, args: Vec| { let mut all_args = vec![Value::String(s.clone())]; all_args.extend(args); - super::builtins::string::trim(_interp, all_args) + super::builtins::string::trim(runtime, all_args) }))) } "substring" => { let s = s.clone(); - Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec| { + Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn super::Runtime, args: Vec| { let mut all_args = vec![Value::String(s.clone())]; all_args.extend(args); - super::builtins::string::substring(_interp, all_args) + super::builtins::string::substring(runtime, all_args) }))) } "replace" => { let s = s.clone(); - Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec| { + Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn super::Runtime, args: Vec| { let mut all_args = vec![Value::String(s.clone())]; all_args.extend(args); - super::builtins::string::replace(_interp, all_args) + super::builtins::string::replace(runtime, all_args) }))) } "contains" => { let s = s.clone(); - Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec| { + Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn super::Runtime, args: Vec| { let mut all_args = vec![Value::String(s.clone())]; all_args.extend(args); - super::builtins::string::contains(_interp, all_args) + super::builtins::string::contains(runtime, all_args) }))) } "starts_with" => { let s = s.clone(); - Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec| { + Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn super::Runtime, args: Vec| { let mut all_args = vec![Value::String(s.clone())]; all_args.extend(args); - super::builtins::string::starts_with(_interp, all_args) + super::builtins::string::starts_with(runtime, all_args) }))) } "ends_with" => { let s = s.clone(); - Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec| { + Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn super::Runtime, args: Vec| { let mut all_args = vec![Value::String(s.clone())]; all_args.extend(args); - super::builtins::string::ends_with(_interp, all_args) + super::builtins::string::ends_with(runtime, all_args) }))) } "split" => { let s = s.clone(); - Ok(Value::NativeFunction(Rc::new(move |_interp: &mut Self, args: Vec| { + Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn super::Runtime, args: Vec| { let mut all_args = vec![Value::String(s.clone())]; all_args.extend(args); - super::builtins::string::split(_interp, all_args) + super::builtins::string::split(runtime, all_args) }))) } _ => Err(RuntimeError::RuntimeError { @@ -481,7 +481,7 @@ impl super::Interpreter { pub fn call_function(&mut self, func_val: Value, args: Vec) -> Result { match func_val { - Value::NativeFunction(native_fn) => native_fn(self, args), + Value::NativeFunction(native_fn) => native_fn(self as &mut dyn super::Runtime, args), Value::Function(f) => self.call_user_function(f, args), _ => Err(RuntimeError::RuntimeError { message: "Attempt to call non-function".to_string(), diff --git a/aster-core/src/interpreter/mod.rs b/aster-core/src/interpreter/mod.rs index 3d0b133..866b1f4 100644 --- a/aster-core/src/interpreter/mod.rs +++ b/aster-core/src/interpreter/mod.rs @@ -13,7 +13,16 @@ use std::rc::Rc; use std::cell::RefCell; use std::fmt; -pub type NativeFn = Rc) -> Result>; +/// Trait abstracting runtime services that native functions may need. +/// Both the tree-walking `Interpreter` and the bytecode `Vm` implement this. +pub trait Runtime { + /// Load and execute a module, returning its exports object. + /// Implementations differ: tree-walker interprets directly, + /// VM compiles to bytecode then executes. + fn require(&mut self, path: &str) -> Result; +} + +pub type NativeFn = Rc) -> Result>; #[derive(Clone)] pub enum Value { diff --git a/aster-core/src/interpreter/module.rs b/aster-core/src/interpreter/module.rs index ba07c1a..33be480 100644 --- a/aster-core/src/interpreter/module.rs +++ b/aster-core/src/interpreter/module.rs @@ -8,15 +8,20 @@ use crate::error::RuntimeError; use crate::lexer::Lexer; use crate::parser::Parser; -use super::{Interpreter, Env, Value, Signal}; +use super::{Interpreter, Env, Value, Signal, Runtime}; use std::cell::RefCell; use std::collections::HashMap; use std::path::Path; use std::rc::Rc; -/// require() 的内置函数实现 -pub fn require_fn(interp: &mut Interpreter, args: Vec) -> Result { - // 1. 参数验证 +impl Runtime for Interpreter { + fn require(&mut self, path: &str) -> Result { + require_impl(self, path) + } +} + +/// require() 的内置函数实现 — 薄包装,委托给 Runtime::require +pub fn require_fn(runtime: &mut dyn Runtime, args: Vec) -> Result { let path_str = match args.first() { Some(Value::String(s)) => s.clone(), Some(other) => return Err(runtime_error(format!( @@ -24,28 +29,32 @@ pub fn require_fn(interp: &mut Interpreter, args: Vec) -> Result return Err(runtime_error( "require() expects 1 argument (string path)")), }; + runtime.require(&path_str) +} - // 2. 路径解析 - let resolved = resolve_path(&interp.current_dir, &path_str)?; +/// require() 的内部实现 (供 Interpreter::require 使用) +fn require_impl(interp: &mut Interpreter, path_str: &str) -> Result { + // 1. 路径解析 + let resolved = resolve_path(&interp.current_dir, path_str)?; - // 3. 缓存查找 + // 2. 缓存查找 if let Some(cached) = interp.module_cache.borrow().get(&resolved) { return Ok(cached.clone()); } - // 4. 读取文件 + // 3. 读取文件 let src = std::fs::read_to_string(&resolved) .map_err(|e| runtime_error(format!( "Module '{}' not found: {}", path_str, e)))?; - // 5. 词法分析 + // 4. 词法分析 let (tokens, lex_errors) = Lexer::new(&src).tokenize(); if !lex_errors.is_empty() { return Err(runtime_error(format!( "Lex error in module '{}': {}", path_str, lex_errors[0]))); } - // 6. 语法分析 + // 5. 语法分析 let mut parser = Parser::new(tokens); let (stmts, parse_errors) = parser.parse(); if !parse_errors.is_empty() { @@ -53,27 +62,26 @@ pub fn require_fn(interp: &mut Interpreter, args: Vec) -> Result { - // 恢复状态后返回错误 interp.env = previous_env; interp.current_dir = previous_dir; interp.module_cache.borrow_mut().remove(&resolved); @@ -81,23 +89,20 @@ pub fn require_fn(interp: &mut Interpreter, args: Vec) -> Result { - // 恢复状态后传播错误 interp.env = previous_env; interp.current_dir = previous_dir; interp.module_cache.borrow_mut().remove(&resolved); return Err(e); } - Ok(Signal::None) | Ok(Signal::Return(_)) => { - // 正常执行或顶层 return,继续 - } + Ok(Signal::None) | Ok(Signal::Return(_)) => {} } } - // 12. 恢复调用者状态 + // 11. 恢复调用者状态 interp.env = previous_env; interp.current_dir = previous_dir; - // 13. 收集 exports(模块 env 中的所有直接绑定) + // 12. 收集 exports(模块 env 中的所有直接绑定) for (name, (val, _mutable)) in module_env.borrow().values.clone() { exports_map.borrow_mut().insert(name, val); } @@ -109,7 +114,6 @@ pub fn require_fn(interp: &mut Interpreter, args: Vec) -> Result Result { let path = Path::new(path_str); let resolved = if path.is_absolute() { @@ -124,7 +128,6 @@ fn resolve_path(current_dir: &str, path_str: &str) -> Result String { Path::new(resolved) .parent() diff --git a/aster-core/src/lib.rs b/aster-core/src/lib.rs index 04e3edb..4766e23 100644 --- a/aster-core/src/lib.rs +++ b/aster-core/src/lib.rs @@ -2,12 +2,15 @@ pub mod lexer; pub mod ast; pub mod parser; pub mod interpreter; +pub mod vm; pub mod error; pub mod analysis; use lexer::Lexer; use parser::Parser; use interpreter::Interpreter; +use vm::compiler::Compiler; +use vm::vm::Vm; use error::RuntimeError; use std::io::Write; @@ -46,6 +49,41 @@ pub fn run_file(filename: &str, src: String) { } } +/// Run a file using the bytecode VM (for performance comparison). +pub fn run_file_vm(filename: &str, src: String) { + let (tokens, lex_errors) = Lexer::new(&src).tokenize(); + if !lex_errors.is_empty() { + print_errors(&lex_errors); + std::process::exit(65); + } + + let mut parser = Parser::new(tokens); + let (stmts, parse_errors) = parser.parse(); + if !parse_errors.is_empty() { + print_errors(&parse_errors); + std::process::exit(65); + } + + let script_dir = std::path::Path::new(filename) + .parent() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|| ".".to_string()); + + let proto = match Compiler::compile(&stmts) { + Ok(p) => p, + Err(e) => { + eprintln!("Compile Error: {}", e); + std::process::exit(70); + } + }; + + let mut vm = Vm::with_current_dir(script_dir); + if let Err(e) = vm.run(std::rc::Rc::new(proto)) { + eprintln!("Error: {}", e); + std::process::exit(70); + } +} + pub fn run_repl() { println!("Welcome to Aster REPL!"); println!("Type ':exit' to quit."); diff --git a/aster-core/src/vm/compiler.rs b/aster-core/src/vm/compiler.rs new file mode 100644 index 0000000..8b83213 --- /dev/null +++ b/aster-core/src/vm/compiler.rs @@ -0,0 +1,988 @@ +//! AST → bytecode compiler for the Aster VM. +//! +//! Walks the AST recursively, emitting stack-based bytecode instructions. +//! Handles local variable resolution (slot indices), upvalue capture for +//! closures, jump backpatching for control flow, and constant pool management. + +use crate::ast::*; +use crate::ast::expr::{Literal, UnaryOp, BinaryOp, LogicalOp, AssignOp}; +use crate::error::RuntimeError; +use crate::interpreter::Value; +use super::opcode::*; + +use std::rc::Rc; +use std::cell::RefCell; + +// ============================================================================ +// FunctionProto — compiled function blueprint +// ============================================================================ + +#[derive(Debug, Clone)] +pub struct FunctionProto { + pub name: Option, + pub arity: u8, + pub code: Vec, + pub constants: Vec, + /// Nested function protos (for closures and function declarations) + pub protos: Vec>, + 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) -> 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 +// ============================================================================ + +#[derive(Clone)] +struct Local { + name: String, + depth: u8, // scope depth where declared; 0 = uninitialized + is_captured: bool, + #[allow(dead_code)] + is_const: bool, +} + +#[derive(Clone)] +struct Upvalue { + index: u8, + is_local: bool, // true = captured from enclosing fn's local; false = from upvalue + name: String, // variable name (for transitive upvalue resolution) +} + +struct LoopContext { + break_patches: Vec, + continue_patches: Vec, +} + +pub struct Compiler { + function: FunctionProto, + locals: Vec, + /// Upvalues for this function (shared with child for transitive resolution) + upvalues: Rc>>, + /// ALL enclosing locals from entire chain (merged, direct parent first) + enclosing_locals: Option>, + /// Direct parent's own locals count (to distinguish parent locals from deeper ones) + parent_locals_count: usize, + /// Direct parent's upvalues (shared) + enclosing_upvalues: Option>>>, + /// Grandparent's upvalues (shared, for 3-level transitive capture) + grandparent_upvalues: Option>>>, + scope_depth: u8, + loop_stack: Vec, +} + +impl Compiler { + pub fn new(name: Option) -> Self { + Self { + function: FunctionProto::new(name), + locals: Vec::new(), + upvalues: Rc::new(RefCell::new(Vec::new())), + enclosing_locals: None, + parent_locals_count: 0, + enclosing_upvalues: None, + grandparent_upvalues: None, + scope_depth: 0, + loop_stack: Vec::new(), + } + } + + /// Convenience: compile a list of statements into a FunctionProto. + pub fn compile(stmts: &[Stmt]) -> Result { + let mut compiler = Self::new(None); + for stmt in stmts { + compiler.compile_stmt(stmt)?; + } + // Implicit return nil at end of function + compiler.emit_op(OpCode::LoadNil); + compiler.emit_op(OpCode::Return); + Ok(compiler.function) + } + + /// Compile a list of statements (for use from nested compilers). + fn compile_stmts(&mut self, stmts: &[Stmt]) -> Result<(), RuntimeError> { + for stmt in stmts { + self.compile_stmt(stmt)?; + } + Ok(()) + } + + // ======================================================================== + // Statement compilation + // ======================================================================== + + fn compile_stmt(&mut self, stmt: &Stmt) -> Result<(), RuntimeError> { + match stmt { + Stmt::Let { name, initializer, mutable } => { + self.compile_let(name, initializer, *mutable)?; + } + Stmt::ExprStmt(expr) => { + self.compile_expr(expr)?; + self.emit_op(OpCode::Pop); + } + Stmt::Block(stmts) => { + self.begin_scope(); + self.compile_stmts(stmts)?; + self.end_scope(); + } + Stmt::If { condition, then_branch, else_branch } => { + self.compile_if(condition, then_branch, else_branch.as_deref())?; + } + Stmt::While { condition, body } => { + self.compile_while(condition, body)?; + } + Stmt::For { initializer, condition, step, body } => { + self.compile_for(initializer.as_deref(), condition.as_ref(), step.as_ref(), body)?; + } + Stmt::ForIn { var_name, iterable, body } => { + self.compile_for_in(var_name, iterable, body)?; + } + Stmt::Function { name, params, body } => { + self.compile_function_decl(name, params, body)?; + // DefineGlobal pushes the value back; pop it as this is a statement + self.emit_op(OpCode::Pop); + } + Stmt::Return(expr) => { + if let Some(e) = expr { + self.compile_expr(e)?; + } else { + self.emit_op(OpCode::LoadNil); + } + self.emit_op(OpCode::Return); + } + Stmt::Break => { + self.compile_break()?; + } + Stmt::Continue => { + self.compile_continue()?; + } + } + Ok(()) + } + + fn compile_let(&mut self, name: &str, initializer: &Expr, mutable: bool) -> Result<(), RuntimeError> { + self.compile_expr(initializer)?; + if self.scope_depth == 0 { + let name_idx = self.add_string_constant(name); + emit_u16(&mut self.function.code, OpCode::DefineGlobal, name_idx); + // DefineGlobal pushes value back; pop it for statement-level let + self.emit_op(OpCode::Pop); + } else { + let slot = self.locals.len() as u8; + self.locals.push(Local { + name: name.to_string(), + depth: self.scope_depth, + is_captured: false, + is_const: !mutable, + }); + // StoreLocal PEEKS the value — it stays on stack as the local + emit_u8(&mut self.function.code, OpCode::StoreLocal, slot); + } + Ok(()) + } + + fn compile_if(&mut self, condition: &Expr, then_branch: &Stmt, else_branch: Option<&Stmt>) -> Result<(), RuntimeError> { + self.compile_expr(condition)?; + let else_jump = self.emit_jump(OpCode::JumpIfFalse); + self.compile_stmt(then_branch)?; + if let Some(else_stmt) = else_branch { + let end_jump = self.emit_jump(OpCode::Jump); + self.patch_jump(else_jump); + self.compile_stmt(else_stmt)?; + self.patch_jump(end_jump); + } else { + self.patch_jump(else_jump); + } + Ok(()) + } + + fn compile_while(&mut self, condition: &Expr, body: &Stmt) -> Result<(), RuntimeError> { + let start_ip = self.function.code.len(); + self.compile_expr(condition)?; + let exit_jump = self.emit_jump(OpCode::JumpIfFalse); + + self.loop_stack.push(LoopContext { + break_patches: Vec::new(), + continue_patches: Vec::new(), + }); + + self.compile_stmt(body)?; + self.emit_loop_jump(start_ip); + + self.patch_jump(exit_jump); + + let loop_ctx = self.loop_stack.pop().unwrap(); + for patch in loop_ctx.break_patches { + self.patch_jump(patch); + } + for patch in loop_ctx.continue_patches { + self.patch_jump_to(patch, start_ip); + } + Ok(()) + } + + fn compile_for(&mut self, initializer: Option<&Stmt>, condition: Option<&Expr>, step: Option<&Expr>, body: &Stmt) -> Result<(), RuntimeError> { + // For loop: new scope for init + self.begin_scope(); + + if let Some(init) = initializer { + self.compile_stmt(init)?; + } + + let start_ip = self.function.code.len(); + if let Some(cond) = condition { + self.compile_expr(cond)?; + } else { + self.emit_op(OpCode::LoadTrue); + } + let exit_jump = self.emit_jump(OpCode::JumpIfFalse); + + self.loop_stack.push(LoopContext { + break_patches: Vec::new(), + continue_patches: Vec::new(), + }); + + self.compile_stmt(body)?; + + // continue lands here (after body, before step) + let continue_ip = self.function.code.len(); + let loop_ctx = self.loop_stack.pop().unwrap(); + for patch in loop_ctx.continue_patches { + self.patch_jump_to(patch, continue_ip); + } + + if let Some(s) = step { + self.compile_expr(s)?; + self.emit_op(OpCode::Pop); + } + + self.emit_loop_jump(start_ip); + self.patch_jump(exit_jump); + + for patch in loop_ctx.break_patches { + self.patch_jump(patch); + } + + self.end_scope(); + Ok(()) + } + + fn compile_for_in(&mut self, var_name: &str, iterable: &Expr, body: &Stmt) -> Result<(), RuntimeError> { + // 1. New scope for hidden iterator locals (items, idx) + self.begin_scope(); + + // 2. Evaluate iterable and compute items array + self.compile_expr(iterable)?; + emit_op(&mut self.function.code, OpCode::ForInInit); // pops iterable, pushes items[] + + // 3. Store items array in a hidden local (peek, no Pop — cleaned by end_scope) + let items_slot = self.locals.len() as u8; + self.locals.push(Local { + name: format!("__iter_items_{}", items_slot), + depth: self.scope_depth, + is_captured: false, + is_const: false, + }); + emit_u8(&mut self.function.code, OpCode::StoreLocal, items_slot); + + // 4. Initialize index = 0 in hidden local + let idx_slot = self.locals.len() as u8; + self.locals.push(Local { + name: format!("__iter_idx_{}", idx_slot), + depth: self.scope_depth, + is_captured: false, + is_const: false, + }); + let zero_idx = self.add_constant(Value::Number(0.0)); + emit_u16(&mut self.function.code, OpCode::LoadConst, zero_idx); + emit_u8(&mut self.function.code, OpCode::StoreLocal, idx_slot); + + // 5. ForInNext: reads items[idx_slot], idx[idx_slot], pushes element + let forin_loc = self.emit_forin_next(items_slot, idx_slot); + + // 6. Store loop variable in a local (peek, value stays on stack) + let loop_var_slot = self.locals.len() as u8; + self.locals.push(Local { + name: var_name.to_string(), + depth: self.scope_depth, + is_captured: false, + is_const: false, + }); + emit_u8(&mut self.function.code, OpCode::StoreLocal, loop_var_slot); + + // 7. Loop body (can resolve var_name to loop_var_slot) + self.loop_stack.push(LoopContext { + break_patches: Vec::new(), + continue_patches: Vec::new(), + }); + + self.compile_stmt(body)?; + + // Patch continue + let continue_ip = self.function.code.len(); + let loop_ctx = self.loop_stack.pop().unwrap(); + for patch in loop_ctx.continue_patches { + self.patch_jump_to(patch, continue_ip); + } + + // 8. Pop loop var element and remove from tracking + self.emit_op(OpCode::Pop); + self.locals.pop(); // loop_var_slot + + // 9. Jump back to ForInNext (next iteration re-adds loop var via StoreLocal) + self.emit_loop_jump(forin_loc); + + // 10. Exit target: patch ForInNext exit and break jumps + let exit_ip = self.function.code.len(); + self.patch_forin_jump(forin_loc, exit_ip); + for patch in loop_ctx.break_patches { + self.patch_jump_to(patch, exit_ip); + } + + // 11. Pop loop var (ForInNext pushes Nil on exit; break leaves element on stack) + self.emit_op(OpCode::Pop); + + // 12. End scope: pops items_slot + idx_slot values (left by StoreLocal peek) + self.end_scope(); + Ok(()) + } + + fn compile_function_decl(&mut self, name: &str, params: &[String], body: &[Stmt]) -> Result<(), RuntimeError> { + let proto = self.compile_nested_function(Some(name.to_string()), params, body)?; + let upvalues = proto.upvalues.clone(); + let proto_idx = self.add_function_proto_constant(proto); + self.emit_closure(proto_idx, &upvalues); + + // Bind to name + if self.scope_depth == 0 { + let name_idx = self.add_string_constant(name); + emit_u16(&mut self.function.code, OpCode::DefineGlobal, name_idx); + } else { + let slot = self.locals.len() as u8; + self.locals.push(Local { + name: name.to_string(), + depth: self.scope_depth, + is_captured: false, + is_const: false, + }); + emit_u8(&mut self.function.code, OpCode::StoreLocal, slot); + } + Ok(()) + } + + fn compile_break(&mut self) -> Result<(), RuntimeError> { + let jump_loc = self.emit_jump_placeholder(); + let loop_ctx = self.loop_stack.last_mut().ok_or_else(|| RuntimeError::RuntimeError { + message: "break outside of loop".into(), + token: None, + })?; + loop_ctx.break_patches.push(jump_loc); + Ok(()) + } + + fn compile_continue(&mut self) -> Result<(), RuntimeError> { + let jump_loc = self.emit_jump_placeholder(); + let loop_ctx = self.loop_stack.last_mut().ok_or_else(|| RuntimeError::RuntimeError { + message: "continue outside of loop".into(), + token: None, + })?; + loop_ctx.continue_patches.push(jump_loc); + Ok(()) + } + + // ======================================================================== + // Expression compilation + // ======================================================================== + + fn compile_expr(&mut self, expr: &Expr) -> Result<(), RuntimeError> { + match expr { + Expr::Literal(lit) => self.compile_literal(lit), + Expr::Variable(name) => self.compile_variable(name), + Expr::Assign { name, op, value } => self.compile_assign(name, op, value), + Expr::Get { object, name } => self.compile_get(object, name), + Expr::Set { object, name, op, value } => self.compile_set(object, name, op, value), + Expr::ObjectLiteral { properties } => self.compile_object(properties), + Expr::ArrayLiteral { elements } => self.compile_array(elements), + Expr::IndexGet { array, index } => self.compile_index_get(array, index), + Expr::IndexSet { array, index, op, value } => self.compile_index_set(array, index, op, value), + Expr::Unary { op, right } => self.compile_unary(op, right), + Expr::Binary { left, op, right } => self.compile_binary(left, op, right), + Expr::Logical { left, op, right } => self.compile_logical(left, op, right), + Expr::Ternary { condition, then_branch, else_branch } => { + self.compile_ternary(condition, then_branch, else_branch) + } + Expr::Call { callee, arguments } => self.compile_call(callee, arguments), + Expr::Lambda { params, body } => self.compile_lambda(params, body), + } + } + + fn compile_literal(&mut self, lit: &Literal) -> Result<(), RuntimeError> { + match lit { + Literal::Number(n) => { + let idx = self.function.add_constant(Value::Number(*n)); + emit_u16(&mut self.function.code, OpCode::LoadConst, idx); + } + Literal::String(s) => { + let idx = self.function.add_constant(Value::String(s.clone())); + emit_u16(&mut self.function.code, OpCode::LoadConst, idx); + } + Literal::Bool(true) => self.emit_op(OpCode::LoadTrue), + Literal::Bool(false) => self.emit_op(OpCode::LoadFalse), + Literal::Nil => self.emit_op(OpCode::LoadNil), + } + Ok(()) + } + + fn compile_variable(&mut self, name: &str) -> Result<(), RuntimeError> { + // Try to resolve as local + if let Some(slot) = self.resolve_local(name) { + emit_u8(&mut self.function.code, OpCode::LoadLocal, slot); + return Ok(()); + } + // Try to resolve as upvalue + if let Some(upvalue_idx) = self.resolve_upvalue(name) { + emit_u8(&mut self.function.code, OpCode::LoadUpvalue, upvalue_idx); + return Ok(()); + } + // Fall back to global + let name_idx = self.add_string_constant(name); + emit_u16(&mut self.function.code, OpCode::LoadGlobal, name_idx); + Ok(()) + } + + fn compile_assign(&mut self, name: &str, op: &AssignOp, value: &Expr) -> Result<(), RuntimeError> { + if *op == AssignOp::Equal { + // Simple assignment + self.compile_expr(value)?; + if let Some(slot) = self.resolve_local(name) { + emit_u8(&mut self.function.code, OpCode::StoreLocal, slot); + } else if let Some(uv_idx) = self.resolve_upvalue(name) { + emit_u8(&mut self.function.code, OpCode::StoreUpvalue, uv_idx); + } else { + let name_idx = self.add_string_constant(name); + emit_u16(&mut self.function.code, OpCode::StoreGlobal, name_idx); + } + } else { + // Compound assignment + self.compile_expr(value)?; + let compound_op = match op { + AssignOp::PlusEqual => CompoundOp::PlusEqual, + AssignOp::MinusEqual => CompoundOp::MinusEqual, + AssignOp::StarEqual => CompoundOp::StarEqual, + AssignOp::SlashEqual => CompoundOp::SlashEqual, + AssignOp::PercentEqual => CompoundOp::PercentEqual, + AssignOp::Equal => unreachable!(), + }; + if let Some(slot) = self.resolve_local(name) { + emit_u8(&mut self.function.code, OpCode::CompoundAssignLocal, slot); + self.function.code.push(compound_op as u8); + } else if self.resolve_upvalue(name).is_some() { + // Compound assign on upvalue: load upvalue, compound op, store + // For simplicity, use a workaround: re-emit this as a separate step + // TODO: add CompoundAssignUpvalue opcode + return Err(RuntimeError::RuntimeError { + message: "Compound assignment on upvalue not yet supported".into(), + token: None, + }); + } else { + 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); + } + } + Ok(()) + } + + fn compile_get(&mut self, object: &Expr, name: &str) -> Result<(), RuntimeError> { + self.compile_expr(object)?; + let name_idx = self.add_string_constant(name); + emit_u16(&mut self.function.code, OpCode::GetProperty, name_idx); + Ok(()) + } + + fn compile_set(&mut self, object: &Expr, name: &str, op: &AssignOp, value: &Expr) -> Result<(), RuntimeError> { + if *op == AssignOp::Equal { + self.compile_expr(object)?; + self.compile_expr(value)?; + let name_idx = self.add_string_constant(name); + 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 + 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 + emit_u16(&mut self.function.code, OpCode::CompoundAssignProp, name_idx); + self.function.code.push(compound_op as u8); + } + Ok(()) + } + + fn compile_object(&mut self, properties: &[(String, Expr)]) -> Result<(), RuntimeError> { + emit_op(&mut self.function.code, OpCode::NewObject); + for (key, value_expr) in properties { + self.emit_op(OpCode::Dup); + self.compile_expr(value_expr)?; + let name_idx = self.add_string_constant(key); + emit_u16(&mut self.function.code, OpCode::SetProperty, name_idx); + // SetProperty leaves the value on stack; we need the object, so Pop the value + self.emit_op(OpCode::Pop); + } + Ok(()) + } + + fn compile_array(&mut self, elements: &[Expr]) -> Result<(), RuntimeError> { + for e in elements { + self.compile_expr(e)?; + } + emit_u16(&mut self.function.code, OpCode::NewArray, elements.len() as u16); + Ok(()) + } + + fn compile_index_get(&mut self, array: &Expr, index: &Expr) -> Result<(), RuntimeError> { + self.compile_expr(array)?; + self.compile_expr(index)?; + emit_op(&mut self.function.code, OpCode::GetIndex); + Ok(()) + } + + fn compile_index_set(&mut self, array: &Expr, index: &Expr, op: &AssignOp, value: &Expr) -> Result<(), RuntimeError> { + self.compile_expr(array)?; + self.compile_expr(index)?; + self.compile_expr(value)?; + if *op == AssignOp::Equal { + emit_op(&mut self.function.code, OpCode::SetIndex); + } else { + let compound_op = assign_op_to_compound(op); + emit_u8(&mut self.function.code, OpCode::CompoundAssignIndex, compound_op as u8); + } + Ok(()) + } + + fn compile_unary(&mut self, op: &UnaryOp, right: &Expr) -> Result<(), RuntimeError> { + self.compile_expr(right)?; + match op { + UnaryOp::Negate => self.emit_op(OpCode::Negate), + UnaryOp::Not => self.emit_op(OpCode::Not), + } + Ok(()) + } + + fn compile_binary(&mut self, left: &Expr, op: &BinaryOp, right: &Expr) -> Result<(), RuntimeError> { + self.compile_expr(left)?; + self.compile_expr(right)?; + let opcode = match op { + BinaryOp::Add => OpCode::Add, + BinaryOp::Sub => OpCode::Sub, + BinaryOp::Mul => OpCode::Mul, + BinaryOp::Div => OpCode::Div, + BinaryOp::Mod => OpCode::Mod, + BinaryOp::Greater => OpCode::Greater, + BinaryOp::GreaterEqual => OpCode::GreaterEqual, + BinaryOp::Less => OpCode::Less, + BinaryOp::LessEqual => OpCode::LessEqual, + BinaryOp::Equal => OpCode::Equal, + BinaryOp::NotEqual => OpCode::NotEqual, + }; + self.emit_op(opcode); + Ok(()) + } + + fn compile_logical(&mut self, left: &Expr, op: &LogicalOp, right: &Expr) -> Result<(), RuntimeError> { + self.compile_expr(left)?; + match op { + LogicalOp::And => { + // Short-circuit: if left is falsy, skip right and return left + let end_jump = self.emit_jump(OpCode::PopJumpIfFalse); + self.emit_op(OpCode::Pop); // discard left (truthy) + self.compile_expr(right)?; + self.patch_jump(end_jump); + } + LogicalOp::Or => { + // Short-circuit: if left is truthy, skip right and return left + self.emit_op(OpCode::Dup); + let end_jump = self.emit_jump(OpCode::JumpIfTrue); + self.emit_op(OpCode::Pop); // discard left (falsy) + self.compile_expr(right)?; + self.patch_jump(end_jump); + } + } + Ok(()) + } + + fn compile_ternary(&mut self, condition: &Expr, then_branch: &Expr, else_branch: &Expr) -> Result<(), RuntimeError> { + self.compile_expr(condition)?; + let else_jump = self.emit_jump(OpCode::JumpIfFalse); + self.compile_expr(then_branch)?; + let end_jump = self.emit_jump(OpCode::Jump); + self.patch_jump(else_jump); + self.compile_expr(else_branch)?; + self.patch_jump(end_jump); + Ok(()) + } + + fn compile_call(&mut self, callee: &Expr, arguments: &[Expr]) -> Result<(), RuntimeError> { + self.compile_expr(callee)?; + for arg in arguments { + self.compile_expr(arg)?; + } + emit_u8(&mut self.function.code, OpCode::Call, arguments.len() as u8); + Ok(()) + } + + fn compile_lambda(&mut self, params: &[String], body: &[Stmt]) -> Result<(), RuntimeError> { + let proto = self.compile_nested_function(None, params, body)?; + let upvalues = proto.upvalues.clone(); + let proto_idx = self.add_function_proto_constant(proto); + self.emit_closure(proto_idx, &upvalues); + Ok(()) + } + + /// Compile a nested function (lambda or function declaration) and return its proto. + fn compile_nested_function(&mut self, name: Option, params: &[String], body: &[Stmt]) -> Result { + let mut child = Compiler::new(name); + // Build merged enclosing locals: our locals + our enclosing chain + let mut all_locals = self.locals.clone(); + if let Some(ref enc) = self.enclosing_locals { + all_locals.extend(enc.iter().cloned()); + } + child.enclosing_locals = Some(all_locals); + child.parent_locals_count = self.locals.len(); + child.enclosing_upvalues = Some(Rc::clone(&self.upvalues)); + child.grandparent_upvalues = self.enclosing_upvalues.clone(); + + // Add params as locals without StoreLocal — they're already on stack from Call + for param in params { + child.locals.push(Local { + name: param.clone(), + depth: 1, // params are at scope depth 1 (function body) + is_captured: false, + is_const: false, + }); + } + child.function.arity = params.len() as u8; + + // Compile the body + child.scope_depth = 1; + for stmt in body { + child.compile_stmt(stmt)?; + } + // Implicit return nil + child.emit_op(OpCode::LoadNil); + child.emit_op(OpCode::Return); + + // Mark captured locals in the parent compiler + for uv in child.upvalues.borrow().iter() { + if uv.is_local { + if let Some(local) = self.locals.get_mut(uv.index as usize) { + local.is_captured = true; + } + } + } + + let child_upvalues = child.upvalues.borrow(); + child.function.upvalue_count = child_upvalues.len() as u8; + child.function.upvalues = child_upvalues.iter().map(|uv| (uv.is_local, uv.index)).collect(); + drop(child_upvalues); + Ok(child.function) + } + + // ======================================================================== + // Scope management + // ======================================================================== + + fn begin_scope(&mut self) { + self.scope_depth += 1; + } + + fn end_scope(&mut self) { + self.scope_depth -= 1; + // Pop locals that are going out of scope (except captured ones) + let mut pop_count = 0u8; + while let Some(local) = self.locals.last() { + if local.depth > self.scope_depth { + if local.is_captured { + // Don't pop — the upvalue still needs it on the stack + // The VM will close the upvalue when the function returns + self.locals.pop(); + } else { + self.locals.pop(); + pop_count += 1; + } + } else { + break; + } + } + for _ in 0..pop_count { + self.emit_op(OpCode::Pop); + } + } + + // ======================================================================== + // Variable resolution + // ======================================================================== + + /// Find a local variable by name, returning its slot index. + fn resolve_local(&self, name: &str) -> Option { + for (i, local) in self.locals.iter().enumerate().rev() { + if local.name == name && local.depth > 0 { + return Some(i as u8); + } + } + None + } + + /// Try to resolve a variable as an upvalue from enclosing functions. + /// Returns the upvalue index in this function's upvalues list. + fn resolve_upvalue(&mut self, name: &str) -> Option { + // Check if already captured + for (j, uv) in self.upvalues.borrow().iter().enumerate() { + if uv.name == name { + return Some(j as u8); + } + } + // Check all enclosing locals (merged chain: parent, grandparent, ...) + if let Some(ref enclosing_locals) = self.enclosing_locals { + for (i, local) in enclosing_locals.iter().enumerate().rev() { + if local.name == name && local.depth > 0 { + if i < self.parent_locals_count { + // Found in direct parent's locals → capture as upvalue + let idx = self.upvalues.borrow().len() as u8; + self.upvalues.borrow_mut().push(Upvalue { + index: i as u8, is_local: true, name: name.to_string() + }); + return Some(idx); + } else { + // Found deeper than parent. Need to create upvalue chain. + if let Some(ref enc_upvalues) = self.enclosing_upvalues { + // Check if parent already has this upvalue + let parent_uv_idx = { + let enc = enc_upvalues.borrow(); + enc.iter().position(|uv| uv.name == name).map(|p| p as u8) + }; + let parent_idx = match parent_uv_idx { + Some(idx) => idx, + None => { + // Add upvalue to parent's list. + // The parent sees this variable at a certain index in its own + // enclosing_locals. That index is (i - self.parent_locals_count) + // in the merged list, which corresponds to the same variable + // in the parent's enclosing_locals. + let enc_idx = enc_upvalues.borrow().len() as u8; + + // If we have grandparent_upvalues, the parent's upvalue + // should be transitive (is_local=false), and we need to + // ensure grandparent has it too. + if let Some(ref gp_upvalues) = self.grandparent_upvalues { + // Ensure grandparent has the upvalue first + let gp_idx = { + let gp = gp_upvalues.borrow(); + gp.iter().position(|uv| uv.name == name).map(|p| p as u8) + }; + let gp_idx = match gp_idx { + Some(idx) => idx, + None => { + let idx = gp_upvalues.borrow().len() as u8; + // Grandparent captures this as a local + // (it's directly in the grandparent's enclosing scope) + gp_upvalues.borrow_mut().push(Upvalue { + index: (i - self.parent_locals_count) as u8, + is_local: true, + name: name.to_string() + }); + idx + } + }; + // Parent's upvalue is transitive through grandparent + enc_upvalues.borrow_mut().push(Upvalue { + index: gp_idx, + is_local: false, + name: name.to_string() + }); + } else { + // No grandparent — parent captures directly as local + enc_upvalues.borrow_mut().push(Upvalue { + index: (i - self.parent_locals_count) as u8, + is_local: true, + name: name.to_string() + }); + } + enc_idx + } + }; + // Add transitive upvalue in self pointing to parent's + let idx = self.upvalues.borrow().len() as u8; + self.upvalues.borrow_mut().push(Upvalue { + index: parent_idx, is_local: false, name: name.to_string() + }); + return Some(idx); + } + } + } + } + } + // Check parent's upvalues (transitive closure over 2 levels) + if let Some(ref enclosing_upvalues) = self.enclosing_upvalues { + for uv in enclosing_upvalues.borrow().iter().rev() { + if uv.name == name { + let idx = self.upvalues.borrow().len() as u8; + self.upvalues.borrow_mut().push(Upvalue { + index: uv.index, is_local: false, name: name.to_string() + }); + return Some(idx); + } + } + } + None + } + + // ======================================================================== + // Bytecode emission helpers + // ======================================================================== + + fn emit_op(&mut self, op: OpCode) { + emit_op(&mut self.function.code, op); + } + + fn emit_jump(&mut self, op: OpCode) -> usize { + let loc = self.function.code.len(); + emit_i16(&mut self.function.code, op, 0x7FFF); // placeholder + loc + } + + fn emit_jump_placeholder(&mut self) -> usize { + let loc = self.function.code.len(); + emit_i16(&mut self.function.code, OpCode::Jump, 0x7FFF); + loc + } + + fn emit_loop_jump(&mut self, target: usize) { + let offset = target as isize - self.function.code.len() as isize; + emit_i16(&mut self.function.code, OpCode::Jump, offset as i16); + } + + fn emit_forin_next(&mut self, items_slot: u8, idx_slot: u8) -> usize { + let loc = self.function.code.len(); + let code = &mut self.function.code; + code.push(OpCode::ForInNext as u8); + code.push(items_slot); + code.push(idx_slot); + code.push(0xFF); // placeholder offset low + code.push(0x7F); // placeholder offset high + loc + } + + fn emit_closure(&mut self, proto_idx: u16, upvalues: &[(bool, u8)]) { + let code = &mut self.function.code; + code.push(OpCode::Closure as u8); + code.push((proto_idx & 0xFF) as u8); + code.push(((proto_idx >> 8) & 0xFF) as u8); + let upvalue_count = upvalues.len() as u8; + code.push(upvalue_count); + for &(is_local, index) in upvalues { + code.push(if is_local { 1u8 } else { 0u8 }); + code.push(index); + } + } + + fn patch_jump(&mut self, jump_loc: usize) { + let offset = (self.function.code.len() - jump_loc) as i16; + let code = &mut self.function.code; + code[jump_loc + 1] = (offset & 0xFF) as u8; + code[jump_loc + 2] = ((offset >> 8) & 0xFF) as u8; + } + + fn patch_jump_to(&mut self, jump_loc: usize, target: usize) { + let offset = target as isize - jump_loc as isize; + let code = &mut self.function.code; + code[jump_loc + 1] = (offset & 0xFF) as u8; + code[jump_loc + 2] = ((offset >> 8) & 0xFF) as u8; + } + + /// Patch ForInNext's exit offset (at jump_loc + 3, +4) + fn patch_forin_jump(&mut self, forin_loc: usize, target: usize) { + let offset = target as isize - forin_loc as isize; + let code = &mut self.function.code; + code[forin_loc + 3] = (offset & 0xFF) as u8; + code[forin_loc + 4] = ((offset >> 8) & 0xFF) as u8; + } + + fn add_constant(&mut self, val: Value) -> u16 { + self.function.add_constant(val) + } + + // ======================================================================== + // Constant pool helpers + // ======================================================================== + + fn add_string_constant(&mut self, s: &str) -> u16 { + self.function.add_constant(Value::String(s.to_string())) + } + + fn add_function_proto_constant(&mut self, proto: FunctionProto) -> u16 { + let idx = self.function.protos.len() as u16; + self.function.protos.push(Rc::new(proto)); + // Store proto index as a sentinel value in constants + self.function.constants.push(Value::Number(f64::from_bits(idx as u64 | 0x_F000_0000_0000_0000))); + idx + } +} + +// ============================================================================ +// Helpers +// ============================================================================ + +fn assign_op_to_compound(op: &AssignOp) -> CompoundOp { + match op { + AssignOp::PlusEqual => CompoundOp::PlusEqual, + AssignOp::MinusEqual => CompoundOp::MinusEqual, + AssignOp::StarEqual => CompoundOp::StarEqual, + AssignOp::SlashEqual => CompoundOp::SlashEqual, + AssignOp::PercentEqual => CompoundOp::PercentEqual, + AssignOp::Equal => unreachable!(), + } +} diff --git a/aster-core/src/vm/mod.rs b/aster-core/src/vm/mod.rs new file mode 100644 index 0000000..9ccfdde --- /dev/null +++ b/aster-core/src/vm/mod.rs @@ -0,0 +1,3 @@ +pub mod opcode; +pub mod compiler; +pub mod vm; diff --git a/aster-core/src/vm/opcode.rs b/aster-core/src/vm/opcode.rs new file mode 100644 index 0000000..667cb04 --- /dev/null +++ b/aster-core/src/vm/opcode.rs @@ -0,0 +1,172 @@ +//! Bytecode opcode definitions for the Aster VM. +//! +//! Each instruction is encoded as a 1-byte opcode tag followed by +//! variable-length operands (u8 for local slots, u16 for constant pool +//! indices, i16 for relative jump offsets). + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum OpCode { + // --- Stack manipulation --- + Pop = 0, + Dup = 1, + + // --- Constants (operand: u16 index into constant pool) --- + LoadConst = 2, + LoadNil = 3, + LoadTrue = 4, + LoadFalse = 5, + + // --- Local variables (operand: u8 slot index) --- + LoadLocal = 6, + StoreLocal = 7, + LoadUpvalue = 42, // operand: u8 upvalue index + StoreUpvalue = 43, // operand: u8 upvalue index + + // --- Global variables (operand: u16 index into constant pool for name) --- + LoadGlobal = 8, + StoreGlobal = 9, + DefineGlobal = 10, + + // --- Property access (operand: u16 index into constant pool for name) --- + GetProperty = 11, + SetProperty = 12, + + // --- Index access (operands on stack) --- + GetIndex = 13, + SetIndex = 14, + + // --- Binary arithmetic --- + Add = 15, + Sub = 16, + Mul = 17, + Div = 18, + Mod = 19, + + // --- Unary --- + Negate = 20, + Not = 21, + + // --- Comparison --- + Equal = 22, + NotEqual = 23, + Greater = 24, + GreaterEqual = 25, + Less = 26, + LessEqual = 27, + + // --- Control flow (operand: i16 relative jump offset) --- + Jump = 28, + JumpIfFalse = 29, + JumpIfTrue = 30, + PopJumpIfFalse = 31, + + // --- Functions --- + Call = 32, // operand: u8 arg count + Return = 33, + Closure = 34, // operand: u16 proto idx, then N*(u8 is_local, u8 index) + + // --- Object/Array construction --- + NewObject = 35, + NewArray = 36, // operand: u16 element count + + // --- For-in support --- + ForInInit = 37, + ForInNext = 38, // operand: i16 done jump offset + + // --- Compound assignment --- + CompoundAssignLocal = 39, // operand: u8 slot + u8 compound-op tag + CompoundAssignProp = 40, // operand: u16 name idx + u8 compound-op tag + CompoundAssignIndex = 41, // operand: u8 compound-op tag +} + +impl OpCode { + pub fn from_u8(byte: u8) -> Option { + match byte { + 0..=41 => Some(unsafe { std::mem::transmute::(byte) }), + 42 => Some(OpCode::LoadUpvalue), + 43 => Some(OpCode::StoreUpvalue), + _ => None, + } + } + + /// Total number of distinct opcodes + pub const COUNT: usize = 44; +} + +/// Compound assignment operator tags (used as operand byte after +/// CompoundAssignLocal/Prop/Index). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum CompoundOp { + PlusEqual = 0, + MinusEqual = 1, + StarEqual = 2, + SlashEqual = 3, + PercentEqual = 4, +} + +impl CompoundOp { + pub fn from_u8(byte: u8) -> Option { + if byte <= 4 { + Some(unsafe { std::mem::transmute::(byte) }) + } else { + None + } + } +} + +// ============================================================================ +// Instruction encoding helpers (for compiler use) +// ============================================================================ + +/// Write a u8 operand after the opcode byte. +pub fn emit_u8(code: &mut Vec, op: OpCode, operand: u8) { + code.push(op as u8); + code.push(operand); +} + +/// Write a u16 operand after the opcode byte (little-endian). +pub fn emit_u16(code: &mut Vec, op: OpCode, operand: u16) { + code.push(op as u8); + code.push((operand & 0xFF) as u8); + code.push(((operand >> 8) & 0xFF) as u8); +} + +/// Write an i16 operand after the opcode byte (little-endian). +pub fn emit_i16(code: &mut Vec, op: OpCode, offset: i16) { + code.push(op as u8); + code.push((offset & 0xFF) as u8); + code.push(((offset >> 8) & 0xFF) as u8); +} + +/// Write a standalone opcode byte. +pub fn emit_op(code: &mut Vec, op: OpCode) { + code.push(op as u8); +} + +// ============================================================================ +// Instruction decoding helpers (for VM use) +// ============================================================================ + +/// Read a u8 at position `ip + 1` (right after the opcode byte). +pub fn read_u8(code: &[u8], ip: usize) -> u8 { + code[ip + 1] +} + +/// Read a u16 at position `ip + 1` (little-endian, right after the opcode byte). +pub fn read_u16(code: &[u8], ip: usize) -> u16 { + (code[ip + 1] as u16) | ((code[ip + 2] as u16) << 8) +} + +/// Read an i16 at position `ip + 1` (little-endian, right after the opcode byte). +pub fn read_i16(code: &[u8], ip: usize) -> i16 { + ((code[ip + 1] as u16) | ((code[ip + 2] as u16) << 8)) as i16 +} + +/// Size in bytes of an opcode with no operand. +pub const SIZE_OP: usize = 1; +/// Size in bytes of an opcode with a u8 operand. +pub const SIZE_U8: usize = 2; +/// Size in bytes of an opcode with a u16/i16 operand. +pub const SIZE_U16: usize = 3; diff --git a/aster-core/src/vm/vm.rs b/aster-core/src/vm/vm.rs new file mode 100644 index 0000000..690df32 --- /dev/null +++ b/aster-core/src/vm/vm.rs @@ -0,0 +1,1111 @@ +//! Stack-based bytecode VM for Aster. +//! +//! Executes compiled bytecode from `FunctionProto`. Uses a value stack with +//! call frames. Implements the `Runtime` trait for native function support. + +use crate::error::RuntimeError; +use crate::interpreter::{Value, Runtime}; +use crate::lexer::Lexer; +use crate::parser::Parser; +use super::opcode::*; +use super::compiler::{Compiler, FunctionProto}; + +use std::cell::RefCell; +use std::collections::HashMap; +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, +} + +/// A runtime closure: compiled function proto + captured upvalues. +#[derive(Debug, Clone)] +struct Closure { + proto: Rc, + upvalues: Vec>>, +} + +/// A call frame on the VM stack. +struct CallFrame { + closure: Rc, + ip: usize, + stack_base: usize, +} + +pub struct Vm { + /// Value stack + pub stack: Vec, + + /// Call frames + frames: Vec, + + /// Script-level globals (top-level let bindings) + pub globals: Rc>>, + + /// Builtins (shared across modules) + pub builtins: Rc>>, + + /// Module cache (shared across require() calls) + pub module_cache: RefCell>, + + /// Current directory for module resolution + pub current_dir: String, + + /// Open upvalues (tracked so closures share the same upvalue object) + open_upvalues: Vec>>, + + /// VM-compiled closures: maps Function Rc pointer → Closure data + closures: RefCell>>, +} + +impl Vm { + pub fn new() -> Self { + let builtins = Rc::new(RefCell::new(HashMap::new())); + let globals = Rc::new(RefCell::new(HashMap::new())); + + // Register builtins into the builtins map + register_builtins(&builtins); + + Self { + stack: Vec::with_capacity(256), + frames: Vec::with_capacity(64), + globals, + builtins, + module_cache: RefCell::new(HashMap::new()), + current_dir: std::env::current_dir() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|_| ".".to_string()), + open_upvalues: Vec::new(), + closures: RefCell::new(HashMap::new()), + } + } + + pub fn with_current_dir(dir: String) -> Self { + let mut vm = Self::new(); + vm.current_dir = dir; + vm + } + + /// Compile and execute a list of statements. + pub fn interpret(&mut self, stmts: &[crate::ast::Stmt]) -> Result<(), RuntimeError> { + let proto = Compiler::compile(stmts)?; + self.run(Rc::new(proto)) + } + + /// Execute a compiled FunctionProto. + pub fn run(&mut self, proto: Rc) -> Result<(), RuntimeError> { + let closure = Rc::new(Closure { + proto: Rc::clone(&proto), + upvalues: Vec::new(), + }); + + self.frames.push(CallFrame { + closure, + ip: 0, + stack_base: 0, + }); + + self.execute_loop() + } + + // ======================================================================== + // Main execution loop + // ======================================================================== + + fn execute_loop(&mut self) -> Result<(), RuntimeError> { + loop { + if self.frames.is_empty() { + return Ok(()); + } + + // Snapshot frame state (must drop borrow before mutating self) + let ip = self.frames.last().unwrap().ip; + let code_len = self.frames.last().unwrap().closure.proto.code.len(); + + if ip >= code_len { + // End of function — implicit return nil + let frame = self.frames.pop().unwrap(); + self.close_upvalues(frame.stack_base); + if self.frames.is_empty() { + return Ok(()); + } + // Remove callee + args + function locals, push nil result + self.stack.truncate(frame.stack_base.saturating_sub(1)); + self.stack.push(Value::Nil); + continue; + } + + // Clone Rc to access code without holding self.frames borrow + let proto: Rc = Rc::clone(&self.frames.last().unwrap().closure.proto); + + // Read opcode byte + let op = OpCode::from_u8(proto.code[ip]) + .ok_or_else(|| RuntimeError::RuntimeError { + message: format!("Unknown opcode: {}", proto.code[ip]), + token: None, + })?; + + // Local reference to code (borrows from proto, not self) + let code = &proto.code; + + match op { + OpCode::Pop => { + self.stack.pop(); + self.advance_ip(SIZE_OP); + } + OpCode::Dup => { + let val = self.stack.last().unwrap().clone(); + self.stack.push(val); + self.advance_ip(SIZE_OP); + } + + // --- Constants --- + OpCode::LoadConst => { + let idx = read_u16(code, ip) as usize; + let val = proto.constants.get(idx).cloned().ok_or_else(|| RuntimeError::RuntimeError { + message: format!("Constant index {} out of bounds", idx), + token: None, + })?; + self.stack.push(val); + self.advance_ip(SIZE_U16); + } + OpCode::LoadNil => { + self.stack.push(Value::Nil); + self.advance_ip(SIZE_OP); + } + OpCode::LoadTrue => { + self.stack.push(Value::Bool(true)); + self.advance_ip(SIZE_OP); + } + OpCode::LoadFalse => { + self.stack.push(Value::Bool(false)); + self.advance_ip(SIZE_OP); + } + + // --- Locals --- + OpCode::LoadLocal => { + let slot = read_u8(code, ip) as usize; + let idx = self.frames.last().unwrap().stack_base + slot; + if idx >= self.stack.len() { + return Err(RuntimeError::RuntimeError { + message: format!("LoadLocal: slot {} uninitialized", slot), + token: None, + }); + } + let val = self.stack[idx].clone(); + self.stack.push(val); + self.advance_ip(SIZE_U8); + } + OpCode::StoreLocal => { + let slot = read_u8(code, ip) as usize; + let val = self.stack.last().unwrap().clone(); // peek + let base = self.frames.last().unwrap().stack_base; + let idx = base + slot; + if idx >= self.stack.len() { + self.stack.resize(idx + 1, Value::Nil); + } + self.stack[idx] = val; + self.advance_ip(SIZE_U8); + } + OpCode::LoadUpvalue => { + let idx = read_u8(code, ip) as usize; + let uv = Rc::clone(&self.frames.last().unwrap().closure.upvalues[idx]); + let uv_ref = uv.borrow(); + let val = if let Some(ref closed) = uv_ref.closed { + closed.clone() + } else { + self.stack[uv_ref.location].clone() + }; + drop(uv_ref); + self.stack.push(val); + self.advance_ip(SIZE_U8); + } + OpCode::StoreUpvalue => { + let idx = read_u8(code, ip) as usize; + let val = self.stack.last().unwrap().clone(); // peek + let uv = Rc::clone(&self.frames.last().unwrap().closure.upvalues[idx]); + let mut uv_ref = uv.borrow_mut(); + if let Some(ref mut closed) = uv_ref.closed { + *closed = val; + } else { + self.stack[uv_ref.location] = val; + } + self.advance_ip(SIZE_U8); + } + + // --- Globals --- + OpCode::LoadGlobal => { + let name_idx = read_u16(code, ip) as usize; + let name = proto_string(&proto, name_idx)?; + let val = self.globals.borrow().get(&name).cloned() + .or_else(|| self.builtins.borrow().get(&name).cloned()) + .ok_or_else(|| RuntimeError::RuntimeError { + message: format!("Undefined variable '{}'", name), + token: None, + })?; + self.stack.push(val); + self.advance_ip(SIZE_U16); + } + OpCode::StoreGlobal => { + let name_idx = read_u16(code, ip) as usize; + let name = proto_string(&proto,name_idx)?; + let val = self.stack.last().unwrap().clone(); + self.globals.borrow_mut().insert(name, val); + self.advance_ip(SIZE_U16); + } + OpCode::DefineGlobal => { + let name_idx = read_u16(code, ip) as usize; + let name = proto_string(&proto,name_idx)?; + let val = self.stack.pop().unwrap(); + self.globals.borrow_mut().insert(name, val.clone()); + self.stack.push(val); + self.advance_ip(SIZE_U16); + } + + // --- Properties --- + OpCode::GetProperty => { + let name_idx = read_u16(code, ip) as usize; + let name = proto_string(&proto,name_idx)?; + let obj = self.stack.pop().unwrap(); + let result = self.get_property(obj, &name)?; + self.stack.push(result); + self.advance_ip(SIZE_U16); + } + OpCode::SetProperty => { + let name_idx = read_u16(code, ip) as usize; + let name = proto_string(&proto,name_idx)?; + let val = self.stack.pop().unwrap(); + let obj = self.stack.pop().unwrap(); + self.set_property(obj, &name, val.clone())?; + self.stack.push(val); + self.advance_ip(SIZE_U16); + } + + // --- Indexing --- + OpCode::GetIndex => { + let index = self.stack.pop().unwrap(); + let target = self.stack.pop().unwrap(); + let result = self.get_index(target, index)?; + self.stack.push(result); + self.advance_ip(SIZE_OP); + } + OpCode::SetIndex => { + let val = self.stack.pop().unwrap(); + let index = self.stack.pop().unwrap(); + let target = self.stack.pop().unwrap(); + self.set_index(target, index, val.clone())?; + self.stack.push(val); + self.advance_ip(SIZE_OP); + } + + // --- Arithmetic --- + OpCode::Add => { + let r = self.stack.pop().unwrap(); + let l = self.stack.pop().unwrap(); + self.stack.push(self.binary_add(l, r)?); + self.advance_ip(SIZE_OP); + } + OpCode::Sub => { + let r = self.stack.pop().unwrap(); + let l = self.stack.pop().unwrap(); + self.stack.push(self.binary_arith(l, r, |a, b| a - b, "-")?); + self.advance_ip(SIZE_OP); + } + OpCode::Mul => { + let r = self.stack.pop().unwrap(); + let l = self.stack.pop().unwrap(); + self.stack.push(self.binary_arith(l, r, |a, b| a * b, "*")?); + self.advance_ip(SIZE_OP); + } + OpCode::Div => { + let r = self.stack.pop().unwrap(); + let l = self.stack.pop().unwrap(); + self.stack.push(self.binary_div(l, r)?); + self.advance_ip(SIZE_OP); + } + OpCode::Mod => { + let r = self.stack.pop().unwrap(); + let l = self.stack.pop().unwrap(); + self.stack.push(self.binary_mod(l, r)?); + self.advance_ip(SIZE_OP); + } + + // --- Unary --- + OpCode::Negate => { + let val = self.stack.pop().unwrap(); + match val { + Value::Number(n) => self.stack.push(Value::Number(-n)), + _ => return Err(RuntimeError::RuntimeError { + message: "Unary '-' on non-number".into(), + token: None, + }), + } + self.advance_ip(SIZE_OP); + } + OpCode::Not => { + let val = self.stack.pop().unwrap(); + let truth = !self.is_truthy(&val); + self.stack.push(Value::Bool(truth)); + self.advance_ip(SIZE_OP); + } + + // --- Comparisons --- + OpCode::Equal => { + let r = self.stack.pop().unwrap(); + let l = self.stack.pop().unwrap(); + self.stack.push(Value::Bool(self.is_equal(&l, &r))); + self.advance_ip(SIZE_OP); + } + OpCode::NotEqual => { + let r = self.stack.pop().unwrap(); + let l = self.stack.pop().unwrap(); + self.stack.push(Value::Bool(!self.is_equal(&l, &r))); + self.advance_ip(SIZE_OP); + } + OpCode::Greater => { + let r = self.stack.pop().unwrap(); + let l = self.stack.pop().unwrap(); + self.stack.push(Value::Bool(self.as_number(&l)? > self.as_number(&r)?)); + self.advance_ip(SIZE_OP); + } + OpCode::GreaterEqual => { + let r = self.stack.pop().unwrap(); + let l = self.stack.pop().unwrap(); + self.stack.push(Value::Bool(self.as_number(&l)? >= self.as_number(&r)?)); + self.advance_ip(SIZE_OP); + } + OpCode::Less => { + let r = self.stack.pop().unwrap(); + let l = self.stack.pop().unwrap(); + self.stack.push(Value::Bool(self.as_number(&l)? < self.as_number(&r)?)); + self.advance_ip(SIZE_OP); + } + OpCode::LessEqual => { + let r = self.stack.pop().unwrap(); + let l = self.stack.pop().unwrap(); + self.stack.push(Value::Bool(self.as_number(&l)? <= self.as_number(&r)?)); + self.advance_ip(SIZE_OP); + } + + // --- Jumps --- + OpCode::Jump => { + let offset = read_i16(code, ip) as isize; + self.advance_ip_to(((ip as isize) + offset) as usize); + } + OpCode::JumpIfFalse => { + let cond = self.stack.pop().unwrap(); + if !self.is_truthy(&cond) { + let offset = read_i16(code, ip) as isize; + self.advance_ip_to(((ip as isize) + offset) as usize); + } else { + self.advance_ip(SIZE_U16); + } + } + OpCode::JumpIfTrue => { + let cond = self.stack.pop().unwrap(); + if self.is_truthy(&cond) { + let offset = read_i16(code, ip) as isize; + self.advance_ip_to(((ip as isize) + offset) as usize); + } else { + self.advance_ip(SIZE_U16); + } + } + OpCode::PopJumpIfFalse => { + let cond = self.stack.last().unwrap(); + if !self.is_truthy(cond) { + let offset = read_i16(code, ip) as isize; + self.advance_ip_to(((ip as isize) + offset) as usize); + } else { + self.advance_ip(SIZE_U16); + } + } + + // --- Functions --- + OpCode::Call => { + let arg_count = read_u8(code, ip) as usize; + self.call_function(arg_count)?; + // call_function updates the frame + } + OpCode::Return => { + let result = self.stack.pop().unwrap(); + let frame = self.frames.pop().unwrap(); + self.close_upvalues(frame.stack_base); + // Remove callee + args + function locals, push return value + self.stack.truncate(frame.stack_base.saturating_sub(1)); + self.stack.push(result); + if self.frames.is_empty() { + return Ok(()); + } + } + OpCode::Closure => { + let proto_idx = read_u16(code, ip) as usize; + let upvalue_count = code[ip + 3] as usize; + let proto = proto.protos.get(proto_idx).ok_or_else(|| RuntimeError::RuntimeError { + message: format!("Closure proto index {} out of bounds", proto_idx), + token: None, + })?.clone(); + + // Collect upvalue capture info + struct UpCapture { is_local: bool, index: usize } + let mut captures: Vec = Vec::new(); + let mut off = ip + 4; + for _ in 0..upvalue_count { + let is_local = code[off] != 0; + off += 1; + let index = code[off] as usize; + off += 1; + captures.push(UpCapture { is_local, index }); + } + + // Do the actual captures + let base = self.frames.last().unwrap().stack_base; + let parent_upvalues = self.frames.last().unwrap().closure.upvalues.clone(); + let mut upvalues = Vec::new(); + for cap in &captures { + if cap.is_local { + let location = base + cap.index; + upvalues.push(self.capture_upvalue(location)); + } else { + upvalues.push(Rc::clone(&parent_upvalues[cap.index])); + } + } + + 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.advance_ip_to(off); + } + + // --- Object/Array --- + OpCode::NewObject => { + self.stack.push(Value::Object(Rc::new(RefCell::new(HashMap::new())))); + self.advance_ip(SIZE_OP); + } + OpCode::NewArray => { + let count = read_u16(code, ip) as usize; + let mut elements = Vec::with_capacity(count); + for _ in 0..count { + elements.push(self.stack.pop().unwrap()); + } + elements.reverse(); + self.stack.push(Value::Array(Rc::new(RefCell::new(elements)))); + self.advance_ip(SIZE_U16); + } + + // --- For-in --- + OpCode::ForInInit => { + let iterable = self.stack.pop().unwrap(); + let items = self.for_in_items(&iterable); + self.stack.push(Value::Array(Rc::new(RefCell::new(items)))); + self.advance_ip(SIZE_OP); + } + OpCode::ForInNext => { + // Encoding: opcode(1) + items_slot(1) + idx_slot(1) + exit_offset(2) = 5 + let items_slot = code[ip + 1] as usize; + let idx_slot = code[ip + 2] as usize; + let exit_offset = ((code[ip + 3] as u16) | ((code[ip + 4] as u16) << 8)) as i16; + let exit_ip = ((ip as isize) + exit_offset as isize) as usize; + let base = self.frames.last().unwrap().stack_base; + + // Read index from local slot + let idx_val = self.stack[base + idx_slot].clone(); + let idx = self.as_usize(&idx_val, "for-in index")?; + let items_val = self.stack[base + items_slot].clone(); + + let items = match &items_val { + Value::Array(arr) => arr.borrow(), + _ => return Err(RuntimeError::RuntimeError { + message: "ForInNext: items is not an array".into(), + token: None, + }), + }; + + if idx >= items.len() { + // Done: push Nil (keeps stack balanced for Pop at exit) + self.stack.push(Value::Nil); + self.advance_ip_to(exit_ip); + } else { + // Push current element for loop body + self.stack.push(items[idx].clone()); + // Increment index in local slot + self.stack[base + idx_slot] = Value::Number((idx + 1) as f64); + self.advance_ip(5); // SIZE_FORIN_NEXT = 5 + } + } + + // --- Compound assignment --- + OpCode::CompoundAssignLocal => { + let slot = read_u8(code, ip) as usize; + let op_tag = code[ip + 2]; + let compound_op = CompoundOp::from_u8(op_tag).unwrap(); + let rhs = self.stack.pop().unwrap(); + let base = self.frames.last().unwrap().stack_base; + let idx = base + slot; + if idx >= self.stack.len() { + return Err(RuntimeError::RuntimeError { + message: format!("CompoundAssignLocal to uninitialized local {}", slot), + token: None, + }); + } + let lhs = self.stack[idx].clone(); + let result = self.apply_compound_op(lhs, rhs, compound_op)?; + self.stack[idx] = result.clone(); + self.stack.push(result); + self.advance_ip(SIZE_U8 + 1); + } + OpCode::CompoundAssignProp => { + 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 obj = self.stack.pop().unwrap(); + let name = proto_string(&proto,name_idx)?; + let lhs = self.get_property(obj.clone(), &name)?; + let result = self.apply_compound_op(lhs, rhs, compound_op)?; + self.set_property(obj, &name, result.clone())?; + self.stack.push(result); + self.advance_ip(SIZE_U16 + 1); + } + OpCode::CompoundAssignIndex => { + let op_tag = code[ip + 1]; + let compound_op = CompoundOp::from_u8(op_tag).unwrap(); + let rhs = self.stack.pop().unwrap(); + let index = self.stack.pop().unwrap(); + let target = self.stack.pop().unwrap(); + let lhs = self.get_index(target.clone(), index.clone())?; + let result = self.apply_compound_op(lhs, rhs, compound_op)?; + self.set_index(target, index, result.clone())?; + self.stack.push(result); + self.advance_ip(SIZE_OP + 1); + } + } + } + } + + // ======================================================================== + // IP management + // ======================================================================== + + fn frame_mut(&mut self) -> &mut CallFrame { + self.frames.last_mut().unwrap() + } + + fn advance_ip(&mut self, size: usize) { + self.frame_mut().ip += size; + } + + fn advance_ip_to(&mut self, target: usize) { + self.frame_mut().ip = target; + } + + // ======================================================================== + // Function calls + // ======================================================================== + + fn call_function(&mut self, arg_count: usize) -> Result<(), RuntimeError> { + let callee_idx = self.stack.len() - 1 - arg_count; + let callee = self.stack[callee_idx].clone(); + + match &callee { + Value::NativeFunction(_) => { + // Get the native function + let native_fn = match self.stack[callee_idx].clone() { + Value::NativeFunction(f) => f, + _ => unreachable!(), + }; + // Pop arguments from stack + let mut args = Vec::new(); + for _ in 0..arg_count { + args.push(self.stack.pop().unwrap()); + } + args.reverse(); + self.stack.pop(); // pop the native function itself + let result = native_fn(self as &mut dyn Runtime, args)?; + 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, + })?; + + // 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 + self.frames.push(CallFrame { + closure, + ip: 0, + stack_base: base, + }); + } + _ => { + return Err(RuntimeError::RuntimeError { + message: "Attempt to call non-function".into(), + token: None, + }); + } + } + Ok(()) + } + + // ======================================================================== + // Property / Index helpers (reuse tree-walker logic) + // ======================================================================== + + fn get_property(&self, obj: Value, name: &str) -> Result { + match &obj { + Value::Object(map) => { + map.borrow().get(name).cloned().ok_or_else(|| RuntimeError::RuntimeError { + message: format!("Undefined property '{}'", name), + token: None, + }) + } + Value::Array(arr) => match name { + "length" => Ok(Value::Number(arr.borrow().len() as f64)), + "push" => { + let arr = Rc::clone(arr); + Ok(Value::NativeFunction(Rc::new(move |_runtime: &mut dyn Runtime, mut args: Vec| { + let val = args.pop().unwrap_or(Value::Nil); + arr.borrow_mut().push(val.clone()); + Ok(val) + }))) + } + "pop" => { + let arr = Rc::clone(arr); + Ok(Value::NativeFunction(Rc::new(move |_runtime: &mut dyn Runtime, _args: Vec| { + arr.borrow_mut().pop().ok_or_else(|| RuntimeError::RuntimeError { + message: "pop() on empty array".into(), + token: None, + }) + }))) + } + _ => Err(RuntimeError::RuntimeError { + message: format!("Array has no property '{}'", name), + token: None, + }), + }, + Value::String(s) => match name { + "length" => Ok(Value::Number(s.chars().count() as f64)), + _ => Err(RuntimeError::RuntimeError { + message: format!("String has no property '{}'", name), + token: None, + }), + }, + _ => Err(RuntimeError::RuntimeError { + message: "Only objects have properties".into(), + token: None, + }), + } + } + + fn set_property(&self, obj: Value, name: &str, val: Value) -> Result<(), RuntimeError> { + match obj { + Value::Object(map) => { + map.borrow_mut().insert(name.to_string(), val); + Ok(()) + } + _ => Err(RuntimeError::RuntimeError { + message: "Only objects have properties".into(), + token: None, + }), + } + } + + fn get_index(&self, target: Value, index: Value) -> Result { + if let Value::Object(_) = &target { + let key = match &index { + Value::String(s) => s.clone(), + _ => return Err(RuntimeError::RuntimeError { + message: "Object index must be a string".into(), + token: None, + }), + }; + return self.get_property(target, &key); + } + let i = self.as_usize(&index, "Index")?; + match &target { + Value::Array(vec) => { + let vec = vec.borrow(); + vec.get(i).cloned().ok_or_else(|| RuntimeError::RuntimeError { + message: format!("Index {} out of bounds (len {})", i, vec.len()), + token: None, + }) + } + Value::String(s) => { + let chars: Vec = s.chars().collect(); + chars.get(i).map(|&c| Value::String(c.to_string())).ok_or_else(|| RuntimeError::RuntimeError { + message: format!("Index {} out of bounds (len {})", i, chars.len()), + token: None, + }) + } + _ => Err(RuntimeError::RuntimeError { + message: "Index access on non-array, non-string, non-object value".into(), + token: None, + }), + } + } + + fn set_index(&self, target: Value, index: Value, val: Value) -> Result<(), RuntimeError> { + if let Value::Object(_) = &target { + let key = match &index { + Value::String(s) => s.clone(), + _ => return Err(RuntimeError::RuntimeError { + message: "Object index must be a string".into(), + token: None, + }), + }; + return self.set_property(target, &key, val); + } + let i = self.as_usize(&index, "Index")?; + match target { + Value::Array(vec) => { + let mut vec = vec.borrow_mut(); + if i >= vec.len() { + return Err(RuntimeError::RuntimeError { + message: format!("Index {} out of bounds (len {})", i, vec.len()), + token: None, + }); + } + vec[i] = val; + Ok(()) + } + _ => Err(RuntimeError::RuntimeError { + message: "Index assignment on non-array, non-object value".into(), + token: None, + }), + } + } + + // ======================================================================== + // Arithmetic helpers + // ======================================================================== + + fn binary_add(&self, l: Value, r: Value) -> Result { + if matches!(l, Value::String(_)) || matches!(r, Value::String(_)) { + Ok(Value::String(format!("{}{}", l, r))) + } else { + match (l, r) { + (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)), + _ => Err(RuntimeError::RuntimeError { + message: "Invalid '+' operands".into(), + token: None, + }), + } + } + } + + fn binary_arith(&self, l: Value, r: Value, f: fn(f64, f64) -> f64, name: &str) -> Result { + match (l, r) { + (Value::Number(a), Value::Number(b)) => Ok(Value::Number(f(a, b))), + _ => Err(RuntimeError::RuntimeError { + message: format!("Invalid '{}' operands", name), + token: None, + }), + } + } + + fn binary_div(&self, l: Value, r: Value) -> Result { + match (l, r) { + (_, Value::Number(b)) if b == 0.0 => Err(RuntimeError::RuntimeError { + message: "Division by zero".into(), + token: None, + }), + (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a / b)), + _ => Err(RuntimeError::RuntimeError { + message: "Invalid '/' operands".into(), + token: None, + }), + } + } + + fn binary_mod(&self, l: Value, r: Value) -> Result { + match (l, r) { + (_, Value::Number(b)) if b == 0.0 => Err(RuntimeError::RuntimeError { + message: "Modulo by zero".into(), + token: None, + }), + (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a % b)), + _ => Err(RuntimeError::RuntimeError { + message: "Invalid '%' operands".into(), + token: None, + }), + } + } + + fn apply_compound_op(&self, lhs: Value, rhs: Value, op: CompoundOp) -> Result { + match op { + CompoundOp::PlusEqual => self.binary_add(lhs, rhs), + CompoundOp::MinusEqual => self.binary_arith(lhs, rhs, |a, b| a - b, "-="), + CompoundOp::StarEqual => self.binary_arith(lhs, rhs, |a, b| a * b, "*="), + CompoundOp::SlashEqual => self.binary_div(lhs, rhs), + CompoundOp::PercentEqual => self.binary_mod(lhs, rhs), + } + } + + // ======================================================================== + // General helpers + // ======================================================================== + + fn is_truthy(&self, val: &Value) -> bool { + match val { + Value::Nil => false, + Value::Bool(b) => *b, + _ => true, + } + } + + fn is_equal(&self, a: &Value, b: &Value) -> bool { + match (a, b) { + (Value::Nil, Value::Nil) => true, + (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 + } + } + + fn as_number(&self, val: &Value) -> Result { + if let Value::Number(n) = val { + Ok(*n) + } else { + Err(RuntimeError::RuntimeError { message: "Expected number".into(), token: None }) + } + } + + fn as_usize(&self, val: &Value, arg_name: &str) -> Result { + if let Value::Number(n) = val { + if *n < 0.0 || n.fract() != 0.0 { + return Err(RuntimeError::RuntimeError { + message: format!("{} must be a non-negative integer, got {}", arg_name, n), + token: None, + }); + } + Ok(*n as usize) + } else { + Err(RuntimeError::RuntimeError { + message: format!("{} must be a number", arg_name), + token: None, + }) + } + } + + + // ======================================================================== + // Upvalues + // ======================================================================== + + fn capture_upvalue(&mut self, location: usize) -> Rc> { + // Check if this location is already captured + for uv in &self.open_upvalues { + if uv.borrow().location == location { + return Rc::clone(uv); + } + } + let uv = Rc::new(RefCell::new(UpvalueObj { + location, + closed: None, + })); + self.open_upvalues.push(Rc::clone(&uv)); + uv + } + + fn close_upvalues(&mut self, last_slot: usize) { + for uv in &self.open_upvalues { + let mut uv_ref = uv.borrow_mut(); + if uv_ref.location >= last_slot && uv_ref.closed.is_none() { + if uv_ref.location < self.stack.len() { + uv_ref.closed = Some(self.stack[uv_ref.location].clone()); + } + uv_ref.location = usize::MAX; + } + } + self.open_upvalues.retain(|uv| uv.borrow().location != usize::MAX); + } + + // ======================================================================== + // For-in helper + // ======================================================================== + + fn for_in_items(&self, collection: &Value) -> Vec { + 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 { + fn require(&mut self, path: &str) -> Result { + // 1. Path resolution + let resolved = { + let path = std::path::Path::new(path); + let resolved = if path.is_absolute() { + path.to_path_buf() + } else { + std::path::Path::new(&self.current_dir).join(path) + }; + std::fs::canonicalize(&resolved) + .map(|p| p.to_string_lossy().to_string()) + .map_err(|_| RuntimeError::RuntimeError { + message: format!("Module '{}' not found", path.display()), + token: None, + })? + }; + + // 2. Cache check + if let Some(cached) = self.module_cache.borrow().get(&resolved) { + return Ok(cached.clone()); + } + + // 3. Read file + let src = std::fs::read_to_string(&resolved) + .map_err(|e| RuntimeError::RuntimeError { + message: format!("Cannot read module '{}': {}", path, e), + token: None, + })?; + + // 4. Lex + let (tokens, lex_errors) = Lexer::new(&src).tokenize(); + if !lex_errors.is_empty() { + return Err(RuntimeError::RuntimeError { + message: format!("Lex error in module '{}': {}", path, lex_errors[0]), + token: None, + }); + } + + // 5. Parse + let mut parser = Parser::new(tokens); + let (stmts, parse_errors) = parser.parse(); + if !parse_errors.is_empty() { + return Err(RuntimeError::RuntimeError { + message: format!("Parse error in module '{}': {}", path, parse_errors[0]), + token: None, + }); + } + + // 6. Compile + let proto = Compiler::compile(&stmts).map_err(|e| RuntimeError::RuntimeError { + message: format!("Compile error in module '{}': {}", path, e), + token: None, + })?; + + // 7. Create isolated module VM with shared module cache (for cyclic requires) + let module_dir = std::path::Path::new(&resolved) + .parent() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|| ".".to_string()); + + let mut module_vm = Vm::new(); + module_vm.current_dir = module_dir; + module_vm.builtins = Rc::clone(&self.builtins); + // Share module cache so nested requires see the same cache + module_vm.module_cache = RefCell::new(HashMap::new()); + + // 8. Insert placeholder in module_vm cache (for cyclic requires within module) + let exports_obj = Value::Object(Rc::new(RefCell::new(HashMap::new()))); + module_vm.module_cache.borrow_mut().insert(resolved.clone(), exports_obj.clone()); + + // 9. Execute module + module_vm.run(Rc::new(proto))?; + + // 10. Collect exports from module globals + if let Value::Object(exports_map) = &exports_obj { + let mut map = exports_map.borrow_mut(); + for (name, val) in module_vm.globals.borrow().iter() { + map.insert(name.clone(), val.clone()); + } + } + + // 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 + self.module_cache.borrow_mut().insert(resolved, exports_obj.clone()); + + Ok(exports_obj) + } +} + +// ============================================================================ +// Helpers (standalone, no self borrow) +// ============================================================================ + +fn proto_string(proto: &FunctionProto, idx: usize) -> Result { + match proto.constants.get(idx) { + Some(Value::String(s)) => Ok(s.clone()), + _ => Err(RuntimeError::RuntimeError { + message: format!("Expected string constant at index {}", idx), + token: None, + }), + } +} + +// ============================================================================ +// Builtin registration (reuses tree-walker's builtins) +// ============================================================================ + +fn register_builtins(map: &Rc>>) { + 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))); +} diff --git a/aster/src/main.rs b/aster/src/main.rs index 17c5021..d992125 100644 --- a/aster/src/main.rs +++ b/aster/src/main.rs @@ -3,13 +3,21 @@ use std::fs; fn main() { let args: Vec = env::args().collect(); + let use_vm = args.iter().any(|a| a == "--vm"); + let file_args: Vec<&String> = args.iter().filter(|a| *a != "--vm").collect(); - match args.len() { + match file_args.len() { 1 => aster_core::run_repl(), 2 => { - let filename = &args[1]; + let filename = file_args[1]; match fs::read_to_string(filename) { - Ok(src) => aster_core::run_file(filename, src), + Ok(src) => { + if use_vm { + aster_core::run_file_vm(filename, src) + } else { + aster_core::run_file(filename, src) + } + } Err(e) => { eprintln!("Error reading file '{}': {}", filename, e); std::process::exit(1); @@ -17,7 +25,7 @@ fn main() { } } _ => { - eprintln!("Usage: aster [script]"); + eprintln!("Usage: aster [--vm] [script]"); std::process::exit(1); } }