Skip to content

fix(hir): class static-field reflection remnant#5036

Merged
proggeramlug merged 2 commits into
mainfrom
class-remnant-v3-parity
Jun 12, 2026
Merged

fix(hir): class static-field reflection remnant#5036
proggeramlug merged 2 commits into
mainfrom
class-remnant-v3-parity

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Closes the static-field leaf of the test262 language/{statements,expressions}/class remnant by fixing three shared root causes behind the class/elements/static-* cluster. All three converge on a single observation: a class constructor is an INT32-tagged ClassRef, not a heap object, so the generic object reflection paths skipped it.

test262 language/statements/class + language/expressions/class:
pass 5088 → 5094 (+6), runtime-fail 196 → 190, diff/compile-fail unchanged → zero regressions.

Root causes & fixes

  1. Object.keys(C) / for (k in C) returned [].
    js_object_keys_value had no ClassRef arm; the INT32 value isn't a pointer, so it fell through to an empty array. Added a class-ref arm returning the enumerable static-field keys from CLASS_DYNAMIC_PROPS in ECMA order (#-private and the non-enumerable length/name/prototype + static methods excluded), via a new class_own_enumerable_field_names helper. Both Object.keys and the for-in desugar funnel through this function.

  2. Object.prototype.propertyIsEnumerable.call(C, 'f') returned false.
    extract_obj_ptr nulls out on the INT32 ClassRef payload, so every key was reported non-enumerable. Added a class-ref string-key arm: a registered static field → enumerable, everything else → non-enumerable. This is the third leg of verifyProperty's isEnumerable (for-in + hasOwnProperty + propertyIsEnumerable); only with Support custom menu bar items #1 and linux compilation and README #2 does the descriptor cluster pass.

  3. Uninitialized non-computed static fields (static foo;, static 0;) registered too late.
    They were handled by init_static_fields_late, which runs after user statements — so Object.keys(C) / getOwnPropertyDescriptor(C, "foo") immediately after the class declaration 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 = … the program performed before module-init end.

Out of scope (remaining static-field tail)

  • Computed-key static fields with a string key (static ['constructor'];) route through the symbol side-table rather than CLASS_DYNAMIC_PROPS (string-vs-symbol key routing).
  • Numeric-key static-field reads (static 0 = 'bar'; C[0]).
  • Computed-key double-evaluation (static [i++] = i++).

Validation

  • Class subset: see counts above (zero regressions — pass strictly up, no bucket grew).
  • native-region-proof gate: unchanged vs origin/main. Its sole failure (h1_buffer_alias_negative, an unrelated HTTP buffer-alias codegen proof) reproduces identically on a clean origin/main build — pre-existing, not introduced here.
  • cargo fmt --all clean; file-size gate green (all edited large files already allowlisted).

Files

  • crates/perry-runtime/src/object/field_get_set.rsjs_object_keys_value class-ref arm
  • crates/perry-runtime/src/object/object_ops.rspropertyIsEnumerable class-ref arm
  • crates/perry-runtime/src/object/class_registry.rsclass_own_enumerable_field_names
  • crates/perry-runtime/src/object/descriptors.rssort_property_names_ecma made pub(crate)
  • crates/perry-codegen/src/codegen/helpers.rs — early registration of no-init static fields

Ralph Küpper and others added 2 commits June 12, 2026 10:27
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).
@proggeramlug proggeramlug merged commit 09795dc into main Jun 12, 2026
13 checks passed
@proggeramlug proggeramlug deleted the class-remnant-v3-parity branch June 12, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant