[Nitra] Области видимости и связывание
От: VladD2 Российская Империя www.nemerle.org
Дата: 28.05.15 19:26
Оценка: 10 (2)
В предыдущей теме
Автор: VladD2
Дата: 28.05.15
я рассказал об AST и зависимых свойствах, которые на могут быть объявлены на узлах AST, и на которых можно производить вычисления по AST.

Там использовались объекты типа Scope и Symbol. В данной теме я расскажу о том, что это такое и как ими пользоваться. Типы эти тесно между собой связаны, так что описывать их лучше вместе.

Symbol


Собственно вся типизация заключается в том, чтобы с каждой ссылкой (именем) в программе был сопоставлен символ. Символ хранит исчерпывающую информацию о сущностных программы. Причем эта информация представлена в закончено виде, так что для ее изучения не нужно производить какие-то дополнительные действия. Символы можно сравнить с объектами из System.Reflection описывающими сущности помещенные в сборку дотнета.

В Nitra символы представляются классами на которые накладывается ряд ограничений и для которых будут доступны некоторые сервисы.

Символ может быть создан:
1. На баз деклараций (специализированного AST), т.е. кода.
2. На на основании метаинформации из внешних файлов (например, сборок дотнета и их метаданных).
3. На базе подразумеваемой информации. Например, символ может быть предопределен в языке или создаваться просто потому что где-то упомянут. Примером последнего может быть пространство имен донтета имя которого задано несколькими идентификаторами:
namespace A.B.C { }

Здесь для пространства имен C есть декларация (именно ее тело идет в коне), а для A и B деклараций, но подразумевается, что пространства имен A и B существуют. Это довольно редкий случай, но он вполне реальный.

Для каждой декларации обязан существовать символа, но у одного символа может быть множество деклараций. Примером символов имеющих множественные декларации являются все те же пространства имен в C# и partial-классы C#.

То сколько деклараций может быть у одного экземпляра символа определяется типом символа. Так для пространств имен может быть ноль или более деклараций. У классов одна или более декларация. А у локальных переменных может быть только одна декларация (ноль и более является ошибкой).

Все символы являются наследниками базового типа символов — Symbol2 ("2" добавлено временно, позже мы избавимся от этого суфикса).
Есть несколько предопределенных типов символов которые позволяют упростить работу и используются в базовых версиях функций связывания:

UnresolvedSymbol — используется в случаях, когда связывание окончилось неудачей.

HierarchicalSymbol — символ объявляющий вложенную область видимость, что выражается в наличии у этого типа символов поля Scope типа Scope. Кроме того у этого типа символов есть поле Parent типа HierarchicalSymbol, что описывает иерархию символов.

IAmbiguousSymbol — используется в случаях, когда при связывании было найдено более одного символа. Сейчас это интерфейс, но в дальнейшем это будет (скорее всего) абстрактный символа.

AmbiguousSymbol — реализация IAmbiguousSymbol хранящая список неоднозначностей любого типа.

AmbiguousHierarchicalSymbol — реализация IAmbiguousSymbol хранящая список неоднозначностей типа HierarchicalSymbol.

HierarchicalBaseSymbol — базовый класс для AmbiguousHierarchicalSymbol и HierarchicalSymbol.

Типы символов конкретного языка могут быть унаследованы от одного из этих типов.

Ниже приведена не полная иерархия символов C# и базовых символов Nitra (выделены жирным):
Symbol2 
  HierarchicalBaseSymbol
    HierarchicalSymbol
      NamespaceOrTypeSymbol
        NamespaceSymbol
        TypeSymbol
          ClassSymbol
            TopClassSymbol
            NestedClassSymbol
          StructSymbol
            TopStructSymbol
            NestedStructSymbol
          InterfaceSymbol
            TopInterfaceSymbol
            NestedInterfaceSymbol
          DelegateSymbol
          EnumSymbol
  AliasSymbol


В другом языке иерархия символов может быть иной, но она так же может использовать базовые типы символов Nitra.

