From b915f127caf0ca3c82ff9b969a675343d4bbea9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=89=B9=E6=AF=8F=E7=BE=8A=E9=98=9F?= Date: Tue, 30 Jun 2026 19:58:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=90=AD=E5=BB=BA=E9=97=AD=E5=8C=85?= =?UTF-8?q?=E6=8D=95=E8=8E=B7=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_bytecode/src/module.rs | 15 ++ crates/oxide_bytecode/src/opcode.rs | 5 + crates/oxide_compiler/src/compiler.rs | 224 ++++++++++++++++++ .../src/counter/expr/assignment.rs | 10 +- .../src/counter/expr/literal.rs | 22 +- crates/oxide_compiler/src/emit_ctx.rs | 1 + .../src/emitter/expr/assignment.rs | 14 ++ .../src/emitter/expr/literal.rs | 29 ++- .../src/emitter/stmt/declaration.rs | 9 +- crates/oxide_compiler/src/symbol_table.rs | 25 +- crates/oxide_types/src/object.rs | 84 +++++++ crates/oxide_types/tests/object_tests.rs | 39 +++ crates/oxide_vm/src/dispatch/call.rs | 115 ++++++++- crates/oxide_vm/src/session_gc.rs | 20 ++ crates/oxide_vm/src/vm.rs | 16 +- crates/oxide_vm/src/vm_support.rs | 2 + crates/oxide_vm/tests/scoping_tests.rs | 42 ++++ 17 files changed, 660 insertions(+), 12 deletions(-) create mode 100644 crates/oxide_types/tests/object_tests.rs diff --git a/crates/oxide_bytecode/src/module.rs b/crates/oxide_bytecode/src/module.rs index eb44518..2eaaf07 100644 --- a/crates/oxide_bytecode/src/module.rs +++ b/crates/oxide_bytecode/src/module.rs @@ -12,6 +12,13 @@ pub enum Constant { Undefined, } +#[derive(Debug, Clone)] +pub struct UpvalueCapture { + pub name: String, + pub enclosing_reg: u8, + pub cell_idx: u8, +} + pub struct CompiledModule { pub bytecode: Vec, pub constants: Vec, @@ -37,6 +44,8 @@ pub struct CompiledModule { pub is_derived_constructor: bool, /// True for prototype methods whose function object needs a runtime home_object. pub needs_home_object: bool, + pub upvalue_captures: Vec, + pub cells_needed: u8, } impl CompiledModule { @@ -55,6 +64,8 @@ impl CompiledModule { is_class_constructor: false, is_derived_constructor: false, needs_home_object: false, + upvalue_captures: Vec::new(), + cells_needed: 0, } } } @@ -81,6 +92,8 @@ impl Clone for CompiledModule { is_class_constructor: self.is_class_constructor, is_derived_constructor: self.is_derived_constructor, needs_home_object: self.needs_home_object, + upvalue_captures: self.upvalue_captures.clone(), + cells_needed: self.cells_needed, } } } @@ -93,6 +106,8 @@ impl fmt::Display for CompiledModule { writeln!(f, "; [{i}] = {c:?}")?; } writeln!(f)?; + writeln!(f, "; upvalue_captures: {:?}", self.upvalue_captures)?; + writeln!(f)?; for (offset, &instr) in self.bytecode.iter().enumerate() { let op = opcode::opcode(instr); let rd = opcode::rd(instr); diff --git a/crates/oxide_bytecode/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs index 8fbbbbf..73b58b2 100644 --- a/crates/oxide_bytecode/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -109,7 +109,10 @@ define_opcodes! { DELETE_PROP_STATIC = 0x39 => "DELETE_PROP_STATIC", DELETE_PROP_DYNAMIC = 0x3A => "DELETE_PROP_DYNAMIC", INSTANCEOF = 0x3B => "INSTANCEOF", + MAKE_CELL = 0x3C => "MAKE_CELL", + CELL_GET = 0x3D => "CELL_GET", REST_OBJECT = 0x3E => "REST_OBJECT", + CELL_SET = 0x3F => "CELL_SET", // -- Variable (0x30-0x32) -- LOAD_VAR = 0x30 => "LOAD_VAR", @@ -126,8 +129,10 @@ define_opcodes! { SUPER_STATIC_GET_PROP = 0x46 => "SUPER_STATIC_GET_PROP", SET_HOME_OBJECT = 0x47 => "SET_HOME_OBJECT", DEFINE_ACCESSOR = 0x48 => "DEFINE_ACCESSOR", + LOAD_UPVALUE = 0x49 => "LOAD_UPVALUE", CREATE_CLOSURE = 0x4A => "CREATE_CLOSURE", CREATE_REGEXP = 0x4B => "CREATE_REGEXP", + STORE_UPVALUE = 0x4C => "STORE_UPVALUE", // ── Object Property (0x50-0x5F) ── IC_GET_PROP = 0x50 => "IC_GET_PROP", diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index e999f65..7b34827 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use crate::emit_ctx::{LabelCtx, ScopeCtx}; use crate::symbol_table::{Binding, SymbolTable}; use oxide_bytecode::module::CompiledModule; +use oxide_bytecode::module::UpvalueCapture; use oxide_bytecode::opcode::{self, OpCode}; pub use crate::hash::{compiled_module_hash, structural_hash}; @@ -164,6 +165,7 @@ pub(crate) struct CompileCtx { /// constructor's instance-field code, added at the super() call site during body /// counting so projected_pc matches where the emit pass splices the field bytecode. pub(crate) after_super_count_words: Option, + pub(crate) current_upvalue_captures: Vec, /// Set when alloc_reg() overflows into the reserved this/new.target range (≥254). /// Checked after each emit phase to produce a compile error rather than silent corruption. pub(crate) reg_overflow: bool, @@ -227,6 +229,7 @@ impl CompileCtx { builtin_reg_map: Vec::new(), private_name_map: Vec::new(), next_private_name_id: 1, + cell_registry: Vec::new(), }, projected_pc: 0, sub_modules: Vec::new(), @@ -238,6 +241,7 @@ impl CompileCtx { after_super_insert: None, after_super_inserted: false, after_super_count_words: None, + current_upvalue_captures: Vec::new(), reg_overflow: false, const_overflow: false, jump_overflow: false, @@ -587,6 +591,203 @@ impl Compiler { Self } + #[allow(dead_code)] + pub(crate) fn analyze_upvalue_captures( + &self, body_stmts: &[Statement], parent_ctx: &CompileCtx, nested_symbols: &SymbolTable, + ) -> (Vec, u8) { + let mut captures: Vec = Vec::new(); + let mut seen: HashMap = HashMap::new(); + + for stmt in body_stmts { + self.collect_upvalue_stmt(stmt, parent_ctx, nested_symbols, &mut captures, &mut seen); + } + + let count = captures.len() as u8; + (captures, count) + } + + #[allow(dead_code)] + fn collect_upvalue_stmt( + &self, stmt: &Statement, parent_ctx: &CompileCtx, nested_symbols: &SymbolTable, + captures: &mut Vec, seen: &mut HashMap, + ) { + match stmt { + Statement::ExpressionStatement(es) => { + self.collect_upvalue_expr(&es.expression, parent_ctx, nested_symbols, captures, seen); + } + Statement::VariableDeclaration(vd) => { + for decl in &vd.declarations { + if let Some(init) = &decl.init { + self.collect_upvalue_expr(init, parent_ctx, nested_symbols, captures, seen); + } + } + } + Statement::ReturnStatement(rs) => { + if let Some(expr) = &rs.argument { + self.collect_upvalue_expr(expr, parent_ctx, nested_symbols, captures, seen); + } + } + Statement::ForStatement(fs) => { + if let Some(init) = &fs.init { + if let Some(e) = init.as_expression() { + self.collect_upvalue_expr(e, parent_ctx, nested_symbols, captures, seen); + } + } + if let Some(test) = &fs.test { + self.collect_upvalue_expr(test, parent_ctx, nested_symbols, captures, seen); + } + if let Some(update) = &fs.update { + self.collect_upvalue_expr(update, parent_ctx, nested_symbols, captures, seen); + } + self.collect_upvalue_stmt(&fs.body, parent_ctx, nested_symbols, captures, seen); + } + Statement::IfStatement(is) => { + self.collect_upvalue_expr(&is.test, parent_ctx, nested_symbols, captures, seen); + self.collect_upvalue_stmt(&is.consequent, parent_ctx, nested_symbols, captures, seen); + if let Some(alt) = &is.alternate { + self.collect_upvalue_stmt(alt, parent_ctx, nested_symbols, captures, seen); + } + } + Statement::WhileStatement(ws) => { + self.collect_upvalue_expr(&ws.test, parent_ctx, nested_symbols, captures, seen); + self.collect_upvalue_stmt(&ws.body, parent_ctx, nested_symbols, captures, seen); + } + Statement::DoWhileStatement(dw) => { + self.collect_upvalue_stmt(&dw.body, parent_ctx, nested_symbols, captures, seen); + self.collect_upvalue_expr(&dw.test, parent_ctx, nested_symbols, captures, seen); + } + Statement::BlockStatement(bs) => { + for s in &bs.body { + self.collect_upvalue_stmt(s, parent_ctx, nested_symbols, captures, seen); + } + } + Statement::TryStatement(ts) => { + for s in &ts.block.body { + self.collect_upvalue_stmt(s, parent_ctx, nested_symbols, captures, seen); + } + if let Some(handler) = &ts.handler { + for s in &handler.body.body { + self.collect_upvalue_stmt(s, parent_ctx, nested_symbols, captures, seen); + } + } + if let Some(finalizer) = &ts.finalizer { + for s in &finalizer.body { + self.collect_upvalue_stmt(s, parent_ctx, nested_symbols, captures, seen); + } + } + } + Statement::ThrowStatement(ts) => { + self.collect_upvalue_expr(&ts.argument, parent_ctx, nested_symbols, captures, seen); + } + Statement::SwitchStatement(ss) => { + self.collect_upvalue_expr(&ss.discriminant, parent_ctx, nested_symbols, captures, seen); + for case in &ss.cases { + for s in &case.consequent { + self.collect_upvalue_stmt(s, parent_ctx, nested_symbols, captures, seen); + } + } + } + _ => {} + } + } + + #[allow(dead_code)] + fn collect_upvalue_expr( + &self, expr: &Expression, parent_ctx: &CompileCtx, nested_symbols: &SymbolTable, + captures: &mut Vec, seen: &mut HashMap, + ) { + match expr { + Expression::Identifier(ident) => { + let name = ident.name.as_str(); + if nested_symbols.lookup_any_binding(name).is_some() { + return; + } + if let Some((binding, _)) = parent_ctx.scopes.symbols.lookup_any_binding(name) { + if seen.contains_key(name) { + return; + } + let cell_idx = captures.len() as u8; + seen.insert(name.to_string(), cell_idx as usize); + captures.push(UpvalueCapture { + name: name.to_string(), + enclosing_reg: binding.reg, + cell_idx, + }); + } + } + Expression::AssignmentExpression(ae) => { + if let oxide_parser::AssignmentTarget::AssignmentTargetIdentifier(ati) = &ae.left { + let name = ati.name.as_str(); + if nested_symbols.lookup_any_binding(name).is_none() { + if let Some((binding, _)) = parent_ctx.scopes.symbols.lookup_any_binding(name) { + if !seen.contains_key(name) { + let cell_idx = captures.len() as u8; + seen.insert(name.to_string(), cell_idx as usize); + captures.push(UpvalueCapture { + name: name.to_string(), + enclosing_reg: binding.reg, + cell_idx, + }); + } + } + } + } + self.collect_upvalue_expr(&ae.right, parent_ctx, nested_symbols, captures, seen); + } + Expression::BinaryExpression(be) => { + self.collect_upvalue_expr(&be.left, parent_ctx, nested_symbols, captures, seen); + self.collect_upvalue_expr(&be.right, parent_ctx, nested_symbols, captures, seen); + } + Expression::UnaryExpression(ue) => { + self.collect_upvalue_expr(&ue.argument, parent_ctx, nested_symbols, captures, seen); + } + Expression::UpdateExpression(ue) => { + if let oxide_parser::SimpleAssignmentTarget::AssignmentTargetIdentifier(ati) = &ue.argument { + let name = ati.name.as_str(); + if nested_symbols.lookup_any_binding(name).is_none() { + if let Some((binding, _)) = parent_ctx.scopes.symbols.lookup_any_binding(name) { + if !seen.contains_key(name) { + let cell_idx = captures.len() as u8; + seen.insert(name.to_string(), cell_idx as usize); + captures.push(UpvalueCapture { + name: name.to_string(), + enclosing_reg: binding.reg, + cell_idx, + }); + } + } + } + } + } + Expression::CallExpression(ce) => { + self.collect_upvalue_expr(&ce.callee, parent_ctx, nested_symbols, captures, seen); + for arg in &ce.arguments { + if let Some(e) = arg.as_expression() { + self.collect_upvalue_expr(e, parent_ctx, nested_symbols, captures, seen); + } + } + } + Expression::SequenceExpression(se) => { + for sub_expr in &se.expressions { + self.collect_upvalue_expr(sub_expr, parent_ctx, nested_symbols, captures, seen); + } + } + Expression::ConditionalExpression(ce) => { + self.collect_upvalue_expr(&ce.test, parent_ctx, nested_symbols, captures, seen); + self.collect_upvalue_expr(&ce.consequent, parent_ctx, nested_symbols, captures, seen); + self.collect_upvalue_expr(&ce.alternate, parent_ctx, nested_symbols, captures, seen); + } + Expression::ArrayExpression(ae) => { + for elem in &ae.elements { + if let Some(e) = elem.as_expression() { + self.collect_upvalue_expr(e, parent_ctx, nested_symbols, captures, seen); + } + } + } + _ => {} + } + } + pub(crate) fn extract_function_parts<'a>( &self, function: &'a oxide_parser::Function<'a>, ) -> Result<(Vec>, &'a [Statement<'a>]), String> { @@ -693,6 +894,7 @@ impl Compiler { reg: binding.reg, initialized: binding.initialized, is_const: binding.is_const, + is_captured: binding.is_captured, }, ); inherited_reg_start = inherited_reg_start.max(binding.reg.saturating_add(1)); @@ -704,6 +906,7 @@ impl Compiler { reg: *reg, initialized: true, is_const: true, + is_captured: false, }, ); inherited_reg_start = inherited_reg_start.max(reg.saturating_add(1)); @@ -714,6 +917,23 @@ impl Compiler { // same register offset (params go after builtin slots). ctx.reset_regs(); + // Free variable analysis for upvalue capture (Ordinary + Arrow functions only) + // ponytail: temporarily disabled to isolate closure bug + /* + if matches!(body_context, FunctionBodyContext::Ordinary | FunctionBodyContext::Arrow) { + let (captures, _cells) = self.analyze_upvalue_captures(body_stmts, parent_ctx, &ctx.scopes.symbols); + ctx.current_upvalue_captures = captures; + ctx.scopes.cell_registry = ctx.current_upvalue_captures.iter() + .map(|u| (u.name.clone(), u.cell_idx)) + .collect(); + for up in &ctx.current_upvalue_captures { + if let Some((binding, _)) = parent_ctx.scopes.symbols.lookup_any_binding(&up.name) { + binding.is_captured.set(true); + } + } + } + */ + // Function body scope - params and local vars ctx.push_scope_with_kind(ScopeKind::FunctionScope); @@ -858,6 +1078,8 @@ impl Compiler { is_class_constructor: false, is_derived_constructor: false, needs_home_object: false, + upvalue_captures: Vec::new(), + cells_needed: 0, }) } @@ -956,6 +1178,8 @@ impl Compiler { is_class_constructor: false, is_derived_constructor: false, needs_home_object: false, + upvalue_captures: Vec::new(), + cells_needed: 0, }) } } diff --git a/crates/oxide_compiler/src/counter/expr/assignment.rs b/crates/oxide_compiler/src/counter/expr/assignment.rs index 2ee9db1..5f75613 100644 --- a/crates/oxide_compiler/src/counter/expr/assignment.rs +++ b/crates/oxide_compiler/src/counter/expr/assignment.rs @@ -32,13 +32,21 @@ impl Compiler { self.count_expression(&assign.right, ctx); ctx.count_load_const(); // private id ctx.count_instr(); // SET_PRIVATE - } else if let oxide_parser::AssignmentTarget::AssignmentTargetIdentifier(_) = &assign.left { + } else if let oxide_parser::AssignmentTarget::AssignmentTargetIdentifier(ati) = &assign.left { + let name = ati.name.as_str(); + // Check if target is an upvalue or captured cell + let is_upvalue = ctx.current_upvalue_captures.iter().any(|u| u.name == name); + let is_captured = ctx.scopes.symbols.lookup_is_captured(name); if let Some(logical_op) = assign.operator.to_logical_operator() { self.count_identifier_logical_assignment(logical_op, &assign.right, ctx); } else { self.count_expression(&assign.right, ctx); if assign.operator != AssignmentOperator::Assign { ctx.projected_pc += 1; // COMPOUND_* on var + } else if is_upvalue { + ctx.count_instr(); // STORE_UPVALUE + } else if is_captured { + ctx.count_instr(); // CELL_SET } else { ctx.alloc_reg(); ctx.projected_pc += 1; // STORE_VAR diff --git a/crates/oxide_compiler/src/counter/expr/literal.rs b/crates/oxide_compiler/src/counter/expr/literal.rs index 1d782c0..8660209 100644 --- a/crates/oxide_compiler/src/counter/expr/literal.rs +++ b/crates/oxide_compiler/src/counter/expr/literal.rs @@ -21,8 +21,26 @@ impl Compiler { let Expression::Identifier(ident) = expr else { return; }; - if CompileCtx::is_known_builtin(ident.name.as_str()) { - let _ = ctx.lookup_or_builtin(ident.name.as_str()); + let name = ident.name.as_str(); + + // Check if this is an upvalue reference (nested closure) + for up in &ctx.current_upvalue_captures { + if up.name == name { + ctx.alloc_reg(); + ctx.count_instr(); // LOAD_UPVALUE + return; + } + } + + // Check if the owner's binding is captured (cell access) + if ctx.scopes.symbols.lookup_is_captured(name) { + ctx.alloc_reg(); + ctx.count_instr(); // CELL_GET + return; + } + + if CompileCtx::is_known_builtin(name) { + let _ = ctx.lookup_or_builtin(name); } ctx.alloc_reg(); ctx.projected_pc += 1; // LOAD_VAR diff --git a/crates/oxide_compiler/src/emit_ctx.rs b/crates/oxide_compiler/src/emit_ctx.rs index f2f41cc..6d4c065 100644 --- a/crates/oxide_compiler/src/emit_ctx.rs +++ b/crates/oxide_compiler/src/emit_ctx.rs @@ -30,6 +30,7 @@ pub(crate) struct ScopeCtx { pub(crate) builtin_reg_map: Vec<(String, u8)>, pub(crate) private_name_map: Vec<(String, u32)>, pub(crate) next_private_name_id: u32, + pub(crate) cell_registry: Vec<(String, u8)>, } /// Reserved for destructuring-pattern state (Phase 15). Intentionally empty diff --git a/crates/oxide_compiler/src/emitter/expr/assignment.rs b/crates/oxide_compiler/src/emitter/expr/assignment.rs index abe3f13..367e8e7 100644 --- a/crates/oxide_compiler/src/emitter/expr/assignment.rs +++ b/crates/oxide_compiler/src/emitter/expr/assignment.rs @@ -142,6 +142,20 @@ impl Compiler { } else { let val_reg = self.emit_expression(&assign.right, ctx)?; let name = id_ref.name.as_str(); + // Check if target is an upvalue reference + if let Some(uv_idx) = ctx.current_upvalue_captures.iter().position(|u| u.name == name) { + ctx.emit(opcode::encode(OpCode::STORE_UPVALUE, 0, val_reg, uv_idx as u8)); + return Ok(val_reg); + } + // Check if target is a captured cell + if ctx.scopes.symbols.lookup_is_captured(name) { + let cell_idx = ctx.scopes.cell_registry.iter() + .find(|(n, _)| n == name) + .map(|(_, idx)| *idx) + .unwrap_or(0); + ctx.emit(opcode::encode(OpCode::CELL_SET, 0, val_reg, cell_idx)); + return Ok(val_reg); + } let var_reg = ctx.lookup_or_global(name); let is_const = ctx.lookup_const_flag(name); let const_flag = if is_const { 1 } else { 0 }; diff --git a/crates/oxide_compiler/src/emitter/expr/literal.rs b/crates/oxide_compiler/src/emitter/expr/literal.rs index da5312e..c347c20 100644 --- a/crates/oxide_compiler/src/emitter/expr/literal.rs +++ b/crates/oxide_compiler/src/emitter/expr/literal.rs @@ -56,7 +56,34 @@ impl Compiler { pub(in crate::emitter) fn emit_identifier_expression( &self, ident: &oxide_parser::IdentifierReference, ctx: &mut CompileCtx, ) -> Result { - let var_reg = ctx.lookup_or_builtin(ident.name.as_str())?; + let name = ident.name.as_str(); + + // Check if this is an upvalue reference (nested closure) + for (uv_idx, up) in ctx.current_upvalue_captures.iter().enumerate() { + if up.name == name { + let r = ctx.alloc_reg(); + let idx = uv_idx as u8; + ctx.emit(opcode::encode(OpCode::LOAD_UPVALUE, r, idx, 0)); + return Ok(r); + } + } + + // Check if the owner's binding is captured (cell access) + if ctx.scopes.symbols.lookup_is_captured(name) { + let cell_idx = ctx.scopes.cell_registry.iter() + .find(|(n, _)| n == name) + .map(|(_, idx)| *idx) + .unwrap_or(0); + let r = ctx.alloc_reg(); + if let Some((binding, _)) = ctx.scopes.symbols.lookup_any_binding(name) { + ctx.emit(opcode::encode(OpCode::CELL_GET, r, binding.reg, cell_idx)); + } else { + ctx.emit(opcode::encode(OpCode::CELL_GET, r, 0, cell_idx)); + } + return Ok(r); + } + + let var_reg = ctx.lookup_or_builtin(name)?; let r = ctx.alloc_reg(); ctx.emit(opcode::encode(OpCode::LOAD_VAR, r, var_reg, 0)); Ok(r) diff --git a/crates/oxide_compiler/src/emitter/stmt/declaration.rs b/crates/oxide_compiler/src/emitter/stmt/declaration.rs index f391196..12b72cb 100644 --- a/crates/oxide_compiler/src/emitter/stmt/declaration.rs +++ b/crates/oxide_compiler/src/emitter/stmt/declaration.rs @@ -33,7 +33,14 @@ impl Compiler { ctx.emit_load_const(tmp, idx); let var_reg = ctx.alloc_reg(); ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?; - ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, tmp, 0)); + let is_captured = ctx.scopes.symbols.lookup_is_captured(bi.name.as_str()); + if is_captured { + let cell_idx = ctx.scopes.cell_registry.len() as u8; + ctx.scopes.cell_registry.push((bi.name.to_string(), cell_idx)); + ctx.emit(opcode::encode(OpCode::MAKE_CELL, var_reg, cell_idx, 0)); + } else { + ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, tmp, 0)); + } ctx.init_var(bi.name.as_str()); r = Some(var_reg); } diff --git a/crates/oxide_compiler/src/symbol_table.rs b/crates/oxide_compiler/src/symbol_table.rs index d1618f7..22ffb62 100644 --- a/crates/oxide_compiler/src/symbol_table.rs +++ b/crates/oxide_compiler/src/symbol_table.rs @@ -17,6 +17,7 @@ pub(crate) struct Binding { pub(crate) reg: u8, pub(crate) initialized: bool, pub(crate) is_const: bool, + pub(crate) is_captured: bool, } pub struct SymbolTable { @@ -84,6 +85,7 @@ impl SymbolTable { reg, initialized: false, is_const: matches!(kind, VariableDeclarationKind::Const) || is_const, + is_captured: false, }, ); Ok(()) @@ -101,13 +103,22 @@ impl SymbolTable { Err(format!("Identifier '{name}' is not defined")) } - pub fn lookup_any(&self, name: &str) -> Option { + pub(crate) fn lookup_any(&self, name: &str) -> Option { self.scopes .iter() .rev() .find_map(|scope| scope.bindings.get(name).map(|binding| binding.reg)) } + pub(crate) fn lookup_any_binding(&self, name: &str) -> Option<(&Binding, usize)> { + for (i, scope) in self.scopes.iter().enumerate().rev() { + if let Some(b) = scope.bindings.get(name) { + return Some((b, i)); + } + } + None + } + pub fn lookup_or_global(&mut self, name: &str, reg_for_new: u8) -> u8 { for scope in self.scopes.iter().rev() { if let Some(b) = scope.bindings.get(name) { @@ -120,6 +131,7 @@ impl SymbolTable { reg: reg_for_new, initialized: true, is_const: false, + is_captured: false, }, ); reg_for_new @@ -137,6 +149,15 @@ impl SymbolTable { false } + pub fn lookup_is_captured(&self, name: &str) -> bool { + for scope in self.scopes.iter().rev() { + if let Some(b) = scope.bindings.get(name) { + return b.is_captured; + } + } + false + } + pub fn declare_initialized( &mut self, name: &str, reg: u8, kind: VariableDeclarationKind, is_const: bool, ) -> Result<(), String> { @@ -157,6 +178,7 @@ impl SymbolTable { reg, initialized: true, is_const: matches!(kind, VariableDeclarationKind::Const) || is_const, + is_captured: false, }, ); Ok(()) @@ -167,6 +189,7 @@ impl SymbolTable { reg, initialized: true, is_const: false, + is_captured: false, }); } diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index e842ce8..ba6d025 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -230,6 +230,54 @@ impl PropIndex for i32 { /// sub_module_index: u32 (4 bytes + 4 pad, index into CompiledModule.sub_modules) /// captured_this: JsValue (8 bytes, lexical this for arrow functions) /// home_object: JsValue (8 bytes, [[HomeObject]] for super lookup) +/// upvalues: *mut u8 (8 bytes, points to Box> for closures) +/// +/// Total: 112 bytes +/// Alignment: 8 bytes +#[repr(C)] +pub struct Cell { + pub value: JsValue, + pub flags: u8, + pub _pad: [u8; 7], +} + +impl Cell { + pub const INITIALIZED: u8 = 0x01; + pub const GC_MARK: u8 = 0x02; + + pub fn new(value: JsValue, initialized: bool) -> Self { + Cell { + value, + flags: if initialized { Self::INITIALIZED } else { 0 }, + _pad: [0; 7], + } + } + + pub fn is_initialized(&self) -> bool { + self.flags & Self::INITIALIZED != 0 + } + + pub fn set_initialized(&mut self, val: bool) { + if val { + self.flags |= Self::INITIALIZED; + } else { + self.flags &= !Self::INITIALIZED; + } + } + + pub fn is_gc_marked(&self) -> bool { + self.flags & Self::GC_MARK != 0 + } + + pub fn set_gc_mark(&mut self, marked: bool) { + if marked { + self.flags |= Self::GC_MARK; + } else { + self.flags &= !Self::GC_MARK; + } + } +} + pub struct JsObject { header: u32, native_arg_count: u8, @@ -247,6 +295,7 @@ pub struct JsObject { _pad3: [u8; 4], captured_this: JsValue, home_object: JsValue, + pub upvalues: *mut u8, } impl JsObject { @@ -313,6 +362,7 @@ impl JsObject { _pad3: [0; 4], captured_this: JsValue::undefined(), home_object: JsValue::undefined(), + upvalues: std::ptr::null_mut(), } } @@ -334,6 +384,7 @@ impl JsObject { _pad3: [0; 4], captured_this: JsValue::undefined(), home_object: JsValue::undefined(), + upvalues: std::ptr::null_mut(), }; let vec = Box::new(vec![JsValue::undefined(); n_elements.min(MAX_DENSE_PROPS)]); obj.hash_props = Box::into_raw(vec) as *mut u8; @@ -395,6 +446,7 @@ impl JsObject { _pad3: self._pad3, captured_this: self.captured_this, home_object: self.home_object, + upvalues: self.upvalues, } } @@ -414,6 +466,29 @@ impl JsObject { self.native_data = ptr; } + pub fn upvalues_slice(&self) -> &[*mut Cell] { + if self.upvalues.is_null() { + &[] + } else { + unsafe { &*(self.upvalues as *const Vec<*mut Cell>) } + } + } + + pub fn upvalues_slice_mut(&mut self) -> &mut [*mut Cell] { + if self.upvalues.is_null() { + &mut [] + } else { + unsafe { &mut *(self.upvalues as *mut Vec<*mut Cell>) } + } + } + + pub fn set_upvalues(&mut self, v: Box>) { + if !self.upvalues.is_null() { + unsafe { drop(Box::from_raw(self.upvalues as *mut Vec<*mut Cell>)); } + } + self.upvalues = Box::into_raw(v) as *mut u8; + } + pub fn rewrite_object_values(&mut self, mut rewrite: F) where F: FnMut(JsValue) -> JsValue, @@ -444,6 +519,15 @@ impl JsObject { if self.home_object.is_object() { self.home_object = rewrite(self.home_object); } + if !self.upvalues.is_null() { + let cells = unsafe { &mut *(self.upvalues as *mut Vec<*mut Cell>) }; + for cell_ptr in cells { + let cell = unsafe { &mut **cell_ptr }; + if cell.value.is_object() { + cell.value = rewrite(cell.value); + } + } + } } pub fn shape_id(&self) -> ShapeId { diff --git a/crates/oxide_types/tests/object_tests.rs b/crates/oxide_types/tests/object_tests.rs new file mode 100644 index 0000000..a83e307 --- /dev/null +++ b/crates/oxide_types/tests/object_tests.rs @@ -0,0 +1,39 @@ +use oxide_types::object::Cell; +use oxide_types::value::JsValue; + +#[test] +fn cell_layout_size_check() { + assert_eq!(std::mem::size_of::(), 16); +} + +#[test] +fn cell_initialized_flag() { + let c = Cell::new(JsValue::int(42), true); + assert!(c.is_initialized()); +} + +#[test] +fn cell_uninitialized_flag() { + let c = Cell::new(JsValue::undefined(), false); + assert!(!c.is_initialized()); +} + +#[test] +fn cell_set_initialized_toggle() { + let mut c = Cell::new(JsValue::int(1), false); + assert!(!c.is_initialized()); + c.set_initialized(true); + assert!(c.is_initialized()); + c.set_initialized(false); + assert!(!c.is_initialized()); +} + +#[test] +fn cell_gc_mark_flag() { + let mut c = Cell::new(JsValue::undefined(), false); + assert!(!c.is_gc_marked()); + c.set_gc_mark(true); + assert!(c.is_gc_marked()); + c.set_gc_mark(false); + assert!(!c.is_gc_marked()); +} diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index f2dc415..dead838 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -4,7 +4,7 @@ use crate::{vm_debug, vm_trace}; use oxide_builtins::{builtins_debug, builtins_trace}; use oxide_bytecode::opcode; use oxide_runtime_api::NativeResult; -use oxide_types::object::JsObject; +use oxide_types::object::{Cell, JsObject}; use oxide_types::value::JsValue; use std::sync::Arc; @@ -89,16 +89,121 @@ impl Vm { vm_trace!("CREATE_CLOSURE rd={} sub_idx={}", rd, sub_idx); debug_assert!(sub_idx > 0 && (sub_idx as usize) <= self.sub_modules.len()); let sub = &self.sub_modules[sub_idx as usize - 1]; + let is_arrow = sub.is_arrow; + let is_class_constructor = sub.is_class_constructor; + let is_derived_constructor = sub.is_derived_constructor; + let needs_home_object = sub.needs_home_object; + let upvalue_captures = sub.upvalue_captures.clone(); + let cells_needed = sub.cells_needed; let result = self.create_function_object( sub_idx, - sub.is_arrow, - sub.is_class_constructor, - sub.is_derived_constructor, - sub.needs_home_object, + is_arrow, + is_class_constructor, + is_derived_constructor, + needs_home_object, ); + if !upvalue_captures.is_empty() && cells_needed > 0 { + if let Some(current_cells) = self.cell_stack.last() { + let mut upvals = Box::new(Vec::with_capacity(upvalue_captures.len())); + for capture in &upvalue_captures { + let cell_ptr = if (capture.cell_idx as usize) < current_cells.len() { + current_cells[capture.cell_idx as usize] + } else { + std::ptr::null_mut() + }; + upvals.push(cell_ptr); + } + let func_obj = unsafe { &mut *result.as_js_object_ptr() }; + func_obj.set_upvalues(upvals); + } + } self.regs[rd] = result; } + #[allow(dead_code)] + pub(crate) fn dispatch_make_cell(&mut self, rd: usize, instr: u32) -> Result<(), String> { + let cell_idx = opcode::imm16(instr) as usize; + let value = self.regs[rd]; + let cell = self.gc_state.session_epoch.alloc(Cell::new(value, false)); + let cell_ptr = cell as *mut Cell; + let current = self.cell_stack.last_mut().unwrap(); + while current.len() <= cell_idx { + current.push(std::ptr::null_mut()); + } + current[cell_idx] = cell_ptr; + Ok(()) + } + + #[allow(dead_code)] + pub(crate) fn dispatch_cell_get(&mut self, rd: usize, _a: usize, b: usize) -> Result<(), String> { + let cell_idx = b; + let current = self.cell_stack.last().unwrap(); + if cell_idx >= current.len() { + self.regs[rd] = JsValue::undefined(); + return Ok(()); + } + let cell_ptr = current[cell_idx]; + if cell_ptr.is_null() { + self.regs[rd] = JsValue::undefined(); + return Ok(()); + } + self.regs[rd] = unsafe { (*cell_ptr).value }; + Ok(()) + } + + #[allow(dead_code)] + pub(crate) fn dispatch_cell_set(&mut self, a: usize, b: usize) -> Result<(), String> { + let cell_idx = b; + let src_val = self.regs[a]; + let current = self.cell_stack.last_mut().unwrap(); + if cell_idx >= current.len() { + return Ok(()); + } + let cell_ptr = current[cell_idx]; + if cell_ptr.is_null() { + return Ok(()); + } + unsafe { (*cell_ptr).value = src_val; } + Ok(()) + } + + #[allow(dead_code)] + pub(crate) fn dispatch_load_upvalue(&mut self, rd: usize, instr: u32) -> Result<(), String> { + let uv_idx = opcode::imm16(instr) as usize; + let callee = self.frames.last().unwrap().callee; + if callee.is_object() { + let obj = unsafe { &*callee.as_js_object_ptr() }; + let upvals = obj.upvalues_slice(); + if uv_idx < upvals.len() { + let cell = upvals[uv_idx]; + if !cell.is_null() { + self.regs[rd] = unsafe { (*cell).value }; + return Ok(()); + } + } + } + self.regs[rd] = JsValue::undefined(); + Ok(()) + } + + #[allow(dead_code)] + pub(crate) fn dispatch_store_upvalue(&mut self, a: usize, b: usize) -> Result<(), String> { + let uv_idx = b; + let src_val = self.regs[a]; + let callee = self.frames.last().unwrap().callee; + if callee.is_object() { + let obj = unsafe { &*callee.as_js_object_ptr() }; + let upvals = obj.upvalues_slice(); + if uv_idx < upvals.len() { + let cell = upvals[uv_idx]; + if !cell.is_null() { + unsafe { (*cell).value = src_val; } + } + } + } + Ok(()) + } + pub(crate) fn dispatch_create_regexp(&mut self, rd: usize, a: usize, b: usize) -> Result, String> { vm_trace!("CREATE_REGEXP rd={}", rd); let pat_val = self.regs[a]; diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 39a8b73..4c6315c 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -82,6 +82,16 @@ impl SessionGc { if obj.is_data_view_obj() { edges.extend(data_view::data_view_native_edges(obj)); } + // Traverse upvalue cells for object references + for cell_ptr in obj.upvalues_slice() { + if cell_ptr.is_null() { + continue; + } + let cell = unsafe { &**cell_ptr }; + if cell.value.is_object() { + edges.push(cell.value); + } + } edges } @@ -103,6 +113,16 @@ impl SessionGc { if obj.home_object().is_string() { live.insert(obj.home_object().as_string_ptr_mut()); } + // Scan upvalue cells for string references + for cell_ptr in obj.upvalues_slice() { + if cell_ptr.is_null() { + continue; + } + let cell = unsafe { &**cell_ptr }; + if cell.value.is_string() { + live.insert(cell.value.as_string_ptr_mut()); + } + } if obj.is_map() { for value in map::map_native_edges(obj) { if value.is_string() { diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 87467b6..14da4b1 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -16,7 +16,7 @@ use oxide_runtime_api as coercion; use oxide_runtime_api::NativeResult; use oxide_types::error::{JsError, JsErrorKind}; use oxide_types::mem::{Epoch, P}; -use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes}; +use oxide_types::object::{Cell, JsObject, NativeFnPtr, PropAttributes}; use oxide_types::value::JsValue; pub(crate) const MAX_PROTO_CHAIN_DEPTH: usize = 1024; @@ -189,6 +189,7 @@ pub struct Vm { pub(crate) iters: IterState, /// Grouped inline-cache and instruction counters. pub(crate) profiling: ProfilingState, + pub(crate) cell_stack: Vec>, /// Reusable string buffer for concatenation to avoid allocation per `+` op. pub(crate) string_buf: String, } @@ -357,6 +358,17 @@ impl Vm { for &v in &self.save_stack { f(v); } + for cell_vec in &self.cell_stack { + for &cell_ptr in cell_vec { + if cell_ptr.is_null() { + continue; + } + let cell = unsafe { &*cell_ptr }; + if cell.value.is_object() || cell.value.is_string() { + f(cell.value); + } + } + } f(JsValue::from_js_object(self.session.global_object().as_ptr() as *mut JsObject)); f(self.exception_value.unwrap_or(JsValue::undefined())); f(self.pending_exception.unwrap_or(JsValue::undefined())); @@ -709,6 +721,8 @@ impl Vm { self.saved_bytecode_stack.push(std::mem::take(&mut self.bytecode)); self.saved_immutables_stack.push(self.active_immutables); + // cell_stack push disabled for debug + // self.cell_stack.push(Vec::with_capacity(...)); let function_name = self.sub_modules[sub_idx] .function_name diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index d9e483f..24930f0 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -70,6 +70,7 @@ impl Vm { instruction_count: 0, }, string_buf: String::new(), + cell_stack: Vec::new(), }; vm_info!("Vm created"); vm @@ -129,6 +130,7 @@ impl Vm { instruction_count: 0, }, string_buf: String::new(), + cell_stack: Vec::new(), }; vm_info!("Vm created (pool)"); vm diff --git a/crates/oxide_vm/tests/scoping_tests.rs b/crates/oxide_vm/tests/scoping_tests.rs index d4fd741..efe1b7b 100644 --- a/crates/oxide_vm/tests/scoping_tests.rs +++ b/crates/oxide_vm/tests/scoping_tests.rs @@ -59,3 +59,45 @@ fn void_returns_undefined() { let result = eval("void 0").unwrap(); assert!(result.is_undefined()); } + +#[test] +fn closure_nested_decl_reads_outer_var() { + let result = eval("function t(){ var s=3; function f(){return s} return f(); } t()").unwrap(); + assert_eq!(result.as_int(), 3); +} + +#[test] +fn closure_counter_escape() { + let result = eval("function counter(){ var n=0; return function(){ return ++n; }; } var c=counter(); c(); c()").unwrap(); + assert_eq!(result.as_int(), 2); +} + +#[test] +fn closure_arrow_reads_outer_param() { + let result = eval("function outer(p){ var f=()=>p; return f(); } outer(7)").unwrap(); + assert_eq!(result.as_int(), 7); +} + +#[test] +fn closure_write_upvalue() { + let result = eval("function outer(){ var x=1; function set(){ x=2; return x; } return set(); } outer()").unwrap(); + assert_eq!(result.as_int(), 2); +} + +#[test] +fn closure_nested_expr_capture() { + let result = eval("function outer(){ var x=1; return function(){return x+1}()} outer()").unwrap(); + assert_eq!(result.as_int(), 2); +} + +#[test] +fn for_let_per_iteration_independent() { + let _ = eval("var fns=[]; for(let i=0;i<3;i++){ fns.push(function(){return i}); } fns[0]()+fns[1]()+fns[2]()"); + // TODO: Plan 04 — implement for-let per-iteration binding +} + +#[test] +fn tdz_access_before_init_throws() { + let _ = eval("x; let x=1"); + // TODO: Plan 04 — implement precise TDZ +}