From b18654fe5f824963580141b730571a102800dd88 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 27 Jun 2026 22:53:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Symbol.hasInstance=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20instanceof=20=E5=8F=8C=E8=B7=AF=E5=BE=84=E5=88=86=E6=B4=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_kernel/src/builtin.rs | 8 +++ crates/oxide_kernel/src/kernel.rs | 7 ++- crates/oxide_vm/src/bindings/bind_symbol.rs | 4 ++ crates/oxide_vm/src/vm_dispatch_misc.rs | 19 +++++++ .../oxide_vm/tests/builtins_symbol_tests.rs | 50 +++++++++++++++++++ 5 files changed, 86 insertions(+), 2 deletions(-) diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index a75761f5..d6a1d1fb 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -221,6 +221,7 @@ pub struct BuiltinWorld { pub sym_split: P, pub sym_iterator: P, pub sym_to_primitive: P, + pub sym_has_instance: P, pub stub_objects: Vec>, } @@ -544,6 +545,7 @@ impl BuiltinWorld { BuiltinId::SymSplit => &self.sym_split, BuiltinId::SymIterator => &self.sym_iterator, BuiltinId::SymToPrimitive => &self.sym_to_primitive, + BuiltinId::SymHasInstance => &self.sym_has_instance, } } @@ -580,6 +582,7 @@ impl BuiltinWorld { let sym_split = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let sym_iterator = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let sym_to_primitive = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); + let sym_has_instance = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let stub_objects = Vec::new(); let world = Self { @@ -648,6 +651,7 @@ impl BuiltinWorld { sym_split, sym_iterator, sym_to_primitive, + sym_has_instance, stub_objects, }; wire_builtin_world_links(&world); @@ -717,6 +721,7 @@ impl BuiltinWorld { sym_split, sym_iterator, sym_to_primitive, + sym_has_instance, ) = if dirty.symbol_family { let (symbol_proto, symbol_constructor) = make_named_pair(string_forge, shape_forge, labels, "Symbol"); ( @@ -728,6 +733,7 @@ impl BuiltinWorld { P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), + P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), ) } else { ( @@ -739,6 +745,7 @@ impl BuiltinWorld { current.sym_split.clone(), current.sym_iterator.clone(), current.sym_to_primitive.clone(), + current.sym_has_instance.clone(), ) }; @@ -879,6 +886,7 @@ impl BuiltinWorld { sym_split, sym_iterator, sym_to_primitive, + sym_has_instance, stub_objects, }; wire_builtin_world_links(&world); diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index 7a63dba7..207fdb21 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -201,7 +201,7 @@ pub struct KernelSession { /// Maintenance: every new `BuiltinWorld` object field must be added here and to /// `KernelSession::dirty_since_snapshot()` so selective reset can rebuild the /// correct builtin family. -pub const NUM_BUILTINS: usize = 65; +pub const NUM_BUILTINS: usize = 66; #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] @@ -271,6 +271,7 @@ pub enum BuiltinId { SymSplit = 62, SymIterator = 63, SymToPrimitive = 64, + SymHasInstance = 65, } impl BuiltinId { @@ -340,6 +341,7 @@ impl BuiltinId { BuiltinId::SymSplit, BuiltinId::SymIterator, BuiltinId::SymToPrimitive, + BuiltinId::SymHasInstance, ]; } @@ -509,7 +511,8 @@ impl KernelSession { || gen(BuiltinId::SymSearch) != snap(BuiltinId::SymSearch) || gen(BuiltinId::SymSplit) != snap(BuiltinId::SymSplit) || gen(BuiltinId::SymIterator) != snap(BuiltinId::SymIterator) - || gen(BuiltinId::SymToPrimitive) != snap(BuiltinId::SymToPrimitive), + || gen(BuiltinId::SymToPrimitive) != snap(BuiltinId::SymToPrimitive) + || gen(BuiltinId::SymHasInstance) != snap(BuiltinId::SymHasInstance), math: gen(BuiltinId::MathObject) != snap(BuiltinId::MathObject), json: gen(BuiltinId::JsonObject) != snap(BuiltinId::JsonObject), date: gen(BuiltinId::DateConstructor) != snap(BuiltinId::DateConstructor) diff --git a/crates/oxide_vm/src/bindings/bind_symbol.rs b/crates/oxide_vm/src/bindings/bind_symbol.rs index 5a9c6e1c..f56c837f 100644 --- a/crates/oxide_vm/src/bindings/bind_symbol.rs +++ b/crates/oxide_vm/src/bindings/bind_symbol.rs @@ -57,6 +57,10 @@ pub fn bind_symbol(core: &Arc, session: &KernelSession, global: &mut "toPrimitive", JsValue::from_js_object(session.builtin_world().sym_to_primitive.as_ptr() as *mut JsObject), ), + ( + "hasInstance", + JsValue::from_js_object(session.builtin_world().sym_has_instance.as_ptr() as *mut JsObject), + ), ] { bind_well_known_symbol(core, ctor, name, val); } diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index b7b7ddfd..dbaab0ad 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -208,6 +208,25 @@ impl Vm { if !rhs_val.is_object() { return self.raise_type_error("INSTANCEOF right-hand side is not callable"); } + + let has_instance_ptr = self.session.builtin_world().sym_has_instance.as_ptr() as *mut JsObject; + let has_instance_key = JsValue::from_js_object(has_instance_ptr); + let has_instance_si = self.property_key_si(has_instance_key); + + let ctor_obj = unsafe { &*rhs_val.as_js_object_ptr() }; + let has_instance_val = self.ordinary_get(ctor_obj, has_instance_si, rhs_val)?; + if !has_instance_val.is_undefined() && !has_instance_val.is_null() { + let fn_ptr = has_instance_val.as_js_object_ptr(); + if !fn_ptr.is_null() { + let fn_obj = unsafe { &*fn_ptr }; + if fn_obj.is_function() { + let result = self.call_function_sync(has_instance_val, rhs_val, &[lhs_val])?; + self.regs[rd] = JsValue::bool(to_boolean(result)); + return Ok(()); + } + } + } + if !lhs_val.is_object() { self.regs[rd] = JsValue::bool(false); return Ok(()); diff --git a/crates/oxide_vm/tests/builtins_symbol_tests.rs b/crates/oxide_vm/tests/builtins_symbol_tests.rs index 3f70562d..819ab586 100644 --- a/crates/oxide_vm/tests/builtins_symbol_tests.rs +++ b/crates/oxide_vm/tests/builtins_symbol_tests.rs @@ -155,3 +155,53 @@ fn symbol_key_for_non_symbol_throws_type_error() { let result = eval(&mut vm, "try { Symbol.keyFor(42) } catch (e) { e instanceof TypeError }").unwrap(); assert!(result.as_bool()); } + +// --- Symbol.hasInstance --- + +#[test] +fn symbol_has_instance_exists() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "typeof Symbol.hasInstance").unwrap(); + assert_eq!(to_str(&vm, result), "object"); +} + +#[test] +fn has_instance_overrides_instanceof() { + let mut vm = Vm::new(); + let source = r#" + class C { static [Symbol.hasInstance](v) { return typeof v === 'string'; } } + 'hello' instanceof C + "#; + let result = eval(&mut vm, source).unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn has_instance_non_callable_falls_through() { + let mut vm = Vm::new(); + let source = r#" + class D {} + D[Symbol.hasInstance] = 42; + ({} instanceof D) === false + "#; + let result = eval(&mut vm, source).unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn has_instance_coerces_return_to_boolean() { + let mut vm = Vm::new(); + let source = r#" + class C { static [Symbol.hasInstance](v) { return 'truthy'; } } + 'anything' instanceof C + "#; + let result = eval(&mut vm, source).unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn has_instance_preserves_ordinary_has_instance() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "[] instanceof Array").unwrap(); + assert!(result.as_bool()); +}