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
7 changes: 7 additions & 0 deletions changelog.d/9296-renamed-namespace-constructor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Fixed

- **Classes constructed through renamed namespace re-exports now receive their
constructor arguments (#9285).** `new ns.PublicChild(value)` resolves the
class metadata from its defining export while retaining the namespace-visible
alias, so constructor parameter properties and other initialization run
normally without sacrificing collision-safe namespace class identity.
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/expr/new_dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// params from the decl-site snapshot. No-op for non-capturing
// classes (no cap params to fill).
if let Some(name) = try_static_class_name(callee.as_ref(), ctx) {
return crate::lower_call::lower_new_member_captured(ctx, name, args);
return crate::lower_call::lower_new_member_captured(ctx, name.as_ref(), args);
}

// date-fns `constructFrom(date, value)`:
Expand Down
38 changes: 24 additions & 14 deletions crates/perry-codegen/src/expr/v8_interop.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! V8-fallback-module interop helpers + static-class-name resolution
//! (extracted from `expr.rs`, issue #1098). Pure move — no logic changes.

use std::borrow::Cow;

use perry_hir::Expr;

use super::FnCtx;
Expand Down Expand Up @@ -272,10 +274,10 @@ pub(crate) fn emit_v8_member_method_call(
/// `ns_id` is a namespace import local (`import * as ns from 'm';
/// new ns.Foo()`). The local id is mapped to its name via
/// `ctx.local_id_to_name`, then checked against
/// `ctx.namespace_imports`. The property name is returned as the
/// class name; the rest of the lower_new path resolves it via the
/// usual `ctx.classes` lookup, which contains imported classes
/// under their original (un-namespaced) names.
/// `ctx.namespace_imports`. Namespace classes are returned under their
/// collision-free `(namespace, member)` registry key; the rest of the
/// lower_new path resolves that key through the usual `ctx.classes`
/// lookup.
fn is_global_object_expr(expr: &Expr) -> bool {
match expr {
Expr::GlobalGet(_) => true,
Expand All @@ -286,9 +288,9 @@ fn is_global_object_expr(expr: &Expr) -> bool {
}
}

pub(crate) fn try_static_class_name<'a>(callee: &'a Expr, ctx: &FnCtx<'_>) -> Option<&'a str> {
pub(crate) fn try_static_class_name<'a>(callee: &'a Expr, ctx: &FnCtx<'_>) -> Option<Cow<'a, str>> {
match callee {
Expr::ClassRef(name) => Some(name.as_str()),
Expr::ClassRef(name) => Some(Cow::Borrowed(name.as_str())),
// Refs #486: `new _X()` where `_X` is the inner self-binding name of
// a class expression (e.g. `var X = class _X { ... new _X() ... }`)
// lowers to `NewDynamic { callee: ExternFuncRef("_X") }` because the
Expand All @@ -299,12 +301,14 @@ pub(crate) fn try_static_class_name<'a>(callee: &'a Expr, ctx: &FnCtx<'_>) -> Op
// compile_module entry. Without this, the call falls through to the
// empty-object placeholder path with class_id=0 and method dispatch
// breaks on the resulting instance.
Expr::ExternFuncRef { name, .. } if ctx.class_ids.contains_key(name) => Some(name.as_str()),
Expr::ExternFuncRef { name, .. } if ctx.class_ids.contains_key(name) => {
Some(Cow::Borrowed(name.as_str()))
}
Expr::PropertyGet {
object, property, ..
} => {
if is_global_object_expr(object.as_ref()) {
return Some(property.as_str());
return Some(Cow::Borrowed(property.as_str()));
}
// Namespace import: `import * as ns from 'm'; new ns.Foo()`.
// The namespace object surfaces either as `LocalGet(id)` (mapped to
Expand All @@ -325,16 +329,22 @@ pub(crate) fn try_static_class_name<'a>(callee: &'a Expr, ctx: &FnCtx<'_>) -> Op
// member as a closure value and run it via
// `js_new_function_construct` (which also intercepts the builtin
// global thunks, so re-exported builtins keep working).
let object_is_namespace = match object.as_ref() {
let namespace = match object.as_ref() {
Expr::LocalGet(id) => ctx
.local_id_to_name
.get(id)
.is_some_and(|name| ctx.namespace_imports.contains(name)),
Expr::ExternFuncRef { name, .. } => ctx.namespace_imports.contains(name),
_ => false,
.filter(|name| ctx.namespace_imports.contains(*name))
.map(String::as_str),
Expr::ExternFuncRef { name, .. } if ctx.namespace_imports.contains(name) => {
Some(name.as_str())
}
_ => None,
};
if object_is_namespace && ctx.classes.contains_key(property.as_str()) {
return Some(property.as_str());
if let Some(namespace) = namespace {
let class_name = crate::namespace_member_class_key(namespace, property);
if ctx.classes.contains_key(&class_name) {
return Some(Cow::Owned(class_name));
}
}
None
}
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/type_analysis/predicates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ pub(crate) fn static_type_of(ctx: &FnCtx<'_>, e: &Expr) -> Option<HirType> {
return Some(HirType::String);
}
if let Some(static_method_ty) = crate::expr::try_static_class_name(object, ctx)
.and_then(|class_name| ctx.classes.get(class_name))
.and_then(|class_name| ctx.classes.get(class_name.as_ref()))
.and_then(|class| {
class
.static_methods
Expand Down
29 changes: 27 additions & 2 deletions crates/perry/src/commands/compile/run_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3826,7 +3826,20 @@ pub fn run_with_parse_cache(
export_name,
));
}
if let Some(class) = exported_classes.get(&key) {
// #9285: re-export renames keep the class
// metadata under its origin name (`Child`), not
// the namespace-visible alias (`PublicChild`).
// The var path above already consults this
// origin key; class registration must do the
// same or `new ns.PublicChild(arg)` falls
// through to a function wrapper and never runs
// the class constructor.
let imported_class = exported_classes.get(&key).or_else(|| {
origin_key_under_origin_name
.as_ref()
.and_then(|origin_key| exported_classes.get(origin_key))
});
if let Some(class) = imported_class {
let class_prefix = canonical_class_source_prefix(
class,
&class_canonical_path,
Expand Down Expand Up @@ -4310,7 +4323,19 @@ pub fn run_with_parse_cache(
),
);
}
if let Some(class) = exported_classes.get(&key) {
// #9285: a renamed class re-export is
// indexed under its origin name, while this
// namespace exposes `export_name`. Resolve
// through the same origin key used for the
// var classification above so the scoped
// ImportedClass retains both identities.
let imported_class =
exported_classes.get(&key).or_else(|| {
origin_key_under_origin_name.as_ref().and_then(
|origin_key| exported_classes.get(origin_key),
)
});
if let Some(class) = imported_class {
let class_prefix = canonical_class_source_prefix(
class,
&class_canonical_path,
Expand Down
Loading