Skip to content

fix(hir): user class shadowing a global name loses to the intrinsic in new expressions (#6233) - #6270

Merged
proggeramlug merged 2 commits into
mainfrom
fix/6233-shadowed-global-class
Jul 11, 2026
Merged

fix(hir): user class shadowing a global name loses to the intrinsic in new expressions (#6233)#6270
proggeramlug merged 2 commits into
mainfrom
fix/6233-shadowed-global-class

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

A module-scope user class that legally shadows a global name — class Symbol extends Base {}new Symbol() — lost to the intrinsic: the built-in constructor arms in lower_new (perry-hir lower/expr_new.rs) fired by NAME alone. Depending on the name this either crashed at module init (Symbol/BigInt → "is not a constructor", Proxy/WeakRef/FinalizationRegistry/AggregateError → the intrinsic's argument validation) or silently constructed the wrong object (Map/Set/Date/Number/String/Boolean/Error/TypeError/Uint8Array/Int32Array → native instance, so field initializers never ran and instanceof the user class was false).

Real-world blocker: effect@4.0.0-beta.97's SchemaAST.ts declares AST node classes named Symbol and BigInt and constructs them at module init, so importing the effect barrel crashed under perry.compilePackages.

Baseline on current main (22-case blast-radius matrix from the issue): 16 of 22 shapes misbehaved. With this PR all 22 match node --experimental-strip-types byte-for-byte.

Root cause & fix

Same bug family as #5912/#5913 (new URL()) and #6003 (class Headers): unconditional by-name native routing that ignores lexical shadowing. Four surfaces:

  1. perry-hir/src/lower/expr_new.rs — the core fix. A shadowed_by_user_binding flag is snapshotted ONCE at the top of the ident arm (next to callee_local_at_entry, for the same scope-stability reason: argument lowering inside an arm can disturb the locals stack, so fresh lookups later are unreliable). It checks lookup_class / the local snapshot / lookup_func / lookup_imported_func / forward_class_names (a sibling class X declared later in the same function body). Every built-in constructor arm now backs off when the name is shadowed, so the construct falls through to the user-class/local-dispatch paths: Map, Set, Date, RegExp, the Symbol/BigInt/Math/JSON non-constructible rejection (runtime: throw TypeError for new Symbol and new BigInt #2883's missing shadow guard), Proxy, Number/String/Boolean boxed primitives, AggregateError, the Error family, WeakRef, FinalizationRegistry, Uint8Array, the other typed arrays, the bare-global MessageChannel/BroadcastChannel arm, and the existing Object/Function/URL/URLSearchParams/URLPattern/TextEncoder/TextDecoder guards are unified onto the same snapshot (they previously missed the forward-declared-class case).

  2. perry-hir/src/lower_types.rsinfer_type_from_expr's new C() arm typed new Map() as the builtin Generic { base: "Map" } by name, which routes the binding's later method calls (m.get(...)) down the collection intrinsic fast paths instead of the user methods. A user class in classes_index now wins (also gated url_encoding_constructor_type). Same guard added to the sibling inference chain in destructuring/var_decl/type_infer.rs (user-class arm moved ahead of the builtin-name arms).

  3. perry-hir/src/lower/lower_expr/arm_bin.rs — the x instanceof WeakRef|FinalizationRegistry compile-time fold answered from the weak-locals pre-scan even when a user class shadows the name; it now backs off (shadows_unqualified_global) so the generic instanceof path tests the user class id.

  4. perry-hir/src/lower/pre_scan.rs — the weak-locals pre-scan tagged const w = new WeakRef(...) as a native WeakRef instance by constructor name alone, so w.deref() dispatched to the intrinsic fast path even when WeakRef is a user class. The scan now collects shadowing declarations (class/function/var/import, same name-keyed scope-blind granularity as its existing poison mechanism) and skips tracking for shadowed names.

Testing

  • New regression test test-files/test_issue_6233_shadowed_global_class_new.ts: the exact issue repro plus the full blast-radius matrix (Symbol, BigInt, Proxy, WeakRef with a reserved deref method name, FinalizationRegistry, AggregateError, Map with reserved get/set method names, Set, Date, Number, String, Boolean, Error, TypeError, Uint8Array, Int32Array, RegExp with ctor args, and the previously-working Widget/Object/Array/Promise/Function controls). Matches node byte-for-byte.
  • 22-case standalone matrix (one file per shadowed name): 6/22 crashed and 10/22 constructed the wrong object on pristine main; 22/22 match node with this PR.
  • Unshadowed-builtin sanity file (native Map/Set/Date/WeakRef/RegExp/Uint8Array/boxed primitives/new Symbol() TypeError, iteration and .size/.exec fast paths) — byte-identical to node, so the guards don't disturb the normal native routing.
  • Targeted parity sweep (~160 existing map/set/date/error/proxy/weakref/regexp/symbol/url/typed-array/string/number tests) — no regressions vs pristine main.
  • cargo test -p perry-hir: 18/19 pass; the 1 failure (logical_property_assignment_short_circuits_the_store_4586) fails identically on pristine origin/main in a fresh worktree — pre-existing, unrelated to this change.
  • cargo fmt --all -- --check clean.

Not covered (separate defect noted in the issue): globalThis.Symbol("x") returning a typeof "object" value — that's the globalThis property read path, not new-expression lowering.

Fixes #6233.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed new resolution and constructor type inference when user-defined bindings shadow built-in constructors (including Symbol, BigInt, Proxy, WeakRef, FinalizationRegistry, Map, Set, Date, typed arrays, and errors).
    • Prevented built-in instanceof folding for WeakRef/FinalizationRegistry when the names are shadowed, ensuring correct runtime dispatch.
  • Tests

    • Added new Issue #6233 regression tests covering shadowed global classes and shadowed constructor-like functions/values.

…n `new` expressions (#6233)

A module-scope `class Symbol extends Base {}` legally shadows the global
Symbol, but the built-in constructor arms in lower_new fired by NAME
alone, so `new Symbol()` bound to the intrinsic and threw "Symbol is not
a constructor" at module init (effect's SchemaAST.ts declares AST node
classes named Symbol and BigInt — importing the effect barrel crashed at
startup under perry.compilePackages). Depending on the name this crashed
(Symbol/BigInt/Proxy/WeakRef/FinalizationRegistry/AggregateError) or
silently constructed the wrong object (Map/Set/Date/Number/String/
Boolean/Error/TypeError/Uint8Array/Int32Array: native instance, field
initializers never ran, instanceof the user class false). 16 of the
issue's 22 blast-radius shapes misbehaved on main.

Same family as #5912/#5913 (new URL) and #6003 (class Headers):
unconditional by-name native routing that ignores lexical shadowing.

- lower/expr_new.rs: snapshot `shadowed_by_user_binding` once at the
  ident-arm top (same scope-stability hazard as callee_local_at_entry:
  argument lowering inside an arm can disturb the locals stack) and gate
  every built-in constructor arm on it: Map/Set/Date/RegExp, the
  Symbol/BigInt/Math/JSON non-constructible rejection (#2883's missing
  shadow guard), Proxy, Number/String/Boolean boxed primitives,
  AggregateError, the Error family, WeakRef, FinalizationRegistry,
  Uint8Array, the other typed arrays, and the bare-global
  MessageChannel/BroadcastChannel arm. The existing Object/Function/URL/
  URLSearchParams/URLPattern/TextEncoder/TextDecoder guards are unified
  onto the same snapshot (adds the forward-declared sibling-class case
  they missed).
- lower_types.rs: infer_type_from_expr typed `new Map()` as the builtin
  Generic{base:"Map"} by name, which routes the binding's method calls
  down the collection intrinsic fast paths — SIGSEGV when the receiver
  is a user-class instance. A user class in classes_index now wins
  (also gated url_encoding_constructor_type). Mirrored in
  destructuring/var_decl/type_infer.rs (user-class arm moved ahead of
  the builtin-name arms).
- lower/lower_expr/arm_bin.rs: the `x instanceof WeakRef |
  FinalizationRegistry` compile-time fold answered from the weak-locals
  pre-scan even when the name is shadowed; it now backs off so the
  generic instanceof path tests the user class id.
- lower/pre_scan.rs: the weak-locals pre-scan tagged
  `const w = new WeakRef(x)` as a native weak instance by constructor
  name alone, so `w.deref()` dispatched to the intrinsic fast path;
  collect shadowing declarations (class/fn/var/import — same name-keyed
  scope-blind granularity as the existing poison set) and skip tracking
  shadowed names.

Regression test test_issue_6233_shadowed_global_class_new.ts covers the
issue repro plus the 22-name blast-radius matrix, reserved native method
names on shadowed classes (Map.get/set, WeakRef.deref), ctor args
through a shadowed RegExp, and instanceof for every case — matches node
byte-for-byte. Targeted parity sweep (~160 existing collection/error/
weak/url/typed-array tests) shows no regressions vs main.

Fixes #6233.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f752587-7908-47ef-a0e1-88970dde333a

📥 Commits

Reviewing files that changed from the base of the PR and between 67db1d9 and e86d232.

📒 Files selected for processing (9)
  • crates/perry-hir/src/destructuring/var_decl/alias_tracking.rs
  • crates/perry-hir/src/destructuring/var_decl/binding_guards.rs
  • crates/perry-hir/src/destructuring/var_decl/native_fetch.rs
  • crates/perry-hir/src/destructuring/var_decl/native_new.rs
  • crates/perry-hir/src/destructuring/var_decl/type_infer.rs
  • crates/perry-hir/src/lower_decl/block.rs
  • crates/perry-hir/src/lower_types.rs
  • crates/perry-hir/src/lower_types/extract.rs
  • test-files/test_issue_6233_shadowed_global_fn_new.ts
💤 Files with no reviewable changes (5)
  • crates/perry-hir/src/destructuring/var_decl/binding_guards.rs
  • crates/perry-hir/src/destructuring/var_decl/native_fetch.rs
  • crates/perry-hir/src/destructuring/var_decl/native_new.rs
  • crates/perry-hir/src/destructuring/var_decl/alias_tracking.rs
  • crates/perry-hir/src/lower_decl/block.rs

📝 Walkthrough

Walkthrough

User-defined classes and bindings that shadow built-in constructor names are respected across constructor lowering, type inference, intrinsic pre-scanning, instanceof handling, and regression tests. Type-extraction helpers were moved into a dedicated module and wildcard imports were removed.

Changes

Constructor shadowing

Layer / File(s) Summary
Shadow-aware type inference and extraction
crates/perry-hir/src/lower_types.rs, crates/perry-hir/src/lower_types/extract.rs, crates/perry-hir/src/destructuring/var_decl/type_infer.rs
User-defined classes take precedence over built-in constructor inference; type-extraction and decorator helpers are centralized in lower_types/extract.rs.
Module shadow tracking
crates/perry-hir/src/lower/pre_scan.rs
Module declarations and imports are collected so shadowed names are excluded from weak and proxy intrinsic classification.
Shadow-aware constructor lowering
crates/perry-hir/src/lower/expr_new.rs, crates/perry-hir/src/lower/lower_expr/arm_bin.rs
Built-in constructor fast paths and WeakRef/FinalizationRegistry instanceof folding now require unshadowed names.
Shadowing regression coverage
test-files/test_issue_6233_shadowed_global_class_new.ts, test-files/test_issue_6233_shadowed_global_fn_new.ts
Adds coverage for shadowed intrinsic classes and functions, instance checks, method dispatch, constructor arguments, and existing constructor names.
Explicit lower_types imports
crates/perry-hir/src/destructuring/var_decl/*, crates/perry-hir/src/lower_decl/block.rs
Removes unused wildcard imports from affected modules.

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

Possibly related issues

  • Issue 6233 — Directly tracks the constructor-shadowing behavior covered by these changes.

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: shadowed user classes losing to intrinsic constructors in new expressions.
Description check ✅ Passed The description is detailed and covers the problem, fix, related issue, and testing, even though it doesn't follow the template exactly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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/6233-shadowed-global-class

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/perry-hir/src/lower_types.rs (1)

553-572: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Hoist the shadow guard above the generic return

new Map<number>() on a user-defined class Map<T> still lowers to Type::Generic { base: "Map", .. }, so the downstream Map fast paths can misclassify it as the builtin. Move the classes_index check ahead of the type_args early-return, or handle shadowed classes in that branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-hir/src/lower_types.rs` around lines 553 - 572, In the
new-expression type lowering logic, user-defined classes must take precedence
over generic builtin handling. Update the logic around the type_args early
return and the classes_index shadow check so
classes_index.contains_key(name.as_str()) returns Type::Named(name) before
constructing Type::Generic, including for new Map<number>().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-hir/src/destructuring/var_decl/type_infer.rs`:
- Around line 98-112: Update the constructor handling around the class-name
lookup to apply the same shadows_unqualified_global guard used by the other
constructor paths. Ensure user-defined or imported Map, Set, URLSearchParams,
and typed-array names bypass the builtin inference arms, while unshadowed
globals continue inferring Type::Named or Type::Generic correctly.

---

Outside diff comments:
In `@crates/perry-hir/src/lower_types.rs`:
- Around line 553-572: In the new-expression type lowering logic, user-defined
classes must take precedence over generic builtin handling. Update the logic
around the type_args early return and the classes_index shadow check so
classes_index.contains_key(name.as_str()) returns Type::Named(name) before
constructing Type::Generic, including for new Map<number>().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c3b7d37e-16cf-431b-9681-f34812dbda10

📥 Commits

Reviewing files that changed from the base of the PR and between 77ea179 and 67db1d9.

📒 Files selected for processing (6)
  • crates/perry-hir/src/destructuring/var_decl/type_infer.rs
  • crates/perry-hir/src/lower/expr_new.rs
  • crates/perry-hir/src/lower/lower_expr/arm_bin.rs
  • crates/perry-hir/src/lower/pre_scan.rs
  • crates/perry-hir/src/lower_types.rs
  • test-files/test_issue_6233_shadowed_global_class_new.ts

Comment thread crates/perry-hir/src/destructuring/var_decl/type_infer.rs
…typing

- lint: lower_types.rs hit 2007 lines (2000-line file cap) after the
  #6233 guards. Split per the repo recipe: the TS-annotation extraction
  group (extract_ts_type & friends, decorator lowering) moves to
  lower_types/extract.rs as a pure re-exported submodule — named
  re-exports keep every existing call path compiling unchanged. The six
  `use crate::lower_types::*` globs that became redundant are removed
  or replaced with named imports (back to the exact pre-change warning
  count).
- CodeRabbit Major: the type-inference shadow guards only honored user
  CLASSES — a local, `function Map() {}`, or import shadowing a
  built-in name still got the built-in declared type, routing method
  calls on the constructed instance down the intrinsic fast paths.
  Both inference sites (infer_type_from_expr's New arm +
  url_encoding_constructor_type, and the var-decl chain in
  destructuring/var_decl/type_infer.rs) now mirror the
  construction-side predicate (shadows_unqualified_global), scoped to
  a conservative builtin_constructor_inference_name set so
  module-export names that legitimately arrive through local bindings
  (Buffer, the stream and event classes) keep their arms.
- CodeRabbit Minor (outside-diff): `new Map<number>()` on a generic
  user `class Map<T>` took the explicit-type-args early return and
  still produced `Generic { base: "Map" }` — exactly what the
  collection fast-path recognizers key on. The user-class check is
  hoisted above that early return; built-in-colliding user classes
  stay `Named` even with explicit type args (both inference sites).
- New regression test test_issue_6233_shadowed_global_fn_new.ts:
  function/const constructors named Map / Set / Uint8Array /
  URLSearchParams whose instances carry intrinsic-colliding members
  (get/set/has/size/length) — matches node byte-for-byte.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant