From a0c7effa88c556ca92aea83581ee097de6edbf93 Mon Sep 17 00:00:00 2001 From: Kevin Thompson Date: Thu, 30 May 2024 18:16:56 -0700 Subject: [PATCH] feat(binder): add new simplified binder --- src/Panther/CodeAnalysis/Binder/Binder.cs | 128 +++++++++++ src/Panther/CodeAnalysis/Binder/Symbol.cs | 67 ++++++ .../CodeAnalysis/Binder/SymbolFlags.cs | 24 ++ src/Panther/CodeAnalysis/Compilation.cs | 18 +- src/Panther/CodeAnalysis/DiagnosticBag.cs | 10 + .../CodeAnalysis/Symbols/SymbolPrinter.cs | 38 ++++ src/Panther/CodeAnalysis/Syntax/SyntaxKind.cs | 2 + src/Panther/Repl/PantherRepl.cs | 14 +- .../CodeAnalysis/Binder/BinderTests.cs | 208 ++++++++++++++++++ .../CodeAnalysis/Binder/SymbolEnumerator.cs | 52 +++++ .../CodeAnalysis/Binder/SymbolTests.cs | 91 ++++++++ tests/Panther.Tests/Panther.Tests.csproj | 1 + 12 files changed, 640 insertions(+), 13 deletions(-) create mode 100644 src/Panther/CodeAnalysis/Binder/Binder.cs create mode 100644 src/Panther/CodeAnalysis/Binder/Symbol.cs create mode 100644 src/Panther/CodeAnalysis/Binder/SymbolFlags.cs create mode 100644 tests/Panther.Tests/CodeAnalysis/Binder/BinderTests.cs create mode 100644 tests/Panther.Tests/CodeAnalysis/Binder/SymbolEnumerator.cs create mode 100644 tests/Panther.Tests/CodeAnalysis/Binder/SymbolTests.cs diff --git a/src/Panther/CodeAnalysis/Binder/Binder.cs b/src/Panther/CodeAnalysis/Binder/Binder.cs new file mode 100644 index 0000000..c4758e0 --- /dev/null +++ b/src/Panther/CodeAnalysis/Binder/Binder.cs @@ -0,0 +1,128 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Panther.CodeAnalysis.Syntax; +using Panther.CodeAnalysis.Text; + +namespace Panther.CodeAnalysis.Binder; + +public class Binder : SyntaxVisitor +{ + private readonly DiagnosticBag _diagnostics; + private Symbol _symbolTable; + + private Binder(Symbol symbolTable, DiagnosticBag diagnostics) + { + _diagnostics = diagnostics; + _symbolTable = symbolTable; + } + + public static (ImmutableArray diagnostics, Symbol symbolTable) Bind( + ImmutableArray syntaxTrees + ) => Bind(Symbol.NewRoot(), syntaxTrees); + + public static (ImmutableArray diagnostics, Symbol symbolTable) Bind( + Symbol symbolTable, + ImmutableArray syntaxTrees + ) + { + var diagnostics = new DiagnosticBag(); + + foreach (var tree in syntaxTrees) + { + var binder = new Binder(symbolTable, diagnostics); + binder.Visit(tree.Root); + } + + return (diagnostics.ToImmutableArray(), symbolTable); + } + + protected override void DefaultVisit(SyntaxNode node) + { + foreach ( + var child in node.GetChildren().Where(child => child.Kind > SyntaxKind.ExpressionMarker) + ) + Visit(child); + } + + private void VisitContainer(SyntaxNode node, SymbolFlags symbolFlags, SyntaxToken identifier) + { + VisitContainer(node, symbolFlags, identifier.Text, identifier.Location); + } + + private void VisitContainer( + SyntaxNode node, + SymbolFlags symbolFlags, + string name, + TextLocation location + ) + { + var (scope, existing) = _symbolTable.DeclareSymbol(name, symbolFlags, location); + if (existing) + { + _diagnostics.ReportDuplicateSymbol(name, scope.Location, location); + } + + var current = _symbolTable; + + _symbolTable = scope; + + this.DefaultVisit(node); + + _symbolTable = current; + } + + private void DeclareSymbol(SyntaxToken identifier, SymbolFlags symbolFlags) + { + var name = identifier.Text; + var location = identifier.Location; + var existing = _symbolTable.Lookup(name); + if (existing is not null) + { + _diagnostics.ReportDuplicateSymbol(name, existing.Location, location); + } + + _symbolTable.DeclareSymbol(name, symbolFlags, location); + } + + public override void VisitNamespaceDeclaration(NamespaceDeclarationSyntax node) + { + VisitContainer(node, SymbolFlags.Namespace, node.Name.ToText(), node.Name.Location); + } + + public override void VisitClassDeclaration(ClassDeclarationSyntax node) + { + VisitContainer(node, SymbolFlags.Class, node.Identifier); + } + + public override void VisitObjectDeclaration(ObjectDeclarationSyntax node) + { + VisitContainer(node, SymbolFlags.Object, node.Identifier); + } + + public override void VisitFunctionDeclaration(FunctionDeclarationSyntax node) + { + VisitContainer(node, SymbolFlags.Method, node.Identifier); + } + + public override void VisitParameter(ParameterSyntax node) + { + var flags = _symbolTable.Flags.HasFlag(SymbolFlags.Class) + ? SymbolFlags.Field + : SymbolFlags.Parameter; + + DeclareSymbol(node.Identifier, flags); + } + + public override void VisitVariableDeclarationStatement(VariableDeclarationStatementSyntax node) + { + var flags = + _symbolTable.Flags.HasFlag(SymbolFlags.Class) + || _symbolTable.Flags.HasFlag(SymbolFlags.Object) + ? SymbolFlags.Field + : SymbolFlags.Local; + + // TODO: need a way to deal with block scope for variables (or maybe we just don't have block scope for now) + DeclareSymbol(node.IdentifierToken, flags); + } +} diff --git a/src/Panther/CodeAnalysis/Binder/Symbol.cs b/src/Panther/CodeAnalysis/Binder/Symbol.cs new file mode 100644 index 0000000..33d0ae2 --- /dev/null +++ b/src/Panther/CodeAnalysis/Binder/Symbol.cs @@ -0,0 +1,67 @@ +using System.Collections; +using System.Collections.Generic; +using Panther.CodeAnalysis.Text; + +namespace Panther.CodeAnalysis.Binder; + +public class Symbol(string name, SymbolFlags flags, TextLocation location, Symbol? parent) + : IEnumerable +{ + private Dictionary? _symbols; + private List? _symbolList; + + public static Symbol NewRoot() => new("", SymbolFlags.None, TextLocation.None, null); + + public string Name => name; + public SymbolFlags Flags => flags; + public TextLocation Location => location; + public Symbol? Parent => parent; + + public string FullName + { + get + { + var parentName = Parent?.FullName; + return string.IsNullOrEmpty(parentName) ? Name : $"{parentName}.{Name}"; + } + } + + + public (Symbol, bool existing) DeclareClass(string name, TextLocation location) => + DeclareSymbol(name, SymbolFlags.Class, location); + + public (Symbol, bool existing) DeclareField(string name, TextLocation location) => + DeclareSymbol(name, SymbolFlags.Field, location); + + public (Symbol, bool existing) DeclareMethod(string name, TextLocation location) => + DeclareSymbol(name, SymbolFlags.Method, location); + + public (Symbol, bool existing) DeclareSymbol(string name, SymbolFlags flags, TextLocation location) + { + _symbols ??= new(); + _symbolList ??= new(); + var symbol = new Symbol(name, flags, location, this); + var existing = !_symbols.TryAdd(name, symbol); + + if(!existing) _symbolList.Add(symbol); + + return (existing ? _symbols[name] : symbol, existing); + } + + public Symbol? Lookup(string name, bool includeParents = true) => + _symbols?.GetValueOrDefault(name) ?? this.Parent?.Lookup(name, includeParents); + + public IEnumerator GetEnumerator() + { + if(_symbolList == null) + yield break; + + foreach (var symbol in _symbolList) + yield return symbol; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/Panther/CodeAnalysis/Binder/SymbolFlags.cs b/src/Panther/CodeAnalysis/Binder/SymbolFlags.cs new file mode 100644 index 0000000..1322d2a --- /dev/null +++ b/src/Panther/CodeAnalysis/Binder/SymbolFlags.cs @@ -0,0 +1,24 @@ +using System; + +namespace Panther.CodeAnalysis.Binder; + +[Flags] +public enum SymbolFlags +{ + None = 0, + + // Symbol type + Namespace = 1, + Object = 1 << 1, + Class = 1 << 2, + Method = 1 << 3, + Field = 1 << 4, + Property = 1 << 5, + Parameter = 1 << 6, + Local = 1 << 7, + + // Symbol Access + Private = None, // may not use this but should eventually be the default access level + Protected = 1 << 8, + Public = 1 << 9, +} diff --git a/src/Panther/CodeAnalysis/Compilation.cs b/src/Panther/CodeAnalysis/Compilation.cs index b211b62..95f3a03 100644 --- a/src/Panther/CodeAnalysis/Compilation.cs +++ b/src/Panther/CodeAnalysis/Compilation.cs @@ -98,23 +98,23 @@ public static Compilation CreateScript( params SyntaxTree[] syntaxTrees ) => new Compilation(references, isScript: true, previous, syntaxTrees); - public IEnumerable GetSymbols() + public IEnumerable GetSymbols() { var compilation = this; var symbolNames = new HashSet(); while (compilation != null) { - foreach ( - var type in compilation.RootSymbol.Types.Where(type => symbolNames.Add(type.Name)) - ) + var (_, symbolTable) = Binder.Binder.Bind(compilation.SyntaxTrees); + + foreach (var type in symbolTable.Where(type => symbolNames.Add(type.FullName))) { yield return type; - - foreach (var member in type.Members) - { - yield return member; - } + // + // foreach (var member in type.Members) + // { + // yield return member; + // } } compilation = compilation.Previous; diff --git a/src/Panther/CodeAnalysis/DiagnosticBag.cs b/src/Panther/CodeAnalysis/DiagnosticBag.cs index 0436639..c7202a3 100644 --- a/src/Panther/CodeAnalysis/DiagnosticBag.cs +++ b/src/Panther/CodeAnalysis/DiagnosticBag.cs @@ -289,4 +289,14 @@ public void ReportGenericTypeNotSupported(TextLocation location, string typeName public void ReportNoThisInScope(TextLocation location, string scopeName) => Report(location, $"`this` keyword not valid in {scopeName} scope"); + + public void ReportDuplicateSymbol( + string name, + TextLocation existing, + TextLocation newIdentifier + ) + { + Report(newIdentifier, $"A symbol with the name '{name}' already exists in this scope"); + Report(existing, $"A symbol with the name '{name}' already exists in this scope"); + } } diff --git a/src/Panther/CodeAnalysis/Symbols/SymbolPrinter.cs b/src/Panther/CodeAnalysis/Symbols/SymbolPrinter.cs index ce961d4..ebd5e39 100644 --- a/src/Panther/CodeAnalysis/Symbols/SymbolPrinter.cs +++ b/src/Panther/CodeAnalysis/Symbols/SymbolPrinter.cs @@ -63,6 +63,44 @@ public static void WriteTo(this Type symbol, TextWriter writer) } } + public static void WriteTo(this Binder.Symbol symbol, TextWriter writer) + { + if (symbol.Flags.HasFlag(Binder.SymbolFlags.Method)) + { + writer.WriteKeyword("def "); + writer.WriteIdentifier(symbol.Name); + writer.WritePunctuation("("); + using var enumerator = symbol.GetEnumerator(); + if (enumerator.MoveNext()) + { + enumerator.Current.WriteTo(writer); + + while (enumerator.MoveNext()) + { + writer.WritePunctuation(", "); + enumerator.Current.WriteTo(writer); + } + } + writer.WritePunctuation(")"); + } + else if ( + symbol.Flags.HasFlag(Binder.SymbolFlags.Field) + || symbol.Flags.HasFlag(Binder.SymbolFlags.Local) + ) + { + writer.WriteKeyword("val "); + writer.WriteIdentifier(symbol.Name); + } + else if (symbol.Flags.HasFlag(Binder.SymbolFlags.Parameter)) + { + writer.WriteIdentifier(symbol.Name); + } + else + { + throw new NotImplementedException(); + } + } + public static void WriteTo(this Symbol symbol, TextWriter writer) { switch (symbol) diff --git a/src/Panther/CodeAnalysis/Syntax/SyntaxKind.cs b/src/Panther/CodeAnalysis/Syntax/SyntaxKind.cs index ce0910e..55b05ae 100644 --- a/src/Panther/CodeAnalysis/Syntax/SyntaxKind.cs +++ b/src/Panther/CodeAnalysis/Syntax/SyntaxKind.cs @@ -116,6 +116,8 @@ public enum SyntaxKind CloseBracketToken, // Expressions + ExpressionMarker, // marker for SyntaxKinds that are more complicated than tokens and keywords + ArrayCreationExpression, AssignmentExpression, BinaryExpression, diff --git a/src/Panther/Repl/PantherRepl.cs b/src/Panther/Repl/PantherRepl.cs index 387ff95..5e506a5 100644 --- a/src/Panther/Repl/PantherRepl.cs +++ b/src/Panther/Repl/PantherRepl.cs @@ -6,9 +6,11 @@ using Mono.Cecil; using Panther.CodeAnalysis; using Panther.CodeAnalysis.Authoring; +using Panther.CodeAnalysis.Symbols; using Panther.CodeAnalysis.Syntax; using Panther.CodeAnalysis.Text; using Panther.IO; +using SymbolFlags = Panther.CodeAnalysis.Binder.SymbolFlags; namespace Panther.Repl; @@ -150,7 +152,11 @@ private void MetaDumpSymbols() if (_previous == null) return; - foreach (var symbol in _previous.GetSymbols()) + foreach ( + var symbol in _previous + .GetSymbols() + .Where(sym => !sym.Flags.HasFlag(SymbolFlags.Parameter)) + ) { symbol.WriteTo(Console.Out); Console.WriteLine(); @@ -166,8 +172,8 @@ private void MetaDumpFunction(string functionName) var function = _previous .GetSymbols() - .Where(m => m.IsMethod) - .FirstOrDefault(func => func.Name == functionName); + .Where(m => m.Flags.HasFlag(SymbolFlags.Method)) + .FirstOrDefault(func => func.FullName == functionName); if (function == null) { @@ -175,7 +181,7 @@ private void MetaDumpFunction(string functionName) return; } - _previous.EmitTree(function, Console.Out); + // _previous.EmitTree(function, Console.Out); } protected override bool IsCompleteSubmission(string text) diff --git a/tests/Panther.Tests/CodeAnalysis/Binder/BinderTests.cs b/tests/Panther.Tests/CodeAnalysis/Binder/BinderTests.cs new file mode 100644 index 0000000..96e57fb --- /dev/null +++ b/tests/Panther.Tests/CodeAnalysis/Binder/BinderTests.cs @@ -0,0 +1,208 @@ +using System.Collections.Immutable; +using Panther.CodeAnalysis.Binder; +using Panther.CodeAnalysis.Syntax; +using Xunit; + +namespace Panther.Tests.CodeAnalysis.Binder; + +public class BinderTests +{ + [Fact] + public void EnumerateSymbolsInRoot() + { + var code = """ + object SomeObject { + def method() = "taco" + } + """; + + using var enumerator = EnumerateCodeSymbols(code); + enumerator.AssertSymbol(SymbolFlags.Object, "SomeObject"); + enumerator.AssertSymbol(SymbolFlags.Method, "method"); + } + + [Fact] + public void EnumerateSymbolsInNestedObject() + { + var code = """ + object SomeObject { + object Nested { + val field = "taco" + } + } + """; + + using var enumerator = EnumerateCodeSymbols(code); + enumerator.AssertSymbol(SymbolFlags.Object, "SomeObject"); + enumerator.AssertSymbol(SymbolFlags.Object, "Nested"); + enumerator.AssertSymbol(SymbolFlags.Field, "field"); + } + + [Fact] + public void EnumerateSymbolsInNestedObjectMethod() + { + var code = """ + object SomeObject { + object Nested { + def method() = "taco" + } + } + """; + + using var enumerator = EnumerateCodeSymbols(code); + enumerator.AssertSymbol(SymbolFlags.Object, "SomeObject"); + enumerator.AssertSymbol(SymbolFlags.Object, "Nested"); + enumerator.AssertSymbol(SymbolFlags.Method, "method"); + } + + [Fact] + public void EnumerateSymbolsInNestedObjectMethodWithParameter() + { + var code = """ + object SomeObject { + object Nested { + def method(x: Int) = "taco" + } + } + """; + + using var enumerator = EnumerateCodeSymbols(code); + enumerator.AssertSymbol(SymbolFlags.Object, "SomeObject"); + enumerator.AssertSymbol(SymbolFlags.Object, "Nested"); + enumerator.AssertSymbol(SymbolFlags.Method, "method"); + enumerator.AssertSymbol(SymbolFlags.Parameter, "x"); + } + + [Fact] + public void EnumerateSymbolsInNestedObjectMethodWithMultipleParameters() + { + var code = """ + object SomeObject { + object Nested { + def method(x: Int, y: String) = "taco" + } + } + """; + + using var enumerator = EnumerateCodeSymbols(code); + enumerator.AssertSymbol(SymbolFlags.Object, "SomeObject"); + enumerator.AssertSymbol(SymbolFlags.Object, "Nested"); + enumerator.AssertSymbol(SymbolFlags.Method, "method"); + enumerator.AssertSymbol(SymbolFlags.Parameter, "x"); + enumerator.AssertSymbol(SymbolFlags.Parameter, "y"); + } + + [Fact] + public void EnumerateSymbolsInNestedObjectMethodWithReturnType() + { + var code = """ + object SomeObject { + object Nested { + def method(): String = "taco" + } + } + """; + + using var enumerator = EnumerateCodeSymbols(code); + enumerator.AssertSymbol(SymbolFlags.Object, "SomeObject"); + enumerator.AssertSymbol(SymbolFlags.Object, "Nested"); + enumerator.AssertSymbol(SymbolFlags.Method, "method"); + } + + [Fact] + public void EnumerateSymbolsInClasses() + { + var code = """ + class Point(X: int, Y: int) + + class Extent(xmin: int, xmax: int, ymin: int, ymax: int) + { + def width(): int = xmax - xmin + def height(): int = ymax - ymin + } + """; + + using var enumerator = EnumerateCodeSymbols(code); + enumerator.AssertSymbol(SymbolFlags.Class, "Point"); + enumerator.AssertSymbol(SymbolFlags.Field, "X"); + enumerator.AssertSymbol(SymbolFlags.Field, "Y"); + enumerator.AssertSymbol(SymbolFlags.Class, "Extent"); + enumerator.AssertSymbol(SymbolFlags.Field, "xmin"); + enumerator.AssertSymbol(SymbolFlags.Field, "xmax"); + enumerator.AssertSymbol(SymbolFlags.Field, "ymin"); + enumerator.AssertSymbol(SymbolFlags.Field, "ymax"); + enumerator.AssertSymbol(SymbolFlags.Method, "width"); + enumerator.AssertSymbol(SymbolFlags.Method, "height"); + } + + [Fact] + public void EnumerateSymbolsInNestedClasses() + { + var code = """ + class Point(X: int, Y: int) + { + class Nested() { + val field = "taco" + } + } + """; + + using var enumerator = EnumerateCodeSymbols(code); + enumerator.AssertSymbol(SymbolFlags.Class, "Point"); + enumerator.AssertSymbol(SymbolFlags.Field, "X"); + enumerator.AssertSymbol(SymbolFlags.Field, "Y"); + enumerator.AssertSymbol(SymbolFlags.Class, "Nested"); + enumerator.AssertSymbol(SymbolFlags.Field, "field"); + } + + [Fact] + public void EnumerateSymbolsInNestedClassesWithMethods() + { + var code = """ + class Point(X: int, Y: int) + { + class Nested() { + def method() = "taco" + } + } + """; + + using var enumerator = EnumerateCodeSymbols(code); + enumerator.AssertSymbol(SymbolFlags.Class, "Point"); + enumerator.AssertSymbol(SymbolFlags.Field, "X"); + enumerator.AssertSymbol(SymbolFlags.Field, "Y"); + enumerator.AssertSymbol(SymbolFlags.Class, "Nested"); + enumerator.AssertSymbol(SymbolFlags.Method, "method"); + } + + [Fact] + public void EnumerateSymbolsInNamespace() + { + var code = """ + namespace HelloNamespace + + object Hello { + def main() = println("Hello World") + } + """; + + using var enumerator = EnumerateCodeSymbols(code); + enumerator.AssertSymbol(SymbolFlags.Namespace, "HelloNamespace"); + enumerator.AssertSymbol(SymbolFlags.Object, "Hello"); + enumerator.AssertSymbol(SymbolFlags.Method, "main"); + } + + private static SymbolEnumerator EnumerateCodeSymbols(string code) + { + var global = Symbol.NewRoot(); + var tree = SyntaxTree.Parse(code); + Assert.Empty(tree.Diagnostics); + var (binderDiags, _) = Panther.CodeAnalysis.Binder.Binder.Bind( + global, + new[] { tree }.ToImmutableArray() + ); + Assert.Empty(binderDiags); + + return new SymbolEnumerator(global); + } +} diff --git a/tests/Panther.Tests/CodeAnalysis/Binder/SymbolEnumerator.cs b/tests/Panther.Tests/CodeAnalysis/Binder/SymbolEnumerator.cs new file mode 100644 index 0000000..3b67069 --- /dev/null +++ b/tests/Panther.Tests/CodeAnalysis/Binder/SymbolEnumerator.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using Panther.CodeAnalysis.Binder; +using Xunit; + +namespace Panther.Tests.CodeAnalysis.Binder; + +class SymbolEnumerator(Symbol root) : IDisposable +{ + private readonly IEnumerator _enumerator = EnumerateSymbols(root, false).GetEnumerator(); + + private bool _hasErrors; + + public Symbol AssertSymbol(SymbolFlags flags, string name) + { + try + { + Assert.True(_enumerator.MoveNext()); + var current = _enumerator.Current; + Assert.NotNull(current); + Assert.Equal(flags, current.Flags); + Assert.Equal(name, current.Name); + return current; + } + catch + { + _hasErrors = true; + throw; + } + } + + public void Dispose() + { + if (!_hasErrors) + { + Assert.False(_enumerator.MoveNext(), $"Additional symbols remain: {_enumerator.Current.Name}"); + } + _enumerator.Dispose(); + } + + private static IEnumerable EnumerateSymbols(Symbol symbol, bool includeRoot = true) + { + if (includeRoot) + yield return symbol; + + foreach (var child in symbol) + foreach (var s in EnumerateSymbols(child)) + { + yield return s; + } + } +} \ No newline at end of file diff --git a/tests/Panther.Tests/CodeAnalysis/Binder/SymbolTests.cs b/tests/Panther.Tests/CodeAnalysis/Binder/SymbolTests.cs new file mode 100644 index 0000000..eb830cf --- /dev/null +++ b/tests/Panther.Tests/CodeAnalysis/Binder/SymbolTests.cs @@ -0,0 +1,91 @@ +using System.Collections.Immutable; +using Panther.CodeAnalysis.Syntax; +using Panther.CodeAnalysis.Text; +using Shouldly; +using Xunit; +using Symbol = Panther.CodeAnalysis.Binder.Symbol; + +namespace Panther.Tests.CodeAnalysis.Binder; + +public class SymbolTests +{ + [Fact] + public void LookupInRoot() + { + var global = Symbol.NewRoot(); + + var (x, _) = global.DeclareClass("x", TextLocation.None); + var (y, _) = global.DeclareClass("y", TextLocation.None); + + Assert.Equal(x, global.Lookup("x")); + Assert.Equal(y, global.Lookup("y")); + } + + [Fact] + public void DeclareSymbolInScope() + { + var global = Symbol.NewRoot(); + var (x, _) = global.DeclareClass("x", TextLocation.None); + + var (y, _) = x.DeclareField("y", TextLocation.None); + + y.FullName.ShouldBe("x.y"); + } + + [Fact] + public void LookupInParentScope() + { + var global = Symbol.NewRoot(); + var (x, _) = global.DeclareClass("x", TextLocation.None); + var (y, _) = x.DeclareField("y", TextLocation.None); + + Assert.Equal(x, y.Lookup("x")); + } + + [Fact] + public void LookupInGrandParentScope() + { + var global = Symbol.NewRoot(); + var (x, _) = global.DeclareClass("x", TextLocation.None); + var (y, _) = x.DeclareField("y", TextLocation.None); + var (z, _) = y.DeclareField("z", TextLocation.None); + + Assert.Equal(x, z.Lookup("x")); + } + + [Fact] + public void LookupInSiblingScope() + { + var global = Symbol.NewRoot(); + var (x, _) = global.DeclareClass("x", TextLocation.None); + var (y, _) = global.DeclareClass("y", TextLocation.None); + + Assert.Equal(y, x.Lookup("y")); + } + + [Fact] + public void DeclareSymbolInParentScope() + { + var global = Symbol.NewRoot(); + var (x, _) = global.DeclareClass("x", TextLocation.None); + var (y, _) = x.DeclareField("y", TextLocation.None); + + var (z, _) = global.DeclareClass("z", TextLocation.None); + var (y2, _) = z.DeclareField("y", TextLocation.None); + + y2.FullName.ShouldBe("z.y"); + } + + [Fact] + public void DeclareSymbolInGlobalScope() + { + var global = Symbol.NewRoot(); + var (x, _) = global.DeclareClass("x", TextLocation.None); + var (y, _) = x.DeclareField("y", TextLocation.None); + + var (y2, _) = global.DeclareField("y", TextLocation.None); + + y2.FullName.ShouldBe("y"); + y.FullName.ShouldBe("x.y"); + } +} diff --git a/tests/Panther.Tests/Panther.Tests.csproj b/tests/Panther.Tests/Panther.Tests.csproj index 79c5ea6..635001c 100644 --- a/tests/Panther.Tests/Panther.Tests.csproj +++ b/tests/Panther.Tests/Panther.Tests.csproj @@ -17,6 +17,7 @@ + all