Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Compilation/CompilationContext.Functions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,37 @@ public string ResolveFunctionName(string simpleFunctionName)
return simpleFunctionName;
}

/// <summary>
/// Resolves a bare identifier to a namespace's static backing field. Namespace fields are keyed
/// by full dotted path (e.g. <c>O.A</c>), so a nested/enclosing namespace referenced by its
/// simple name from inside a namespace member body — <c>A</c> inside <c>O.B.f</c>, where
/// <c>A</c> is <c>O.A</c> — does not match a direct lookup. Walks the current namespace path
/// innermost → outermost (<c>O.B.A</c>, then <c>O.A</c>) and returns the first existing field,
/// mirroring <see cref="ResolveFunctionName"/>'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.
/// </summary>
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;
}

/// <summary>
/// Gets the qualified function name for the current module + namespace context. Module
/// qualification (#418) keeps same-named functions in different modules distinct; namespace
Expand Down
5 changes: 4 additions & 1 deletion Compilation/ExpressionEmitterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 4 additions & 2 deletions Compilation/ILEmitter.Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
116 changes: 116 additions & 0 deletions SharpTS.Tests/SharedTests/NamespaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
21 changes: 8 additions & 13 deletions TypeSystem/TypeChecker.Compatibility.Structural.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>`) 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;
Expand Down
7 changes: 7 additions & 0 deletions TypeSystem/TypeChecker.Namespaces.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }`).
Expand Down
Loading