You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A class with a computed-key member loses this.prototype inside every static method (named-binding read still works) #9369
constK="dyn"+"Key";// a computed, non-well-known keyclassE{[K](){return1;}staticprobe(){returntypeofthis.prototype;}}console.log(E.probe(),typeofE.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.
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{returnclass_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.
Measured findings, and two fixes that failed — recording so the next attempt starts here
The receiver is a CLOSURE, not a class ref. Instrumenting the prototype read in js_object_get_field_by_name:
read
NaN-box bits
tag
E.prototype (named binding)
0x7ffe000000000001
0x7FFE — INT32 class ref, class_id 1 → resolves
this.prototype (static method)
0x0000051ae5e38d78
0x0000 — a raw heap pointer
control class, no computed member
0x7ffe000000000002
0x7FFE → resolves
GC header of that pointer: (obj_type=4, gc_flags=2) — GC_TYPE_CLOSURE. The .prototype resolution is gated on (bits >> 48) == 0x7FFE, so it never fires for this, and the read falls through to a lookup that finds nothing. That is the whole "one function, two answers" symptom.
Two failed fixes (do not repeat)
An arm keyed on is_class_object_ptr — never fires. That predicate requires GC_TYPE_OBJECT plus shape kind Class; the receiver is a closure. js_object_get_class_id on it returns 0.
Recovering class_id by matching ClosureHeader.func_ptr against CLASS_CONSTRUCTORS (class_id → func_ptr, registered by js_register_class_constructor) — the arm is reached, but the lookup misses: hdr=Some((4,2)) func_ptr=0x105186f84 cid=None. Adding an explicit constructor(){} does not help; both forms still fail.
An unresolved observation worth chasing first
With a two-class fixture, no diagnostic printed at all — that prototype read did not reach js_object_get_field_by_name. So the read path may vary with lowering. Locate where the read actually resolves before patching, e.g. PERRY_LLVM_KEEP_IR=1 and read the static method's emitted IR.
The likely root, and why it may be bigger than .prototype
crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs (~95–130) deliberately passes the INT32 class ref as this for Expr::ClassRef(_) — "this === ClassRef". A class with a computed member evidently stops lowering its binding as ClassRef and becomes a materialized closure, so this is a closure and every class-ref-keyed path changes behaviour, not only .prototype.
If that holds, the durable fix is in lowering/dispatch rather than a runtime patch — and it plausibly also closes #9362, #9364, #9365 and #9366, all filed from the same investigation.
Useful hooks for whoever takes it: js_register_class_computed_method(class_id, key, func_ptr, …)does carry the class_id, so it is a natural structural place to record a closure↔class link; class_decl_prototype_value(class_id) already returns the correct prototype (that is why the named-binding read works).
Activity
proggeramlug commentedon Sep 1, 2026
Measured findings, and two fixes that failed — recording so the next attempt starts here
The receiver is a CLOSURE, not a class ref. Instrumenting the
prototyperead injs_object_get_field_by_name:E.prototype(named binding)0x7ffe0000000000010x7FFE— INT32 class ref, class_id 1 → resolvesthis.prototype(static method)0x0000051ae5e38d780x0000— a raw heap pointer0x7ffe0000000000020x7FFE→ resolvesGC header of that pointer:
(obj_type=4, gc_flags=2)—GC_TYPE_CLOSURE. The.prototyperesolution is gated on(bits >> 48) == 0x7FFE, so it never fires forthis, and the read falls through to a lookup that finds nothing. That is the whole "one function, two answers" symptom.Two failed fixes (do not repeat)
is_class_object_ptr— never fires. That predicate requiresGC_TYPE_OBJECTplus shape kindClass; the receiver is a closure.js_object_get_class_idon it returns 0.class_idby matchingClosureHeader.func_ptragainstCLASS_CONSTRUCTORS(class_id → func_ptr, registered byjs_register_class_constructor) — the arm is reached, but the lookup misses:hdr=Some((4,2)) func_ptr=0x105186f84 cid=None. Adding an explicitconstructor(){}does not help; both forms still fail.An unresolved observation worth chasing first
With a two-class fixture, no diagnostic printed at all — that
prototyperead did not reachjs_object_get_field_by_name. So the read path may vary with lowering. Locate where the read actually resolves before patching, e.g.PERRY_LLVM_KEEP_IR=1and read the static method's emitted IR.The likely root, and why it may be bigger than
.prototypecrates/perry-codegen/src/lower_call/property_get/static_dispatch.rs(~95–130) deliberately passes the INT32 class ref asthisforExpr::ClassRef(_)— "this === ClassRef". A class with a computed member evidently stops lowering its binding asClassRefand becomes a materialized closure, sothisis a closure and every class-ref-keyed path changes behaviour, not only.prototype.If that holds, the durable fix is in lowering/dispatch rather than a runtime patch — and it plausibly also closes #9362, #9364, #9365 and #9366, all filed from the same investigation.
Useful hooks for whoever takes it:
js_register_class_computed_method(class_id, key, func_ptr, …)does carry the class_id, so it is a natural structural place to record a closure↔class link;class_decl_prototype_value(class_id)already returns the correct prototype (that is why the named-binding read works).fix(codegen): PerryTS#9369 — a static body's `this` is the class, not…
fix(codegen): #9369 — a static body's `this` is the class, not an ins…
fix: static this is the constructor; class name/toString/inspect repo…
proggeramlug commentedon Sep 2, 2026
Closing: fixed in #9386 (a static body's this is the class, not an instance).