diff --git a/Compilation/CompilationContext.Functions.cs b/Compilation/CompilationContext.Functions.cs
index 96a151ae..3be47b2d 100644
--- a/Compilation/CompilationContext.Functions.cs
+++ b/Compilation/CompilationContext.Functions.cs
@@ -89,6 +89,37 @@ public string ResolveFunctionName(string simpleFunctionName)
return simpleFunctionName;
}
+ ///
+ /// Resolves a bare identifier to a namespace's static backing field. Namespace fields are keyed
+ /// by full dotted path (e.g. O.A), so a nested/enclosing namespace referenced by its
+ /// simple name from inside a namespace member body — A inside O.B.f, where
+ /// A is O.A — does not match a direct lookup. Walks the current namespace path
+ /// innermost → outermost (O.B.A, then O.A) and returns the first existing field,
+ /// mirroring 's namespace-scoped function resolution so a
+ /// nested namespace can reference an enclosing namespace's namespace-typed member (#665). The
+ /// namespace-scoped pass runs first so an inner namespace shadows a same-named top-level one;
+ /// the direct lookup then covers a top-level namespace reference (or any reference outside a
+ /// namespace). Returns null when no namespace matches.
+ ///
+ public FieldBuilder? ResolveNamespaceField(string name)
+ {
+ if (NamespaceFields == null)
+ return null;
+
+ if (CurrentNamespacePath != null)
+ {
+ var parts = CurrentNamespacePath.Split('.');
+ for (int i = parts.Length; i >= 1; i--)
+ {
+ string nsPrefix = string.Join('.', parts.Take(i));
+ if (NamespaceFields.TryGetValue($"{nsPrefix}.{name}", out var scoped))
+ return scoped;
+ }
+ }
+
+ return NamespaceFields.TryGetValue(name, out var direct) ? direct : null;
+ }
+
///
/// Gets the qualified function name for the current module + namespace context. Module
/// qualification (#418) keeps same-named functions in different modules distinct; namespace
diff --git a/Compilation/ExpressionEmitterBase.cs b/Compilation/ExpressionEmitterBase.cs
index 9303e23e..ca9b5865 100644
--- a/Compilation/ExpressionEmitterBase.cs
+++ b/Compilation/ExpressionEmitterBase.cs
@@ -1724,7 +1724,10 @@ protected virtual bool TryEmitGlobalVariable(string name)
return true;
}
- if (Ctx.NamespaceFields?.TryGetValue(name, out var nsField) == true)
+ // ResolveNamespaceField walks enclosing namespace prefixes so a nested namespace's member
+ // body can name a sibling/enclosing namespace by its simple name (#665). Shared with
+ // ILEmitter's sync path so state-machine bodies resolve namespaces identically.
+ if (Ctx.ResolveNamespaceField(name) is { } nsField)
{
IL.Emit(OpCodes.Ldsfld, nsField);
SetStackUnknown();
diff --git a/Compilation/ILEmitter.Expressions.cs b/Compilation/ILEmitter.Expressions.cs
index 86043d11..bebaef25 100644
--- a/Compilation/ILEmitter.Expressions.cs
+++ b/Compilation/ILEmitter.Expressions.cs
@@ -257,8 +257,10 @@ protected override void EmitVariable(Expr.Variable v)
return;
}
- // Check if it's a namespace - load the static field
- if (_ctx.NamespaceFields?.TryGetValue(name, out var nsField) == true)
+ // Check if it's a namespace - load the static field. ResolveNamespaceField walks enclosing
+ // namespace prefixes so a nested namespace's member body can name a sibling/enclosing
+ // namespace by its simple name (#665), not just a top-level namespace by full path.
+ if (_ctx.ResolveNamespaceField(name) is { } nsField)
{
IL.Emit(OpCodes.Ldsfld, nsField);
SetStackUnknown();
diff --git a/SharpTS.Tests/SharedTests/NamespaceTests.cs b/SharpTS.Tests/SharedTests/NamespaceTests.cs
index 9d751c2d..42fae33a 100644
--- a/SharpTS.Tests/SharedTests/NamespaceTests.cs
+++ b/SharpTS.Tests/SharedTests/NamespaceTests.cs
@@ -681,4 +681,120 @@ namespace N { export const x = 5; }
}
#endregion
+
+ #region Nested namespace bare-name reference into enclosing namespace scope (#665)
+
+ // A function declared in a NESTED namespace must resolve a bare reference to a member of its
+ // ENCLOSING namespace — the enclosing members are in lexical scope of the nested bodies (tsc).
+ // The type checker previously rejected this ("Undefined variable") in both modes because a
+ // nested namespace is fully checked during the enclosing namespace's first pass, before its
+ // sibling functions were registered; CheckNamespace now hoists function declarations first.
+
+ [Theory]
+ [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))]
+ public void NestedNamespaceFunction_CallsNonExportedEnclosingFunction(ExecutionMode mode)
+ {
+ // The exact issue repro: O.I.f() reaches O's non-exported helper by bare name.
+ var code = @"
+ namespace O {
+ function outerHelp() { return 7; }
+ export namespace I {
+ export function f() { return outerHelp(); }
+ }
+ }
+ console.log(O.I.f());
+ ";
+ Assert.Equal("7\n", TestHarness.Run(code, mode));
+ }
+
+ [Theory]
+ [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))]
+ public void NestedNamespaceFunction_ForwardReferencesEnclosingFunction(ExecutionMode mode)
+ {
+ // The enclosing helper is declared AFTER the nested namespace — function hoisting makes it
+ // visible regardless of source order.
+ var code = @"
+ namespace O {
+ export namespace I {
+ export function f() { return outerHelp(); }
+ }
+ function outerHelp() { return 7; }
+ }
+ console.log(O.I.f());
+ ";
+ Assert.Equal("7\n", TestHarness.Run(code, mode));
+ }
+
+ [Theory]
+ [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))]
+ public void NestedNamespaceFunction_ReferencesEnclosingClassByBareName(ExecutionMode mode)
+ {
+ var code = @"
+ namespace O {
+ class Helper { val = 42; }
+ export namespace I {
+ export function f() { return new Helper().val; }
+ }
+ }
+ console.log(O.I.f());
+ ";
+ Assert.Equal("42\n", TestHarness.Run(code, mode));
+ }
+
+ [Theory]
+ [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))]
+ public void NestedNamespaceFunction_ReferencesSiblingNamespaceByBareName(ExecutionMode mode)
+ {
+ // B.f names sibling namespace A by its simple name (A is a member of the enclosing O). The
+ // checker already admitted this (namespaces register eagerly); compiled codegen previously
+ // threw at runtime because namespace fields are keyed by full path — ResolveNamespaceField
+ // now walks enclosing prefixes so the bare name reaches O.A.
+ var code = @"
+ namespace O {
+ export namespace A { export function g() { return 5; } }
+ export namespace B { export function f() { return A.g(); } }
+ }
+ console.log(O.B.f());
+ ";
+ Assert.Equal("5\n", TestHarness.Run(code, mode));
+ }
+
+ [Theory]
+ [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))]
+ public void DeeplyNestedNamespaceFunction_ReferencesOutermostFunction(ExecutionMode mode)
+ {
+ var code = @"
+ namespace A {
+ function help() { return 1; }
+ export namespace B {
+ export namespace C {
+ export function f() { return help(); }
+ }
+ }
+ }
+ console.log(A.B.C.f());
+ ";
+ Assert.Equal("1\n", TestHarness.Run(code, mode));
+ }
+
+ [Theory]
+ [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))]
+ public void NestedNamespaceFunction_OwnHelperShadowsEnclosingOfSameName(ExecutionMode mode)
+ {
+ // The nested namespace declares its own `help`; the bare reference resolves to the inner
+ // one (2), shadowing the enclosing namespace's `help` (1).
+ var code = @"
+ namespace O {
+ function help() { return 1; }
+ export namespace I {
+ function help() { return 2; }
+ export function f() { return help(); }
+ }
+ }
+ console.log(O.I.f());
+ ";
+ Assert.Equal("2\n", TestHarness.Run(code, mode));
+ }
+
+ #endregion
}
diff --git a/TypeSystem/TypeChecker.Compatibility.Structural.cs b/TypeSystem/TypeChecker.Compatibility.Structural.cs
index ebad4a73..e94cd3e3 100644
--- a/TypeSystem/TypeChecker.Compatibility.Structural.cs
+++ b/TypeSystem/TypeChecker.Compatibility.Structural.cs
@@ -207,19 +207,14 @@ private void CheckExcessProperties(TypeInfo.Record actual, TypeInfo expected, Ex
// Check fields first, then methods
if (mutableClass.FieldTypes.TryGetValue(name, out var fieldType)) return fieldType;
if (mutableClass.Methods.TryGetValue(name, out var methodType)) return methodType;
- // Check frozen version if available (may have superclass methods)
- if (mutableClass.Frozen is TypeInfo.Class frozen)
- {
- TypeInfo? current = frozen.Superclass;
- while (current != null)
- {
- var fields = GetFieldTypes(current);
- if (fields != null && fields.TryGetValue(name, out var superField)) return superField;
- var methods = GetMethods(current);
- if (methods != null && methods.TryGetValue(name, out var superMethod)) return superMethod;
- current = GetSuperclass(current);
- }
- }
+ // Walk the frozen superclass chain (it may carry inherited members) through the
+ // substitution-aware resolver, so an inherited generic-base member (`value: T` on
+ // `Base`) resolves to its instantiated type rather than the bare `T`. Keeps
+ // this signature-collection-time path consistent with the frozen-instance path above
+ // and the member-set collectors (#639); the unsubstituted GetFieldTypes/GetMethods
+ // walk it replaced returned the bare type parameter through a generic base.
+ if (mutableClass.Frozen is TypeInfo.Class { Superclass: { } superclass })
+ return ResolveClassMemberTypeSubstituted(superclass, name);
}
}
return null;
diff --git a/TypeSystem/TypeChecker.Namespaces.cs b/TypeSystem/TypeChecker.Namespaces.cs
index eb31c102..e4418764 100644
--- a/TypeSystem/TypeChecker.Namespaces.cs
+++ b/TypeSystem/TypeChecker.Namespaces.cs
@@ -59,6 +59,13 @@ private void CheckNamespace(Stmt.Namespace ns)
// not a same-named one from an enclosing or earlier sibling scope.
PreRegisterTypeDeclarations(ns.Members);
+ // Hoist function declarations into the namespace scope before the first pass, mirroring
+ // the top-level pass. The first pass fully checks any NESTED namespace (its bodies
+ // resolve here), so a nested member's bare reference to an enclosing-namespace function
+ // (`outerHelp()` from `O.I.f`) must already be bound — and functions are otherwise only
+ // registered in the second pass, after nested namespaces are checked (#665).
+ HoistFunctionDeclarations(ns.Members);
+
// Hoist var declarations (as `any`) before anything resolves, mirroring the top-level
// pass: a member's own annotation/initializer may reference itself or a later var
// (`var a: { foo: typeof a }`, `var a2 = { foo: a2 }`).