diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 690926e0..4c8d051e 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -17,17 +17,84 @@ pub(crate) fn is_int_literal(value: f64) -> bool { } pub(crate) fn is_side_effect_free(expr: &Expression) -> bool { - matches!( - expr, + match expr { Expression::NumericLiteral(_) - | Expression::StringLiteral(_) - | Expression::BooleanLiteral(_) - | Expression::NullLiteral(_) - | Expression::Identifier(_) - | Expression::RegExpLiteral(_) - ) + | Expression::StringLiteral(_) + | Expression::BooleanLiteral(_) + | Expression::NullLiteral(_) + | Expression::Identifier(_) + | Expression::RegExpLiteral(_) + | Expression::ThisExpression(_) => true, + Expression::ParenthesizedExpression(p) => is_side_effect_free(&p.expression), + Expression::BinaryExpression(bin) => is_side_effect_free(&bin.left) && is_side_effect_free(&bin.right), + Expression::UnaryExpression(un) => { + !matches!(un.operator, UnaryOperator::Delete) && is_side_effect_free(&un.argument) + } + _ => false, + } } +const BUILTIN_GLOBALS: &[&str] = &[ + "NaN", + "undefined", + "Infinity", + "globalThis", + "Object", + "Array", + "String", + "Number", + "Boolean", + "Function", + "Error", + "TypeError", + "ReferenceError", + "RangeError", + "SyntaxError", + "URIError", + "EvalError", + "Math", + "JSON", + "Promise", + "Date", + "Set", + "Map", + "RegExp", + "Symbol", + "parseInt", + "parseFloat", + "isNaN", + "isFinite", + "Proxy", + "WeakMap", + "WeakSet", + "WeakRef", + "FinalizationRegistry", + "Atomics", + "SharedArrayBuffer", + "ArrayBuffer", + "DataView", + "Iterator", + "BigInt", + "Int8Array", + "Uint8Array", + "Uint8ClampedArray", + "Int16Array", + "Uint16Array", + "Int32Array", + "Uint32Array", + "Float32Array", + "Float64Array", + "BigInt64Array", + "BigUint64Array", + "Reflect", + "escape", + "unescape", + "encodeURI", + "decodeURI", + "encodeURIComponent", + "decodeURIComponent", +]; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[allow(dead_code)] pub(crate) enum Label { @@ -122,9 +189,9 @@ impl CompileCtx { bytecode: Vec::new(), constants: Vec::new(), constant_map: HashMap::new(), - next_reg: 0, - max_regs: 0, - reserved_reg_start: 0, + next_reg: 1, + max_regs: 1, + reserved_reg_start: 1, symbols: SymbolTable::new(), label_map: HashMap::new(), loop_stack: Vec::new(), @@ -167,7 +234,7 @@ impl CompileCtx { } pub(crate) fn reset_regs(&mut self) { - self.next_reg = (self.builtin_reg_map.len() as u8).max(self.reserved_reg_start); + self.next_reg = self.builtin_reg_floor().max(self.reserved_reg_start); self.projected_pc = 0; self.label_counter = 0; } @@ -243,7 +310,23 @@ impl CompileCtx { self.symbols.lookup(name) } + pub(crate) fn lookup_or_builtin(&mut self, name: &str) -> Result { + match self.symbols.lookup(name) { + Ok(reg) => Ok(reg), + Err(err) if Self::is_known_builtin(name) && err.contains("is not defined") => { + let reg = self.alloc_reg(); + self.symbols.pre_register_global(name, reg); + self.builtin_reg_map.push((name.to_string(), reg)); + Ok(reg) + } + Err(err) => Err(err), + } + } + pub(crate) fn lookup_or_global(&mut self, name: &str) -> u8 { + if let Some(reg) = self.symbols.lookup_any(name) { + return reg; + } let reg = self.alloc_reg(); self.symbols.lookup_or_global(name, reg) } @@ -290,76 +373,22 @@ impl CompileCtx { self.builtin_reg_map.iter().any(|(n, _)| n == name) } + pub(crate) fn is_known_builtin(name: &str) -> bool { + BUILTIN_GLOBALS.contains(&name) + } + + fn builtin_reg_floor(&self) -> u8 { + self.builtin_reg_map + .iter() + .map(|(_, reg)| reg.saturating_add(1)) + .max() + .unwrap_or(0) + } + pub(crate) fn pre_register_builtins(&mut self) { - let builtins = [ - "NaN", - "undefined", - "Infinity", - "globalThis", - "Object", - "Array", - "String", - "Number", - "Boolean", - "Function", - "Error", - "TypeError", - "ReferenceError", - "RangeError", - "SyntaxError", - "URIError", - "EvalError", - "Math", - "JSON", - "Promise", - "Date", - "Set", - "Map", - "RegExp", - "Symbol", - "parseInt", - "parseFloat", - "isNaN", - "isFinite", - // Out-of-scope constructors: bound as TypeError-throwing stubs - "Proxy", - "WeakMap", - "WeakSet", - "WeakRef", - "FinalizationRegistry", - "Atomics", - "SharedArrayBuffer", - "ArrayBuffer", - "DataView", - "Iterator", - "BigInt", - // TypedArray family stubs - "Int8Array", - "Uint8Array", - "Uint8ClampedArray", - "Int16Array", - "Uint16Array", - "Int32Array", - "Uint32Array", - "Float32Array", - "Float64Array", - "BigInt64Array", - "BigUint64Array", - // Reflect namespace object - "Reflect", - // URI utility functions - "escape", - "unescape", - "encodeURI", - "decodeURI", - "encodeURIComponent", - "decodeURIComponent", - ]; - for name in &builtins { - let reg = self.alloc_reg(); - self.symbols.pre_register_global(name, reg); - self.builtin_reg_map.push((name.to_string(), reg)); - } + // Builtin globals are resolved lazily by lookup_or_builtin(). Keeping this + // hook preserves the compile pipeline without reserving ~60 registers in + // every module. } } @@ -456,7 +485,7 @@ impl Compiler { // Inherit parent's global scope entries so previously-declared function names // are visible from within the body. - let mut inherited_reg_start = ctx.builtin_reg_map.len() as u8; + let mut inherited_reg_start = 1u8.max(ctx.builtin_reg_floor()); for (name, binding) in &parent_ctx.symbols.scopes[0].bindings { ctx.symbols.scopes[0].bindings.insert( name.clone(), @@ -479,7 +508,7 @@ impl Compiler { ); inherited_reg_start = inherited_reg_start.max(reg.saturating_add(1)); } - ctx.reserved_reg_start = inherited_reg_start; + ctx.reserved_reg_start = inherited_reg_start.max(1); // Align next_reg with builtin count so both count and emit passes start at the // same register offset (params go after builtin slots). @@ -502,6 +531,7 @@ impl Compiler { self.count_statement(stmt, &mut ctx); } ctx.max_regs = ctx.max_regs.max(1); + ctx.reg_overflow = false; ctx.reset_regs(); // Emit pass - reallocate params (same order = same regs after reset) @@ -584,6 +614,7 @@ impl Compiler { self.count_statement(stmt, &mut ctx); } ctx.max_regs = ctx.max_regs.max(1); + ctx.reg_overflow = false; ctx.reset_regs(); // First sub-pass: emit FunctionDeclarations (hoisting) diff --git a/crates/oxide_compiler/src/counter.rs b/crates/oxide_compiler/src/counter.rs index f69e333c..1458a9ca 100644 --- a/crates/oxide_compiler/src/counter.rs +++ b/crates/oxide_compiler/src/counter.rs @@ -446,10 +446,13 @@ impl Compiler { pub(crate) fn count_expression(&self, expr: &Expression, ctx: &mut CompileCtx) { match expr { Expression::BinaryExpression(bin) => { + let checkpoint = ctx.reg_checkpoint(); self.count_expression(&bin.left, ctx); self.count_expression(&bin.right, ctx); - ctx.alloc_reg(); ctx.projected_pc += 1; // ADD/SUB/MUL/DIV/etc. + if is_side_effect_free(&bin.left) && is_side_effect_free(&bin.right) { + ctx.restore_reg_checkpoint(checkpoint.saturating_add(1)); + } } Expression::UnaryExpression(un) => { match un.operator { @@ -460,13 +463,11 @@ impl Compiler { } Expression::StaticMemberExpression(member) => { self.count_expression(&member.object, ctx); - ctx.alloc_reg(); ctx.projected_pc += 2; // DELETE_PROP_STATIC + ext word } Expression::ComputedMemberExpression(member) => { self.count_expression(&member.object, ctx); self.count_expression(&member.expression, ctx); - ctx.alloc_reg(); ctx.projected_pc += 1; // DELETE_PROP_DYNAMIC } _ => {} @@ -474,7 +475,6 @@ impl Compiler { } _ => { self.count_expression(&un.argument, ctx); - ctx.alloc_reg(); ctx.projected_pc += 1; // NEG/TYPEOF/VOID/NOT } } @@ -602,6 +602,7 @@ impl Compiler { Expression::ObjectExpression(obj) => { ctx.alloc_reg(); ctx.projected_pc += 1; // NEW_OBJECT + let prop_checkpoint = ctx.reg_checkpoint(); for prop in &obj.properties { if let oxide_parser::ObjectPropertyKind::ObjectProperty(p) = prop { if matches!(p.kind, oxide_parser::PropertyKind::Get | oxide_parser::PropertyKind::Set) { @@ -614,6 +615,7 @@ impl Compiler { self.count_expression(&p.value, ctx); ctx.projected_pc += 1; // SET_PROP } + ctx.restore_reg_checkpoint(prop_checkpoint); } } } @@ -713,12 +715,14 @@ impl Compiler { Expression::ArrayExpression(arr) => { ctx.alloc_reg(); ctx.projected_pc += 1; // NEW_ARRAY + let elem_checkpoint = ctx.reg_checkpoint(); for elem in &arr.elements { if let Some(e) = elem.as_expression() { self.count_expression(e, ctx); ctx.alloc_reg(); ctx.projected_pc += 1; // LOAD_CONST index ctx.projected_pc += 1; // SET_ELEM + ctx.restore_reg_checkpoint(elem_checkpoint); } } } @@ -751,6 +755,13 @@ impl Compiler { ctx.alloc_reg(); ctx.projected_pc += 1; // LOAD_VAR from reg 254 } + Expression::Identifier(ident) => { + if CompileCtx::is_known_builtin(ident.name.as_str()) { + let _ = ctx.lookup_or_builtin(ident.name.as_str()); + } + ctx.alloc_reg(); + ctx.projected_pc += 1; // LOAD_VAR + } Expression::UpdateExpression(update) => match &update.argument { SimpleAssignmentTarget::AssignmentTargetIdentifier(_) => { ctx.alloc_reg(); diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index e36e800f..22c3df20 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -147,7 +147,7 @@ impl Compiler { match key { PropertyKey::Identifier(ident) => { let name = ident.name.as_str(); - let var_reg = ctx.lookup(name)?; + let var_reg = ctx.lookup_or_builtin(name)?; let key_reg = ctx.alloc_reg(); ctx.emit(opcode::encode(OpCode::LOAD_VAR, key_reg, var_reg, 0)); Ok(key_reg) @@ -1097,6 +1097,7 @@ impl Compiler { Ok(r) } Expression::BinaryExpression(bin) => { + let checkpoint = ctx.reg_checkpoint(); let left = self.emit_expression(&bin.left, ctx)?; let right = self.emit_expression(&bin.right, ctx)?; let op = match bin.operator { @@ -1123,42 +1124,38 @@ impl Compiler { BinaryOperator::StrictInequality => OpCode::STRICT_NEQ, _ => return Err(format!("unsupported binary operator: {:?}", bin.operator)), }; - let r = ctx.alloc_reg(); - ctx.emit(opcode::encode(op, r, left, right)); - Ok(r) + ctx.emit(opcode::encode(op, left, left, right)); + if is_side_effect_free(&bin.left) && is_side_effect_free(&bin.right) { + ctx.restore_reg_checkpoint(checkpoint.saturating_add(1).max(left.saturating_add(1))); + } + Ok(left) } Expression::UnaryExpression(un) => { let arg = self.emit_expression(&un.argument, ctx)?; match un.operator { UnaryOperator::UnaryNegation => { - let r = ctx.alloc_reg(); - ctx.emit(opcode::encode(OpCode::NEG, r, arg, 0)); - Ok(r) + ctx.emit(opcode::encode(OpCode::NEG, arg, arg, 0)); + Ok(arg) } UnaryOperator::Typeof => { - let r = ctx.alloc_reg(); - ctx.emit(opcode::encode(OpCode::TYPEOF, r, arg, 0)); - Ok(r) + ctx.emit(opcode::encode(OpCode::TYPEOF, arg, arg, 0)); + Ok(arg) } UnaryOperator::Void => { - let r = ctx.alloc_reg(); - ctx.emit(opcode::encode(OpCode::VOID, r, arg, 0)); - Ok(r) + ctx.emit(opcode::encode(OpCode::VOID, arg, arg, 0)); + Ok(arg) } UnaryOperator::LogicalNot => { - let r = ctx.alloc_reg(); - ctx.emit(opcode::encode(OpCode::NOT, r, arg, 0)); - Ok(r) + ctx.emit(opcode::encode(OpCode::NOT, arg, arg, 0)); + Ok(arg) } UnaryOperator::BitwiseNot => { - let r = ctx.alloc_reg(); - ctx.emit(opcode::encode(OpCode::BIT_NOT, r, arg, 0)); - Ok(r) + ctx.emit(opcode::encode(OpCode::BIT_NOT, arg, arg, 0)); + Ok(arg) } UnaryOperator::UnaryPlus => { - let r = ctx.alloc_reg(); - ctx.emit(opcode::encode(OpCode::UNARY_PLUS, r, arg, 0)); - Ok(r) + ctx.emit(opcode::encode(OpCode::UNARY_PLUS, arg, arg, 0)); + Ok(arg) } UnaryOperator::Delete => match &un.argument { Expression::Identifier(_) => { @@ -1168,17 +1165,15 @@ impl Compiler { let obj_reg = self.emit_expression(&member.object, ctx)?; let prop_name = member.property.name.as_str(); let const_idx = ctx.add_constant(Constant::String(prop_name.to_string())); - let r = ctx.alloc_reg(); - ctx.emit(opcode::encode(OpCode::DELETE_PROP_STATIC, r, obj_reg, 0)); + ctx.emit(opcode::encode(OpCode::DELETE_PROP_STATIC, obj_reg, obj_reg, 0)); ctx.emit(const_idx as u32); - Ok(r) + Ok(obj_reg) } Expression::ComputedMemberExpression(member) => { let obj_reg = self.emit_expression(&member.object, ctx)?; let key_reg = self.emit_expression(&member.expression, ctx)?; - let r = ctx.alloc_reg(); - ctx.emit(opcode::encode(OpCode::DELETE_PROP_DYNAMIC, r, obj_reg, key_reg)); - Ok(r) + ctx.emit(opcode::encode(OpCode::DELETE_PROP_DYNAMIC, obj_reg, obj_reg, key_reg)); + Ok(obj_reg) } _ => Err("invalid delete target".into()), }, @@ -1298,6 +1293,7 @@ impl Compiler { Expression::ObjectExpression(obj) => { let obj_reg = ctx.alloc_reg(); ctx.emit(opcode::encode(OpCode::NEW_OBJECT, obj_reg, 0, 0)); + let prop_checkpoint = ctx.reg_checkpoint(); for prop in &obj.properties { let oxide_parser::ObjectPropertyKind::ObjectProperty(p) = prop else { return Err("spread properties not yet supported".into()); @@ -1321,6 +1317,7 @@ impl Compiler { let idx = ctx.add_constant(Constant::String(prop_name.to_string())); ctx.emit(opcode::encode(OpCode::DEFINE_ACCESSOR, obj_reg, get_reg, set_reg)); ctx.emit(idx as u32); + ctx.restore_reg_checkpoint(prop_checkpoint); } else { let idx = ctx.add_constant(Constant::String(prop_name.to_string())); let key_reg = ctx.alloc_reg(); @@ -1334,6 +1331,7 @@ impl Compiler { } } ctx.emit(opcode::encode(OpCode::SET_PROP, obj_reg, val_reg, key_reg)); + ctx.restore_reg_checkpoint(prop_checkpoint); } } Ok(obj_reg) @@ -1342,6 +1340,7 @@ impl Compiler { let arr_reg = ctx.alloc_reg(); let n = arr.elements.len() as u16; ctx.emit(opcode::encode(OpCode::NEW_ARRAY, arr_reg, (n & 0xFF) as u8, ((n >> 8) & 0xFF) as u8)); + let elem_checkpoint = ctx.reg_checkpoint(); for (i, elem) in arr.elements.iter().enumerate() { let Some(e) = elem.as_expression() else { return Err("spread not supported".into()); @@ -1351,6 +1350,7 @@ impl Compiler { let idx = ctx.add_constant(Constant::Int(i as i32)); ctx.emit_load_const(idx_reg, idx); ctx.emit(opcode::encode(OpCode::SET_ELEM, arr_reg, idx_reg, val_reg)); + ctx.restore_reg_checkpoint(elem_checkpoint); } Ok(arr_reg) } @@ -1509,7 +1509,7 @@ impl Compiler { _ => Err("member update not yet supported".into()), }, Expression::Identifier(ident) => { - let var_reg = ctx.lookup(ident.name.as_str())?; + let var_reg = ctx.lookup_or_builtin(ident.name.as_str())?; 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/symbol_table.rs b/crates/oxide_compiler/src/symbol_table.rs index 1b5ecb17..d1618f75 100644 --- a/crates/oxide_compiler/src/symbol_table.rs +++ b/crates/oxide_compiler/src/symbol_table.rs @@ -101,6 +101,13 @@ impl SymbolTable { Err(format!("Identifier '{name}' is not defined")) } + pub fn lookup_any(&self, name: &str) -> Option { + self.scopes + .iter() + .rev() + .find_map(|scope| scope.bindings.get(name).map(|binding| binding.reg)) + } + 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) { diff --git a/crates/oxide_compiler/tests/compile_expr_tests.rs b/crates/oxide_compiler/tests/compile_expr_tests.rs index 7746e9bd..5e9a7df6 100644 --- a/crates/oxide_compiler/tests/compile_expr_tests.rs +++ b/crates/oxide_compiler/tests/compile_expr_tests.rs @@ -444,3 +444,20 @@ fn compile_class_expression_default_constructor() { "expected at least one class constructor submodule" ); } + +#[test] +fn compile_large_pure_expression_reuses_temp_registers() { + let expr = std::iter::repeat("1").take(120).collect::>().join(" + "); + let module = compile_source(&expr); + assert!( + module.n_registers < 16, + "pure expression should reuse temp registers, got {}", + module.n_registers + ); +} + +#[test] +fn compile_builtin_globals_are_registered_lazily() { + let module = compile_source("Object; Array; Math; JSON"); + assert_eq!(module.builtin_reg_map.len(), 4, "only referenced builtins should allocate registers"); +} diff --git a/crates/oxide_vm/tests/regression_tests.rs b/crates/oxide_vm/tests/regression_tests.rs index 9b373180..05158ea0 100644 --- a/crates/oxide_vm/tests/regression_tests.rs +++ b/crates/oxide_vm/tests/regression_tests.rs @@ -151,3 +151,22 @@ fn regression_delete_dynamic_property_throws_type_error() { "vm error: uncaught TypeError: property deletion not supported" ); } + +#[test] +fn regression_large_expression_no_register_overflow() { + let expr = std::iter::repeat("1").take(120).collect::>().join(" + "); + let source = format!("function f() {{ return {expr}; }} f()"); + assert_eq!(eval(&source), "120"); +} + +#[test] +fn regression_many_declarations_keep_stable_registers() { + let decls = (0..120).map(|i| format!("var v{i} = {i};")).collect::>().join(""); + let source = format!("{decls} v0 + v57 + v119"); + assert_eq!(eval(&source), "176"); +} + +#[test] +fn regression_method_call_receiver_survives_register_reuse() { + assert_eq!(eval("var o = { x: 41, f: function() { return this.x + 1; } }; o.f()"), "42"); +}