Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 55 additions & 22 deletions crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1033,33 +1033,66 @@ pub extern "C" fn js_object_get_field_by_name(
// chain walk so the #2059 own-name synthesis below answers
// with THIS class's registered name instead of an ancestor's.
if name != "name" {
let mut cid = class_id;
// Walk the class-object proto chain for an inherited static
// DATA field. At EACH level the class's pinned
// per-evaluation parent OBJECT is consulted BEFORE the
// parent's registry props (`CLASS_DYNAMIC_PROPS`).
//
// #6552: a subclass of a class-EXPRESSION value evaluated
// more than once (`function make(a){return class{static
// ast=a}}`, then `class Number$ extends make(x) {}` /
// `class Widget$ extends make(y) {}`) records THIS
// evaluation's parent object as its static prototype
// (`class_prototype_object`, #1788), but the parent's
// `CLASS_DYNAMIC_PROPS` are keyed by the class-expression
// TEMPLATE id — shared, last-wins across every evaluation.
// Reading the registry entry for such a parent collapses
// sibling subclasses to the LAST `make(...)` (effect Schema:
// `Number$.ast`/`Widget$.ast` both read the last parent's
// `ast`). The pinned object carries this evaluation's own
// edge, so it is authoritative; the registry read remains
// the fallback for a plain declaration parent (#6443:
// Auth.js `SignInError.kind`), which has no pinned object.
let mut child = class_id;
let mut depth = 0usize;
while depth < 32 {
match get_parent_class_id(cid) {
Some(p) if p != 0 && p != cid => {
cid = p;
depth += 1;
let proto =
super::super::class_registry::class_prototype_object(child);
if !proto.is_null() {
let v = js_object_get_field_by_name(proto as *const _, key);
// Return a value present on the pinned object even
// when it is `null` — a static explicitly set to
// `null` on THIS evaluation is authoritative and
// must not fall through to the last-wins registry
// entry (a sibling evaluation's value). Only
// `undefined` means "absent here", which continues
// the walk to the parent's registry props / a higher
// ancestor.
if !v.is_undefined() {
return v;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
_ => break,
}
if super::super::class_registry::class_is_key_deleted(cid, name) {
// A key deleted on THIS ancestor is not provided by
// it, but a higher ancestor may still define it —
// `delete Mid.foo` must let `Sub.foo` inherit
// `Base.foo`, not resolve to undefined. Skip this
// level and keep walking up (safe: `cid`/`depth`
// advance at the top of every iteration).
continue;
}
let inherited = CLASS_DYNAMIC_PROPS.with(|m| {
m.borrow()
.get(&cid)
.and_then(|props| props.get(name).copied())
});
if let Some(v) = inherited {
return JSValue::from_bits(v.to_bits());
let p = match get_parent_class_id(child) {
Some(p) if p != 0 && p != child => p,
_ => break,
};
// A key deleted on THIS ancestor is not provided by it,
// but a higher ancestor may still define it — `delete
// Mid.foo` must let `Sub.foo` inherit `Base.foo`, not
// resolve to undefined. Skip the registry read for the
// deleted level and keep walking up.
if !super::super::class_registry::class_is_key_deleted(p, name) {
let inherited = CLASS_DYNAMIC_PROPS.with(|m| {
m.borrow()
.get(&p)
.and_then(|props| props.get(name).copied())
});
if let Some(v) = inherited {
return JSValue::from_bits(v.to_bits());
}
}
child = p;
depth += 1;
}
}
if super::super::class_registry::lookup_static_method_in_chain(class_id, name)
Expand Down
7 changes: 6 additions & 1 deletion test-files/_helpers/renamed_class_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ export function make(a: any) {

class Number$ extends make({ _tag: "NumberKeyword" }) {}
class Widget$ extends make({ _tag: "WidgetKeyword" }) {}
// A per-evaluation static explicitly set to `null` — its own value must win
// over any sibling's (last-wins) registry `ast`. Declared before `DirectCls`
// so the last `make(...)` to register is a NON-null value, making `null` a real
// discriminator (regression guard for the #6552 fix's proto-vs-registry order).
class NullAst$ extends make(null) {}
export class DirectCls extends make({ _tag: "DirectKeyword" }) {}

// Renamed exports. `Number` deliberately collides with the JS global
// `Number` — pre-fix, `M.Number` resolved to the global constructor.
export { Number$ as Number, Widget$ as Widget };
export { Number$ as Number, Widget$ as Widget, NullAst$ as NullAst };
5 changes: 5 additions & 0 deletions test-files/test_gap_renamed_class_export_namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ console.log("(3) M.DirectCls.ast._tag:", (M as any).DirectCls.ast?._tag);

// (4) the global `Number` is still itself (not shadowed by the import).
console.log("(4) global Number(42):", Number("42"));

// (5) a renamed export whose per-evaluation static is explicitly `null` reads
// back `null` — it must not fall through to a sibling evaluation's (last-wins)
// `ast`. Guards the proto-object-before-registry ordering of the #6552 fix.
console.log("(5) M.NullAst.ast:", (M as any).NullAst.ast);
Loading