From a654b5834fff5911e5184d2a1c3e59e93bcba51c Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Tue, 9 Jun 2026 16:22:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=AE=BF=E9=97=AE?= =?UTF-8?q?=E5=99=A8=E6=8F=8F=E8=BF=B0=E7=AC=A6=E4=B8=8E=20get/set=20?= =?UTF-8?q?=E8=AF=AD=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_compiler/src/counter.rs | 20 ++- crates/oxide_compiler/src/emitter.rs | 101 ++++++++++++-- crates/oxide_compiler/src/hash.rs | 23 +++- crates/oxide_compiler/src/opcode.rs | 3 + .../tests/compile_expr_tests.rs | 18 +++ .../tests/compile_stmt_tests.rs | 18 +++ crates/oxide_vm/src/builtins/object.rs | 123 +++++++++++++----- crates/oxide_vm/src/dispatch/call.rs | 20 +-- crates/oxide_vm/src/dispatch/property.rs | 32 ++--- crates/oxide_vm/src/vm.rs | 98 +++++++++++++- .../oxide_vm/tests/builtins_object_tests.rs | 42 ++++++ crates/oxide_vm/tests/class_tests.rs | 21 +++ crates/oxide_vm/tests/object_tests.rs | 15 +++ 13 files changed, 457 insertions(+), 77 deletions(-) diff --git a/crates/oxide_compiler/src/counter.rs b/crates/oxide_compiler/src/counter.rs index 30aecc36..d1c83961 100644 --- a/crates/oxide_compiler/src/counter.rs +++ b/crates/oxide_compiler/src/counter.rs @@ -33,7 +33,11 @@ impl Compiler { ctx.alloc_reg(); // key_reg ctx.projected_pc += 4; // LOAD_CONST method + SET_HOME_OBJECT + LOAD_CONST key + SET_PROP } - MethodDefinitionKind::Get | MethodDefinitionKind::Set => {} + MethodDefinitionKind::Get | MethodDefinitionKind::Set => { + ctx.alloc_reg(); // method_reg + ctx.alloc_reg(); // undefined getter/setter placeholder + ctx.projected_pc += 5; // LOAD_CONST method + SET_HOME_OBJECT + LOAD_CONST undefined + DEFINE_ACCESSOR + ext + } } } @@ -463,10 +467,16 @@ impl Compiler { ctx.projected_pc += 1; // NEW_OBJECT for prop in &obj.properties { if let oxide_parser::ObjectPropertyKind::ObjectProperty(p) = prop { - ctx.alloc_reg(); - ctx.projected_pc += 1; // LOAD_CONST key - self.count_expression(&p.value, ctx); - ctx.projected_pc += 1; // SET_PROP + if matches!(p.kind, oxide_parser::PropertyKind::Get | oxide_parser::PropertyKind::Set) { + self.count_expression(&p.value, ctx); + ctx.alloc_reg(); // undefined getter/setter placeholder + ctx.projected_pc += 3; // LOAD_CONST undefined + DEFINE_ACCESSOR + ext + } else { + ctx.alloc_reg(); + ctx.projected_pc += 1; // LOAD_CONST key + self.count_expression(&p.value, ctx); + ctx.projected_pc += 1; // SET_PROP + } } } } diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index 35bb8c7d..fa854dae 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -2,7 +2,8 @@ use crate::compiler::Label; use crate::opcode::{self, OpCode}; use oxide_parser::{ AssignmentOperator, Class, ClassElement, Expression, ForStatementInit, ForStatementLeft, MethodDefinitionKind, - PropertyKey, SimpleAssignmentTarget, Statement, UnaryOperator, UpdateOperator, VariableDeclarationKind, + PropertyKey, PropertyKind, SimpleAssignmentTarget, Statement, UnaryOperator, UpdateOperator, + VariableDeclarationKind, }; use crate::compiler::{is_int_literal, is_side_effect_free, BinaryOperator, CompileCtx, Compiler}; @@ -22,6 +23,8 @@ impl Compiler { let mut constructor_method = None; let mut instance_methods = Vec::new(); let mut static_methods = Vec::new(); + let mut instance_accessors = Vec::new(); + let mut static_accessors = Vec::new(); let is_derived = class.super_class.is_some(); for element in &class.body.body { @@ -49,7 +52,12 @@ impl Compiler { } } MethodDefinitionKind::Get | MethodDefinitionKind::Set => { - return Err("getters/setters not yet supported".into()); + let name = self.class_property_name(&method.key)?; + if method.r#static { + static_accessors.push((method, name)); + } else { + instance_accessors.push((method, name)); + } } } } @@ -128,6 +136,19 @@ impl Compiler { ctx.emit(opcode::encode(OpCode::SET_PROP, proto_reg, method_reg, key_reg)); } + for (method, method_name) in instance_accessors { + let accessor_reg = self.emit_class_method_function(method, &method_name, proto_reg, ctx, &self_binding)?; + let undef_reg = self.emit_undefined(ctx); + let (get_reg, set_reg) = match method.kind { + MethodDefinitionKind::Get => (accessor_reg, undef_reg), + MethodDefinitionKind::Set => (undef_reg, accessor_reg), + _ => unreachable!(), + }; + let key_idx = ctx.add_constant(Constant::String(method_name)); + ctx.emit(opcode::encode(OpCode::DEFINE_ACCESSOR, proto_reg, get_reg, set_reg)); + ctx.emit(key_idx as u32); + } + for (method, method_name) in static_methods { let (param_names, body_stmts) = self.extract_function_parts(method.value.as_ref())?; let saved_static = ctx.in_static_method; @@ -150,6 +171,19 @@ impl Compiler { ctx.emit(opcode::encode(OpCode::SET_PROP, ctor_reg, method_reg, key_reg)); } + for (method, method_name) in static_accessors { + let accessor_reg = self.emit_class_method_function(method, &method_name, ctor_reg, ctx, &self_binding)?; + let undef_reg = self.emit_undefined(ctx); + let (get_reg, set_reg) = match method.kind { + MethodDefinitionKind::Get => (accessor_reg, undef_reg), + MethodDefinitionKind::Set => (undef_reg, accessor_reg), + _ => unreachable!(), + }; + let key_idx = ctx.add_constant(Constant::String(method_name)); + ctx.emit(opcode::encode(OpCode::DEFINE_ACCESSOR, ctor_reg, get_reg, set_reg)); + ctx.emit(key_idx as u32); + } + let ctor_key_idx = ctx.add_constant(Constant::String("constructor".to_string())); let ctor_key_reg = ctx.alloc_reg(); ctx.emit_load_const(ctor_key_reg, ctor_key_idx); @@ -163,6 +197,37 @@ impl Compiler { Ok(ctor_reg) } + fn emit_undefined(&self, ctx: &mut CompileCtx) -> u8 { + let idx = ctx.add_constant(Constant::Undefined); + let reg = ctx.alloc_reg(); + ctx.emit_load_const(reg, idx); + reg + } + + fn emit_class_method_function( + &self, method: &oxide_parser::MethodDefinition, method_name: &str, home_reg: u8, ctx: &mut CompileCtx, + self_binding: &[(&str, u8)], + ) -> Result { + let (param_names, body_stmts) = self.extract_function_parts(method.value.as_ref())?; + let saved_instance = ctx.in_instance_method; + let saved_static = ctx.in_static_method; + ctx.in_instance_method = !method.r#static; + ctx.in_static_method = method.r#static; + let mut method_module = + self.compile_function_body_with_bindings(¶m_names, body_stmts, ctx, false, self_binding)?; + ctx.in_instance_method = saved_instance; + ctx.in_static_method = saved_static; + method_module.function_name = Some(method_name.to_string()); + method_module.needs_home_object = true; + ctx.sub_modules.push(method_module); + + let method_idx = ctx.add_constant(Constant::BytecodeFunc(ctx.sub_modules.len() as u32)); + let method_reg = ctx.alloc_reg(); + ctx.emit_load_const(method_reg, method_idx); + ctx.emit(opcode::encode(OpCode::SET_HOME_OBJECT, method_reg, home_reg, 0)); + Ok(method_reg) + } + pub(crate) fn emit_statement(&self, stmt: &Statement, ctx: &mut CompileCtx) -> Result, String> { match stmt { Statement::ExpressionStatement(es) => Ok(Some(self.emit_expression(&es.expression, ctx)?)), @@ -871,18 +936,34 @@ impl Compiler { oxide_parser::PropertyKey::StringLiteral(s) => s.value.to_string(), _ => return Err("unsupported object property key type".into()), }; - let idx = ctx.add_constant(Constant::String(prop_name.to_string())); - let key_reg = ctx.alloc_reg(); - ctx.emit_load_const(key_reg, idx); - let val_reg = self.emit_expression(&p.value, ctx)?; - // Name inference (D-04): if property value is an arrow function, - // set the compiled sub_module's function_name to the property key. - if matches!(&p.value, Expression::ArrowFunctionExpression(_)) { + if matches!(p.kind, PropertyKind::Get | PropertyKind::Set) { + let accessor_reg = self.emit_expression(&p.value, ctx)?; if let Some(sub_mod) = ctx.sub_modules.last_mut() { sub_mod.function_name = Some(prop_name.to_string()); } + let undef_reg = self.emit_undefined(ctx); + let (get_reg, set_reg) = match p.kind { + PropertyKind::Get => (accessor_reg, undef_reg), + PropertyKind::Set => (undef_reg, accessor_reg), + _ => unreachable!(), + }; + 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); + } else { + let idx = ctx.add_constant(Constant::String(prop_name.to_string())); + let key_reg = ctx.alloc_reg(); + ctx.emit_load_const(key_reg, idx); + let val_reg = self.emit_expression(&p.value, ctx)?; + // Name inference (D-04): if property value is an arrow function, + // set the compiled sub_module's function_name to the property key. + if matches!(&p.value, Expression::ArrowFunctionExpression(_)) { + if let Some(sub_mod) = ctx.sub_modules.last_mut() { + sub_mod.function_name = Some(prop_name.to_string()); + } + } + ctx.emit(opcode::encode(OpCode::SET_PROP, obj_reg, val_reg, key_reg)); } - ctx.emit(opcode::encode(OpCode::SET_PROP, obj_reg, val_reg, key_reg)); } Ok(obj_reg) } diff --git a/crates/oxide_compiler/src/hash.rs b/crates/oxide_compiler/src/hash.rs index dbf9ea0b..0ecb7887 100644 --- a/crates/oxide_compiler/src/hash.rs +++ b/crates/oxide_compiler/src/hash.rs @@ -1,4 +1,6 @@ -use oxide_parser::{ClassElement, Expression, ForStatementInit, PropertyKey, SimpleAssignmentTarget, Statement}; +use oxide_parser::{ + ClassElement, Expression, ForStatementInit, ObjectPropertyKind, PropertyKey, SimpleAssignmentTarget, Statement, +}; pub fn structural_hash(program: &oxide_parser::Program) -> u64 { use std::hash::Hasher; @@ -329,6 +331,25 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { hash_class_element(element, h); } } + Expression::ObjectExpression(obj) => { + 20u8.hash(h); + (obj.properties.len() as u32).hash(h); + for prop in &obj.properties { + match prop { + ObjectPropertyKind::ObjectProperty(p) => { + 0u8.hash(h); + std::mem::discriminant(&p.kind).hash(h); + p.method.hash(h); + p.computed.hash(h); + hash_property_key(&p.key, h); + hash_expression(&p.value, h); + } + ObjectPropertyKind::SpreadProperty(_) => { + 1u8.hash(h); + } + } + } + } Expression::NewExpression(ne) => { 14u8.hash(h); hash_expression(&ne.callee, h); diff --git a/crates/oxide_compiler/src/opcode.rs b/crates/oxide_compiler/src/opcode.rs index 41788e7e..91ced12e 100644 --- a/crates/oxide_compiler/src/opcode.rs +++ b/crates/oxide_compiler/src/opcode.rs @@ -88,6 +88,7 @@ pub enum OpCode { SUPER_GET_PROP = 0x45, SUPER_STATIC_GET_PROP = 0x46, SET_HOME_OBJECT = 0x47, + DEFINE_ACCESSOR = 0x48, // ── Object Property (0x50-0x5F) ── IC_GET_PROP = 0x50, @@ -240,6 +241,7 @@ impl TryFrom for OpCode { 0x45 => Ok(OpCode::SUPER_GET_PROP), 0x46 => Ok(OpCode::SUPER_STATIC_GET_PROP), 0x47 => Ok(OpCode::SET_HOME_OBJECT), + 0x48 => Ok(OpCode::DEFINE_ACCESSOR), 0x50 => Ok(OpCode::IC_GET_PROP), 0x51 => Ok(OpCode::IC_SET_PROP), 0x52 => Ok(OpCode::GET_PROP), @@ -351,6 +353,7 @@ impl fmt::Display for OpCode { OpCode::SUPER_GET_PROP => "SUPER_GET_PROP", OpCode::SUPER_STATIC_GET_PROP => "SUPER_STATIC_GET_PROP", OpCode::SET_HOME_OBJECT => "SET_HOME_OBJECT", + OpCode::DEFINE_ACCESSOR => "DEFINE_ACCESSOR", OpCode::IC_GET_PROP => "IC_GET_PROP", OpCode::IC_SET_PROP => "IC_SET_PROP", OpCode::GET_PROP => "GET_PROP", diff --git a/crates/oxide_compiler/tests/compile_expr_tests.rs b/crates/oxide_compiler/tests/compile_expr_tests.rs index b571a45d..b3dd0bec 100644 --- a/crates/oxide_compiler/tests/compile_expr_tests.rs +++ b/crates/oxide_compiler/tests/compile_expr_tests.rs @@ -72,6 +72,24 @@ fn compile_multiple_stmts() { assert!(load_count >= 3); } +#[test] +fn compile_object_literal_getter_emits_define_accessor() { + let module = compile_source("var o = { get x() { return 1; } };"); + assert!( + module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::DEFINE_ACCESSOR), + "object literal getter should emit DEFINE_ACCESSOR" + ); +} + +#[test] +fn compile_object_literal_setter_emits_define_accessor() { + let module = compile_source("var o = { set x(v) { this.y = v; } };"); + assert!( + module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::DEFINE_ACCESSOR), + "object literal setter should emit DEFINE_ACCESSOR" + ); +} + #[test] fn compile_binary_ops() { let tests = [ diff --git a/crates/oxide_compiler/tests/compile_stmt_tests.rs b/crates/oxide_compiler/tests/compile_stmt_tests.rs index efe62831..dc300163 100644 --- a/crates/oxide_compiler/tests/compile_stmt_tests.rs +++ b/crates/oxide_compiler/tests/compile_stmt_tests.rs @@ -278,3 +278,21 @@ fn compile_class_declaration_default_constructor_creates_submodule() { "default constructor submodule should be marked as class constructor" ); } + +#[test] +fn compile_class_instance_getter_emits_define_accessor() { + let module = compile_source("class A { get x() { return 1; } }"); + assert!( + module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::DEFINE_ACCESSOR), + "class getter should emit DEFINE_ACCESSOR" + ); +} + +#[test] +fn compile_class_static_setter_emits_define_accessor() { + let module = compile_source("class A { static set x(v) { this.y = v; } }"); + assert!( + module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::DEFINE_ACCESSOR), + "static class setter should emit DEFINE_ACCESSOR" + ); +} diff --git a/crates/oxide_vm/src/builtins/object.rs b/crates/oxide_vm/src/builtins/object.rs index e3081b72..e6990526 100644 --- a/crates/oxide_vm/src/builtins/object.rs +++ b/crates/oxide_vm/src/builtins/object.rs @@ -1,6 +1,6 @@ use oxide_kernel::shape_forge::{ShapeForge, EMPTY_SHAPE_ID}; use oxide_kernel::string_forge::StringForge; -use oxide_types::object::JsObject; +use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::value::JsValue; use crate::coercion; @@ -153,18 +153,54 @@ pub fn object_define_property(vm: &mut Vm, args: &[u8]) -> NativeResult { } let prop_name_str = coercion::to_string(vm.kernel().string_forge().as_ref(), vm.reg(args[2])); let si = vm.kernel().string_forge().intern(&prop_name_str).0; - let shape_forge = vm.kernel().shape_forge().as_ref(); - let new_shape = shape_forge.make_shape(unsafe { (&*obj_ptr).shape_id() }, si); - let value = { - let desc = unsafe { &*desc_ptr }; - desc.hash_props_vec() - .and_then(|v| v.first().map(|b| **b)) - .unwrap_or(JsValue::undefined()) - }; - { - let obj = unsafe { &mut *obj_ptr }; - obj.set_shape_id(new_shape); - obj.ensure_hash_props().push(Box::new(value)); + + let value_si = vm.kernel().string_forge().intern("value").0; + let get_si = vm.kernel().string_forge().intern("get").0; + let set_si = vm.kernel().string_forge().intern("set").0; + let writable_si = vm.kernel().string_forge().intern("writable").0; + let enumerable_si = vm.kernel().string_forge().intern("enumerable").0; + let configurable_si = vm.kernel().string_forge().intern("configurable").0; + + let desc = unsafe { &*desc_ptr }; + let value_field = own_field(vm, desc, value_si); + let get_field = own_field(vm, desc, get_si); + let set_field = own_field(vm, desc, set_si); + let writable_field = own_field(vm, desc, writable_si); + let enumerable = own_field(vm, desc, enumerable_si) + .map(|v| coercion::to_boolean(v, vm.kernel().string_forge().as_ref())) + .unwrap_or(false); + let configurable = own_field(vm, desc, configurable_si) + .map(|v| coercion::to_boolean(v, vm.kernel().string_forge().as_ref())) + .unwrap_or(false); + + let has_data = value_field.is_some() || writable_field.is_some(); + let has_accessor = get_field.is_some() || set_field.is_some(); + if has_data && has_accessor { + return Err(crate::builtins::error::create_type_error( + vm, + "Invalid property descriptor: cannot mix data and accessor fields", + )); + } + + let obj = unsafe { &mut *obj_ptr }; + if has_accessor { + let get = get_field.unwrap_or(JsValue::undefined()); + let set = set_field.unwrap_or(JsValue::undefined()); + if (!get.is_undefined() && !is_callable(get)) || (!set.is_undefined() && !is_callable(set)) { + return Err(crate::builtins::error::create_type_error( + vm, + "accessor descriptor get/set must be callable or undefined", + )); + } + vm.define_accessor_property(obj, si, get, set, PropAttributes::new(false, enumerable, configurable)) + .map_err(|_| crate::builtins::error::create_type_error(vm, "Cannot define property"))?; + } else { + let value = value_field.unwrap_or(JsValue::undefined()); + let writable = writable_field + .map(|v| coercion::to_boolean(v, vm.kernel().string_forge().as_ref())) + .unwrap_or(false); + vm.define_data_property(obj, si, value, PropAttributes::new(writable, enumerable, configurable)) + .map_err(|_| crate::builtins::error::create_type_error(vm, "Cannot define property"))?; } Ok(obj_val) } @@ -184,19 +220,21 @@ pub fn object_get_own_property_descriptor(vm: &mut Vm, args: &[u8]) -> NativeRes let prop_name_str = coercion::to_string(vm.kernel().string_forge().as_ref(), vm.reg(args[2])); let si = vm.kernel().string_forge().intern(&prop_name_str).0; - let (found_value, found) = { + let (found_value, found_meta, found) = { let obj = unsafe { &*obj_ptr }; let keys = walk_own_keys(vm, obj); let mut found_value = JsValue::undefined(); + let mut found_meta = None; let mut found = false; for (prop_si, offset) in keys { if prop_si == si { found_value = obj.get_prop_at(offset); + found_meta = obj.prop_meta_at(offset); found = true; break; } } - (found_value, found) + (found_value, found_meta, found) }; if !found { @@ -209,34 +247,49 @@ pub fn object_get_own_property_descriptor(vm: &mut Vm, args: &[u8]) -> NativeRes let desc = vm .epoch() .alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(desc_proto))); - let true_val = JsValue::bool(true); let sf = unsafe { &*sf_ptr }; let sh = unsafe { &*sh_ptr }; let d: &mut JsObject = unsafe { &mut *desc }; - let si_val = sf.intern("value").0; - let sh_val = sh.make_shape(EMPTY_SHAPE_ID, si_val); - d.set_shape_id(sh_val); - d.ensure_hash_props().push(Box::new(found_value)); - - let si_wr = sf.intern("writable").0; - let sh_wr = sh.make_shape(sh_val, si_wr); - d.set_shape_id(sh_wr); - d.ensure_hash_props().push(Box::new(true_val)); - - let si_en = sf.intern("enumerable").0; - let sh_en = sh.make_shape(sh_wr, si_en); - d.set_shape_id(sh_en); - d.ensure_hash_props().push(Box::new(true_val)); - - let si_cf = sf.intern("configurable").0; - let sh_cf = sh.make_shape(sh_en, si_cf); - d.set_shape_id(sh_cf); - d.ensure_hash_props().push(Box::new(true_val)); + let meta = found_meta.unwrap_or_else(|| oxide_types::object::PropMetaEntry::data(PropAttributes::DEFAULT_DATA)); + if meta.is_accessor { + push_desc_prop(d, sh, sf.intern("get").0, meta.get); + push_desc_prop(d, sh, sf.intern("set").0, meta.set); + push_desc_prop(d, sh, sf.intern("enumerable").0, JsValue::bool(meta.attributes.enumerable())); + push_desc_prop(d, sh, sf.intern("configurable").0, JsValue::bool(meta.attributes.configurable())); + } else { + push_desc_prop(d, sh, sf.intern("value").0, found_value); + push_desc_prop(d, sh, sf.intern("writable").0, JsValue::bool(meta.attributes.writable())); + push_desc_prop(d, sh, sf.intern("enumerable").0, JsValue::bool(meta.attributes.enumerable())); + push_desc_prop(d, sh, sf.intern("configurable").0, JsValue::bool(meta.attributes.configurable())); + } Ok(JsValue::from_js_object(desc)) } +fn own_field(vm: &Vm, obj: &JsObject, prop_si: u32) -> Option { + vm.kernel() + .shape_forge() + .lookup_position(obj.shape_id(), prop_si) + .and_then(|pos| { + if obj.prop_vec_len() > pos as usize { + Some(obj.get_prop_at(pos)) + } else { + None + } + }) +} + +fn is_callable(value: JsValue) -> bool { + value.is_object() && unsafe { &*value.as_js_object_ptr() }.is_function() +} + +fn push_desc_prop(obj: &mut JsObject, shape_forge: &ShapeForge, prop_si: u32, val: JsValue) { + let shape_id = shape_forge.make_shape(obj.shape_id(), prop_si); + obj.set_shape_id(shape_id); + obj.push_prop(val); +} + fn require_obj_arg(vm: &mut Vm, args: &[u8], fn_name: &str) -> Result<*mut JsObject, JsValue> { if args.len() < 2 { return Err(crate::builtins::error::create_type_error( diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index 5e9e60a8..72d58c3c 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -30,17 +30,21 @@ impl Vm { Ok(()) } Err(err_val) => { - let msg = if err_val.is_string() { - self.kernel() - .string_forge() - .lookup(err_val.as_string_index()) - .unwrap_or_else(|| format!("{err_val}")) + let (error, kind) = if err_val.is_object() { + (err_val, self.thrown_error_kind(err_val)) } else { - format!("{err_val}") + let msg = if err_val.is_string() { + self.kernel() + .string_forge() + .lookup(err_val.as_string_index()) + .unwrap_or_else(|| format!("{err_val}")) + } else { + format!("{err_val}") + }; + (crate::builtins::error::create_error(self, &msg), "Error") }; - let error = crate::builtins::error::create_error(self, &msg); self.exception_value = Some(error); - self.pending_error_kind = Some("Error"); + self.pending_error_kind = Some(kind); self.unwind() } } diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index f41deca1..7cb78a4f 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -127,21 +127,23 @@ impl Vm { self.write_ic_back(obj.shape_id(), ptr); } } else { - let new_shape_id = self.kernel.shape_forge().make_shape(obj.shape_id(), prop_name_si); - obj.set_shape_id(new_shape_id); - let new_pos = obj.push_prop(value); - obj.bump_generation(); - if let Some(ptr) = obj.prop_ptr_at(new_pos) { - self.write_ic_back(new_shape_id, ptr); - self.kernel.prop_forge().upsert( - new_shape_id, - PropTemplate { - shape_id: new_shape_id, - prop_name: prop_name_si, - position: new_pos, - generation: obj.generation(), - }, - ); + let old_shape = obj.shape_id(); + self.ordinary_set(obj, prop_name_si, value, receiver)?; + if old_shape != obj.shape_id() { + if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + if let Some(ptr) = obj.prop_ptr_at(pos) { + self.write_ic_back(obj.shape_id(), ptr); + self.kernel.prop_forge().upsert( + obj.shape_id(), + PropTemplate { + shape_id: obj.shape_id(), + prop_name: prop_name_si, + position: pos, + generation: obj.generation(), + }, + ); + } + } } } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index fcaf59a6..f3f92e9a 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -14,7 +14,7 @@ use oxide_kernel::kernel::{KernelConfig, OxideKernel}; use oxide_kernel::prop_forge::PropTemplate; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::mem::{Epoch, P}; -use oxide_types::object::JsObject; +use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::value::JsValue; #[allow(unused_macros)] @@ -364,7 +364,7 @@ impl Vm { self.kernel.string_forge().lookup(val.as_string_index()) } - fn thrown_error_kind(&self, val: JsValue) -> &'static str { + pub(crate) fn thrown_error_kind(&self, val: JsValue) -> &'static str { if !val.is_object() { return "Error"; } @@ -700,6 +700,67 @@ impl Vm { } } + pub(crate) fn define_data_property( + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, attributes: PropAttributes, + ) -> Result<(), String> { + let pos = if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + pos + } else { + let new_shape_id = self.kernel.shape_forge().make_shape(obj.shape_id(), prop_name_si); + obj.set_shape_id(new_shape_id); + obj.push_prop(JsValue::undefined()) + }; + if let Some(current) = obj.prop_meta_at(pos) { + if !current.attributes.configurable() { + if current.is_accessor { + return self.raise_type_error("cannot redefine non-configurable property"); + } + if current.attributes.enumerable() != attributes.enumerable() { + return self.raise_type_error("cannot redefine non-configurable property"); + } + if !current.attributes.writable() + && (attributes.writable() || !coercion::same_value(obj.get_prop_at(pos), val)) + { + return self.raise_type_error("cannot redefine non-configurable property"); + } + if current.attributes.configurable() != attributes.configurable() { + return self.raise_type_error("cannot redefine non-configurable property"); + } + } + } + obj.set_prop_at(pos, val); + obj.set_data_meta(pos, attributes); + obj.bump_generation(); + Ok(()) + } + + pub(crate) fn define_accessor_property( + &mut self, obj: &mut JsObject, prop_name_si: u32, get: JsValue, set: JsValue, attributes: PropAttributes, + ) -> Result<(), String> { + let pos = if let Some(pos) = self.kernel.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + pos + } else { + let new_shape_id = self.kernel.shape_forge().make_shape(obj.shape_id(), prop_name_si); + obj.set_shape_id(new_shape_id); + obj.push_prop(JsValue::undefined()) + }; + if let Some(current) = obj.prop_meta_at(pos) { + if !current.attributes.configurable() + && (!current.is_accessor + || current.attributes.enumerable() != attributes.enumerable() + || current.attributes.configurable() != attributes.configurable() + || current.get != get + || current.set != set) + { + return self.raise_type_error("cannot redefine non-configurable property"); + } + } + obj.set_prop_at(pos, JsValue::undefined()); + obj.set_accessor_meta(pos, get, set, attributes); + obj.bump_generation(); + Ok(()) + } + pub(crate) fn write_ic_back(&mut self, shape_id: u32, ptr: *const JsValue) { debug_assert!(self.pc >= 3, "IC write-back requires 3 extension words before pc"); self.bytecode[self.pc - 3] = shape_id & 0x00FF_FFFF; @@ -1399,7 +1460,7 @@ impl Vm { self.regs[rd] = JsValue::undefined(); } else { let super_obj = unsafe { &*super_base.as_js_object_ptr() }; - self.regs[rd] = self.resolve_property(super_obj, prop_name_si).unwrap_or(JsValue::undefined()); + self.regs[rd] = self.ordinary_get(super_obj, prop_name_si, self.regs[a])?; } } @@ -1416,6 +1477,37 @@ impl Vm { func_obj.set_home_object(home_val); } + OpCode::DEFINE_ACCESSOR => { + let prop_idx = self.bytecode[self.pc] as usize; + self.pc += 1; + if prop_idx >= self.constants.len() { + throw_err!(self, TypeError, "DEFINE_ACCESSOR constant index out of bounds"); + } + let obj_val = self.regs[rd]; + if !obj_val.is_object() { + throw_err!(self, TypeError, "DEFINE_ACCESSOR target is not object"); + } + let prop_name_si = self.property_key_si(self.constants[prop_idx]); + let getter = self.regs[a]; + let setter = self.regs[b]; + let obj = unsafe { &mut *obj_val.as_js_object_ptr() }; + let existing = self + .get_own_property_slot(obj, prop_name_si) + .and_then(|pos| obj.prop_meta_at(pos)) + .filter(|meta| meta.is_accessor); + let get = if getter.is_undefined() { + existing.map(|meta| meta.get).unwrap_or(JsValue::undefined()) + } else { + getter + }; + let set = if setter.is_undefined() { + existing.map(|meta| meta.set).unwrap_or(JsValue::undefined()) + } else { + setter + }; + self.define_accessor_property(obj, prop_name_si, get, set, PropAttributes::DEFAULT_DATA)?; + } + OpCode::RETURN => { let result = self.regs[rd]; if let Some(frame) = self.frames.pop() { diff --git a/crates/oxide_vm/tests/builtins_object_tests.rs b/crates/oxide_vm/tests/builtins_object_tests.rs index 81c6c78a..5ff55204 100644 --- a/crates/oxide_vm/tests/builtins_object_tests.rs +++ b/crates/oxide_vm/tests/builtins_object_tests.rs @@ -55,6 +55,48 @@ fn object_define_property_sets_value() { assert!(result.is_object()); } +#[test] +fn object_define_property_data_defaults_non_writable() { + let (_vm, result) = + eval("var o={}; Object.defineProperty(o,'x',{value:1}); Object.getOwnPropertyDescriptor(o,'x').writable") + .unwrap(); + assert_eq!(result, JsValue::bool(false)); +} + +#[test] +fn object_define_property_accessor_descriptor_gets() { + let (_vm, result) = eval( + "var o={}; Object.defineProperty(o,'x',{get:function(){return 7}, enumerable:true, configurable:true}); o.x", + ) + .unwrap(); + assert_eq!(result, JsValue::int(7)); +} + +#[test] +fn object_define_property_rejects_mixed_descriptor() { + let err = match eval("var o={}; Object.defineProperty(o,'x',{value:1,get:function(){return 2}})") { + Ok(_) => panic!("expected mixed descriptor to fail"), + Err(err) => err, + }; + assert!(err.contains("TypeError")); +} + +#[test] +fn object_define_property_non_writable_assignment_throws() { + let err = match eval("var o={}; Object.defineProperty(o,'x',{value:1}); o.x=2") { + Ok(_) => panic!("expected assignment to fail"), + Err(err) => err, + }; + assert!(err.contains("TypeError")); +} + +#[test] +fn object_get_own_property_descriptor_accessor_shape() { + let (_vm, result) = + eval("var o={}; Object.defineProperty(o,'x',{get:function(){return 1}, configurable:true}); Object.getOwnPropertyDescriptor(o,'x').value").unwrap(); + assert!(result.is_undefined()); +} + #[test] fn object_get_prototype_of() { let (_vm, result) = eval("Object.getPrototypeOf({})").unwrap(); diff --git a/crates/oxide_vm/tests/class_tests.rs b/crates/oxide_vm/tests/class_tests.rs index 165d1674..e43f0c89 100644 --- a/crates/oxide_vm/tests/class_tests.rs +++ b/crates/oxide_vm/tests/class_tests.rs @@ -29,6 +29,27 @@ fn class_method_is_on_prototype() { assert_eq!(result.as_int(), 1); } +#[test] +fn class_instance_getter_returns_value() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "class A { get x() { return 2; } } new A().x").unwrap(); + assert_eq!(result.as_int(), 2); +} + +#[test] +fn class_instance_setter_updates_receiver() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "class A { set x(v) { this.y = v; } } var a = new A(); a.x = 4; a.y").unwrap(); + assert_eq!(result.as_int(), 4); +} + +#[test] +fn class_static_getter_returns_value() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "class A { static get x() { return 5; } } A.x").unwrap(); + assert_eq!(result.as_int(), 5); +} + #[test] fn class_constructor_initializes_instance_state() { let mut vm = Vm::new(); diff --git a/crates/oxide_vm/tests/object_tests.rs b/crates/oxide_vm/tests/object_tests.rs index de602b42..b40a0dff 100644 --- a/crates/oxide_vm/tests/object_tests.rs +++ b/crates/oxide_vm/tests/object_tests.rs @@ -137,3 +137,18 @@ fn eval_numeric_property_key_compound_update() { fn eval_in_operator_coerces_numeric_key() { assert_eq!(eval("var o={}; o[1]=42; 1 in o"), "true"); } + +#[test] +fn object_literal_getter_returns_value() { + assert_eq!(eval("var o={ get x(){ return 7 } }; o.x"), "7"); +} + +#[test] +fn object_literal_setter_updates_receiver() { + assert_eq!(eval("var o={ set x(v){ this.y=v } }; o.x=4; o.y"), "4"); +} + +#[test] +fn object_literal_getter_setter_pair() { + assert_eq!(eval("var o={ get x(){ return this.y }, set x(v){ this.y=v } }; o.x=9; o.x"), "9"); +}