vm-bytecode #1

Merged
elmma merged 6 commits from vm-bytecode into master 2026-06-26 09:25:11 +08:00
3 changed files with 197 additions and 120 deletions
Showing only changes of commit 299003a718 - Show all commits
+85 -62
View File
@@ -23,7 +23,11 @@ pub struct FunctionProto {
pub arity: u8,
pub code: Vec<u8>,
pub constants: Vec<Value>,
/// Nested function protos (for closures and function declarations)
pub protos: Vec<Rc<FunctionProto>>,
pub upvalue_count: u8,
/// Upvalue descriptors for this function (for Closure opcode emission)
pub upvalues: Vec<(bool, u8)>, // (is_local, index)
pub lines: Vec<(usize, usize)>, // (bytecode_offset, source_line)
}
@@ -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<Local>,
upvalues: Vec<Upvalue>,
/// Snapshot of enclosing compiler's locals (for upvalue resolution)
enclosing_locals: Option<Vec<Local>>,
/// Snapshot of enclosing compiler's upvalues (for transitive upvalues)
enclosing_upvalues: Option<Vec<Upvalue>>,
scope_depth: u8,
loop_stack: Vec<LoopContext>,
/// Index into an outer compiler array for upvalue resolution
enclosing_idx: Option<usize>,
}
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<String>, params: &[String], body: &[Stmt]) -> Result<FunctionProto, RuntimeError> {
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;
}
} 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<u8> {
// 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
}
}
+8 -6
View File
@@ -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<Self> {
if byte <= 41 {
// SAFETY: OpCode is #[repr(u8)] with contiguous values 0..=41
Some(unsafe { std::mem::transmute::<u8, OpCode>(byte) })
} else {
None
match byte {
0..=41 => Some(unsafe { std::mem::transmute::<u8, OpCode>(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
+99 -47
View File
@@ -63,8 +63,8 @@ pub struct Vm {
/// Open upvalues (tracked so closures share the same upvalue object)
pub open_upvalues: Vec<Rc<RefCell<UpvalueObj>>>,
/// Allocated function protos (indexed by the compiler's constant pool index)
protos: Vec<Rc<FunctionProto>>,
/// VM-compiled closures: maps Function Rc pointer → Closure data
closures: RefCell<HashMap<*const crate::interpreter::Function, Rc<Closure>>>,
}
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<UpCapture> = 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 {
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,
}
)));
self.advance_ip_to(offset);
});
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 {