From a0746366199c5f5feadb45d565755879d9158c6a Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 17 Jun 2026 08:56:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20class=20=E7=A7=81?= =?UTF-8?q?=E6=9C=89=E5=AD=97=E6=AE=B5=E4=B8=8E=E5=93=81=E7=89=8C=E6=A3=80?= =?UTF-8?q?=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_compiler/src/compiler.rs | 6 + crates/oxide_compiler/src/counter.rs | 31 ++++ crates/oxide_compiler/src/emitter.rs | 199 ++++++++++++++++++++--- crates/oxide_compiler/src/hash.rs | 15 ++ crates/oxide_compiler/src/opcode.rs | 12 ++ crates/oxide_kernel/src/shape_forge.rs | 10 ++ crates/oxide_test262/src/main.rs | 11 +- crates/oxide_types/src/lib.rs | 1 + crates/oxide_types/src/private_key.rs | 24 +++ crates/oxide_vm/src/builtins/object.rs | 5 +- crates/oxide_vm/src/dispatch/property.rs | 92 +++++++++++ crates/oxide_vm/src/vm.rs | 6 +- crates/oxide_vm/src/vm_dispatch_misc.rs | 8 +- crates/oxide_vm/tests/class_tests.rs | 98 +++++++++++ 14 files changed, 481 insertions(+), 37 deletions(-) create mode 100644 crates/oxide_types/src/private_key.rs diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 457967f1..5ea52fc7 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -144,6 +144,8 @@ pub(crate) struct CompileCtx { pub(crate) in_instance_method: bool, pub(crate) in_static_method: bool, pub(crate) static_block_this_reg: Option, + pub(crate) private_name_map: Vec<(String, u32)>, + pub(crate) next_private_name_id: u32, pub(crate) after_super_insert: Option>, pub(crate) after_super_inserted: bool, /// Set when alloc_reg() overflows into the reserved this/new.target range (≥254). @@ -208,6 +210,8 @@ impl CompileCtx { in_instance_method: false, in_static_method: false, static_block_this_reg: None, + private_name_map: Vec::new(), + next_private_name_id: 1, after_super_insert: None, after_super_inserted: false, reg_overflow: false, @@ -497,6 +501,8 @@ impl Compiler { // 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(); + ctx.private_name_map = parent_ctx.private_name_map.clone(); + ctx.next_private_name_id = parent_ctx.next_private_name_id; // Propagate enclosing_this_reg so nested arrow functions capture the correct `this`. ctx.enclosing_this_reg = parent_ctx.enclosing_this_reg; diff --git a/crates/oxide_compiler/src/counter.rs b/crates/oxide_compiler/src/counter.rs index 929941d1..695dabf9 100644 --- a/crates/oxide_compiler/src/counter.rs +++ b/crates/oxide_compiler/src/counter.rs @@ -132,6 +132,14 @@ impl Compiler { if matches!(method.kind, MethodDefinitionKind::Constructor) { continue; } + if matches!(method.key, oxide_parser::PropertyKey::PrivateIdentifier(_)) { + if method.r#static { + ctx.alloc_reg(); + ctx.alloc_reg(); + ctx.projected_pc += 3; + } + continue; + } ctx.alloc_reg(); // key_reg ctx.projected_pc += 1; ctx.alloc_reg(); // method_reg @@ -479,6 +487,12 @@ impl Compiler { ctx.restore_reg_checkpoint(checkpoint.saturating_add(1)); } } + Expression::PrivateInExpression(pin) => { + self.count_expression(&pin.right, ctx); + ctx.alloc_reg(); + ctx.alloc_reg(); + ctx.projected_pc += 2; + } Expression::UnaryExpression(un) => { match un.operator { UnaryOperator::Delete => { @@ -516,6 +530,12 @@ impl Compiler { return; } match &call.callee { + Expression::PrivateFieldExpression(member) => { + self.count_expression(&member.object, ctx); + ctx.alloc_reg(); + ctx.alloc_reg(); + ctx.projected_pc += 2; + } Expression::StaticMemberExpression(member) => { if matches!(&member.object, Expression::Super(_)) { ctx.alloc_reg(); // this register @@ -566,6 +586,11 @@ impl Compiler { self.count_expression(&assign.right, ctx); ctx.alloc_reg(); ctx.projected_pc += 1; // SET_PROP_DYNAMIC + } else if let oxide_parser::AssignmentTarget::PrivateFieldExpression(member) = &assign.left { + self.count_expression(&member.object, ctx); + self.count_expression(&assign.right, ctx); + ctx.alloc_reg(); + ctx.projected_pc += 2; // LOAD_CONST private id + SET_PRIVATE } else if let oxide_parser::AssignmentTarget::ArrayAssignmentTarget(ap) = &assign.left { self.count_expression(&assign.right, ctx); self.count_array_assignment(ap, ctx); @@ -773,6 +798,12 @@ impl Compiler { ctx.alloc_reg(); ctx.projected_pc += 1; // GET_PROP_DYNAMIC } + Expression::PrivateFieldExpression(member) => { + self.count_expression(&member.object, ctx); + ctx.alloc_reg(); + ctx.alloc_reg(); + ctx.projected_pc += 2; + } Expression::ParenthesizedExpression(p) => { self.count_expression(&p.expression, ctx); } diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index 34c1af77..8556d3a7 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -13,6 +13,21 @@ use crate::compiler::{ use crate::module::Constant; impl Compiler { + fn private_name_id(&self, name: &str, ctx: &CompileCtx) -> Result { + ctx.private_name_map + .iter() + .find_map(|(n, id)| (n == name).then_some(*id)) + .ok_or_else(|| format!("private name #{name} is not defined")) + } + + fn emit_private_id_reg(&self, name: &str, ctx: &mut CompileCtx) -> Result { + let id = self.private_name_id(name, ctx)?; + let idx = ctx.add_constant(Constant::Int(id as i32)); + let reg = ctx.alloc_reg(); + ctx.emit_load_const(reg, idx); + Ok(reg) + } + fn emit_class_key_reg(&self, key: &PropertyKey, computed: bool, ctx: &mut CompileCtx) -> Result { if !computed { let name = self.class_property_name(key)?; @@ -54,6 +69,32 @@ impl Compiler { Ok(()) } + fn emit_private_field_init( + &self, target_reg: u8, name: &str, value: Option<&Expression>, ctx: &mut CompileCtx, + ) -> Result<(), String> { + let key_reg = self.emit_private_id_reg(name, ctx)?; + let value_reg = if let Some(expr) = value { + self.emit_expression(expr, ctx)? + } else { + self.emit_undefined(ctx) + }; + ctx.emit(opcode::encode(OpCode::INIT_PRIVATE, target_reg, value_reg, key_reg)); + Ok(()) + } + + fn emit_private_method_init( + &self, target_reg: u8, method: &oxide_parser::MethodDefinition, home_reg: u8, ctx: &mut CompileCtx, + ) -> Result<(), String> { + let PropertyKey::PrivateIdentifier(private) = &method.key else { + return Err("expected private method key".into()); + }; + let name = private.name.as_str(); + let key_reg = self.emit_private_id_reg(name, ctx)?; + let method_reg = self.emit_class_method_function(method, name, home_reg, ctx, &[])?; + ctx.emit(opcode::encode(OpCode::INIT_PRIVATE, target_reg, method_reg, key_reg)); + Ok(()) + } + fn count_public_field_init( &self, key: &PropertyKey, computed: bool, value: Option<&Expression>, ctx: &mut CompileCtx, ) { @@ -67,6 +108,18 @@ impl Compiler { ctx.projected_pc += 1; } + fn count_private_field_init(&self, value: Option<&Expression>, ctx: &mut CompileCtx) { + ctx.alloc_reg(); + ctx.projected_pc += 1; + if let Some(expr) = value { + self.count_expression(expr, ctx); + } else { + ctx.alloc_reg(); + ctx.projected_pc += 1; + } + ctx.projected_pc += 1; + } + fn static_property_name(&self, key: &PropertyKey) -> Result { match key { PropertyKey::StaticIdentifier(ident) => Ok(ident.name.as_str().to_string()), @@ -368,14 +421,21 @@ impl Compiler { fn emit_class(&self, class: &Class, ctx: &mut CompileCtx) -> Result { let mut constructor_method = None; let mut instance_fields = Vec::new(); + let mut private_names = Vec::<(String, u32)>::new(); let is_derived = class.super_class.is_some(); for element in &class.body.body { match element { ClassElement::MethodDefinition(method) => { let method = method.as_ref(); - if matches!(method.key, PropertyKey::PrivateIdentifier(_)) { - return Err("private class elements not yet supported".into()); + if let PropertyKey::PrivateIdentifier(private) = &method.key { + let name = private.name.as_str().to_string(); + if private_names.iter().any(|(existing, _)| existing == &name) { + return Err(format!("duplicate private name #{name}")); + } + let id = ctx.next_private_name_id; + ctx.next_private_name_id = ctx.next_private_name_id.saturating_add(1); + private_names.push((name, id)); } if method.kind == MethodDefinitionKind::Constructor { if constructor_method.is_some() { @@ -386,8 +446,14 @@ impl Compiler { } ClassElement::PropertyDefinition(prop) => { let prop = prop.as_ref(); - if matches!(prop.key, PropertyKey::PrivateIdentifier(_)) { - return Err("private class elements not yet supported".into()); + if let PropertyKey::PrivateIdentifier(private) = &prop.key { + let name = private.name.as_str().to_string(); + if private_names.iter().any(|(existing, _)| existing == &name) { + return Err(format!("duplicate private name #{name}")); + } + let id = ctx.next_private_name_id; + ctx.next_private_name_id = ctx.next_private_name_id.saturating_add(1); + private_names.push((name, id)); } if !prop.r#static { instance_fields.push(prop); @@ -409,7 +475,9 @@ impl Compiler { }; let saved_derived = ctx.in_derived_constructor; + let saved_private_names = ctx.private_name_map.clone(); ctx.in_derived_constructor = is_derived; + ctx.private_name_map = private_names.clone(); let mut ctor_module = if let Some(method) = constructor_method { let (param_names, body_stmts) = self.extract_function_parts(method.value.as_ref())?; @@ -422,18 +490,36 @@ impl Compiler { FunctionBodyContext::ClassElement, Some(|compiler: &Compiler, field_ctx: &mut CompileCtx| { for field in &instance_fields { - compiler.count_public_field_init(&field.key, field.computed, field.value.as_ref(), field_ctx); + if matches!(field.key, PropertyKey::PrivateIdentifier(_)) { + compiler.count_private_field_init(field.value.as_ref(), field_ctx); + } else { + compiler.count_public_field_init( + &field.key, + field.computed, + field.value.as_ref(), + field_ctx, + ); + } } }), Some(|compiler: &Compiler, field_ctx: &mut CompileCtx| -> Result<(), String> { for field in &instance_fields { - compiler.emit_public_field_init( - 254, - &field.key, - field.computed, - field.value.as_ref(), - field_ctx, - )?; + if let PropertyKey::PrivateIdentifier(private) = &field.key { + compiler.emit_private_field_init( + 254, + private.name.as_str(), + field.value.as_ref(), + field_ctx, + )?; + } else { + compiler.emit_public_field_init( + 254, + &field.key, + field.computed, + field.value.as_ref(), + field_ctx, + )?; + } } Ok(()) }), @@ -449,18 +535,36 @@ impl Compiler { FunctionBodyContext::ClassElement, Some(|compiler: &Compiler, field_ctx: &mut CompileCtx| { for field in &instance_fields { - compiler.count_public_field_init(&field.key, field.computed, field.value.as_ref(), field_ctx); + if matches!(field.key, PropertyKey::PrivateIdentifier(_)) { + compiler.count_private_field_init(field.value.as_ref(), field_ctx); + } else { + compiler.count_public_field_init( + &field.key, + field.computed, + field.value.as_ref(), + field_ctx, + ); + } } }), Some(|compiler: &Compiler, field_ctx: &mut CompileCtx| -> Result<(), String> { for field in &instance_fields { - compiler.emit_public_field_init( - 254, - &field.key, - field.computed, - field.value.as_ref(), - field_ctx, - )?; + if let PropertyKey::PrivateIdentifier(private) = &field.key { + compiler.emit_private_field_init( + 254, + private.name.as_str(), + field.value.as_ref(), + field_ctx, + )?; + } else { + compiler.emit_public_field_init( + 254, + &field.key, + field.computed, + field.value.as_ref(), + field_ctx, + )?; + } } Ok(()) }), @@ -473,8 +577,19 @@ impl Compiler { module.bytecode.push(opcode::encode(OpCode::SUPER_CALL, 0, 0, 0)); module.bytecode.push(0); let mut field_ctx = CompileCtx::new(); + field_ctx.private_name_map = private_names.clone(); for field in &instance_fields { - self.emit_public_field_init(254, &field.key, field.computed, field.value.as_ref(), &mut field_ctx)?; + if let PropertyKey::PrivateIdentifier(private) = &field.key { + self.emit_private_field_init(254, private.name.as_str(), field.value.as_ref(), &mut field_ctx)?; + } else { + self.emit_public_field_init( + 254, + &field.key, + field.computed, + field.value.as_ref(), + &mut field_ctx, + )?; + } } module.bytecode.extend(field_ctx.bytecode); module.constants = field_ctx.constants; @@ -524,6 +639,11 @@ impl Compiler { if method.kind == MethodDefinitionKind::Constructor { continue; } + if matches!(method.key, PropertyKey::PrivateIdentifier(_)) { + let home_reg = if method.r#static { ctor_reg } else { proto_reg }; + self.emit_private_method_init(home_reg, method, home_reg, ctx)?; + continue; + } let home_reg = if method.r#static { ctor_reg } else { proto_reg }; let key_reg = self.emit_class_key_reg(&method.key, method.computed, ctx)?; let method_name = if method.computed { @@ -564,7 +684,11 @@ impl Compiler { if prop.r#static { let saved_static_this = ctx.static_block_this_reg; ctx.static_block_this_reg = Some(ctor_reg); - self.emit_public_field_init(ctor_reg, &prop.key, prop.computed, prop.value.as_ref(), ctx)?; + if let PropertyKey::PrivateIdentifier(private) = &prop.key { + self.emit_private_field_init(ctor_reg, private.name.as_str(), prop.value.as_ref(), ctx)?; + } else { + self.emit_public_field_init(ctor_reg, &prop.key, prop.computed, prop.value.as_ref(), ctx)?; + } ctx.static_block_this_reg = saved_static_this; } } @@ -582,6 +706,7 @@ impl Compiler { } } + ctx.private_name_map = saved_private_names; Ok(ctor_reg) } @@ -1168,6 +1293,13 @@ impl Compiler { ctx.emit_load_const(r, idx); Ok(r) } + Expression::PrivateInExpression(pin) => { + let obj_reg = self.emit_expression(&pin.right, ctx)?; + let key_reg = self.emit_private_id_reg(pin.left.name.as_str(), ctx)?; + let result_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::PRIVATE_BRAND_IN, result_reg, obj_reg, key_reg)); + Ok(result_reg) + } Expression::BinaryExpression(bin) => { let checkpoint = ctx.reg_checkpoint(); let left = self.emit_expression(&bin.left, ctx)?; @@ -1362,6 +1494,13 @@ impl Compiler { ctx.emit(opcode::encode(OpCode::GET_PROP_DYNAMIC, obj_reg, key_reg, r)); Ok(r) } + Expression::PrivateFieldExpression(member) => { + let obj_reg = self.emit_expression(&member.object, ctx)?; + let key_reg = self.emit_private_id_reg(member.field.name.as_str(), ctx)?; + let r = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::GET_PRIVATE, r, obj_reg, key_reg)); + Ok(r) + } Expression::ObjectExpression(obj) => { let obj_reg = ctx.alloc_reg(); ctx.emit(opcode::encode(OpCode::NEW_OBJECT, obj_reg, 0, 0)); @@ -1464,6 +1603,15 @@ impl Compiler { let val_reg = self.emit_expression(&assign.right, ctx)?; ctx.emit(opcode::encode(OpCode::SET_PROP_DYNAMIC, obj_reg, key_reg, val_reg)); Ok(val_reg) + } else if let oxide_parser::AssignmentTarget::PrivateFieldExpression(member) = &assign.left { + if assign.operator != AssignmentOperator::Assign { + return Err("compound assignment to private fields not supported".into()); + } + let obj_reg = self.emit_expression(&member.object, ctx)?; + let val_reg = self.emit_expression(&assign.right, ctx)?; + let key_reg = self.emit_private_id_reg(member.field.name.as_str(), ctx)?; + ctx.emit(opcode::encode(OpCode::SET_PRIVATE, obj_reg, val_reg, key_reg)); + Ok(val_reg) } else if let oxide_parser::AssignmentTarget::AssignmentTargetIdentifier(id_ref) = &assign.left { if assign.operator != AssignmentOperator::Assign { if assign.operator == AssignmentOperator::Addition @@ -1820,6 +1968,13 @@ impl Compiler { return Ok(result_reg); } let (callee_reg, this_reg) = match &call.callee { + Expression::PrivateFieldExpression(member) => { + let obj_reg = self.emit_expression(&member.object, ctx)?; + let key_reg = self.emit_private_id_reg(member.field.name.as_str(), ctx)?; + let callee_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::GET_PRIVATE, callee_reg, obj_reg, key_reg)); + (callee_reg, obj_reg) + } Expression::StaticMemberExpression(member) => { let is_super_member = matches!(&member.object, Expression::Super(_)); let obj_reg = if is_super_member { diff --git a/crates/oxide_compiler/src/hash.rs b/crates/oxide_compiler/src/hash.rs index 037d02bc..56cda01a 100644 --- a/crates/oxide_compiler/src/hash.rs +++ b/crates/oxide_compiler/src/hash.rs @@ -250,6 +250,10 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { hash_expression(&cond.consequent, h); hash_expression(&cond.alternate, h); } + Expression::PrivateInExpression(pin) => { + pin.left.name.as_str().hash(h); + hash_expression(&pin.right, h); + } Expression::Identifier(_) => {} Expression::NumericLiteral(num) => { num.value.to_bits().hash(h); @@ -262,6 +266,9 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { } Expression::AssignmentExpression(assign) => { std::mem::discriminant(&assign.operator).hash(h); + if let Some(target) = assign.left.as_simple_assignment_target() { + hash_simple_assignment_target(target, h); + } hash_expression(&assign.right, h); } Expression::UpdateExpression(update) => { @@ -323,6 +330,10 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { } } } + Expression::PrivateFieldExpression(member) => { + hash_expression(&member.object, h); + member.field.name.as_str().hash(h); + } Expression::Super(_) => {} Expression::RegExpLiteral(lit) => { if let Some(raw) = &lit.raw { @@ -356,6 +367,10 @@ fn hash_simple_assignment_target(target: &SimpleAssignmentTarget, h: &mut rustc_ hash_expression(&member.object, h); hash_expression(&member.expression, h); } + SimpleAssignmentTarget::PrivateFieldExpression(member) => { + hash_expression(&member.object, h); + member.field.name.as_str().hash(h); + } _ => {} }); } diff --git a/crates/oxide_compiler/src/opcode.rs b/crates/oxide_compiler/src/opcode.rs index fcfcf9a7..f0fcd881 100644 --- a/crates/oxide_compiler/src/opcode.rs +++ b/crates/oxide_compiler/src/opcode.rs @@ -123,6 +123,10 @@ pub enum OpCode { // ── Parallel — placeholders (0x70-0x7F) ── FORK = 0x70, JOIN = 0x71, + GET_PRIVATE = 0x72, + SET_PRIVATE = 0x73, + INIT_PRIVATE = 0x74, + PRIVATE_BRAND_IN = 0x75, // -- Bitwise (0x80-0x8F) -- BIT_AND = 0x80, @@ -269,6 +273,10 @@ impl TryFrom for OpCode { 0x66 => Ok(OpCode::PROFILE_CALL), 0x70 => Ok(OpCode::FORK), 0x71 => Ok(OpCode::JOIN), + 0x72 => Ok(OpCode::GET_PRIVATE), + 0x73 => Ok(OpCode::SET_PRIVATE), + 0x74 => Ok(OpCode::INIT_PRIVATE), + 0x75 => Ok(OpCode::PRIVATE_BRAND_IN), 0x80 => Ok(OpCode::BIT_AND), 0x81 => Ok(OpCode::BIT_OR), 0x82 => Ok(OpCode::BIT_XOR), @@ -382,6 +390,10 @@ impl fmt::Display for OpCode { OpCode::PROFILE_CALL => "PROFILE_CALL", OpCode::FORK => "FORK", OpCode::JOIN => "JOIN", + OpCode::GET_PRIVATE => "GET_PRIVATE", + OpCode::SET_PRIVATE => "SET_PRIVATE", + OpCode::INIT_PRIVATE => "INIT_PRIVATE", + OpCode::PRIVATE_BRAND_IN => "PRIVATE_BRAND_IN", OpCode::BIT_AND => "BIT_AND", OpCode::BIT_OR => "BIT_OR", OpCode::BIT_XOR => "BIT_XOR", diff --git a/crates/oxide_kernel/src/shape_forge.rs b/crates/oxide_kernel/src/shape_forge.rs index 88568a0c..a55f8e7c 100644 --- a/crates/oxide_kernel/src/shape_forge.rs +++ b/crates/oxide_kernel/src/shape_forge.rs @@ -297,4 +297,14 @@ mod tests { assert!(forge.has_property(s2, 1_000_310)); assert!(!forge.has_property(s2, 99)); } + + #[test] + fn private_band_keys_are_shape_properties() { + let forge = ShapeForge::new(); + let private_key = oxide_types::private_key::make_private_name_id(12); + let shape = forge.make_shape(EMPTY_SHAPE_ID, private_key); + assert!(oxide_types::private_key::is_private_name_key(private_key)); + assert!(forge.has_property(shape, private_key)); + assert_eq!(forge.lookup_position(shape, private_key), Some(0)); + } } diff --git a/crates/oxide_test262/src/main.rs b/crates/oxide_test262/src/main.rs index 878fdd97..885bff15 100644 --- a/crates/oxide_test262/src/main.rs +++ b/crates/oxide_test262/src/main.rs @@ -367,13 +367,7 @@ fn is_skipped(meta: &TestMeta) -> Option { "well-formed-json-stringify", "optional-chaining", "symbols-as-weakmap-keys", - "class-fields-public", - "class-fields-private", - "class-static-block", - "class-methods-private", "class-accessors-private", - "class-fields-public-static", - "class-fields-private-static", ]; for feat in &meta.features { @@ -560,10 +554,7 @@ fn process_path( if path_str.contains("built-ins/Promise/") { return TestResult::skip(path.to_path_buf(), "Promise tests excluded".into()); } - if path_str.contains("/private-") - || path_str.contains("/class-fields/") - || path_str.contains("/class-static-block/") - || path_str.contains("/dstr/") + if path_str.contains("/dstr/") || path_str.contains("/eval/") || path_str.contains("/function-ctor/") || path_str.contains("/realm/") diff --git a/crates/oxide_types/src/lib.rs b/crates/oxide_types/src/lib.rs index 38f3613d..d8361224 100644 --- a/crates/oxide_types/src/lib.rs +++ b/crates/oxide_types/src/lib.rs @@ -3,6 +3,7 @@ pub mod error; pub mod mem; pub mod object; +pub mod private_key; pub mod shape; pub mod value; diff --git a/crates/oxide_types/src/private_key.rs b/crates/oxide_types/src/private_key.rs new file mode 100644 index 00000000..7103630f --- /dev/null +++ b/crates/oxide_types/src/private_key.rs @@ -0,0 +1,24 @@ +pub const PRIVATE_NAME_BASE: u32 = 0x8000_0000; + +#[inline] +pub const fn is_private_name_key(key: u32) -> bool { + key >= PRIVATE_NAME_BASE +} + +#[inline] +pub const fn make_private_name_id(local_id: u32) -> u32 { + PRIVATE_NAME_BASE | (local_id & !PRIVATE_NAME_BASE) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn private_name_ids_use_high_band() { + let id = make_private_name_id(7); + assert!(is_private_name_key(id)); + assert_ne!(id, u32::MAX); + assert!(!is_private_name_key(7)); + } +} diff --git a/crates/oxide_vm/src/builtins/object.rs b/crates/oxide_vm/src/builtins/object.rs index e0084670..0c03555d 100644 --- a/crates/oxide_vm/src/builtins/object.rs +++ b/crates/oxide_vm/src/builtins/object.rs @@ -1,6 +1,7 @@ use oxide_kernel::shape_forge::{ShapeForge, EMPTY_SHAPE_ID}; use oxide_kernel::string_forge::StringForge; use oxide_types::object::{JsObject, PropAttributes}; +use oxide_types::private_key::is_private_name_key; use oxide_types::value::JsValue; use crate::coercion; @@ -19,7 +20,7 @@ pub(crate) fn walk_own_keys(vm: &Vm, obj: &JsObject) -> Vec<(u32, u32)> { } if let Some(shape) = vm.kernel_core().shape_forge().get_shape(id) { cursor = shape.parent; - if shape.property_name != u32::MAX { + if shape.property_name != u32::MAX && !is_private_name_key(shape.property_name) { shape_ids.push(id); } } else { @@ -28,7 +29,7 @@ pub(crate) fn walk_own_keys(vm: &Vm, obj: &JsObject) -> Vec<(u32, u32)> { } for id in shape_ids.iter().rev() { if let Some(shape) = vm.kernel_core().shape_forge().get_shape(*id) { - if shape.property_name != 0 { + if shape.property_name != 0 && !is_private_name_key(shape.property_name) { keys.push((shape.property_name, pos)); } } diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index 7316240c..b46deb7a 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -2,6 +2,7 @@ use oxide_compiler::opcode::OpCode; use oxide_kernel::prop_forge::PropTemplate; use oxide_kernel::{ic_debug, ic_trace}; use oxide_types::object::JsObject; +use oxide_types::private_key::make_private_name_id; use oxide_types::value::JsValue; use crate::vm::Vm; @@ -38,10 +39,101 @@ impl Vm { OpCode::GET_PROP_DYNAMIC => self.dispatch_get_prop_dynamic(rd, a, b), OpCode::SET_PROP_DYNAMIC => self.dispatch_set_prop_dynamic(rd, a, b), OpCode::SET_ELEM => self.dispatch_set_elem(rd, a, b), + OpCode::GET_PRIVATE => self.dispatch_get_private(rd, a, b), + OpCode::SET_PRIVATE => self.dispatch_set_private(rd, a, b), + OpCode::INIT_PRIVATE => self.dispatch_init_private(rd, a, b), + OpCode::PRIVATE_BRAND_IN => self.dispatch_private_brand_in(rd, a, b), _ => unreachable!("non-property opcode passed to dispatch_property_op"), } } + fn private_key_from_reg(&self, reg: usize) -> u32 { + let value = self.regs[reg]; + let local_id = if value.is_int() { + value.as_int().max(0) as u32 + } else if value.is_double() { + value.as_double().max(0.0) as u32 + } else { + 0 + }; + make_private_name_id(local_id) + } + + fn private_slot(&self, obj: &JsObject, private_key: u32) -> Option { + self.kernel_core.shape_forge().lookup_position(obj.shape_id(), private_key) + } + + fn resolve_private_value(&self, obj: &JsObject, private_key: u32) -> Option { + if let Some(pos) = self.private_slot(obj, private_key) { + return Some(obj.get_prop_at(pos)); + } + let mut proto = obj.proto(); + while proto.is_object() { + let proto_obj = unsafe { &*proto.as_js_object_ptr() }; + if let Some(pos) = self.private_slot(proto_obj, private_key) { + return Some(proto_obj.get_prop_at(pos)); + } + proto = proto_obj.proto(); + } + None + } + + fn dispatch_get_private(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + let Some(obj_ptr) = self.checked_object_ptr(self.regs[a], "private field access on non-object")? else { + return Ok(()); + }; + let obj = unsafe { &*obj_ptr }; + let private_key = self.private_key_from_reg(b); + let Some(value) = self.resolve_private_value(obj, private_key) else { + return self.raise_type_error("private field brand check failed"); + }; + self.regs[rd] = value; + Ok(()) + } + + fn dispatch_set_private(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "private field assignment on non-object")? else { + return Ok(()); + }; + let private_key = self.private_key_from_reg(b); + let obj = unsafe { &mut *obj_ptr }; + let Some(pos) = self.private_slot(obj, private_key) else { + return self.raise_type_error("private field brand check failed"); + }; + let value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[a]); + let obj = unsafe { &mut *obj_ptr }; + obj.set_prop_at(pos, value); + Ok(()) + } + + fn dispatch_init_private(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "private field initialization on non-object")? + else { + return Ok(()); + }; + let private_key = self.private_key_from_reg(b); + let value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[a]); + let obj = unsafe { &mut *obj_ptr }; + if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), private_key) { + obj.set_prop_at(pos, value); + } else { + self.set_or_create_prop_value(obj, private_key, value); + } + Ok(()) + } + + fn dispatch_private_brand_in(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + let obj_val = self.regs[a]; + if !obj_val.is_object() { + self.regs[rd] = JsValue::bool(false); + return Ok(()); + } + let obj = unsafe { &*obj_val.as_js_object_ptr() }; + let private_key = self.private_key_from_reg(b); + self.regs[rd] = JsValue::bool(self.resolve_private_value(obj, private_key).is_some()); + Ok(()) + } + fn dispatch_ic_get_prop(&mut self, a: usize, b: usize) -> Result<(), String> { let val = self.regs[a]; let obj_ptr = val.as_object_ptr() as *mut JsObject; diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 4a759024..12346b3c 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -913,7 +913,11 @@ impl Vm { | OpCode::SET_PROP | OpCode::GET_PROP_DYNAMIC | OpCode::SET_PROP_DYNAMIC - | OpCode::SET_ELEM => { + | OpCode::SET_ELEM + | OpCode::GET_PRIVATE + | OpCode::SET_PRIVATE + | OpCode::INIT_PRIVATE + | OpCode::PRIVATE_BRAND_IN => { self.dispatch_property_op(op, rd, a, b)?; } diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index a9f130e8..66865494 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -1,6 +1,7 @@ use crate::native::{NativeFn, NativeResult}; use crate::vm::{native_fn_ptr_to_fn, CallFrame, ForInIter, FrameContinuation, Vm}; use oxide_types::object::{JsObject, PropAttributes}; +use oxide_types::private_key::is_private_name_key; use oxide_types::value::JsValue; impl Vm { @@ -273,7 +274,10 @@ impl Vm { break; } if let Some(shape) = self.kernel_core.shape_forge().get_shape(id) { - if shape.property_name != u32::MAX && seen.insert(shape.property_name) { + if shape.property_name != u32::MAX + && !is_private_name_key(shape.property_name) + && seen.insert(shape.property_name) + { let enumerable = self .kernel_core .shape_forge() @@ -434,7 +438,7 @@ impl Vm { let Some(shape) = self.kernel_core.shape_forge().get_shape(shape_id) else { break; }; - if shape.property_name != u32::MAX { + if shape.property_name != u32::MAX && !is_private_name_key(shape.property_name) { if let Some(name) = self.kernel_core.string_forge().lookup(shape.property_name) { if !excluded.contains(name.as_str()) { if let Some(pos) = self diff --git a/crates/oxide_vm/tests/class_tests.rs b/crates/oxide_vm/tests/class_tests.rs index f796f7fd..6b19e817 100644 --- a/crates/oxide_vm/tests/class_tests.rs +++ b/crates/oxide_vm/tests/class_tests.rs @@ -243,3 +243,101 @@ fn computed_static_class_field_key_is_supported() { let result = eval(&mut vm, "var k = 'x'; class C { static [k] = 7; } C.x").unwrap(); assert_eq!(result.as_int(), 7); } + +#[test] +fn private_class_field_read_returns_value() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "class C { #x = 1; get(){ return this.#x; } } new C().get()").unwrap(); + assert_eq!(result.as_int(), 1); +} + +#[test] +fn private_class_field_write_round_trips() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "class C { #x; set(v){ this.#x = v; } get(){ return this.#x; } } var c = new C(); c.set(4); c.get()", + ) + .unwrap(); + assert_eq!(result.as_int(), 4); +} + +#[test] +fn private_class_method_call_uses_receiver() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "class C { #m(){ return 4; } get(){ return this.#m(); } } new C().get()").unwrap(); + assert_eq!(result.as_int(), 4); +} + +#[test] +fn private_class_brand_in_checks_presence() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "class C { #x = 1; has(o){ return #x in o; } } var c = new C(); c.has(c) && !c.has({})", + ) + .unwrap(); + assert!(result.is_bool() && result.as_bool()); +} + +#[test] +fn private_class_missing_brand_throws_type_error() { + let mut vm = Vm::new(); + let err = eval(&mut vm, "class C { #x = 1; get(o){ return o.#x; } } new C().get({})").unwrap_err(); + assert!(err.contains("TypeError"), "expected TypeError, got: {err}"); +} + +#[test] +fn private_static_field_and_method_work_on_constructor() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "class C { static #x = 2; static #m(){ return this.#x + 3; } static get(){ return this.#m(); } } C.get()", + ) + .unwrap(); + assert_num(result, 5.0); +} + +#[test] +fn derived_private_field_initializes_after_super() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "class B { constructor(){ this.b = 3; } } class D extends B { #x = this.b; get(){ return this.#x; } } new D().get()", + ) + .unwrap(); + assert_eq!(result.as_int(), 3); +} + +#[test] +fn private_class_fields_are_hidden_from_reflection_and_string_access() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "class C { #x = 1; y = 2; keys(){ return Object.keys(this).length + Object.getOwnPropertyNames(this).length + (this['#x'] === undefined ? 10 : 0); } } new C().keys()", + ) + .unwrap(); + assert_num(result, 12.0); +} + +#[test] +fn private_class_fields_are_hidden_from_for_in() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "class C { #x = 1; } var o = new C(); var n = 0; for (var k in o) { n = n + 1; } n", + ) + .unwrap(); + assert_num(result, 1.0); +} + +#[test] +fn private_class_same_name_different_classes_do_not_share_brand() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "class A { #x = 1; has(o){ return #x in o; } } class B { #x = 2; } var a = new A(); a.has(a) && !a.has(new B())", + ) + .unwrap(); + assert!(result.is_bool() && result.as_bool()); +}