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 }`).