Skip to content

fix(runtime): Object.defineProperty on a class installs a static own property (#7190) - #7798

Merged
proggeramlug merged 1 commit into
mainfrom
fix/7190-class-static-define-property
Aug 11, 2026
Merged

fix(runtime): Object.defineProperty on a class installs a static own property (#7190)#7798
proggeramlug merged 1 commit into
mainfrom
fix/7190-class-static-define-property

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Object.defineProperty(SomeClass, key, descriptor) now installs a static own property (#7190). It was silently dropped — not misfiled, dropped: C.zzz came back undefined and so did new C().zzz, so the value went nowhere at all.

The cause is that C and C.prototype answer class_ref_id with the same class id — Perry maps a prototype ref back to its class — so the define path could not tell the two receivers apart and treated every one as a prototype install. That is correct for Object.defineProperty(C.prototype, …), the drizzle applyMixins case the arm was written for, and wrong for the class itself. class_prototype_ref_id is the discriminator, and descriptors.rs was already using it to tell the two apart when reporting descriptors; the define path now does the same and routes a bare class ref into CLASS_DYNAMIC_PROPS, the table static x = … already writes to, so the existing static read path finds it with no new lookup.

The user-visible form was zod: it renames constructors with Object.defineProperty(Cls, "name", { value }), and Perry kept resolving .name through the class registry, so class errors reported constructor.name === "Definition".

Two things that had to come with it, both found by the oracle rather than by reasoning:

  • Attributes. A declared static x = … is writable and enumerable (CreateDataPropertyOrThrow); a defineProperty data descriptor is neither. Both now live in one table, so the descriptor-installed ones carry their (writable, enumerable, configurable) bits and an absent entry keeps the previous (true, true, true) reporting for declared fields. Without this, Object.keys(C) gained a key Node does not report — the first cut of this fix did exactly that, leaking a non-enumerable hidden into both Object.keys and for…in.
  • configurable is retain-or-default, not default. ECMA-262 [[DefineOwnProperty]] defaults an omitted field to false on a NEW property but RETAINS it on an existing one. The built-in name/length slots are configurable: true, so redefining name without saying configurable must stay configurable while a brand-new key must not. Hardcoding either answer fails one of the two, and both appear in the same test.

getOwnPropertyDescriptor(C, "name") now agrees with C.name too — previously the value read reported the redefined string while the descriptor still reported the declared one, which is the state that makes a define look like it never happened.

Verified against Node v26.5.1: the new gap test test_gap_class_static_define_property_7190.ts passes byte-for-byte, covering all three receivers (function, class, class prototype), an arbitrary key as well as name, subclasses, class expressions, and the enumerability/descriptor bits. test_gap_class (25) and test_gap_static (3) stay green.

Two pre-existing failures were checked rather than assumed: test_gap_2159_defineproperty_class_prototype fails on clean main in the release sweep and its diff is an unsettled top-level await, not a descriptor; and the runtime lib suite's intermittent failure is #7365obj_dispatch_ic_tests::a_hit_requires_matching_name_bytes_not_a_matching_address fails 10 of 12 isolated runs on clean main against 7 of 12 with this change, so it is order-dependent flake and not fallout here.

Summary by CodeRabbit

  • New Features

    • Object.defineProperty now works correctly on class constructors and static properties.
    • Static property descriptors preserve writable, enumerable, and configurable settings.
    • Property reads, descriptors, and key enumeration now remain consistent, including for inherited classes and existing properties.
    • Constructor and prototype properties are handled distinctly.
  • Bug Fixes

    • Corrected default configurability behavior for newly defined and existing static properties.
  • Tests

    • Added regression coverage for classes, subclasses, class expressions, functions, static fields, and prototype properties.

@proggeramlug
proggeramlug force-pushed the fix/7190-class-static-define-property branch from 1d67ff0 to 350a1b9 Compare August 10, 2026 21:06
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Object.defineProperty now supports class-constructor static properties. The runtime stores descriptor attributes, separates static and prototype receivers, filters non-enumerable keys, and aligns property descriptors with reads. Regression tests cover constructors, subclasses, functions, class expressions, and prototypes.

Changes

Class static defineProperty behavior

Layer / File(s) Summary
Static attribute state
crates/perry-runtime/src/object/mod.rs, crates/perry-runtime/src/object/class_registry/...
The runtime records writable, enumerable, and configurable attributes for defined static properties. Non-enumerable static keys are excluded from enumeration.
defineProperty routing
crates/perry-runtime/src/object/object_ops/...
Class constructors store static properties in CLASS_DYNAMIC_PROPS and retain descriptor defaults for existing properties. Prototype receivers continue through the prototype path.
Descriptor lookup and regression validation
crates/perry-runtime/src/object/descriptors.rs, test-files/test_gap_class_static_define_property_7190.ts, changelog.d/7798-class-static-define-property.md
Descriptors use recorded attributes, dynamic name properties override synthesized class names, and tests validate static, prototype, enumeration, and descriptor behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ObjectDefineProperty
  participant DefinePropertyHandler
  participant ClassRegistry
  participant CLASS_DYNAMIC_PROPS
  Caller->>ObjectDefineProperty: defineProperty(class, key, descriptor)
  ObjectDefineProperty->>DefinePropertyHandler: process descriptor
  DefinePropertyHandler->>ClassRegistry: distinguish static from prototype receiver
  DefinePropertyHandler->>CLASS_DYNAMIC_PROPS: store static value
  DefinePropertyHandler->>ClassRegistry: record descriptor attributes
  Caller->>ClassRegistry: read keys or descriptor
  ClassRegistry-->>Caller: filtered keys and recorded attributes
Loading

Possibly related PRs

  • PerryTS/perry#6757: Extends Object.defineProperty and descriptor-helper behavior for class static properties.
  • PerryTS/perry#7134: Modifies class constructor and static-property lookup paths.

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the runtime fix for installing static own properties with Object.defineProperty on classes.
Description check ✅ Passed The description clearly covers the problem, implementation, issue reference, test coverage, and pre-existing failures, despite not using the template headings or checklist.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7190-class-static-define-property

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/perry-runtime/src/object/object_ops/define_property.rs (1)

513-577: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Implement complete static [[DefineOwnProperty]] behavior.

Use descriptor-field presence for value; { value: undefined } and generic descriptors such as { enumerable: true } must not be ignored.

Retain omitted writable and enumerable attributes for existing static properties. Validate non-configurable and non-writable invariants before class_dynamic_prop_root_store. Add regression cases for these descriptor and redefinition paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/object_ops/define_property.rs` around lines
513 - 577, Update the static-property branch identified by
class_prototype_ref_id in define_property to detect descriptor value-field
presence rather than treating an undefined value as absent, so generic
descriptors still update attributes and { value: undefined } stores correctly.
Preserve existing writable and enumerable attributes when those fields are
omitted, and validate non-configurable/non-writable redefinition invariants
before class_dynamic_prop_root_store. Add regression coverage for undefined
values, generic descriptors, omitted attributes, and invalid redefinitions.
🧹 Nitpick comments (1)
changelog.d/7798-class-static-define-property.md (1)

1-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce this fragment to one release-note entry.

Lines 3-16 include internal routing details, oracle investigation, test results, and unrelated flaky-test analysis. Keep the user-visible fix and descriptor behavior. Remove development narrative.

Proposed release-note text
- **`Object.defineProperty(SomeClass, key, descriptor)` now installs a static own property** (`#7190`). It was silently dropped — not misfiled, dropped: `C.zzz` came back `undefined` and so did `new C().zzz`, so the value went nowhere at all.
-
- The cause is that `C` and `C.prototype` answer `class_ref_id` with the **same class id** — Perry maps a prototype ref back to its class — so the define path could not tell the two receivers apart and treated every one as a prototype install. That is correct for `Object.defineProperty(C.prototype, …)`, the drizzle `applyMixins` case the arm was written for, and wrong for the class itself. `class_prototype_ref_id` is the discriminator, and `descriptors.rs` was already using it to tell the two apart when reporting descriptors; the define path now does the same and routes a bare class ref into `CLASS_DYNAMIC_PROPS`, the table `static x = …` already writes to, so the existing static read path finds it with no new lookup.
+ **Fix class static properties defined with `Object.defineProperty`.** The runtime now installs these properties on the class constructor and preserves their descriptor attributes.

Based on learnings: changelog fragments must describe final shipped behavior as one coherent release-note entry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@changelog.d/7798-class-static-define-property.md` around lines 1 - 16, Reduce
the changelog fragment to one concise release-note entry describing the
user-visible fix: Object.defineProperty on a class now correctly defines static
own properties, including constructor name updates, while prototype definitions
remain correct. Retain the descriptor-attribute behavior, including writable,
enumerable, and configurable defaults/retention, and remove internal
implementation details, investigation narrative, test results, and unrelated
failure analysis.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/object/class_registry/state.rs`:
- Around line 122-127: Update the static-property deletion path to remove the
deleted property’s entry from CLASS_STATIC_DEFINED_ATTRS alongside its value,
using the class identifier and property name. Ensure a later assignment
recreates metadata with the new property’s actual attributes so
Object.getOwnPropertyDescriptor and Object.keys reflect the recreated property.

---

Outside diff comments:
In `@crates/perry-runtime/src/object/object_ops/define_property.rs`:
- Around line 513-577: Update the static-property branch identified by
class_prototype_ref_id in define_property to detect descriptor value-field
presence rather than treating an undefined value as absent, so generic
descriptors still update attributes and { value: undefined } stores correctly.
Preserve existing writable and enumerable attributes when those fields are
omitted, and validate non-configurable/non-writable redefinition invariants
before class_dynamic_prop_root_store. Add regression coverage for undefined
values, generic descriptors, omitted attributes, and invalid redefinitions.

---

Nitpick comments:
In `@changelog.d/7798-class-static-define-property.md`:
- Around line 1-16: Reduce the changelog fragment to one concise release-note
entry describing the user-visible fix: Object.defineProperty on a class now
correctly defines static own properties, including constructor name updates,
while prototype definitions remain correct. Retain the descriptor-attribute
behavior, including writable, enumerable, and configurable defaults/retention,
and remove internal implementation details, investigation narrative, test
results, and unrelated failure analysis.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 691e9798-91ee-40d5-84fa-098eb9b89d92

📥 Commits

Reviewing files that changed from the base of the PR and between b9415d7 and 350a1b9.

📒 Files selected for processing (9)
  • changelog.d/7798-class-static-define-property.md
  • crates/perry-runtime/src/object/class_registry.rs
  • crates/perry-runtime/src/object/class_registry/state.rs
  • crates/perry-runtime/src/object/descriptors.rs
  • crates/perry-runtime/src/object/mod.rs
  • crates/perry-runtime/src/object/object_ops.rs
  • crates/perry-runtime/src/object/object_ops/define_property.rs
  • crates/perry-runtime/src/object/object_ops/descriptor_helpers.rs
  • test-files/test_gap_class_static_define_property_7190.ts

Comment on lines +122 to +127
crate::object::CLASS_STATIC_DEFINED_ATTRS.with(|m| {
m.borrow_mut()
.entry(class_id)
.or_default()
.insert(name.to_string(), (writable, enumerable, configurable));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 5 \
  'CLASS_STATIC_DEFINED_ATTRS|CLASS_DELETED_KEYS|class_dynamic_prop_root_store|class_dynamic_prop_root' \
  crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 31040


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- state.rs ---'
cat -n crates/perry-runtime/src/object/class_registry/state.rs | sed -n '30,150p'

echo '--- deletion and static assignment call sites ---'
rg -n -C 12 \
  'class_delete_own_dynamic_prop|class_mark_key_deleted|class_static_defined_attrs|class_static_key_is_non_enumerable|class_dynamic_prop_root_store' \
  crates/perry-runtime/src/object crates/perry-runtime/src/symbol/properties.rs

echo '--- descriptor handling ---'
cat -n crates/perry-runtime/src/object/object_ops/define_property.rs | sed -n '470,585p'

echo '--- tests mentioning static delete/re-add or descriptor attributes ---'
rg -n -C 8 \
  'delete.*static|static.*delete|defineProperty|non.?enumerable|CLASS_STATIC_DEFINED_ATTRS|class_static_defined_attrs' \
  crates/perry-runtime/src --glob '*test*' --glob '*.rs' | head -n 500

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- class delete path ---'
cat -n crates/perry-runtime/src/object/delete_rest.rs | sed -n '45,95p'

echo '--- class side-table deletion functions ---'
cat -n crates/perry-runtime/src/object/class_registry/state.rs | sed -n '141,165p'

echo '--- class assignment path ---'
cat -n crates/perry-runtime/src/object/field_set_by_name.rs | sed -n '330,435p'
cat -n crates/perry-runtime/src/object/field_set_by_name/write_helpers.rs | sed -n '1,90p'

echo '--- defineProperty static branch ---'
cat -n crates/perry-runtime/src/object/object_ops/define_property.rs | sed -n '505,590p'

echo '--- focused tests ---'
rg -n -C 5 \
  'class_dynamic_prop_root_store|class_delete_own_dynamic_prop|class_mark_key_deleted|class_static_set_defined_attrs|class_static_defined_attrs|Object\.keys|delete ' \
  crates/perry-runtime/src --glob '*.rs' | rg -i 'test|static|class|defined|delete|enumer' | head -n 250

echo '--- source invariant probe ---'
python3 - <<'PY'
from pathlib import Path

state = Path("crates/perry-runtime/src/object/class_registry/state.rs").read_text()
delete = Path("crates/perry-runtime/src/object/delete_rest.rs").read_text()
assignment = Path("crates/perry-runtime/src/object/field_set_by_name.rs").read_text()
helpers = Path("crates/perry-runtime/src/object/field_set_by_name/write_helpers.rs").read_text()

def body(text, signature, start=0):
    i = text.index(signature, start)
    brace = text.index("{", i)
    depth = 0
    for j in range(brace, len(text)):
        if text[j] == "{":
            depth += 1
        elif text[j] == "}":
            depth -= 1
            if depth == 0:
                return text[i:j+1]
    raise RuntimeError(signature)

store = body(state, "pub(crate) fn class_dynamic_prop_root_store")
delete_fn = body(state, "pub(crate) fn class_delete_own_dynamic_prop")
mark = body(state, "pub(crate) fn class_mark_key_deleted")
print("store clears CLASS_DELETED_KEYS:", "CLASS_DELETED_KEYS" in store)
print("store clears CLASS_STATIC_DEFINED_ATTRS:", "CLASS_STATIC_DEFINED_ATTRS" in store)
print("delete clears CLASS_DYNAMIC_PROPS:", "props.remove(name)" in delete_fn)
print("delete clears CLASS_STATIC_DEFINED_ATTRS:", "CLASS_STATIC_DEFINED_ATTRS" in delete_fn)
print("delete path calls dynamic delete then mark:",
      "class_delete_own_dynamic_prop(class_id, name)" in delete and
      "class_mark_key_deleted(class_id, name)" in delete)
print("assignment path calls store:", "class_dynamic_prop_root_store(class_id, name, value)" in assignment)
print("helper assignment path calls store:",
      "class_dynamic_prop_root_store((*obj).class_id, name.to_string(), value)" in helpers)
PY

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- class delete path ---'
cat -n crates/perry-runtime/src/object/delete_rest.rs | sed -n '45,95p'

echo '--- class side-table deletion functions ---'
cat -n crates/perry-runtime/src/object/class_registry/state.rs | sed -n '141,165p'

echo '--- class assignment path ---'
cat -n crates/perry-runtime/src/object/field_set_by_name.rs | sed -n '330,435p'
cat -n crates/perry-runtime/src/object/field_set_by_name/write_helpers.rs | sed -n '1,90p'

echo '--- defineProperty static branch ---'
cat -n crates/perry-runtime/src/object/object_ops/define_property.rs | sed -n '505,590p'

echo '--- focused tests ---'
rg -n -C 5 \
  'class_dynamic_prop_root_store|class_delete_own_dynamic_prop|class_mark_key_deleted|class_static_set_defined_attrs|class_static_defined_attrs|Object\.keys|delete ' \
  crates/perry-runtime/src --glob '*.rs' | rg -i 'test|static|class|defined|delete|enumer' | head -n 250

echo '--- source invariant probe ---'
python3 - <<'PY'
from pathlib import Path

state = Path("crates/perry-runtime/src/object/class_registry/state.rs").read_text()
delete = Path("crates/perry-runtime/src/object/delete_rest.rs").read_text()
assignment = Path("crates/perry-runtime/src/object/field_set_by_name.rs").read_text()
helpers = Path("crates/perry-runtime/src/object/field_set_by_name/write_helpers.rs").read_text()

def body(text, signature, start=0):
    i = text.index(signature, start)
    brace = text.index("{", i)
    depth = 0
    for j in range(brace, len(text)):
        if text[j] == "{":
            depth += 1
        elif text[j] == "}":
            depth -= 1
            if depth == 0:
                return text[i:j+1]
    raise RuntimeError(signature)

store = body(state, "pub(crate) fn class_dynamic_prop_root_store")
delete_fn = body(state, "pub(crate) fn class_delete_own_dynamic_prop")
print("store clears CLASS_DELETED_KEYS:", "CLASS_DELETED_KEYS" in store)
print("store clears CLASS_STATIC_DEFINED_ATTRS:", "CLASS_STATIC_DEFINED_ATTRS" in store)
print("delete clears CLASS_DYNAMIC_PROPS:", "props.remove(name)" in delete_fn)
print("delete clears CLASS_STATIC_DEFINED_ATTRS:", "CLASS_STATIC_DEFINED_ATTRS" in delete_fn)
print("delete path calls dynamic delete then mark:",
      "class_delete_own_dynamic_prop(class_id, name)" in delete and
      "class_mark_key_deleted(class_id, name)" in delete)
print("assignment path calls store:", "class_dynamic_prop_root_store(class_id, name, value)" in assignment)
print("helper assignment path calls store:",
      "class_dynamic_prop_root_store((*obj).class_id, name.to_string(), value)" in helpers)
PY

Repository: PerryTS/perry

Length of output: 49865


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

state = Path("crates/perry-runtime/src/object/class_registry/state.rs").read_text()
delete = Path("crates/perry-runtime/src/object/delete_rest.rs").read_text()
assignment = Path("crates/perry-runtime/src/object/field_set_by_name.rs").read_text()
descriptors = Path("crates/perry-runtime/src/object/descriptors.rs").read_text()

def body(text, signature):
    i = text.index(signature)
    brace = text.index("{", i)
    depth = 0
    for j in range(brace, len(text)):
        if text[j] == "{":
            depth += 1
        elif text[j] == "}":
            depth -= 1
            if depth == 0:
                return text[i:j + 1]
    raise RuntimeError(signature)

store = body(state, "pub(crate) fn class_dynamic_prop_root_store")
delete_fn = body(state, "pub(crate) fn class_delete_own_dynamic_prop")
mark = body(state, "pub(crate) fn class_mark_key_deleted")

facts = {
    "store clears deleted marker": "CLASS_DELETED_KEYS" in store,
    "store clears descriptor attrs": "CLASS_STATIC_DEFINED_ATTRS" in store,
    "delete removes dynamic value": "props.remove(name)" in delete_fn,
    "delete clears descriptor attrs": "CLASS_STATIC_DEFINED_ATTRS" in delete_fn,
    "class delete calls value removal": "class_delete_own_dynamic_prop(class_id, name)" in delete,
    "class delete marks key deleted": "class_mark_key_deleted(class_id, name)" in delete,
    "class assignment stores value": "class_dynamic_prop_root_store(class_id, name, value)" in assignment,
    "descriptor read consults attrs": "class_static_defined_attrs(" in descriptors,
}
for name, value in facts.items():
    print(f"{name}: {value}")

assert facts["store clears deleted marker"]
assert not facts["store clears descriptor attrs"]
assert facts["delete removes dynamic value"]
assert not facts["delete clears descriptor attrs"]
assert facts["class delete calls value removal"]
assert facts["class delete marks key deleted"]
assert facts["class assignment stores value"]
assert facts["descriptor read consults attrs"]
print("invariant: stale descriptor metadata survives delete and re-add")
PY

Repository: PerryTS/perry

Length of output: 508


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

state = Path("crates/perry-runtime/src/object/class_registry/state.rs").read_text()
delete = Path("crates/perry-runtime/src/object/delete_rest.rs").read_text()
assignment = Path("crates/perry-runtime/src/object/field_set_by_name.rs").read_text()
descriptors = Path("crates/perry-runtime/src/object/descriptors.rs").read_text()

def body(text, signature):
    i = text.index(signature)
    brace = text.index("{", i)
    depth = 0
    for j in range(brace, len(text)):
        if text[j] == "{":
            depth += 1
        elif text[j] == "}":
            depth -= 1
            if depth == 0:
                return text[i:j + 1]
    raise RuntimeError(signature)

store = body(state, "pub(crate) fn class_dynamic_prop_root_store")
delete_fn = body(state, "pub(crate) fn class_delete_own_dynamic_prop")

facts = {
    "store clears deleted marker": "CLASS_DELETED_KEYS" in store,
    "store clears descriptor attrs": "CLASS_STATIC_DEFINED_ATTRS" in store,
    "delete removes dynamic value": "props.remove(name)" in delete_fn,
    "delete clears descriptor attrs": "CLASS_STATIC_DEFINED_ATTRS" in delete_fn,
    "class delete calls value removal": "class_delete_own_dynamic_prop(class_id, name)" in delete,
    "class delete marks key deleted": "class_mark_key_deleted(class_id, name)" in delete,
    "class assignment stores value": "class_dynamic_prop_root_store(class_id, name, value)" in assignment,
    "descriptor read consults attrs": "class_static_defined_attrs(" in descriptors,
}
for name, value in facts.items():
    print(f"{name}: {value}")

assert facts["store clears deleted marker"]
assert not facts["store clears descriptor attrs"]
assert facts["delete removes dynamic value"]
assert not facts["delete clears descriptor attrs"]
assert facts["class delete calls value removal"]
assert facts["class delete marks key deleted"]
assert facts["class assignment stores value"]
assert facts["descriptor read consults attrs"]
print("invariant: stale descriptor metadata survives delete and re-add")
PY

Repository: PerryTS/perry

Length of output: 508


Clear CLASS_STATIC_DEFINED_ATTRS when a static property is deleted.

delete C.x removes the value but leaves its descriptor metadata. After C.x = 2, Object.getOwnPropertyDescriptor(C, "x") can report stale attributes, and Object.keys(C) can omit x. Clear the metadata during deletion or when assignment recreates the property.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/class_registry/state.rs` around lines 122 -
127, Update the static-property deletion path to remove the deleted property’s
entry from CLASS_STATIC_DEFINED_ATTRS alongside its value, using the class
identifier and property name. Ensure a later assignment recreates metadata with
the new property’s actual attributes so Object.getOwnPropertyDescriptor and
Object.keys reflect the recreated property.

@proggeramlug
proggeramlug merged commit 28c6364 into main Aug 11, 2026
12 of 18 checks passed
@proggeramlug
proggeramlug deleted the fix/7190-class-static-define-property branch August 11, 2026 05:24
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