Skip to content

fix(runtime): class-prototype own-keys enumeration — accessors listed, symbols real, spec order (36 diverging fixture lines → 0) - #9315

Merged
proggeramlug merged 3 commits into
PerryTS:mainfrom
proggeramlug:fix/9226-own-keys-enumeration
Aug 31, 2026
Merged

fix(runtime): class-prototype own-keys enumeration — accessors listed, symbols real, spec order (36 diverging fixture lines → 0)#9315
proggeramlug merged 3 commits into
PerryTS:mainfrom
proggeramlug:fix/9226-own-keys-enumeration

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes #9226. Own-keys enumeration on class prototypes was wrong three independent ways: accessors missing from getOwnPropertyNames, a symbol key leaking into the string list as the literal "@@iterator", and getOwnPropertySymbols returning nothing — while hasOwnProperty and getOwnPropertyDescriptor disagreed with all of it.

unmodified main this branch
gap fixture, 41 assertions 36 lines diverge 0
the 104-assertion #9214 class-prototype differential 11 diverge 0
symbol/order probe, 69 assertions 52 diff-lines 10 (both residuals pre-existing)

Every number was produced twice — once on 6642990812, then re-run from scratch after rebasing past #9291 (which landed a Bloom filter over exactly these registries). Both rounds agree exactly.

Three causes, all in the enumeration/lowering, none in the data

  • Accessors were missing because the names builder read only vtable.methods.
  • The "@@iterator" string was a lowering artifact: generic_computed_member_key diverted every well-known-symbol class member off the computed-key path, so the member registered under a synthetic string name and no real Symbol key was ever installed where getOwnPropertySymbols could see it. The exclusion is narrowed to the three forms that genuinely need special lowering; the synthetic alias stays in the vtable for fast dispatch but is filtered from enumeration. This fixes the enumeration, not the spelling — checked explicitly: a source method literally named "@@iterator" still lists (it carries a definition-order record; an alias does not).
  • Order was whatever the dispatch hash maps yielded. Now reconstructed from the member's HIR id — allocated while walking the ClassBody, so it orders across the separate method/getter/setter/Symbol registries — and sort_property_names_ecma partitions rather than sorts, preserving it. Verified byte-identical to node on the discriminating case: integer-like ascending, then strings interleaving methods with accessors in source order, then symbols in insertion order; statics list length|name|prototype first, as node does.

A fourth defect the issue didn't name, also fixed: hasOwnProperty(C.prototype, someSymbol) returned false while getOwnPropertyDescriptor returned a descriptor for the same key.

The consistency triple now holds everywhere probed: every reported key answers true to both hasOwnProperty and getOwnPropertyDescriptor; inherited members are correctly not own; delete-then-recreate moves a key to the end; expandos append. Stable-hash and FFI arity updates are included so the module cache cannot serve stale objects.

Tests

  • cargo test -p perry-runtime --lib -- --test-threads=1: 2,873 passed, 0 failed, 4 ignored.
  • Gap suite: EXIT=0, green — 602/607, 0 crashes, 0 compile failures, snapshot OK, the new fixture passing inside the gate.
  • The fixture was demonstrated failing on a compiler built from unmodified main (36/41 lines) before the fix was applied.
  • cargo fmt clean on all 27 touched files.

Residual, pre-existing, byte-identical on base and this branch

static [Symbol.hasInstance] and get [Symbol.toStringTag] are lifted to top-level functions during lowering and never become class members at all, so they are absent from own keys (node lists them in getOwnPropertySymbols). A different and milder defect than the "@@" leak; not regressed here, and untracked as an issue.

A finding for #9273 discovered during verification

Five of the gap suite's "false regressions" — backoff_options, cron_cronjob, dayjs_factory_arg, moment_methods, ratelimiter_memory — have a concrete cause: missing npm dependencies, not a stale snapshot. They compare perry's native package emulation against the real package under node; the packages are in package.json but run_gap_tests.sh never provisions node_modules, so the node oracle emitted ERR_MODULE_NOT_FOUND and the mismatch was scored against the branch. After npm ci, all five pass inside the gate with the same binaries. The gate should fail loudly on a missing oracle dependency rather than scoring it.

Base