Интерфейс Symbol2 выглядит следующим образом:
public          Name          : IReference          { get; }
public abstract FullName      : string              { get; }
public abstract Kind          : string              { get; }
public          Declarations  : list[IDeclaration]  { get; set; default []; }
    
public virtual  SpanClass : string { get { "Default" } }
public virtual IsResolved : bool { get { true } }
    
public virtual TryBind(reference : IReference) : Symbol
{
  UnresolvedSymbol(reference)
}

public Bind(reference : IReference) : Symbol
{
  def sym = TryBind(reference);
  ReportError(reference, sym);
  sym
}    

public override ToString() : string { Name?.Text + " (" + Kind + ")" }


Name — ссылка на AST-имени декларирующего данный символ. В принципе таких имен может быть несколько (если есть несколько деклараций). В Name помещается одно из них. Из Name можно получить текстовое имя символа (короткое).

FullName — для иерархических символов представляет полностью квалифицированное имя символа (включает имена всех родительских символов). Данное свойство может быть переопределено в наследниках и должно представлять его полное имя.

Kind — строка определяющая вид символа. Например, "ambiguous symbol" для AmbiguousSymbol или "namespace" для NamespaceSymbol.

Declarations — список деклараций по которым порожден данный символ. Данные декларации используются IDE для навигации и при рефакторингах. Это обратная связь с кодом. Это поле может быть не заполнено, если символ порожден из внешних метаданных.

SpanClass — определяет стиль которым будет отображаться участки кода с которыми сопоставлены символы данного типа.

IsResolved — позволяет узнать был ли символ корректно связан.

TryBind — функция позволяющая связать имя в контексте данного символа. Данный метод переопределяется в HierarchicalSymbol переадресуя вызов к вложенной области видимости и в AliasSymbol переадресуя связывание символу вычисляемому для поля NamespaceOrType. Метод TryBind не должен выдавать сообщение об ошибке, так как может использоваться в спекулятивных сценариях связывания.

Bind — вызывает TryBind и проверяет его результат выдавая сообщения об ошибке в случае, если связывание закончилось неудачей.

Scope


Scope — это дотнет-тип (точнее вариантный тип Nemerle) описывающий область видимости. С помощью Scope можно порождать новые области видимости или комбинировать уже имеющиеся. Вот его описание:
public variant Scope
{
  | Table
    {
        [RecordIgnore] private _nameTable : Hashtable[int, Symbol];
        
        public DefineSymbol(symbol : Symbol) : void;
        public DefineNestedSymbol(parentSymbol : HierarchicalSymbol, symbol : HierarchicalSymbol) : void;
        public GetOrDefineSymbol(symbol : Symbol) : Symbol;
        public TryGetSymbol(name : IReference) : Symbol;
    }

  | Union  { public Scopes : list[Scope] { get; } }
  | Hide   { public Scope  : Scope       { get; } public Hidden : Scope { get; } }
  | Filter { public Scope  : Scope       { get; } public Predicate : Symbol -> bool { get; } }
  | Nil
    
  public Bind(reference : IReference) : Symbol;
  /// Если не может связать возвращает AmbiguousSymbol или UnresolvedSymbol.
  public TryBind(reference : IReference) : Symbol;
  public BindMany(reference : IReference) : IEnumerable[Symbol];
}


Scope.Table — позволяет создать новую область видимости и "набить" ее символами. Например, Scope.Table используется для объявления области видимости пространства имен или класса.
Scope.Union — позволяет объединить несколько областей видимости в одну. Scope.Union используется, например, для открытия пространств имен using-декларациями.
Scope.Hide — позволяет скрыть символами объявленными в одной области видимости символы объявленные в другом. Используется, например, при объявлении локальных областей видимости.
Scope.Filter — позволяет задать условие фильтрации. Если вложенная область видимости позволяет связать некоторый символ, но Predicate возвращает false для найденного символа, то связывание считается неудачным. Предпологается использовать этого типа для реализации языков наподобие Java. Хотя, возможно, в будущем мы устраним это вхождение.

Реальный пример


