From 2f7fe3cedf62db902148bbcb8e81c6ca3267fefa Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 5 Aug 2026 15:44:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=B8=8D=E7=AC=A6?= =?UTF-8?q?=E5=90=88=20JS=20=E8=AF=AD=E4=B9=89=E7=BA=A6=E6=9D=9F=E7=9A=84?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E8=A1=8C=E4=B8=BA=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_kernel/src/builtin.rs | 7 ++++++ crates/oxide_vm/src/vm_dispatch_misc.rs | 23 +++++++++++++++++++ .../oxide_vm/tests/builtins_object_tests.rs | 3 ++- crates/oxide_vm/tests/control_flow_tests.rs | 8 +++---- crates/oxide_vm/tests/regression_tests.rs | 4 ++-- 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 97bee636..7cd37366 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -257,6 +257,9 @@ fn make_pair( ctor.set_data_meta(0u32, oxide_types::object::PropAttributes::new(false, false, false)); // .name (ctor[1]) = writable:false, enumerable:false, configurable:true ctor.set_data_meta(1u32, oxide_types::object::PropAttributes::new(false, false, true)); + // .constructor (proto[0]) = writable:true, enumerable:false, configurable:true + // (JS spec: prototype.constructor is non-enumerable so it doesn't leak into for-in) + proto.set_data_meta(0u32, oxide_types::object::PropAttributes::new(true, false, true)); (P::new(proto), P::new(ctor)) } @@ -1175,6 +1178,10 @@ impl BuiltinWorld { let new_shape = shape_forge.make_shape(proto.shape_id(), si); proto.set_shape_id(new_shape); proto.ensure_hash_props().push(wrapper_val); + // Built-in prototype methods are non-enumerable per the ES spec; otherwise + // they leak into for-in enumeration (e.g. array push/pop showing up in `for k in []`). + let method_pos = proto.hash_props_vec().map_or(0, |v| v.len() as u32).saturating_sub(1); + proto.set_data_meta(method_pos, oxide_types::object::PropAttributes::new(true, false, true)); proto.bump_generation(); Ok(()) } diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index 64b084ca..2634225d 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -300,6 +300,29 @@ impl Vm { bumpalo::collections::Vec::new_in(self.epoch.bump()); let mut seen = std::collections::HashSet::new(); let mut current = obj_val; + + // Arrays store integer-indexed elements in prop_vec, which is not part of + // the shape chain. Enumerate them (ES: array indices are enumerable string + // keys, ordered ascending before other own keys). + if current.is_object() { + let arr = unsafe { &*current.as_js_object_ptr() }; + if arr.is_array() { + for i in 0..arr.prop_vec_len() { + let is_enum = arr + .prop_meta_at(i) + .map(|m| m.attributes.enumerable()) + .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()); + if is_enum { + let idx = self.kernel_core.perm_interner().intern(&i.to_string()).0; + keys_vec.push(( + JsValue::perm_string(self.kernel_core.perm_interner().string_ptr(idx)), + idx, + )); + } + } + } + } + let mut depth = 0usize; loop { diff --git a/crates/oxide_vm/tests/builtins_object_tests.rs b/crates/oxide_vm/tests/builtins_object_tests.rs index 4d39a7cd..437c64a8 100644 --- a/crates/oxide_vm/tests/builtins_object_tests.rs +++ b/crates/oxide_vm/tests/builtins_object_tests.rs @@ -166,7 +166,8 @@ fn object_proto_has_own_property_call_works() { #[test] fn object_proto_property_is_enumerable_call_works() { let (_vm, result) = eval("Object.prototype.propertyIsEnumerable.call(Object, 'assign')").unwrap(); - assert_eq!(result, JsValue::bool(true)); + // Built-in methods are non-enumerable per the ES spec. + assert_eq!(result, JsValue::bool(false)); } #[test] diff --git a/crates/oxide_vm/tests/control_flow_tests.rs b/crates/oxide_vm/tests/control_flow_tests.rs index 672eb9ea..8848c329 100644 --- a/crates/oxide_vm/tests/control_flow_tests.rs +++ b/crates/oxide_vm/tests/control_flow_tests.rs @@ -159,12 +159,12 @@ fn eval_do_while_continue() { #[test] fn eval_for_in_enumerates_keys() { - assert_eq!(eval("var n=0; for (k in {a:1,b:2}) { n=n+1; } n"), "3"); + assert_eq!(eval("var n=0; for (k in {a:1,b:2}) { n=n+1; } n"), "2"); } #[test] fn eval_for_in_empty_object() { - assert_eq!(eval("var r=0; for (k in {}) { r=1; } r"), "1"); + assert_eq!(eval("var r=0; for (k in {}) { r=1; } r"), "0"); } #[test] @@ -175,7 +175,7 @@ fn eval_for_in_non_object_throws() { #[test] fn eval_for_in_nested() { - assert_eq!(eval("var r=0; for (k in {a:{x:1}}) { for (j in {y:2}) { r=r+1; } } r"), "2"); + assert_eq!(eval("var r=0; for (k in {a:{x:1}}) { for (j in {y:2}) { r=r+1; } } r"), "1"); } #[test] @@ -185,7 +185,7 @@ fn eval_for_in_break() { #[test] fn eval_for_in_continue() { - assert_eq!(eval("var r=0; for (k in {a:1,b:2}) { if (k=='a') continue; r=r+1; } r"), "2"); + assert_eq!(eval("var r=0; for (k in {a:1,b:2}) { if (k=='a') continue; r=r+1; } r"), "1"); } #[test] diff --git a/crates/oxide_vm/tests/regression_tests.rs b/crates/oxide_vm/tests/regression_tests.rs index dc5473ed..4afa4ab6 100644 --- a/crates/oxide_vm/tests/regression_tests.rs +++ b/crates/oxide_vm/tests/regression_tests.rs @@ -104,8 +104,8 @@ fn regression_throw_statement_preserves_syntax_error_kind() { fn regression_for_in_prototype_chain() { assert_eq!( eval("var c=0;for(var k in {a:1}){c=c+1;}c"), - "2", - "for-in should include inherited constructor from prototype" + "1", + "for-in must not include non-enumerable prototype properties like constructor" ); }