From 90d5313a7f322c6ab1b5bd86e6a849ca77c6cbe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 1 Sep 2026 22:22:01 +0200 Subject: [PATCH 1/2] fix(codegen,runtime): static `this` is the constructor, not an instance (#9404) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inside a `static` body, `this` is the class CONSTRUCTOR — an INT32 class ref — not a heap instance. Two independent defects both made instance members resolve on it, and each is reachable on its own. 1. `receiver_class_name(Expr::This)` and `static_type_of(Expr::This)` answered `Named(class_stack.last())` in a static body exactly as in an instance body, so every consumer was entitled to prove instance facts about the constructor object: field slots, shape ids, direct method dispatch. Both now return `None` under `FnCtx::in_static_member`, and `CodegenTypeFacts::this_type` carries the same gate so the generic HIR inference cannot re-derive the instance type for an expression that merely CONTAINS `this`. This closes the alias residual #9386 left open (`static viaLocal(){ const t = this; … }`: `undefined|` -> `object|9`). "The constructor object of C" would not be a better answer: static members are inherited, so a static body's `this` is whatever subclass the call came through. `None` is the only sound answer. 2. Declared instance methods are mirrored onto the reflective `C.prototype` object as own data fields, and the CONSTRUCTOR-side read in `js_object_get_field_by_name` walked that object via `resolve_proto_chain_field`. So `class P { m(){} }` gave `typeof P.m === "function"` and `P.m === P.prototype.m` with no `this` involved at all. `js_object_has_property` already had the gate (`"m" in P` was correctly false); the receiver-less `resolve_proto_chain_field` — whose one caller is that static-side read — now refuses a name that `class_instance_has_member` reports as a prototype method/getter/setter of the chain. Keyed on that predicate, NOT on "skip the decl-prototype entirely". The blanket form was tried and measured: it also removes `C.constructor`, which the decl-prototype carries as a data field, and that answer is load-bearing because perry hands a PROPERTY DECORATOR the class itself where the spec hands `Class.prototype` — so NestJS-style `Reflect.defineMetadata(k, v, target.constructor)` relies on `C.constructor === C`. Node says `Function`; perry has two divergences that cancel, and the blanket form took `test_decorators_nest_common_canary` and `test_decorators_legacy_property_metadata` from pass to parity_fail. The decorator-target defect is the one worth fixing and is not this issue. The `class_prototype_object` step is never skipped: for a subclass of a class-EXPRESSION value it holds the parent CLASS OBJECT (#1788/#6552), genuinely on the constructor's static chain. Known residual, deliberately not asserted in the fixture: the CALL form `this.m()` in a static body still resolves the instance vtable method. The value read is fixed, and both `P.m()` and read-then-call throw correctly; the residual is keyed on the runtime class id (an inherited `static go(){ return this.m() }` called on a subclass runs the SUBCLASS's override), so it is the runtime call tower, not a codegen direct call. Repro in the fixture's comment. Refs #9404, #9369, #9386. Claude-Session: https://claude.ai/code/session_014knX724SYDogwzsXybCGxp --- .../9404-static-this-is-not-an-instance.md | 101 ++++++++++++++++++ crates/perry-codegen/src/expr/mod.rs | 9 ++ .../src/type_analysis/predicates.rs | 30 +++++- .../perry-codegen/src/type_analysis_facts.rs | 10 ++ .../perry-codegen/src/type_analysis_tests.rs | 22 ++++ .../class_registry/prototype_objects.rs | 60 ++++++++++- ...est_static_this_is_not_an_instance_9404.ts | 93 ++++++++++++++++ 7 files changed, 319 insertions(+), 6 deletions(-) create mode 100644 changelog.d/9404-static-this-is-not-an-instance.md create mode 100644 test-files/test_static_this_is_not_an_instance_9404.ts diff --git a/changelog.d/9404-static-this-is-not-an-instance.md b/changelog.d/9404-static-this-is-not-an-instance.md new file mode 100644 index 0000000000..0420f9626a --- /dev/null +++ b/changelog.d/9404-static-this-is-not-an-instance.md @@ -0,0 +1,101 @@ +### Fixed + +- **Inside a `static` body, `this` is no longer treated as an instance of the + class.** `class P { m() { return 1; } static probe() { return typeof this.m; } }` + answered `"function"`; node answers `"undefined"`. Worse than the `typeof`: + `this.m()` in a static body *succeeded*, running the instance method body with + the class ref as its receiver, where node throws a `TypeError`. + + Two independent defects produced that one symptom, and each is reachable on + its own. + + **1. The codegen type predicates typed static `this` as an instance.** + `receiver_class_name(Expr::This)` and `static_type_of(Expr::This)` + (`crates/perry-codegen/src/type_analysis/predicates.rs`) both answered + `Named(class_stack.last())` in a static body exactly as they do in an instance + body. `class_stack` names the owning class in a static body too — that is what + `super.x` resolves against — but a static body's `this` is the class + CONSTRUCTOR: an INT32 class ref, never a heap instance. Every consumer of + those two answers was therefore entitled to prove instance facts about the + constructor object: instance field slots, shape ids, direct method dispatch. + + `Named(C)` is not merely imprecise here, and "the constructor object of C" + would not have been a better answer: static members are INHERITED, so `this` + in a static body of `Base` is whatever subclass the call came through + (`Sub.inherited()` sees `this === Sub`, and `Sub` may override every static + member the body touches). `None` is the only sound answer, and it is what both + predicates now return under `FnCtx::in_static_member`. + + This is what closes the alias residual #9386 documented and left open: + `static viaLocal() { const t = this; … }` reached the computed-member route + through `guarded_declared_class_get_candidate`, which reads `local_types` — + written by `refine_type_from_init` from `static_type_of`. With that predicate + honest the wrong type never enters `local_types` + (`G.viaLocal()`: `undefined|` → `object|9`). + + **2. The runtime's constructor-side property walk read `C.prototype`.** + Declared instance methods are mirrored onto the reflective `C.prototype` + object as own data fields. `resolve_proto_chain_field` walks that object, and + the CONSTRUCTOR-side read in `js_object_get_field_by_name` (`C.foo` on a class + ref, after own statics and the static-method chain miss) called it — so every + prototype method resolved on the class object. This needs no `this` at all: + on `dcf1ec0fbc`, `class P { m(){} }` gave `typeof P.m === "function"` and + `P.m === P.prototype.m`, via the dot, computed, and `Reflect.get` forms alike. + `js_object_has_property` already had the gate (`"m" in P` was correctly + `false`), and the `is_prototype_ref` gate in the same file plugged this hole on + the direct-vtable door for #1021/NestJS — this is that door's chain-walk twin. + + The receiver-less `resolve_proto_chain_field` has exactly one caller and it is + that static-side read, so the exclusion is applied there rather than at the + call site. It is keyed on `class_instance_has_member` — the exact "is this a + prototype method / getter / setter of the chain" predicate — and NOT on "skip + the decl-prototype entirely". A blanket skip was tried first and is wrong: it + also removes `C.constructor`, which the decl-prototype carries as an ordinary + data field. That answer is load-bearing today for a reason outside this issue: + **perry hands a PROPERTY DECORATOR the class itself where the spec hands it + `Class.prototype`**, so NestJS-style + `Reflect.defineMetadata(k, v, target.constructor)` relies on + `C.constructor === C`. Node says `C.constructor === Function`, so perry has two + divergences that cancel, and removing either alone breaks decorator metadata — + measured: `test_decorators_nest_common_canary` and + `test_decorators_legacy_property_metadata` both went pass -> parity_fail on the + blanket version. The decorator-target defect is the one worth fixing, and it is + not this issue. + + The `class_prototype_object` step of the same walk is never skipped: for a + subclass of a class-EXPRESSION value it holds the parent CLASS OBJECT + (#1788/#6552), which is genuinely on the constructor's static chain. + + Fixing only (1) would have left the issue's own example broken, and would have + moved one shape — `const t = this; typeof t.computedMethod` — from + accidentally-right to wrong, because it stopped taking the computed-member + route (which answered `undefined` for the wrong reason) and joined every other + instance-member read on the leaking generic path. + + Affected files: + + - `crates/perry-codegen/src/type_analysis/predicates.rs` — a guarded + `Expr::This if ctx.in_static_member => None` arm ahead of each existing + `Expr::This` arm. + - `crates/perry-codegen/src/type_analysis_facts.rs` — + `CodegenTypeFacts::this_type` carries the same gate. Without it the generic + HIR inference (`infer_expr_type`) re-derived `Named(C)` for every expression + that merely *contains* `this`, routing around `static_type_of`'s refusal. + - `crates/perry-runtime/src/object/class_registry/prototype_objects.rs` — + `resolve_proto_chain_field_inner` takes `skip_decl_prototype`, set for the + constructor-side form only. + + No fast path is lost for the operations a static body actually performs. + Static field reads through `this` (`this.sf`), static method calls through + `this` (`this.other()`), `this.prototype` and `this.name` were already on the + generic class-ref dispatch: `class_field_global_index` never matched a static + field, and `resolve_static_dispatch_cls` has no `Expr::This` arm — + deliberately, because static inheritance means `this` in a static body cannot + be resolved to the declaring class at compile time. + + Validation: `test-files/test_static_this_is_not_an_instance_9404.ts`, + byte-compared against `node --experimental-strip-types`, covering a static + method, a static block, a static getter, `this === C`, static-to-static + dispatch through `this`, the same on a subclass where `this` is the *sub*class, + the `const t = this` alias (plain and computed member), an instance-side + control, and a static method whose name collides with a String method. diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 89aa6d254c..073d1fa500 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -2441,6 +2441,15 @@ impl<'a> FnCtx<'a> { /// /// Sees through `Expr::PrivateGuard`, which returns its receiver /// unchanged, mirroring `receiver_class_name`'s own arm for it. + /// + /// #9404 made `receiver_class_name` / `static_type_of` themselves refuse + /// `Expr::This` in a static body, so the two call sites below are now + /// defense in depth rather than the only guard: neither can be reached + /// with a static `this` any more (the class name they gate on comes from + /// `receiver_class_name`, which answers `None`, or from + /// `guarded_declared_class_{get,store}_candidate`, which requires a + /// `LocalGet`). They are kept because they encode the reason — a class ref + /// has no instance layout — at the sites that would strip the NaN-box. pub(crate) fn is_static_class_this(&self, e: &perry_hir::Expr) -> bool { if !self.in_static_member { return false; diff --git a/crates/perry-codegen/src/type_analysis/predicates.rs b/crates/perry-codegen/src/type_analysis/predicates.rs index 79f9482947..e6ad932f7b 100644 --- a/crates/perry-codegen/src/type_analysis/predicates.rs +++ b/crates/perry-codegen/src/type_analysis/predicates.rs @@ -360,9 +360,26 @@ pub(crate) fn receiver_class_name(ctx: &FnCtx<'_>, e: &Expr) -> Option { None => Some(class_name.clone()), }, e if net_result_class(e).is_some() => net_result_class(e).map(str::to_string), - // `this` inside a constructor or method body — the class name is - // at the top of class_stack (for inlined constructors) or comes - // from the enclosing method's owning class. + // #9404: NOT in a static body. `class_stack` names the owning class + // there too — that is what `super.x` resolves against — but this + // function answers "is the receiver a known INSTANCE of a Named + // class", and a static body's `this` is the class CONSTRUCTOR: an + // INT32 class ref, never a heap instance. Answering `Some(C)` let + // every consumer prove instance facts about the constructor object. + // Measured on `757beace0`: `this.instanceMethod` in a static body + // lowered to a bound-method closure (`typeof` reported "function" + // where node says "undefined"), and CALLING it ran the instance body + // with the class ref as receiver instead of throwing a TypeError. + // + // "The constructor object of C" would not be a better answer either: + // static members are INHERITED, so `this` in a static body of `Base` + // is whatever subclass the call came through — `Sub.inherited()` sees + // `this === Sub`, and `Sub` may override every static member the body + // touches. Only `None` is sound. + Expr::This if ctx.in_static_member => None, + // `this` inside a constructor or instance method body — the class + // name is at the top of class_stack (for inlined constructors) or + // comes from the enclosing method's owning class. Expr::This => ctx.class_stack.last().cloned(), // A private-access brand guard returns its receiver unchanged; see // through it so shadowed private-field slot resolution stays accurate. @@ -686,6 +703,13 @@ pub(crate) fn static_type_of(ctx: &FnCtx<'_>, e: &Expr) -> Option { } hir_inferred_static_type(ctx, e) } + // #9404: a static body's `this` is the class constructor, not an + // instance of the class — see `receiver_class_name`'s matching arm. + // `refine_type_from_init` writes this answer into `local_types`, so + // `const t = this` in a static body used to hand every LocalGet + // consumer an instance type for the constructor object (the #9386 + // alias residual). + Expr::This if ctx.in_static_member => None, Expr::This => { let cls = ctx.class_stack.last()?.clone(); Some(HirType::Named(cls)) diff --git a/crates/perry-codegen/src/type_analysis_facts.rs b/crates/perry-codegen/src/type_analysis_facts.rs index 6eee542f72..5a4bf29bf0 100644 --- a/crates/perry-codegen/src/type_analysis_facts.rs +++ b/crates/perry-codegen/src/type_analysis_facts.rs @@ -12,6 +12,12 @@ pub(crate) struct CodegenTypeFacts<'a> { pub(crate) classes: &'a std::collections::HashMap, pub(crate) interfaces: &'a std::collections::HashMap, pub(crate) class_stack: &'a [String], + /// #9404: mirrors `FnCtx::in_static_member`. `this_type` must not answer + /// `Named()` for a static body's `this` — the generic HIR + /// inference reached by every expression that CONTAINS `this` (e.g. + /// `PropertyGet { object: This }`) would otherwise re-derive the instance + /// type that `static_type_of`'s own `Expr::This` arm now refuses. + pub(crate) in_static_member: bool, pub(crate) enums: &'a std::collections::HashMap<(String, String), perry_hir::EnumValue>, } @@ -24,6 +30,7 @@ impl<'a> CodegenTypeFacts<'a> { classes: ctx.classes, interfaces: ctx.interfaces, class_stack: &ctx.class_stack, + in_static_member: ctx.in_static_member, enums: ctx.enums, } } @@ -56,6 +63,9 @@ impl HirTypeFacts for CodegenTypeFacts<'_> { } fn this_type(&self) -> Option { + if self.in_static_member { + return None; + } self.class_stack.last().cloned().map(HirType::Named) } diff --git a/crates/perry-codegen/src/type_analysis_tests.rs b/crates/perry-codegen/src/type_analysis_tests.rs index 0d694d86e5..52a9ab2052 100644 --- a/crates/perry-codegen/src/type_analysis_tests.rs +++ b/crates/perry-codegen/src/type_analysis_tests.rs @@ -181,6 +181,7 @@ fn hir_inferred_types_reuse_imported_function_return_facts() { classes: &classes, interfaces: &interfaces, class_stack: &class_stack, + in_static_member: false, enums: &enums, }; let call = Expr::Call { @@ -219,6 +220,7 @@ fn codegen_type_facts_invalidate_reassigned_local_hints() { classes: &classes, interfaces: &interfaces, class_stack: &class_stack, + in_static_member: false, enums: &enums, }; @@ -376,6 +378,7 @@ fn hir_inferred_types_reuse_codegen_contextual_class_facts() { classes: &classes, interfaces: &interfaces, class_stack: &class_stack, + in_static_member: false, enums: &enums, }; @@ -383,6 +386,24 @@ fn hir_inferred_types_reuse_codegen_contextual_class_facts() { infer_expr_type(&Expr::This, &facts), HirType::Named("Widget".to_string()) ); + // #9404: the SAME class stack in a STATIC body must NOT infer an instance + // type. `this` there is the class constructor, and every consumer of this + // answer goes on to prove instance facts (field slots, shape ids, direct + // method dispatch) that are false for it. `Any` is the honest answer — + // and it must stay `Any` rather than becoming `Named("Widget")` again, + // because static members are inherited, so a static body's `this` can be + // any subclass of the class named on the stack. + let static_facts = CodegenTypeFacts { + proven_local_types: &local_types, + reassigned_locals: &reassigned_locals, + imported_func_return_types: &imported_func_return_types, + classes: &classes, + interfaces: &interfaces, + class_stack: &class_stack, + in_static_member: true, + enums: &enums, + }; + assert_eq!(infer_expr_type(&Expr::This, &static_facts), HirType::Any); assert_eq!( infer_expr_type( &Expr::EnumMember { @@ -497,6 +518,7 @@ fn function_return_type_is_conservative() { classes: &classes, interfaces: &interfaces, class_stack: &class_stack, + in_static_member: false, enums: &enums, }; assert_eq!(facts.function_return_type(0), None); diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs index 3fe6bc5fd1..1271493e24 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -334,11 +334,14 @@ pub(crate) fn class_prototype_object(class_id: u32) -> *mut ObjectHeader { /// when it has no `keys_array` at all (an `Object.create(proto)` result, or /// a `Function.prototype = obj` instance with no own props). Returns the /// first defined, non-null field found on the chain. +/// The receiver-less form, whose ONE caller is the CONSTRUCTOR-side read in +/// `js_object_get_field_by_name` (`C.foo` on an INT32 class ref). It therefore +/// refuses a name that is a declared INSTANCE member — see `constructor_side`. pub(crate) unsafe fn resolve_proto_chain_field( class_id: u32, key: *const crate::StringHeader, ) -> Option { - resolve_proto_chain_field_inner(class_id, key, None) + resolve_proto_chain_field_inner(class_id, key, None, true) } pub(crate) unsafe fn resolve_proto_chain_field_with_receiver( @@ -346,7 +349,7 @@ pub(crate) unsafe fn resolve_proto_chain_field_with_receiver( key: *const crate::StringHeader, receiver: f64, ) -> Option { - resolve_proto_chain_field_inner(class_id, key, Some(receiver)) + resolve_proto_chain_field_inner(class_id, key, Some(receiver), false) } unsafe fn inherited_proto_accessor_value( @@ -380,11 +383,58 @@ unsafe fn inherited_proto_accessor_value( )) } +/// `constructor_side`: this walk serves a read on the class CONSTRUCTOR, so a +/// name that is a declared INSTANCE member must not resolve through it. +/// +/// #9404: declared instance methods are MIRRORED onto the reflective +/// `C.prototype` object as own data fields (see the `own_data_field_by_name` +/// arm below). That is right for an instance read, which is what the +/// `_with_receiver` form serves. It is wrong for the CONSTRUCTOR-side read the +/// receiver-less form serves: in JS a class object does not expose its +/// prototype methods as statics — `class C { m(){} }` has `C.m === undefined`, +/// because `m` lives on `C.prototype`. Walking into the decl-prototype from a +/// static lookup made every instance method resolve on the class ref, so +/// `typeof C.m` answered "function", `C.m === C.prototype.m` was true, and a +/// static body's `typeof this.m` answered "function" where node says +/// "undefined" — the surviving half of #9404 after the codegen predicate was +/// made honest. `js_object_has_property` already had this gate (`"m" in C` was +/// correctly false), and the sibling `is_prototype_ref` gate two hundred lines +/// up in `get_field_by_name.rs` plugged the same hole on the direct-vtable +/// door for #1021/NestJS; this is that door's chain-walk twin. +/// +/// The exclusion is keyed on `class_instance_has_member` — the exact +/// "is this a prototype method / getter / setter of the chain" predicate — and +/// NOT on "skip the decl-prototype entirely". A blanket skip also removed +/// `C.constructor`, which the decl-prototype carries as an ordinary data field, +/// and that is load-bearing today for a reason outside this issue: perry hands +/// a PROPERTY DECORATOR the class itself where the spec hands it +/// `Class.prototype`, so NestJS-style `Reflect.defineMetadata(k, v, +/// target.constructor)` relies on `C.constructor === C`. Node says +/// `C.constructor === Function`, so perry has two divergences that cancel; +/// removing either one alone breaks decorator metadata +/// (`test_decorators_nest_common_canary`, +/// `test_decorators_legacy_property_metadata`). The decorator-target defect is +/// the one to fix, and it is not this issue. +/// +/// The `class_prototype_object` step below is never skipped: for a subclass of +/// a class-EXPRESSION value it holds the parent CLASS OBJECT (#1788/#6552), +/// which is genuinely on the constructor's static chain. unsafe fn resolve_proto_chain_field_inner( class_id: u32, key: *const crate::StringHeader, receiver: Option, + constructor_side: bool, ) -> Option { + // Resolved once: `class_instance_has_member` already walks the parent + // chain, so a parent's instance method is excluded from a subclass's + // constructor read too. + let skip_decl_prototype = constructor_side && !key.is_null() && { + let key_ptr = crate::string::string_data(key); + let key_len = (*key).byte_len as usize; + std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)) + .map(|name| crate::object::class_instance_has_member(class_id, name)) + .unwrap_or(false) + }; let mut cid = class_id; let mut depth = 0usize; while depth < 32 { @@ -411,7 +461,11 @@ unsafe fn resolve_proto_chain_field_inner( // re-walk) so a computed prototype write is visible as // `(new C()).name` (#6945). Class methods / vtable data still come // from the vtable + `class_prototype_object` path below. - let decl_proto = class_decl_prototype_object(cid); + let decl_proto = if skip_decl_prototype { + std::ptr::null_mut() + } else { + class_decl_prototype_object(cid) + }; if !decl_proto.is_null() { if let Some(receiver) = receiver { if let Some(value) = inherited_proto_accessor_value(decl_proto, key, receiver) { diff --git a/test-files/test_static_this_is_not_an_instance_9404.ts b/test-files/test_static_this_is_not_an_instance_9404.ts new file mode 100644 index 0000000000..9280709e46 --- /dev/null +++ b/test-files/test_static_this_is_not_an_instance_9404.ts @@ -0,0 +1,93 @@ +// #9404: inside a STATIC body `this` is the CONSTRUCTOR object, not an +// instance. The compiler must not type it as an instance of the class: +// instance members must be absent, and static members must still resolve. +class P { + static sf = 7; + static sobj = { a: 1 }; + x = 3; + m() { return 1; } + static probeMethod() { return typeof this.m; } + static probeField() { return typeof this.x; } + static probeValue() { return String((this as any).m); } + // NOT asserted: the CALL form `this.m()` with a static-`this` receiver. + // The VALUE read is fixed (see `probeValue` above — `undefined`), and both + // `P.m()` on the class binding and read-then-call (`const f = this.m; f()`) + // throw correctly, but the runtime call tower still resolves the instance + // vtable method for a CONSTRUCTOR class ref given a method name. It is keyed + // on the runtime class id — `class S extends B {}` calling an inherited + // `static go(){ return this.m(); }` runs `S`'s override, not `B`'s — so it is + // the runtime dispatch, not a codegen direct call. Repro: + // class B { m(){return "B.m"} static go(){ return (this as any).m() } } + // B.go() // node: TypeError perry: "B.m" + static readStatic() { return this.sf; } + static readStaticNested() { return this.sobj.a; } + static isConstructor() { return this === P; } + static other() { return 42; } + static viaThis() { return this.other(); } + static protoKind() { return typeof this.prototype; } + static ownName() { return this.name; } + static { console.log("static-block:", typeof this.m, this === P, this.sf); } + static get sgetter() { return typeof this.m; } +} +console.log("typeof this.m:", P.probeMethod()); +console.log("typeof this.x:", P.probeField()); +console.log("String(this.m):", P.probeValue()); +console.log("this.sf:", P.readStatic()); +console.log("this.sobj.a:", P.readStaticNested()); +console.log("this === P:", P.isConstructor()); +console.log("this.other():", P.viaThis()); +console.log("typeof this.prototype:", P.protoKind()); +console.log("this.name:", P.ownName()); +console.log("static getter:", P.sgetter); + +// #9386 residual: aliasing `this` to a local made the receiver a LocalGet, +// so the static-`this` gate no longer applied and the wrong type reached +// the computed-member route through `local_types`. +const K = "dy" + "n"; +class G { + static sf = 9; + [K]() { return 4; } + static viaLocal() { const t: any = this; return [typeof t.prototype, t.sf].join("|"); } + static aliasMethod() { const t: any = this; return typeof t.dyn; } +} +console.log("alias prototype|sf:", G.viaLocal()); +console.log("alias computed method:", G.aliasMethod()); + +// A static body on a SUBCLASS: `this` is the SUBclass, not the declarer. +class Base { static tag = "base"; static who() { return this.name; } static readTag() { return this.tag; } } +class Kid extends Base { static tag = "kid"; } +console.log("subclass this.name:", Kid.who(), Base.who()); +console.log("subclass this.tag:", Kid.readTag(), Base.readTag()); + +// A static method reaching another static method through `this` on a +// subclass resolves against the SUBCLASS's override. +class B3 { static a() { return "B3.a"; } static callA() { return this.a(); } } +class S3 extends B3 { static a() { return "S3.a"; } } +console.log("subclass static dispatch:", S3.callA(), B3.callA()); + +// An instance method of the same class is unaffected: `this` IS an instance. +class Inst { v = 5; get2() { return this.v * 2; } run() { return typeof this.get2; } } +console.log("instance side:", new Inst().get2(), new Inst().run()); + +// A static method whose name collides with a String method, called through +// `this` — the static receiver must dispatch to the static member. +class Collide { + static split() { return "static-split"; } + static go() { return (this as any).split(); } +} +console.log("collide:", Collide.go()); + +// The constructor-side half, with no `this` at all: a class object does not +// expose its prototype methods as statics. Reachable through every read form. +class R { m() { return 1; } static s() { return 2; } } +const key = "m"; +console.log("C.m:", typeof (R as any).m, typeof (R as any)[key], typeof Reflect.get(R as any, "m")); +console.log("C.s:", typeof R.s); +console.log("C.prototype.m:", typeof R.prototype.m); +console.log("m in C:", "m" in (R as any), "| own:", Object.getOwnPropertyNames(R).join(",")); +console.log("C.m === C.prototype.m:", (R as any).m === (R as any).prototype.m); + +// A subclass must not inherit its parent's prototype methods as statics +// either, while genuine static inheritance keeps working. +class RSub extends R { static t() { return 3; } } +console.log("sub:", typeof (RSub as any).m, typeof RSub.s, typeof RSub.t); From 314c78ab52e55e8c36b19f421a9cb3f9b32d702f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 1 Sep 2026 22:22:15 +0200 Subject: [PATCH 2/2] fix: class `.name`, `toString` and inspect report source identity (#9413) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three leaks of the compiler's internal class identity into user-visible strings, plus the missing half of `Function.prototype.toString`. `.name` reported the disambiguation key. Two `class Made {}` in sibling scopes are distinct classes, so the second registers under a uniquified key (`Made$0`) to keep the name-keyed dedup from aliasing the bodies; that key reached `js_register_class_name`. And a class expression constructed IN PLACE lost its name entirely: `new (class Q {})()` gave `__anon_class_6`, `new (class extends Error {})("m")` gave `__anon_class_8` where node gives `""`. Both are fixed by populating the existing `Module::class_display_names` override that `codegen/string_pool.rs` already prefers over the registration key — the sibling `lower_expr/arm_class.rs` had recorded exactly that since #5592. `console.log(C)` / `util.inspect(C)` printed the raw class id (`6`): a class ref shares the INT32 encoding with a tagged small integer, and the console formatter's `is_int32()` arm printed the payload. It now renders node's `[class Klass]` / `[class Sub extends Named]` / `[class (anonymous)]`, gated on the class-id registry — the same probe `js_jsvalue_to_string` already used, so a plain small integer whose value collides with a live class id still prints as a number (asserted). `String(C)` / `C.toString()` returned `function C() { [native code] }`. Perry already retained function source (`closure_source_text`, #4101); the same span-slice-at-lowering mechanism, keyed by ClassId, was simply never applied to classes — the one callable kind that is not a ClosureHeader and so cannot recover source from the closure registry. `Module::class_source_text` is sliced against `ast::Class::span`, emitted as `js_register_class_source`, and read by all three class-ref toString sites. A class with no registered source keeps the `[native code]` form, which Test262's `assertToStringOrNativeFunction` accepts. Monomorphized specializations inherit the origin's source, as #7632 makes them inherit its name. Not addressed: `String(C.prototype.m)` for a class METHOD still returns `function () { [native code] }`. Class methods compile to `perry_method_*` symbols rather than closures with a registered source, so that needs the method-side equivalent of the closure source registry. Object-literal methods already work and are kept in the fixture as the control. Refs #9413, #5592, #4101, #7632. Claude-Session: https://claude.ai/code/session_014knX724SYDogwzsXybCGxp --- .../9413-class-name-and-source-text.md | 84 +++++++++++++++++++ crates/perry-codegen-arkts/src/tests.rs | 1 + .../tests/phase2_full_app_smoke.rs | 1 + crates/perry-codegen/src/codegen/artifacts.rs | 1 + .../src/codegen/clone_suffix_tests.rs | 1 + .../src/codegen/declared_string_add_tests.rs | 1 + .../src/codegen/emission_order_tests.rs | 1 + .../perry-codegen/src/codegen/entry/tests.rs | 1 + .../src/codegen/number_exactness_tests.rs | 1 + .../perry-codegen/src/codegen/string_pool.rs | 46 ++++++++++ .../src/native_root_coverage/mod.rs | 1 + .../src/runtime_decls/strings.rs | 2 + .../src/temp_root_coverage/mod.rs | 1 + .../src/type_analysis/numeric/tests.rs | 1 + .../src/type_analysis/strings/tests.rs | 1 + .../tests/app_window_config_options.rs | 1 + .../tests/argless_builtin_extra_args.rs | 1 + .../tests/class_field_store_pointer_test.rs | 1 + .../perry-codegen/tests/class_keys_gc_root.rs | 1 + .../tests/constructor_recursion.rs | 1 + .../tests/i64_spec_ternary_recursion.rs | 1 + .../tests/ios_platform_api_lowering.rs | 1 + .../tests/large_object_barriers.rs | 2 + .../tests/loop_safepoint_purity.rs | 1 + .../tests/macos_bundle_chdir_gate.rs | 1 + .../tests/native_proof_buffer_views.rs | 1 + .../tests/native_proof_regressions.rs | 7 ++ .../tests/node_test_mock_property_presence.rs | 1 + .../tests/perry_builtin_name_collision.rs | 1 + .../tests/private_guard_declaring_class.rs | 1 + .../tests/release_boxes_lowering.rs | 1 + .../tests/scalar_replaced_slot_roots.rs | 1 + .../tests/shadow_slot_hygiene.rs | 7 ++ .../tests/static_symbol_hygiene.rs | 2 + .../tests/temp_root_operand_temporaries.rs | 1 + crates/perry-codegen/tests/typed_feedback.rs | 1 + .../typed_shape_declared_at_allocation.rs | 1 + .../tests/typed_shape_descriptor.rs | 1 + .../tests/typed_shape_descriptors.rs | 1 + crates/perry-hir/src/ir/module.rs | 13 +++ crates/perry-hir/src/lower/context.rs | 1 + .../perry-hir/src/lower/expr_new/non_ident.rs | 15 +++- crates/perry-hir/src/lower/lower_module_fn.rs | 4 + .../perry-hir/src/lower/lowering_context.rs | 4 + crates/perry-hir/src/lower_decl/class_decl.rs | 31 +++++++ crates/perry-hir/src/monomorph/driver.rs | 10 +++ crates/perry-hir/src/stable_hash/module.rs | 10 +++ .../perry-runtime/src/builtins/formatting.rs | 8 ++ .../src/object/class_registry.rs | 9 +- .../src/object/class_registry/class_meta.rs | 84 +++++++++++++++++++ .../src/object/global_this/array_error.rs | 7 +- .../native_call_method/common_methods.rs | 12 +-- crates/perry-runtime/src/value/to_string.rs | 11 +-- .../class_name_default_export_9413.ts | 5 ++ test-files/test_class_name_and_source_9413.ts | 80 ++++++++++++++++++ test-files/test_class_name_cjs_9413.cts | 23 +++++ 56 files changed, 489 insertions(+), 20 deletions(-) create mode 100644 changelog.d/9413-class-name-and-source-text.md create mode 100644 test-files/_helpers/class_name_default_export_9413.ts create mode 100644 test-files/test_class_name_and_source_9413.ts create mode 100644 test-files/test_class_name_cjs_9413.cts diff --git a/changelog.d/9413-class-name-and-source-text.md b/changelog.d/9413-class-name-and-source-text.md new file mode 100644 index 0000000000..7d1a0db481 --- /dev/null +++ b/changelog.d/9413-class-name-and-source-text.md @@ -0,0 +1,84 @@ +### Fixed + +- **A class's compiler-internal identity no longer escapes into `.name`, + `Function.prototype.toString`, or `util.inspect`.** Three separate leaks, all + of the same shape: a registration key or a class id that only the compiler + should ever see, handed to the program as a user-visible string. + + 1. **`.name` reported the disambiguation key.** Two `class Made {}` in sibling + function bodies are distinct classes, so the second registers under a + uniquified key (`Made$0`) to keep the name-keyed dedup from aliasing the two + bodies onto one ClassId — see `maybe_rename_colliding_class`. That key + reached `js_register_class_name`, so `Made.name` and + `new Made().constructor.name` answered `"Made$0"`. + + 2. **A class expression constructed in place lost its name entirely.** + `new (class extends Error {})("m").constructor.name` answered + `"__anon_class_8"` (node: `""`), and even a *named* one — + `new (class Q {})().constructor.name` — answered `"__anon_class_6"` instead + of `"Q"`. `lower_new_non_ident` lowers straight to a `New` on a synthetic + key and never recorded the spec name, while its sibling + `lower_expr/arm_class.rs` had recorded exactly that override + (`display_override`) since #5592. + + Both are fixed by populating the existing `Module::class_display_names` + override that `codegen/string_pool.rs` already prefers over the + registration key. No new mechanism. + + 3. **`console.log(C)` and `util.inspect(C)` printed the raw class id.** + `util.inspect(Klass)` answered `6`. A class ref shares the INT32 encoding + with a tagged small integer, and the console formatter's `is_int32()` arm + printed the payload. It now renders node's form — `[class Klass]`, + `[class Sub extends Named]`, `[class (anonymous)]`. + +- **`String(C)` / `C.toString()` now return the class's source text.** They + returned `function Klass() { [native code] }`, which is not what node produces + for a class and not something a caller can parse. Perry already retained + function source (`Module::closure_source_text`, #4101) — the same + span-slice-at-lowering mechanism, keyed by ClassId, was simply never applied to + classes, which are the one callable kind that is not a `ClosureHeader` and so + cannot recover source from the closure registry. + + `Module::class_source_text` is populated at lowering by slicing the module + source against `ast::Class::span` (SWC anchors it at the `class` keyword and + closes it at the body's `}`, so the slice is exactly the class's + `[[SourceText]]`), emitted by codegen as `js_register_class_source`, and read + by all three class-ref `toString` sites. A class with no registered source (a + builtin, or one perry synthesized) still gets the `[native code]` form, which + Test262's `assertToStringOrNativeFunction` accepts. Monomorphized + specializations inherit the origin's source, for the same reason #7632 makes + them inherit its name. + + Affected files: + + - `crates/perry-hir/src/lower_decl/class_decl.rs` — `capture_class_source` + (the class sibling of `capture_function_source`), plus the display-name + override for a renamed duplicate. + - `crates/perry-hir/src/lower/expr_new/non_ident.rs` — record the spec `.name` + of an in-place-constructed class expression. + - `crates/perry-hir/src/ir/module.rs`, + `crates/perry-hir/src/lower/{context,lowering_context,lower_module_fn}.rs`, + `crates/perry-hir/src/stable_hash/module.rs`, + `crates/perry-hir/src/monomorph/driver.rs` — the `class_source_text` map and + its flush; it participates in the stable hash because it drives codegen. + - `crates/perry-codegen/src/codegen/{string_pool,artifacts}.rs`, + `crates/perry-codegen/src/runtime_decls/strings.rs` — emit + `js_register_class_source`. + - `crates/perry-runtime/src/object/class_registry/class_meta.rs` — the source + side table, `class_ref_to_string`, `class_ref_inspect_label`. + - `crates/perry-runtime/src/value/to_string.rs`, + `crates/perry-runtime/src/object/native_call_method/common_methods.rs`, + `crates/perry-runtime/src/object/global_this/array_error.rs`, + `crates/perry-runtime/src/builtins/formatting.rs` — the four read sites. + + Not addressed, and still divergent: `String(C.prototype.m)` for a class + METHOD returns `function () { [native code] }` (node returns the method's + source). Class methods compile to `perry_method_*` symbols rather than + closures with a registered source, so this needs the method-side equivalent of + the closure source registry, not another read of this one. Object-literal + methods already work and are kept in the fixture as the control. + + Validation: `test-files/test_class_name_and_source_9413.ts` (ESM) and + `test-files/test_class_name_cjs_9413.cts` (CommonJS, for the + `module.exports = class {}` spellings that get no NamedEvaluation), both + byte-compared against `node --experimental-strip-types`. diff --git a/crates/perry-codegen-arkts/src/tests.rs b/crates/perry-codegen-arkts/src/tests.rs index 4ab35f7520..5d2ee1eb31 100644 --- a/crates/perry-codegen-arkts/src/tests.rs +++ b/crates/perry-codegen-arkts/src/tests.rs @@ -50,6 +50,7 @@ pub(crate) fn empty_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), local_source_spans: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen-arkts/tests/phase2_full_app_smoke.rs b/crates/perry-codegen-arkts/tests/phase2_full_app_smoke.rs index 224997d6db..8feff4b1e5 100644 --- a/crates/perry-codegen-arkts/tests/phase2_full_app_smoke.rs +++ b/crates/perry-codegen-arkts/tests/phase2_full_app_smoke.rs @@ -59,6 +59,7 @@ fn empty_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), local_source_spans: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index 94684a5b86..8ef5073852 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -1878,6 +1878,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { class_ids, class_table, &hir.class_display_names, + &hir.class_source_text, &ctor_arity_overrides, closure_rest_params, closure_arities, diff --git a/crates/perry-codegen/src/codegen/clone_suffix_tests.rs b/crates/perry-codegen/src/codegen/clone_suffix_tests.rs index bfb01dae83..ec15bd2528 100644 --- a/crates/perry-codegen/src/codegen/clone_suffix_tests.rs +++ b/crates/perry-codegen/src/codegen/clone_suffix_tests.rs @@ -98,6 +98,7 @@ fn module_with(functions: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/src/codegen/declared_string_add_tests.rs b/crates/perry-codegen/src/codegen/declared_string_add_tests.rs index 8a2e185a40..f1e2a6578b 100644 --- a/crates/perry-codegen/src/codegen/declared_string_add_tests.rs +++ b/crates/perry-codegen/src/codegen/declared_string_add_tests.rs @@ -95,6 +95,7 @@ fn module_with(function: Function) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/src/codegen/emission_order_tests.rs b/crates/perry-codegen/src/codegen/emission_order_tests.rs index dba3b46622..07a692e3cc 100644 --- a/crates/perry-codegen/src/codegen/emission_order_tests.rs +++ b/crates/perry-codegen/src/codegen/emission_order_tests.rs @@ -153,6 +153,7 @@ fn empty_module(name: &str) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/src/codegen/entry/tests.rs b/crates/perry-codegen/src/codegen/entry/tests.rs index 92402da46a..a9f4f744d0 100644 --- a/crates/perry-codegen/src/codegen/entry/tests.rs +++ b/crates/perry-codegen/src/codegen/entry/tests.rs @@ -88,6 +88,7 @@ fn empty_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/src/codegen/number_exactness_tests.rs b/crates/perry-codegen/src/codegen/number_exactness_tests.rs index 7133baaa52..3bb778eb8d 100644 --- a/crates/perry-codegen/src/codegen/number_exactness_tests.rs +++ b/crates/perry-codegen/src/codegen/number_exactness_tests.rs @@ -143,6 +143,7 @@ fn module_with(functions: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/src/codegen/string_pool.rs b/crates/perry-codegen/src/codegen/string_pool.rs index ded5ceba4c..fefcd27854 100644 --- a/crates/perry-codegen/src/codegen/string_pool.rs +++ b/crates/perry-codegen/src/codegen/string_pool.rs @@ -112,6 +112,10 @@ pub(super) fn emit_string_pool( // #5592: user-visible `.name` overrides keyed by ClassId, for classes // whose HIR registration key was uniquified away from their JS name. class_display_names: &HashMap, + // #9413: retained class SOURCE text keyed by ClassId, so `String(C)` / + // `C.toString()` return the class source instead of a synthesized + // `function C() { [native code] }`. + class_source_text: &HashMap, // Wall 51: per-class standalone-constructor arity, accounting for the // synthesized `super(...args)` forwarding ctor a no-own-ctor class with // heritage inherits (its arity comes from the nearest ancestor ctor, which @@ -305,6 +309,31 @@ pub(super) fn emit_string_pool( } } + // #9413: the same pre-allocation for retained class source text — also + // before `init_fn` borrows `llmod`. + let mut class_source_constants: Vec<(u32, String, usize)> = Vec::new(); + { + let mut sources: Vec<(u32, &String)> = Vec::new(); + for (class_name, class) in classes.iter() { + if *class_name != class.name || class_name.starts_with("__AnonShape_") { + continue; + } + let cid = match class_ids.get(class_name).copied() { + Some(c) if c != 0 => c, + _ => continue, + }; + if let Some(src) = class_source_text.get(&cid) { + sources.push((cid, src)); + } + } + sources.sort_by_key(|entry| entry.0); + sources.dedup_by_key(|(cid, _)| *cid); + for (cid, src) in sources { + let (const_name, byte_len) = llmod.add_string_constant(src); + class_source_constants.push((cid, const_name, byte_len)); + } + } + // Emit per-class typed-shape raw-f64 and pointer-mask globals. Empty masks // emit no storage. Must run BEFORE // `init_fn = llmod.define_function(...)` because that call holds a @@ -1105,6 +1134,23 @@ pub(super) fn emit_string_pool( ], ); } + // #9413: mirror each class's retained source text into the runtime so + // `Function.prototype.toString` on a class REF (an INT32 immediate, not a + // ClosureHeader) answers with the class source. Same shape as the + // `js_register_function_source` loop above. + for (cid, const_name, byte_len) in &class_source_constants { + chunker.roll_if_full(); + let blk = chunker.current_block(); + let const_ref = format!("@{}", const_name); + blk.call_void( + "js_register_class_source", + &[ + (crate::types::I32, &cid.to_string()), + (crate::types::PTR, &const_ref), + (crate::types::I32, &byte_len.to_string()), + ], + ); + } // Class refs are immediate values, not heap Function objects. Register // each constructor's visible arity so the field-get path can reify the // Function-compatible own `length` property. diff --git a/crates/perry-codegen/src/native_root_coverage/mod.rs b/crates/perry-codegen/src/native_root_coverage/mod.rs index 94f954df63..2a617d73dd 100644 --- a/crates/perry-codegen/src/native_root_coverage/mod.rs +++ b/crates/perry-codegen/src/native_root_coverage/mod.rs @@ -199,6 +199,7 @@ fn bare_module(name: &str) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index 611ac76ec1..abb3879d17 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -1377,6 +1377,8 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { // is non-empty. Codegen emits one call per registered class id at // program init, mirroring `js_register_class_id`. module.declare_function("js_register_class_name", VOID, &[I32, PTR, I32]); + // #9413: the class-source sibling of `js_register_function_source`. + module.declare_function("js_register_class_source", VOID, &[I32, PTR, I32]); module.declare_function("js_register_class_length", VOID, &[I32, I32]); // Anon-shape class registration so `.constructor` reads on object // literals (`{ x: 1 }`) return the global `Object` constructor diff --git a/crates/perry-codegen/src/temp_root_coverage/mod.rs b/crates/perry-codegen/src/temp_root_coverage/mod.rs index 371f62558d..0c1cfa1550 100644 --- a/crates/perry-codegen/src/temp_root_coverage/mod.rs +++ b/crates/perry-codegen/src/temp_root_coverage/mod.rs @@ -134,6 +134,7 @@ fn module_with_init(name: &str, init: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/src/type_analysis/numeric/tests.rs b/crates/perry-codegen/src/type_analysis/numeric/tests.rs index 0a8d1c2f71..1e5f4d914d 100644 --- a/crates/perry-codegen/src/type_analysis/numeric/tests.rs +++ b/crates/perry-codegen/src/type_analysis/numeric/tests.rs @@ -113,6 +113,7 @@ fn probe_module(name: &str, params: Vec, body: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/src/type_analysis/strings/tests.rs b/crates/perry-codegen/src/type_analysis/strings/tests.rs index ed32969f0f..875b61cca2 100644 --- a/crates/perry-codegen/src/type_analysis/strings/tests.rs +++ b/crates/perry-codegen/src/type_analysis/strings/tests.rs @@ -107,6 +107,7 @@ fn concat_probe_ir(property: &str) -> String { closure_display_names: HashMap::new(), class_display_names: HashMap::new(), closure_source_text: HashMap::new(), + class_source_text: HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: HashMap::new(), diff --git a/crates/perry-codegen/tests/app_window_config_options.rs b/crates/perry-codegen/tests/app_window_config_options.rs index 560c8d3cc8..a4bbe8a7ce 100644 --- a/crates/perry-codegen/tests/app_window_config_options.rs +++ b/crates/perry-codegen/tests/app_window_config_options.rs @@ -114,6 +114,7 @@ fn module(name: &str, body: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/argless_builtin_extra_args.rs b/crates/perry-codegen/tests/argless_builtin_extra_args.rs index f65ebbe58b..a55e1a834e 100644 --- a/crates/perry-codegen/tests/argless_builtin_extra_args.rs +++ b/crates/perry-codegen/tests/argless_builtin_extra_args.rs @@ -95,6 +95,7 @@ fn module_with_init(init: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/class_field_store_pointer_test.rs b/crates/perry-codegen/tests/class_field_store_pointer_test.rs index bd6c2d0986..047ba839c2 100644 --- a/crates/perry-codegen/tests/class_field_store_pointer_test.rs +++ b/crates/perry-codegen/tests/class_field_store_pointer_test.rs @@ -216,6 +216,7 @@ fn module_with_new(class: Class, args: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/class_keys_gc_root.rs b/crates/perry-codegen/tests/class_keys_gc_root.rs index b8e83ee683..f49c8469f8 100644 --- a/crates/perry-codegen/tests/class_keys_gc_root.rs +++ b/crates/perry-codegen/tests/class_keys_gc_root.rs @@ -152,6 +152,7 @@ fn module_with_declared_field_class() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/constructor_recursion.rs b/crates/perry-codegen/tests/constructor_recursion.rs index c0bbd2bdf6..d89a9cc53f 100644 --- a/crates/perry-codegen/tests/constructor_recursion.rs +++ b/crates/perry-codegen/tests/constructor_recursion.rs @@ -159,6 +159,7 @@ fn module_with_recursive_constructor_return() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/i64_spec_ternary_recursion.rs b/crates/perry-codegen/tests/i64_spec_ternary_recursion.rs index d789a07cd0..905dcbe978 100644 --- a/crates/perry-codegen/tests/i64_spec_ternary_recursion.rs +++ b/crates/perry-codegen/tests/i64_spec_ternary_recursion.rs @@ -155,6 +155,7 @@ fn module_with(functions: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/ios_platform_api_lowering.rs b/crates/perry-codegen/tests/ios_platform_api_lowering.rs index 0cff00eead..7490a39868 100644 --- a/crates/perry-codegen/tests/ios_platform_api_lowering.rs +++ b/crates/perry-codegen/tests/ios_platform_api_lowering.rs @@ -116,6 +116,7 @@ fn module(body: Vec) -> Module { closure_display_names: Default::default(), class_display_names: Default::default(), closure_source_text: Default::default(), + class_source_text: Default::default(), async_generator_funcs: Default::default(), local_source_spans: Default::default(), gen_param_prologue_len: Default::default(), diff --git a/crates/perry-codegen/tests/large_object_barriers.rs b/crates/perry-codegen/tests/large_object_barriers.rs index 2538550940..2011e035df 100644 --- a/crates/perry-codegen/tests/large_object_barriers.rs +++ b/crates/perry-codegen/tests/large_object_barriers.rs @@ -141,6 +141,7 @@ fn module_with_large_pointer_array_literal(element_count: usize) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -214,6 +215,7 @@ fn module_with_large_local_array_push(element_count: usize) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/loop_safepoint_purity.rs b/crates/perry-codegen/tests/loop_safepoint_purity.rs index 9e9225ca3a..52c44a75c2 100644 --- a/crates/perry-codegen/tests/loop_safepoint_purity.rs +++ b/crates/perry-codegen/tests/loop_safepoint_purity.rs @@ -125,6 +125,7 @@ fn module_with_init(name: &str, init: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs b/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs index 96dbcc0910..8da9fc11fd 100644 --- a/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs +++ b/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs @@ -95,6 +95,7 @@ fn empty_entry_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/native_proof_buffer_views.rs b/crates/perry-codegen/tests/native_proof_buffer_views.rs index 92ca52c6bb..c08f2a33a9 100644 --- a/crates/perry-codegen/tests/native_proof_buffer_views.rs +++ b/crates/perry-codegen/tests/native_proof_buffer_views.rs @@ -145,6 +145,7 @@ fn module_with_classes_and_params( closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index 68fdfdf815..098a463d3b 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -142,6 +142,7 @@ fn module_with_classes_and_params( closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -8159,6 +8160,7 @@ fn typed_f64_clone_test_module(use_any_param: bool) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -8335,6 +8337,7 @@ fn typed_i1_clone_test_module_named(name: &str) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -8429,6 +8432,7 @@ fn typed_string_clone_test_module(case: &str) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -8545,6 +8549,7 @@ fn typed_i1_numeric_predicate_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -8624,6 +8629,7 @@ fn typed_i1_i32_predicate_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -8752,6 +8758,7 @@ fn typed_i32_return_module(case: &str) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/node_test_mock_property_presence.rs b/crates/perry-codegen/tests/node_test_mock_property_presence.rs index 44b823bfe2..0efc0f6afc 100644 --- a/crates/perry-codegen/tests/node_test_mock_property_presence.rs +++ b/crates/perry-codegen/tests/node_test_mock_property_presence.rs @@ -109,6 +109,7 @@ fn fixture_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/perry_builtin_name_collision.rs b/crates/perry-codegen/tests/perry_builtin_name_collision.rs index 4308c6916d..cb45b1f556 100644 --- a/crates/perry-codegen/tests/perry_builtin_name_collision.rs +++ b/crates/perry-codegen/tests/perry_builtin_name_collision.rs @@ -138,6 +138,7 @@ fn module_with(imports: Vec, init: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/private_guard_declaring_class.rs b/crates/perry-codegen/tests/private_guard_declaring_class.rs index f139585ff8..de51926bea 100644 --- a/crates/perry-codegen/tests/private_guard_declaring_class.rs +++ b/crates/perry-codegen/tests/private_guard_declaring_class.rs @@ -106,6 +106,7 @@ fn module_with(classes: Vec, body: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/release_boxes_lowering.rs b/crates/perry-codegen/tests/release_boxes_lowering.rs index 6c0f6de3e8..fdf676806f 100644 --- a/crates/perry-codegen/tests/release_boxes_lowering.rs +++ b/crates/perry-codegen/tests/release_boxes_lowering.rs @@ -105,6 +105,7 @@ fn module_with_init(name: &str, init: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/scalar_replaced_slot_roots.rs b/crates/perry-codegen/tests/scalar_replaced_slot_roots.rs index b3c81b61c2..bdcacb09b8 100644 --- a/crates/perry-codegen/tests/scalar_replaced_slot_roots.rs +++ b/crates/perry-codegen/tests/scalar_replaced_slot_roots.rs @@ -145,6 +145,7 @@ fn module_with_init(name: &str, init: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/shadow_slot_hygiene.rs b/crates/perry-codegen/tests/shadow_slot_hygiene.rs index 25fce24ab4..5d0ad376df 100644 --- a/crates/perry-codegen/tests/shadow_slot_hygiene.rs +++ b/crates/perry-codegen/tests/shadow_slot_hygiene.rs @@ -164,6 +164,7 @@ fn shadow_hygiene_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -224,6 +225,7 @@ fn top_level_shadow_module(name: &str) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -345,6 +347,7 @@ fn flat_const_row_alias_shadow_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -407,6 +410,7 @@ fn reassigned_any_shadow_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -484,6 +488,7 @@ fn mixed_any_alias_shadow_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -569,6 +574,7 @@ fn closure_captured_write_shadow_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -1161,6 +1167,7 @@ fn canonical_str_shadow_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/static_symbol_hygiene.rs b/crates/perry-codegen/tests/static_symbol_hygiene.rs index 138dbc3ad3..2f0ef8ebf5 100644 --- a/crates/perry-codegen/tests/static_symbol_hygiene.rs +++ b/crates/perry-codegen/tests/static_symbol_hygiene.rs @@ -156,6 +156,7 @@ fn duplicate_static_module() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), @@ -196,6 +197,7 @@ fn class_with_instance_and_static_method() -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/temp_root_operand_temporaries.rs b/crates/perry-codegen/tests/temp_root_operand_temporaries.rs index d984c7b968..e79bcc5fdc 100644 --- a/crates/perry-codegen/tests/temp_root_operand_temporaries.rs +++ b/crates/perry-codegen/tests/temp_root_operand_temporaries.rs @@ -145,6 +145,7 @@ fn module_with_init(name: &str, init: Vec) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/typed_feedback.rs b/crates/perry-codegen/tests/typed_feedback.rs index aef5f18d36..1c624f80c2 100644 --- a/crates/perry-codegen/tests/typed_feedback.rs +++ b/crates/perry-codegen/tests/typed_feedback.rs @@ -251,6 +251,7 @@ fn module_with_classes( closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs b/crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs index af9763cc8d..f9b8a1d585 100644 --- a/crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs +++ b/crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs @@ -220,6 +220,7 @@ fn module_with_new(class: Class, arg_count: usize) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/typed_shape_descriptor.rs b/crates/perry-codegen/tests/typed_shape_descriptor.rs index fcdea7df4b..bbfad5180c 100644 --- a/crates/perry-codegen/tests/typed_shape_descriptor.rs +++ b/crates/perry-codegen/tests/typed_shape_descriptor.rs @@ -152,6 +152,7 @@ fn module_with_new(class: Class) -> Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-codegen/tests/typed_shape_descriptors.rs b/crates/perry-codegen/tests/typed_shape_descriptors.rs index 4998997b65..4fd638d3e1 100644 --- a/crates/perry-codegen/tests/typed_shape_descriptors.rs +++ b/crates/perry-codegen/tests/typed_shape_descriptors.rs @@ -130,6 +130,7 @@ fn base_module(name: &str, body: Vec, interfaces: Vec) -> Modul closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-hir/src/ir/module.rs b/crates/perry-hir/src/ir/module.rs index 7b4c79a82c..20327364f2 100644 --- a/crates/perry-hir/src/ir/module.rs +++ b/crates/perry-hir/src/ir/module.rs @@ -145,6 +145,18 @@ pub struct Module { /// (and `Function.prototype.toString.call(fn)`) reconstruct the source /// instead of returning the generic `"[object Object]"`. pub closure_source_text: std::collections::HashMap, + /// #9413: original source text for each user class, keyed by ClassId. + /// Populated at lowering by slicing the module source against the class's + /// AST span (SWC anchors `Class::span` at the `class` keyword and ends it + /// at the closing brace, so the slice is exactly what + /// `Function.prototype.toString` must return). Consumed by codegen to emit + /// `js_register_class_source`, so `String(C)` / `C.toString()` / + /// `` `${C}` `` reconstruct the class source instead of the + /// `function C() { [native code] }` placeholder a class ref used to get — + /// classes are the one callable kind whose source perry retained nowhere, + /// even though the sibling `closure_source_text` had done it for every + /// function since #4101. + pub class_source_text: std::collections::HashMap, /// #3664: func_ids of `async function*` declarations and `async function*(){}` /// expressions. The generator transform clears `is_async`/`is_generator` /// before codegen, erasing the async-vs-sync distinction (both lower to a @@ -206,6 +218,7 @@ impl Module { closure_display_names: std::collections::HashMap::new(), class_display_names: std::collections::HashMap::new(), closure_source_text: std::collections::HashMap::new(), + class_source_text: std::collections::HashMap::new(), async_generator_funcs: std::collections::HashSet::new(), local_source_spans: std::collections::HashMap::new(), gen_param_prologue_len: std::collections::HashMap::new(), diff --git a/crates/perry-hir/src/lower/context.rs b/crates/perry-hir/src/lower/context.rs index 445f910296..532ead4347 100644 --- a/crates/perry-hir/src/lower/context.rs +++ b/crates/perry-hir/src/lower/context.rs @@ -129,6 +129,7 @@ impl LoweringContext { assignment_inferred_name: None, inferred_class_bindings: std::collections::HashSet::new(), closure_source_text: HashMap::new(), + class_source_text: HashMap::new(), func_return_native_instances: Vec::new(), pending_classes: Vec::new(), func_return_types: Vec::new(), diff --git a/crates/perry-hir/src/lower/expr_new/non_ident.rs b/crates/perry-hir/src/lower/expr_new/non_ident.rs index f3e9550efe..16305e3cfb 100644 --- a/crates/perry-hir/src/lower/expr_new/non_ident.rs +++ b/crates/perry-hir/src/lower/expr_new/non_ident.rs @@ -131,8 +131,21 @@ pub(crate) fn lower_new_non_ident( }; if let Some(class_expr) = class_expr_opt { let synthetic_name = format!("__anon_class_{}", ctx.fresh_class()); - ctx.pending_class_inner_name = class_expr.ident.as_ref().map(|i| i.sym.to_string()); + let source_class_name = class_expr.ident.as_ref().map(|i| i.sym.to_string()); + ctx.pending_class_inner_name = source_class_name.clone(); let class = lower_class_from_ast(ctx, &class_expr.class, &synthetic_name, false)?; + // #9413: `__anon_class_N` is a registration key minted so this in-place + // class expression gets its own ClassId — it is not a name. Per spec + // the constructor's `.name` is the class expression's own binding + // identifier, or `""` when it has none. The general class-expression + // arm (`lower_expr/arm_class.rs`) already records exactly this override + // via `display_override`; this arm, which lowers straight to a `New` on + // the synthetic name, never did — so + // `new (class extends Error {})("m").constructor.name` answered + // `"__anon_class_8"` and `new (class Q {})().constructor.name` + // answered `"__anon_class_6"` instead of `"Q"`. + ctx.class_display_names + .insert(class.id, source_class_name.unwrap_or_default()); // #6336: a class expression's `Subclass → Parent` registry edge is a SIDE // EFFECT of evaluating the expression — `lower_class_expr` sequences a // `RegisterClassParentDynamic` in front of the `ClassRef` it yields diff --git a/crates/perry-hir/src/lower/lower_module_fn.rs b/crates/perry-hir/src/lower/lower_module_fn.rs index 64f243d8df..ad26c0f4c0 100644 --- a/crates/perry-hir/src/lower/lower_module_fn.rs +++ b/crates/perry-hir/src/lower/lower_module_fn.rs @@ -1489,6 +1489,10 @@ pub fn lower_module_full( for (id, src) in ctx.closure_source_text.drain() { module.closure_source_text.insert(id, src); } + // #9413: same, for classes — `String(C)` must produce class source. + for (id, src) in ctx.class_source_text.drain() { + module.class_source_text.insert(id, src); + } // Flush any pending classes created during expression lowering // (e.g., class expressions in `new (class extends Command { ... })()`) for class in ctx.pending_classes.drain(..) { diff --git a/crates/perry-hir/src/lower/lowering_context.rs b/crates/perry-hir/src/lower/lowering_context.rs index 154815cf05..f744350041 100644 --- a/crates/perry-hir/src/lower/lowering_context.rs +++ b/crates/perry-hir/src/lower/lowering_context.rs @@ -367,6 +367,10 @@ pub struct LoweringContext { /// module source against each function's AST span at lowering time. /// Flushed into `Module.closure_source_text` alongside `pending_functions`. pub(crate) closure_source_text: HashMap, + /// #9413: original source text keyed by ClassId, captured by slicing the + /// module source against each class's AST span at lowering time. Flushed + /// into `Module.class_source_text`. See that field's docs. + pub(crate) class_source_text: HashMap, /// Functions that return native module instances: func_name -> (module_name, class_name) /// Tracks user-defined functions whose return type annotation is a native module type /// (e.g., initializePool(): mysql.Pool -> ("mysql2/promise", "Pool")) diff --git a/crates/perry-hir/src/lower_decl/class_decl.rs b/crates/perry-hir/src/lower_decl/class_decl.rs index 970a7af71e..6daacccc90 100644 --- a/crates/perry-hir/src/lower_decl/class_decl.rs +++ b/crates/perry-hir/src/lower_decl/class_decl.rs @@ -178,6 +178,24 @@ fn noncomputed_member_registration_name( format!("{}_{}_{}", base, method.span.lo.0, method.span.hi.0) } +/// #9413: retain a class's original source text keyed by ClassId so +/// `Function.prototype.toString` can reconstruct it, mirroring +/// `capture_function_source` (#4101) for functions. SWC anchors +/// `ast::Class::span` at the `class` keyword (decorators sit outside it) and +/// closes it at the class body's `}`, so the slice is exactly the class source +/// node's `[[SourceText]]`. A no-op when no module source is installed (unit +/// tests / `check`), and idempotent — last write wins, matching the name +/// registry. +pub(crate) fn capture_class_source( + ctx: &mut LoweringContext, + class_id: crate::ClassId, + class: &ast::Class, +) { + if let Some(src) = crate::ir::current_module_source_slice(class.span.lo.0, class.span.hi.0) { + ctx.class_source_text.insert(class_id, src); + } +} + pub fn lower_class_decl( ctx: &mut LoweringContext, class_decl: &ast::ClassDecl, @@ -196,6 +214,18 @@ pub fn lower_class_decl( id } }; + // #9413: a body-local `class X` that collides with an already-registered + // `X` registers under a uniquified key (`X$0`) so the name-keyed dedup + // keeps the two bodies apart — see `maybe_rename_colliding_class`. That + // key is a COMPILER artifact: `.name`, `String(C)` and `constructor.name` + // must still report the SOURCE name. Record the #5592 display-name + // override, which is what codegen already emits for + // `js_register_class_name` in place of the registration key. + if name != class_decl.ident.sym.as_str() { + ctx.class_display_names + .insert(class_id, class_decl.ident.sym.to_string()); + } + capture_class_source(ctx, class_id, &class_decl.class); if let Some(ast::Expr::Ident(parent)) = class_decl.class.super_class.as_deref() { if let Some(crate::lower::fn_ctor_env::FnCtorShape::DynCtor(kind)) = ctx.fn_ctor_env.entries.get(parent.sym.as_ref()).cloned() @@ -1270,6 +1300,7 @@ pub fn lower_class_from_ast( id } }; + capture_class_source(ctx, class_id, class); let old_class = ctx.current_class.take(); ctx.current_class = Some(name.to_string()); diff --git a/crates/perry-hir/src/monomorph/driver.rs b/crates/perry-hir/src/monomorph/driver.rs index d8a257e1e8..b6601bb6a4 100644 --- a/crates/perry-hir/src/monomorph/driver.rs +++ b/crates/perry-hir/src/monomorph/driver.rs @@ -14,6 +14,7 @@ pub fn monomorphize_module(module: &mut Module) { let mut new_classes = Vec::new(); // #7632: (specialization class id, the JS-visible name of its origin). let mut new_display_names: Vec<(crate::ClassId, String)> = Vec::new(); + let mut new_source_texts: Vec<(crate::ClassId, String)> = Vec::new(); while !ctx.func_work_queue.is_empty() || !ctx.class_work_queue.is_empty() { // Process function specializations @@ -91,6 +92,12 @@ pub fn monomorphize_module(module: &mut Module) { .cloned() .unwrap_or_else(|| original.name.clone()); new_display_names.push((new_id, display_name)); + // #9413: `String(Gen$num)` must show the origin's source for + // the same reason `.name` must show the origin's name — the + // mangling is perry's business, not the program's. + if let Some(src) = module.class_source_text.get(&original.id).cloned() { + new_source_texts.push((new_id, src)); + } let specialized = specialize_class(original, &request.type_args, new_id); new_classes.push(specialized); } @@ -104,6 +111,9 @@ pub fn monomorphize_module(module: &mut Module) { for (class_id, display_name) in new_display_names { module.class_display_names.insert(class_id, display_name); } + for (class_id, source) in new_source_texts { + module.class_source_text.insert(class_id, source); + } // Update call sites to use specialized versions update_call_sites(module, &ctx); diff --git a/crates/perry-hir/src/stable_hash/module.rs b/crates/perry-hir/src/stable_hash/module.rs index 5299c43a3c..8228e23588 100644 --- a/crates/perry-hir/src/stable_hash/module.rs +++ b/crates/perry-hir/src/stable_hash/module.rs @@ -39,6 +39,7 @@ impl SH for Module { closure_display_names, class_display_names, closure_source_text, + class_source_text, async_generator_funcs, // Observational source metadata does not affect emitted code and // therefore must not invalidate the object cache. @@ -110,6 +111,15 @@ impl SH for Module { id.hash(h); src.hash(h); } + // #9413: class source text drives the js_register_class_source calls, + // so it participates in the stable hash for the same reason. + let mut class_source_pairs: Vec<(u32, &String)> = + class_source_text.iter().map(|(k, v)| (*k, v)).collect(); + class_source_pairs.sort_unstable_by_key(|(k, _)| *k); + for (id, src) in class_source_pairs { + id.hash(h); + src.hash(h); + } // Generator param-prologue lengths drive the transform's prologue lift, // which changes codegen output — include in the stable hash. let mut prologue_pairs: Vec<(u32, usize)> = gen_param_prologue_len diff --git a/crates/perry-runtime/src/builtins/formatting.rs b/crates/perry-runtime/src/builtins/formatting.rs index b5b665ad28..a4fa3a384c 100644 --- a/crates/perry-runtime/src/builtins/formatting.rs +++ b/crates/perry-runtime/src/builtins/formatting.rs @@ -1093,6 +1093,14 @@ pub(crate) fn format_jsvalue(value: f64, depth: usize) -> String { } } } else if jsval.is_int32() { + // #9413: a class ref shares this encoding with a tagged small + // integer, and this arm printed the raw class id. The registry + // probe is the one `js_jsvalue_to_string` already uses to tell + // the two apart; see `class_ref_inspect_label`. + let cid = (value.to_bits() & 0xFFFF_FFFF) as u32; + if crate::object::is_class_id_registered(cid) { + return crate::object::class_ref_inspect_label(cid); + } jsval.as_int32().to_string() } else { // Regular number — but first check for raw (non-NaN-boxed) heap diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 05c595e74e..d711b8ca8a 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -103,10 +103,11 @@ pub(crate) use class_meta::test_text_encoding_stream_new_with_constructor; #[cfg(feature = "global-text")] pub(crate) use class_meta::text_decoder_bool_option; pub use class_meta::{ - class_length_for_id, class_name_for_id, declared_class_outranks_anon_shape, - is_anon_shape_class_id, js_compression_stream_new, js_decompression_stream_new, - js_register_anon_shape_class_id, js_register_class_id, js_register_class_length, - js_register_class_name, js_text_decoder_stream_new, js_text_encoder_stream_new, + class_length_for_id, class_name_for_id, class_ref_inspect_label, class_ref_to_string, + class_source_for_id, declared_class_outranks_anon_shape, is_anon_shape_class_id, + js_compression_stream_new, js_decompression_stream_new, js_register_anon_shape_class_id, + js_register_class_id, js_register_class_length, js_register_class_name, + js_register_class_source, js_text_decoder_stream_new, js_text_encoder_stream_new, js_text_encoding_stream_new, ANON_SHAPE_CLASS_IDS, CLASS_LENGTHS, CLASS_NAMES, }; pub(crate) use class_meta::{ diff --git a/crates/perry-runtime/src/object/class_registry/class_meta.rs b/crates/perry-runtime/src/object/class_registry/class_meta.rs index b08227e491..cc1692ccd3 100644 --- a/crates/perry-runtime/src/object/class_registry/class_meta.rs +++ b/crates/perry-runtime/src/object/class_registry/class_meta.rs @@ -65,6 +65,90 @@ pub fn class_name_for_id(class_id: u32) -> Option { guard.as_ref()?.get(&class_id).cloned() } +/// #9413: `class_id → the class's original source text`. Populated by codegen +/// via `js_register_class_source`, exactly as `js_register_function_source` +/// does for functions (#4101). A class ref is an INT32 immediate rather than a +/// heap Function object, so `Function.prototype.toString` cannot recover its +/// source from a `ClosureHeader` — this side table is the only record. Kept +/// out of the heap image (unlike `CLASS_NAMES`) because nothing but +/// `Function.prototype.toString` reads it. +fn class_source_registry( +) -> &'static std::sync::Mutex>> { + static REGISTRY: std::sync::OnceLock< + std::sync::Mutex>>, + > = std::sync::OnceLock::new(); + REGISTRY.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new())) +} + +/// Register the original source text of a class. Idempotent — last write wins, +/// matching `js_register_class_name`. +/// +/// # Safety +/// +/// `src_ptr..src_ptr + src_len` must point at a valid UTF-8 byte slice that +/// outlives the call (we copy it). `class_id` is used only as a map key. +#[no_mangle] +pub unsafe extern "C" fn js_register_class_source(class_id: u32, src_ptr: *const u8, src_len: u32) { + if class_id == 0 || src_ptr.is_null() || src_len == 0 { + return; + } + let slice = std::slice::from_raw_parts(src_ptr, src_len as usize); + let Ok(text) = std::str::from_utf8(slice) else { + return; + }; + if let Ok(mut map) = class_source_registry().lock() { + map.insert(class_id, std::sync::Arc::from(text)); + } +} + +/// The retained source text of a registered class, or `None` when codegen +/// registered none (a builtin, or a class synthesized at runtime). +pub fn class_source_for_id(class_id: u32) -> Option { + let map = class_source_registry().lock().ok()?; + map.get(&class_id).map(|text| text.to_string()) +} + +/// `Function.prototype.toString` for a class REF: the retained class source +/// when codegen registered it, otherwise the NativeFunction placeholder Node +/// uses for callables with no recoverable source. Mirrors +/// `builtins::function_source_for_func_ptr` for the INT32 class-ref encoding. +pub fn class_ref_to_string(class_id: u32) -> String { + if let Some(src) = class_source_for_id(class_id) { + return src; + } + let name = class_name_for_id(class_id).unwrap_or_default(); + format!("function {name}() {{ [native code] }}") +} + +/// #9413: Node's `util.inspect` / `console.log` rendering of a class +/// constructor — `[class Name]`, `[class Name extends Parent]`, +/// `[class (anonymous)]` when its `.name` is the empty string. +/// +/// A class ref is an INT32-tagged NaN box carrying the class id, the same +/// encoding a tagged small integer uses, and the console formatter's +/// `is_int32()` arm printed that payload — so `console.log(Klass)` and +/// `util.inspect(Klass)` both answered `6`, handing the program the +/// compiler's internal class identity. Callers must gate on +/// `is_class_id_registered` first, exactly as `js_jsvalue_to_string` does, +/// so a plain small integer whose value collides with a live class id still +/// prints as a number. +pub fn class_ref_inspect_label(class_id: u32) -> String { + let name = class_name_for_id(class_id).unwrap_or_default(); + let label = if name.is_empty() { + "(anonymous)" + } else { + name.as_str() + }; + match crate::object::get_parent_class_id(class_id) + .filter(|parent| *parent != 0) + .and_then(class_name_for_id) + .filter(|parent| !parent.is_empty()) + { + Some(parent) => format!("[class {label} extends {parent}]"), + None => format!("[class {label}]"), + } +} + #[no_mangle] pub extern "C" fn js_register_class_length(class_id: u32, length: u32) { if class_id == 0 { diff --git a/crates/perry-runtime/src/object/global_this/array_error.rs b/crates/perry-runtime/src/object/global_this/array_error.rs index e251412dd9..b88cea18bf 100644 --- a/crates/perry-runtime/src/object/global_this/array_error.rs +++ b/crates/perry-runtime/src/object/global_this/array_error.rs @@ -559,12 +559,11 @@ pub(crate) extern "C" fn function_prototype_to_string_thunk( return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); } // A class reference (INT32-tagged registered class id) is a function - // value; Perry retains no class source, so emit the NativeFunction - // form with the class name. + // value; #9413 retains its source text at compile time, so answer with + // that and keep the NativeFunction form only for classes with none. if super::super::class_prototype_ref_id(this_val).is_none() { if let Some(cid) = super::super::native_module::class_ref_id(this_val) { - let name = super::super::class_registry::class_name_for_id(cid).unwrap_or_default(); - let s = format!("function {name}() {{ [native code] }}"); + let s = super::super::class_registry::class_ref_to_string(cid); let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); } diff --git a/crates/perry-runtime/src/object/native_call_method/common_methods.rs b/crates/perry-runtime/src/object/native_call_method/common_methods.rs index 738315045a..ce77fe0da8 100644 --- a/crates/perry-runtime/src/object/native_call_method/common_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/common_methods.rs @@ -746,14 +746,14 @@ pub(super) unsafe fn dispatch_common( // Common string methods on string values "toString" => { // A class REFERENCE (INT32-tagged registered class id) is a - // function value: `C.toString()` must produce function source, - // not the numeric rendering of its class id ("1"). Perry doesn't - // retain class source text, so emit the NativeFunction form — - // Test262's assertToStringOrNativeFunction accepts it. + // function value: `C.toString()` must produce the class source, + // not the numeric rendering of its class id ("1"). #9413 retains + // that source at compile time; classes perry synthesized (no + // registered source) still get the NativeFunction form, which + // Test262's assertToStringOrNativeFunction accepts. if super::class_prototype_ref_id(object).is_none() { if let Some(cid) = super::native_module::class_ref_id(object) { - let name = super::class_registry::class_name_for_id(cid).unwrap_or_default(); - let s = format!("function {name}() {{ [native code] }}"); + let s = super::class_registry::class_ref_to_string(cid); let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); } diff --git a/crates/perry-runtime/src/value/to_string.rs b/crates/perry-runtime/src/value/to_string.rs index 4b687b9bc5..7210c15403 100644 --- a/crates/perry-runtime/src/value/to_string.rs +++ b/crates/perry-runtime/src/value/to_string.rs @@ -1041,9 +1041,11 @@ pub extern "C" fn js_jsvalue_to_string(value: f64) -> *mut crate::string::String } } else if jsval.is_int32() { // A registered class id shares the INT32 encoding (`Expr::ClassRef`) - // — `String(C)` / `"" + C` must produce function source, not the - // numeric id. Perry keeps no class source, so the NativeFunction - // form with the class name. + // — `String(C)` / `"" + C` must produce the class's source text, not + // the numeric id. #9413 gave codegen a class-source side table + // (`js_register_class_source`), so this is the real source when the + // class came from user code and the NativeFunction placeholder only + // for classes perry synthesized. let n = jsval.as_int32(); let cid = (value.to_bits() & 0xFFFF_FFFF) as u32; if crate::object::is_class_id_registered(cid) { @@ -1051,8 +1053,7 @@ pub extern "C" fn js_jsvalue_to_string(value: f64) -> *mut crate::string::String let primitive = unsafe { class_ref_to_primitive(value, 2) }; return js_jsvalue_to_string(primitive); } - let name = crate::object::class_name_for_id(cid).unwrap_or_default(); - let s = format!("function {name}() {{ [native code] }}"); + let s = crate::object::class_ref_to_string(cid); return crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); } let s = n.to_string(); diff --git a/test-files/_helpers/class_name_default_export_9413.ts b/test-files/_helpers/class_name_default_export_9413.ts new file mode 100644 index 0000000000..be8d70c5eb --- /dev/null +++ b/test-files/_helpers/class_name_default_export_9413.ts @@ -0,0 +1,5 @@ +// #9413 helper: `export default class {}` is the only spelling whose +// `.name` is "default" (ExportDeclaration NamedEvaluation), and it needs +// a second module to observe it. +export default class {} +export const namedDefault = class {}; diff --git a/test-files/test_class_name_and_source_9413.ts b/test-files/test_class_name_and_source_9413.ts new file mode 100644 index 0000000000..57133699e3 --- /dev/null +++ b/test-files/test_class_name_and_source_9413.ts @@ -0,0 +1,80 @@ +// #9413: `.name` and `Function.prototype.toString` must report the SOURCE +// identity of a class, never the compiler's disambiguation key +// (`Made$0`, `__anon_class_8`). +import AnonDefault from "./_helpers/class_name_default_export_9413.ts"; +import { inspect } from "node:util"; + +// --- Function.name: the whole node rule set ------------------------------- +class Named {} +const AnonConst = class {}; +let AnonLet = class {}; +var AnonVar = class {}; +const WithBinding = class Inner {}; +class Sub extends Named {} + +function nameOf(f: any) { return f.name; } + +console.log("decl:", Named.name); +console.log("const:", AnonConst.name); +console.log("let:", AnonLet.name); +console.log("var:", AnonVar.name); +console.log("expr-binding:", WithBinding.name); +console.log("subclass:", Sub.name); +console.log("export-default:", AnonDefault.name); +console.log("arg-anon:", nameOf(class {})); +console.log("arg-named:", nameOf(class ArgNamed {})); +console.log("arg-anon-extends:", nameOf(class extends Named {})); + +// Nested / shadowed same-name classes in sibling scopes: both are "Made". +function scopeA() { class Made {} return Made.name; } +function scopeB() { class Made {} return Made.name; } +class Made {} +console.log("shadowed:", Made.name, scopeA(), scopeB()); + +// Same, observed through an instance's constructor. +function ctorA() { class Dup {} return new Dup().constructor.name; } +function ctorB() { class Dup {} return new Dup().constructor.name; } +console.log("ctor-shadowed:", ctorA(), ctorB()); + +// A class expression constructed IN PLACE keeps the spec name. +console.log("new-anon:", new (class {})().constructor.name); +console.log("new-named:", new (class Zed {})().constructor.name); +console.log("new-anon-extends:", new (class extends Named {})().constructor.name); +console.log("new-anon-error:", new (class extends Error {})("m").constructor.name); +console.log("new-named-error:", new (class NErr extends Error {})("m").constructor.name); + +// Nested function declarations keep their own name. +function outer() { function inner() {} return inner.name; } +console.log("nested-fn:", outer()); + +// `.name` is configurable: defineProperty replaces it. +class Renamed {} +Object.defineProperty(Renamed, "name", { value: "Custom" }); +console.log("defineProperty:", Renamed.name); + +// A `static name` member wins over the inferred name. +class StaticName { static name = "override"; } +console.log("static-name:", StaticName.name); + +// --- Function.prototype.toString ------------------------------------------ +class Klass { x = 1; m() { return this.x; } } +console.log("String:", String(Klass)); +console.log("toString:", Klass.toString()); +console.log("template:", `${Klass}`); +console.log("concat:", "" + Klass); +console.log("expr-toString:", String(class Anon { y = 2; })); +console.log("subclass-toString:", String(class ExtNamed extends Named {})); +console.log("objmethod-toString:", String(({ m() { return 1; } }).m)); + +// util.inspect / console.log of a class object. +console.log("direct:", Klass); +console.log("inspect:", inspect(Klass)); +console.log("inspect-sub:", inspect(Sub)); +console.log("inspect-anon:", inspect(AnonConst)); + +// Control: a plain small integer must still print as a number even when its +// value collides with a live class id. A class ref shares the INT32 NaN-box +// encoding with tagged small integers, so the `[class …]` rendering above is +// gated on the class-id registry — the same probe `String(C)` already used. +const six = 6; +console.log("int-control:", six, 1, 2, 3, 6, 7, [6, 7], { v: 6 }, String(6)); diff --git a/test-files/test_class_name_cjs_9413.cts b/test-files/test_class_name_cjs_9413.cts new file mode 100644 index 0000000000..ed84776d7c --- /dev/null +++ b/test-files/test_class_name_cjs_9413.cts @@ -0,0 +1,23 @@ +// #9413, CommonJS arm: the same three leaks, in a module goal where the +// compiler additionally runs a source-level CJS wrap. `.ts` in this repo is +// ESM (`"type": "module"`), so this file is the only place the CJS lowering +// path is exercised. +// +// Deliberately NOT covered here: `module.exports = class {}` and +// `exports.Foo = class {}`. Both are member assignments, which per spec get no +// NamedEvaluation (node: `""`), but perry's CJS source rewrite turns the first +// into a NAMED declaration (`__perry_cjs_default__`) before parsing, and drops +// the binding-name inference for the local-`const` form. Those are defects of +// `crates/perry/src/commands/compile/cjs_wrap/hoist_classes.rs`, upstream of +// anything class metadata can reach — reported separately. +class Named {} +function scopeA() { class Made { } return Made.name; } +class Made {} + +console.log("decl:", Named.name); +console.log("ctor:", new Named().constructor.name); +console.log("shadowed:", Made.name, scopeA()); +console.log("new-anon:", new (class {})().constructor.name); +console.log("new-named:", new (class Zed {})().constructor.name); +console.log("String:", String(Named)); +console.log("inspect:", Named);