From 973d79ee2ea9a8801442637245417ecbf3c6db74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 12 Jun 2026 10:27:06 +0200 Subject: [PATCH] fix(hir): class static-field reflection remnant (test262 +6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three shared root causes behind the class/elements static-field cluster: 1. Object.keys(C) / for-in returned nothing for a class constructor. A class ref is INT32-tagged (not a heap pointer), so js_object_keys_value fell through to an empty array. Added a class-ref arm returning the enumerable static-field keys (CLASS_DYNAMIC_PROPS, ECMA order; #-private and the non-enumerable length/name/prototype + static methods excluded), via a new class_own_enumerable_field_names helper. 2. Object.prototype.propertyIsEnumerable.call(C, 'f') returned false for a static field: extract_obj_ptr nulls out on the INT32 ClassRef payload, so every key reported non-enumerable. Added a class-ref string-key arm: static fields enumerable, everything else non-enumerable. This was the third leg of verifyProperty's isEnumerable check (for-in + hasOwn + propertyIsEnumerable) — fixes #1/#2 together let the descriptor cluster pass. 3. Uninitialized non-computed static fields (static foo; static 0;) were registered by init_static_fields_late, which runs AFTER user statements — so Object.keys/getOwnPropertyDescriptor immediately after the class decl saw nothing. Their value is the compile-time constant undefined and a class name is in TDZ before its declaration, so registration moved to init_static_fields_early (before user code); the late else-branch was removed (it also clobbered any C.foo=... write made before module-init end). test262 language/{statements,expressions}/class: pass 5088 -> 5094 (+6), runtime-fail 196 -> 190, diff/compile-fail unchanged (zero regressions). native-region-proof gate unchanged vs origin/main (its sole failure, h1_buffer_alias_negative, reproduces identically on clean main — pre-existing). --- crates/perry-codegen/src/codegen/helpers.rs | 50 +++++++++++++++++-- .../src/object/class_registry.rs | 22 ++++++++ .../perry-runtime/src/object/descriptors.rs | 2 +- .../perry-runtime/src/object/field_get_set.rs | 18 +++++++ crates/perry-runtime/src/object/object_ops.rs | 18 +++++++ 5 files changed, 105 insertions(+), 5 deletions(-) diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs index c968929ae7..00618325de 100644 --- a/crates/perry-codegen/src/codegen/helpers.rs +++ b/crates/perry-codegen/src/codegen/helpers.rs @@ -584,6 +584,46 @@ pub(super) fn init_static_fields_early( &[(crate::types::I32, &cid_str), (I64, &func_ptr_i64)], ); } + // Uninitialized, non-computed static fields (`static foo;`, `static "g";`, + // `static 0;`) are own data properties of the constructor with value + // `undefined` per ClassDefinitionEvaluation. Their value is a compile-time + // constant (`undefined`) with no dependency on user lets, and a class name + // is in TDZ before its declaration, so registering them here — before user + // code — is observably identical to registering at the class-decl position + // and strictly earlier than the `init_static_fields_late` fallback that + // previously handled them (which ran AFTER user statements, so + // `Object.keys(C)` / `getOwnPropertyDescriptor(C, "foo")` immediately after + // the declaration saw nothing). test262 class/elements static-as-valid- + // static-field & friends. Initialized and computed-key fields are emitted + // inline at their source position elsewhere and are skipped here. + for c in &hir.classes { + let Some(&class_id) = ctx.class_ids.get(&c.name) else { + continue; + }; + if class_id == 0 { + continue; + } + for sf in &c.static_fields { + if sf.key_expr.is_some() || sf.init.is_some() || sf.name.starts_with('#') { + continue; + } + let idx = ctx.strings.intern(&sf.name); + let entry = ctx.strings.entry(idx); + let bytes_ref = format!("@{}", entry.bytes_global); + let len_str = entry.byte_len.to_string(); + let cid_str = class_id.to_string(); + let undef = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); + ctx.block().call_void( + "js_class_register_static_field", + &[ + (crate::types::I32, &cid_str), + (crate::types::PTR, &bytes_ref), + (crate::types::I64, &len_str), + (DOUBLE, &undef), + ], + ); + } + } Ok(()) } @@ -744,11 +784,13 @@ pub(super) fn init_static_fields_late( let g_ref = format!("@{}", global_name); crate::expr::emit_root_nanbox_store_on_block(ctx.block(), &v, &g_ref); emit_static_field_registration(ctx, &v); - } else { - let undef = - crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); - emit_static_field_registration(ctx, &undef); } + // Uninitialized non-computed static fields are now registered in + // `init_static_fields_early` (before user code) with value + // `undefined`. Re-registering here — after user statements — would + // clobber any `C.foo = …` the program performed between the class + // declaration and module-init end, so the no-init `else` branch was + // intentionally removed. } } // Static blocks — emitted as synthetic static methods with the diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 4e1e34f856..ce331c9a3c 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -92,6 +92,28 @@ pub(crate) fn class_own_static_field_value(class_id: u32, name: &str) -> Option< }) } +/// Enumerable own string keys of a class constructor: the static fields (and +/// runtime `C.x = …` assignments) recorded in CLASS_DYNAMIC_PROPS. The built-in +/// `length`/`name`/`prototype` slots and static *methods*/*accessors* are +/// non-enumerable, so they are intentionally excluded — this is exactly the set +/// `Object.keys(C)` / `for (k in C)` must yield. Private (`#`) keys are filtered +/// here too (never reflectable). Returned unsorted; the caller applies ECMA +/// ordering. (test262 class/elements static-field-declaration & friends.) +pub(crate) fn class_own_enumerable_field_names(class_id: u32) -> Vec { + CLASS_DYNAMIC_PROPS.with(|m| { + m.borrow() + .get(&class_id) + .map(|props| { + props + .keys() + .filter(|k| !k.starts_with('#')) + .cloned() + .collect() + }) + .unwrap_or_default() + }) +} + pub(crate) fn class_delete_own_dynamic_prop(class_id: u32, name: &str) { CLASS_DYNAMIC_PROPS.with(|m| { if let Some(props) = m.borrow_mut().get_mut(&class_id) { diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index e3be7b9a64..63e9f46c46 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -17,7 +17,7 @@ fn property_name_array_index(name: &str) -> Option { Some(value) } -fn sort_property_names_ecma(names: &mut Vec) { +pub(crate) fn sort_property_names_ecma(names: &mut Vec) { let mut indexed = Vec::new(); let mut rest = Vec::new(); for name in names.drain(..) { diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index 19016f4337..437b9c1bfb 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -1197,6 +1197,24 @@ pub extern "C" fn js_object_keys_value(value: f64) -> *mut ArrayHeader { ) }; } + // A class constructor ref `C` is an INT32-tagged value (not a pointer), so it + // would otherwise fall through to the empty-array tail below. Its enumerable + // own keys are the static fields registered in CLASS_DYNAMIC_PROPS — built-in + // `length`/`name`/`prototype` and static methods are non-enumerable. Backs + // `Object.keys(C)` / `for (k in C)` (test262 class/elements static-field-*). + if let Some(class_id) = super::class_ref_id(value) { + if super::class_prototype_ref_id(value).is_none() { + let mut names = super::class_registry::class_own_enumerable_field_names(class_id); + super::descriptors::sort_property_names_ecma(&mut names); + let arr = crate::array::js_array_alloc(names.len().max(1) as u32); + let mut out = arr; + for name in names { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + out = crate::array::js_array_push(out, JSValue::string_ptr(key)); + } + return out; + } + } if jv.is_pointer() { let ptr = jv.as_pointer::() as usize; if crate::value::addr_class::is_small_handle(ptr) { diff --git a/crates/perry-runtime/src/object/object_ops.rs b/crates/perry-runtime/src/object/object_ops.rs index f3c6d9cbc0..468578bc77 100644 --- a/crates/perry-runtime/src/object/object_ops.rs +++ b/crates/perry-runtime/src/object/object_ops.rs @@ -994,6 +994,24 @@ pub extern "C" fn js_object_property_is_enumerable(obj_value: f64, key_value: f6 return f64::from_bits(TAG_FALSE); } + // ClassRef receiver (INT32-tagged constructor, not a heap object): the + // only enumerable own string keys are the static FIELDS recorded in + // CLASS_DYNAMIC_PROPS — `length`/`name`/`prototype` and static + // methods/accessors are non-enumerable. `extract_obj_ptr` below would + // null out on the INT32 payload and report every key non-enumerable, so + // `verifyProperty(C, "f", …)`'s isEnumerable check failed (test262 + // class/elements static-field-declaration & friends). + if let Some(class_id) = super::class_ref_id(obj_value) { + if super::class_prototype_ref_id(obj_value).is_none() { + if let Some(key_name) = super::has_own_helpers::str_from_string_header(key_str) { + let is_static_field = !key_name.starts_with('#') + && super::class_registry::class_own_static_field_value(class_id, key_name) + .is_some(); + return f64::from_bits(if is_static_field { TAG_TRUE } else { TAG_FALSE }); + } + } + } + // String primitives: index keys in range are enumerable own props; // "length" is a non-enumerable own prop; everything else absent. if obj_jv.is_any_string() {