Skip to content

fix: static this is the constructor; class name/toString/inspect report source identity (from #9465) - #9472

Merged
proggeramlug merged 3 commits into
mainfrom
fix/9465-static-this-identity
Sep 2, 2026
Merged

fix: static this is the constructor; class name/toString/inspect report source identity (from #9465)#9472
proggeramlug merged 3 commits into
mainfrom
fix/9465-static-this-identity

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Lands #9465, which cherry-picks with conflicts but merges cleanly; this carries a merge of current main. Author's commits preserved.

One conflict, resolved toward main's canonical form. Both this PR (#9413) and #9461 (#9415) fixed the same int32-tag/class-ref confusion in formatting.rs's inspect ladder. Main's version routes through value_repr::int32_or_class_repr — the declared single decision point for that tag, with the identical registry-probe behaviour this PR implemented inline — plus an array-hole arm this PR predates. I verified the helper's body subsumes the inline version before choosing it.

Verified behaviourally: static who() { return this.name } answers the class name on a plain class AND on one with a computed member — the #9404 shape that took down cc --help — and name/toString()/util.inspect all report source identity, byte-identical to node.

Validation: perry-runtime + perry-codegen green (39 suites) under RUST_TEST_THREADS=1; release build clean, no warnings; file-size, raw-handle, addr-class, census, root-holder, thread-local and fmt gates all pass.

Summary by CodeRabbit

  • New Features

    • Class names now display correctly, including anonymous, exported, and scoped classes.
    • String(Class), Class.toString(), and related output now show the class’s source text when available.
    • Class inspection output now uses readable [class ...] formatting.
  • Bug Fixes

    • Corrected static this behavior so instance members are not exposed as static members.
    • Preserved proper static inheritance and constructor behavior.
  • Tests

    • Added coverage for class naming, source rendering, inspection, inheritance, and static-member behavior.

Ralph Küpper added 3 commits September 1, 2026 23:17
…ce (#9404)

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
)

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
# Conflicts:
#	crates/perry-runtime/src/builtins/formatting.rs
@proggeramlug
proggeramlug merged commit e86a261 into main Sep 2, 2026
16 of 20 checks passed
@proggeramlug
proggeramlug deleted the fix/9465-static-this-identity branch September 2, 2026 02:04
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 35014d7d-9a80-4a93-a5a8-064649149309

📥 Commits

Reviewing files that changed from the base of the PR and between 326d574 and ede2a45.

📒 Files selected for processing (62)
  • changelog.d/9404-static-this-is-not-an-instance.md
  • changelog.d/9413-class-name-and-source-text.md
  • crates/perry-codegen-arkts/src/tests.rs
  • crates/perry-codegen-arkts/tests/phase2_full_app_smoke.rs
  • crates/perry-codegen/src/codegen/artifacts.rs
  • crates/perry-codegen/src/codegen/clone_suffix_tests.rs
  • crates/perry-codegen/src/codegen/declared_string_add_tests.rs
  • crates/perry-codegen/src/codegen/emission_order_tests.rs
  • crates/perry-codegen/src/codegen/entry/tests.rs
  • crates/perry-codegen/src/codegen/number_exactness_tests.rs
  • crates/perry-codegen/src/codegen/string_pool.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/native_root_coverage/mod.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-codegen/src/temp_root_coverage/mod.rs
  • crates/perry-codegen/src/type_analysis/numeric/tests.rs
  • crates/perry-codegen/src/type_analysis/predicates.rs
  • crates/perry-codegen/src/type_analysis/strings/tests.rs
  • crates/perry-codegen/src/type_analysis_facts.rs
  • crates/perry-codegen/src/type_analysis_tests.rs
  • crates/perry-codegen/tests/app_window_config_options.rs
  • crates/perry-codegen/tests/argless_builtin_extra_args.rs
  • crates/perry-codegen/tests/class_field_store_pointer_test.rs
  • crates/perry-codegen/tests/class_keys_gc_root.rs
  • crates/perry-codegen/tests/constructor_recursion.rs
  • crates/perry-codegen/tests/i64_spec_ternary_recursion.rs
  • crates/perry-codegen/tests/ios_platform_api_lowering.rs
  • crates/perry-codegen/tests/large_object_barriers.rs
  • crates/perry-codegen/tests/loop_safepoint_purity.rs
  • crates/perry-codegen/tests/macos_bundle_chdir_gate.rs
  • crates/perry-codegen/tests/native_proof_buffer_views.rs
  • crates/perry-codegen/tests/native_proof_regressions.rs
  • crates/perry-codegen/tests/node_test_mock_property_presence.rs
  • crates/perry-codegen/tests/perry_builtin_name_collision.rs
  • crates/perry-codegen/tests/private_guard_declaring_class.rs
  • crates/perry-codegen/tests/release_boxes_lowering.rs
  • crates/perry-codegen/tests/scalar_replaced_slot_roots.rs
  • crates/perry-codegen/tests/shadow_slot_hygiene.rs
  • crates/perry-codegen/tests/static_symbol_hygiene.rs
  • crates/perry-codegen/tests/temp_root_operand_temporaries.rs
  • crates/perry-codegen/tests/typed_feedback.rs
  • crates/perry-codegen/tests/typed_shape_declared_at_allocation.rs
  • crates/perry-codegen/tests/typed_shape_descriptor.rs
  • crates/perry-codegen/tests/typed_shape_descriptors.rs
  • crates/perry-hir/src/ir/module.rs
  • crates/perry-hir/src/lower/context.rs
  • crates/perry-hir/src/lower/expr_new/non_ident.rs
  • crates/perry-hir/src/lower/lower_module_fn.rs
  • crates/perry-hir/src/lower/lowering_context.rs
  • crates/perry-hir/src/lower_decl/class_decl.rs
  • crates/perry-hir/src/monomorph/driver.rs
  • crates/perry-hir/src/stable_hash/module.rs
  • crates/perry-runtime/src/object/class_registry.rs
  • crates/perry-runtime/src/object/class_registry/class_meta.rs
  • crates/perry-runtime/src/object/class_registry/prototype_objects.rs
  • crates/perry-runtime/src/object/global_this/array_error.rs
  • crates/perry-runtime/src/object/native_call_method/common_methods.rs
  • crates/perry-runtime/src/value/to_string.rs
  • test-files/_helpers/class_name_default_export_9413.ts
  • test-files/test_class_name_and_source_9413.ts
  • test-files/test_class_name_cjs_9413.cts
  • test-files/test_static_this_is_not_an_instance_9404.ts

📝 Walkthrough

Walkthrough

The change corrects static this handling and constructor-side property lookup. It also preserves source-level class names and class text through HIR, code generation, and runtime formatting. New regression tests cover static inheritance, class names, inspection, and stringification.

Changes

Class behavior and identity

Layer / File(s) Summary
Static receiver semantics
crates/perry-codegen/src/type_analysis/*, crates/perry-runtime/src/object/class_registry/prototype_objects.rs, test-files/test_static_this_is_not_an_instance_9404.ts
Static this no longer infers as an instance. Constructor-side lookup excludes declared instance members while preserving static inheritance and C.constructor.
Class names and source registration
crates/perry-hir/src/lower*, crates/perry-hir/src/monomorph/driver.rs, crates/perry-codegen/src/codegen/*, crates/perry-runtime/src/object/class_registry/*, crates/perry-runtime/src/object/*to_string*, test-files/test_class_name*
Class display names and source text are captured, emitted, registered, and used by .name, toString(), String(), and inspection formatting.
Module shape updates
crates/perry-hir/src/ir/module.rs, crates/perry-codegen*/**tests*, crates/perry-codegen/tests/*
Module fixtures initialize the new class_source_text map.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StaticMethod
  participant TypeAnalysis
  participant RuntimeLookup
  StaticMethod->>TypeAnalysis: resolve static this
  TypeAnalysis-->>StaticMethod: avoid instance type
  StaticMethod->>RuntimeLookup: read constructor property
  RuntimeLookup-->>StaticMethod: resolve static member only
Loading
sequenceDiagram
  participant ClassLowering
  participant HIRModule
  participant Codegen
  participant Runtime
  ClassLowering->>HIRModule: capture class name and source
  HIRModule->>Codegen: emit class metadata
  Codegen->>Runtime: register class source
  Runtime-->>Codegen: format class text and inspect label
Loading

Suggested reviewers: thehypnoo

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/9465-static-this-identity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

proggeramlug added a commit that referenced this pull request Sep 2, 2026
…C.constructor === Function (#9467) (#9496)

* fix(decorators): instance-member decorators receive Class.prototype; C.constructor === Function (#9467)

Two divergences that cancelled, found while fixing #9404 (#9465/#9472):

1. A legacy decorator on an INSTANCE member (property, method, method
   parameter) received the class itself as `target`; tsc's
   `__decorate([...], C.prototype, key, desc)` hands it `Class.prototype`.
   Static members correctly keep the constructor.
2. `C.constructor === C`; node says `Function`. The decl-prototype carries
   `constructor` as an ordinary data field and the constructor-side chain
   walk (`resolve_proto_chain_field`) returned it before the class-ref
   arm's existing `constructor -> Function` tail fallback was reached.

NestJS-style `Reflect.defineMetadata(k, v, target.constructor)` only landed
on `C` because both were wrong. Both halves fixed together:

- perry-hir `lower/decorators.rs`: `member_decorator_target` hands instance
  members `PropertyGet(ClassRef, "prototype")` (the reflective decl-proto
  object), statics `ClassRef`; `design:type` / `design:paramtypes` ride the
  same target. The metadata store's prototype->class fold keeps the
  historical `getMetadata(..., Class, prop)` reads resolving.
- perry-runtime `prototype_objects.rs`: the constructor-side walk also
  skips the `constructor` key, so `C.constructor` falls through to
  `Function`; `C.prototype.constructor` and instance reads are untouched.

Fixture `test_decorators_target_prototype_9467` (expected output from
tsc --experimentalDecorators --emitDecoratorMetadata + reflect-metadata
under node) pins: target identity per member kind, `C.constructor ===
Function`, `p.constructor === C`, and `Reflect.getMetadata` round-trips
through both `target` and `target.constructor`, including inheritance.

Claude-Session: https://claude.ai/code/session_01MmDfS97fv8TRgyDnj6bgsL

* changelog: #9496 decorator target / C.constructor fragment

* 9467: `"constructor" in C` agrees with the read; fixture pins user-class metadata types only

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant