feat: 支持 class 私有字段与品牌检查

This commit is contained in:
2026-06-17 08:56:58 +08:00
parent e71c236dcd
commit a074636619
14 changed files with 481 additions and 37 deletions
+6
View File
@@ -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<u8>,
pub(crate) private_name_map: Vec<(String, u32)>,
pub(crate) next_private_name_id: u32,
pub(crate) after_super_insert: Option<Vec<opcode::Instr>>,
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;
+31
View File
@@ -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);
}
+177 -22
View File
@@ -13,6 +13,21 @@ use crate::compiler::{
use crate::module::Constant;
impl Compiler {
fn private_name_id(&self, name: &str, ctx: &CompileCtx) -> Result<u32, String> {
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<u8, String> {
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<u8, String> {
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<String, String> {
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<u8, String> {
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 {
+15
View File
@@ -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);
}
_ => {}
});
}
+12
View File
@@ -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<u8> 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",
+10
View File
@@ -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));
}
}
+1 -10
View File
@@ -367,13 +367,7 @@ fn is_skipped(meta: &TestMeta) -> Option<String> {
"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/")
+1
View File
@@ -3,6 +3,7 @@
pub mod error;
pub mod mem;
pub mod object;
pub mod private_key;
pub mod shape;
pub mod value;
+24
View File
@@ -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));
}
}
+3 -2
View File
@@ -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));
}
}
+92
View File
@@ -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<u32> {
self.kernel_core.shape_forge().lookup_position(obj.shape_id(), private_key)
}
fn resolve_private_value(&self, obj: &JsObject, private_key: u32) -> Option<JsValue> {
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;
+5 -1
View File
@@ -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)?;
}
+6 -2
View File
@@ -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
+98
View File
@@ -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());
}