Repro
const K = "dyn" + "Key"; // a computed, non-well-known key
class E {
[K]() { return 1; }
static probe() { return typeof this.prototype; }
}
console.log(E.probe(), typeof E.prototype);
node : object object
perry: undefined object
Inside the static method, this === E is true, typeof this is "function" and this.name is "E" — the receiver is right by every other measure. Only this.prototype answers undefined, and reading the very same property through the class's own name (E.prototype) in the very same function answers object.
Remove the [K]() member and this.prototype is an object again. A declaration-position control shows it is not about ordering — a static method before the computed member fails identically to one after it. Instance methods are unaffected.
Scope
Any class carrying a generic computed member loses this.prototype in every static method of that class.
class A {
static before() { return typeof this.prototype; } // undefined
[K]() { return 1; }
static after() { return typeof this.prototype; } // undefined
method() { return typeof this; } // object — fine
}
Pre-existing: reproduces identically on 83754818e (#9242) and on current main.
Why it is worth fixing independently of #9341
This is the hole #9341 fell into. #9315 narrowed generic_computed_member_key so that well-known-symbol computed members ([Symbol.iterator] generator and non-generator, [Symbol.asyncIterator], [Symbol.toPrimitive], [util.inspect.custom]) stopped getting special lowering and became generic computed members — i.e. it routed the common cases onto the path this issue describes. axios's AxiosHeaders has a non-generator [Symbol.iterator]() and a static accessor() whose first act is let z = this.prototype, so cc --help started throwing Object.defineProperty called on non-object.
So reverting #9315 restores cc --help but leaves this in place, one plain computed key away from the next occurrence — and costs #9226's own-keys fix. Fixing this closes both.
Where it goes wrong
The class-ref .prototype read in crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs:
if name == "prototype" && class_id != 0 && is_class_id_registered(class_id) && !is_prototype_ref {
let value = class_registry::class_decl_prototype_value(class_id);
if value.to_bits() == TAG_UNDEFINED { return class_prototype_ref_value(class_id); }
return value;
}
Reached with a recognised class ref this cannot return undefined — there is a synthetic fallback. So this in these static methods is not being recognised as a class ref on this path, the branch is skipped, and the read falls through to a lookup that finds nothing. The named-binding read still takes the class-ref path, which is exactly why the two disagree inside one function.
Found while bisecting #9341.
Repro
Inside the static method,
this === Eistrue,typeof thisis"function"andthis.nameis"E"— the receiver is right by every other measure. Onlythis.prototypeanswersundefined, and reading the very same property through the class's own name (E.prototype) in the very same function answersobject.Remove the
[K]()member andthis.prototypeis an object again. A declaration-position control shows it is not about ordering — a static method before the computed member fails identically to one after it. Instance methods are unaffected.Scope
Any class carrying a generic computed member loses
this.prototypein every static method of that class.Pre-existing: reproduces identically on
83754818e(#9242) and on current main.Why it is worth fixing independently of #9341
This is the hole #9341 fell into. #9315 narrowed
generic_computed_member_keyso that well-known-symbol computed members ([Symbol.iterator]generator and non-generator,[Symbol.asyncIterator],[Symbol.toPrimitive],[util.inspect.custom]) stopped getting special lowering and became generic computed members — i.e. it routed the common cases onto the path this issue describes. axios'sAxiosHeadershas a non-generator[Symbol.iterator]()and astatic accessor()whose first act islet z = this.prototype, socc --helpstarted throwingObject.defineProperty called on non-object.So reverting #9315 restores
cc --helpbut leaves this in place, one plain computed key away from the next occurrence — and costs #9226's own-keys fix. Fixing this closes both.Where it goes wrong
The class-ref
.prototyperead incrates/perry-runtime/src/object/field_get_set/get_field_by_name.rs:Reached with a recognised class ref this cannot return
undefined— there is a synthetic fallback. Sothisin these static methods is not being recognised as a class ref on this path, the branch is skipped, and the read falls through to a lookup that finds nothing. The named-binding read still takes the class-ref path, which is exactly why the two disagree inside one function.Found while bisecting #9341.