Давайте попробуем использовать Scope и Symbol для реализации связывания в C#. Ограничимся связыванием верхней части программы, до тел типов.

Наша реализация сможет связать вот такой пример C#-кода:
using A.B.C;
using XXX = A.B;
using Alias = X.Y.Z;

namespace A.B.C
{
  class C1 : Alias.C2, ITest1 {}
  interface ITest1 {}
}
namespace X.Y.Z
{
  using A.B.C;
  class C2 {}
  class C3 : C1, ITest1 {}
}


Для начала объявим AST описывающий файл C#:
asts CompilationUnit
{
  UsingDirectives : UsingDirective*;
  Members         : NamespaceMember*;
}

Он называется CompilationUnit, так как файл в C# является единицей компиляции. Такое же имя имеет и соответствующее правило грамматики C#.

Я намеренно немного упростил CompilationUnit убрав из него глобальные атрибуты и ExternAlias.

В C# используется многопроходная система типизации. На первом проходе необходимо построить иерархию пространств имен и вложенных в них типов. Она будет нужна на следующем проходе, для связывания имен в using-директивах и списке базовых типов у классов, структур и интерфейсов.

Чтобы выразить иерархическую структуру пространств имен объявим тип символа пространства имен следующим образом:
[Record]
public abstract class NamespaceOrTypeSymbol : HierarchicalSymbol { }
  
[Record]
public class NamespaceSymbol : NamespaceOrTypeSymbol
{
  public static RootNamespace : NamespaceSymbol = NamespaceSymbol(null, null);

  public new Parent : NamespaceSymbol { get { (this : NamespaceOrTypeSymbol).Parent :> NamespaceSymbol } }
    
  public override Kind : string { get { "namespace" } }
  public override SpanClass : string { get { "NitraCSharpNamespace" } }
    
  public override ToString() : string { (if (Name == null) "<root>" else FullName) + " (namespace)" }
}


NamespaceOrTypeSymbol нам нужен так как классы могут быть вложены как в пространства имен, так и в другие типы. Наследуя NamespaceOrTypeSymbol от HierarchicalSymbol мы получаем реализацию свойств:
    public Scope    : Scope.Table        { get; }
    public Parent   : HierarchicalSymbol { get; }
    public FullName : string             { get; }

необходимых для выстраивания иерархии.

Для всех типов символов описывающих типы C# введем базовый класс TypeSymbol:
[Record]
public abstract class TypeSymbol : NamespaceOrTypeSymbol
{
  public override SpanClass : string { get { "NitraCSharpType" } }
}

Он, так же как и NamespaceSymbol унаследован от NamespaceOrTypeSymbol, а значит может храниться в одном списке с NamespaceSymbol.

Иерархия типов для символов классов будет выглядеть следующим образом:
[Record]
public abstract class ClassSymbol : TypeSymbol
{
  public static Create(declaration : Type.Class) : ClassSymbol
  {
    def name = declaration.Name;
    def parent = declaration.Parent;

    def symbol =
        if (declaration.Parent is NamespaceSymbol as p)
          TopClassSymbol(name, p)
        else
          NestedClassSymbol(name, declaration.Parent);

    name.Symbol = symbol;
    parent.DefineNestedSymbol(symbol);
    symbol.Declarations ::= declaration;
    symbol
  }
}
  
[Record]
public class TopClassSymbol : ClassSymbol
{
  public new Parent : NamespaceSymbol { get { (this : NamespaceOrTypeSymbol).Parent :> NamespaceSymbol } }
    
  public override Kind : string { get { "class" } }
}

[Record]
public class NestedClassSymbol : ClassSymbol
{
  public new Parent : TypeSymbol { get { (this : NamespaceOrTypeSymbol).Parent :> TypeSymbol } }
    
  public override Kind : string { get { "nested class" } }
}


Иерархия для интерфейсов и структур аналогична, так что здесь она не показана.

Декларации


Теперь опишем AST для using-деректив. Они, в C#, бывают двух видов: открывающие и создающие псевдоним:
abstract ast UsingDirective
{
}

