From ec5ec682a916a7fb5f8408869bea4aac0b83775e Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Sun, 22 Mar 2026 12:44:54 +0100 Subject: [PATCH] Implement JS closures, const immutability, and method-call this binding Add upvalue-based closure system using GC-allocated cells: - New bytecode ops: NewCell, CellLoad, CellStore, LoadUpvalue, StoreUpvalue - UpvalueDef metadata on Function for closure capture resolution - HeapObject::Cell variant for mutable captured variable storage - Free variable analysis with transitive capture support - Captured parameters are boxed into cells at function entry - const declarations enforce immutability at compile time - Method calls (obj.method()) set this before invocation Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/js/src/bytecode.rs | 77 +++ crates/js/src/compiler.rs | 1008 ++++++++++++++++++++++++++++++++++--- crates/js/src/vm.rs | 323 +++++++++++- 3 files changed, 1340 insertions(+), 68 deletions(-) diff --git a/crates/js/src/bytecode.rs b/crates/js/src/bytecode.rs index ec5ac3d..c491861 100644 --- a/crates/js/src/bytecode.rs +++ b/crates/js/src/bytecode.rs @@ -152,6 +152,16 @@ pub enum Op { PushExceptionHandler = 0x76, /// PopExceptionHandler — remove the current exception handler PopExceptionHandler = 0x77, + /// NewCell dst — allocate a new GC cell initialized to undefined + NewCell = 0x78, + /// CellLoad dst, cell_reg — read the value stored in the cell + CellLoad = 0x79, + /// CellStore cell_reg, src — write a value into the cell + CellStore = 0x7A, + /// LoadUpvalue dst, idx(u8) — load from the closure's captured upvalue cell + LoadUpvalue = 0x7B, + /// StoreUpvalue idx(u8), src — store into the closure's captured upvalue cell + StoreUpvalue = 0x7C, } impl Op { @@ -215,6 +225,11 @@ impl Op { 0x75 => Some(Op::GetPrototype), 0x76 => Some(Op::PushExceptionHandler), 0x77 => Some(Op::PopExceptionHandler), + 0x78 => Some(Op::NewCell), + 0x79 => Some(Op::CellLoad), + 0x7A => Some(Op::CellStore), + 0x7B => Some(Op::LoadUpvalue), + 0x7C => Some(Op::StoreUpvalue), _ => None, } } @@ -227,6 +242,17 @@ pub enum Constant { String(String), } +/// Describes how a closure captures a single variable from its enclosing scope. +#[derive(Debug, Clone)] +pub struct UpvalueDef { + /// If true, `index` refers to a register in the immediately enclosing function + /// (which must hold a cell GcRef). If false, `index` refers to an upvalue slot + /// of the enclosing function (transitive capture). + pub is_local: bool, + /// Register index (if `is_local`) or upvalue index (if not) in the parent. + pub index: u8, +} + /// A compiled bytecode function. #[derive(Debug, Clone)] pub struct Function { @@ -246,6 +272,8 @@ pub struct Function { pub functions: Vec, /// Source map: bytecode offset → source line (sorted by offset). pub source_map: Vec<(u32, u32)>, + /// Upvalue definitions: how this function captures variables from its parent. + pub upvalue_defs: Vec, } impl Function { @@ -259,6 +287,7 @@ impl Function { names: Vec::new(), functions: Vec::new(), source_map: Vec::new(), + upvalue_defs: Vec::new(), } } } @@ -484,6 +513,20 @@ impl BytecodeBuilder { self.emit_u8(value as u8); } + /// Emit: LoadUpvalue dst, idx + pub fn emit_load_upvalue(&mut self, dst: Reg, idx: u8) { + self.emit_u8(Op::LoadUpvalue as u8); + self.emit_u8(dst); + self.emit_u8(idx); + } + + /// Emit: StoreUpvalue idx, src + pub fn emit_store_upvalue(&mut self, idx: u8, src: Reg) { + self.emit_u8(Op::StoreUpvalue as u8); + self.emit_u8(idx); + self.emit_u8(src); + } + /// Add a source map entry: current bytecode offset → source line. pub fn add_source_map(&mut self, line: u32) { let offset = self.offset() as u32; @@ -807,6 +850,35 @@ impl Function { format!("PushExceptionHandler r{catch_reg}, @{target:04X}") } Op::PopExceptionHandler => "PopExceptionHandler".to_string(), + Op::NewCell => { + let dst = code[pc]; + pc += 1; + format!("NewCell r{dst}") + } + Op::CellLoad => { + let dst = code[pc]; + let cell = code[pc + 1]; + pc += 2; + format!("CellLoad r{dst}, r{cell}") + } + Op::CellStore => { + let cell = code[pc]; + let src = code[pc + 1]; + pc += 2; + format!("CellStore r{cell}, r{src}") + } + Op::LoadUpvalue => { + let dst = code[pc]; + let idx = code[pc + 1]; + pc += 2; + format!("LoadUpvalue r{dst}, uv{idx}") + } + Op::StoreUpvalue => { + let idx = code[pc]; + let src = code[pc + 1]; + pc += 2; + format!("StoreUpvalue uv{idx}, r{src}") + } }; out.push_str(&format!(" {offset:04X} {line}\n")); } @@ -895,6 +967,11 @@ mod tests { Op::GetPrototype, Op::PushExceptionHandler, Op::PopExceptionHandler, + Op::NewCell, + Op::CellLoad, + Op::CellStore, + Op::LoadUpvalue, + Op::StoreUpvalue, ]; for op in ops { assert_eq!( diff --git a/crates/js/src/compiler.rs b/crates/js/src/compiler.rs index 6ac968f..5f8feac 100644 --- a/crates/js/src/compiler.rs +++ b/crates/js/src/compiler.rs @@ -7,6 +7,7 @@ use crate::ast::*; use crate::bytecode::*; use crate::JsError; +use std::collections::HashSet; /// Compiler state for a single function scope. struct FunctionCompiler { @@ -17,12 +18,31 @@ struct FunctionCompiler { next_reg: u8, /// Stack of loop contexts for break/continue. loop_stack: Vec, + /// Upvalues captured from the parent scope (used in inner functions). + upvalues: Vec, + /// Set of local variable names that are captured by inner functions. + /// Pre-populated before compilation by scanning inner function bodies. + captured_names: HashSet, } #[derive(Debug, Clone)] struct Local { name: String, reg: Reg, + /// Whether this variable is captured by an inner function (stored in a cell). + is_captured: bool, + /// Whether this variable was declared with `const`. + is_const: bool, +} + +/// An upvalue entry tracking how this function captures an outer variable. +struct UpvalueEntry { + /// Name of the captured variable (for dedup during resolution). + name: String, + /// The resolved upvalue definition. + def: UpvalueDef, + /// Whether the original declaration was `const`. + is_const: bool, } struct LoopCtx { @@ -41,6 +61,8 @@ impl FunctionCompiler { locals: Vec::new(), next_reg: 0, loop_stack: Vec::new(), + upvalues: Vec::new(), + captured_names: HashSet::new(), } } @@ -64,30 +86,641 @@ impl FunctionCompiler { self.next_reg -= 1; } - /// Look up a local variable by name. + /// Look up a local variable by name, returning full info. + fn find_local_info(&self, name: &str) -> Option<&Local> { + self.locals.iter().rev().find(|l| l.name == name) + } + + /// Look up a local variable by name (register only). fn find_local(&self, name: &str) -> Option { - self.locals + self.find_local_info(name).map(|l| l.reg) + } + + /// Look up an upvalue by name, returning its index. + fn find_upvalue(&self, name: &str) -> Option { + self.upvalues .iter() - .rev() - .find(|l| l.name == name) - .map(|l| l.reg) + .position(|u| u.name == name) + .map(|i| i as u8) + } + + /// Check if an upvalue is const. + fn is_upvalue_const(&self, idx: u8) -> bool { + self.upvalues + .get(idx as usize) + .map(|u| u.is_const) + .unwrap_or(false) } /// Define a local variable. fn define_local(&mut self, name: &str) -> Reg { + self.define_local_ext(name, false, false) + } + + /// Define a local variable with capture and const flags. + fn define_local_ext(&mut self, name: &str, is_captured: bool, is_const: bool) -> Reg { let reg = self.alloc_reg(); self.locals.push(Local { name: name.to_string(), reg, + is_captured, + is_const, }); reg } } +// ── Free variable analysis ────────────────────────────────── + +/// Collect identifiers referenced inside `body` that are not declared locally +/// (params or variable declarations within the body). This set represents the +/// "free variables" of a function body — variables that must be captured. +/// This includes transitive free variables from nested functions. +fn collect_free_vars(params: &[Pattern], body: &[Stmt]) -> HashSet { + let mut declared = HashSet::new(); + let mut referenced = HashSet::new(); + + // Params are local declarations. + for p in params { + collect_pattern_names(p, &mut declared); + } + + // Collect declarations and references from the body. + for stmt in body { + collect_stmt_decls(stmt, &mut declared); + } + for stmt in body { + collect_stmt_refs(stmt, &declared, &mut referenced); + } + + // Also include transitive free variables from nested inner functions. + // If an inner function references `x` and `x` is not declared in THIS scope, + // then `x` is also a free variable of THIS function. + let inner_caps = collect_inner_captures(body); + for name in inner_caps { + if !declared.contains(&name) { + referenced.insert(name); + } + } + + referenced +} + +/// Collect free variables from an arrow function body. +fn collect_free_vars_arrow(params: &[Pattern], body: &ArrowBody) -> HashSet { + let mut declared = HashSet::new(); + let mut referenced = HashSet::new(); + + for p in params { + collect_pattern_names(p, &mut declared); + } + + match body { + ArrowBody::Expr(expr) => { + collect_expr_refs(expr, &declared, &mut referenced); + } + ArrowBody::Block(stmts) => { + for stmt in stmts { + collect_stmt_decls(stmt, &mut declared); + } + for stmt in stmts { + collect_stmt_refs(stmt, &declared, &mut referenced); + } + // Transitive free variables from nested functions. + let inner_caps = collect_inner_captures(stmts); + for name in inner_caps { + if !declared.contains(&name) { + referenced.insert(name); + } + } + } + } + + referenced +} + +fn collect_pattern_names(pat: &Pattern, names: &mut HashSet) { + match &pat.kind { + PatternKind::Identifier(name) => { + names.insert(name.clone()); + } + PatternKind::Array { elements, rest } => { + for elem in elements.iter().flatten() { + collect_pattern_names(elem, names); + } + if let Some(rest) = rest { + collect_pattern_names(rest, names); + } + } + PatternKind::Object { properties, rest } => { + for prop in properties { + collect_pattern_names(&prop.value, names); + } + if let Some(rest) = rest { + collect_pattern_names(rest, names); + } + } + PatternKind::Assign { left, .. } => { + collect_pattern_names(left, names); + } + } +} + +/// Collect all variable/function declarations in a statement (not recursing into +/// inner functions — those form their own scope). +fn collect_stmt_decls(stmt: &Stmt, declared: &mut HashSet) { + match &stmt.kind { + StmtKind::VarDecl { declarators, .. } => { + for d in declarators { + collect_pattern_names(&d.pattern, declared); + } + } + StmtKind::FunctionDecl(f) => { + if let Some(name) = &f.id { + declared.insert(name.clone()); + } + } + StmtKind::ClassDecl(c) => { + if let Some(name) = &c.id { + declared.insert(name.clone()); + } + } + StmtKind::Block(stmts) => { + for s in stmts { + collect_stmt_decls(s, declared); + } + } + StmtKind::If { + consequent, + alternate, + .. + } => { + collect_stmt_decls(consequent, declared); + if let Some(alt) = alternate { + collect_stmt_decls(alt, declared); + } + } + StmtKind::While { body, .. } + | StmtKind::DoWhile { body, .. } + | StmtKind::Labeled { body, .. } => { + collect_stmt_decls(body, declared); + } + StmtKind::For { init, body, .. } => { + if let Some(ForInit::VarDecl { declarators, .. }) = init { + for d in declarators { + collect_pattern_names(&d.pattern, declared); + } + } + collect_stmt_decls(body, declared); + } + StmtKind::ForIn { left, body, .. } | StmtKind::ForOf { left, body, .. } => { + if let ForInOfLeft::VarDecl { pattern, .. } = left { + collect_pattern_names(pattern, declared); + } + collect_stmt_decls(body, declared); + } + StmtKind::Try { + block, + handler, + finalizer, + } => { + for s in block { + collect_stmt_decls(s, declared); + } + if let Some(h) = handler { + if let Some(param) = &h.param { + collect_pattern_names(param, declared); + } + for s in &h.body { + collect_stmt_decls(s, declared); + } + } + if let Some(fin) = finalizer { + for s in fin { + collect_stmt_decls(s, declared); + } + } + } + StmtKind::Switch { cases, .. } => { + for case in cases { + for s in &case.consequent { + collect_stmt_decls(s, declared); + } + } + } + _ => {} + } +} + +/// Collect all identifier references in a statement, excluding inner function scopes. +/// Identifiers that are in `declared` are local and skipped. +fn collect_stmt_refs(stmt: &Stmt, declared: &HashSet, refs: &mut HashSet) { + match &stmt.kind { + StmtKind::Expr(expr) => collect_expr_refs(expr, declared, refs), + StmtKind::Block(stmts) => { + for s in stmts { + collect_stmt_refs(s, declared, refs); + } + } + StmtKind::VarDecl { declarators, .. } => { + for d in declarators { + if let Some(init) = &d.init { + collect_expr_refs(init, declared, refs); + } + } + } + StmtKind::FunctionDecl(_) => { + // Don't recurse into inner functions — they have their own scope. + } + StmtKind::If { + test, + consequent, + alternate, + } => { + collect_expr_refs(test, declared, refs); + collect_stmt_refs(consequent, declared, refs); + if let Some(alt) = alternate { + collect_stmt_refs(alt, declared, refs); + } + } + StmtKind::While { test, body } => { + collect_expr_refs(test, declared, refs); + collect_stmt_refs(body, declared, refs); + } + StmtKind::DoWhile { body, test } => { + collect_stmt_refs(body, declared, refs); + collect_expr_refs(test, declared, refs); + } + StmtKind::For { + init, + test, + update, + body, + } => { + if let Some(init) = init { + match init { + ForInit::VarDecl { declarators, .. } => { + for d in declarators { + if let Some(init) = &d.init { + collect_expr_refs(init, declared, refs); + } + } + } + ForInit::Expr(e) => collect_expr_refs(e, declared, refs), + } + } + if let Some(t) = test { + collect_expr_refs(t, declared, refs); + } + if let Some(u) = update { + collect_expr_refs(u, declared, refs); + } + collect_stmt_refs(body, declared, refs); + } + StmtKind::ForIn { right, body, .. } | StmtKind::ForOf { right, body, .. } => { + collect_expr_refs(right, declared, refs); + collect_stmt_refs(body, declared, refs); + } + StmtKind::Return(Some(expr)) | StmtKind::Throw(expr) => { + collect_expr_refs(expr, declared, refs); + } + StmtKind::Try { + block, + handler, + finalizer, + } => { + for s in block { + collect_stmt_refs(s, declared, refs); + } + if let Some(h) = handler { + for s in &h.body { + collect_stmt_refs(s, declared, refs); + } + } + if let Some(fin) = finalizer { + for s in fin { + collect_stmt_refs(s, declared, refs); + } + } + } + StmtKind::Switch { + discriminant, + cases, + } => { + collect_expr_refs(discriminant, declared, refs); + for case in cases { + if let Some(test) = &case.test { + collect_expr_refs(test, declared, refs); + } + for s in &case.consequent { + collect_stmt_refs(s, declared, refs); + } + } + } + StmtKind::Labeled { body, .. } => { + collect_stmt_refs(body, declared, refs); + } + _ => {} + } +} + +/// Collect identifier references in an expression. Does NOT recurse into +/// inner function/arrow bodies (those form their own scope). +fn collect_expr_refs(expr: &Expr, declared: &HashSet, refs: &mut HashSet) { + match &expr.kind { + ExprKind::Identifier(name) => { + if !declared.contains(name) { + refs.insert(name.clone()); + } + } + ExprKind::Binary { left, right, .. } + | ExprKind::Logical { left, right, .. } + | ExprKind::Assignment { left, right, .. } => { + collect_expr_refs(left, declared, refs); + collect_expr_refs(right, declared, refs); + } + ExprKind::Unary { argument, .. } | ExprKind::Update { argument, .. } => { + collect_expr_refs(argument, declared, refs); + } + ExprKind::Conditional { + test, + consequent, + alternate, + } => { + collect_expr_refs(test, declared, refs); + collect_expr_refs(consequent, declared, refs); + collect_expr_refs(alternate, declared, refs); + } + ExprKind::Call { callee, arguments } | ExprKind::New { callee, arguments } => { + collect_expr_refs(callee, declared, refs); + for arg in arguments { + collect_expr_refs(arg, declared, refs); + } + } + ExprKind::Member { + object, + property, + computed, + .. + } => { + collect_expr_refs(object, declared, refs); + if *computed { + collect_expr_refs(property, declared, refs); + } + } + ExprKind::Array(elements) => { + for elem in elements.iter().flatten() { + match elem { + ArrayElement::Expr(e) | ArrayElement::Spread(e) => { + collect_expr_refs(e, declared, refs); + } + } + } + } + ExprKind::Object(props) => { + for prop in props { + if let PropertyKey::Computed(e) = &prop.key { + collect_expr_refs(e, declared, refs); + } + if let Some(val) = &prop.value { + collect_expr_refs(val, declared, refs); + } + } + } + ExprKind::Sequence(exprs) => { + for e in exprs { + collect_expr_refs(e, declared, refs); + } + } + ExprKind::Spread(inner) => { + collect_expr_refs(inner, declared, refs); + } + ExprKind::TemplateLiteral { expressions, .. } => { + for e in expressions { + collect_expr_refs(e, declared, refs); + } + } + // Function/Arrow/Class bodies are new scopes — don't recurse. + ExprKind::Function(_) | ExprKind::Arrow { .. } | ExprKind::Class(_) => {} + _ => {} + } +} + +/// Collect the free variables of ALL inner functions/arrows within a list of +/// statements. Returns the set of outer-scope names they reference. +fn collect_inner_captures(stmts: &[Stmt]) -> HashSet { + let mut captures = HashSet::new(); + for stmt in stmts { + collect_inner_captures_stmt(stmt, &mut captures); + } + captures +} + +fn collect_inner_captures_stmt(stmt: &Stmt, caps: &mut HashSet) { + match &stmt.kind { + StmtKind::FunctionDecl(f) => { + let fv = collect_free_vars(&f.params, &f.body); + caps.extend(fv); + } + StmtKind::Expr(expr) => collect_inner_captures_expr(expr, caps), + StmtKind::VarDecl { declarators, .. } => { + for d in declarators { + if let Some(init) = &d.init { + collect_inner_captures_expr(init, caps); + } + } + } + StmtKind::Block(stmts) => { + for s in stmts { + collect_inner_captures_stmt(s, caps); + } + } + StmtKind::If { + test, + consequent, + alternate, + } => { + collect_inner_captures_expr(test, caps); + collect_inner_captures_stmt(consequent, caps); + if let Some(alt) = alternate { + collect_inner_captures_stmt(alt, caps); + } + } + StmtKind::While { test, body } => { + collect_inner_captures_expr(test, caps); + collect_inner_captures_stmt(body, caps); + } + StmtKind::DoWhile { body, test } => { + collect_inner_captures_stmt(body, caps); + collect_inner_captures_expr(test, caps); + } + StmtKind::For { + init, + test, + update, + body, + } => { + if let Some(ForInit::Expr(e)) = init { + collect_inner_captures_expr(e, caps); + } + if let Some(ForInit::VarDecl { declarators, .. }) = init { + for d in declarators { + if let Some(init) = &d.init { + collect_inner_captures_expr(init, caps); + } + } + } + if let Some(t) = test { + collect_inner_captures_expr(t, caps); + } + if let Some(u) = update { + collect_inner_captures_expr(u, caps); + } + collect_inner_captures_stmt(body, caps); + } + StmtKind::ForIn { right, body, .. } | StmtKind::ForOf { right, body, .. } => { + collect_inner_captures_expr(right, caps); + collect_inner_captures_stmt(body, caps); + } + StmtKind::Return(Some(expr)) | StmtKind::Throw(expr) => { + collect_inner_captures_expr(expr, caps); + } + StmtKind::Try { + block, + handler, + finalizer, + } => { + for s in block { + collect_inner_captures_stmt(s, caps); + } + if let Some(h) = handler { + for s in &h.body { + collect_inner_captures_stmt(s, caps); + } + } + if let Some(fin) = finalizer { + for s in fin { + collect_inner_captures_stmt(s, caps); + } + } + } + StmtKind::Switch { + discriminant, + cases, + } => { + collect_inner_captures_expr(discriminant, caps); + for case in cases { + if let Some(test) = &case.test { + collect_inner_captures_expr(test, caps); + } + for s in &case.consequent { + collect_inner_captures_stmt(s, caps); + } + } + } + StmtKind::Labeled { body, .. } => { + collect_inner_captures_stmt(body, caps); + } + _ => {} + } +} + +fn collect_inner_captures_expr(expr: &Expr, caps: &mut HashSet) { + match &expr.kind { + ExprKind::Function(f) => { + let fv = collect_free_vars(&f.params, &f.body); + caps.extend(fv); + } + ExprKind::Arrow { params, body, .. } => { + let fv = collect_free_vars_arrow(params, body); + caps.extend(fv); + } + ExprKind::Binary { left, right, .. } + | ExprKind::Logical { left, right, .. } + | ExprKind::Assignment { left, right, .. } => { + collect_inner_captures_expr(left, caps); + collect_inner_captures_expr(right, caps); + } + ExprKind::Unary { argument, .. } | ExprKind::Update { argument, .. } => { + collect_inner_captures_expr(argument, caps); + } + ExprKind::Conditional { + test, + consequent, + alternate, + } => { + collect_inner_captures_expr(test, caps); + collect_inner_captures_expr(consequent, caps); + collect_inner_captures_expr(alternate, caps); + } + ExprKind::Call { callee, arguments } | ExprKind::New { callee, arguments } => { + collect_inner_captures_expr(callee, caps); + for arg in arguments { + collect_inner_captures_expr(arg, caps); + } + } + ExprKind::Member { + object, + property, + computed, + .. + } => { + collect_inner_captures_expr(object, caps); + if *computed { + collect_inner_captures_expr(property, caps); + } + } + ExprKind::Array(elements) => { + for elem in elements.iter().flatten() { + match elem { + ArrayElement::Expr(e) | ArrayElement::Spread(e) => { + collect_inner_captures_expr(e, caps); + } + } + } + } + ExprKind::Object(props) => { + for prop in props { + if let PropertyKey::Computed(e) = &prop.key { + collect_inner_captures_expr(e, caps); + } + if let Some(val) = &prop.value { + collect_inner_captures_expr(val, caps); + } + } + } + ExprKind::Sequence(exprs) => { + for e in exprs { + collect_inner_captures_expr(e, caps); + } + } + ExprKind::Spread(inner) => { + collect_inner_captures_expr(inner, caps); + } + ExprKind::TemplateLiteral { expressions, .. } => { + for e in expressions { + collect_inner_captures_expr(e, caps); + } + } + ExprKind::Class(c) => { + for member in &c.body { + if let ClassMemberKind::Method { value, .. } = &member.kind { + let fv = collect_free_vars(&value.params, &value.body); + caps.extend(fv); + } + } + } + _ => {} + } +} + /// Compile a parsed program into a top-level bytecode function. pub fn compile(program: &Program) -> Result { let mut fc = FunctionCompiler::new("
".into(), 0); + // Pre-scan to find which top-level locals are captured by inner functions. + fc.captured_names = collect_inner_captures(&program.body); + // Reserve r0 for the implicit return value. let result_reg = fc.alloc_reg(); fc.builder.emit_reg(Op::LoadUndefined, result_reg); @@ -125,12 +758,9 @@ fn compile_stmt(fc: &mut FunctionCompiler, stmt: &Stmt, result_reg: Reg) -> Resu fc.next_reg = saved_next; } - StmtKind::VarDecl { - kind: _, - declarators, - } => { + StmtKind::VarDecl { kind, declarators } => { for decl in declarators { - compile_var_declarator(fc, decl)?; + compile_var_declarator(fc, decl, *kind)?; } } @@ -433,11 +1063,35 @@ fn compile_stmt(fc: &mut FunctionCompiler, stmt: &Stmt, result_reg: Reg) -> Resu // ── Variable declarations ─────────────────────────────────── -fn compile_var_declarator(fc: &mut FunctionCompiler, decl: &VarDeclarator) -> Result<(), JsError> { +fn compile_var_declarator( + fc: &mut FunctionCompiler, + decl: &VarDeclarator, + kind: VarKind, +) -> Result<(), JsError> { match &decl.pattern.kind { PatternKind::Identifier(name) => { - let reg = fc.define_local(name); - if let Some(init) = &decl.init { + let is_const = kind == VarKind::Const; + let is_captured = fc.captured_names.contains(name.as_str()); + + if is_const && decl.init.is_none() { + return Err(JsError::SyntaxError( + "Missing initializer in const declaration".into(), + )); + } + + let reg = fc.define_local_ext(name, is_captured, is_const); + + if is_captured { + // Allocate a cell for this variable. + fc.builder.emit_reg(Op::NewCell, reg); + if let Some(init) = &decl.init { + let tmp = fc.alloc_reg(); + compile_expr(fc, init, tmp)?; + fc.builder.emit_reg_reg(Op::CellStore, reg, tmp); + fc.free_reg(tmp); + } + // No init => cell stays undefined (already the default). + } else if let Some(init) = &decl.init { compile_expr(fc, init, reg)?; } else { fc.builder.emit_reg(Op::LoadUndefined, reg); @@ -533,36 +1187,136 @@ fn compile_destructuring_pattern( fn compile_function_decl(fc: &mut FunctionCompiler, func_def: &FunctionDef) -> Result<(), JsError> { let name = func_def.id.clone().unwrap_or_default(); - let inner = compile_function_body(func_def)?; + let inner = compile_function_body_with_captures(fc, func_def)?; let func_idx = fc.builder.add_function(inner); - let reg = fc.define_local(&name); - fc.builder.emit_reg_u16(Op::CreateClosure, reg, func_idx); + let is_captured = fc.captured_names.contains(name.as_str()); + let reg = fc.define_local_ext(&name, is_captured, false); - // Also store as global so inner/recursive calls via LoadGlobal can find it. - if !name.is_empty() { - let name_idx = fc.builder.add_name(&name); - fc.builder.emit_store_global(name_idx, reg); + if is_captured { + // Create a cell, then create the closure into a temp, then store into cell. + fc.builder.emit_reg(Op::NewCell, reg); + let tmp = fc.alloc_reg(); + fc.builder.emit_reg_u16(Op::CreateClosure, tmp, func_idx); + fc.builder.emit_reg_reg(Op::CellStore, reg, tmp); + // Also store as global. + if !name.is_empty() { + let name_idx = fc.builder.add_name(&name); + fc.builder.emit_store_global(name_idx, tmp); + } + fc.free_reg(tmp); + } else { + fc.builder.emit_reg_u16(Op::CreateClosure, reg, func_idx); + // Also store as global so inner/recursive calls via LoadGlobal can find it. + if !name.is_empty() { + let name_idx = fc.builder.add_name(&name); + fc.builder.emit_store_global(name_idx, reg); + } } Ok(()) } -fn compile_function_body(func_def: &FunctionDef) -> Result { +/// Compile a function body, resolving upvalue captures from the parent scope. +fn compile_function_body_with_captures( + parent: &mut FunctionCompiler, + func_def: &FunctionDef, +) -> Result { + // 1. Collect free variables of this inner function. + let free_vars = collect_free_vars(&func_def.params, &func_def.body); + + // 2. Build upvalue list by resolving free vars against the parent scope. + let mut upvalue_entries = Vec::new(); + for name in &free_vars { + if let Some(local) = parent.find_local_info(name) { + let reg = local.reg; + let is_const = local.is_const; + // Mark the parent's local as captured (if not already). + // We need to update the parent's local, so find the index and mutate. + if let Some(l) = parent.locals.iter_mut().rev().find(|l| l.name == *name) { + l.is_captured = true; + } + upvalue_entries.push(UpvalueEntry { + name: name.clone(), + def: UpvalueDef { + is_local: true, + index: reg, + }, + is_const, + }); + } else if let Some(parent_uv_idx) = parent.find_upvalue(name) { + // Transitive capture: the parent captures it from its own parent. + let is_const = parent.is_upvalue_const(parent_uv_idx); + upvalue_entries.push(UpvalueEntry { + name: name.clone(), + def: UpvalueDef { + is_local: false, + index: parent_uv_idx, + }, + is_const, + }); + } + // If not found in parent or parent's upvalues, it must be a global — no upvalue needed. + } + + // 3. Compile the inner function with its own scope. + let mut inner = compile_function_body_inner(func_def, &upvalue_entries)?; + + // 4. Attach upvalue definitions to the compiled function. + inner.upvalue_defs = upvalue_entries.iter().map(|e| e.def.clone()).collect(); + + Ok(inner) +} + +/// Core function body compilation. The `upvalue_entries` tell this function which +/// outer variables it can access via LoadUpvalue/StoreUpvalue. +fn compile_function_body_inner( + func_def: &FunctionDef, + upvalue_entries: &[UpvalueEntry], +) -> Result { let name = func_def.id.clone().unwrap_or_default(); let param_count = func_def.params.len().min(255) as u8; let mut inner = FunctionCompiler::new(name, param_count); + // Copy upvalue entries into the inner compiler so it can resolve references. + for entry in upvalue_entries { + inner.upvalues.push(UpvalueEntry { + name: entry.name.clone(), + def: entry.def.clone(), + is_const: entry.is_const, + }); + } + + // Pre-scan to find which of this function's locals are captured by ITS inner functions. + let inner_caps = collect_inner_captures(&func_def.body); + inner.captured_names = inner_caps; + // Allocate registers for parameters. for p in &func_def.params { - if let PatternKind::Identifier(name) = &p.kind { - inner.define_local(name); + if let PatternKind::Identifier(pname) = &p.kind { + let is_captured = inner.captured_names.contains(pname.as_str()); + inner.define_local_ext(pname, is_captured, false); } else { - // Destructuring param: allocate a register for the whole param, - // then destructure from it. let _ = inner.alloc_reg(); } } + // Box captured parameters into cells. + for p in &func_def.params { + if let PatternKind::Identifier(pname) = &p.kind { + if let Some(local) = inner.find_local_info(pname) { + if local.is_captured { + let reg = local.reg; + // Move param value to temp, allocate cell, store value into cell. + let tmp = inner.alloc_reg(); + inner.builder.emit_reg_reg(Op::Move, tmp, reg); + inner.builder.emit_reg(Op::NewCell, reg); + inner.builder.emit_reg_reg(Op::CellStore, reg, tmp); + inner.free_reg(tmp); + } + } + } + } + // Result register for the function body. let result_reg = inner.alloc_reg(); inner.builder.emit_reg(Op::LoadUndefined, result_reg); @@ -593,7 +1347,7 @@ fn compile_class_decl(fc: &mut FunctionCompiler, class_def: &ClassDef) -> Result if let Some(member) = ctor { if let ClassMemberKind::Method { value, .. } = &member.kind { - let inner = compile_function_body(value)?; + let inner = compile_function_body_with_captures(fc, value)?; let func_idx = fc.builder.add_function(inner); fc.builder.emit_reg_u16(Op::CreateClosure, reg, func_idx); } @@ -626,7 +1380,7 @@ fn compile_class_decl(fc: &mut FunctionCompiler, class_def: &ClassDef) -> Result PropertyKey::Identifier(s) | PropertyKey::String(s) => s.clone(), _ => continue, }; - let inner = compile_function_body(value)?; + let inner = compile_function_body_with_captures(fc, value)?; let func_idx = fc.builder.add_function(inner); let method_reg = fc.alloc_reg(); fc.builder @@ -783,12 +1537,9 @@ fn compile_for( // Init. if let Some(init) = init { match init { - ForInit::VarDecl { - kind: _, - declarators, - } => { + ForInit::VarDecl { kind, declarators } => { for decl in declarators { - compile_var_declarator(fc, decl)?; + compile_var_declarator(fc, decl, *kind)?; } } ForInit::Expr(expr) => { @@ -977,10 +1728,16 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), } ExprKind::Identifier(name) => { - if let Some(local_reg) = fc.find_local(name) { - if local_reg != dst { - fc.builder.emit_reg_reg(Op::Move, dst, local_reg); + if let Some(local) = fc.find_local_info(name) { + let reg = local.reg; + let captured = local.is_captured; + if captured { + fc.builder.emit_reg_reg(Op::CellLoad, dst, reg); + } else if reg != dst { + fc.builder.emit_reg_reg(Op::Move, dst, reg); } + } else if let Some(uv_idx) = fc.find_upvalue(name) { + fc.builder.emit_load_upvalue(dst, uv_idx); } else { // Global lookup. let ni = fc.builder.add_name(name); @@ -1169,23 +1926,74 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), } ExprKind::Call { callee, arguments } => { - let func_reg = fc.alloc_reg(); - compile_expr(fc, callee, func_reg)?; + // Detect method calls (obj.method()) to set `this`. + if let ExprKind::Member { + object, + property, + computed, + } = &callee.kind + { + // Layout: [obj_reg] [func_reg] [arg0] [arg1] ... + // We keep obj_reg alive so we can set `this` before the call. + let obj_reg = fc.alloc_reg(); + compile_expr(fc, object, obj_reg)?; + let func_reg = fc.alloc_reg(); + if !computed { + if let ExprKind::Identifier(name) = &property.kind { + let ni = fc.builder.add_name(name); + fc.builder.emit_get_prop_name(func_reg, obj_reg, ni); + } else { + let key_reg = fc.alloc_reg(); + compile_expr(fc, property, key_reg)?; + fc.builder + .emit_reg3(Op::GetProperty, func_reg, obj_reg, key_reg); + fc.free_reg(key_reg); + } + } else { + let key_reg = fc.alloc_reg(); + compile_expr(fc, property, key_reg)?; + fc.builder + .emit_reg3(Op::GetProperty, func_reg, obj_reg, key_reg); + fc.free_reg(key_reg); + } - let args_start = fc.next_reg; - let arg_count = arguments.len().min(255) as u8; - for arg in arguments { - let arg_reg = fc.alloc_reg(); - compile_expr(fc, arg, arg_reg)?; - } + // Set `this` to the receiver object before calling. + let this_ni = fc.builder.add_name("this"); + fc.builder.emit_store_global(this_ni, obj_reg); - fc.builder.emit_call(dst, func_reg, args_start, arg_count); + let args_start = fc.next_reg; + let arg_count = arguments.len().min(255) as u8; + for arg in arguments { + let arg_reg = fc.alloc_reg(); + compile_expr(fc, arg, arg_reg)?; + } - // Free argument registers (in reverse). - for _ in 0..arg_count { - fc.next_reg -= 1; + fc.builder.emit_call(dst, func_reg, args_start, arg_count); + + // Free in LIFO order: args, func_reg, obj_reg. + for _ in 0..arg_count { + fc.next_reg -= 1; + } + fc.free_reg(func_reg); + fc.free_reg(obj_reg); + } else { + let func_reg = fc.alloc_reg(); + compile_expr(fc, callee, func_reg)?; + + let args_start = fc.next_reg; + let arg_count = arguments.len().min(255) as u8; + for arg in arguments { + let arg_reg = fc.alloc_reg(); + compile_expr(fc, arg, arg_reg)?; + } + + fc.builder.emit_call(dst, func_reg, args_start, arg_count); + + for _ in 0..arg_count { + fc.next_reg -= 1; + } + fc.free_reg(func_reg); } - fc.free_reg(func_reg); } ExprKind::New { callee, arguments } => { @@ -1308,7 +2116,7 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), } ExprKind::Function(func_def) => { - let inner = compile_function_body(func_def)?; + let inner = compile_function_body_with_captures(fc, func_def)?; let func_idx = fc.builder.add_function(inner); fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx); } @@ -1318,15 +2126,84 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), body, is_async: _, } => { + // Collect free variables from the arrow body. + let free_vars = collect_free_vars_arrow(params, body); + + // Resolve upvalues against the parent scope. + let mut upvalue_entries = Vec::new(); + for name in &free_vars { + if let Some(local) = fc.find_local_info(name) { + let reg = local.reg; + let is_const = local.is_const; + if let Some(l) = fc.locals.iter_mut().rev().find(|l| l.name == *name) { + l.is_captured = true; + } + upvalue_entries.push(UpvalueEntry { + name: name.clone(), + def: UpvalueDef { + is_local: true, + index: reg, + }, + is_const, + }); + } else if let Some(parent_uv_idx) = fc.find_upvalue(name) { + let is_const = fc.is_upvalue_const(parent_uv_idx); + upvalue_entries.push(UpvalueEntry { + name: name.clone(), + def: UpvalueDef { + is_local: false, + index: parent_uv_idx, + }, + is_const, + }); + } + } + let param_count = params.len().min(255) as u8; let mut inner = FunctionCompiler::new("".into(), param_count); + + // Copy upvalue entries. + for entry in &upvalue_entries { + inner.upvalues.push(UpvalueEntry { + name: entry.name.clone(), + def: entry.def.clone(), + is_const: entry.is_const, + }); + } + + // Pre-scan for inner captures within the arrow body. + match body { + ArrowBody::Expr(_) => {} + ArrowBody::Block(stmts) => { + inner.captured_names = collect_inner_captures(stmts); + } + } + for p in params { - if let PatternKind::Identifier(name) = &p.kind { - inner.define_local(name); + if let PatternKind::Identifier(pname) = &p.kind { + let is_captured = inner.captured_names.contains(pname.as_str()); + inner.define_local_ext(pname, is_captured, false); } else { let _ = inner.alloc_reg(); } } + + // Box captured parameters. + for p in params { + if let PatternKind::Identifier(pname) = &p.kind { + if let Some(local) = inner.find_local_info(pname) { + if local.is_captured { + let reg = local.reg; + let tmp = inner.alloc_reg(); + inner.builder.emit_reg_reg(Op::Move, tmp, reg); + inner.builder.emit_reg(Op::NewCell, reg); + inner.builder.emit_reg_reg(Op::CellStore, reg, tmp); + inner.free_reg(tmp); + } + } + } + } + let result = inner.alloc_reg(); match body { ArrowBody::Expr(e) => { @@ -1338,7 +2215,8 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), } } inner.builder.emit_reg(Op::Return, result); - let inner_func = inner.builder.finish(); + let mut inner_func = inner.builder.finish(); + inner_func.upvalue_defs = upvalue_entries.iter().map(|e| e.def.clone()).collect(); let func_idx = fc.builder.add_function(inner_func); fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx); } @@ -1358,7 +2236,7 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), }); if let Some(member) = ctor { if let ClassMemberKind::Method { value, .. } = &member.kind { - let inner = compile_function_body(value)?; + let inner = compile_function_body_with_captures(fc, value)?; let func_idx = fc.builder.add_function(inner); fc.builder.emit_reg_u16(Op::CreateClosure, dst, func_idx); } @@ -1389,7 +2267,7 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), PropertyKey::Identifier(s) | PropertyKey::String(s) => s.clone(), _ => continue, }; - let inner = compile_function_body(value)?; + let inner = compile_function_body_with_captures(fc, value)?; let func_idx = fc.builder.add_function(inner); let method_reg = fc.alloc_reg(); fc.builder @@ -1487,10 +2365,26 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), fn compile_store(fc: &mut FunctionCompiler, target: &Expr, src: Reg) -> Result<(), JsError> { match &target.kind { ExprKind::Identifier(name) => { - if let Some(local) = fc.find_local(name) { - if local != src { - fc.builder.emit_reg_reg(Op::Move, local, src); + if let Some(local) = fc.find_local_info(name) { + if local.is_const { + return Err(JsError::SyntaxError(format!( + "Assignment to constant variable '{name}'" + ))); + } + let reg = local.reg; + let captured = local.is_captured; + if captured { + fc.builder.emit_reg_reg(Op::CellStore, reg, src); + } else if reg != src { + fc.builder.emit_reg_reg(Op::Move, reg, src); + } + } else if let Some(uv_idx) = fc.find_upvalue(name) { + if fc.is_upvalue_const(uv_idx) { + return Err(JsError::SyntaxError(format!( + "Assignment to constant variable '{name}'" + ))); } + fc.builder.emit_store_upvalue(uv_idx, src); } else { let ni = fc.builder.add_name(name); fc.builder.emit_store_global(ni, src); diff --git a/crates/js/src/vm.rs b/crates/js/src/vm.rs index b7f97e9..6b57873 100644 --- a/crates/js/src/vm.rs +++ b/crates/js/src/vm.rs @@ -12,10 +12,12 @@ use std::fmt; // ── Heap objects (GC-managed) ──────────────────────────────── -/// A GC-managed heap object: either a plain object or a function. +/// A GC-managed heap object: a plain object, a function, or a closure cell. pub enum HeapObject { Object(ObjectData), - Function(FunctionData), + Function(Box), + /// A mutable cell holding one Value — used for closure-captured variables. + Cell(Value), } impl Traceable for HeapObject { @@ -40,6 +42,14 @@ impl Traceable for HeapObject { visitor(r); } } + for &uv in &fdata.upvalues { + visitor(uv); + } + } + HeapObject::Cell(val) => { + if let Some(r) = val.gc_ref() { + visitor(r); + } } } } @@ -116,6 +126,8 @@ pub struct FunctionData { pub prototype_obj: Option, /// Arbitrary properties set on this function (functions are objects in JS). pub properties: HashMap, + /// Captured upvalue cells (GcRefs to HeapObject::Cell values). + pub upvalues: Vec, } #[derive(Clone)] @@ -631,6 +643,8 @@ struct CallFrame { return_reg: usize, /// Exception handler stack for this frame. exception_handlers: Vec, + /// Captured upvalue cells from the closure that created this call frame. + upvalues: Vec, } /// An exception handler entry (for try/catch). @@ -694,6 +708,7 @@ impl Vm { base: 0, return_reg: 0, exception_handlers: Vec::new(), + upvalues: Vec::new(), }); self.run() @@ -752,6 +767,11 @@ impl Vm { roots.push(r); } } + for frame in &self.frames { + for &uv in &frame.upvalues { + roots.push(uv); + } + } roots } @@ -1140,7 +1160,9 @@ impl Vm { match self.gc.get(func_gc_ref) { Some(HeapObject::Function(fdata)) => match &fdata.kind { FunctionKind::Native(n) => CallInfo::Native(n.callback), - FunctionKind::Bytecode(bc) => CallInfo::Bytecode(bc.func.clone()), + FunctionKind::Bytecode(bc) => { + CallInfo::Bytecode(bc.func.clone(), fdata.upvalues.clone()) + } }, _ => { let err = RuntimeError::type_error("not a function"); @@ -1165,7 +1187,7 @@ impl Vm { } } }, - CallInfo::Bytecode(callee_func) => { + CallInfo::Bytecode(callee_func, callee_upvalues) => { if self.frames.len() >= MAX_CALL_DEPTH { let err = RuntimeError::range_error("Maximum call stack size exceeded"); @@ -1195,6 +1217,7 @@ impl Vm { base: callee_base, return_reg: base + dst as usize, exception_handlers: Vec::new(), + upvalues: callee_upvalues, }); } } @@ -1231,14 +1254,39 @@ impl Vm { let base = self.frames[fi].base; let inner_func = self.frames[fi].func.functions[func_idx].clone(); let name = inner_func.name.clone(); + + // Resolve upvalues from the parent scope. + let mut upvalues = Vec::with_capacity(inner_func.upvalue_defs.len()); + for def in &inner_func.upvalue_defs { + let cell_ref = if def.is_local { + // Parent has a cell in register `def.index`. + match &self.registers[base + def.index as usize] { + Value::Object(r) => *r, + _ => { + return Err(RuntimeError { + kind: ErrorKind::Error, + message: + "CreateClosure: upvalue register does not hold a cell" + .into(), + }); + } + } + } else { + // Transitive: parent's own upvalue at `def.index`. + self.frames[fi].upvalues[def.index as usize] + }; + upvalues.push(cell_ref); + } + // Create a .prototype object for the function (for instanceof). let proto_obj = self.gc.alloc(HeapObject::Object(ObjectData::new())); - let gc_ref = self.gc.alloc(HeapObject::Function(FunctionData { + let gc_ref = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { name, kind: FunctionKind::Bytecode(BytecodeFunc { func: inner_func }), prototype_obj: Some(proto_obj), properties: HashMap::new(), - })); + upvalues, + }))); // Set .prototype.constructor = this function. if let Some(HeapObject::Object(data)) = self.gc.get_mut(proto_obj) { data.properties.insert( @@ -1512,6 +1560,77 @@ impl Vm { Op::PopExceptionHandler => { self.frames[fi].exception_handlers.pop(); } + + // ── Closure / upvalue ops ───────────────────────── + Op::NewCell => { + let dst = Self::read_u8(&mut self.frames[fi]); + let base = self.frames[fi].base; + let cell = self.gc.alloc(HeapObject::Cell(Value::Undefined)); + self.registers[base + dst as usize] = Value::Object(cell); + + if self.gc.should_collect() { + let roots = self.collect_roots(); + self.gc.collect(&roots); + } + } + Op::CellLoad => { + let dst = Self::read_u8(&mut self.frames[fi]); + let cell_reg = Self::read_u8(&mut self.frames[fi]); + let base = self.frames[fi].base; + let cell_ref = match &self.registers[base + cell_reg as usize] { + Value::Object(r) => *r, + _ => { + return Err(RuntimeError { + kind: ErrorKind::Error, + message: "CellLoad: register does not hold a cell".into(), + }); + } + }; + let val = match self.gc.get(cell_ref) { + Some(HeapObject::Cell(v)) => v.clone(), + _ => Value::Undefined, + }; + self.registers[base + dst as usize] = val; + } + Op::CellStore => { + let cell_reg = Self::read_u8(&mut self.frames[fi]); + let src = Self::read_u8(&mut self.frames[fi]); + let base = self.frames[fi].base; + let cell_ref = match &self.registers[base + cell_reg as usize] { + Value::Object(r) => *r, + _ => { + return Err(RuntimeError { + kind: ErrorKind::Error, + message: "CellStore: register does not hold a cell".into(), + }); + } + }; + let val = self.registers[base + src as usize].clone(); + if let Some(HeapObject::Cell(cell_val)) = self.gc.get_mut(cell_ref) { + *cell_val = val; + } + } + Op::LoadUpvalue => { + let dst = Self::read_u8(&mut self.frames[fi]); + let idx = Self::read_u8(&mut self.frames[fi]) as usize; + let base = self.frames[fi].base; + let cell_ref = self.frames[fi].upvalues[idx]; + let val = match self.gc.get(cell_ref) { + Some(HeapObject::Cell(v)) => v.clone(), + _ => Value::Undefined, + }; + self.registers[base + dst as usize] = val; + } + Op::StoreUpvalue => { + let idx = Self::read_u8(&mut self.frames[fi]) as usize; + let src = Self::read_u8(&mut self.frames[fi]); + let base = self.frames[fi].base; + let val = self.registers[base + src as usize].clone(); + let cell_ref = self.frames[fi].upvalues[idx]; + if let Some(HeapObject::Cell(cell_val)) = self.gc.get_mut(cell_ref) { + *cell_val = val; + } + } } } } @@ -1549,12 +1668,13 @@ impl Vm { name: &str, callback: fn(&[Value]) -> Result, ) { - let gc_ref = self.gc.alloc(HeapObject::Function(FunctionData { + let gc_ref = self.gc.alloc(HeapObject::Function(Box::new(FunctionData { name: name.to_string(), kind: FunctionKind::Native(NativeFunc { callback }), prototype_obj: None, properties: HashMap::new(), - })); + upvalues: Vec::new(), + }))); self.globals .insert(name.to_string(), Value::Function(gc_ref)); } @@ -1579,7 +1699,7 @@ impl Default for Vm { /// Internal enum to avoid holding a GC borrow across the call setup. enum CallInfo { Native(fn(&[Value]) -> Result), - Bytecode(Function), + Bytecode(Function, Vec), } // ── Tests ──────────────────────────────────────────────────── @@ -2400,14 +2520,15 @@ mod tests { // Create a constructor function with a .prototype object. let proto = gc.alloc(HeapObject::Object(ObjectData::new())); - let ctor = gc.alloc(HeapObject::Function(FunctionData { + let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData { name: "Foo".to_string(), kind: FunctionKind::Native(NativeFunc { callback: |_| Ok(Value::Undefined), }), prototype_obj: Some(proto), properties: HashMap::new(), - })); + upvalues: Vec::new(), + }))); // Create an object whose [[Prototype]] is the constructor's .prototype. let mut obj_data = ObjectData::new(); @@ -2449,4 +2570,184 @@ mod tests { v => panic!("expected true, got {v:?}"), } } + + // ── Closure tests ──────────────────────────────────────── + + #[test] + fn test_closure_basic() { + // Basic closure: inner function reads outer variable. + let src = r#" + function outer() { + var x = 10; + function inner() { + return x; + } + return inner(); + } + outer() + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 10.0), + v => panic!("expected 10, got {v:?}"), + } + } + + #[test] + fn test_closure_return_function() { + // Closure survives the outer function's return. + let src = r#" + function makeAdder(x) { + return function(y) { return x + y; }; + } + var add5 = makeAdder(5); + add5(3) + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 8.0), + v => panic!("expected 8, got {v:?}"), + } + } + + #[test] + fn test_closure_mutation() { + // Closures share live references — mutation is visible. + let src = r#" + function counter() { + var n = 0; + return function() { n = n + 1; return n; }; + } + var c = counter(); + c(); + c(); + c() + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 3.0), + v => panic!("expected 3, got {v:?}"), + } + } + + #[test] + fn test_closure_shared_variable() { + // Two closures from the same scope share the same variable. + let src = r#" + function make() { + var x = 0; + function inc() { x = x + 1; } + function get() { return x; } + inc(); + inc(); + return get(); + } + make() + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 2.0), + v => panic!("expected 2, got {v:?}"), + } + } + + #[test] + fn test_closure_arrow() { + // Arrow function captures outer variable. + let src = r#" + function outer() { + var x = 42; + var f = () => x; + return f(); + } + outer() + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 42.0), + v => panic!("expected 42, got {v:?}"), + } + } + + #[test] + fn test_closure_nested() { + // Transitive capture: grandchild function reads grandparent variable. + let src = r#" + function outer() { + var x = 100; + function middle() { + function inner() { + return x; + } + return inner(); + } + return middle(); + } + outer() + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 100.0), + v => panic!("expected 100, got {v:?}"), + } + } + + #[test] + fn test_closure_param_capture() { + // Closure captures a function parameter. + let src = r#" + function multiply(factor) { + return function(x) { return x * factor; }; + } + var double = multiply(2); + double(7) + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 14.0), + v => panic!("expected 14, got {v:?}"), + } + } + + // ── const tests ────────────────────────────────────────── + + #[test] + fn test_const_basic() { + let src = "const x = 42; x"; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 42.0), + v => panic!("expected 42, got {v:?}"), + } + } + + #[test] + fn test_const_reassignment_error() { + let src = "const x = 1; x = 2;"; + let program = crate::parser::Parser::parse(src).expect("parse ok"); + let result = crate::compiler::compile(&program); + assert!( + result.is_err(), + "const reassignment should be a compile error" + ); + } + + #[test] + fn test_const_missing_init_error() { + let src = "const x;"; + let program = crate::parser::Parser::parse(src).expect("parse ok"); + let result = crate::compiler::compile(&program); + assert!( + result.is_err(), + "const without initializer should be a compile error" + ); + } + + // ── this binding tests ─────────────────────────────────── + + #[test] + fn test_method_call_this() { + let src = r#" + var obj = {}; + obj.x = 10; + obj.getX = function() { return this.x; }; + obj.getX() + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 10.0), + v => panic!("expected 10, got {v:?}"), + } + } } -- 2.51.2