diff --git a/crates/perry-hir/src/destructuring/var_decl/alias_tracking.rs b/crates/perry-hir/src/destructuring/var_decl/alias_tracking.rs index 648eb698b8..3457423e3c 100644 --- a/crates/perry-hir/src/destructuring/var_decl/alias_tracking.rs +++ b/crates/perry-hir/src/destructuring/var_decl/alias_tracking.rs @@ -10,7 +10,6 @@ use swc_ecma_ast as ast; use crate::ir::*; use crate::lower::{lower_expr, LoweringContext}; use crate::lower_patterns::*; -use crate::lower_types::*; use crate::destructuring::var_decl_sources::*; diff --git a/crates/perry-hir/src/destructuring/var_decl/binding_guards.rs b/crates/perry-hir/src/destructuring/var_decl/binding_guards.rs index 112ff83ea6..b3f6517633 100644 --- a/crates/perry-hir/src/destructuring/var_decl/binding_guards.rs +++ b/crates/perry-hir/src/destructuring/var_decl/binding_guards.rs @@ -11,7 +11,6 @@ use swc_ecma_ast as ast; use crate::ir::*; use crate::lower::{lower_expr, LoweringContext}; use crate::lower_patterns::*; -use crate::lower_types::*; use crate::destructuring::var_decl_sources::*; diff --git a/crates/perry-hir/src/destructuring/var_decl/native_fetch.rs b/crates/perry-hir/src/destructuring/var_decl/native_fetch.rs index 8f84aa3f6d..9dee97c2f6 100644 --- a/crates/perry-hir/src/destructuring/var_decl/native_fetch.rs +++ b/crates/perry-hir/src/destructuring/var_decl/native_fetch.rs @@ -11,7 +11,6 @@ use swc_ecma_ast as ast; use crate::ir::*; use crate::lower::{lower_expr, LoweringContext}; use crate::lower_patterns::*; -use crate::lower_types::*; use crate::destructuring::helpers::{get_fetch_module, is_member_fetch_call}; use crate::destructuring::var_decl_sources::*; diff --git a/crates/perry-hir/src/destructuring/var_decl/native_new.rs b/crates/perry-hir/src/destructuring/var_decl/native_new.rs index 2fe636045b..115514a10b 100644 --- a/crates/perry-hir/src/destructuring/var_decl/native_new.rs +++ b/crates/perry-hir/src/destructuring/var_decl/native_new.rs @@ -11,7 +11,6 @@ use swc_ecma_ast as ast; use crate::ir::*; use crate::lower::{lower_expr, LoweringContext}; use crate::lower_patterns::*; -use crate::lower_types::*; use crate::destructuring::var_decl_sources::*; diff --git a/crates/perry-hir/src/destructuring/var_decl/type_infer.rs b/crates/perry-hir/src/destructuring/var_decl/type_infer.rs index 5ea7525379..1e65889c52 100644 --- a/crates/perry-hir/src/destructuring/var_decl/type_infer.rs +++ b/crates/perry-hir/src/destructuring/var_decl/type_infer.rs @@ -11,7 +11,7 @@ use swc_ecma_ast as ast; use crate::ir::*; use crate::lower::{lower_expr, LoweringContext}; use crate::lower_patterns::*; -use crate::lower_types::*; +use crate::lower_types::{extract_ts_type, infer_type_from_expr}; use crate::destructuring::var_decl_sources::*; @@ -90,7 +90,46 @@ pub(crate) fn infer_decl_type( if let ast::Expr::New(new_expr) = init_expr.as_ref() { if let ast::Expr::Ident(class_ident) = new_expr.callee.as_ref() { let class_name = class_ident.sym.as_ref(); - if class_name == "Set" || class_name == "Map" { + // #6233: a USER class of this name lexically shadows the + // same-named built-in (`class Map { get(){…} }` … `const m + // = new Map()`), so the user-class arm must win — typing + // the binding as the built-in would route its method calls + // (`m.get(...)`) to the native intrinsic fast paths. For a + // built-in-colliding name the type stays `Named` even with + // explicit type args: `Generic { base: "Map", .. }` is + // exactly what the collection fast-path recognizers key + // on, so a generic user `class Map` must not produce + // it. + if ctx.classes_index.contains_key(class_name) { + let type_args: Vec = new_expr + .type_args + .as_ref() + .map(|ta| ta.params.iter().map(|t| extract_ts_type(t)).collect()) + .unwrap_or_default(); + if type_args.is_empty() + || crate::lower_types::builtin_constructor_inference_name(class_name) + { + ty = Type::Named(class_name.to_string()); + } else { + ty = Type::Generic { + base: class_name.to_string(), + type_args, + }; + } + } else if crate::lower_types::builtin_constructor_inference_name(class_name) + && ctx.shadows_unqualified_global(class_name) + { + // #6233 (review follow-up): a NON-class user binding — + // a local, `function Map() {}`, an import — also + // shadows the built-in (mirrors the construction-side + // guard in expr_new.rs). Leave `ty` as inferred (Any) + // so none of the builtin arms below claims the binding + // for an intrinsic fast path. Scoped to the same + // conservative name set as the infer_type_from_expr + // guard: module-export names that legitimately arrive + // through local bindings (`Buffer`, the stream and + // event classes) keep their arms. + } else if class_name == "Set" || class_name == "Map" { // Extract type arguments from new Set() or new Map() let type_args: Vec = new_expr .type_args @@ -131,21 +170,6 @@ pub(crate) fn infer_decl_type( | "Float64Array" ) { ty = Type::Named(class_name.to_string()); - } else if ctx.classes_index.contains_key(class_name) { - // User-defined class: infer type from new ClassName(...) - let type_args: Vec = new_expr - .type_args - .as_ref() - .map(|ta| ta.params.iter().map(|t| extract_ts_type(t)).collect()) - .unwrap_or_default(); - if type_args.is_empty() { - ty = Type::Named(class_name.to_string()); - } else { - ty = Type::Generic { - base: class_name.to_string(), - type_args, - }; - } } } } diff --git a/crates/perry-hir/src/lower/expr_new.rs b/crates/perry-hir/src/lower/expr_new.rs index 0a98ea01cb..38931ddaf6 100644 --- a/crates/perry-hir/src/lower/expr_new.rs +++ b/crates/perry-hir/src/lower/expr_new.rs @@ -186,6 +186,26 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R } else { None }; + // #6233: a user-declared binding — `class Symbol extends Base {}`, + // a local/param, a `function` declaration, or an imported binding — + // lexically shadows the same-named global for every reference in + // scope, `new` expressions included. Every built-in constructor arm + // below (Map/Set/Date/RegExp, the Symbol/BigInt/Math/JSON + // non-constructible rejection, Proxy, the boxed primitives, the + // Error family, WeakRef, FinalizationRegistry, AggregateError, + // typed arrays, …) must back off when the name is shadowed so the + // construct falls through to the user-class / local-dispatch paths + // at the bottom. Snapshotted ONCE here, next to + // `callee_local_at_entry`, for the same reason it is: argument + // lowering inside an arm can disturb the locals scope stack, so a + // fresh lookup later is unreliable. `forward_class_names` covers a + // sibling `class X` declared later in the same function body + // (pre-registered by the Phase-1.5 scan but not yet lowered). + let shadowed_by_user_binding = ctx.lookup_class(&class_name).is_some() + || callee_local_at_entry.is_some() + || ctx.lookup_func(&class_name).is_some() + || ctx.lookup_imported_func(&class_name).is_some() + || ctx.forward_class_names.contains(class_name.as_str()); if matches!( ctx.lookup_native_module(&class_name), Some(("url", Some("Url"))) @@ -250,9 +270,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // runtime globals delegate to the registered worker_threads // factories when the stdlib is present, so ports stay fully // functional in graphs that have it. - if is_worker_messaging_constructor_name(&class_name) - && ctx.lookup_local(&class_name).is_none() - { + if is_worker_messaging_constructor_name(&class_name) && !shadowed_by_user_binding { return Ok(Expr::New { class_name: class_name.to_string(), args: lower_optional_args(ctx, new_expr.args.as_deref())?, @@ -452,11 +470,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // runtime-unknown bucket with a precise diagnostic; log the // const/known-codegen buckets and fall through to the existing // placeholder lowering. - if class_name == "Function" - && ctx.lookup_local("Function").is_none() - && ctx.lookup_func("Function").is_none() - && ctx.lookup_class("Function").is_none() - { + if class_name == "Function" && !shadowed_by_user_binding { let args_slice = new_expr.args.as_deref().unwrap_or(&[]); if let Some(folded) = super::const_fold_fn::try_const_fold_function_construct( ctx, @@ -529,11 +543,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R } // Handle built-in types - if class_name == "Object" - && ctx.lookup_local("Object").is_none() - && ctx.lookup_func("Object").is_none() - && ctx.lookup_class("Object").is_none() - { + if class_name == "Object" && !shadowed_by_user_binding { let mut args = new_expr .args .as_ref() @@ -547,7 +557,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R let arg = args.drain(..).next().unwrap_or(Expr::Undefined); return Ok(Expr::ObjectCoerce(Box::new(arg))); } - if class_name == "Map" { + if class_name == "Map" && !shadowed_by_user_binding { // new Map() or new Map(entries) let args = new_expr .args @@ -567,7 +577,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R ))); } } - if class_name == "Set" { + if class_name == "Set" && !shadowed_by_user_binding { // new Set() or new Set(iterable) let args = new_expr .args @@ -587,7 +597,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R ))); } } - if class_name == "Date" { + if class_name == "Date" && !shadowed_by_user_binding { // new Date() / new Date(ts) / new Date(year, month, day, h?, m?, s?, ms?). // The multi-arg form is what dayjs's parseDate uses // (`new Date(d[1], m, d[3] || 1, ...)`) — without it the @@ -607,7 +617,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R .unwrap_or_default(); return Ok(Expr::DateNew(args)); } - if class_name == "RegExp" { + if class_name == "RegExp" && !shadowed_by_user_binding { // new RegExp(pattern[, flags]) — for string-literal args, // route to the same `Expr::RegExp { pattern, flags }` // variant the literal `/foo/g` syntax produces. The @@ -684,7 +694,9 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R } } } - if matches!(class_name.as_str(), "Symbol" | "BigInt" | "Math" | "JSON") { + if matches!(class_name.as_str(), "Symbol" | "BigInt" | "Math" | "JSON") + && !shadowed_by_user_binding + { let args = new_expr .args .as_ref() @@ -697,7 +709,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R .unwrap_or_default(); return Ok(nonconstructable_builtin_throw_expr(&class_name, args)); } - if class_name == "Proxy" { + if class_name == "Proxy" && !shadowed_by_user_binding { let args = new_expr .args .as_ref() @@ -716,7 +728,9 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R handler: Box::new(handler), }); } - if matches!(class_name.as_str(), "Number" | "String" | "Boolean") { + if matches!(class_name.as_str(), "Number" | "String" | "Boolean") + && !shadowed_by_user_binding + { let mut args = new_expr .args .as_ref() @@ -793,7 +807,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // throws TypeError on a missing/non-iterable argument — so a // missing first arg defaults to `undefined`, NOT an empty array. // #2836: the third `options` argument carries `{ cause }`. - if class_name == "AggregateError" { + if class_name == "AggregateError" && !shadowed_by_user_binding { let args = new_expr .args .as_ref() @@ -816,12 +830,13 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R } // Handle Error and its subclasses - if class_name == "Error" + if (class_name == "Error" || class_name == "TypeError" || class_name == "RangeError" || class_name == "ReferenceError" || class_name == "SyntaxError" - || class_name == "BugIndicatingError" + || class_name == "BugIndicatingError") + && !shadowed_by_user_binding { // new Error() / new Error(message) / new Error(message, { cause }) // @@ -949,29 +964,17 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R } } - // Handle URL class. #5912: gated on `callee_local_at_entry` / - // `lookup_func` / `lookup_imported_func` so a local function/ - // const/imported-binding shadowing the global name (e.g. a - // vendored `function URL(url?) {...}` polyfill, or `import { - // URL } from "./my-url-polyfill"`) routes through the generic - // local-dispatch fallback below instead of always binding to - // perry's native WHATWG URL constructor — matches the - // `lookup_local`/`lookup_func`/`lookup_class` shadowing guard - // used for `Function`/`Object` above (a named function - // declaration is tracked via `lookup_func`, not - // `lookup_local`/`callee_local_at_entry`). Deliberately doesn't - // use the `shadows_unqualified_global` one-liner here: that - // helper's `lookup_local` call is a FRESH scope lookup, but - // `callee_local_at_entry` must stay a pre-captured snapshot (see - // the comment above its definition) — the Error-type branch - // just above already lowers `new_expr.args`, which can disturb - // the locals scope stack before we get here. - if class_name == "URL" - && callee_local_at_entry.is_none() - && ctx.lookup_func(&class_name).is_none() - && ctx.lookup_imported_func(&class_name).is_none() - && ctx.lookup_class(&class_name).is_none() - { + // Handle URL class. #5912: gated so a local function/const/ + // imported-binding shadowing the global name (e.g. a vendored + // `function URL(url?) {...}` polyfill, or `import { URL } from + // "./my-url-polyfill"`) routes through the generic local-dispatch + // fallback below instead of always binding to perry's native + // WHATWG URL constructor. Uses the `shadowed_by_user_binding` + // snapshot (NOT fresh `shadows_unqualified_global` lookups): + // earlier arms lower `new_expr.args`, which can disturb the + // locals scope stack before we get here (see the comment above + // `callee_local_at_entry`). + if class_name == "URL" && !shadowed_by_user_binding { return Ok( lower_url_encoding_constructor(ctx, "URL", new_expr.args.as_deref())?.unwrap(), ); @@ -979,10 +982,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // Handle URLSearchParams / URLPattern classes if matches!(class_name.as_str(), "URLSearchParams" | "URLPattern") - && callee_local_at_entry.is_none() - && ctx.lookup_func(&class_name).is_none() - && ctx.lookup_imported_func(&class_name).is_none() - && ctx.lookup_class(&class_name).is_none() + && !shadowed_by_user_binding { return Ok(lower_url_encoding_constructor( ctx, @@ -994,7 +994,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // Handle WeakRef class — wraps a value (object) in a weak reference object. // Pragmatic implementation: stores a strong reference and `deref()` always returns it. - if class_name == "WeakRef" { + if class_name == "WeakRef" && !shadowed_by_user_binding { let args = new_expr .args .as_ref() @@ -1012,7 +1012,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // Handle FinalizationRegistry class — registers cleanup callbacks invoked when // tracked targets are GC'd. Pragmatic implementation: stores registrations but // never fires the callback (Perry's GC doesn't track weak references yet). - if class_name == "FinalizationRegistry" { + if class_name == "FinalizationRegistry" && !shadowed_by_user_binding { let args = new_expr .args .as_ref() @@ -1027,12 +1027,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R return Ok(Expr::FinalizationRegistryNew(Box::new(cb))); } // Handle TextEncoder constructor - if class_name == "TextEncoder" - && callee_local_at_entry.is_none() - && ctx.lookup_func(&class_name).is_none() - && ctx.lookup_imported_func(&class_name).is_none() - && ctx.lookup_class(&class_name).is_none() - { + if class_name == "TextEncoder" && !shadowed_by_user_binding { return Ok(lower_url_encoding_constructor( ctx, "TextEncoder", @@ -1041,12 +1036,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R .unwrap()); } // Handle TextDecoder constructor: new TextDecoder(label?, opts?) - if class_name == "TextDecoder" - && callee_local_at_entry.is_none() - && ctx.lookup_func(&class_name).is_none() - && ctx.lookup_imported_func(&class_name).is_none() - && ctx.lookup_class(&class_name).is_none() - { + if class_name == "TextDecoder" && !shadowed_by_user_binding { return Ok(lower_url_encoding_constructor( ctx, "TextDecoder", @@ -1056,7 +1046,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R } // Handle Uint8Array constructor - if class_name == "Uint8Array" { + if class_name == "Uint8Array" && !shadowed_by_user_binding { // new Uint8Array() or new Uint8Array(length) or new Uint8Array(array) let args = new_expr .args @@ -1082,7 +1072,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // Handle other typed-array constructors (Int8/16/32, Uint16/32, Float32/64, // Uint8ClampedArray). Uint8Array stays on the Buffer path above. if let Some(kind) = crate::ir::typed_array_kind_for_name(class_name.as_str()) { - if class_name != "Uint8Array" { + if class_name != "Uint8Array" && !shadowed_by_user_binding { let args = new_expr .args .as_ref() diff --git a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs index 45be50bd46..ab7629655d 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs @@ -48,7 +48,13 @@ pub(crate) fn lower_bin_expr(ctx: &mut LoweringContext, bin: &ast::BinExpr) -> R // lowering time when we recognise the receiver. if let ast::Expr::Ident(class_ident) = bin.right.as_ref() { let class_name = class_ident.sym.as_ref(); - if class_name == "WeakRef" || class_name == "FinalizationRegistry" { + // #6233: only fold when the RHS really is the GLOBAL WeakRef / + // FinalizationRegistry — a user `class WeakRef {}` (or a local/ + // function/import of that name) shadows the global, and its + // instances must take the generic instanceof path below. + if (class_name == "WeakRef" || class_name == "FinalizationRegistry") + && !ctx.shadows_unqualified_global(class_name) + { if let ast::Expr::Ident(left_ident) = bin.left.as_ref() { let local_name = left_ident.sym.to_string(); let is_match = (class_name == "WeakRef" diff --git a/crates/perry-hir/src/lower/pre_scan.rs b/crates/perry-hir/src/lower/pre_scan.rs index a2d8cef9b9..87dff64759 100644 --- a/crates/perry-hir/src/lower/pre_scan.rs +++ b/crates/perry-hir/src/lower/pre_scan.rs @@ -19,9 +19,18 @@ use crate::ir::*; /// without requiring type inference (Perry's existing var-decl type inference /// doesn't extend to WeakRef/FinalizationRegistry). pub(crate) fn pre_scan_weakref_locals(ast_module: &ast::Module, ctx: &mut LoweringContext) { - fn classify_new(new_expr: &ast::NewExpr) -> Option<&'static str> { + fn classify_new(new_expr: &ast::NewExpr, shadowed: &HashSet) -> Option<&'static str> { if let ast::Expr::Ident(ident) = new_expr.callee.as_ref() { - match ident.sym.as_ref() { + let name = ident.sym.as_ref(); + // #6233: a user declaration of this name (`class Proxy {}`, + // `function WeakRef() {}`, an import, …) shadows the global, so + // `new ()` constructs the USER binding — don't track the + // result as a weak/proxy intrinsic instance. Name-keyed and + // scope-blind, matching this scan's own granularity. + if shadowed.contains(name) { + return None; + } + match name { "WeakRef" => Some("WeakRef"), "FinalizationRegistry" => Some("FinalizationRegistry"), "WeakMap" => Some("WeakMap"), @@ -61,12 +70,13 @@ pub(crate) fn pre_scan_weakref_locals(ast_module: &ast::Module, ctx: &mut Loweri decl: &ast::VarDeclarator, ctx: &mut LoweringContext, poison: &mut HashSet, + shadowed: &HashSet, ) { if let (ast::Pat::Ident(ident), Some(init)) = (&decl.name, decl.init.as_ref()) { let init_unwrapped = unwrap_init(init); if let ast::Expr::New(new_expr) = init_unwrapped { let name = ident.id.sym.to_string(); - match classify_new(new_expr) { + match classify_new(new_expr, shadowed) { Some("WeakRef") => { ctx.weakref_locals.insert(name); } @@ -123,16 +133,21 @@ pub(crate) fn pre_scan_weakref_locals(ast_module: &ast::Module, ctx: &mut Loweri } } } - fn walk_stmt(stmt: &ast::Stmt, ctx: &mut LoweringContext, poison: &mut HashSet) { + fn walk_stmt( + stmt: &ast::Stmt, + ctx: &mut LoweringContext, + poison: &mut HashSet, + shadowed: &HashSet, + ) { match stmt { ast::Stmt::Decl(ast::Decl::Var(var_decl)) => { for decl in &var_decl.decls { - record_var(decl, ctx, poison); + record_var(decl, ctx, poison, shadowed); } } ast::Stmt::Decl(ast::Decl::Using(using_decl)) => { for decl in &using_decl.decls { - record_var(decl, ctx, poison); + record_var(decl, ctx, poison, shadowed); } } // Function declarations — descend into the body so `const @@ -142,66 +157,173 @@ pub(crate) fn pre_scan_weakref_locals(ast_module: &ast::Module, ctx: &mut Loweri ast::Stmt::Decl(ast::Decl::Fn(fn_decl)) => { if let Some(body) = &fn_decl.function.body { for s in &body.stmts { - walk_stmt(s, ctx, poison); + walk_stmt(s, ctx, poison, shadowed); } } } ast::Stmt::Block(block) => { for s in &block.stmts { - walk_stmt(s, ctx, poison); + walk_stmt(s, ctx, poison, shadowed); } } ast::Stmt::If(if_stmt) => { - walk_stmt(&if_stmt.cons, ctx, poison); + walk_stmt(&if_stmt.cons, ctx, poison, shadowed); if let Some(alt) = &if_stmt.alt { - walk_stmt(alt, ctx, poison); + walk_stmt(alt, ctx, poison, shadowed); } } - ast::Stmt::While(w) => walk_stmt(&w.body, ctx, poison), - ast::Stmt::DoWhile(w) => walk_stmt(&w.body, ctx, poison), + ast::Stmt::While(w) => walk_stmt(&w.body, ctx, poison, shadowed), + ast::Stmt::DoWhile(w) => walk_stmt(&w.body, ctx, poison, shadowed), ast::Stmt::For(f) => { if let Some(ast::VarDeclOrExpr::VarDecl(vd)) = &f.init { for decl in &vd.decls { - record_var(decl, ctx, poison); + record_var(decl, ctx, poison, shadowed); } } - walk_stmt(&f.body, ctx, poison); + walk_stmt(&f.body, ctx, poison, shadowed); } - ast::Stmt::ForIn(f) => walk_stmt(&f.body, ctx, poison), - ast::Stmt::ForOf(f) => walk_stmt(&f.body, ctx, poison), + ast::Stmt::ForIn(f) => walk_stmt(&f.body, ctx, poison, shadowed), + ast::Stmt::ForOf(f) => walk_stmt(&f.body, ctx, poison, shadowed), ast::Stmt::Try(t) => { for s in &t.block.stmts { - walk_stmt(s, ctx, poison); + walk_stmt(s, ctx, poison, shadowed); } if let Some(catch) = &t.handler { for s in &catch.body.stmts { - walk_stmt(s, ctx, poison); + walk_stmt(s, ctx, poison, shadowed); } } if let Some(finalizer) = &t.finalizer { for s in &finalizer.stmts { - walk_stmt(s, ctx, poison); + walk_stmt(s, ctx, poison, shadowed); } } } ast::Stmt::Switch(s) => { for case in &s.cases { for s in &case.cons { - walk_stmt(s, ctx, poison); + walk_stmt(s, ctx, poison, shadowed); } } } _ => {} } } + // #6233: collect user declarations that shadow one of the tracked + // constructor names — `class Proxy {}` / `function WeakRef() {}` / + // `const WeakMap = …` / `import { WeakSet } from …`. This scan is + // name-keyed with no scope discrimination (see the poison-set comment + // below), so a shadow anywhere in the module suppresses tracking + // module-wide and the affected locals fall back to ordinary dispatch. + fn record_shadow(name: &str, shadowed: &mut HashSet) { + if matches!( + name, + "WeakRef" | "FinalizationRegistry" | "WeakMap" | "WeakSet" | "Proxy" + ) { + shadowed.insert(name.to_string()); + } + } + fn collect_shadowing_decls(stmt: &ast::Stmt, shadowed: &mut HashSet) { + match stmt { + ast::Stmt::Decl(ast::Decl::Class(cd)) => record_shadow(cd.ident.sym.as_ref(), shadowed), + ast::Stmt::Decl(ast::Decl::Fn(fd)) => { + record_shadow(fd.ident.sym.as_ref(), shadowed); + if let Some(body) = &fd.function.body { + for s in &body.stmts { + collect_shadowing_decls(s, shadowed); + } + } + } + ast::Stmt::Decl(ast::Decl::Var(vd)) => { + for d in &vd.decls { + if let ast::Pat::Ident(ident) = &d.name { + // `const Proxy = …` — the declared NAME shadows; a + // `new Proxy()` bound to an ordinary local is handled + // by classify_new/poison, not here. + record_shadow(ident.id.sym.as_ref(), shadowed); + } + } + } + ast::Stmt::Block(block) => { + for s in &block.stmts { + collect_shadowing_decls(s, shadowed); + } + } + ast::Stmt::If(if_stmt) => { + collect_shadowing_decls(&if_stmt.cons, shadowed); + if let Some(alt) = &if_stmt.alt { + collect_shadowing_decls(alt, shadowed); + } + } + ast::Stmt::While(w) => collect_shadowing_decls(&w.body, shadowed), + ast::Stmt::DoWhile(w) => collect_shadowing_decls(&w.body, shadowed), + ast::Stmt::For(f) => collect_shadowing_decls(&f.body, shadowed), + ast::Stmt::ForIn(f) => collect_shadowing_decls(&f.body, shadowed), + ast::Stmt::ForOf(f) => collect_shadowing_decls(&f.body, shadowed), + ast::Stmt::Try(t) => { + for s in &t.block.stmts { + collect_shadowing_decls(s, shadowed); + } + if let Some(catch) = &t.handler { + for s in &catch.body.stmts { + collect_shadowing_decls(s, shadowed); + } + } + if let Some(finalizer) = &t.finalizer { + for s in &finalizer.stmts { + collect_shadowing_decls(s, shadowed); + } + } + } + ast::Stmt::Switch(s) => { + for case in &s.cases { + for s in &case.cons { + collect_shadowing_decls(s, shadowed); + } + } + } + _ => {} + } + } + let mut shadowed: HashSet = HashSet::new(); + for item in &ast_module.body { + match item { + ast::ModuleItem::Stmt(stmt) => collect_shadowing_decls(stmt, &mut shadowed), + ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportDecl(export_decl)) => { + match &export_decl.decl { + ast::Decl::Class(cd) => record_shadow(cd.ident.sym.as_ref(), &mut shadowed), + ast::Decl::Fn(fd) => record_shadow(fd.ident.sym.as_ref(), &mut shadowed), + ast::Decl::Var(vd) => { + for d in &vd.decls { + if let ast::Pat::Ident(ident) = &d.name { + record_shadow(ident.id.sym.as_ref(), &mut shadowed); + } + } + } + _ => {} + } + } + ast::ModuleItem::ModuleDecl(ast::ModuleDecl::Import(import_decl)) => { + for spec in &import_decl.specifiers { + let local = match spec { + ast::ImportSpecifier::Named(named) => named.local.sym.as_ref(), + ast::ImportSpecifier::Default(default) => default.local.sym.as_ref(), + ast::ImportSpecifier::Namespace(ns) => ns.local.sym.as_ref(), + }; + record_shadow(local, &mut shadowed); + } + } + _ => {} + } + } let mut poison: HashSet = HashSet::new(); for item in &ast_module.body { match item { - ast::ModuleItem::Stmt(stmt) => walk_stmt(stmt, ctx, &mut poison), + ast::ModuleItem::Stmt(stmt) => walk_stmt(stmt, ctx, &mut poison, &shadowed), ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportDecl(export_decl)) => { if let ast::Decl::Var(var_decl) = &export_decl.decl { for decl in &var_decl.decls { - record_var(decl, ctx, &mut poison); + record_var(decl, ctx, &mut poison, &shadowed); } } } diff --git a/crates/perry-hir/src/lower_decl/block.rs b/crates/perry-hir/src/lower_decl/block.rs index 94bd11470e..2c3c35f351 100644 --- a/crates/perry-hir/src/lower_decl/block.rs +++ b/crates/perry-hir/src/lower_decl/block.rs @@ -9,7 +9,6 @@ use crate::lower::{ collect_for_of_pattern_leaves, emit_for_of_pattern_binding, lower_expr, LoweringContext, }; use crate::lower_patterns::*; -use crate::lower_types::*; use super::*; diff --git a/crates/perry-hir/src/lower_types.rs b/crates/perry-hir/src/lower_types.rs index 0838cc2b96..2e3a7770ec 100644 --- a/crates/perry-hir/src/lower_types.rs +++ b/crates/perry-hir/src/lower_types.rs @@ -7,8 +7,8 @@ use perry_types::{Type, TypeParam}; use swc_ecma_ast as ast; use crate::ir::*; -use crate::lower::{lower_expr, LoweringContext}; -use crate::lower_patterns::{get_pat_name, lower_lit}; +use crate::lower::LoweringContext; +use crate::lower_patterns::get_pat_name; pub(crate) const FILEHANDLE_READLINES_ITERATOR_TYPE: &str = "__PerryFileHandleReadLinesIterator"; @@ -191,6 +191,33 @@ fn infer_native_arena_call_return_type( } } +/// #6233: built-in constructor names whose INFERRED declared type drives a +/// name-keyed fast path downstream (`is_map_expr` / `is_set_expr` / +/// `is_url_search_params_expr`, the typed-array and collection dispatch). +/// When a user binding shadows one of these, the inference sites must not +/// hand the binding the built-in type. Deliberately excludes node-module +/// export names that commonly arrive through legitimate local bindings +/// (`Buffer`, `Readable`/`Writable`/…, `EventTarget`, …) — for those a local +/// binding usually IS the built-in, resolved by import source rather than by +/// this predicate. +pub(crate) fn builtin_constructor_inference_name(name: &str) -> bool { + matches!( + name, + "Map" + | "WeakMap" + | "Set" + | "WeakSet" + | "Array" + | "Promise" + | "URL" + | "URLSearchParams" + | "URLPattern" + | "TextEncoder" + | "TextDecoder" + | "Uint8Array" + ) || typed_array_name_for_name(name).is_some() +} + fn url_encoding_constructor_type(ctx: &LoweringContext, callee: &ast::Expr) -> Option { fn class_type(name: &str) -> Option { match name { @@ -215,6 +242,12 @@ fn url_encoding_constructor_type(ctx: &LoweringContext, callee: &ast::Expr) -> O match callee { ast::Expr::Ident(ident) => { let name = ident.sym.as_ref(); + // #6233: any user binding of this name — a class, a local, a + // `function URL() {}`, an import — shadows the built-in; let the + // generic inference in the caller type it instead. + if ctx.shadows_unqualified_global(name) { + return None; + } if let Some(ty) = class_type(name) { return Some(ty); } @@ -545,6 +578,23 @@ fn infer_type_from_expr_inner(expr: &ast::Expr, ctx: &LoweringContext) -> Type { } if let ast::Expr::Ident(ident) = new_expr.callee.as_ref() { let name = ident.sym.to_string(); + // #6233: a user binding lexically shadowing a built-in + // constructor name owns `new ()`. A user CLASS resolves + // to its class type — checked BEFORE the explicit-type-args + // early return below, because `Generic { base: "Map", .. }` is + // exactly what the Map/Set/collection fast-path recognizers + // key on, so a generic user `class Map` must not produce + // it. Any OTHER shadowing binding (a local, `function Map() + // {}`, an import) constructs an unknown instance — return + // `Any` so no declared-type-keyed fast path claims it. + if builtin_constructor_inference_name(&name) { + if ctx.classes_index.contains_key(name.as_str()) { + return Type::Named(name); + } + if ctx.shadows_unqualified_global(&name) { + return Type::Any; + } + } if let Some(type_args) = new_expr.type_args.as_ref() { if !type_args.params.is_empty() { let args: Vec = type_args @@ -559,6 +609,12 @@ fn infer_type_from_expr_inner(expr: &ast::Expr, ctx: &LoweringContext) -> Type { } } match name.as_str() { + // #6233: a user class of this name lexically shadows the + // same-named built-in, so `new Map()` constructs the USER + // class — typing it as the builtin Generic would send its + // method calls (`m.get(...)`) down the collection fast + // paths. `classes_index` holds only user classes. + _ if ctx.classes_index.contains_key(name.as_str()) => Type::Named(name), // Issue #533: walk the entries arg of `new Map([...])` / // `new WeakMap([...])` so K/V are populated when no explicit // is given. Without this, downstream `m.get(k)` @@ -1382,615 +1438,10 @@ pub(crate) fn infer_call_return_type(callee: &ast::Expr, ctx: &LoweringContext) } } -/// Extract type parameters from SWC's TsTypeParamDecl -pub(crate) fn extract_type_params(decl: &ast::TsTypeParamDecl) -> Vec { - decl.params - .iter() - .map(|p| { - let name = p.name.sym.to_string(); - let constraint = p.constraint.as_ref().map(|c| Box::new(extract_ts_type(c))); - let default = p.default.as_ref().map(|d| Box::new(extract_ts_type(d))); - TypeParam { - name, - constraint, - default, - } - }) - .collect() -} - -/// Extract a Type from an SWC TypeScript type annotation -/// This version doesn't have access to type parameter context -pub(crate) fn extract_ts_type(ts_type: &ast::TsType) -> Type { - extract_ts_type_with_ctx(ts_type, None) -} - -/// Extract a Type from an SWC TypeScript type annotation with type parameter context -pub(crate) fn extract_ts_type_with_ctx( - ts_type: &ast::TsType, - ctx: Option<&LoweringContext>, -) -> Type { - use ast::TsKeywordTypeKind::*; - use ast::TsType::*; - - match ts_type { - // Keyword types (primitives) - TsKeywordType(kw) => match kw.kind { - TsNumberKeyword => Type::Number, - TsStringKeyword => Type::String, - TsBooleanKeyword => Type::Boolean, - TsBigIntKeyword => Type::BigInt, - TsVoidKeyword => Type::Void, - TsNullKeyword => Type::Null, - TsUndefinedKeyword => Type::Void, - TsAnyKeyword => Type::Any, - TsUnknownKeyword => Type::Unknown, - TsNeverKeyword => Type::Never, - TsSymbolKeyword => Type::Symbol, - TsObjectKeyword => Type::Any, // Generic object - TsIntrinsicKeyword => Type::Any, - }, - - // Array type: T[] - TsArrayType(arr) => { - let elem_type = extract_ts_type_with_ctx(&arr.elem_type, ctx); - Type::Array(Box::new(elem_type)) - } - - // Tuple type: [T, U, V] - TsTupleType(tuple) => { - let elem_types: Vec = tuple - .elem_types - .iter() - .map(|elem| extract_ts_type_with_ctx(&elem.ty, ctx)) - .collect(); - Type::Tuple(elem_types) - } - - // Union type: A | B | C - TsUnionOrIntersectionType(union_or_inter) => { - match union_or_inter { - ast::TsUnionOrIntersectionType::TsUnionType(union) => { - let types: Vec = union - .types - .iter() - .map(|t| extract_ts_type_with_ctx(t, ctx)) - .collect(); - Type::Union(types) - } - ast::TsUnionOrIntersectionType::TsIntersectionType(_) => { - // Intersection types are complex - treat as Any for now - Type::Any - } - } - } - - // Type reference: Array, MyClass, T (type param), etc. - TsTypeRef(type_ref) => { - let name = match &type_ref.type_name { - ast::TsEntityName::Ident(ident) => ident.sym.to_string(), - ast::TsEntityName::TsQualifiedName(qname) => { - // Qualified names like Foo.Bar - format!("{}.{}", get_ts_entity_name(&qname.left), qname.right.sym) - } - }; - - // First check if this is a type parameter reference (like T, K, V). - // - // When the parameter has a runtime-meaningful upper-bound - // constraint (``, ``, - // `` …) substitute the constraint type - // here, so the rest of the lowering + codegen sees the - // narrowed runtime type directly. Without this, perry's - // codegen `is_string_expr`/`is_array_expr`/`is_numeric_expr` - // fast paths don't fire on `(self: T) - // => self[0]` and the IndexGet falls through to the - // polymorphic-object runtime helper, which reads a - // `StringHeader*` as `ArrayHeader*` and returns header - // bytes as a subnormal f64 (#321: effect `Str.capitalize` - // surfaced as `1.5E-323oo`). Arrow functions and - // function-typed-local indirections in particular bypass - // generic-call monomorphization entirely, so the - // un-substituted body would be the one codegen emits. - // - // Constraints that don't usefully narrow the runtime - // representation (named class, literal type, intersection, - // `unknown`/`any`) fall through to `TypeVar(name)` as - // before — preserving the existing native-instance tagging - // / class-id propagation paths. - if let Some(context) = ctx { - if context.is_type_param(&name) { - if let Some(resolved) = context.resolve_type_param_constraint(&name) { - return resolved; - } - return Type::TypeVar(name); - } - } - - // Check for built-in generic types or generic instantiations - if let Some(type_params) = &type_ref.type_params { - match name.as_str() { - "Array" if !type_params.params.is_empty() => { - let elem_type = extract_ts_type_with_ctx(&type_params.params[0], ctx); - return Type::Array(Box::new(elem_type)); - } - "Promise" if !type_params.params.is_empty() => { - let result_type = extract_ts_type_with_ctx(&type_params.params[0], ctx); - return Type::Promise(Box::new(result_type)); - } - _ => { - // Generic type instantiation (e.g., Box, Map) - let type_args: Vec = type_params - .params - .iter() - .map(|t| extract_ts_type_with_ctx(t, ctx)) - .collect(); - return Type::Generic { - base: name, - type_args, - }; - } - } - } - - if matches!( - name.as_str(), - "PerryU32" - | "PerryU64" - | "PerryUSize" - | "PerryF32" - | "PerryF64" - | "PerryI32" - | "PerryI64" - | "PerryBufferLen" - | "PerryHandleId" - ) { - return Type::Named(name); - } - - // Check if this is a type alias — resolve to the underlying type - // so the codegen sees Union/String/Number instead of Named("BlockTag"). - // Without this, `type BlockTag = 'latest' | number | string` stays as - // Named("BlockTag") which the codegen treats as I64 (object pointer), - // causing ABI mismatch when the actual value is a NaN-boxed union. - if let Some(context) = ctx { - if let Some(resolved) = context.resolve_type_alias(&name) { - return resolved; - } - } - - Type::Named(name) - } - - // Function type: (a: T, b: U) => R - TsFnOrConstructorType(fn_type) => { - match fn_type { - ast::TsFnOrConstructorType::TsFnType(fn_ty) => { - // Extract parameter types - let params: Vec<(String, Type, bool)> = fn_ty - .params - .iter() - .map(|p| { - let (name, ty) = get_fn_param_name_and_type_with_ctx(p, ctx); - (name, ty, false) // TODO: detect optional params - }) - .collect(); - - let return_type = extract_ts_type_with_ctx(&fn_ty.type_ann.type_ann, ctx); - - Type::Function(perry_types::FunctionType { - params, - return_type: Box::new(return_type), - is_async: false, - is_generator: false, - }) - } - ast::TsFnOrConstructorType::TsConstructorType(_) => { - // Constructor types are complex - treat as Any for now - Type::Any - } - } - } - - // Literal types: "foo", 42, true - TsLitType(lit) => match &lit.lit { - ast::TsLit::Number(_) => Type::Number, - ast::TsLit::Str(s) => Type::StringLiteral(s.value.as_str().unwrap_or("").to_string()), - ast::TsLit::Bool(_) => Type::Boolean, - ast::TsLit::BigInt(_) => Type::BigInt, - ast::TsLit::Tpl(_) => Type::String, - }, - - // Parenthesized type: (T) - TsParenthesizedType(paren) => extract_ts_type_with_ctx(&paren.type_ann, ctx), - - // Optional type: T? - TsOptionalType(opt) => extract_ts_type_with_ctx(&opt.type_ann, ctx), - - // Rest type: ...T - TsRestType(rest) => extract_ts_type_with_ctx(&rest.type_ann, ctx), - - // Type query: typeof x - TsTypeQuery(_) => Type::Any, - - // Conditional type: T extends U ? X : Y - TsConditionalType(_) => Type::Any, - - // Mapped type: { [K in T]: U } - TsMappedType(_) => Type::Any, - - // Index access: T[K] - TsIndexedAccessType(_) => Type::Any, - - // Infer type: infer T - TsInferType(_) => Type::Any, - - // this type - TsThisType(_) => Type::Any, - - // Type predicate: x is T - TsTypePredicate(_) => Type::Boolean, - - // Import type: import("module").Type - TsImportType(_) => Type::Any, - - // Type operator: keyof T, readonly T, unique symbol. - // For `readonly T` we just return the inner type (the readonly - // modifier is purely a type-system concept; runtime treatment is - // identical to T). keyof and unique symbol stay as Any. - TsTypeOperator(op) => { - use swc_ecma_ast::TsTypeOperatorOp; - match op.op { - TsTypeOperatorOp::ReadOnly => extract_ts_type_with_ctx(&op.type_ann, ctx), - TsTypeOperatorOp::KeyOf => Type::String, - _ => Type::Any, - } - } - - // Type literal: { a: T, b: U } - TsTypeLit(lit) => { - let mut properties = std::collections::HashMap::new(); - let mut property_order = Vec::new(); - for member in &lit.members { - match member { - ast::TsTypeElement::TsPropertySignature(prop) => { - if let ast::Expr::Ident(ident) = prop.key.as_ref() { - let field_name = ident.sym.to_string(); - let field_type = if let Some(ann) = &prop.type_ann { - extract_ts_type_with_ctx(&ann.type_ann, ctx) - } else { - Type::Any - }; - if !properties.contains_key(&field_name) { - property_order.push(field_name.clone()); - } - properties.insert( - field_name, - perry_types::PropertyInfo { - ty: field_type, - optional: prop.optional, - readonly: prop.readonly, - }, - ); - } - } - ast::TsTypeElement::TsMethodSignature(method) => { - if let ast::Expr::Ident(ident) = method.key.as_ref() { - let method_name = ident.sym.to_string(); - let return_type = method - .type_ann - .as_ref() - .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) - .unwrap_or(Type::Any); - let params: Vec<(String, Type, bool)> = method - .params - .iter() - .map(|p| { - let (name, ty) = get_fn_param_name_and_type_with_ctx(p, ctx); - (name, ty, false) - }) - .collect(); - properties.insert( - method_name, - perry_types::PropertyInfo { - ty: Type::Function(perry_types::FunctionType { - params, - return_type: Box::new(return_type), - is_async: false, - is_generator: false, - }), - optional: method.optional, - readonly: false, - }, - ); - } - } - ast::TsTypeElement::TsIndexSignature(idx_sig) => { - // index signature: { [key: string]: T } - if let Some(ann) = &idx_sig.type_ann { - let val_type = extract_ts_type_with_ctx(&ann.type_ann, ctx); - return Type::Object(perry_types::ObjectType { - name: None, - properties, - property_order: Some(property_order), - index_signature: Some(Box::new(val_type)), - }); - } - } - _ => {} - } - } - if properties.is_empty() { - Type::Any - } else { - Type::Object(perry_types::ObjectType { - name: None, - properties, - property_order: Some(property_order), - index_signature: None, - }) - } - } - } -} - -/// Helper to get name from TsEntityName -pub(crate) fn get_ts_entity_name(entity: &ast::TsEntityName) -> String { - match entity { - ast::TsEntityName::Ident(ident) => ident.sym.to_string(), - ast::TsEntityName::TsQualifiedName(qname) => { - format!("{}.{}", get_ts_entity_name(&qname.left), qname.right.sym) - } - } -} - -/// Helper to get parameter name and type from TsFnParam with context -pub(crate) fn get_fn_param_name_and_type_with_ctx( - param: &ast::TsFnParam, - ctx: Option<&LoweringContext>, -) -> (String, Type) { - match param { - ast::TsFnParam::Ident(ident) => { - let name = ident.id.sym.to_string(); - let ty = ident - .type_ann - .as_ref() - .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) - .unwrap_or(Type::Any); - (name, ty) - } - ast::TsFnParam::Array(arr) => { - let ty = arr - .type_ann - .as_ref() - .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) - .unwrap_or(Type::Any); - ("_array".to_string(), ty) - } - ast::TsFnParam::Rest(rest) => { - let ty = rest - .type_ann - .as_ref() - .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) - .unwrap_or(Type::Any); - ("_rest".to_string(), ty) - } - ast::TsFnParam::Object(obj) => { - let ty = obj - .type_ann - .as_ref() - .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) - .unwrap_or(Type::Any); - ("_obj".to_string(), ty) - } - } -} - -/// Extract class name from a member expression (e.g., "ethers.JsonRpcProvider" -> "JsonRpcProvider") -/// This is used for extends clauses that reference external module classes -pub(crate) fn extract_member_class_name(member: &ast::MemberExpr) -> String { - match &member.prop { - ast::MemberProp::Ident(ident) => ident.sym.to_string(), - ast::MemberProp::Computed(computed) => { - if let ast::Expr::Lit(ast::Lit::Str(s)) = computed.expr.as_ref() { - s.value.as_str().unwrap_or("UnknownClass").to_string() - } else { - "UnknownClass".to_string() - } - } - ast::MemberProp::PrivateName(priv_name) => priv_name.name.to_string(), - } -} - -/// Extract type from a pattern (handles BindingIdent with type annotation) -/// Used for both parameter patterns and variable declaration bindings -pub(crate) fn extract_pattern_type(pat: &ast::Pat) -> Type { - extract_pattern_type_with_ctx(pat, None) -} - -/// Extract type from a pattern with type parameter context -pub(crate) fn extract_pattern_type_with_ctx(pat: &ast::Pat, ctx: Option<&LoweringContext>) -> Type { - match pat { - ast::Pat::Ident(ident) => ident - .type_ann - .as_ref() - .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) - .unwrap_or(Type::Any), - ast::Pat::Array(arr) => arr - .type_ann - .as_ref() - .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) - .unwrap_or(Type::Any), - ast::Pat::Rest(rest) => rest - .type_ann - .as_ref() - .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) - .unwrap_or(Type::Any), - ast::Pat::Object(obj) => obj - .type_ann - .as_ref() - .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) - .unwrap_or(Type::Any), - ast::Pat::Assign(assign) => { - // For default parameters, get type from the left side - extract_pattern_type_with_ctx(&assign.left, ctx) - } - ast::Pat::Invalid(_) | ast::Pat::Expr(_) => Type::Any, - } -} - -/// Alias for parameter type extraction with context -pub(crate) fn extract_param_type_with_ctx(pat: &ast::Pat, ctx: Option<&LoweringContext>) -> Type { - extract_pattern_type_with_ctx(pat, ctx) -} - -/// Extract type from a variable declaration binding -pub(crate) fn extract_binding_type(binding: &ast::Pat) -> Type { - extract_pattern_type(binding) -} - -/// Lower decorators from SWC AST to HIR Decorators -pub(crate) fn lower_decorators( - ctx: &mut LoweringContext, - decorators: &[ast::Decorator], -) -> Vec { - decorators - .iter() - .filter_map(|dec| { - // The decorator expression can be: - // - Identifier: @log - // - Call expression: @log("prefix") - match dec.expr.as_ref() { - ast::Expr::Ident(ident) => Some(Decorator { - name: ident.sym.to_string(), - args: Vec::new(), - is_factory: false, - is_reflect_metadata: false, - }), - ast::Expr::Call(call) => { - // Get the callee name - if let ast::Callee::Expr(callee_expr) = &call.callee { - if let ast::Expr::Member(member) = callee_expr.as_ref() { - if let ast::Expr::Ident(obj) = member.obj.as_ref() { - if obj.sym.as_ref() == "Reflect" { - if let ast::MemberProp::Ident(method) = &member.prop { - if method.sym.as_ref() == "metadata" { - let args: Vec = call - .args - .iter() - .filter_map(|arg| { - if arg.spread.is_some() { - None - } else { - lower_decorator_arg(ctx, arg.expr.as_ref()) - } - }) - .collect(); - return Some(Decorator { - name: "Reflect.metadata".to_string(), - args, - is_factory: true, - is_reflect_metadata: true, - }); - } - } - } - } - } - if let ast::Expr::Ident(ident) = callee_expr.as_ref() { - let args: Vec = call - .args - .iter() - .filter_map(|arg| { - if arg.spread.is_some() { - None - } else { - lower_decorator_arg(ctx, arg.expr.as_ref()) - } - }) - .collect(); - return Some(Decorator { - name: ident.sym.to_string(), - args, - is_factory: true, - is_reflect_metadata: false, - }); - } - } - None - } - _ => None, - } - }) - .collect() -} - -fn lower_decorator_arg(ctx: &mut LoweringContext, expr: &ast::Expr) -> Option { - match expr { - ast::Expr::Lit(lit) => lower_lit(lit).ok(), - ast::Expr::Ident(ident) => match lower_expr(ctx, expr).ok() { - Some(Expr::GlobalGet(0)) => Some(Expr::ClassRef(ident.sym.to_string())), - // Bare built-in name `Date`/`Array`/`Object`/... now lowers - // to `PropertyGet { GlobalGet(0), name }` (so the value-side - // identity comparison `inst.constructor === Date` matches). - // For decorator-arg use it's still a class ref. - Some(Expr::PropertyGet { - object: ref obj, - property: _, - }) if matches!(obj.as_ref(), Expr::GlobalGet(0)) => { - Some(Expr::ClassRef(ident.sym.to_string())) - } - other => other, - }, - ast::Expr::Array(arr) => { - let items = arr - .elems - .iter() - .map(|elem| { - elem.as_ref() - .and_then(|elem| { - if elem.spread.is_some() { - None - } else { - lower_decorator_arg(ctx, elem.expr.as_ref()) - } - }) - .unwrap_or(Expr::Undefined) - }) - .collect(); - Some(Expr::Array(items)) - } - ast::Expr::Object(obj) => { - let mut fields = Vec::new(); - for prop in &obj.props { - let ast::PropOrSpread::Prop(prop) = prop else { - return None; - }; - match prop.as_ref() { - ast::Prop::KeyValue(kv) => { - let key = decorator_prop_name(&kv.key)?; - let value = lower_decorator_arg(ctx, kv.value.as_ref())?; - fields.push((key, value)); - } - ast::Prop::Shorthand(ident) => { - let name = ident.sym.to_string(); - let value = lower_decorator_arg(ctx, &ast::Expr::Ident(ident.clone()))?; - fields.push((name, value)); - } - _ => return None, - } - } - Some(Expr::Object(fields)) - } - _ => lower_expr(ctx, expr).ok(), - } -} +mod extract; -fn decorator_prop_name(name: &ast::PropName) -> Option { - match name { - ast::PropName::Ident(ident) => Some(ident.sym.to_string()), - ast::PropName::Str(s) => Some(s.value.as_str().unwrap_or("").to_string()), - ast::PropName::Num(n) => Some(n.value.to_string()), - _ => None, - } -} +pub(crate) use extract::{ + extract_binding_type, extract_member_class_name, extract_param_type_with_ctx, + extract_pattern_type, extract_pattern_type_with_ctx, extract_ts_type, extract_ts_type_with_ctx, + extract_type_params, get_fn_param_name_and_type_with_ctx, get_ts_entity_name, lower_decorators, +}; diff --git a/crates/perry-hir/src/lower_types/extract.rs b/crates/perry-hir/src/lower_types/extract.rs new file mode 100644 index 0000000000..42b79954d3 --- /dev/null +++ b/crates/perry-hir/src/lower_types/extract.rs @@ -0,0 +1,625 @@ +//! TypeScript annotation extraction — `extract_ts_type` and friends, plus +//! decorator lowering. Split out of `lower_types.rs` (#6233 follow-up) to +//! keep the parent under the 2000-line lint cap; pure move, no logic change. +//! Named re-exports in `lower_types.rs` keep every existing call path +//! (`crate::lower_types::extract_ts_type`, ...) compiling unchanged. + +use perry_types::{Type, TypeParam}; +use swc_ecma_ast as ast; + +use crate::ir::*; +use crate::lower::{lower_expr, LoweringContext}; +use crate::lower_patterns::{get_pat_name, lower_lit}; + +/// Extract type parameters from SWC's TsTypeParamDecl +pub(crate) fn extract_type_params(decl: &ast::TsTypeParamDecl) -> Vec { + decl.params + .iter() + .map(|p| { + let name = p.name.sym.to_string(); + let constraint = p.constraint.as_ref().map(|c| Box::new(extract_ts_type(c))); + let default = p.default.as_ref().map(|d| Box::new(extract_ts_type(d))); + TypeParam { + name, + constraint, + default, + } + }) + .collect() +} + +/// Extract a Type from an SWC TypeScript type annotation +/// This version doesn't have access to type parameter context +pub(crate) fn extract_ts_type(ts_type: &ast::TsType) -> Type { + extract_ts_type_with_ctx(ts_type, None) +} + +/// Extract a Type from an SWC TypeScript type annotation with type parameter context +pub(crate) fn extract_ts_type_with_ctx( + ts_type: &ast::TsType, + ctx: Option<&LoweringContext>, +) -> Type { + use ast::TsKeywordTypeKind::*; + use ast::TsType::*; + + match ts_type { + // Keyword types (primitives) + TsKeywordType(kw) => match kw.kind { + TsNumberKeyword => Type::Number, + TsStringKeyword => Type::String, + TsBooleanKeyword => Type::Boolean, + TsBigIntKeyword => Type::BigInt, + TsVoidKeyword => Type::Void, + TsNullKeyword => Type::Null, + TsUndefinedKeyword => Type::Void, + TsAnyKeyword => Type::Any, + TsUnknownKeyword => Type::Unknown, + TsNeverKeyword => Type::Never, + TsSymbolKeyword => Type::Symbol, + TsObjectKeyword => Type::Any, // Generic object + TsIntrinsicKeyword => Type::Any, + }, + + // Array type: T[] + TsArrayType(arr) => { + let elem_type = extract_ts_type_with_ctx(&arr.elem_type, ctx); + Type::Array(Box::new(elem_type)) + } + + // Tuple type: [T, U, V] + TsTupleType(tuple) => { + let elem_types: Vec = tuple + .elem_types + .iter() + .map(|elem| extract_ts_type_with_ctx(&elem.ty, ctx)) + .collect(); + Type::Tuple(elem_types) + } + + // Union type: A | B | C + TsUnionOrIntersectionType(union_or_inter) => { + match union_or_inter { + ast::TsUnionOrIntersectionType::TsUnionType(union) => { + let types: Vec = union + .types + .iter() + .map(|t| extract_ts_type_with_ctx(t, ctx)) + .collect(); + Type::Union(types) + } + ast::TsUnionOrIntersectionType::TsIntersectionType(_) => { + // Intersection types are complex - treat as Any for now + Type::Any + } + } + } + + // Type reference: Array, MyClass, T (type param), etc. + TsTypeRef(type_ref) => { + let name = match &type_ref.type_name { + ast::TsEntityName::Ident(ident) => ident.sym.to_string(), + ast::TsEntityName::TsQualifiedName(qname) => { + // Qualified names like Foo.Bar + format!("{}.{}", get_ts_entity_name(&qname.left), qname.right.sym) + } + }; + + // First check if this is a type parameter reference (like T, K, V). + // + // When the parameter has a runtime-meaningful upper-bound + // constraint (``, ``, + // `` …) substitute the constraint type + // here, so the rest of the lowering + codegen sees the + // narrowed runtime type directly. Without this, perry's + // codegen `is_string_expr`/`is_array_expr`/`is_numeric_expr` + // fast paths don't fire on `(self: T) + // => self[0]` and the IndexGet falls through to the + // polymorphic-object runtime helper, which reads a + // `StringHeader*` as `ArrayHeader*` and returns header + // bytes as a subnormal f64 (#321: effect `Str.capitalize` + // surfaced as `1.5E-323oo`). Arrow functions and + // function-typed-local indirections in particular bypass + // generic-call monomorphization entirely, so the + // un-substituted body would be the one codegen emits. + // + // Constraints that don't usefully narrow the runtime + // representation (named class, literal type, intersection, + // `unknown`/`any`) fall through to `TypeVar(name)` as + // before — preserving the existing native-instance tagging + // / class-id propagation paths. + if let Some(context) = ctx { + if context.is_type_param(&name) { + if let Some(resolved) = context.resolve_type_param_constraint(&name) { + return resolved; + } + return Type::TypeVar(name); + } + } + + // Check for built-in generic types or generic instantiations + if let Some(type_params) = &type_ref.type_params { + match name.as_str() { + "Array" if !type_params.params.is_empty() => { + let elem_type = extract_ts_type_with_ctx(&type_params.params[0], ctx); + return Type::Array(Box::new(elem_type)); + } + "Promise" if !type_params.params.is_empty() => { + let result_type = extract_ts_type_with_ctx(&type_params.params[0], ctx); + return Type::Promise(Box::new(result_type)); + } + _ => { + // Generic type instantiation (e.g., Box, Map) + let type_args: Vec = type_params + .params + .iter() + .map(|t| extract_ts_type_with_ctx(t, ctx)) + .collect(); + return Type::Generic { + base: name, + type_args, + }; + } + } + } + + if matches!( + name.as_str(), + "PerryU32" + | "PerryU64" + | "PerryUSize" + | "PerryF32" + | "PerryF64" + | "PerryI32" + | "PerryI64" + | "PerryBufferLen" + | "PerryHandleId" + ) { + return Type::Named(name); + } + + // Check if this is a type alias — resolve to the underlying type + // so the codegen sees Union/String/Number instead of Named("BlockTag"). + // Without this, `type BlockTag = 'latest' | number | string` stays as + // Named("BlockTag") which the codegen treats as I64 (object pointer), + // causing ABI mismatch when the actual value is a NaN-boxed union. + if let Some(context) = ctx { + if let Some(resolved) = context.resolve_type_alias(&name) { + return resolved; + } + } + + Type::Named(name) + } + + // Function type: (a: T, b: U) => R + TsFnOrConstructorType(fn_type) => { + match fn_type { + ast::TsFnOrConstructorType::TsFnType(fn_ty) => { + // Extract parameter types + let params: Vec<(String, Type, bool)> = fn_ty + .params + .iter() + .map(|p| { + let (name, ty) = get_fn_param_name_and_type_with_ctx(p, ctx); + (name, ty, false) // TODO: detect optional params + }) + .collect(); + + let return_type = extract_ts_type_with_ctx(&fn_ty.type_ann.type_ann, ctx); + + Type::Function(perry_types::FunctionType { + params, + return_type: Box::new(return_type), + is_async: false, + is_generator: false, + }) + } + ast::TsFnOrConstructorType::TsConstructorType(_) => { + // Constructor types are complex - treat as Any for now + Type::Any + } + } + } + + // Literal types: "foo", 42, true + TsLitType(lit) => match &lit.lit { + ast::TsLit::Number(_) => Type::Number, + ast::TsLit::Str(s) => Type::StringLiteral(s.value.as_str().unwrap_or("").to_string()), + ast::TsLit::Bool(_) => Type::Boolean, + ast::TsLit::BigInt(_) => Type::BigInt, + ast::TsLit::Tpl(_) => Type::String, + }, + + // Parenthesized type: (T) + TsParenthesizedType(paren) => extract_ts_type_with_ctx(&paren.type_ann, ctx), + + // Optional type: T? + TsOptionalType(opt) => extract_ts_type_with_ctx(&opt.type_ann, ctx), + + // Rest type: ...T + TsRestType(rest) => extract_ts_type_with_ctx(&rest.type_ann, ctx), + + // Type query: typeof x + TsTypeQuery(_) => Type::Any, + + // Conditional type: T extends U ? X : Y + TsConditionalType(_) => Type::Any, + + // Mapped type: { [K in T]: U } + TsMappedType(_) => Type::Any, + + // Index access: T[K] + TsIndexedAccessType(_) => Type::Any, + + // Infer type: infer T + TsInferType(_) => Type::Any, + + // this type + TsThisType(_) => Type::Any, + + // Type predicate: x is T + TsTypePredicate(_) => Type::Boolean, + + // Import type: import("module").Type + TsImportType(_) => Type::Any, + + // Type operator: keyof T, readonly T, unique symbol. + // For `readonly T` we just return the inner type (the readonly + // modifier is purely a type-system concept; runtime treatment is + // identical to T). keyof and unique symbol stay as Any. + TsTypeOperator(op) => { + use swc_ecma_ast::TsTypeOperatorOp; + match op.op { + TsTypeOperatorOp::ReadOnly => extract_ts_type_with_ctx(&op.type_ann, ctx), + TsTypeOperatorOp::KeyOf => Type::String, + _ => Type::Any, + } + } + + // Type literal: { a: T, b: U } + TsTypeLit(lit) => { + let mut properties = std::collections::HashMap::new(); + let mut property_order = Vec::new(); + for member in &lit.members { + match member { + ast::TsTypeElement::TsPropertySignature(prop) => { + if let ast::Expr::Ident(ident) = prop.key.as_ref() { + let field_name = ident.sym.to_string(); + let field_type = if let Some(ann) = &prop.type_ann { + extract_ts_type_with_ctx(&ann.type_ann, ctx) + } else { + Type::Any + }; + if !properties.contains_key(&field_name) { + property_order.push(field_name.clone()); + } + properties.insert( + field_name, + perry_types::PropertyInfo { + ty: field_type, + optional: prop.optional, + readonly: prop.readonly, + }, + ); + } + } + ast::TsTypeElement::TsMethodSignature(method) => { + if let ast::Expr::Ident(ident) = method.key.as_ref() { + let method_name = ident.sym.to_string(); + let return_type = method + .type_ann + .as_ref() + .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) + .unwrap_or(Type::Any); + let params: Vec<(String, Type, bool)> = method + .params + .iter() + .map(|p| { + let (name, ty) = get_fn_param_name_and_type_with_ctx(p, ctx); + (name, ty, false) + }) + .collect(); + properties.insert( + method_name, + perry_types::PropertyInfo { + ty: Type::Function(perry_types::FunctionType { + params, + return_type: Box::new(return_type), + is_async: false, + is_generator: false, + }), + optional: method.optional, + readonly: false, + }, + ); + } + } + ast::TsTypeElement::TsIndexSignature(idx_sig) => { + // index signature: { [key: string]: T } + if let Some(ann) = &idx_sig.type_ann { + let val_type = extract_ts_type_with_ctx(&ann.type_ann, ctx); + return Type::Object(perry_types::ObjectType { + name: None, + properties, + property_order: Some(property_order), + index_signature: Some(Box::new(val_type)), + }); + } + } + _ => {} + } + } + if properties.is_empty() { + Type::Any + } else { + Type::Object(perry_types::ObjectType { + name: None, + properties, + property_order: Some(property_order), + index_signature: None, + }) + } + } + } +} + +/// Helper to get name from TsEntityName +pub(crate) fn get_ts_entity_name(entity: &ast::TsEntityName) -> String { + match entity { + ast::TsEntityName::Ident(ident) => ident.sym.to_string(), + ast::TsEntityName::TsQualifiedName(qname) => { + format!("{}.{}", get_ts_entity_name(&qname.left), qname.right.sym) + } + } +} + +/// Helper to get parameter name and type from TsFnParam with context +pub(crate) fn get_fn_param_name_and_type_with_ctx( + param: &ast::TsFnParam, + ctx: Option<&LoweringContext>, +) -> (String, Type) { + match param { + ast::TsFnParam::Ident(ident) => { + let name = ident.id.sym.to_string(); + let ty = ident + .type_ann + .as_ref() + .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) + .unwrap_or(Type::Any); + (name, ty) + } + ast::TsFnParam::Array(arr) => { + let ty = arr + .type_ann + .as_ref() + .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) + .unwrap_or(Type::Any); + ("_array".to_string(), ty) + } + ast::TsFnParam::Rest(rest) => { + let ty = rest + .type_ann + .as_ref() + .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) + .unwrap_or(Type::Any); + ("_rest".to_string(), ty) + } + ast::TsFnParam::Object(obj) => { + let ty = obj + .type_ann + .as_ref() + .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) + .unwrap_or(Type::Any); + ("_obj".to_string(), ty) + } + } +} + +/// Extract class name from a member expression (e.g., "ethers.JsonRpcProvider" -> "JsonRpcProvider") +/// This is used for extends clauses that reference external module classes +pub(crate) fn extract_member_class_name(member: &ast::MemberExpr) -> String { + match &member.prop { + ast::MemberProp::Ident(ident) => ident.sym.to_string(), + ast::MemberProp::Computed(computed) => { + if let ast::Expr::Lit(ast::Lit::Str(s)) = computed.expr.as_ref() { + s.value.as_str().unwrap_or("UnknownClass").to_string() + } else { + "UnknownClass".to_string() + } + } + ast::MemberProp::PrivateName(priv_name) => priv_name.name.to_string(), + } +} + +/// Extract type from a pattern (handles BindingIdent with type annotation) +/// Used for both parameter patterns and variable declaration bindings +pub(crate) fn extract_pattern_type(pat: &ast::Pat) -> Type { + extract_pattern_type_with_ctx(pat, None) +} + +/// Extract type from a pattern with type parameter context +pub(crate) fn extract_pattern_type_with_ctx(pat: &ast::Pat, ctx: Option<&LoweringContext>) -> Type { + match pat { + ast::Pat::Ident(ident) => ident + .type_ann + .as_ref() + .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) + .unwrap_or(Type::Any), + ast::Pat::Array(arr) => arr + .type_ann + .as_ref() + .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) + .unwrap_or(Type::Any), + ast::Pat::Rest(rest) => rest + .type_ann + .as_ref() + .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) + .unwrap_or(Type::Any), + ast::Pat::Object(obj) => obj + .type_ann + .as_ref() + .map(|ann| extract_ts_type_with_ctx(&ann.type_ann, ctx)) + .unwrap_or(Type::Any), + ast::Pat::Assign(assign) => { + // For default parameters, get type from the left side + extract_pattern_type_with_ctx(&assign.left, ctx) + } + ast::Pat::Invalid(_) | ast::Pat::Expr(_) => Type::Any, + } +} + +/// Alias for parameter type extraction with context +pub(crate) fn extract_param_type_with_ctx(pat: &ast::Pat, ctx: Option<&LoweringContext>) -> Type { + extract_pattern_type_with_ctx(pat, ctx) +} + +/// Extract type from a variable declaration binding +pub(crate) fn extract_binding_type(binding: &ast::Pat) -> Type { + extract_pattern_type(binding) +} + +/// Lower decorators from SWC AST to HIR Decorators +pub(crate) fn lower_decorators( + ctx: &mut LoweringContext, + decorators: &[ast::Decorator], +) -> Vec { + decorators + .iter() + .filter_map(|dec| { + // The decorator expression can be: + // - Identifier: @log + // - Call expression: @log("prefix") + match dec.expr.as_ref() { + ast::Expr::Ident(ident) => Some(Decorator { + name: ident.sym.to_string(), + args: Vec::new(), + is_factory: false, + is_reflect_metadata: false, + }), + ast::Expr::Call(call) => { + // Get the callee name + if let ast::Callee::Expr(callee_expr) = &call.callee { + if let ast::Expr::Member(member) = callee_expr.as_ref() { + if let ast::Expr::Ident(obj) = member.obj.as_ref() { + if obj.sym.as_ref() == "Reflect" { + if let ast::MemberProp::Ident(method) = &member.prop { + if method.sym.as_ref() == "metadata" { + let args: Vec = call + .args + .iter() + .filter_map(|arg| { + if arg.spread.is_some() { + None + } else { + lower_decorator_arg(ctx, arg.expr.as_ref()) + } + }) + .collect(); + return Some(Decorator { + name: "Reflect.metadata".to_string(), + args, + is_factory: true, + is_reflect_metadata: true, + }); + } + } + } + } + } + if let ast::Expr::Ident(ident) = callee_expr.as_ref() { + let args: Vec = call + .args + .iter() + .filter_map(|arg| { + if arg.spread.is_some() { + None + } else { + lower_decorator_arg(ctx, arg.expr.as_ref()) + } + }) + .collect(); + return Some(Decorator { + name: ident.sym.to_string(), + args, + is_factory: true, + is_reflect_metadata: false, + }); + } + } + None + } + _ => None, + } + }) + .collect() +} + +fn lower_decorator_arg(ctx: &mut LoweringContext, expr: &ast::Expr) -> Option { + match expr { + ast::Expr::Lit(lit) => lower_lit(lit).ok(), + ast::Expr::Ident(ident) => match lower_expr(ctx, expr).ok() { + Some(Expr::GlobalGet(0)) => Some(Expr::ClassRef(ident.sym.to_string())), + // Bare built-in name `Date`/`Array`/`Object`/... now lowers + // to `PropertyGet { GlobalGet(0), name }` (so the value-side + // identity comparison `inst.constructor === Date` matches). + // For decorator-arg use it's still a class ref. + Some(Expr::PropertyGet { + object: ref obj, + property: _, + }) if matches!(obj.as_ref(), Expr::GlobalGet(0)) => { + Some(Expr::ClassRef(ident.sym.to_string())) + } + other => other, + }, + ast::Expr::Array(arr) => { + let items = arr + .elems + .iter() + .map(|elem| { + elem.as_ref() + .and_then(|elem| { + if elem.spread.is_some() { + None + } else { + lower_decorator_arg(ctx, elem.expr.as_ref()) + } + }) + .unwrap_or(Expr::Undefined) + }) + .collect(); + Some(Expr::Array(items)) + } + ast::Expr::Object(obj) => { + let mut fields = Vec::new(); + for prop in &obj.props { + let ast::PropOrSpread::Prop(prop) = prop else { + return None; + }; + match prop.as_ref() { + ast::Prop::KeyValue(kv) => { + let key = decorator_prop_name(&kv.key)?; + let value = lower_decorator_arg(ctx, kv.value.as_ref())?; + fields.push((key, value)); + } + ast::Prop::Shorthand(ident) => { + let name = ident.sym.to_string(); + let value = lower_decorator_arg(ctx, &ast::Expr::Ident(ident.clone()))?; + fields.push((name, value)); + } + _ => return None, + } + } + Some(Expr::Object(fields)) + } + _ => lower_expr(ctx, expr).ok(), + } +} + +fn decorator_prop_name(name: &ast::PropName) -> Option { + match name { + ast::PropName::Ident(ident) => Some(ident.sym.to_string()), + ast::PropName::Str(s) => Some(s.value.as_str().unwrap_or("").to_string()), + ast::PropName::Num(n) => Some(n.value.to_string()), + _ => None, + } +} diff --git a/test-files/test_issue_6233_shadowed_global_class_new.ts b/test-files/test_issue_6233_shadowed_global_class_new.ts new file mode 100644 index 0000000000..8202c48e71 --- /dev/null +++ b/test-files/test_issue_6233_shadowed_global_class_new.ts @@ -0,0 +1,183 @@ +// Issue #6233 — a module-scope user class legally shadows a same-named +// global (`class Symbol extends Base {}` … `new Symbol()`), but the built-in +// constructor arms in `lower_new` fired by NAME alone, so the `new` bound to +// the intrinsic instead of the user class. Depending on the name this either +// threw ("Symbol is not a constructor", the Proxy target TypeError, the +// WeakRef/FinalizationRegistry/AggregateError argument validation), or +// silently constructed the wrong object (native Map/Set/Date/boxed +// primitive/Error/typed array — field initializers never ran, instanceof +// against the user class was false). Found via effect's SchemaAST.ts, which +// declares AST node classes named `Symbol` and `BigInt` and constructs them +// at module init. + +class Base { + describe(): string { + return "base"; + } +} + +// The exact shape from the issue / effect's SchemaAST.ts. +class Symbol extends Base { + readonly _tag = "Symbol"; +} +const node = new Symbol(); +console.log("constructed:", node._tag, node instanceof Symbol, node.describe()); + +// Crash bucket: bound to a non-constructible intrinsic or the intrinsic's +// argument validation. +class BigInt extends Base { + readonly tag = "BigInt"; +} +const big = new BigInt(); +console.log("BIGINT", big.tag, big instanceof BigInt); + +class Proxy { + readonly target: string; + constructor(target: string) { + this.target = target; + } + who(): string { + return "user-proxy-" + this.target; + } +} +const prox = new Proxy("t"); +console.log("PROXY", prox.who(), prox instanceof Proxy); + +class WeakRef { + readonly inner: string; + constructor(inner: string) { + this.inner = inner; + } + // Reserved native method name — must dispatch to the USER method, not the + // WeakRef intrinsic fast path (the weak-locals pre-scan tagged the binding + // by constructor name alone). + deref(): string { + return "user-deref-" + this.inner; + } +} +const wref = new WeakRef("w"); +console.log("WEAKREF", wref.deref(), wref instanceof WeakRef); + +class FinalizationRegistry { + readonly tag = "FinalizationRegistry"; +} +const finreg = new FinalizationRegistry(); +console.log("FINREG", finreg.tag, finreg instanceof FinalizationRegistry); + +class AggregateError { + readonly tag = "AggregateError"; +} +const agg = new AggregateError(); +console.log("AGG", agg.tag, agg instanceof AggregateError); + +// Wrong-object bucket: the native constructor ran instead of the user class, +// so field initializers never executed and instanceof was false. +class Map { + private store: Record = {}; + // Reserved native Map method names — must hit the user methods. + set(k: string, v: number): void { + this.store[k] = v; + } + get(k: string): number { + return this.store[k] ?? -1; + } +} +const map = new Map(); +map.set("a", 41); +console.log("MAP", map.get("a"), map.get("missing"), map instanceof Map); + +class Set { + readonly tag = "Set"; +} +const set = new Set(); +console.log("SET", set.tag, set instanceof Set); + +class Date { + readonly tag = "Date"; +} +const date = new Date(); +console.log("DATE", date.tag, date instanceof Date); + +class Number { + readonly tag = "Number"; +} +const num = new Number(); +console.log("NUMBER", num.tag, num instanceof Number); + +class String { + readonly tag = "String"; +} +const str = new String(); +console.log("STRING", str.tag, str instanceof String); + +class Boolean { + readonly tag = "Boolean"; +} +const bool = new Boolean(); +console.log("BOOLEAN", bool.tag, bool instanceof Boolean); + +class Error { + readonly tag = "Error"; +} +const err = new Error(); +console.log("ERROR", err.tag, err instanceof Error); + +class TypeError { + readonly tag = "TypeError"; +} +const terr = new TypeError(); +console.log("TYPEERROR", terr.tag, terr instanceof TypeError); + +class Uint8Array { + readonly tag = "Uint8Array"; +} +const u8 = new Uint8Array(); +console.log("UINT8", u8.tag, u8 instanceof Uint8Array); + +class Int32Array { + readonly tag = "Int32Array"; +} +const i32 = new Int32Array(); +console.log("INT32", i32.tag, i32 instanceof Int32Array); + +// Constructor arguments still flow to the user class (the RegExp arm used to +// consume them for the intrinsic's literal/dynamic paths). +class RegExp { + readonly pattern: string; + constructor(pattern: string) { + this.pattern = pattern; + } +} +const rx = new RegExp("user-pattern"); +console.log("REGEXP", rx.pattern, rx instanceof RegExp); + +// Previously-working names — must stay correct. +class Widget extends Base { + readonly tag = "Widget"; +} +const widget = new Widget(); +console.log("WIDGET", widget.tag, widget instanceof Widget); + +class Object { + readonly tag = "Object"; +} +const obj = new Object(); +console.log("OBJECT", obj.tag, obj instanceof Object); + +class Array { + readonly tag = "Array"; +} +const arr = new Array(); +console.log("ARRAY", arr.tag, arr instanceof Array); + +class Promise { + readonly tag = "Promise"; +} +const prom = new Promise(); +console.log("PROMISE", prom.tag, prom instanceof Promise); + +class Function { + readonly tag = "Function"; +} +const func = new Function(); +console.log("FUNCTION", func.tag, func instanceof Function); diff --git a/test-files/test_issue_6233_shadowed_global_fn_new.ts b/test-files/test_issue_6233_shadowed_global_fn_new.ts new file mode 100644 index 0000000000..1348acba6e --- /dev/null +++ b/test-files/test_issue_6233_shadowed_global_fn_new.ts @@ -0,0 +1,42 @@ +// Issue #6233 follow-up (review): NON-class user bindings — `function Map() +// {}`, a const holding a constructor — also lexically shadow the built-in. +// Construction already routed through the dynamic path, but the var-decl +// type inference still typed the binding as the built-in (`Generic { base: +// "Map" }` / `Named("Uint8Array")` / `Named("URLSearchParams")`), so method +// calls on the constructed instance dispatched down the intrinsic fast +// paths instead of the instance's own members. + +function Map(this: any) { + this.tag = "fn-map"; + this.get = function (k: string): string { + return "fn-get-" + k; + }; + this.set = function (_k: string, _v: number): string { + return "fn-set"; + }; +} +const map = new (Map as any)(); +console.log("FNMAP", map.tag, map.set("a", 1), map.get("a")); + +function Uint8Array(this: any) { + this.tag = "fn-u8"; + this.length = 99; +} +const u8 = new (Uint8Array as any)(); +console.log("FNU8", u8.tag, u8.length); + +function URLSearchParams(this: any) { + this.tag = "fn-usp"; + this.size = -5; +} +const usp = new (URLSearchParams as any)(); +console.log("FNUSP", usp.tag, usp.size); + +const Set = function (this: any) { + this.tag = "const-set"; + this.has = function (_v: number): string { + return "own-has"; + }; +} as any; +const set = new Set(); +console.log("CONSTSET", set.tag, set.has(1));