declaration UsingAliasDirective : UsingDirective
{
  NamespaceOrTypeName : QualifiedReference;
}

ast UsingOpenDirective : UsingDirective
{
  NamespaceOrTypeName : QualifiedReference;
}


UsingDirective является базовым типом AST для UsingAliasDirective и UsingAliasDirective. В данном случае мы не можем объявить UsingDirective как asts, так как один из наследников (UsingAliasDirective) является не простой веткой AST, а декларацией (declaration). По этому мы используем паттерн — наследование от абстрактного AST.

Обратите, так же, внимание на то, что в UsingAliasDirective не объявлено поле для имени самого псевдонима. Дело в том, что declaration автоматически определяет это поле за нас. Тип этого свойства Reference, а имя — Name. Reference — это тоже тип AST, но он является предопределенным (определен в стандартной библиотеке).

QualifiedReference — это тоже ветка AST описанная в предыдущей теме
Автор: VladD2
Дата: 28.05.15
.

Теперь объявим AST для пространств имен и типов:
abstract declaration NamespaceOrType { }

abstract declaration NamespaceMember : NamespaceOrType { }

declaration Namespace : NamespaceMember
{
  Path            : Reference*;
  UsingDirectives : UsingDirective*;
  Members         : NamespaceMember*;
}

declarations Type : NamespaceMember, TypeMember
{
  | Class
    {
      TypeBase : QualifiedReference*;
    }

  | Interface
    {
      TypeBase : QualifiedReference*;
    }

  | Struct
  | Delegate
  | Enum
}

abstract declaration TypeMember : NamespaceOrType { }


Заполнение дерева пространства имен и типов на ЗС


Теперь у нас все готово, чтобы заполнить дерево пространств имен и типов.

Для этого добавим в представленные выше декларации зависимые свойства (ЗС) описывающие пространства имен. Начнем с CompilationUnit. Добавим в него ЗС RootNamespace:
asts CompilationUnit
{
  in RootNamespace : NamespaceSymbol;

  UsingDirectives : UsingDirective*;
  Members         : NamespaceMember*;
}

В самом начале типизации в него будет помещаться ссылка на корневое пространство имен.

Так как это in-свойство, это делается из внешнего кода. Позже я покажу этот код.

Нам нужно передать это пространство имен в члены CompilationUnit, т.е. элементам списка Members. Для этого нам нужно описать в NamespaceMember входящее ЗС (ВЗС) типа NamespaceSymbol. Назовем это ЗС Parent. Вот только родитель (Parent) может быть не только у членов пространств имен (NamespaceMember), но и у типов, которым могут быть как членами просранств имен, так и членами типов. По этому мы объявляем ВЗС Parent не непосредственно в NamespaceMember, а в его базовом типе — NamespaceOrType:
abstract declaration NamespaceOrType
{
  in Parent       : NamespaceOrTypeSymbol;
}

Теперь мы можем написать код вычисления (присвоения) этого ВЗС. Находиться он будет в CompilationUnit:
asts CompilationUnit
{
  in RootNamespace : NamespaceSymbol;

  Members.Parent = RootNamespace;

  UsingDirectives : UsingDirective*;
  Members         : NamespaceMember*;
}

Members имеет тип "список элементов типа NamespaceMember". Мы не объявляли у него свойства Parent. Однако Nitra автоматически генерирует у списков in- и inout-свойства аналогичные ЗС их элементов. Для ВЗС генерируемый код присваивает значение ВЗС всех элементов списка, так что этот код аналогичен вот такому (гипотетическому) циклу:
foreach (member in Members)
  member.Parent = RootNamespace;

Но DSL вычисления ЗС не допускает циклов, так как в них трудно определить зависмости. Вместо этого нам доступен приведенный выше синтаксис.

Теперь перейдем к вычислениям в NamespaceMember. Начнем с Namespace. Namespace — это декларация, значит мы должны создать для него символ и поместить его в возвращаемое ЗС (ВЗС) получившийся символ. Это ВЗС назовем — Symbol.
declaration Namespace : NamespaceMember
{
  out Symbol : NamespaceSymbol;

  Path            : Reference*;
  UsingDirectives : UsingDirective*;
  Members         : NamespaceMember*;
}

