feat: VM函数调用、闭包、upvalue捕获支持
- 实现用户定义函数调用 (CallFrame创建、参数传递、返回) - 实现闭包/upvalue捕获 (编译器解析、VM捕获、LoadUpvalue/StoreUpvalue) - 修复栈管理: StoreLocal peek语义、局部变量槽位分配 - 修复跳转偏移计算 (移除错误的-3偏置) - 修复对象字面量编译 (每字段后Pop) - 修复函数声明栈泄漏 (DefineGlobal后Pop) - 添加ForInNext值Pop (避免栈累积) - 添加OpCode::from_u8对非连续值的支持 通过: 算术、循环、条件、数组、对象、字符串、递归、闭包、upvalue 部分通过: for-in迭代器 (基本工作,复杂嵌套场景待修复) 未实现: VM版require()、复合upvalue赋值 测试: 327/327 通过
This commit is contained in:
@@ -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;
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user