Deliberately left on d20fb4fd25 — the base actually verified — rather than speculatively rebased onto a base that wasn't. A range-diff probe shows the rebase onto current main is clean, all three patches carrying identically; the single overlapping file is a declaration-arity table with no interaction with the enumeration logic.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed class prototype key reflection so Object.getOwnPropertyNames, Object.getOwnPropertySymbols, and Reflect.ownKeys return consistent, spec-compliant results.
    • Preserved class member definition order across methods, accessors, fields, and symbol-keyed members.
    • Corrected handling of computed symbol properties, including Symbol.iterator.
    • Ensured deleted and re-added properties follow expected enumeration order.
    • Excluded internal synthetic aliases from reflected property lists.

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f58a5735-1fe4-4831-9732-c57748489f93

📥 Commits

Reviewing files that changed from the base of the PR and between 9c8cdfc and 7c6740c.

📒 Files selected for processing (27)
  • changelog.d/9226-class-prototype-own-keys.md
  • crates/perry-codegen/src/codegen/string_pool.rs
  • crates/perry-codegen/src/expr/static_field_meta.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs
  • crates/perry-codegen/src/runtime_decls/strings_part2.rs
  • crates/perry-hir/src/analysis/value_types_tests.rs
  • crates/perry-hir/src/ir/expr.rs
  • crates/perry-hir/src/lower_decl/class_computed.rs
  • crates/perry-hir/src/lower_decl/class_decl.rs
  • crates/perry-hir/src/stable_hash/expr.rs
  • crates/perry-runtime/src/object/class_image.rs
  • crates/perry-runtime/src/object/class_registry.rs
  • crates/perry-runtime/src/object/class_registry/gc_roots.rs
  • crates/perry-runtime/src/object/class_registry/parent_static.rs
  • crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs
  • crates/perry-runtime/src/object/class_registry/registration.rs
  • crates/perry-runtime/src/object/class_registry/state.rs
  • crates/perry-runtime/src/object/delete_rest.rs
  • crates/perry-runtime/src/object/descriptors.rs
  • crates/perry-runtime/src/object/mod.rs
  • crates/perry-runtime/src/object/object_ops/has_own.rs
  • crates/perry-runtime/src/object/property_key.rs
  • crates/perry-runtime/src/symbol.rs
  • crates/perry-runtime/src/symbol/gc_roots.rs
  • crates/perry-runtime/src/symbol/iterator.rs
  • crates/perry-runtime/src/symbol/properties.rs
  • test-files/test_gap_9226_class_prototype_own_keys.ts
💤 Files with no reviewable changes (1)
  • crates/perry-runtime/src/symbol/iterator.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

This change updates class lowering, code generation, and runtime reflection. Class methods and accessors retain source order across registries. Computed symbols use symbol keys. Own-key and descriptor checks now include accessors, symbols, dynamic properties, and ordered prototype members.

Changes

Class prototype own-key ordering