Теперь нужно рассчитать значение этого свойства.
declaration Namespace : NamespaceMember
{
  out Symbol : NamespaceSymbol;

  Symbol                  = EnterNamespace(Parent :> NamespaceSymbol, Path, this);
  Name.Symbol             = Symbol;
  Path.Symbol             = Symbol;
  Members.Parent          = Symbol;

  Path            : Reference*;
  UsingDirectives : UsingDirective*;
  Members         : NamespaceMember*;
}

Создание символа пространства имен вынесено в функцию Nemerle — EnterNamespace:
public EnterNamespace(mutable ns : NamespaceSymbol, path : Reference.IAstList, namespaceDeclaration : Namespace) : NamespaceSymbol
{
  def lastIndex = path.Count - 1;
  foreach (name in path with i)
  {
    mutable symbol = ns.Scope.TryGetSymbol(name);
    when (symbol == null)
    {
      def namespaceSymbol = NamespaceSymbol(name, ns);
      name.Symbol = namespaceSymbol;
      ns.DefineNestedSymbol(namespaceSymbol);
      symbol = namespaceSymbol;
    }
    when (i == lastIndex)
      symbol.Declarations ::= namespaceDeclaration;
          
    ns = (symbol :> NamespaceSymbol);
  }
      
  ns
}


EnterNamespace получает на вход относительный путь (параметр path). Например для пространства имен:
namespace A.B.C { }

Параметр path будет содержать список ссылок (Reference): Reference("A"), Reference("B") и Reference("C").
Для всех элементов пути кроме последнего создаются символы без деклараций. Таким образом, в нашем примере, декларацию будет иметь только пространство имен "C". Остальные являются подразумеваемыми.

Если символ вложенного пространства имен уже имеется внутри родительского пространства имен, берется он, а не создается новый символ. Таким образом сколько бы деклараций пространств имен не существовало, будет создан только один символ.

Последний (самый вложенный) символ возвращается в качестве возвращаемого значения функции EnterNamespace и присваивается ВЗС Symbol декларации Namespace. Строки:
  Name.Symbol             = Symbol;
  Members.Parent          = Symbol;

присваивают этот символ в ВЗС Symbol поля Name (тип которого Reference) и всем ИЗС Parent всех членов пространства имен.

Если членом пространства имен является пространство же имен, то процесс повторится рекурсивно и для него так же будет вычесаны ВЗС Parent и ИЗС Symbol. Если же членом будет тип, то ничего не произойдет, так как процесс расчета для них еще не написан. Давайте это проделаем.
declarations Type : NamespaceMember, TypeMember
{
  | Class
    {
      out Symbol : ClassSymbol = ClassSymbol.Create(this);

      Members.Parent = Symbol;

      TypeBase : QualifiedReference*;
      Members : TypeMember*;
    }

  | Struct
    {
      out Symbol : StructSymbol = StructSymbol.Create(this);
        
      Members.Parent        = Symbol;

      Members : TypeMember*;
    }

  | Interface
    {
      out Symbol : InterfaceSymbol = InterfaceSymbol.Create(this);

      Members.Parent        = Symbol;

      Members : TypeMember*;
    }

  | Delegate
    {
      out Symbol : DelegateSymbol = DelegateSymbol.Create(this);
    }

  | Enum
    {
      out Symbol : EnumSymbol = EnumSymbol.Create(this);
    }
}

Здесь все просто и вряд ли требует объяснений. Остается только показать код функции ClassSymbol.Create. Собственно он уже был приведен выше, но для удобства я его повторю здесь:
public static Create(declaration : Type.Class) : ClassSymbol
{
  def name = declaration.Name;
  def parent = declaration.Parent;

  def symbol =
      if (declaration.Parent is NamespaceSymbol as p)
        TopClassSymbol(name, p)
      else
        NestedClassSymbol(name, declaration.Parent);

  name.Symbol = symbol;
  parent.DefineNestedSymbol(symbol);
  symbol.Declarations ::= declaration;
  symbol
}

