Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 37 additions & 13 deletions crates/perry-hir/src/destructuring/var_decl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,17 +279,41 @@ pub(crate) fn lower_var_decl_with_destructuring(
if let Some(init_expr) = &decl.init {
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();
let local_name = class_ident.sym.as_ref();
// A user `class Big {...}` in scope shadows the
// hardcoded library-name fallback below. Without
// this gate `class Big { f0=0; ... } const b = new
// Big()` routed through big.js's handle-based
// dispatch so every property read returned 0.
let user_class_defined = ctx.classes_index.contains_key(class_name)
|| ctx.pending_classes.iter().any(|c| c.name == class_name);
let user_class_defined = ctx.classes_index.contains_key(local_name)
|| ctx.pending_classes.iter().any(|c| c.name == local_name);
// #wall: alias-aware native-instance tagging. An
// ALIASED import (`import { BlockList as Wj4 } from
// "net"; const q = new Wj4()`) must register `q` under
// the IMPORTED class ("BlockList"), not the local alias
// ("Wj4"), or `q.addSubnet(...)` dispatch (keyed on
// `("net","BlockList")`) misses and falls to generic
// property access ("addSubnet is not a function").
// `lookup_native_module` is alias-aware (the named
// import registers `local → (module, Some(<imported>))`),
// so resolve the local to its imported export name and
// use THAT as the class name for the hardcoded match and
// the final registration. For the un-aliased case the
// export equals the local, so this is a no-op.
let class_name: &str = ctx
.lookup_native_module(local_name)
.and_then(|(_m, method)| method)
.filter(|export| {
export
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false)
})
.unwrap_or(local_name);
// First try the general native module lookup (covers all imported native classes)
let module_name =
if let Some((m, method)) = ctx.lookup_native_module(class_name) {
if let Some((m, method)) = ctx.lookup_native_module(local_name) {
match (m, method) {
("url", Some("URL" | "URLSearchParams"))
| ("util", Some("TextEncoder" | "TextDecoder")) => None,
Expand Down Expand Up @@ -1463,7 +1487,7 @@ pub(crate) fn lower_var_decl_with_destructuring(
) || decl
.init
.as_deref()
.is_some_and(ast_expr_contains_function_expr);
.map_or(false, ast_expr_contains_function_expr);
let pre_id = if is_function_expr_init
&& !ctx.pre_registered_module_vars.contains(&name)
&& ctx.lookup_local(&name).is_none()
Expand All @@ -1479,10 +1503,10 @@ pub(crate) fn lower_var_decl_with_destructuring(
let init = decl.init.as_ref().map(|e| lower_expr(ctx, e)).transpose()?;
if matches!(ty, Type::Any) {
match &init {
Some(Expr::NativeMethodCall { module, method, .. })
if module == "stream" && method == "from" =>
{
ty = Type::Named("Readable".to_string());
Some(Expr::NativeMethodCall { module, method, .. }) => {
if module == "stream" && method == "from" {
ty = Type::Named("Readable".to_string());
}
}
Some(Expr::NewDynamic { callee, .. }) => {
if let Expr::PropertyGet { object, property } = callee.as_ref() {
Expand Down Expand Up @@ -1858,10 +1882,10 @@ pub(crate) fn lower_var_decl_with_destructuring(
// recogniser later emits
// `RegisterFunctionPrototypeMethod { func:
// LocalGet(M_id), … }`.
Expr::LocalGet(src_local)
if ctx.function_valued_locals.contains(src_local) =>
{
ctx.prototype_function_locals.insert(id, *src_local);
Expr::LocalGet(src_local) => {
if ctx.function_valued_locals.contains(src_local) {
ctx.prototype_function_locals.insert(id, *src_local);
}
}
_ => {}
}
Expand Down
49 changes: 44 additions & 5 deletions crates/perry-hir/src/lower/expr_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,8 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
let is_module_constructor = ctx
.lookup_native_module(callee_ident.sym.as_ref())
.map(|(module_name, method)| {
module_name == "module" && matches!(method, Some("Module") | Some("default"))
module_name == "module"
&& matches!(method.as_deref(), Some("Module") | Some("default"))
})
.unwrap_or(false);
if is_module_constructor {
Expand Down Expand Up @@ -403,7 +404,8 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
let is_events_module_value = ctx
.lookup_native_module(callee_ident.sym.as_ref())
.map(|(module_name, method)| {
module_name == "events" && (method.is_none() || method == Some("default"))
module_name == "events"
&& (method.is_none() || method.as_deref() == Some("default"))
})
.unwrap_or(false)
|| ctx.lookup_builtin_module_alias(callee_ident.sym.as_ref()) == Some("events");
Expand Down Expand Up @@ -584,7 +586,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
ctx.lookup_native_module(obj_name)
.and_then(|(module_name, method)| {
if matches!(module_name, "dns" | "dns/promises")
&& (method.is_none() || method == Some("default"))
&& (method.is_none() || method.as_deref() == Some("default"))
{
Some(module_name.to_string())
} else {
Expand Down Expand Up @@ -643,7 +645,8 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
|| ctx
.lookup_native_module(obj_name)
.map(|(module_name, method)| {
module_name == "vm" && (method.is_none() || method == Some("default"))
module_name == "vm"
&& (method.is_none() || method.as_deref() == Some("default"))
})
.unwrap_or(false);
if is_vm_module
Expand Down Expand Up @@ -933,7 +936,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
ast::Expr::Ident(ident) => {
// Resolve through any scope-local class rename so `new X` binds to
// the lexically-correct (possibly disambiguated) class.
let class_name = ctx.resolve_class_name(ident.sym.as_str());
let mut class_name = ctx.resolve_class_name(ident.sym.as_str());
if matches!(
ctx.lookup_native_module(&class_name),
Some(("url", Some("Url")))
Expand Down Expand Up @@ -1898,6 +1901,42 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
// via `js_new_function_construct` — see
// `perry-codegen/src/lower_call/new.rs`.
}
// #wall: an ALIASED named import of a native built-in class
// (`import { BlockList as Wj4 } from "net"; new Wj4()`) must
// construct exactly like the un-aliased form. The bare-ident
// construction below falls through to `Expr::New { class_name }`,
// and codegen's builtin-`New` dispatch recognizes the class by its
// LITERAL name ("BlockList", "SocketAddress", "Socket", "Server",
// "URL", …). Under an alias the local name ("Wj4") misses every
// arm, so codegen builds an empty placeholder object with no native
// methods (`q.addSubnet` → "addSubnet is not a function").
//
// `lookup_native_module` is already alias-aware: the named-import
// lowering registers `local → (module, Some(<imported>))`, so a
// native-class import resolves the alias to its imported export
// name. Rewrite `class_name` to that export so the alias path is
// byte-for-byte identical to the un-aliased path. This is a no-op
// for the un-aliased case (export == local) and only fires for
// native-module class imports (a user `import { foo as bar }` from
// a TS module registers as an imported func, NOT a native module,
// so `lookup_native_module` returns None — no over-trigger). A
// user class or local of the alias name shadows it (handled by the
// `lookup_class`/`lookup_local` returns above that precede this).
if class_name
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false)
{
if let Some((_module, Some(export))) = ctx.lookup_native_module(&class_name) {
if export != class_name
&& ctx.lookup_class(&class_name).is_none()
&& ctx.lookup_local(&class_name).is_none()
{
class_name = export.to_string();
}
}
}
// Issue #212: classes nested in a function may capture
// enclosing-scope locals. `lower_class_decl` extended the
// constructor with one synthesized param per captured id;
Expand Down
Loading
Loading