From 475498f7c8a5bfade0e0fc2e0f971dc67f51fa56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 7 Sep 2026 02:32:32 +0200 Subject: [PATCH 1/3] fix(hir): resolve typeof class expression self names --- .../src/lower/lower_expr/arm_unary.rs | 9 +++++++ crates/perry-hir/src/lower/tests.rs | 27 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/crates/perry-hir/src/lower/lower_expr/arm_unary.rs b/crates/perry-hir/src/lower/lower_expr/arm_unary.rs index 848592b200..49d98308eb 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_unary.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_unary.rs @@ -124,6 +124,15 @@ pub(crate) fn lower_unary_expr(ctx: &mut LoweringContext, unary: &ast::UnaryExpr && ctx.lookup_native_module(n).is_none() && ctx.lookup_imported_func(n).is_none() && ctx.lookup_class(n).is_none() + // A named class expression can use a synthetic registry key + // while its source-level inner binding keeps the written + // name. Let ordinary identifier lowering resolve that binding + // to the current class instead of treating it as an optional + // global. For example, `var B = class l { static f() { + // return typeof l } }` registers the class under a generated + // `l__class_expr_*` key, but `l` is still lexically bound in + // the class body. + && ctx.current_class_inner_name.as_deref() != Some(n) && !is_builtin_function(n) && !is_known_global_identifier_name(n) && !matches!(n, "undefined" | "null" | "NaN" | "Infinity") diff --git a/crates/perry-hir/src/lower/tests.rs b/crates/perry-hir/src/lower/tests.rs index fcf1c2943d..946f1264e9 100644 --- a/crates/perry-hir/src/lower/tests.rs +++ b/crates/perry-hir/src/lower/tests.rs @@ -1182,6 +1182,33 @@ fn named_class_expr_static_private_update_in_arrow_keeps_lexical_brand_owner() { ); } +/// A named class expression whose outer binding has a different name uses a +/// synthetic registry key. `typeof` must still resolve the source-level inner +/// name through the class body's lexical binding rather than an optional +/// global lookup. +#[test] +fn typeof_named_class_expr_inner_binding_uses_the_current_class() { + let source = r#" + var B = class l { + static selfType(): string { return typeof l; } + }; + "#; + let module = perry_parser::parse_typescript(source, "t.ts").expect("source parses"); + let hir = super::lower_module(&module, "t", "t.ts").expect("source lowers"); + let method = hir + .classes + .iter() + .flat_map(|class| &class.static_methods) + .find(|method| method.name == "selfType") + .expect("static selfType method is lowered"); + let body = format!("{:#?}", method.body); + + assert!( + body.contains("ClassRef") && !body.contains("js_global_get_optional"), + "the class's inner name must resolve to its synthetic ClassRef: {body}" + ); +} + /// A sibling class declaration is already a known lexical binding while an /// earlier class method is lowered, even though its registry entry is emitted /// later. The unresolved-constructor guard must preserve that forward binding. From fd35c335df75bb63be9e87087ee1838c741ab320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 7 Sep 2026 02:33:09 +0200 Subject: [PATCH 2/3] docs(changelog): note class expression typeof fix --- changelog.d/9931-class-expression-typeof-self.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog.d/9931-class-expression-typeof-self.md diff --git a/changelog.d/9931-class-expression-typeof-self.md b/changelog.d/9931-class-expression-typeof-self.md new file mode 100644 index 0000000000..b08f8f753a --- /dev/null +++ b/changelog.d/9931-class-expression-typeof-self.md @@ -0,0 +1,5 @@ +### Fixed + +- Resolve a named class expression's inner name inside `typeof`, including + when its outer binding has a different name and the compiler registers the + class under a generated key. From de9887c18d5fb34ca9acf49e1cf490c95ebd327b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 7 Sep 2026 02:51:55 +0200 Subject: [PATCH 3/3] docs(changelog): clarify class self binding fix --- changelog.d/9931-class-expression-typeof-self.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.d/9931-class-expression-typeof-self.md b/changelog.d/9931-class-expression-typeof-self.md index b08f8f753a..1a805508fa 100644 --- a/changelog.d/9931-class-expression-typeof-self.md +++ b/changelog.d/9931-class-expression-typeof-self.md @@ -2,4 +2,6 @@ - Resolve a named class expression's inner name inside `typeof`, including when its outer binding has a different name and the compiler registers the - class under a generated key. + class under a generated key. The unresolved-global shortcut now excludes the + active class inner name so lexical lowering can resolve the class binding; + an HIR regression covers that path.