diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index f272696..690926e 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -44,6 +44,8 @@ pub(crate) enum Label { DoWhileEnd(u32), ForInStart(u32), ForInEnd(u32), + ForOfStart(u32), + ForOfEnd(u32), SwitchEnd(u32), SwitchCase(u32, u32), CatchBody(u32), @@ -86,6 +88,23 @@ pub(crate) enum FunctionBodyContext { ClassElement, } +pub(crate) enum ParamSpec<'a> { + Identifier(String), + Pattern { + synthetic_name: String, + pattern: &'a oxide_parser::BindingPattern<'a>, + }, +} + +impl ParamSpec<'_> { + pub(crate) fn register_name(&self) -> &str { + match self { + Self::Identifier(name) => name, + Self::Pattern { synthetic_name, .. } => synthetic_name, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] enum ConstantKey { Number(u64), @@ -366,18 +385,23 @@ impl Compiler { pub(crate) fn extract_function_parts<'a>( &self, function: &'a oxide_parser::Function<'a>, - ) -> Result<(Vec, &'a [Statement<'a>]), String> { - let mut param_names = Vec::new(); - for param in &function.params.items { + ) -> Result<(Vec>, &'a [Statement<'a>]), String> { + let mut param_specs = Vec::new(); + for (idx, param) in function.params.items.iter().enumerate() { match ¶m.pattern { oxide_parser::BindingPattern::BindingIdentifier(bi) => { - param_names.push(bi.name.to_string()); + param_specs.push(ParamSpec::Identifier(bi.name.to_string())); + } + pattern => { + param_specs.push(ParamSpec::Pattern { + synthetic_name: format!("@@param_{idx}"), + pattern, + }); } - _ => return Err("destructuring parameters not supported".into()), } } let body_stmts: &[Statement] = if let Some(body) = &function.body { &body.statements } else { &[] }; - Ok((param_names, body_stmts)) + Ok((param_specs, body_stmts)) } /// Compile a function body (used for FD, FE, and arrow functions). @@ -388,8 +412,8 @@ impl Compiler { /// (true for arrow functions, which have lexical super) or reset to false /// (false for regular functions, which create a new super scope). pub(crate) fn compile_function_body<'a>( - &self, param_names: &[String], body_stmts: &[Statement<'a>], parent_ctx: &CompileCtx, is_expression_body: bool, - is_arrow: bool, + &self, param_specs: &[ParamSpec<'a>], body_stmts: &[Statement<'a>], parent_ctx: &CompileCtx, + is_expression_body: bool, is_arrow: bool, ) -> Result { let body_context = if is_arrow { FunctionBodyContext::Arrow @@ -397,7 +421,7 @@ impl Compiler { FunctionBodyContext::Ordinary }; self.compile_function_body_with_bindings( - param_names, + param_specs, body_stmts, parent_ctx, is_expression_body, @@ -407,8 +431,8 @@ impl Compiler { } pub(crate) fn compile_function_body_with_bindings<'a>( - &self, param_names: &[String], body_stmts: &[Statement<'a>], parent_ctx: &CompileCtx, is_expression_body: bool, - extra_bindings: &[(&str, u8)], body_context: FunctionBodyContext, + &self, param_specs: &[ParamSpec<'a>], body_stmts: &[Statement<'a>], parent_ctx: &CompileCtx, + is_expression_body: bool, extra_bindings: &[(&str, u8)], body_context: FunctionBodyContext, ) -> Result { let mut ctx = CompileCtx::new(); @@ -467,7 +491,8 @@ impl Compiler { let param_base = ctx.next_reg; // Register parameters as initialized. - for name in param_names { + for spec in param_specs { + let name = spec.register_name(); let reg = ctx.alloc_reg(); ctx.declare_initialized(name, reg, VariableDeclarationKind::Var, false)?; } @@ -480,11 +505,19 @@ impl Compiler { ctx.reset_regs(); // Emit pass - reallocate params (same order = same regs after reset) - for name in param_names { + for spec in param_specs { + let name = spec.register_name(); let reg = ctx.alloc_reg(); ctx.declare_initialized(name, reg, VariableDeclarationKind::Var, false)?; } + for spec in param_specs { + if let ParamSpec::Pattern { synthetic_name, pattern } = spec { + let src_reg = ctx.lookup(synthetic_name)?; + self.emit_binding_pattern(pattern, src_reg, VariableDeclarationKind::Var, false, &mut ctx)?; + } + } + // Emit body statements. // Capture the last statement's expression result for expression-body arrows. let mut last_result_reg = None; @@ -530,7 +563,7 @@ impl Compiler { bytecode: ctx.bytecode, constants: ctx.constants, n_registers: ctx.max_regs, - n_args: param_names.len() as u8, + n_args: param_specs.len() as u8, param_base, builtin_reg_map: ctx.builtin_reg_map, sub_modules: ctx.sub_modules, diff --git a/crates/oxide_compiler/src/counter.rs b/crates/oxide_compiler/src/counter.rs index d1c8396..f69e333 100644 --- a/crates/oxide_compiler/src/counter.rs +++ b/crates/oxide_compiler/src/counter.rs @@ -6,6 +6,102 @@ use oxide_parser::{ use crate::compiler::{is_side_effect_free, CompileCtx, Compiler, Label}; impl Compiler { + fn count_binding_pattern(&self, pattern: &oxide_parser::BindingPattern, ctx: &mut CompileCtx) { + match pattern { + oxide_parser::BindingPattern::BindingIdentifier(_) => { + ctx.alloc_reg(); + ctx.projected_pc += 1; + } + oxide_parser::BindingPattern::ArrayPattern(ap) => { + ctx.projected_pc += 1; // FOR_OF_INIT + for elem in &ap.elements { + ctx.alloc_reg(); + ctx.projected_pc += 1; // FOR_OF_DONE + ctx.alloc_reg(); + ctx.projected_pc += 1; // FOR_OF_NEXT + if let Some(inner) = elem { + self.count_binding_pattern(inner, ctx); + } + } + if let Some(rest) = &ap.rest { + self.count_rest_array(ctx); + self.count_binding_pattern(&rest.argument, ctx); + } + ctx.projected_pc += 1; // FOR_OF_CLOSE + } + oxide_parser::BindingPattern::ObjectPattern(op) => { + for prop in &op.properties { + ctx.alloc_reg(); + ctx.projected_pc += 5; // LOAD_VAR + LOAD_CONST + IC_GET_PROP + 3 ext + ctx.alloc_reg(); + self.count_binding_pattern(&prop.value, ctx); + } + if let Some(rest) = &op.rest { + ctx.alloc_reg(); + ctx.projected_pc += 2; // REST_OBJECT + ext + self.count_binding_pattern(&rest.argument, ctx); + } + } + oxide_parser::BindingPattern::AssignmentPattern(ap) => { + ctx.alloc_reg(); + ctx.projected_pc += 2; // LOAD_CONST undefined + STRICT_EQ + ctx.projected_pc += 1; // JMP_IF_FALSE + self.count_expression(&ap.right, ctx); + ctx.projected_pc += 1; // LOAD_VAR default + self.count_binding_pattern(&ap.left, ctx); + } + } + } + + fn count_rest_array(&self, ctx: &mut CompileCtx) { + ctx.alloc_reg(); // rest + ctx.projected_pc += 1; // NEW_ARRAY + ctx.alloc_reg(); // idx + ctx.projected_pc += 1; // LOAD_CONST 0 + ctx.alloc_reg(); // has + ctx.projected_pc += 2; // FOR_OF_DONE + JMP_IF_FALSE + ctx.alloc_reg(); // val + ctx.projected_pc += 1; // FOR_OF_NEXT + ctx.projected_pc += 1; // SET_ELEM + ctx.alloc_reg(); // inc tmp + ctx.projected_pc += 2; // INC_PRE + JMP + } + + fn count_array_assignment(&self, ap: &oxide_parser::ArrayAssignmentTarget, ctx: &mut CompileCtx) { + ctx.projected_pc += 1; // FOR_OF_INIT + for elem in &ap.elements { + ctx.alloc_reg(); + ctx.projected_pc += 1; // FOR_OF_DONE + ctx.alloc_reg(); + ctx.projected_pc += 1; // FOR_OF_NEXT + if elem.is_some() { + ctx.alloc_reg(); + ctx.projected_pc += 1; + } + } + if ap.rest.is_some() { + self.count_rest_array(ctx); + ctx.alloc_reg(); + ctx.projected_pc += 1; + } + ctx.projected_pc += 1; // FOR_OF_CLOSE + } + + fn count_object_assignment(&self, op: &oxide_parser::ObjectAssignmentTarget, ctx: &mut CompileCtx) { + for _ in &op.properties { + ctx.alloc_reg(); + ctx.projected_pc += 5; // LOAD_VAR + LOAD_CONST + IC_GET_PROP + 3 ext + ctx.alloc_reg(); + ctx.projected_pc += 1; + } + if op.rest.is_some() { + ctx.alloc_reg(); + ctx.projected_pc += 2; // REST_OBJECT + ext + ctx.alloc_reg(); + ctx.projected_pc += 1; + } + } + fn count_class(&self, class: &oxide_parser::Class, ctx: &mut CompileCtx) { ctx.alloc_reg(); // ctor_reg ctx.alloc_reg(); // proto_reg @@ -57,11 +153,11 @@ impl Compiler { } Statement::VariableDeclaration(decl) => { for d in &decl.declarations { - ctx.alloc_reg(); if let Some(init) = &d.init { self.count_expression(init, ctx); - ctx.projected_pc += 1; // STORE_VAR + self.count_binding_pattern(&d.id, ctx); } else { + ctx.alloc_reg(); ctx.projected_pc += 2; // LOAD_CONST(undefined) + STORE_VAR } } @@ -126,11 +222,11 @@ impl Compiler { self.count_expression(expr, ctx); } else if let ForStatementInit::VariableDeclaration(decl) = init { for d in &decl.declarations { - ctx.alloc_reg(); if let Some(init_expr) = &d.init { self.count_expression(init_expr, ctx); - ctx.projected_pc += 1; // STORE_VAR + self.count_binding_pattern(&d.id, ctx); } else { + ctx.alloc_reg(); ctx.projected_pc += 2; // LOAD_CONST(undefined) + STORE_VAR } } @@ -181,6 +277,41 @@ impl Compiler { ctx.label_map.insert(end_label, ctx.projected_pc); ctx.projected_pc += 1; // FOR_IN_CLEANUP } + Statement::ForOfStatement(fo) => { + let id = ctx.next_label_id(); + let start_label = Label::ForOfStart(id); + let end_label = Label::ForOfEnd(id); + + self.count_expression(&fo.right, ctx); + ctx.projected_pc += 1; // FOR_OF_INIT + ctx.label_map.insert(start_label, ctx.projected_pc); + ctx.alloc_reg(); // has_reg + ctx.projected_pc += 2; // FOR_OF_DONE + JMP_IF_FALSE + ctx.alloc_reg(); // val_reg + ctx.projected_pc += 1; // FOR_OF_NEXT + match &fo.left { + oxide_parser::ForStatementLeft::VariableDeclaration(decl) => { + for d in &decl.declarations { + self.count_binding_pattern(&d.id, ctx); + } + } + oxide_parser::ForStatementLeft::AssignmentTargetIdentifier(_) => { + ctx.alloc_reg(); + ctx.projected_pc += 1; + } + oxide_parser::ForStatementLeft::ArrayAssignmentTarget(ap) => { + self.count_array_assignment(ap, ctx); + } + oxide_parser::ForStatementLeft::ObjectAssignmentTarget(op) => { + self.count_object_assignment(op, ctx); + } + _ => {} + } + self.count_statement(&fo.body, ctx); + ctx.projected_pc += 1; // JMP back + ctx.label_map.insert(end_label, ctx.projected_pc); + ctx.projected_pc += 1; // FOR_OF_CLOSE + } Statement::SwitchStatement(sw) => { let id = ctx.next_label_id(); let end_label = Label::SwitchEnd(id); @@ -410,6 +541,12 @@ impl Compiler { self.count_expression(&assign.right, ctx); ctx.alloc_reg(); ctx.projected_pc += 1; // SET_PROP_DYNAMIC + } else if let oxide_parser::AssignmentTarget::ArrayAssignmentTarget(ap) = &assign.left { + self.count_expression(&assign.right, ctx); + self.count_array_assignment(ap, ctx); + } else if let oxide_parser::AssignmentTarget::ObjectAssignmentTarget(op) = &assign.left { + self.count_expression(&assign.right, ctx); + self.count_object_assignment(op, ctx); } else { self.count_expression(&assign.right, ctx); ctx.alloc_reg(); diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index 3f6615b..df187d8 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -1,15 +1,305 @@ use crate::compiler::Label; use crate::opcode::{self, OpCode}; use oxide_parser::{ - AssignmentOperator, Class, ClassElement, Expression, ForStatementInit, ForStatementLeft, MethodDefinitionKind, + AssignmentOperator, AssignmentTarget, AssignmentTargetMaybeDefault, AssignmentTargetProperty, BindingPattern, + Class, ClassElement, Expression, ForStatementInit, ForStatementLeft, MethodDefinitionKind, ObjectAssignmentTarget, PropertyKey, PropertyKind, SimpleAssignmentTarget, Statement, UnaryOperator, UpdateOperator, VariableDeclarationKind, }; -use crate::compiler::{is_int_literal, is_side_effect_free, BinaryOperator, CompileCtx, Compiler, FunctionBodyContext}; +use crate::compiler::{ + is_int_literal, is_side_effect_free, BinaryOperator, CompileCtx, Compiler, FunctionBodyContext, ParamSpec, +}; use crate::module::Constant; impl Compiler { + fn static_property_name(&self, key: &PropertyKey) -> Result { + match key { + PropertyKey::StaticIdentifier(ident) => Ok(ident.name.as_str().to_string()), + PropertyKey::Identifier(ident) => Ok(ident.name.as_str().to_string()), + PropertyKey::StringLiteral(s) => Ok(s.value.to_string()), + PropertyKey::NumericLiteral(n) => Ok(n.value.to_string()), + _ => Err("computed destructuring keys not yet supported".into()), + } + } + + fn emit_default_if_undefined( + &self, val_reg: u8, default_expr: &Expression, ctx: &mut CompileCtx, + ) -> Result { + let undef_reg = self.emit_undefined(ctx); + let eq_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::STRICT_EQ, eq_reg, val_reg, undef_reg)); + let jump_pos = ctx.bytecode.len(); + ctx.emit(opcode::encode_jmp_if_false(eq_reg, 0)); + let default_reg = self.emit_expression(default_expr, ctx)?; + ctx.emit(opcode::encode(OpCode::LOAD_VAR, val_reg, default_reg, 0)); + let after = ctx.bytecode.len(); + let offset = after as isize - jump_pos as isize; + ctx.bytecode[jump_pos] = opcode::encode_jmp_if_false(eq_reg, offset as i16); + Ok(val_reg) + } + + fn emit_bind_target( + &self, name: &str, src_reg: u8, kind: VariableDeclarationKind, is_const: bool, ctx: &mut CompileCtx, + ) -> Result<(), String> { + let var_reg = ctx.alloc_reg(); + ctx.declare(name, var_reg, kind, is_const)?; + ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, src_reg, if is_const { 1 } else { 0 })); + ctx.init_var(name); + Ok(()) + } + + fn emit_assign_target(&self, target: &AssignmentTarget, src_reg: u8, ctx: &mut CompileCtx) -> Result<(), String> { + match target { + AssignmentTarget::AssignmentTargetIdentifier(id) => { + let name = id.name.as_str(); + let var_reg = ctx.lookup_or_global(name); + ctx.emit(opcode::encode( + OpCode::STORE_VAR, + var_reg, + src_reg, + if ctx.lookup_const_flag(name) { 1 } else { 0 }, + )); + Ok(()) + } + AssignmentTarget::ArrayAssignmentTarget(ap) => self.emit_array_assignment(ap, src_reg, ctx), + AssignmentTarget::ObjectAssignmentTarget(op) => self.emit_object_assignment(op, src_reg, ctx), + _ => Err("assignment target not supported".into()), + } + } + + pub(crate) fn emit_binding_pattern( + &self, pattern: &BindingPattern, src_reg: u8, kind: VariableDeclarationKind, is_const: bool, + ctx: &mut CompileCtx, + ) -> Result<(), String> { + match pattern { + BindingPattern::BindingIdentifier(bi) => { + self.emit_bind_target(bi.name.as_str(), src_reg, kind, is_const, ctx) + } + BindingPattern::ArrayPattern(ap) => self.emit_array_binding(ap, src_reg, kind, is_const, ctx), + BindingPattern::ObjectPattern(op) => self.emit_object_binding(op, src_reg, kind, is_const, ctx), + BindingPattern::AssignmentPattern(ap) => { + let val_reg = self.emit_default_if_undefined(src_reg, &ap.right, ctx)?; + self.emit_binding_pattern(&ap.left, val_reg, kind, is_const, ctx) + } + } + } + + fn emit_array_binding( + &self, ap: &oxide_parser::ArrayPattern, src_reg: u8, kind: VariableDeclarationKind, is_const: bool, + ctx: &mut CompileCtx, + ) -> Result<(), String> { + ctx.emit(opcode::encode(OpCode::FOR_OF_INIT, 0, src_reg, 0)); + for elem in &ap.elements { + let has_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::FOR_OF_DONE, has_reg, 0, 0)); + let val_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::FOR_OF_NEXT, val_reg, 0, 0)); + if let Some(pattern) = elem { + self.emit_binding_pattern(pattern, val_reg, kind, is_const, ctx)?; + } + } + if let Some(rest) = &ap.rest { + let rest_reg = self.emit_collect_rest_array(ctx)?; + self.emit_binding_pattern(&rest.argument, rest_reg, kind, is_const, ctx)?; + } + ctx.emit(opcode::encode(OpCode::FOR_OF_CLOSE, 0, 0, 0)); + Ok(()) + } + + fn emit_collect_rest_array(&self, ctx: &mut CompileCtx) -> Result { + let rest_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::NEW_ARRAY, rest_reg, 0, 0)); + let idx_reg = ctx.alloc_reg(); + let zero_idx = ctx.add_constant(Constant::Int(0)); + ctx.emit_load_const(idx_reg, zero_idx); + let loop_start = ctx.bytecode.len(); + let has_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::FOR_OF_DONE, has_reg, 0, 0)); + let end_jmp = ctx.bytecode.len(); + ctx.emit(opcode::encode_jmp_if_false(has_reg, 0)); + let val_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::FOR_OF_NEXT, val_reg, 0, 0)); + ctx.emit(opcode::encode(OpCode::SET_ELEM, rest_reg, idx_reg, val_reg)); + let tmp_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::INC_PRE, idx_reg, tmp_reg, tmp_reg)); + let back = loop_start as isize - ctx.bytecode.len() as isize; + ctx.emit(opcode::encode_jmp(back as i16)); + let after = ctx.bytecode.len(); + ctx.bytecode[end_jmp] = opcode::encode_jmp_if_false(has_reg, (after as isize - end_jmp as isize) as i16); + Ok(rest_reg) + } + + fn emit_object_property_read(&self, src_reg: u8, key: &str, ctx: &mut CompileCtx) -> u8 { + let prop_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::LOAD_VAR, prop_reg, src_reg, 0)); + let key_idx = ctx.add_constant(Constant::String(key.to_string())); + let key_reg = ctx.alloc_reg(); + ctx.emit_load_const(key_reg, key_idx); + ctx.emit(opcode::encode(OpCode::IC_GET_PROP, 0, prop_reg, key_reg)); + ctx.emit(0); + ctx.emit(0); + ctx.emit(0); + prop_reg + } + + fn emit_property_key_expression(&self, key: &PropertyKey, ctx: &mut CompileCtx) -> Result { + match key { + PropertyKey::Identifier(ident) => { + let name = ident.name.as_str(); + let var_reg = ctx.lookup(name)?; + let key_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::LOAD_VAR, key_reg, var_reg, 0)); + Ok(key_reg) + } + PropertyKey::StringLiteral(s) => { + let key_idx = ctx.add_constant(Constant::String(s.value.to_string())); + let key_reg = ctx.alloc_reg(); + ctx.emit_load_const(key_reg, key_idx); + Ok(key_reg) + } + PropertyKey::NumericLiteral(n) => { + let key_idx = ctx.add_constant(Constant::Number(n.value)); + let key_reg = ctx.alloc_reg(); + ctx.emit_load_const(key_reg, key_idx); + Ok(key_reg) + } + PropertyKey::StaticIdentifier(ident) => { + let key_idx = ctx.add_constant(Constant::String(ident.name.as_str().to_string())); + let key_reg = ctx.alloc_reg(); + ctx.emit_load_const(key_reg, key_idx); + Ok(key_reg) + } + _ => Err("computed destructuring key expression not supported".into()), + } + } + + fn emit_object_property_read_key( + &self, src_reg: u8, key: &PropertyKey, computed: bool, ctx: &mut CompileCtx, + ) -> Result<(u8, Option), String> { + if !computed { + let key_name = self.static_property_name(key)?; + let prop_reg = self.emit_object_property_read(src_reg, &key_name, ctx); + return Ok((prop_reg, Some(key_name))); + } + let prop_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::LOAD_VAR, prop_reg, src_reg, 0)); + let key_reg = self.emit_property_key_expression(key, ctx)?; + let val_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::GET_PROP_DYNAMIC, prop_reg, key_reg, val_reg)); + Ok((val_reg, None)) + } + + fn emit_object_binding( + &self, op: &oxide_parser::ObjectPattern, src_reg: u8, kind: VariableDeclarationKind, is_const: bool, + ctx: &mut CompileCtx, + ) -> Result<(), String> { + let mut excluded = Vec::new(); + for prop in &op.properties { + let (prop_reg, static_key) = self.emit_object_property_read_key(src_reg, &prop.key, prop.computed, ctx)?; + if let Some(key) = static_key { + excluded.push(key); + } + self.emit_binding_pattern(&prop.value, prop_reg, kind, is_const, ctx)?; + } + if let Some(rest) = &op.rest { + let rest_reg = ctx.alloc_reg(); + let excluded_idx = ctx.add_constant(Constant::String(excluded.join("\0"))); + ctx.emit(opcode::encode(OpCode::REST_OBJECT, rest_reg, src_reg, 0)); + ctx.emit(excluded_idx as u32); + self.emit_binding_pattern(&rest.argument, rest_reg, kind, is_const, ctx)?; + } + Ok(()) + } + + fn emit_assignment_maybe_default( + &self, target: &AssignmentTargetMaybeDefault, src_reg: u8, ctx: &mut CompileCtx, + ) -> Result<(), String> { + match target { + AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(default) => { + let val_reg = self.emit_default_if_undefined(src_reg, &default.init, ctx)?; + self.emit_assign_target(&default.binding, val_reg, ctx) + } + AssignmentTargetMaybeDefault::ArrayAssignmentTarget(ap) => self.emit_array_assignment(ap, src_reg, ctx), + AssignmentTargetMaybeDefault::ObjectAssignmentTarget(op) => self.emit_object_assignment(op, src_reg, ctx), + AssignmentTargetMaybeDefault::AssignmentTargetIdentifier(id) => { + let name = id.name.as_str(); + let var_reg = ctx.lookup_or_global(name); + ctx.emit(opcode::encode( + OpCode::STORE_VAR, + var_reg, + src_reg, + if ctx.lookup_const_flag(name) { 1 } else { 0 }, + )); + Ok(()) + } + _ => Err("assignment target not supported".into()), + } + } + + fn emit_array_assignment( + &self, ap: &oxide_parser::ArrayAssignmentTarget, src_reg: u8, ctx: &mut CompileCtx, + ) -> Result<(), String> { + ctx.emit(opcode::encode(OpCode::FOR_OF_INIT, 0, src_reg, 0)); + for elem in &ap.elements { + let has_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::FOR_OF_DONE, has_reg, 0, 0)); + let val_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::FOR_OF_NEXT, val_reg, 0, 0)); + if let Some(target) = elem { + self.emit_assignment_maybe_default(target, val_reg, ctx)?; + } + } + if let Some(rest) = &ap.rest { + let rest_reg = self.emit_collect_rest_array(ctx)?; + self.emit_assign_target(&rest.target, rest_reg, ctx)?; + } + ctx.emit(opcode::encode(OpCode::FOR_OF_CLOSE, 0, 0, 0)); + Ok(()) + } + + fn emit_object_assignment( + &self, op: &ObjectAssignmentTarget, src_reg: u8, ctx: &mut CompileCtx, + ) -> Result<(), String> { + let mut excluded = Vec::new(); + for prop in &op.properties { + match prop { + AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(id) => { + let key = id.binding.name.as_str().to_string(); + excluded.push(key.clone()); + let mut prop_reg = self.emit_object_property_read(src_reg, &key, ctx); + if let Some(default_expr) = &id.init { + prop_reg = self.emit_default_if_undefined(prop_reg, default_expr, ctx)?; + } + let name = id.binding.name.as_str(); + let var_reg = ctx.lookup_or_global(name); + ctx.emit(opcode::encode( + OpCode::STORE_VAR, + var_reg, + prop_reg, + if ctx.lookup_const_flag(name) { 1 } else { 0 }, + )); + } + AssignmentTargetProperty::AssignmentTargetPropertyProperty(prop) => { + let (prop_reg, static_key) = + self.emit_object_property_read_key(src_reg, &prop.name, prop.computed, ctx)?; + if let Some(key) = static_key { + excluded.push(key); + } + self.emit_assignment_maybe_default(&prop.binding, prop_reg, ctx)?; + } + } + } + if let Some(rest) = &op.rest { + let rest_reg = ctx.alloc_reg(); + let excluded_idx = ctx.add_constant(Constant::String(excluded.join("\0"))); + ctx.emit(opcode::encode(OpCode::REST_OBJECT, rest_reg, src_reg, 0)); + ctx.emit(excluded_idx as u32); + self.emit_assign_target(&rest.target, rest_reg, ctx)?; + } + Ok(()) + } + fn class_property_name(&self, key: &PropertyKey) -> Result { match key { PropertyKey::StaticIdentifier(ident) => Ok(ident.name.as_str().to_string()), @@ -266,35 +556,34 @@ impl Compiler { Statement::VariableDeclaration(decl) => { let mut r = None; for d in &decl.declarations { - let name = match &d.id { - oxide_parser::BindingPattern::BindingIdentifier(bi) => bi.name.as_str(), - _ => return Err("destructuring not supported".into()), - }; let is_const = matches!(decl.kind, VariableDeclarationKind::Const); if is_const && d.init.is_none() { return Err("const declaration must have an initializer".into()); } - let var_reg = ctx.alloc_reg(); - ctx.declare(name, var_reg, decl.kind, is_const)?; if let Some(init) = &d.init { let val_reg = self.emit_expression(init, ctx)?; - let const_flag = if is_const { 1 } else { 0 }; - ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, const_flag)); - ctx.init_var(name); + self.emit_binding_pattern(&d.id, val_reg, decl.kind, is_const, ctx)?; // Name inference (D-04): if the initializer is an arrow function, // set the compiled sub_module's function_name. - if matches!(*init, Expression::ArrowFunctionExpression(_)) { - if let Some(sub_mod) = ctx.sub_modules.last_mut() { - sub_mod.function_name = Some(name.to_string()); + if let BindingPattern::BindingIdentifier(bi) = &d.id { + if matches!(*init, Expression::ArrowFunctionExpression(_)) { + if let Some(sub_mod) = ctx.sub_modules.last_mut() { + sub_mod.function_name = Some(bi.name.to_string()); + } } } r = Some(val_reg); } else { + let BindingPattern::BindingIdentifier(bi) = &d.id else { + return Err("destructuring declaration requires an initializer".into()); + }; let idx = ctx.add_constant(Constant::Undefined); let tmp = ctx.alloc_reg(); ctx.emit_load_const(tmp, idx); + let var_reg = ctx.alloc_reg(); + ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?; ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, tmp, 0)); - ctx.init_var(name); + ctx.init_var(bi.name.as_str()); r = Some(var_reg); } } @@ -415,16 +704,13 @@ impl Compiler { self.emit_expression(expr, ctx)?; } else if let ForStatementInit::VariableDeclaration(decl) = init { for d in &decl.declarations { - let name = match &d.id { - oxide_parser::BindingPattern::BindingIdentifier(bi) => bi.name.as_str(), - _ => return Err("destructuring not supported".into()), - }; - let var_reg = ctx.alloc_reg(); - ctx.declare(name, var_reg, decl.kind, matches!(decl.kind, VariableDeclarationKind::Const))?; + let is_const = matches!(decl.kind, VariableDeclarationKind::Const); if let Some(init_expr) = &d.init { let val_reg = self.emit_expression(init_expr, ctx)?; - ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, 0)); - ctx.init_var(name); + self.emit_binding_pattern(&d.id, val_reg, decl.kind, is_const, ctx)?; + } else if let BindingPattern::BindingIdentifier(bi) = &d.id { + let var_reg = ctx.alloc_reg(); + ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?; } } } @@ -504,6 +790,51 @@ impl Compiler { ctx.pop_loop(); Ok(None) } + Statement::ForOfStatement(fo) => { + let id = ctx.next_label_id(); + let start_label = Label::ForOfStart(id); + let end_label = Label::ForOfEnd(id); + + let iter_src_reg = self.emit_expression(&fo.right, ctx)?; + ctx.emit(opcode::encode(OpCode::FOR_OF_INIT, 0, iter_src_reg, 0)); + ctx.push_loop(end_label, start_label); + + let has_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::FOR_OF_DONE, has_reg, 0, 0)); + let end_pos = ctx.resolve_label(end_label)?; + let offset = (end_pos as isize) - (ctx.bytecode.len() as isize); + ctx.emit(opcode::encode_jmp_if_false(has_reg, offset as i16)); + + let val_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::FOR_OF_NEXT, val_reg, 0, 0)); + match &fo.left { + ForStatementLeft::VariableDeclaration(decl) => { + for d in &decl.declarations { + self.emit_binding_pattern(&d.id, val_reg, decl.kind, false, ctx)?; + } + } + ForStatementLeft::AssignmentTargetIdentifier(id_ref) => { + let name = id_ref.name.as_str(); + let var_reg = ctx.lookup_or_global(name); + ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, 0)); + } + ForStatementLeft::ArrayAssignmentTarget(ap) => { + self.emit_array_assignment(ap, val_reg, ctx)?; + } + ForStatementLeft::ObjectAssignmentTarget(op) => { + self.emit_object_assignment(op, val_reg, ctx)?; + } + _ => return Err("unsupported for-of left-hand side".into()), + } + + self.emit_statement(&fo.body, ctx)?; + let start_pos = ctx.resolve_label(start_label)?; + let offset = (start_pos as isize) - (ctx.bytecode.len() as isize); + ctx.emit(opcode::encode_jmp(offset as i16)); + ctx.emit(opcode::encode(OpCode::FOR_OF_CLOSE, 0, 0, 0)); + ctx.pop_loop(); + Ok(None) + } Statement::SwitchStatement(sw) => { let id = ctx.next_label_id(); let end_label = Label::SwitchEnd(id); @@ -572,9 +903,17 @@ impl Compiler { // 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()); + for (idx, param) in fd.params.items.iter().enumerate() { + match ¶m.pattern { + oxide_parser::BindingPattern::BindingIdentifier(bi) => { + param_names.push(ParamSpec::Identifier(bi.name.to_string())); + } + pattern => { + param_names.push(ParamSpec::Pattern { + synthetic_name: format!("@@param_{idx}"), + pattern, + }); + } } } @@ -1105,6 +1444,17 @@ impl Compiler { ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, const_flag)); Ok(val_reg) } + } else if matches!( + &assign.left, + oxide_parser::AssignmentTarget::ArrayAssignmentTarget(_) + | oxide_parser::AssignmentTarget::ObjectAssignmentTarget(_) + ) { + if assign.operator != AssignmentOperator::Assign { + return Err("compound destructuring assignment not supported".into()); + } + let val_reg = self.emit_expression(&assign.right, ctx)?; + self.emit_assign_target(&assign.left, val_reg, ctx)?; + Ok(val_reg) } else { Err("assignment target not supported".into()) } @@ -1295,9 +1645,17 @@ impl Compiler { // Extract param names (same pattern as FunctionExpression) let mut param_names = Vec::new(); - for param in &arrow.params.items { - if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern { - param_names.push(bi.name.to_string()); + for (idx, param) in arrow.params.items.iter().enumerate() { + match ¶m.pattern { + oxide_parser::BindingPattern::BindingIdentifier(bi) => { + param_names.push(ParamSpec::Identifier(bi.name.to_string())); + } + pattern => { + param_names.push(ParamSpec::Pattern { + synthetic_name: format!("@@param_{idx}"), + pattern, + }); + } } } @@ -1321,9 +1679,17 @@ impl Compiler { 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()); + for (idx, param) in fe.params.items.iter().enumerate() { + match ¶m.pattern { + oxide_parser::BindingPattern::BindingIdentifier(bi) => { + param_names.push(ParamSpec::Identifier(bi.name.to_string())); + } + pattern => { + param_names.push(ParamSpec::Pattern { + synthetic_name: format!("@@param_{idx}"), + pattern, + }); + } } } diff --git a/crates/oxide_compiler/src/opcode.rs b/crates/oxide_compiler/src/opcode.rs index 91ced12..fcfcf9a 100644 --- a/crates/oxide_compiler/src/opcode.rs +++ b/crates/oxide_compiler/src/opcode.rs @@ -73,6 +73,7 @@ pub enum OpCode { DELETE_PROP_STATIC = 0x39, DELETE_PROP_DYNAMIC = 0x3A, INSTANCEOF = 0x3B, + REST_OBJECT = 0x3E, // -- Variable (0x30-0x32) -- LOAD_VAR = 0x30, @@ -233,6 +234,7 @@ impl TryFrom for OpCode { 0x39 => Ok(OpCode::DELETE_PROP_STATIC), 0x3A => Ok(OpCode::DELETE_PROP_DYNAMIC), 0x3B => Ok(OpCode::INSTANCEOF), + 0x3E => Ok(OpCode::REST_OBJECT), 0x40 => Ok(OpCode::CALL), 0x41 => Ok(OpCode::RETURN), 0x42 => Ok(OpCode::CALL_NATIVE), @@ -342,6 +344,7 @@ impl fmt::Display for OpCode { OpCode::DELETE_PROP_STATIC => "DELETE_PROP_STATIC", OpCode::DELETE_PROP_DYNAMIC => "DELETE_PROP_DYNAMIC", OpCode::INSTANCEOF => "INSTANCEOF", + OpCode::REST_OBJECT => "REST_OBJECT", OpCode::LOAD_VAR => "LOAD_VAR", OpCode::STORE_VAR => "STORE_VAR", OpCode::LOAD_CONST => "LOAD_CONST", diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 6a431f3..5858f91 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -92,6 +92,7 @@ pub struct ArrayMethods { pub find_last: *const (), pub reduce_right: *const (), pub sort: *const (), + pub values: *const (), } pub struct ErrorMethods { @@ -588,7 +589,16 @@ impl BuiltinWorld { ("findLast", methods.find_last, 1), ("reduceRight", methods.reduce_right, 1), ("sort", methods.sort, 0), + ("values", methods.values, 0), ); + + let si = string_forge.intern("@@iterator").0; + let raw = methods.values; + // SAFETY: methods.values is a NativeFn fn-item passed by the VM binding layer. + let func_ptr = unsafe { NativeFnPtr::from_raw(raw) }; + let _ = + Self::bind_method_static(proto, shape_forge, string_forge, "@@iterator", func_ptr, 0, self.fn_proto_val()); + debug_assert!(shape_forge.lookup_position(proto.shape_id(), si).is_some()); } pub fn bind_error_methods(&self, methods: &ErrorMethods, string_forge: &StringForge, shape_forge: &ShapeForge) { diff --git a/crates/oxide_vm/src/bindings/bind_array.rs b/crates/oxide_vm/src/bindings/bind_array.rs index dc605c4..72f6343 100644 --- a/crates/oxide_vm/src/bindings/bind_array.rs +++ b/crates/oxide_vm/src/bindings/bind_array.rs @@ -37,6 +37,7 @@ pub fn bind_array(core: &Arc, session: &KernelSession, global: &mut find_last: crate::builtins::array::array_find_last as *const (), reduce_right: crate::builtins::array::array_reduce_right as *const (), sort: crate::builtins::array::array_sort as *const (), + values: crate::builtins::array::array_values as *const (), }; session.builtin_world().bind_array_methods( diff --git a/crates/oxide_vm/src/builtins/array.rs b/crates/oxide_vm/src/builtins/array.rs index 88b049e..d5f5eb7 100644 --- a/crates/oxide_vm/src/builtins/array.rs +++ b/crates/oxide_vm/src/builtins/array.rs @@ -840,3 +840,11 @@ pub fn array_sort(vm: &mut Vm, _args: &[u8]) -> NativeResult { } NativeResult::Ok(vm.reg(_args[0])) } + +pub fn array_values(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(args[0]); + match crate::builtins::iterator::make_iterator_for_value(vm, this_val) { + Ok(iterator) => NativeResult::Ok(iterator), + Err(err) => NativeResult::Err(err), + } +} diff --git a/crates/oxide_vm/src/builtins/iterator.rs b/crates/oxide_vm/src/builtins/iterator.rs index 4824a4b..7357e98 100644 --- a/crates/oxide_vm/src/builtins/iterator.rs +++ b/crates/oxide_vm/src/builtins/iterator.rs @@ -15,11 +15,14 @@ pub fn iterator_constructor(vm: &mut Vm, _args: &[u8]) -> NativeResult { pub fn iterator_from(vm: &mut Vm, args: &[u8]) -> NativeResult { let iterable = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let inner = match get_iterator(vm, iterable) { - Ok(inner) => inner, - Err(err) => return err, - }; + match make_iterator_for_value(vm, iterable) { + Ok(iterator) => NativeResult::Ok(iterator), + Err(err) => NativeResult::Err(err), + } +} +pub(crate) fn make_iterator_for_value(vm: &mut Vm, value: JsValue) -> Result { + let inner = get_iterator(vm, value)?; let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject; let wrapper = vm .epoch() @@ -35,7 +38,7 @@ pub fn iterator_from(vm: &mut Vm, args: &[u8]) -> NativeResult { let next_fn = make_native_function(vm, "next", iterator_wrapper_next as *const (), 0); vm.set_or_create_prop_value(wrapper_obj, next_si, next_fn); - NativeResult::Ok(JsValue::from_js_object(wrapper)) + Ok(JsValue::from_js_object(wrapper)) } pub fn iterator_wrapper_next(vm: &mut Vm, args: &[u8]) -> NativeResult { @@ -80,7 +83,7 @@ pub fn iterator_wrapper_next(vm: &mut Vm, args: &[u8]) -> NativeResult { NativeResult::Err(crate::builtins::error::create_type_error(vm, "value is not iterable")) } -fn get_iterator(vm: &mut Vm, value: JsValue) -> Result { +fn get_iterator(vm: &mut Vm, value: JsValue) -> Result { if value.is_string() || is_array_value(value) { return Ok(value); } @@ -95,7 +98,7 @@ fn get_iterator(vm: &mut Vm, value: JsValue) -> Result { } } - Err(NativeResult::Err(crate::builtins::error::create_type_error(vm, "value is not iterable"))) + Err(crate::builtins::error::create_type_error(vm, "value is not iterable")) } fn next_array_like(vm: &mut Vm, wrapper: &mut JsObject, inner: JsValue, index_si: u32) -> Option { diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index 79661b7..f7198f6 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -258,7 +258,13 @@ impl Vm { let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "SET_ELEM on non-object")? else { return Ok(()); }; - let idx = self.regs[a].as_int().max(0) as u32; + let idx = if self.regs[a].is_int() { + self.regs[a].as_int().max(0) as u32 + } else if self.regs[a].is_double() { + self.regs[a].as_double().max(0.0) as u32 + } else { + 0 + }; let obj = unsafe { &mut *obj_ptr }; obj.set_prop_at(idx, self.regs[b]); Ok(()) diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 599636a..0e5cd05 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -125,6 +125,8 @@ pub(crate) struct InlineSyncState { pub(crate) pending_exception: Option, pub(crate) pending_error_kind: Option<&'static str>, pub(crate) for_in_iters: Vec<*mut ForInIter<'static>>, + pub(crate) for_of_iters: Vec, + pub(crate) last_for_of_result: JsValue, pub(crate) saved_bytecode_stack: Vec>, pub(crate) saved_constants_stack: Vec>, } @@ -153,8 +155,8 @@ pub struct Vm { pub(crate) symbol_counter: u32, pub(crate) symbol_descriptions: Vec, pub(crate) symbol_registry: HashMap, - #[allow(dead_code)] - pub(crate) for_of_iters: Vec<*mut u8>, + pub(crate) for_of_iters: Vec, + pub(crate) last_for_of_result: JsValue, pub(crate) root_reg_limit: u8, pub(crate) active_reg_limit: u8, pub(crate) native_call_depth: usize, @@ -291,11 +293,26 @@ impl Vm { self.kernel_core.string_forge().intern(&key).0 } + pub(crate) fn array_index_from_property_key(&self, prop_name_si: u32) -> Option { + let key = self.kernel_core.string_forge().lookup(prop_name_si)?; + if key.is_empty() || (key.len() > 1 && key.starts_with('0')) { + return None; + } + key.parse::().ok() + } + pub(crate) fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option { let length_si = self.kernel_core.string_forge().intern("length").0; if obj.is_array() && prop_name_si == length_si { return Some(JsValue::int(obj.prop_count() as i32)); } + if obj.is_array() { + if let Some(index) = self.array_index_from_property_key(prop_name_si) { + if index < obj.prop_vec_len() as u32 { + return Some(obj.get_prop_at(index)); + } + } + } if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { let val = obj.get_prop_at(pos); if !val.is_undefined() || obj.prop_vec_len() > pos as usize { @@ -325,6 +342,13 @@ impl Vm { if obj.is_array() && prop_name_si == length_si { return None; } + if obj.is_array() { + if let Some(index) = self.array_index_from_property_key(prop_name_si) { + if index < obj.prop_vec_len() as u32 { + return Some(index); + } + } + } self.kernel_core .shape_forge() .lookup_position(obj.shape_id(), prop_name_si) @@ -1020,19 +1044,23 @@ impl Vm { } OpCode::FOR_OF_INIT => { - return Err("opcode FOR_OF_INIT not yet implemented".into()); + self.dispatch_for_of_init(a)?; } OpCode::FOR_OF_NEXT => { - return Err("opcode FOR_OF_NEXT not yet implemented".into()); + self.dispatch_for_of_next(rd)?; } OpCode::FOR_OF_DONE => { - return Err("opcode FOR_OF_DONE not yet implemented".into()); + self.dispatch_for_of_done(rd)?; } OpCode::FOR_OF_CLOSE => { - return Err("opcode FOR_OF_CLOSE not yet implemented".into()); + self.dispatch_for_of_close()?; + } + + OpCode::REST_OBJECT => { + self.dispatch_rest_object(rd, a)?; } OpCode::THROW => match self.dispatch_throw(rd) { @@ -1157,7 +1185,7 @@ mod tests { continuation: super::FrameContinuation::None, }); vm.for_in_iters.push(std::ptr::dangling_mut::>()); - vm.for_of_iters.push(std::ptr::dangling_mut::()); + vm.for_of_iters.push(JsValue::undefined()); vm.saved_bytecode_stack .push(vec![opcode::encode(opcode::OpCode::HALT, 0, 0, 0)]); vm.saved_constants_stack.push(vec![JsValue::int(1)]); @@ -1201,22 +1229,21 @@ mod tests { } #[test] - fn for_of_opcodes_fail_explicitly() { - for (op, expected) in [ - (opcode::OpCode::FOR_OF_INIT, "opcode FOR_OF_INIT not yet implemented"), - (opcode::OpCode::FOR_OF_NEXT, "opcode FOR_OF_NEXT not yet implemented"), - (opcode::OpCode::FOR_OF_DONE, "opcode FOR_OF_DONE not yet implemented"), - (opcode::OpCode::FOR_OF_CLOSE, "opcode FOR_OF_CLOSE not yet implemented"), - ] { - let module = CompiledModule { - bytecode: vec![opcode::encode(op, 0, 0, 0), opcode::encode(opcode::OpCode::HALT, 0, 0, 0)], - n_registers: 1, - ..CompiledModule::new() - }; - let mut vm = Vm::new(); - let err = vm.run(&module).expect_err("FOR_OF opcode should fail explicitly"); - assert_eq!(err, expected); - } + fn for_of_close_pops_iterator_stack() { + let module = CompiledModule { + bytecode: vec![ + opcode::encode(opcode::OpCode::FOR_OF_CLOSE, 0, 0, 0), + opcode::encode(opcode::OpCode::HALT, 0, 0, 0), + ], + n_registers: 1, + ..CompiledModule::new() + }; + let mut vm = Vm::new(); + vm.for_of_iters.push(JsValue::undefined()); + + vm.run(&module).expect("FOR_OF_CLOSE should tolerate non-object sentinel"); + + assert!(vm.for_of_iters.is_empty()); } #[test] diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index de2b716..a9f130e 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -327,4 +327,131 @@ impl Vm { pub(crate) fn dispatch_for_in_cleanup(&mut self) { self.for_in_iters.pop(); } + + pub(crate) fn dispatch_for_of_init(&mut self, a: usize) -> Result<(), String> { + let iterable = self.regs[a]; + match crate::builtins::iterator::make_iterator_for_value(self, iterable) { + Ok(iterator) => { + self.for_of_iters.push(iterator); + self.last_for_of_result = JsValue::undefined(); + Ok(()) + } + Err(err) => { + self.exception_value = Some(err); + self.pending_error_kind = Some(self.thrown_error_kind(err)); + self.unwind() + } + } + } + + pub(crate) fn dispatch_for_of_done(&mut self, rd: usize) -> Result<(), String> { + let Some(iterator) = self.for_of_iters.last().copied() else { + return Err("FOR_OF_DONE without active iterator".into()); + }; + if !iterator.is_object() { + return Err("FOR_OF_DONE iterator is not an object".into()); + } + + let iter_obj = unsafe { &*iterator.as_js_object_ptr() }; + let next_si = self.kernel_core.string_forge().intern("next").0; + let next_fn = self.ordinary_get(iter_obj, next_si, iterator)?; + let result = self.call_function_sync(next_fn, iterator, &[])?; + if !result.is_object() { + return self.raise_type_error("iterator result is not an object"); + } + + let result_obj = unsafe { &*result.as_js_object_ptr() }; + let done_si = self.kernel_core.string_forge().intern("done").0; + let done_val = self.ordinary_get(result_obj, done_si, result)?; + let done = crate::coercion::to_boolean(done_val, self.kernel_core.string_forge().as_ref()); + self.last_for_of_result = result; + self.regs[rd] = JsValue::bool(!done); + Ok(()) + } + + pub(crate) fn dispatch_for_of_next(&mut self, rd: usize) -> Result<(), String> { + let result = self.last_for_of_result; + if !result.is_object() { + self.regs[rd] = JsValue::undefined(); + return Ok(()); + } + let result_obj = unsafe { &*result.as_js_object_ptr() }; + let value_si = self.kernel_core.string_forge().intern("value").0; + self.regs[rd] = self.ordinary_get(result_obj, value_si, result)?; + Ok(()) + } + + pub(crate) fn dispatch_for_of_close(&mut self) -> Result<(), String> { + let Some(iterator) = self.for_of_iters.pop() else { + return Ok(()); + }; + if !iterator.is_object() { + return Ok(()); + } + let iter_obj = unsafe { &*iterator.as_js_object_ptr() }; + let return_si = self.kernel_core.string_forge().intern("return").0; + let return_fn = self.ordinary_get(iter_obj, return_si, iterator)?; + if return_fn.is_object() { + let return_obj = unsafe { &*return_fn.as_js_object_ptr() }; + if return_obj.is_function() { + let _ = self.call_function_sync(return_fn, iterator, &[])?; + } + } + Ok(()) + } + + pub(crate) fn dispatch_rest_object(&mut self, rd: usize, a: usize) -> Result<(), String> { + let src = self.regs[a]; + if !src.is_object() { + return self.raise_type_error("Cannot destructure property of null/undefined"); + } + let excluded_idx = self.bytecode[self.pc] as usize; + self.pc += 1; + let excluded = self + .constants + .get(excluded_idx) + .and_then(|v| { + if v.is_string() { + self.kernel_core.string_forge().lookup(v.as_string_index()) + } else { + None + } + }) + .unwrap_or_default(); + let excluded: std::collections::HashSet<&str> = excluded.split('\0').filter(|s| !s.is_empty()).collect(); + + let proto_ptr = self.session.builtin_world().object_proto.as_ptr() as *mut JsObject; + let rest_ptr = self.epoch.alloc(JsObject::new_empty( + oxide_kernel::shape_forge::EMPTY_SHAPE_ID, + JsValue::from_js_object(proto_ptr), + )); + let src_obj = unsafe { &*src.as_js_object_ptr() }; + let mut cursor = Some(src_obj.shape_id()); + while let Some(shape_id) = cursor { + if shape_id == oxide_kernel::shape_forge::EMPTY_SHAPE_ID { + break; + } + let Some(shape) = self.kernel_core.shape_forge().get_shape(shape_id) else { + break; + }; + if shape.property_name != u32::MAX { + if let Some(name) = self.kernel_core.string_forge().lookup(shape.property_name) { + if !excluded.contains(name.as_str()) { + if let Some(pos) = self + .kernel_core + .shape_forge() + .lookup_position(src_obj.shape_id(), shape.property_name) + { + let val = src_obj.get_prop_at(pos); + let rest = unsafe { &mut *rest_ptr }; + self.set_or_create_prop_value(rest, shape.property_name, val); + } + } + } + } + cursor = shape.parent; + } + self.regs[rd] = JsValue::from_js_object(rest_ptr); + Ok(()) + } } diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 78270ac..34c0569 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -23,6 +23,13 @@ impl Vm { if obj.is_array() && prop_name_si == length_si { return Ok(JsValue::int(obj.prop_count() as i32)); } + if obj.is_array() { + if let Some(index) = self.array_index_from_property_key(prop_name_si) { + if index < obj.prop_vec_len() as u32 { + return Ok(obj.get_prop_at(index)); + } + } + } if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { if let Some(meta) = obj.prop_meta_at(pos) { if meta.is_accessor { diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index 514b56f..1367df6 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -40,6 +40,8 @@ impl Vm { pending_exception: self.pending_exception.take(), pending_error_kind: self.pending_error_kind.take(), for_in_iters: std::mem::take(&mut self.for_in_iters), + for_of_iters: std::mem::take(&mut self.for_of_iters), + last_for_of_result: self.last_for_of_result, saved_bytecode_stack: std::mem::take(&mut self.saved_bytecode_stack), saved_constants_stack: std::mem::take(&mut self.saved_constants_stack), }); @@ -78,6 +80,8 @@ impl Vm { self.pending_exception = saved.pending_exception; self.pending_error_kind = saved.pending_error_kind; self.for_in_iters = saved.for_in_iters; + self.for_of_iters = saved.for_of_iters; + self.last_for_of_result = saved.last_for_of_result; self.saved_bytecode_stack = saved.saved_bytecode_stack; self.saved_constants_stack = saved.saved_constants_stack; diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 079c5b7..3549538 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -44,6 +44,7 @@ impl Vm { symbol_descriptions: Vec::new(), symbol_registry: std::collections::HashMap::new(), for_of_iters: Vec::new(), + last_for_of_result: JsValue::undefined(), root_reg_limit: 0, active_reg_limit: 0, native_call_depth: 0, @@ -80,6 +81,7 @@ impl Vm { symbol_descriptions: Vec::new(), symbol_registry: std::collections::HashMap::new(), for_of_iters: Vec::new(), + last_for_of_result: JsValue::undefined(), root_reg_limit: 0, active_reg_limit: 0, native_call_depth: 0, @@ -115,6 +117,7 @@ impl Vm { self.frames.clear(); self.for_in_iters.clear(); self.for_of_iters.clear(); + self.last_for_of_result = JsValue::undefined(); self.saved_bytecode_stack.clear(); self.saved_constants_stack.clear(); self.try_stack.clear(); diff --git a/crates/oxide_vm/tests/destructuring_tests.rs b/crates/oxide_vm/tests/destructuring_tests.rs new file mode 100644 index 0000000..9c65188 --- /dev/null +++ b/crates/oxide_vm/tests/destructuring_tests.rs @@ -0,0 +1,75 @@ +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) +} + +fn assert_num(value: JsValue, expected: f64) { + let actual = if value.is_int() { value.as_int() as f64 } else { value.as_double() }; + assert!((actual - expected).abs() < 0.0001, "expected {expected}, got {value:?}"); +} + +#[test] +fn for_of_array_values() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "let sum=0; for (const x of [1,2,3]) { sum += x; } sum").unwrap(); + assert_num(result, 6.0); +} + +#[test] +fn array_binding_and_defaults() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "const [a,b=4]=[1]; a+b").unwrap(); + assert_num(result, 5.0); +} + +#[test] +fn array_rest_keeps_numeric_index_access() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "const [a,...rest]=[1,2,3]; rest[0] + rest[1] + rest.length").unwrap(); + assert_num(result, 7.0); +} + +#[test] +fn object_binding_rest_and_computed_key() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "const key='x'; const {[key]: a, ...rest}={x:1,y:2}; a + rest.y").unwrap(); + assert_num(result, 3.0); +} + +#[test] +fn nested_binding_patterns() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "const [[a], {b}] = [[1], {b:2}]; a+b").unwrap(); + assert_num(result, 3.0); +} + +#[test] +fn destructuring_assignment_swap_and_object_target() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "let a=1,b=2; [a,b]=[b,a]; ({x:a,y:b}={x:3,y:4}); a*10+b").unwrap(); + assert_num(result, 34.0); +} + +#[test] +fn for_of_destructuring_left_side() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "let sum=0; for (const [k,v] of [[1,2],[3,4]]) { sum += k + v; } sum").unwrap(); + assert_num(result, 10.0); +} + +#[test] +fn function_and_method_parameter_destructuring() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "function f([a=4], {x}) { return a+x; } class C { m({y}) { return y; } } f([], {x:1}) + new C().m({y:5})", + ) + .unwrap(); + assert_num(result, 10.0); +}