Skip to content

A class with a computed-key member loses this.prototype inside every static method (named-binding read still works) #9369

Description

@proggeramlug

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions