diff --git a/Bond.Parser.Tests/ParserFacadeTests.cs b/Bond.Parser.Tests/ParserFacadeTests.cs index a636a24..d81c3c3 100644 --- a/Bond.Parser.Tests/ParserFacadeTests.cs +++ b/Bond.Parser.Tests/ParserFacadeTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Threading.Tasks; using Bond.Parser.Parser; using Bond.Parser.Syntax; @@ -940,5 +941,35 @@ struct User { result.Success.Should().BeTrue(); } + [Fact] + public async Task Alias_IsFileScoped_WhenImportDefinesSameAlias() + { + var input = """ + import "common.bond" + namespace Test + using ID = string; + struct User { 0: required ID id; } + """; + + async Task<(string, string)> Resolver(string _, string importPath) + { + var content = """ + namespace Test + using ID = int32; + struct Other { 0: required ID id; } + """; + return await Task.FromResult((importPath, content)); + } + + var result = await Parse(input, Resolver); + + result.Success.Should().BeTrue(); + var user = result.Ast!.Declarations.OfType().First(d => d.Name == "User"); + var fieldType = user.Fields[0].Type as BondType.UserDefined; + fieldType.Should().NotBeNull(); + var alias = fieldType!.Declaration.Should().BeOfType().Subject; + alias.AliasedType.Should().BeOfType(); + } + #endregion } diff --git a/Bond.Parser/Parser/SemanticAnalyzer.cs b/Bond.Parser/Parser/SemanticAnalyzer.cs index 72ceb63..72c5495 100644 --- a/Bond.Parser/Parser/SemanticAnalyzer.cs +++ b/Bond.Parser/Parser/SemanticAnalyzer.cs @@ -29,17 +29,30 @@ public SemanticAnalyzer(SymbolTable symbolTable, ImportResolver importResolver, /// public async Task AnalyzeAsync(Syntax.Bond bond) { - // Process imports first - foreach (var import in bond.Imports) + _symbolTable.PushAliasScope(); + try { - await ProcessImportAsync(import); - } + // Process imports first + foreach (var import in bond.Imports) + { + await ProcessImportAsync(import); + } - // Add all declarations to symbol table - foreach (var declaration in bond.Declarations) + // Add all declarations to symbol table + foreach (var declaration in bond.Declarations) + { + _symbolTable.AddDeclaration(declaration); + } + + // Validate declarations after all symbols are registered + foreach (var declaration in bond.Declarations) + { + ValidateDeclaration(declaration); + } + } + finally { - _symbolTable.AddDeclaration(declaration, bond.Namespaces); - ValidateDeclaration(declaration); + _symbolTable.PopAliasScope(); } } diff --git a/Bond.Parser/Parser/SymbolTable.cs b/Bond.Parser/Parser/SymbolTable.cs index 67d4b31..9f6f0b9 100644 --- a/Bond.Parser/Parser/SymbolTable.cs +++ b/Bond.Parser/Parser/SymbolTable.cs @@ -10,17 +10,24 @@ namespace Bond.Parser.Parser; /// public class SymbolTable { - private readonly List _declarations = []; + private readonly List _globalDeclarations = []; + private readonly Stack> _aliasScopes = new(); private readonly HashSet _processedImports = []; /// /// Adds a declaration to the symbol table with duplicate checking /// - public void AddDeclaration(Declaration declaration, Namespace[] currentNamespaces) + public void AddDeclaration(Declaration declaration) { + if (declaration is AliasDeclaration alias) + { + AddAliasDeclaration(alias); + return; + } + // Find duplicates in the same namespace - var duplicates = _declarations - .Where(d => d.Name == declaration.Name && d.Namespaces.Any(ns1 => currentNamespaces.Any(ns2 => NamespacesMatch(ns1, ns2)))) + var duplicates = _globalDeclarations + .Where(d => d.Name == declaration.Name && d.Namespaces.Any(ns1 => declaration.Namespaces.Any(ns2 => NamespacesMatch(ns1, ns2)))) .ToList(); foreach (var duplicate in duplicates) @@ -33,18 +40,32 @@ public void AddDeclaration(Declaration declaration, Namespace[] currentNamespace } } - _declarations.Add(declaration); + _globalDeclarations.Add(declaration); } /// /// Finds a symbol by qualified name in the given namespaces /// public Declaration? FindSymbol(string[] qualifiedName, Namespace[] currentNamespaces) + { + var alias = FindAlias(qualifiedName, currentNamespaces); + if (alias != null) + { + return alias; + } + + return FindGlobalSymbol(qualifiedName, currentNamespaces); + } + + /// + /// Finds a symbol by qualified name in the given namespaces from global declarations + /// + private Declaration? FindGlobalSymbol(string[] qualifiedName, Namespace[] currentNamespaces) { if (qualifiedName.Length == 1) { // Unqualified name - search in current namespaces - return _declarations.FirstOrDefault(d => + return _globalDeclarations.FirstOrDefault(d => d.Name == qualifiedName[0] && d.Namespaces.Any(ns1 => currentNamespaces.Any(ns2 => NamespacesMatch(ns1, ns2)))); } @@ -54,7 +75,7 @@ public void AddDeclaration(Declaration declaration, Namespace[] currentNamespace var namespacePart = qualifiedName[..^1]; var namePart = qualifiedName[^1]; - return _declarations.FirstOrDefault(d => + return _globalDeclarations.FirstOrDefault(d => d.Name == namePart && d.Namespaces.Any(ns => ns.Name.SequenceEqual(namespacePart))); } @@ -65,7 +86,7 @@ public void AddDeclaration(Declaration declaration, Namespace[] currentNamespace /// public StructDeclaration? FindStruct(string[] qualifiedName, Namespace[] currentNamespaces) { - var symbol = FindSymbol(qualifiedName, currentNamespaces); + var symbol = FindGlobalSymbol(qualifiedName, currentNamespaces); return symbol as StructDeclaration; } @@ -88,15 +109,87 @@ public void MarkImportProcessed(string canonicalPath) /// /// Gets all declarations /// - public IReadOnlyList Declarations => _declarations.AsReadOnly(); + public IReadOnlyList GlobalDeclarations => _globalDeclarations.AsReadOnly(); + + /// + /// Pushes a new alias scope for a file. + /// + public void PushAliasScope() + { + _aliasScopes.Push([]); + } + + /// + /// Pops the current alias scope. + /// + public void PopAliasScope() + { + if (_aliasScopes.Count == 0) + { + throw new InvalidOperationException("Alias scope stack is empty"); + } + + _aliasScopes.Pop(); + } /// /// Clears all declarations from the symbol table /// - public void Clear() + public void ClearGlobalDeclarations() { - _declarations.Clear(); - // Don't clear processed imports as those are still valid + _globalDeclarations.Clear(); + } + + /// + /// Clears all alias scopes + /// + public void ClearAliasScopes() + { + _aliasScopes.Clear(); + } + + private void AddAliasDeclaration(AliasDeclaration alias) + { + if (_aliasScopes.Count == 0) + { + throw new InvalidOperationException("Alias scope is not initialized"); + } + + var scope = _aliasScopes.Peek(); + var duplicates = scope + .Where(d => d.Name == alias.Name && d.Namespaces.Any(ns1 => alias.Namespaces.Any(ns2 => NamespacesMatch(ns1, ns2)))) + .ToList(); + + if (duplicates.Count > 0) + { + throw new InvalidOperationException( + $"Duplicate declaration: alias '{alias.Name}' was already declared as alias"); + } + + scope.Add(alias); + } + + private AliasDeclaration? FindAlias(string[] qualifiedName, Namespace[] currentNamespaces) + { + if (_aliasScopes.Count == 0) + { + return null; + } + + var scope = _aliasScopes.Peek(); + + if (qualifiedName.Length == 1) + { + return scope.FirstOrDefault(d => + d.Name == qualifiedName[0] && + d.Namespaces.Any(ns1 => currentNamespaces.Any(ns2 => NamespacesMatch(ns1, ns2)))); + } + + var namespacePart = qualifiedName[..^1]; + var namePart = qualifiedName[^1]; + return scope.FirstOrDefault(d => + d.Name == namePart && + d.Namespaces.Any(ns => ns.Name.SequenceEqual(namespacePart))); } private static bool NamespacesMatch(Namespace ns1, Namespace ns2) diff --git a/Bond.Parser/Parser/TypeResolver.cs b/Bond.Parser/Parser/TypeResolver.cs index bcf8de2..7218314 100644 --- a/Bond.Parser/Parser/TypeResolver.cs +++ b/Bond.Parser/Parser/TypeResolver.cs @@ -20,21 +20,23 @@ public Syntax.Bond ResolveTypes(Syntax.Bond ast) const int maxPasses = 10; // Prevent infinite loops // Preserve declarations that came from imports so we can re-add them each pass - var importedDeclarations = symbolTable.Declarations + var importedDeclarations = symbolTable.GlobalDeclarations .Where(d => !ast.Declarations.Contains(d)) .ToArray(); for (int pass = 0; pass < maxPasses; pass++) { // Update symbol table with current declarations - symbolTable.Clear(); + symbolTable.ClearGlobalDeclarations(); + symbolTable.ClearAliasScopes(); + symbolTable.PushAliasScope(); foreach (var importDecl in importedDeclarations) { - symbolTable.AddDeclaration(importDecl, currentAst.Namespaces); + symbolTable.AddDeclaration(importDecl); } foreach (var decl in currentAst.Declarations) { - symbolTable.AddDeclaration(decl, currentAst.Namespaces); + symbolTable.AddDeclaration(decl); } // Resolve all declarations diff --git a/version b/version index abd4105..3a4036f 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.2.4 +0.2.5