Тут все очень похоже на то что было в пространстве имен, но проще, так как в отличии от пространств имен классы нельзя создавать "пачками". Единственно на что можно обратить внимание — это на то, что для вложенных классов создается отдельный тип символа — NestedClassSymbol.

Тестирование вычислений


Теперь, когда вычисления первого прохода написаны мы можем протестировать что у нас получилось. Но есть одна проблема. Дело в том, что значение для ВЗС RootNamespace объявленное в CompilationUnit не задано. Это значит, что расчеты не выполнятся. Чтобы задать это значение, а так же выполнить метод EvalProperties у CompilationUnit нам нужно написать отдельное приложение. Но это довольно большой объем работы. Чтобы избежать ее мы придумали одну хитрость. Корневая ветка AST может реализовать интерфейс IProjectSupport:
public interface IProjectSupport
{
  RefreshProject(asts : Seq[IAst], compilerMessages : ICompilerMessages, statistics : StatisticsTask.Container) : void;
}

Если он реализован, то тестовая утилита Nitra.Visualizer.exe запросит его и вызовет метод RefreshProject передав ему корневые AST полученные после маппинга, а так же compilerMessages через который можно выдать сообщения об ошибках и даже statistics через который можно отобразить в Nitra.Visualizer.exe самопальную статистику.

Как же реализовать этот интерфейс в ветке AST? Nitra генерирует классы для AST с модификатором partial. Это позволяет добавить в эти классы собственные члены и даже реализовать интерфейсы. Добавим в проект, содержащий синтаксические модули C# и AST C# файл со следующим содержанием:
using CSharp.Symbols;

using Nemerle;
using Nemerle.Collections;
using Nemerle.Text;
using Nemerle.Utility;

using Nitra;
using Nitra.Declarations;
using Nitra.Internal;

using System;
using System.Collections.Generic;
using System.Linq;

namespace CSharp
{
  public partial class CompilationUnit : AstBase, IProjectSupport
  {
    public RefreshProject(asts : Seq[IAst], compilerMessages : ICompilerMessages, statistics : StatisticsTask.Container) : void
    {
      def context = DependentPropertyEvalContext();
      def rootNamespace = NamespaceSymbol(null, null);
      
      foreach (cu is CompilationUnit in asts)
        cu.RootNamespace = rootNamespace;
      AstUtils.EvalProperties(context, compilerMessages, asts);
    }
  }
}


