fix(hir): drop static extends_name for lexically-shadowed class heritage - #6106
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughCodegen for ChangesLexically shadowed extends handling
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Lowering as HIR Lowering
participant ClassDecl as ClassDecl (extends)
participant Codegen as SuperCall Codegen
participant Runtime as Runtime Dispatch
Lowering->>ClassDecl: resolve extends parent identifier
alt parent is lexically shadowed local
ClassDecl->>ClassDecl: set extends_name = None, extends_expr = lowered expr
else parent is class declaration
ClassDecl->>ClassDecl: keep extends_name = Some(name)
end
ClassDecl->>Codegen: current_class.extends_name / extends_expr
alt extends_name present
Codegen->>Codegen: parent_name = extends_name
else extends_name absent, extends_expr present
Codegen->>Codegen: parent_name = "" (dynamic)
Codegen->>Runtime: dispatch super() via extends_expr
else both absent
Codegen->>Codegen: lower super_args, return undefined
end
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
`class A extends Base` where `Base` is an in-scope lexical local (a
let/const/param), not a class, is heritage-shadowed: the parent is a
runtime value resolved dynamically via `extends_expr`. Lowering already
captured `extends_expr` and set `extends = None`, but still left
`extends_name = Some("Base")`. The many STATIC parent-chain walks in
codegen (packed-keys field layout, `js_register_class_parent` edge,
inherited-method / vtable install, type-facts) re-resolve that bare name
through the module-wide name->class map, binding to an UNRELATED same-named
class elsewhere in the module -- e.g. a function-local `class Base` that
leaked into the global map.
In a large minified program this mis-bound
`let Y = _?.Parent ?? Object; class A extends Y {}` to a captured
function-local iterator class also named `Y` (declaring a private `#q`), so
`A` instances inherited that class's layout/methods and a `this.#q` access
threw "Cannot access private member from an object whose class did not
declare it" on a legal receiver.
Fix: for a lexically-shadowed heritage, leave both `extends` and
`extends_name` `None` (matching the fully-dynamic
`class X extends <runtimeValue>` shape). The parent edge is wired at runtime
via `js_register_class_parent_dynamic` and `super()` runs through
`extends_expr` + `heritage_lexically_shadowed`. The super-call codegen gate
is updated to proceed via `extends_expr` when `extends_name` is absent, so a
shadowed subclass's `super()` still runs its (dynamic) parent constructor.
Adds HIR lowering regression tests: a lexically-shadowed heritage lowers to
`extends_name = None` + `extends_expr = Some`, while a plain
class-to-class heritage keeps its static `extends_name`.
1e465bf to
2cc8a60
Compare
Problem
class A extends BasewhereBaseis an in-scope lexical local (alet/const/param), not a class, is heritage-shadowed: the parent is a runtime value that must be resolved dynamically viaextends_expr.The lowering already handled this partially — it captured
extends_exprand setextends = None— but it still leftextends_name = Some("Base"). That textual name is then re-resolved by the many STATIC parent-chain walks in codegen (packed-keys field layout, thejs_register_class_parentedge, inherited-method / vtable install, type-facts) through the module-wide name→class map, binding the subclass to an unrelated same-named class elsewhere in the module — e.g. a function-localclass Basethat leaked into the global map.How it surfaces
In a large minified program, a helper does:
Ywas mis-bound to an unrelated captured, function-local iterator class also namedY(which declares a private#q).Ainstances then inherited that class's field layout / methods, and athis.#qaccess threw:on a perfectly legal receiver (
A's real runtime parent isObject, so its class-id chain never reaches the iterator's).Fix
For a lexically-shadowed heritage, leave both
extendsandextends_nameNone— matching the well-supported fully-dynamicclass X extends <runtimeValue>shape. The parent edge is wired at runtime viajs_register_class_parent_dynamic, andsuper()runs throughextends_expr+heritage_lexically_shadowed.The super-call codegen gate (
this_super_call.rs) is updated to proceed viaextends_exprwhenextends_nameis absent, so a shadowed subclass'ssuper()still runs its (dynamic) parent constructor rather than silently no-op'ing.Both the class-declaration and class-expression arms in
class_decl.rsare fixed (they already documented this intent forextends, but the retainedextends_namedefeated it).Why this is safe
locally_shadowedrequires the parent name to be alet/const/var/param local; class declarations are not inctx.locals, so ordinary class-to-class inheritance is unaffected (verified by the second regression test and by the existing#5437suite, which extends same-named class declarations, not locals).Tests
test_lexically_shadowed_heritage_drops_static_extends_name: alet Base = …; class A extends Base {}lowers toextends_name == None,extends == None,extends_expr == Some,heritage_lexically_shadowed == true.test_plain_class_to_class_heritage_keeps_static_extends_name: a plainclass Sub extends Base {}(both class decls) keeps its staticextends_name.Also verified end-to-end: the private-member throw no longer occurs in the affected program, and a shadowed subclass calling
super()on a runtime-value parent still runs the parent constructor (instanceof+ inherited + own fields all correct).Summary by CodeRabbit
super()now works correctly with dynamically resolved parents instead of stopping early.