-
-
Notifications
You must be signed in to change notification settings - Fork 161
fix(codegen): a static body's this is the class, not an instance — unblocks cc --help (#9369, #9341)
#9386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(codegen): a static body's this is the class, not an instance — unblocks cc --help (#9369, #9341)
#9386
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| **A class with a computed-key member no longer loses `this` inside its static | ||
| methods** — `this.prototype`, `this.<staticField>` and static-`this` writes | ||
| answer what node answers, which is what brought `cc --help` back (#9369, | ||
| unblocking #9341). | ||
|
|
||
| The reduced case was two answers to one question inside one function: | ||
|
|
||
| ```js | ||
| const K = "dyn" + "Key"; | ||
| class E { | ||
| [K]() { return 1; } | ||
| static probe() { return typeof this.prototype; } // perry: "undefined" | ||
| } | ||
| typeof E.prototype // perry: "object" | ||
| ``` | ||
|
|
||
| `class_has_computed_runtime_members` is a statement about a class's | ||
| *instances*: their key set is not described by the packed shape, so an | ||
| instance read has to go by name. Codegen applied it to every receiver whose | ||
| proven class had computed members, and `receiver_class_name` answers with the | ||
| owning class for `Expr::This` in a static body exactly as it does in an | ||
| instance body — both read `class_stack`. But a static body's `this` is the | ||
| class CONSTRUCTOR, an INT32-tagged class ref, and the by-name helper strips | ||
| the receiver NaN-box to a raw `ObjectHeader*`. The class ref's tag was masked | ||
| away, so the runtime received the bare class id as a pointer — below the | ||
| handle band, therefore not an object, therefore `undefined`. `this.name` was | ||
| the one survivor, because `js_object_get_field_by_name_f64` already reads a | ||
| small-integer receiver back as a class id for that single key. | ||
|
|
||
| `FnCtx::in_static_member` now records the distinction the receiver-class | ||
| answer cannot carry, and the computed-member routes (read and store) ask | ||
| before treating a proven class name as a claim about the receiver's layout. | ||
| Static bodies fall through to the general dispatch tower, which classifies | ||
| the receiver tag and already has a class-ref arm — so a static method of a | ||
| computed-member class now lowers exactly like the same method on a class | ||
| without one. | ||
|
|
||
| #9315 is what made this reach a real workload: it stopped giving | ||
| `[Symbol.iterator]`, `[Symbol.asyncIterator]`, `[Symbol.toPrimitive]` and | ||
| `[util.inspect.custom]` special lowering, so the common well-known-symbol | ||
| members became generic computed members. axios's `AxiosHeaders` has a | ||
| non-generator `[Symbol.iterator]()` and a `static accessor()` whose first act | ||
| is `let z = this.prototype`, and `Object.defineProperty(undefined, …)` threw | ||
| on every `cc --help`. The gap fixture | ||
| `test_gap_9369_static_this_computed_member.ts` pins all five member kinds | ||
| that take the generic path, plus static-before/after-computed, an | ||
| instance-method control, and the named-binding read that used to disagree. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1075,6 +1075,12 @@ pub(super) fn compile_closure( | |
| this_stack, | ||
| new_target_stack, | ||
| class_stack, | ||
| // Closures are compiled from their own HIR node; the enclosing | ||
| // member's staticness is not carried on it, so an arrow inside a | ||
| // static body still lowers `this` as an instance receiver. Tracked | ||
| // separately from #9369, whose fixture family is the static body | ||
| // itself. | ||
| in_static_member: false, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Preserve static-member context for lexical closures. When an arrow captures 🤖 Prompt for AI Agents |
||
| super_called_stack: Vec::new(), | ||
| shared_super_scope_active: false, | ||
| lexical_this_uses_derived_binding: captures_this | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // #9369: a class carrying a generic computed member must not lose `this` in | ||
| // its STATIC bodies. `this` there is the class CONSTRUCTOR — an INT32 class | ||
| // ref — not an instance, so codegen may not route the read through the | ||
| // instance-shaped by-name helper that strips the NaN-box to an | ||
| // `ObjectHeader*`. Doing so handed the runtime the bare class id as a | ||
| // pointer, and every static-`this` read but `name` answered `undefined` | ||
| // while the same property read through the class's own binding answered an | ||
| // object — one function, two answers. | ||
| // | ||
| // The member kinds below are the ones that reach the generic computed path: | ||
| // `[Symbol.iterator]` (generator and non-generator), `[Symbol.asyncIterator]`, | ||
| // `[Symbol.toPrimitive]`, and a plain computed key. #9315 routed the | ||
| // well-known-symbol forms onto it, which is how axios's `AxiosHeaders` | ||
| // (`[Symbol.iterator]()` plus `static accessor(){ let z = this.prototype; … }`) | ||
| // took `cc --help` down with `Object.defineProperty called on non-object` | ||
| // (#9341). | ||
|
|
||
| function T(name, fn) { | ||
| try { const r = fn(); console.log(name + " => " + String(r)); } | ||
| catch (e) { console.log(name + " !! " + (e && e.message ? e.message : String(e))); } | ||
| } | ||
| class A { | ||
| static before() { return typeof this.prototype; } | ||
| toJSON() { return { a: 1 }; } | ||
| [Symbol.iterator]() { return Object.entries(this.toJSON())[Symbol.iterator](); } | ||
| static after() { return typeof this.prototype; } | ||
| method() { return typeof this; } | ||
| } | ||
| T("static-BEFORE-computed", () => A.before()); | ||
| T("static-AFTER-computed", () => A.after()); | ||
| T("instance-method-after", () => new A().method()); | ||
| T("named-binding", () => typeof A.prototype); | ||
|
|
||
| // which computed keys trigger it? | ||
| class B { toJSON(){return{a:1}} [Symbol.asyncIterator]() { return null; } static after() { return typeof this.prototype; } } | ||
| T("asyncIterator-trigger", () => B.after()); | ||
| class C { toJSON(){return{a:1}} *[Symbol.iterator]() { yield 1; } static after() { return typeof this.prototype; } } | ||
| T("generator-iterator-trigger", () => C.after()); | ||
| class D { toJSON(){return{a:1}} [Symbol.toPrimitive]() { return 1; } static after() { return typeof this.prototype; } } | ||
| T("toPrimitive-trigger", () => D.after()); | ||
| const K = "dyn" + "Key"; | ||
| class E { toJSON(){return{a:1}} [K]() { return 1; } static after() { return typeof this.prototype; } } | ||
| T("plain-computed-key-trigger", () => E.after()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the malformed Markdown heading.
Line 38 uses
#9315without the required space. UseIssue#9315`` as prose, or use# 9315if this must be a heading.🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 38-38: No space after hash on atx style heading
(MD018, no-missing-space-atx)
🤖 Prompt for AI Agents
Source: Linters/SAST tools