From 28a9a53804004ab9d028ee560b7b95248d120b69 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Thu, 18 Jun 2026 01:03:47 -0700 Subject: [PATCH] Fix #746: nested namespace shadowing same-named top-level namespace throws in interpreter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ExecuteNamespace called _environment.GetNamespace(name), which walks up the full scope chain. When a nested `O.A` was declared and the global scope already held a top-level `namespace A`, GetNamespace found the global one and entered the merge path — skipping the DefineNamespace call that would have planted "A" in namespaceEnvO._values. The VariableResolver had correctly pre-computed distance=2 for the bare `A` reference inside O.B.f (pointing to O's scope), but GetAt(2, "A") returned Undefined because that slot was never written. The subsequent property access on Undefined threw "Only instances and objects have properties". Fix: add GetLocalNamespace (checks _namespaces on the current scope only, no chain walk) and use it in ExecuteNamespace. Same-level declaration merging is unaffected — GetLocalNamespace still finds a previously-defined namespace in the same scope. Compiled mode was already correct via ResolveNamespaceField. --- Execution/Interpreter.Namespaces.cs | 7 +++++-- Runtime/RuntimeEnvironment.cs | 11 +++++++++++ SharpTS.Tests/SharedTests/NamespaceTests.cs | 20 ++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Execution/Interpreter.Namespaces.cs b/Execution/Interpreter.Namespaces.cs index e9bab8e2..462f8eb9 100644 --- a/Execution/Interpreter.Namespaces.cs +++ b/Execution/Interpreter.Namespaces.cs @@ -16,8 +16,11 @@ private ExecutionResult ExecuteNamespace(Stmt.Namespace ns) { string name = ns.Name.Lexeme; - // Get or create namespace object - SharpTSNamespace? existingNs = _environment.GetNamespace(name); + // Get or create namespace object — check ONLY the current scope, not up the chain. + // GetNamespace walks up and would find a same-named outer namespace (e.g. top-level A + // when declaring O.A), incorrectly merging the nested namespace into the outer one and + // leaving the current scope without an "A" binding (#746). + SharpTSNamespace? existingNs = _environment.GetLocalNamespace(name); SharpTSNamespace nsObj; if (existingNs != null) diff --git a/Runtime/RuntimeEnvironment.cs b/Runtime/RuntimeEnvironment.cs index 5d71e3a0..3647be75 100644 --- a/Runtime/RuntimeEnvironment.cs +++ b/Runtime/RuntimeEnvironment.cs @@ -154,6 +154,17 @@ public void DefineNamespace(string name, SharpTSNamespace ns) return Enclosing?.GetNamespace(name); } + /// + /// Gets a namespace by name from THIS scope only (no chain traversal). + /// Use when deciding whether to merge vs. create a new namespace declaration — + /// avoids treating a same-named namespace in an enclosing scope as a merge target (#746). + /// + public SharpTSNamespace? GetLocalNamespace(string name) + { + _namespaces.TryGetValue(name, out var ns); + return ns; + } + /// /// Defines a variable with a boxed value (legacy compatibility). /// Wraps the value in RuntimeValue.FromBoxed automatically. diff --git a/SharpTS.Tests/SharedTests/NamespaceTests.cs b/SharpTS.Tests/SharedTests/NamespaceTests.cs index 42fae33a..1a23b77f 100644 --- a/SharpTS.Tests/SharedTests/NamespaceTests.cs +++ b/SharpTS.Tests/SharedTests/NamespaceTests.cs @@ -796,5 +796,25 @@ export namespace I { Assert.Equal("2\n", TestHarness.Run(code, mode)); } + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void NestedNamespace_ShadowsSameNamedTopLevelNamespace(ExecutionMode mode) + { + // Nested O.A shadows the top-level A for bare references from sibling O.B. + // The interpreter previously merged O.A's members into the global A and left + // namespaceEnvO without an "A" binding, so the pre-resolved distance-2 lookup + // returned Undefined → "Only instances and objects have properties" (#746). + var code = @" + namespace A { export function g() { return 100; } } + namespace O { + export namespace A { export function g() { return 5; } } + export namespace B { export function f() { return A.g(); } } + } + console.log(O.B.f()); + console.log(A.g()); + "; + Assert.Equal("5\n100\n", TestHarness.Run(code, mode)); + } + #endregion }