Layer / File(s) Summary
HIR member-order propagation
crates/perry-hir/src/ir/expr.rs, crates/perry-hir/src/lower_decl/*, crates/perry-hir/src/stable_hash/expr.rs, crates/perry-hir/src/analysis/value_types_tests.rs
Computed class method and accessor expressions now carry definition order. Iterator wrappers use computed-member lowering instead of synthetic string-method entries.
Code generation order registration
crates/perry-codegen/src/codegen/string_pool.rs, crates/perry-codegen/src/expr/static_field_meta.rs, crates/perry-codegen/src/runtime_decls/*, crates/perry-runtime/src/object/property_key.rs
Code generation passes member HIR ids to string-member and computed-member registration calls. Runtime declarations and registration tests use the expanded signatures.
Runtime order state and mutation tracking
crates/perry-runtime/src/object/class_image.rs, crates/perry-runtime/src/object/class_registry/*, crates/perry-runtime/src/object/mod.rs, crates/perry-runtime/src/object/delete_rest.rs, crates/perry-runtime/src/symbol*
Runtime tables track string-member, symbol-member, static-symbol, and dynamic-property order. Registration, deletion, invalidation, symbol lookup, and test cleanup maintain these tables.
Reflection and regression coverage
crates/perry-runtime/src/object/descriptors.rs, crates/perry-runtime/src/object/object_ops/has_own.rs, crates/perry-runtime/src/symbol/*, test-files/test_gap_9226_class_prototype_own_keys.ts, changelog.d/9226-class-prototype-own-keys.md
Own-key and descriptor paths include class accessors and symbols, filter synthetic aliases, and merge keys in ECMAScript order. The regression test covers base and derived prototypes, static members, accessors, integer keys, symbols, and inheritance.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🔵 Low · up to 7c674

The PR corrects class reflection keys, descriptors, ownership checks, and ordering. It is mergeable with owner awareness that an exceptional registration failure could preserve the member while losing its source-order metadata, causing incorrect reflection ordering.

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.22% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 45 functions across 25 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the runtime fix: class-prototype own-key enumeration now includes accessors, handles symbols correctly, and preserves specification order.
Description check ✅ Passed The description provides a clear summary, detailed changes, related issue reference, test results, before-and-after output, residual limitations, and validation context. It does not reproduce every te…
Linked Issues check ✅ Passed The implementation satisfies the primary requirements in #9226 [#9226]: accessor names are enumerated, synthetic "@@iterator" aliases are excluded from string names, symbol keys are returned separatel…
Out of Scope Changes check ✅ Passed The changes remain related to class-prototype own-key behavior. Ordering tables, deletion invalidation, symbol handling, stable-hash updates, FFI arity changes, regression tests, and changelog documen…
Full details: Description check

Explanation

The description provides a clear summary, detailed changes, related issue reference, test results, before-and-after output, residual limitations, and validation context. It does not reproduce every template heading or checklist item, but the required information is mostly present.

Full details: Linked Issues check

Explanation

The implementation satisfies the primary requirements in #9226 [#9226]: accessor names are enumerated, synthetic "@@iterator" aliases are excluded from string names, symbol keys are returned separately, ECMAScript ordering is reconstructed, and enumeration is aligned with hasOwnProperty and getOwnPropertyDescriptor. The documented Symbol.hasInstance and Symbol.toStringTag lowering gaps are distinct pre-existing defects outside the reported reproduction.

Full details: Out of Scope Changes check

Explanation

The changes remain related to class-prototype own-key behavior. Ordering tables, deletion invalidation, symbol handling, stable-hash updates, FFI arity changes, regression tests, and changelog documentation support the stated objectives and do not introduce unrelated code.

Full details: Docstring Coverage

Explanation

Docstring coverage is 42.22% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 45 functions across 25 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug merged commit 8b2cfe6 into PerryTS:main Aug 31, 2026
20 checks passed
proggeramlug added a commit that referenced this pull request Aug 31, 2026
* chore(gc): classify the two order side-tables in the root-holder inventory

CLASS_SYMBOL_MEMBER_ORDERS is keyed by SymbolHeader::id (stable, not an
address) with a u32 order value; CLASS_DYNAMIC_PROP_ORDER holds owned Rust
strings. Neither stores a JSValue, so neither is a GC root.

* refactor(hir): split class member-registration helpers out of class_decl.rs

class_decl.rs was at 1989 lines; #9315's additions take it over the 2000-line
gate. The non-computed member registration and Symbol.iterator wrapper helpers
move to a sibling module unchanged.

* fix(runtime): drop unnecessary parens in the numeric-range header read

`-D warnings` in CI treats unused_parens as an error.

* chore: changelog fragment for the train16 follow-up

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
proggeramlug pushed a commit to proggeramlug/perry that referenced this pull request Sep 1, 2026
… an instance

A class carrying a generic computed member lost `this.prototype` (and every
other static-`this` read, and static-`this` writes) inside all of its static
methods, while the same property read through the class's own binding
answered correctly — one function, two answers.

`class_has_computed_runtime_members` says the class's INSTANCES have keys the
packed shape does not describe, so an instance access must go by name. The
by-name helper strips the receiver NaN-box to a raw `ObjectHeader*`. But
`receiver_class_name` answers with the owning class for `Expr::This` in a
static body just as in an instance body (both read `class_stack`), and a
static body's `this` is the class CONSTRUCTOR — an INT32 class ref. Masking
it handed the runtime the bare class id as a pointer, below the handle band,
so every such read answered `undefined`; `this.name` survived only because
`js_object_get_field_by_name_f64` already reads a small-integer receiver back
as a class id for that one key.

`FnCtx::in_static_member` records what the receiver-class answer cannot, and
the computed-member read/store routes consult it before treating a proven
class name as a claim about the receiver's layout. Static bodies fall through
to the general dispatch tower, which classifies the receiver tag and has a
class-ref arm — the same lowering the identical static method already got
when its class carried no computed member.

This is what took `cc --help` down (PerryTS#9341): PerryTS#9315 routed well-known-symbol
computed members onto the generic path, and axios's `AxiosHeaders` pairs
`[Symbol.iterator]()` with `static accessor(){ let z = this.prototype; … }`.

Refs PerryTS#9369, PerryTS#9341.

Claude-Session: https://claude.ai/code/session_014knX724SYDogwzsXybCGxp
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.

getOwnPropertyNames(C.prototype) omits accessors and leaks a symbol key as the literal string "@@iterator"

1 participant