diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index bb957a94..04031801 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use crate::module::CompiledModule; use crate::opcode::{self, OpCode}; -use crate::symbol_table::SymbolTable; +use crate::symbol_table::{Binding, SymbolTable}; pub use crate::hash::structural_hash; pub use crate::module::Constant; @@ -58,6 +58,7 @@ pub(crate) struct CompileCtx { pub(crate) label_counter: u32, pub(crate) projected_pc: usize, pub(crate) builtin_reg_map: Vec<(String, u8)>, + pub(crate) sub_modules: Vec, } impl CompileCtx { @@ -74,6 +75,7 @@ impl CompileCtx { label_counter: 0, projected_pc: 0, builtin_reg_map: Vec::new(), + sub_modules: Vec::new(), } } @@ -124,6 +126,10 @@ impl CompileCtx { self.symbols.declare(name, reg) } + pub(crate) fn declare_initialized(&mut self, name: &str, reg: u8) -> Result<(), String> { + self.symbols.declare_initialized(name, reg) + } + pub(crate) fn lookup(&self, name: &str) -> Result { self.symbols.lookup(name) } @@ -203,6 +209,84 @@ impl Compiler { Self } + /// Compile a function body (used for FD and FE). + /// This performs both counting and emitting in one pass. + pub(crate) fn compile_function_body<'a>( + &self, + param_names: &[String], + body_stmts: &[Statement<'a>], + parent_ctx: &CompileCtx, + ) -> Result { + let mut ctx = CompileCtx::new(); + + // Inherit parent's builtin_reg_map so builtin identifiers (Math, Object, etc.) + // resolve to the correct pre-allocated registers in the sub-module's register file. + ctx.builtin_reg_map = parent_ctx.builtin_reg_map.clone(); + + // Inherit parent's global scope entries so previously-declared function names + // are visible from within the body. + for (name, binding) in &parent_ctx.symbols.scopes[0] { + ctx.symbols.scopes[0].insert( + name.clone(), + Binding { + reg: binding.reg, + initialized: binding.initialized, + }, + ); + } + + // Align next_reg with builtin count so both count and emit passes start at the + // same register offset (params go after builtin slots). + ctx.reset_regs(); + + // Function body scope - params and local vars + ctx.push_scope(); + + // Register parameters as initialized. + for name in param_names { + let reg = ctx.alloc_reg(); + ctx.declare_initialized(name, reg)?; + } + + // Count pass + for stmt in body_stmts { + self.count_statement(stmt, &mut ctx); + } + ctx.max_regs = ctx.max_regs.max(1); + ctx.reset_regs(); + + // Emit pass - reallocate params (same order = same regs after reset) + for name in param_names { + let reg = ctx.alloc_reg(); + ctx.declare_initialized(name, reg)?; + } + + // Emit body statements. + // After all statements, emit an implicit RETURN undefined. + for stmt in body_stmts { + let _ = self.emit_statement(stmt, &mut ctx)?; + } + + let undef_idx = ctx.add_constant(Constant::Undefined); + let undef_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode( + OpCode::LOAD_CONST, + undef_reg, + (undef_idx & 0xFF) as u8, + ((undef_idx >> 8) & 0xFF) as u8, + )); + ctx.emit(opcode::encode(OpCode::RETURN, undef_reg, 0, 0)); + + Ok(CompiledModule { + bytecode: ctx.bytecode, + constants: ctx.constants, + n_registers: ctx.max_regs, + n_args: param_names.len() as u8, + builtin_reg_map: ctx.builtin_reg_map, + sub_modules: ctx.sub_modules, + }) + } + pub fn compile(&self, program: &oxide_parser::Program) -> Result { let mut ctx = CompileCtx::new(); ctx.pre_register_builtins(); @@ -213,8 +297,20 @@ impl Compiler { ctx.max_regs = ctx.max_regs.max(1); ctx.reset_regs(); + // First sub-pass: emit FunctionDeclarations (hoisting) + // This ensures function objects are available before any code runs. + for stmt in &program.body { + if matches!(stmt, Statement::FunctionDeclaration(_)) { + self.emit_statement(stmt, &mut ctx)?; + } + } + + // Second sub-pass: emit all other statements let mut last_result: Option = None; for stmt in &program.body { + if matches!(stmt, Statement::FunctionDeclaration(_)) { + continue; // Already emitted above + } match self.emit_statement(stmt, &mut ctx)? { Some(r) => last_result = Some(r), None => last_result = None, @@ -238,8 +334,9 @@ impl Compiler { bytecode: ctx.bytecode, constants: ctx.constants, n_registers: ctx.max_regs, + n_args: 0, builtin_reg_map: ctx.builtin_reg_map, - sub_modules: Vec::new(), + sub_modules: ctx.sub_modules, }) } } diff --git a/crates/oxide_compiler/src/counter.rs b/crates/oxide_compiler/src/counter.rs index 8e328151..220b7af6 100644 --- a/crates/oxide_compiler/src/counter.rs +++ b/crates/oxide_compiler/src/counter.rs @@ -180,6 +180,21 @@ impl Compiler { self.count_statement(s, ctx); } } + Statement::FunctionDeclaration(fd) => { + let name = if let Some(id) = &fd.id { + id.name.to_string() + } else { + return; + }; + + // Hoisting: declare function name as initialized + let func_reg = ctx.alloc_reg(); + let _ = ctx.declare_initialized(&name, func_reg); + + // Body is compiled in the emit pass only. + // FD emits LOAD_CONST(BytecodeFunc) + STORE_VAR + ctx.projected_pc += 2; + } _ => {} } } @@ -217,7 +232,21 @@ impl Compiler { self.count_expression(expr, ctx); } } - ctx.projected_pc += 2; + // CALL_NATIVE: 1 opcode + 1 ext word; CALL(0x40): 1 opcode + let is_builtin = match &call.callee { + Expression::Identifier(ident) => ctx.is_builtin(ident.name.as_str()), + Expression::StaticMemberExpression(m) => { + if let Expression::Identifier(ident) = &m.object { + ctx.is_builtin(ident.name.as_str()) + } else { + false + } + } + _ => false, + }; + ctx.projected_pc += if is_builtin { 2 } else { 1 }; + ctx.alloc_reg(); // result register + ctx.projected_pc += 1; // LOAD_VAR result <- regs[0] } Expression::AssignmentExpression(assign) => { if let oxide_parser::AssignmentTarget::StaticMemberExpression(member) = &assign.left @@ -304,6 +333,24 @@ impl Compiler { } } } + Expression::FunctionExpression(_fe) => { + // No hoisting - function created at expression position. + // Body is compiled in the emit pass only. + ctx.alloc_reg(); + ctx.projected_pc += 1; // LOAD_CONST + } + Expression::NewExpression(ne) => { + // Count callee expression + self.count_expression(&ne.callee, ctx); + // Count arguments + for arg in &ne.arguments { + if let Some(expr) = arg.as_expression() { + self.count_expression(expr, ctx); + } + } + ctx.alloc_reg(); // result register + ctx.projected_pc += 2; // NEW_EXPRESSION + ext word + } Expression::ArrayExpression(arr) => { ctx.alloc_reg(); ctx.projected_pc += 1; // NEW_ARRAY diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index 80cfe359..2a7b4e91 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -326,6 +326,51 @@ impl Compiler { ctx.emit(opcode::encode_jmp(offset as i16)); Ok(None) } + Statement::FunctionDeclaration(fd) => { + // FunctionDeclaration: emit LOAD_CONST(BytecodeFunc) + STORE_VAR + let name = if let Some(id) = &fd.id { + id.name.to_string() + } else { + return Err("FunctionDeclaration without name".into()); + }; + + // Extract params + let mut param_names = Vec::new(); + for param in &fd.params.items { + if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern { + param_names.push(bi.name.to_string()); + } + } + + // Extract body statements (pass by reference) + let body_stmts: &[Statement] = if let Some(body) = &fd.body { + &body.statements + } else { + &[] + }; + + // Compile body into sub-module + let sub_module = self.compile_function_body(¶m_names, body_stmts, ctx)?; + ctx.sub_modules.push(sub_module); + // 1-indexed: 0 = no sub_module (sentinel) + let sub_idx = ctx.sub_modules.len() as u32; + + // Add BytecodeFunc constant and emit LOAD_CONST + STORE_VAR + let const_idx = ctx.add_constant(Constant::BytecodeFunc(sub_idx)); + let func_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode( + OpCode::LOAD_CONST, + func_reg, + (const_idx & 0xFF) as u8, + ((const_idx >> 8) & 0xFF) as u8, + )); + + // Get the register that was allocated for this FD in the counter pass + let var_reg = ctx.lookup_or_global(&name); + ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, func_reg, 0)); + + Ok(None) + } _ => Ok(None), } } @@ -745,6 +790,59 @@ impl Compiler { ctx.emit(opcode::encode(OpCode::LOAD_VAR, r, var_reg, 0)); Ok(r) } + Expression::FunctionExpression(fe) => { + // FunctionExpression: compile body, emit LOAD_CONST(BytecodeFunc) + let mut param_names = Vec::new(); + for param in &fe.params.items { + if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern { + param_names.push(bi.name.to_string()); + } + } + + let body_stmts: &[Statement] = if let Some(body) = &fe.body { + &body.statements + } else { + &[] + }; + + let sub_module = self.compile_function_body(¶m_names, body_stmts, ctx)?; + ctx.sub_modules.push(sub_module); + // 1-indexed: 0 = no sub_module (sentinel) + let sub_idx = ctx.sub_modules.len() as u32; + + let const_idx = ctx.add_constant(Constant::BytecodeFunc(sub_idx)); + let r = ctx.alloc_reg(); + ctx.emit(opcode::encode( + OpCode::LOAD_CONST, + r, + (const_idx & 0xFF) as u8, + ((const_idx >> 8) & 0xFF) as u8, + )); + Ok(r) + } + Expression::NewExpression(ne) => { + let constructor_reg = self.emit_expression(&ne.callee, ctx)?; + let mut arg_regs = Vec::new(); + for arg in &ne.arguments { + if let Some(expr) = arg.as_expression() { + arg_regs.push(self.emit_expression(expr, ctx)?); + } + } + let first_arg_reg = if arg_regs.is_empty() { + 0u8 + } else { + arg_regs[0] + }; + let r = ctx.alloc_reg(); + ctx.emit(opcode::encode( + OpCode::NEW_EXPRESSION, + r, + constructor_reg, + first_arg_reg, + )); + ctx.emit(arg_regs.len() as u32); + Ok(r) + } Expression::ParenthesizedExpression(p) => self.emit_expression(&p.expression, ctx), Expression::CallExpression(call) => { let (callee_reg, this_reg) = match &call.callee { @@ -795,11 +893,28 @@ impl Compiler { Expression::Identifier(ident) if ctx.is_builtin(ident.name.as_str()) => { OpCode::CALL_NATIVE } + Expression::StaticMemberExpression(m) => { + if let Expression::Identifier(ident) = &m.object { + if ctx.is_builtin(ident.name.as_str()) { + OpCode::CALL_NATIVE + } else { + OpCode::CALL + } + } else { + OpCode::CALL + } + } _ => OpCode::CALL, }; ctx.emit(opcode::encode(op, callee_reg, this_reg, first_arg_reg)); - ctx.emit(arg_regs.len() as u32); - Ok(0u8) + if matches!(op, OpCode::CALL_NATIVE) { + ctx.emit(arg_regs.len() as u32); + } + // Copy result from regs[0] into a dedicated register so multiple + // call expressions don't overwrite each other. + let result_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::LOAD_VAR, result_reg, 0, 0)); + Ok(result_reg) } _ => Err(format!("unsupported expression type: {:?}", expr)), } diff --git a/crates/oxide_compiler/src/hash.rs b/crates/oxide_compiler/src/hash.rs index c75fcfd6..71d1d6c4 100644 --- a/crates/oxide_compiler/src/hash.rs +++ b/crates/oxide_compiler/src/hash.rs @@ -104,6 +104,18 @@ fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher) { } } } + Statement::FunctionDeclaration(fd) => { + 12u8.hash(h); + if let Some(id) = &fd.id { + id.name.as_str().hash(h); + } + (fd.params.items.len() as u32).hash(h); + if let Some(body) = &fd.body { + for s in &body.statements { + hash_statement(s, h); + } + } + } _ => { std::mem::discriminant(stmt).hash(h); } @@ -187,6 +199,25 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { _ => {} } } + Expression::FunctionExpression(fe) => { + 13u8.hash(h); + (fe.params.items.len() as u32).hash(h); + if let Some(body) = &fe.body { + for s in &body.statements { + hash_statement(s, h); + } + } + } + Expression::NewExpression(ne) => { + 14u8.hash(h); + hash_expression(&ne.callee, h); + (ne.arguments.len() as u32).hash(h); + for arg in &ne.arguments { + if let Some(expr) = arg.as_expression() { + hash_expression(expr, h); + } + } + } _ => { std::mem::discriminant(expr).hash(h); } diff --git a/crates/oxide_compiler/src/module.rs b/crates/oxide_compiler/src/module.rs index c6d80b24..f3be48b9 100644 --- a/crates/oxide_compiler/src/module.rs +++ b/crates/oxide_compiler/src/module.rs @@ -17,16 +17,37 @@ pub struct CompiledModule { pub bytecode: Vec, pub constants: Vec, pub n_registers: u8, + pub n_args: u8, pub builtin_reg_map: Vec<(String, u8)>, pub sub_modules: Vec, } +impl CompiledModule { + pub fn new() -> Self { + Self { + bytecode: Vec::new(), + constants: Vec::new(), + n_registers: 0, + n_args: 0, + builtin_reg_map: Vec::new(), + sub_modules: Vec::new(), + } + } +} + +impl Default for CompiledModule { + fn default() -> Self { + Self::new() + } +} + impl Clone for CompiledModule { fn clone(&self) -> Self { Self { bytecode: self.bytecode.clone(), constants: self.constants.clone(), n_registers: self.n_registers, + n_args: self.n_args, builtin_reg_map: self.builtin_reg_map.clone(), sub_modules: self.sub_modules.clone(), } diff --git a/crates/oxide_compiler/src/symbol_table.rs b/crates/oxide_compiler/src/symbol_table.rs index a9458d7f..d5ff7872 100644 --- a/crates/oxide_compiler/src/symbol_table.rs +++ b/crates/oxide_compiler/src/symbol_table.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; -struct Binding { - reg: u8, - initialized: bool, +pub(crate) struct Binding { + pub(crate) reg: u8, + pub(crate) initialized: bool, } pub struct SymbolTable { - scopes: Vec>, + pub(crate) scopes: Vec>, } impl Default for SymbolTable { @@ -73,6 +73,24 @@ impl SymbolTable { reg_for_new } + /// Declare a variable that is already initialized (for FunctionDeclaration hoisting). + /// If the name already exists in the current scope, just mark it as initialized. + pub fn declare_initialized(&mut self, name: &str, reg: u8) -> Result<(), String> { + let current = self.scopes.last_mut().unwrap(); + if let Some(b) = current.get_mut(name) { + b.initialized = true; + return Ok(()); + } + current.insert( + name.to_string(), + Binding { + reg, + initialized: true, + }, + ); + Ok(()) + } + pub fn pre_register_global(&mut self, name: &str, reg: u8) { self.scopes[0].entry(name.to_string()).or_insert(Binding { reg, diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 8331b79b..94e511ac 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -2,7 +2,7 @@ use crate::value::JsValue; pub type ShapeId = u32; -/// Layout (40B): +/// Layout (48B): /// header: u32 bits /// [0:23] shape_id /// [29] is_array @@ -13,6 +13,7 @@ pub type ShapeId = u32; /// proto: JsValue (8 bytes) /// generation: u32 (4 bytes + 4 pad) /// native_fn: u64 (8 bytes, 0 = None sentinel) +/// sub_module_index: u32 (4 bytes + 4 pad, index into CompiledModule.sub_modules) #[repr(C)] pub struct JsObject { header: u32, @@ -23,6 +24,8 @@ pub struct JsObject { generation: u32, _pad2: [u8; 4], native_fn: u64, + sub_module_index: u32, + _pad3: [u8; 4], } impl JsObject { @@ -36,6 +39,8 @@ impl JsObject { generation: 1, _pad2: [0; 4], native_fn: 0, + sub_module_index: 0, + _pad3: [0; 4], } } @@ -54,6 +59,8 @@ impl JsObject { generation: 1, _pad2: [0; 4], native_fn: 0, + sub_module_index: 0, + _pad3: [0; 4], }; let vec = Box::new(vec![Box::new(JsValue::undefined()); n_elements]); obj.hash_props = Box::into_raw(vec) as *mut u8; @@ -246,6 +253,14 @@ impl JsObject { pub fn set_native_arg_count(&mut self, n: u8) { self.native_arg_count = n; } + + pub fn sub_module_index(&self) -> u32 { + self.sub_module_index + } + + pub fn set_sub_module_index(&mut self, idx: u32) { + self.sub_module_index = idx; + } } #[cfg(test)] @@ -257,8 +272,8 @@ mod tests { fn object_size_bounds() { let sz = std::mem::size_of::(); assert!( - sz >= 32 && sz <= 64, - "JsObject layout mismatch - expected 32-64B, got {sz}B" + sz >= 40 && sz <= 56, + "JsObject layout mismatch - expected 40-56B, got {sz}B" ); } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index f8a45370..c0b01ea9 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -63,6 +63,10 @@ pub struct Vm { pub math_rng_state: u64, sub_modules: Vec, sub_module_constants: Vec>, + /// Stack of saved bytecode for nested bytecode calls + saved_bytecode_stack: Vec>, + /// Stack of saved constants for nested bytecode calls + saved_constants_stack: Vec>, } fn bind_object(kernel: &Arc, global: &mut JsObject) { @@ -87,6 +91,16 @@ fn bind_object(kernel: &Arc, global: &mut JsObject) { ); global.set_shape_id(obj_shape); global.push_prop(obj_val); + + // Set native constructor on Object + { + let obj_ctor_ptr = kernel.builtin_world().object_constructor.as_ptr() as *mut JsObject; + let obj_ctor = unsafe { &mut *obj_ctor_ptr }; + obj_ctor.set_native_fn(Some( + crate::builtins::object::object_constructor as *const (), + )); + obj_ctor.set_native_arg_count(1); + } } fn bind_array(kernel: &Arc, global: &mut JsObject) { @@ -124,6 +138,14 @@ fn bind_array(kernel: &Arc, global: &mut JsObject) { global.set_shape_id(arr_shape); global.ensure_hash_props().push(Box::new(arr_val)); global.bump_generation(); + + // Set native constructor on Array + { + let arr_ctor_ptr = kernel.builtin_world().array_constructor.as_ptr() as *mut JsObject; + let arr_ctor = unsafe { &mut *arr_ctor_ptr }; + arr_ctor.set_native_fn(Some(crate::builtins::array::array_constructor as *const ())); + arr_ctor.set_native_arg_count(1); + } } fn bind_error(kernel: &Arc, global: &mut JsObject) { @@ -636,6 +658,8 @@ impl Vm { math_rng_state: 0, sub_modules: Vec::new(), sub_module_constants: Vec::new(), + saved_bytecode_stack: Vec::new(), + saved_constants_stack: Vec::new(), } } @@ -654,6 +678,8 @@ impl Vm { math_rng_state: 0, sub_modules: Vec::new(), sub_module_constants: Vec::new(), + saved_bytecode_stack: Vec::new(), + saved_constants_stack: Vec::new(), } } @@ -715,6 +741,34 @@ impl Vm { JsValue::string(idx, hash) } + /// Create a function JsObject for a BytecodeFunc constant. + fn create_function_object(&mut self, sub_idx: u32) -> JsValue { + let func_proto_ptr = self.kernel.builtin_world().function_proto.as_ptr() as *mut JsObject; + let proto_val = JsValue::from_js_object(func_proto_ptr); + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto_val); + obj.set_function(true); + obj.set_sub_module_index(sub_idx); + let obj_ptr = self.epoch.alloc(obj); + JsValue::object(obj_ptr as *mut u8) + } + + /// Convert a CompiledModule's constants into JsValue vec. + fn convert_constants(&mut self, module: &CompiledModule) -> Vec { + module + .constants + .iter() + .map(|c| match c { + Constant::Number(v) => JsValue::float(*v), + Constant::Int(v) => JsValue::int(*v), + Constant::String(s) => self.intern(s), + Constant::Boolean(b) => JsValue::bool(*b), + Constant::Null => JsValue::null(), + Constant::Undefined => JsValue::undefined(), + Constant::BytecodeFunc(idx) => self.create_function_object(*idx), + }) + .collect() + } + pub fn lookup_str(&self, val: JsValue) -> Option { if !val.is_string() { return None; @@ -814,25 +868,15 @@ impl Vm { } pub fn run(&mut self, module: &CompiledModule) -> Result { - self.constants = module - .constants - .iter() - .map(|c| match c { - Constant::Number(v) => JsValue::float(*v), - Constant::Int(v) => JsValue::int(*v), - Constant::String(s) => self.intern(s), - Constant::Boolean(b) => JsValue::bool(*b), - Constant::Null => JsValue::null(), - Constant::Undefined => JsValue::undefined(), - Constant::BytecodeFunc(_idx) => JsValue::undefined(), - }) - .collect(); + self.constants = self.convert_constants(module); self.sub_modules = module.sub_modules.clone(); self.sub_module_constants = vec![Vec::new(); self.sub_modules.len()]; self.bytecode = module.bytecode.clone(); self.pc = 0; self.regs = [JsValue::undefined(); 256]; self.frames.clear(); + self.saved_bytecode_stack.clear(); + self.saved_constants_stack.clear(); for (name, reg) in &module.builtin_reg_map { let si = self.kernel.string_forge().intern(name.as_str()).0; @@ -1048,35 +1092,111 @@ impl Vm { let obj_ptr = callee.as_js_object_ptr(); if !obj_ptr.is_null() { let obj = unsafe { &*obj_ptr }; - if obj.is_function() && obj.native_fn().is_some() { - let ext = self.bytecode[self.pc]; - self.pc += 1; - let arg_count = (ext & 0xFF) as usize; + if obj.is_function() { + if obj.native_fn().is_some() { + // Native call - use extension word for arg_count + let ext = self.bytecode[self.pc]; + self.pc += 1; + let arg_count = (ext & 0xFF) as usize; - let mut args_buf = [0u8; 257]; - args_buf[0] = this_reg; - for i in 0..arg_count.min(256) { - args_buf[i + 1] = first_arg_reg.wrapping_add(i as u8); - } - let args_slice = &args_buf[..arg_count + 1]; - - let func: NativeFn = - unsafe { std::mem::transmute(obj.native_fn().unwrap()) }; - self.regs[254] = callee; - match func(self, args_slice) { - Ok(val) => self.regs[0] = val, - Err(err_val) => { - return Err(format!("Native error: {:?}", err_val)); + let mut args_buf = [0u8; 257]; + args_buf[0] = this_reg; + for i in 0..arg_count.min(256) { + args_buf[i + 1] = first_arg_reg.wrapping_add(i as u8); } + let args_slice = &args_buf[..arg_count + 1]; + + let func: NativeFn = + unsafe { std::mem::transmute(obj.native_fn().unwrap()) }; + self.regs[254] = callee; + match func(self, args_slice) { + Ok(val) => self.regs[0] = val, + Err(err_val) => { + return Err(format!("Native error: {:?}", err_val)); + } + } + continue; + } else if obj.sub_module_index() > 0 { + // 1-indexed sub_module_index (0 = not a bytecode function) + let sub_idx = obj.sub_module_index() as usize - 1; + if sub_idx >= self.sub_modules.len() { + return Err(format!( + "CALL: sub_module_index {} out of bounds (max {})", + sub_idx, + self.sub_modules.len() + )); + } + + // Clone sub_module data before mutably borrowing self + let sub_bytecode = self.sub_modules[sub_idx].bytecode.clone(); + let sub_n_args = self.sub_modules[sub_idx].n_args as usize; + let sub_constants = self.sub_modules[sub_idx].constants.clone(); + let sub_builtin_count = + self.sub_modules[sub_idx].builtin_reg_map.len(); + + // Copy args to regs[sub_builtin_count..sub_builtin_count+n_args] + // (builtins occupy regs[0..sub_builtin_count-1]) + for i in 0..sub_n_args { + let src_reg = first_arg_reg.wrapping_add(i as u8) as usize; + self.regs[sub_builtin_count + i] = self.regs[src_reg]; + } + // Set this (reg 254 convention) + self.regs[254] = callee; + + // Save current bytecode/constants + self.saved_bytecode_stack + .push(std::mem::take(&mut self.bytecode)); + self.saved_constants_stack + .push(std::mem::take(&mut self.constants)); + + // Push call frame + self.frames.push(CallFrame { + return_addr: self.pc, + n_locals: sub_n_args as u8, + n_args: sub_n_args as u8, + function_obj_reg: callee_reg as u8, + frame_base: sub_builtin_count as u8, + }); + + // Convert sub_module constants + self.bytecode = sub_bytecode; + self.constants = sub_constants + .iter() + .map(|c| match c { + Constant::Number(v) => JsValue::float(*v), + Constant::Int(v) => JsValue::int(*v), + Constant::String(s) => self.intern(s), + Constant::Boolean(b) => JsValue::bool(*b), + Constant::Null => JsValue::null(), + Constant::Undefined => JsValue::undefined(), + Constant::BytecodeFunc(idx) => { + self.create_function_object(*idx) + } + }) + .collect(); + + // Pre-fill builtin registers from the global object + // (same as Vm::run does for the main module). + for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map { + let si = self.kernel.string_forge().intern(name.as_str()).0; + let global = self.kernel.global_object(); + if let Some(pos) = self + .kernel + .shape_forge() + .lookup_position(global.shape_id(), si) + { + self.regs[*reg as usize] = global.get_prop_at(pos); + } + } + + self.pc = 0; + continue; } - continue; } - // bytecode path skeleton - wired in plan 12.1-03 - return Err("bytecode function calls not yet supported".into()); } } - return Err("CALL target is not a native function".into()); + return Err("CALL target is not callable".into()); } OpCode::CALL_NATIVE => { @@ -1161,11 +1281,10 @@ impl Vm { // If constructor has native_fn, call it with this=new_obj if ctor_obj.native_fn().is_some() { let new_obj_val = JsValue::object(new_obj as *mut u8); - // this_reg = register holding new_obj; we use regs[255] as scratch self.regs[255] = new_obj_val; let mut args_buf = [0u8; 257]; - args_buf[0] = 255u8; // this = new_obj + args_buf[0] = 255u8; for i in 0..arg_count.min(256) { args_buf[i + 1] = first_arg_reg.wrapping_add(i as u8); } @@ -1176,11 +1295,10 @@ impl Vm { self.regs[254] = constructor; match func(self, args_slice) { Ok(val) => { - // If constructor returns an object, use that; else use new_obj if val.is_object() { - self.regs[0] = val; + self.regs[rd] = val; } else { - self.regs[0] = new_obj_val; + self.regs[rd] = new_obj_val; } } Err(err_val) => { @@ -1198,10 +1316,14 @@ impl Vm { OpCode::RETURN => { let result = self.regs[rd]; if let Some(frame) = self.frames.pop() { + // Restore caller's saved bytecode and constants + if let Some(saved_bc) = self.saved_bytecode_stack.pop() { + self.bytecode = saved_bc; + } + if let Some(saved_consts) = self.saved_constants_stack.pop() { + self.constants = saved_consts; + } // Restore caller's pc and merge result into caller's regs[0]. - // frame.frame_base marks the first register of the caller frame; - // when bytecode calls are wired in 12.1-03, registers above - // frame_base will be cleared on RETURN. self.pc = frame.return_addr; self.regs[0] = result; } else { diff --git a/crates/oxide_vm/tests/function_tests.rs b/crates/oxide_vm/tests/function_tests.rs new file mode 100644 index 00000000..78ce240b --- /dev/null +++ b/crates/oxide_vm/tests/function_tests.rs @@ -0,0 +1,150 @@ +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(vm: &mut Vm, source: &str) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = + oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new() + .compile(&program) + .map_err(|e| format!("Compile error: {}", e))?; + vm.run(&module) +} + +// --- Function Declaration Basics --- + +#[test] +fn fd_return_literal() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "function f() { return 42; } f()").unwrap(); + assert_eq!(result.as_int(), 42); +} + +#[test] +fn fd_return_void() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "function f() { 1; } f()").unwrap(); + assert!(result.is_undefined()); +} + +#[test] +fn fd_hoisting() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "foo(); function foo() { return 1; }").unwrap(); + assert_eq!(result.as_int(), 1); +} + +#[test] +fn fd_with_params() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "function add(a,b) { return a + b; } add(2, 3)").unwrap(); + assert!((result.as_double() - 5.0).abs() < 0.0001); +} + +#[test] +fn fd_single_param() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "function echo(x) { return x; } echo(99)").unwrap(); + assert_eq!(result.as_int(), 99); +} + +// --- Function Expression --- + +#[test] +fn fe_basic() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var f = function() { return 99; }; f()").unwrap(); + assert_eq!(result.as_int(), 99); +} + +#[test] +fn fe_with_params() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var mul = function(x,y) { return x * y; }; mul(6, 7)", + ) + .unwrap(); + assert!((result.as_double() - 42.0).abs() < 0.0001); +} + +// --- Cross-function calls --- + +#[test] +fn cross_func_call() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "function a() { return 1; } function b() { return a() + 2; } b()", + ) + .unwrap(); + assert!((result.as_double() - 3.0).abs() < 0.0001); +} + +#[test] +fn two_funcs_called_from_global() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "function a() { return 1; } function b() { return 2; } a() + b()", + ) + .unwrap(); + assert!(result.is_double(), "expected double, got: {:?}", result); + assert!((result.as_double() - 3.0).abs() < 0.0001); +} + +// --- Builtins inside functions --- + +#[test] +fn fd_calls_builtin_return() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "function f() { return Math.abs(-5); } f()").unwrap(); + assert!(result.is_double(), "expected double, got: {:?}", result); + assert!((result.as_double() - 5.0).abs() < 0.0001); +} + +#[test] +fn fd_calls_builtin_with_param() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "function f(x) { return Math.abs(x); } f(-10)").unwrap(); + assert!(result.is_double(), "expected double, got: {:?}", result); + assert!((result.as_double() - 10.0).abs() < 0.0001); +} + +#[test] +fn fd_calls_builtin_two_args() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "function f(x) { return Math.pow(x, 2); } f(3)").unwrap(); + assert!(result.is_double(), "expected double, got: {:?}", result); + assert!((result.as_double() - 9.0).abs() < 0.0001); +} + +// --- Multiple FDs, first FD calls second --- + +#[test] +fn fd_chain_call() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "function a() { return 10; } function b() { return a(); } b()", + ) + .unwrap(); + assert_eq!(result.as_int(), 10); +} + +// --- new Xxx() in function --- + +#[test] +fn fd_returns_new_object() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "function f() { return new Object(); } f()").unwrap(); + assert!(result.is_object(), "expected object, got: {:?}", result); +} + +#[test] +fn fd_returns_new_array() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "function f() { return new Array(3); } f()").unwrap(); + assert!(result.is_object()); +}