Этот код создает "свежее" корневое пространство имен, присваивает его в ВЗС RootNamespace каждого CompilationUnit и, затем, выполняет EvalProperties для всех CompilationUnit (для чего используется вспомогательная функция AstUtils.EvalProperties.

Теперь остается скомпилировать проект и добавить получившуюся сборки в Nitra.Visualizer.exe в качестве отельного тестового набора (Test Suite). После этого можно добавлять тесты или редактировать код в редакторе и смотреть на результат.

Расстановка областей видимости


Для того чтобы заработало связывание осталось только правильно сформировать области видимости (объекты типа Scpe) и "протащить" их сквозь AST. В этом нам снова помогут ЗС.

Это чуть более сложная задача чем прошлая, так как правила формирования областей видимости в пространствах имен C# довольно сложные.

Для вычисления областей видимости нам нужен отдельный проход типизации который должен выполняться после того как будет сформировано полное дерево пространств имен и типов, причем, для всех CompilationUnit проекта. Для этого нам нужно ввести еще одно ВЗС (RootScope) в CompilationUnit, инициализировать его в методе RefreshProject и произвести еще один прогон вычисления зависимых свойств.

Добавляем свойство RootScope в CompilationUnit:
asts CompilationUnit
{
  in RootNamespace : NamespaceSymbol;
  in RootScope     : Scope;

  UsingDirectives : UsingDirective*;
  Members         : NamespaceMember*;
}

Затем добавляет отдельный проход в RefreshProject:
  public partial class CompilationUnit : AstBase, IProjectSupport
  {
    public RefreshProject(asts : Seq[IAst], compilerMessages : ICompilerMessages, statistics : StatisticsTask.Container) : void
    {
      def context = DependentPropertyEvalContext();
      def rootNamespace = NamespaceSymbol(null, null);
      
      // Первый проход, создающий дерево пространств имен и типов
      foreach (cu is CompilationUnit in asts)
        cu.RootNamespace = rootNamespace;
      AstUtils.EvalProperties(context, compilerMessages, asts);

      // Второй проход, расставляющий области видимости
      def rootScope = rootNamespace.Scope;
      foreach (cu is CompilationUnit.CSharp in asts)
        cu.RootScope = rootScope;
      AstUtils.EvalProperties(context, compilerMessages, asts);
    }
  }

Теперь нам нужно написать вычисление областей видимости для пространств имен и CompilationUnit. Для этого добавим в UsingDirective inout ЗС ScopeBuilder типа UsingsScopeBuilder:
abstract ast UsingDirective
{
  inout ScopeBuilder : UsingsScopeBuilder;
}

UsingsScopeBuilder — это вспомогательный класс-построитель который позволит нам упростить формирование довольно сложной (композитной) области видимости формируемой using-директивами C#.

Логика формирования области видимости пространств (далее Scope) имен следующая.

Декларации пространств имен образуют Scope который совмещает определения из декларируемого пространства имен, псевдонимов задаваемых UsingAliasDirective и "открытых" с помощью UsingOpenDirective пространств имен. При этом псевдонимы и открытые пространства имен видны только в рамках текущего файла, а декларации из декларируемого пространства имен видны все: и те что определены в этом файле, и те что определены в этом же пространстве имен, но в других файлах. Именно по этому нам потребовалось создать дерево пространств имен отдельным шагом.

Интересно, что связывание внутри using-директив используется без учета открытых предыдущими using-директивами, объявленными в той же области видимости, но символы из декларируемого пространства имен доступны. Доступны так же и открытие пространства имен и псевдонимы, тех using-директив, которые были объявлены в более внешних (по отношению к объявляемой) областях видимости.

Для выполнения этих не простых правил нам и нужен вспомогательный класс UsingsScopeBuilder. Вот как он устроен:
public class UsingsScopeBuilder
{
  public          Current         : Scope { get; }
  private mutable _opens          : list[Scope] = [];
  private mutable _aliases        : Scope.Table;

  public this(current : Scope)
  {
    Current = current;
  }

  public Open(symbol : Symbol2, namespaceOrType : QualifiedReference) : UsingsScopeBuilder
  {
    if (symbol is NamespaceSymbol as ns)
      _opens ::= ns.Scope;
    else when (symbol.IsResolved)
      AstContext.CompilerMessages.Error(namespaceOrType, <#Using directive can open only namespace.#>);

    this
  }
    
  public Alias(_symbol : Symbol2, usingAliasDirective : UsingAliasDirective) : UsingsScopeBuilder
  {
    def alias = usingAliasDirective.Name;
    def sym = AliasSymbol(alias, usingAliasDirective.NamespaceOrTypeName);
    sym.Declarations ::= usingAliasDirective;
    alias.Symbol = sym;
    Aliases.DefineSymbol(sym);
    this
  }

  public ResultScope : Scope
  {
    get
    {
      def makeCurrentScope(scope : Scope) : Scope
      {
        def withAliases =
          if (_aliases == null)
            scope
          else
            Scope.Union([scope, _aliases]);

        def withUsings =
          match (_opens)
          {
            | []    => withAliases
            | [one] => Scope.Hide(withAliases, one)
            | _     => Scope.Hide(withAliases, Scope.Union(_opens))
          };
            
        withUsings
      }

      match (Current)
      {
        | Hide as h with (scope = h.Scope, parent = h.Hidden) => Scope.Hide(makeCurrentScope(scope), parent)
        | scope => makeCurrentScope(scope)
      }
    }
  }
    
  private Aliases : Scope.Table { get { when (_aliases == null) _aliases = Scope.Table(); _aliases } }
}


Свойство Current хранит начальный Scope. Для пространства имен A.B.C это будет Scope для "C" скрывающий Scope для "B" скрывающий, в свою очередь, Scope для "A". Назовем такой Scope "входом в пространство имен".
Поле _opens накапливает Scope-ы пространств имен открываемых UsingOpenDirective, а _aliases содержит Scope в который помещаются создаваемые с помощью UsingAliasDirective символы псевдонимов (AliasSymbol).

Соответственно метод Open заполняет _opens, а метод Alias — _aliases.

В конце вычислений происходит обращение к свойству ResultScope, в котором и формируется окончательный Scope. Здесь отрабатываются правила затенения и объедения C#. Так исходный Scope (т.е. скоп пространства имен) объединяется с помощью Scope.Union с псевдонимами (в этом листинге так же описаны изменения в другом AST):
Scope.Union([scope, _aliases]);

в единый Scope и они вместе затеняют Scope образованный из _opens:
Scope.Hide(withAliases, Scope.Union(_opens))

Теперь применим этот класс:
abstract ast UsingDirective
{
  inout ScopeBuilder : UsingsScopeBuilder;
}

declaration UsingAliasDirective : UsingDirective
{
  NamespaceOrTypeName.Scope = ScopeBuilderIn.Current;
  ScopeBuilderOut = ScopeBuilderIn.Alias(NamespaceOrTypeName.Symbol, this);

  NamespaceOrTypeName : QualifiedReference;
}

ast UsingOpenDirective : UsingDirective
{
  NamespaceOrTypeName.Scope = ScopeBuilderIn.Current;
  ScopeBuilderOut = ScopeBuilderIn.Open(NamespaceOrTypeName.Symbol, NamespaceOrTypeName);

  NamespaceOrTypeName : QualifiedReference;
}

asts CompilationUnit
{
  in RootNamespace : NamespaceSymbol;
  in RootScope     : Scope;

  UsingDirectives.ScopeBuilderIn = UsingsScopeBuilder(RootScope);
  Members.Scope                  = UsingDirectives.ScopeBuilderOut.ResultScope;

  UsingDirectives : UsingDirective*;
  Members         : NamespaceMember*;
}

abstract declaration NamespaceMember : NamespaceOrType
{
  in Scope : Scope;
}

declaration Namespace : NamespaceMember
{
  out Symbol : NamespaceSymbol;

  Symbol                  = EnterNamespace(Parent :> NamespaceSymbol, Path, this);
  Name.Symbol             = Symbol;
  Members.Parent          = Symbol;

  UsingDirectives.ScopeBuilderIn = UsingsScopeBuilder(MakeEnteredScope(Scope, Parent :> NamespaceSymbol, Symbol));
  Members.Scope                  = UsingDirectives.ScopeBuilderOut.ResultScope;

  Path            : Reference*;
  ExternAlias     : ExternAliasDirective*;
  UsingDirectives : UsingDirective*;
  Members         : NamespaceMember*;
}

declarations Type : NamespaceMember, TypeMember
{
  | Class
    {
      out Symbol : ClassSymbol = ClassSymbol.Create(this);

      TypeBase.Scope = Scope.Hide(this.Symbol.Scope, this.Scope);
      Members.Parent = Symbol;

      TypeBase : QualifiedReference*;
      Members : TypeMember*;
    }
...

Теперь, если добавить, вычисление областей видимости и символов в QualifiedReference
Автор: VladD2
Дата: 28.05.15
, мы получаем рабочее решение, которое можно попробовать в Nitra.Visualizer.exe:


При этом у нас автоматом заработают многие сервисы IDE.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Отредактировано 28.05.2015 20:42 VladD2 . Предыдущая версия . Еще …
Отредактировано 28.05.2015 20:09 VladD2 . Предыдущая версия .
Отредактировано 28.05.2015 20:08 VladD2 . Предыдущая версия .
Отредактировано 28.05.2015 20:07 VladD2 . Предыдущая версия .
Отредактировано 28.05.2015 19:41 VladD2 . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.