-
-
Notifications
You must be signed in to change notification settings - Fork 158
fix(runtime): resolve native-constructor superclass prototype in dynamic extends (#8760) #8763
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
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 @@ | ||
| Fix `class X extends Y` throwing `TypeError: Class extends value does not have valid prototype property` at module init when `Y` is a bound native-module constructor export imported directly (`import { EventEmitter as EE } from "events"; class X extends EE {}`, and the same shape for `Stream` and the `net`/`http` server classes). These exports are modeled as bound-method closures whose `.prototype` object — the one carrying the EventEmitter method surface — is materialized lazily, so the runtime's dynamic-parent registration read the raw `prototype` dynamic slot, still `undefined`, and rejected the superclass even though Node accepts it. The dynamic-parent path now resolves the superclass prototype through the same lazy materialization an ordinary `Y.prototype` read uses, so the `extends` edge links to the real prototype; a genuinely prototype-less parent (a bare `fn.bind(...)`) still throws, matching Node. This unblocked the natively compiled Claude Code cli.js 2.1.112 bundle, which extends the aliased `events.EventEmitter` export at module init and crashed on nearly every command. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -752,7 +752,28 @@ pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { | |
| if crate::closure::is_closure_ptr(parent_addr) { | ||
| let parent_proto = | ||
| crate::closure::closure_get_dynamic_prop(parent_addr, "prototype"); | ||
| if let Some(bits) = class_parent_prototype_bits(parent_proto) { | ||
| // A bound native-module constructor export imported directly | ||
| // (`import { EventEmitter } from "events"; class X extends | ||
| // EventEmitter {}`) carries `.prototype` only lazily: the raw | ||
| // dynamic-slot read above is still `undefined` because the | ||
| // synthetic prototype object (which carries the EventEmitter | ||
| // method surface) is materialized on demand, not at closure | ||
| // mint time. Resolve it exactly as an ordinary `Y.prototype` | ||
| // property read does, so the `extends` edge links to that real | ||
| // prototype instead of throwing. `Stream` and the net/http | ||
| // server classes share this shape. A non-constructor bound | ||
| // method still resolves to `None` here, so a genuinely | ||
| // prototype-less parent (e.g. a bare `fn.bind(...)`) still | ||
| // throws below, matching Node. | ||
| let resolved_proto = if class_parent_prototype_bits(parent_proto).is_some() { | ||
| parent_proto | ||
| } else { | ||
| super::function_prototype::ordinary_function_prototype_value_for_read( | ||
| dynamic_parent.get_nanbox_f64(), | ||
| ) | ||
| .unwrap_or(parent_proto) | ||
| }; | ||
|
Comment on lines
+768
to
+775
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 Do not resolve an explicit invalid Lines 768-775 call Distinguish an absent lazy native slot from an explicit own 🤖 Prompt for AI Agents |
||
| if let Some(bits) = class_parent_prototype_bits(resolved_proto) { | ||
| Some(bits) | ||
| } else { | ||
| super::super::object_ops::throw_object_type_error( | ||
|
|
@@ -950,4 +971,60 @@ mod class_parent_prototype_tests { | |
| ); | ||
| assert_eq!(class_parent_prototype_bits(1.0), None); | ||
| } | ||
|
|
||
| /// #8760: a `class X extends <bound native EventEmitter export>` registered | ||
| /// through the dynamic-parent path (`import { EventEmitter as EE } from | ||
| /// "events"; class X extends EE {}` — cli.js 2.1.112's exact shape) must | ||
| /// link its declared prototype to EventEmitter's real, lazily-materialized | ||
| /// prototype instead of throwing "Class extends value does not have valid | ||
| /// prototype property". The raw `prototype` dynamic slot on the bound export | ||
| /// closure is still `undefined` here, so the resolution must fall through to | ||
| /// the same lazy materialization an ordinary `EE.prototype` read uses. | ||
| #[test] | ||
| fn native_constructor_export_parent_links_declared_prototype() { | ||
| const CLASS_ID: u32 = 0x7d01_8760; | ||
| const CLASS_NAME: &[u8] = b"Issue8760Subclass"; | ||
|
|
||
| // The bound `events.EventEmitter` export — a BOUND_METHOD closure whose | ||
| // `.prototype` object is materialized on demand, not at mint time. | ||
| let ee_ctor = crate::object::bound_native_callable_export_value("events", "EventEmitter"); | ||
|
|
||
| // Precondition: the raw dynamic `prototype` slot is undefined — the exact | ||
| // condition that made the dynamic-parent registration throw before the fix. | ||
| let ee_addr = (ee_ctor.to_bits() & crate::value::POINTER_MASK) as usize; | ||
| assert_eq!( | ||
| crate::closure::closure_get_dynamic_prop(ee_addr, "prototype").to_bits(), | ||
| crate::value::TAG_UNDEFINED, | ||
| "precondition: the bound export's raw prototype slot must be undefined" | ||
| ); | ||
|
|
||
| unsafe { | ||
| js_register_class_name(CLASS_ID, CLASS_NAME.as_ptr(), CLASS_NAME.len() as u32); | ||
| } | ||
| js_register_class_parent_dynamic(CLASS_ID, ee_ctor); | ||
|
|
||
| // Must not throw, and must materialize a real prototype object. | ||
| let decl_proto = class_decl_prototype_value(CLASS_ID); | ||
| assert_eq!( | ||
| decl_proto.to_bits() & crate::value::TAG_MASK, | ||
| crate::value::POINTER_TAG, | ||
| "the subclass prototype must materialize (no TypeError) for a native constructor parent" | ||
| ); | ||
|
|
||
| // Its [[Prototype]] must be EventEmitter's canonical prototype — the same | ||
| // object an ordinary `EE.prototype` read resolves to — so `instanceof` | ||
| // and inherited `emit`/`on` work through the chain. | ||
| let ee_proto = super::function_prototype::ordinary_function_prototype_value_for_read( | ||
| crate::object::bound_native_callable_export_value("events", "EventEmitter"), | ||
| ) | ||
| .expect("EventEmitter export must expose a prototype object"); | ||
| let decl_proto_addr = (decl_proto.to_bits() & crate::value::POINTER_MASK) as usize; | ||
| let linked = super::super::prototype_chain::object_static_prototype(decl_proto_addr) | ||
| .expect("the subclass prototype must have a linked [[Prototype]]"); | ||
| assert_eq!( | ||
| linked & crate::value::POINTER_MASK, | ||
| ee_proto.to_bits() & crate::value::POINTER_MASK, | ||
| "the subclass prototype must inherit from EventEmitter.prototype" | ||
| ); | ||
| } | ||
| } | ||
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
🧩 Analysis chain
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 6565
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 1115
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 2084
Rename the fragment to
changelog.d/8763-class-extends-native-constructor-prototype.md;8760is the issue number, and8763is the pull-request number.🤖 Prompt for AI Agents
Source: Coding guidelines