Skip to content

fix(hir): emit nested namespaces (#5130) - #5167

Merged
proggeramlug merged 1 commit into
mainfrom
fix/5130-nested-namespace
Jun 15, 2026
Merged

fix(hir): emit nested namespaces (#5130)#5167
proggeramlug merged 1 commit into
mainfrom
fix/5130-nested-namespace

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #5130.

Problem

A nested namespace was not emitted — accessing a member of an inner namespace yielded undefined and then threw. Top-level namespaces worked.

namespace G {
  export const PI = 3.14;
  export function area(r: number) { return PI * r * r; } // works
  export namespace Nested { export const value = 42; }   // was not emitted
}
G.area(2);        // 12.56
G.Nested.value;   // threw: Cannot read properties of undefined  (expected 42)

Root cause

lower_namespace_as_class dropped nested TsModule items — the ExportDecl and Stmt match arms fell through to _ => {} — and the dotted TsNamespaceDecl body returned an empty class.

Fix

Nested namespaces are now lowered recursively as their own synthetic class registered under a qualified Outer.Inner name, and the outer namespace gains a static field Inner holding a ClassRef to it:

  • The nested-namespace names are registered as static fields up front, so Outer.Inner resolves via has_static_fieldStaticFieldGet (returning the ClassRef).
  • Outer.Inner.member is then a runtime property/method access on a class-ref, which already resolves the inner class's static fields and methods.

Handles arbitrary nesting depth, cross-level references (an inner function reading an enclosing namespace's members), and aliasing a nested namespace to a value (const M = Outer.Mid). Both exported and non-exported nested namespaces are routed the same way.

Verification

G.area(2)            => 12.56
G.Nested.value       => 42
G.Nested.f()         => 43
Outer.Mid.Inner.sum() => 111   # deep(1) + m(10) + base(100), cross-level
const M = Outer.Mid; M.Inner.deep => 1

Two new integration tests in crates/perry/tests/issue_5130_nested_namespace.rs (green). perry-hir suite green.

No changelog/version bump per maintainer's release-at-merge workflow.

Summary by CodeRabbit

  • New Features
    • Added support for nested TypeScript namespace declarations with dot-notation access (e.g., Outer.Inner.member).
    • Multi-level namespace nesting is now supported with proper resolution of exported and non-exported members across nested scopes.

A nested namespace was not emitted — accessing a member of an inner
namespace (G.Nested.value) yielded undefined and then threw, while
top-level namespaces worked.

Root cause: lower_namespace_as_class dropped nested TsModule items (the
ExportDecl/Stmt match arms fell through to _ => {}), and the dotted
TsNamespaceDecl body returned an empty class.

Fix: lower nested namespaces recursively as their own synthetic class
registered under a qualified Outer.Inner name, and give the outer
namespace a static field Inner holding a ClassRef to it. Outer.Inner then
resolves to the inner namespace (registered as a static field so
has_static_field routes it to StaticFieldGet) and Outer.Inner.member reads
its statics — a runtime property/method access on a class-ref already
resolves static fields and methods. Works to any nesting depth, across
cross-level references, and when a nested namespace is aliased to a value.
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds support for nested TypeScript namespace declarations in the Perry HIR lowering pass. Two new helper functions extract inner namespace names and lower them into synthetic classes. lower_namespace_as_class is extended with a two-pass approach: first collecting nested namespace names for static registration, then lowering each inner namespace via lower_nested_namespace. A regression test file is added covering simple and deeply nested cases.

Changes

Nested namespace lowering and regression tests

Layer / File(s) Summary
Nested namespace helper functions
crates/perry-hir/src/lower/module_decl.rs
Adds get_nested_ns_name to extract a simple identifier from a TsModule node, and lower_nested_namespace to recursively lower an inner namespace into a synthetic class and produce a read-only static ClassRef field for the outer namespace.
First-pass collection and static registration
crates/perry-hir/src/lower/module_decl.rs
Introduces a nested_ns_names accumulator, extends first-pass match arms to detect both exported and non-exported TsModule items, and passes the collected names to ctx.register_class_statics instead of an empty list.
Second-pass lowering of nested TsModule items
crates/perry-hir/src/lower/module_decl.rs
Adds match arms in the second pass for non-exported and exported nested TsModule items, each calling lower_nested_namespace and inserting the resulting static field into ns_static_fields.
Regression tests
crates/perry/tests/issue_5130_nested_namespace.rs
Adds binary-location helpers and end-to-end compile/run tests: one for G.Nested.value/G.Nested.f() access, and one for Outer.Mid.Inner deep nesting with cross-scope references and namespace aliasing via const M = Outer.Mid.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A namespace within a namespace did hide,
Its members were undefined, cast far aside.
🐇 Two passes now gather, then lower with care,
Synthetic class fields wire Inner right there.
Outer.Mid.Inner.deep — hops without fail,
This rabbit's refactor blazed quite a trail!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(hir): emit nested namespaces (#5130)' is concise, follows the conventional commit format, and clearly identifies the main change as fixing the emission of nested namespaces in the HIR layer.
Description check ✅ Passed The PR description provides comprehensive coverage of the problem, root cause, solution, verification steps, and test additions, though test plan checkboxes are not explicitly marked as completed.
Linked Issues check ✅ Passed The PR successfully addresses all core requirements from issue #5130: enabling nested namespace emission with correct member resolution via static field access and recursive lowering of inner namespaces as synthetic classes.
Out of Scope Changes check ✅ Passed All changes in the PR are directly scoped to issue #5130: modifying the HIR lowering logic for nested namespaces in module_decl.rs and adding integration tests to verify the fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/5130-nested-namespace

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/perry/tests/issue_5130_nested_namespace.rs (1)

58-106: ⚡ Quick win

Add a regression for dotted namespace syntax (namespace A.B {}) to close the remaining gap.

Current tests validate block-nested namespaces, but not dotted declarations. Given TsNamespaceDecl still has a separate lowering path in crates/perry-hir/src/lower/module_decl.rs (Line 2001), a dedicated test here would lock expected behavior (either supported output or explicit unsupported assertion).

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

In `@crates/perry/tests/issue_5130_nested_namespace.rs` around lines 58 - 106, Add
a new test function in the same file following the pattern of
nested_namespace_members_resolve and
deeply_nested_namespaces_and_cross_level_refs to cover dotted namespace syntax
(e.g., namespace A.B.C {}). The test should use compile_and_run to verify either
the expected output behavior when using dotted declarations or provide an
explicit assertion showing the feature is unsupported. This regression test will
lock in the expected behavior for the dotted namespace syntax path referenced in
the TsNamespaceDecl lowering logic, complementing the block-nested namespace
tests already present.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-hir/src/lower/module_decl.rs`:
- Around line 2284-2291: The exported nested namespace handling path (around
line 2284 where lower_nested_namespace is called) is missing a guard for the
declare modifier that exists in the non-exported arm at line 2152. Add a check
to skip the lower_nested_namespace call when ts_module.declare is true, ensuring
that export declare namespace declarations are not lowered to runtime artifacts.
This guard should match the logic in the non-exported arm to prevent type-only
namespace declarations from generating class or static field emissions.

---

Nitpick comments:
In `@crates/perry/tests/issue_5130_nested_namespace.rs`:
- Around line 58-106: Add a new test function in the same file following the
pattern of nested_namespace_members_resolve and
deeply_nested_namespaces_and_cross_level_refs to cover dotted namespace syntax
(e.g., namespace A.B.C {}). The test should use compile_and_run to verify either
the expected output behavior when using dotted declarations or provide an
explicit assertion showing the feature is unsupported. This regression test will
lock in the expected behavior for the dotted namespace syntax path referenced in
the TsNamespaceDecl lowering logic, complementing the block-nested namespace
tests already present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 26499f83-f148-4093-9c28-f9cff874e2ca

📥 Commits

Reviewing files that changed from the base of the PR and between 1ada5d9 and 6b059c9.

📒 Files selected for processing (2)
  • crates/perry-hir/src/lower/module_decl.rs
  • crates/perry/tests/issue_5130_nested_namespace.rs

Comment on lines +2284 to +2291
ast::Decl::TsModule(ts_module) => {
lower_nested_namespace(
ctx,
module,
ns_name,
ts_module,
&mut ns_static_fields,
)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing declare guard emits runtime namespace artifacts for type-only declarations.

At Line 2284, exported nested namespaces are always lowered. Unlike the non-exported arm (Line 2152), this path does not skip ts_module.declare, so export declare namespace ... can incorrectly produce runtime class/static-field emission.

Proposed fix
-                    ast::Decl::TsModule(ts_module) => {
-                        lower_nested_namespace(
-                            ctx,
-                            module,
-                            ns_name,
-                            ts_module,
-                            &mut ns_static_fields,
-                        )?;
-                    }
+                    ast::Decl::TsModule(ts_module) => {
+                        if !ts_module.declare {
+                            lower_nested_namespace(
+                                ctx,
+                                module,
+                                ns_name,
+                                ts_module,
+                                &mut ns_static_fields,
+                            )?;
+                        }
+                    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ast::Decl::TsModule(ts_module) => {
lower_nested_namespace(
ctx,
module,
ns_name,
ts_module,
&mut ns_static_fields,
)?;
ast::Decl::TsModule(ts_module) => {
if !ts_module.declare {
lower_nested_namespace(
ctx,
module,
ns_name,
ts_module,
&mut ns_static_fields,
)?;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-hir/src/lower/module_decl.rs` around lines 2284 - 2291, The
exported nested namespace handling path (around line 2284 where
lower_nested_namespace is called) is missing a guard for the declare modifier
that exists in the non-exported arm at line 2152. Add a check to skip the
lower_nested_namespace call when ts_module.declare is true, ensuring that export
declare namespace declarations are not lowered to runtime artifacts. This guard
should match the logic in the non-exported arm to prevent type-only namespace
declarations from generating class or static field emissions.

@proggeramlug
proggeramlug merged commit 0cff879 into main Jun 15, 2026
15 checks passed
@proggeramlug
proggeramlug deleted the fix/5130-nested-namespace branch June 15, 2026 06:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nested namespace not emitted — inner namespace members are undefined

1 participant