diff --git a/aster-core/src/vm/compiler.rs b/aster-core/src/vm/compiler.rs index 9f14ac3..9d9b75d 100644 --- a/aster-core/src/vm/compiler.rs +++ b/aster-core/src/vm/compiler.rs @@ -23,7 +23,11 @@ pub struct FunctionProto { 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) } @@ -34,7 +38,9 @@ impl FunctionProto { arity: 0, code: Vec::new(), constants: Vec::new(), + protos: Vec::new(), upvalue_count: 0, + upvalues: Vec::new(), lines: Vec::new(), } } @@ -66,6 +72,7 @@ fn values_eq(a: &Value, b: &Value) -> bool { // Compiler // ============================================================================ +#[derive(Clone)] struct Local { name: String, depth: u8, // scope depth where declared; 0 = uninitialized @@ -73,6 +80,7 @@ struct Local { is_const: bool, } +#[derive(Clone)] struct Upvalue { index: u8, is_local: bool, // true = captured from enclosing fn's local; false = from upvalue @@ -89,10 +97,12 @@ pub struct Compiler { function: FunctionProto, locals: Vec, upvalues: Vec, + /// Snapshot of enclosing compiler's locals (for upvalue resolution) + enclosing_locals: Option>, + /// Snapshot of enclosing compiler's upvalues (for transitive upvalues) + enclosing_upvalues: Option>, scope_depth: u8, loop_stack: Vec, - /// Index into an outer compiler array for upvalue resolution - enclosing_idx: Option, } impl Compiler { @@ -101,9 +111,10 @@ impl Compiler { function: FunctionProto::new(name), locals: Vec::new(), upvalues: Vec::new(), + enclosing_locals: None, + enclosing_upvalues: None, scope_depth: 0, loop_stack: Vec::new(), - enclosing_idx: None, } } @@ -159,6 +170,8 @@ impl Compiler { } 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 { @@ -181,11 +194,11 @@ impl Compiler { fn compile_let(&mut self, name: &str, initializer: &Expr, mutable: bool) -> Result<(), RuntimeError> { self.compile_expr(initializer)?; if self.scope_depth == 0 { - // Global variable 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 { - // Local variable let slot = self.locals.len() as u8; self.locals.push(Local { name: name.to_string(), @@ -193,6 +206,7 @@ impl Compiler { 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(()) @@ -304,6 +318,8 @@ impl Compiler { is_const: false, }); emit_u8(&mut self.function.code, OpCode::StoreLocal, loop_var_slot); + // Pop the ForInNext value from stack top (it's stored in the local) + self.emit_op(OpCode::Pop); let start_ip = self.function.code.len(); @@ -324,7 +340,7 @@ impl Compiler { } // Jump back to ForInNext - self.emit_loop_jump(start_ip - 3); // jump back to the ForInNext instruction + self.emit_loop_jump(exit_jump); let exit_ip = self.function.code.len(); self.patch_jump_to(exit_jump, exit_ip); @@ -344,20 +360,9 @@ impl Compiler { 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 const_idx = self.function.add_constant(Value::Function(Rc::new( - crate::interpreter::Function { - params: params.to_vec(), - body: body.to_vec(), - env: Rc::new(RefCell::new(crate::interpreter::Env::new(None))), - name: Some(name.to_string()), - } - ))); - // For now, we also need to store the proto for the VM to use. - // We'll store it as a special constant. + let upvalues = proto.upvalues.clone(); let proto_idx = self.add_function_proto_constant(proto); - - // Emit Closure opcode with upvalues - self.emit_closure(proto_idx); + self.emit_closure(proto_idx, &upvalues); // Bind to name if self.scope_depth == 0 { @@ -447,17 +452,8 @@ impl Compiler { } // Try to resolve as upvalue if let Some(upvalue_idx) = self.resolve_upvalue(name) { - // Upvalue access: LoadUpvalue is LoadLocal with slot = upvalue-local-marker - // For simplicity, we use a convention: upvalues are locals with special marking. - // Actually, we need a dedicated LoadUpvalue opcode. Let's add it... - // For now, store upvalues at "local slots" offset by 256. - emit_u16(&mut self.function.code, OpCode::LoadConst, 0xFFFF); // placeholder - // We'll handle upvalues properly when we have LoadUpvalue - // TODO: Add LoadUpvalue opcode - return Err(RuntimeError::RuntimeError { - message: format!("Upvalue '{}' not yet supported", name), - token: None, - }); + emit_u8(&mut self.function.code, OpCode::LoadUpvalue, upvalue_idx); + return Ok(()); } // Fall back to global let name_idx = self.add_string_constant(name); @@ -471,6 +467,8 @@ impl Compiler { 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); @@ -489,6 +487,14 @@ impl Compiler { 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); @@ -643,27 +649,27 @@ impl Compiler { 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); + 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); - child.enclosing_idx = Some(0); // placeholder — we handle upvalues differently + // Pass enclosing state for upvalue resolution + child.enclosing_locals = Some(self.locals.clone()); + child.enclosing_upvalues = Some(self.upvalues.clone()); - // Add params as locals + // Add params as locals without StoreLocal — they're already on stack from Call for param in params { - let slot = child.locals.len() as u8; child.locals.push(Local { name: param.clone(), depth: 1, // params are at scope depth 1 (function body) is_captured: false, is_const: false, }); - // Params are already on stack from Call; StoreLocal just peeks - emit_u8(&mut child.function.code, OpCode::StoreLocal, slot); } child.function.arity = params.len() as u8; @@ -676,11 +682,17 @@ impl Compiler { child.emit_op(OpCode::LoadNil); child.emit_op(OpCode::Return); - // Resolve upvalues: for each variable reference in the child that wasn't - // resolved locally, check if it exists in the parent's locals/upvalues. - // (We handle this lazily in compile_variable for now — if not local, try upvalue.) + // Mark captured locals in the parent compiler + for uv in &child.upvalues { + if uv.is_local { + if let Some(local) = self.locals.get_mut(uv.index as usize) { + local.is_captured = true; + } + } + } child.function.upvalue_count = child.upvalues.len() as u8; + child.function.upvalues = child.upvalues.iter().map(|uv| (uv.is_local, uv.index)).collect(); Ok(child.function) } @@ -694,16 +706,18 @@ impl Compiler { fn end_scope(&mut self) { self.scope_depth -= 1; - // Pop locals that are going out of scope + // 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 { - // Close upvalue instead of pop - // (For now, just pop — upvalue closing handled in VM) + // 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; } - self.locals.pop(); - pop_count += 1; } else { break; } @@ -728,9 +742,24 @@ impl Compiler { } /// 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 { - // For now, we don't have enclosing compiler access. - // Upvalues will be fully implemented in a follow-up. + 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 { + // Check if we already captured this upvalue + for (j, uv) in self.upvalues.iter().enumerate() { + if uv.is_local && uv.index == i as u8 { + return Some(j as u8); + } + } + let idx = self.upvalues.len() as u8; + self.upvalues.push(Upvalue { index: i as u8, is_local: true }); + return Some(idx); + } + } + } + // Transitive upvalues (from enclosing's upvalues) not yet implemented None } @@ -759,13 +788,17 @@ impl Compiler { emit_i16(&mut self.function.code, OpCode::Jump, offset as i16); } - fn emit_closure(&mut self, proto_idx: u16) { + 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); - // No upvalues yet - code.push(0u8); // upvalue count = 0 for now + 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) { @@ -791,20 +824,10 @@ impl Compiler { } fn add_function_proto_constant(&mut self, proto: FunctionProto) -> u16 { - // Store compiled proto as a special marker. - // We use Value::Nil as placeholder since FunctionProto isn't a Value. - // The VM will look up the proto from a separate table. - // For now, store the proto's index in a side table. - // Actually, let's store it as a string tag that the VM can recognize. - // We'll use a dedicated proto storage: just append to a Vec. - // But FunctionProto isn't a Value... let's store it inline. - // HACK: store proto in constants with a special Object wrapping - let idx = self.function.constants.len() as u16; - // Use a Value::Object with a special marker - // The VM will need to handle this - let mut map = std::collections::HashMap::new(); - map.insert("__proto__".to_string(), Value::String(format!("proto_{}", idx))); - self.function.constants.push(Value::Object(Rc::new(RefCell::new(map)))); + 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 } } diff --git a/aster-core/src/vm/opcode.rs b/aster-core/src/vm/opcode.rs index 4d7d988..667cb04 100644 --- a/aster-core/src/vm/opcode.rs +++ b/aster-core/src/vm/opcode.rs @@ -20,6 +20,8 @@ pub enum OpCode { // --- 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, @@ -80,16 +82,16 @@ pub enum OpCode { impl OpCode { pub fn from_u8(byte: u8) -> Option { - if byte <= 41 { - // SAFETY: OpCode is #[repr(u8)] with contiguous values 0..=41 - Some(unsafe { std::mem::transmute::(byte) }) - } else { - None + 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 = 42; + pub const COUNT: usize = 44; } /// Compound assignment operator tags (used as operand byte after diff --git a/aster-core/src/vm/vm.rs b/aster-core/src/vm/vm.rs index 34ac06f..7fc7f26 100644 --- a/aster-core/src/vm/vm.rs +++ b/aster-core/src/vm/vm.rs @@ -63,8 +63,8 @@ pub struct Vm { /// Open upvalues (tracked so closures share the same upvalue object) pub open_upvalues: Vec>>, - /// Allocated function protos (indexed by the compiler's constant pool index) - protos: Vec>, + /// VM-compiled closures: maps Function Rc pointer → Closure data + closures: RefCell>>, } impl Vm { @@ -85,7 +85,7 @@ impl Vm { .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|_| ".".to_string()), open_upvalues: Vec::new(), - protos: Vec::new(), + closures: RefCell::new(HashMap::new()), } } @@ -133,12 +133,13 @@ impl Vm { if ip >= code_len { // End of function — implicit return nil - self.frames.pop(); + let frame = self.frames.pop().unwrap(); + self.close_upvalues(frame.stack_base); if self.frames.is_empty() { return Ok(()); } - let base = self.frames.last().unwrap().stack_base; - self.stack.truncate(base); + // Remove callee + args + function locals, push nil result + self.stack.truncate(frame.stack_base.saturating_sub(1)); self.stack.push(Value::Nil); continue; } @@ -193,15 +194,51 @@ impl Vm { // --- Locals --- OpCode::LoadLocal => { let slot = read_u8(code, ip) as usize; - let val = self.stack[self.frame().stack_base + slot].clone(); + 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(); + let val = self.stack.last().unwrap().clone(); // peek let base = self.frames.last().unwrap().stack_base; - self.stack[base + slot] = val; + 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); } @@ -402,31 +439,34 @@ impl Vm { let result = self.stack.pop().unwrap(); let frame = self.frames.pop().unwrap(); self.close_upvalues(frame.stack_base); - self.stack.truncate(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(()); } - // IP of caller is unchanged (already at next instruction) } OpCode::Closure => { let proto_idx = read_u16(code, ip) as usize; let upvalue_count = code[ip + 3] as usize; - let proto = Rc::clone(&self.protos[proto_idx]); + 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 from bytecode first + // Collect upvalue capture info struct UpCapture { is_local: bool, index: usize } let mut captures: Vec = Vec::new(); - let mut offset = ip + 4; + let mut off = ip + 4; for _ in 0..upvalue_count { - let is_local = code[offset] != 0; - offset += 1; - let index = code[offset] as usize; - offset += 1; + let is_local = code[off] != 0; + off += 1; + let index = code[off] as usize; + off += 1; captures.push(UpCapture { is_local, index }); } - // Now do the actual captures (no conflicting borrows) + // 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(); @@ -439,17 +479,17 @@ impl Vm { } } - // Store closure (stub for now) - let _closure = Rc::new(Closure { proto, upvalues }); - self.stack.push(Value::Function(Rc::new( - crate::interpreter::Function { - params: Vec::new(), - body: Vec::new(), - env: Rc::new(RefCell::new(crate::interpreter::Env::new(None))), - name: None, - } - ))); - self.advance_ip_to(offset); + 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 --- @@ -507,9 +547,16 @@ impl Vm { let compound_op = CompoundOp::from_u8(op_tag).unwrap(); let rhs = self.stack.pop().unwrap(); let base = self.frames.last().unwrap().stack_base; - let lhs = self.stack[base + slot].clone(); + 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[base + slot] = result.clone(); + self.stack[idx] = result.clone(); self.stack.push(result); self.advance_ip(SIZE_U8 + 1); } @@ -570,8 +617,13 @@ impl Vm { let callee_idx = self.stack.len() - 1 - arg_count; let callee = self.stack[callee_idx].clone(); - match callee { - Value::NativeFunction(native_fn) => { + 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 { @@ -583,27 +635,27 @@ impl Vm { self.stack.push(result); self.advance_ip(SIZE_U8); } - Value::Function(f) => { - // User-defined function: create new call frame - let base = callee_idx; // where the new frame's locals start - let new_closure = Rc::new(Closure { - proto: Rc::new(FunctionProto::new(f.name.clone())), - upvalues: Vec::new(), - }); + 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: new_closure, + closure, ip: 0, stack_base: base, }); - // Arguments are already on the stack at the right position - // (they become locals 0..arg_count in the new frame) - // The callee itself becomes slot 0 (TODO: handle properly) - // For now, this is a stub — full function support needs - // proper closure/proto integration - return Err(RuntimeError::RuntimeError { - message: "VM user-defined function calls not yet fully implemented".into(), - token: None, - }); } _ => { return Err(RuntimeError::RuntimeError {