Documentation (#3211)

* Add Compiler documentation

* Add Intellisense documentation

* Move documentation folder to root directory
This commit is contained in:
AlexanderZemlyak 2024-10-08 10:00:14 +03:00 committed by GitHub
parent 79f9ab73a0
commit 271d5792ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 2677 additions and 0 deletions

View file

@ -0,0 +1,163 @@
Owner: Alexander Zemlyak
Tags: Infrastructure
### Алгоритм на псевдокоде
- Полная версия
```
CompileUnit(ИмяФайла)
1.CompileUnit(new СписокМодулей, ИмяФайла)
2.Докомпилировать модули из СписокОтложенойКомпиляции;
CompileUnit(СписокМодулей, ИмяФайла)
1.ТекущийМодуль = ТаблицаМодулей[ИмяФайла];
Если (ТекущийМодуль != 0) то
Если (ТекущийМодуль.Состояние != BeginCompilation)
СписокМодулей.Добавить(ТекущийМодуль);
Выход;
иначе перейти к пункту 5
2.Если ЭтоФайлDLL(ИмяФайла) то
Если ((ТекущийМодуль = СчитатьDLL(ИмяФайла)) != 0) то
СписокМодулей.Добавить(ТекущийМодуль);
ТаблицаМодулей.Добавить(ТекущийМодуль);
Выход;
иначе
Ошибка("Не могу подключить сборку");
Выход;
3.Если ЭтоФайлPCU(ИмяФайла) то
Если ((ТекущийМодуль = СчитатьPCU(ИмяФайла)) != 0) то
СписокМодулей.Добавить(ТекущийМодуль);
ТаблицаМодулей.Добавить(ТекущийМодуль);
Выход;
иначе
иначе перейти к пункту 4;
4.ТекущийМодуль = новыйМодуль();
ТекущийМодуль.СинтаксическоеДерево = Парасеры.Парсить(ИмяФайла,ТекущийМодуль.СписокОшибок);
Если (ТекущийМодуль.СинтаксическоеДерево == 0) то
Если (ТекущийМодуль.СписокОшибок.Количество == 0) то
Ошибка("Модуль не неайден");
иначе
Ошибка(ТекущийМодуль.СписокОшибок[0]);
ТаблицаМодулей[ИмяФайла] = ТекущийМодуль;
ТекущийМодуль.Состояние = BeginCompilation;
5.СинтаксическийСписокМодулей = ТекущийМодуль.СинтаксическоеДерево.Interface.usesList;
Для(i = СинтаксическийСписокМодулей.Количество - 1 - ТекущийМодуль.КомпилированыеВInterface.Количество; i >= 0; i--)
ТекушийМодуль.ТекущийUsesМодуль = СинтаксическийСписокМодулей[i].ИмяФайла;
ИмяUsesФайла = СинтаксическийСписокМодулей[i].ИмяФайла;
Если (ТаблицаМодулей[ИмяUsesФайла] != 0)
Если (ТаблицаМодулей[ИмяUsesФайла].Состояние == BeginCompilation)
Если (ТаблицаМодулей[ТаблицаМодулей[ИмяUsesФайла].ТекущийUsesМодуль].Состояние=BeginCompilation)
Ошибка("Циклическая связь модулей");
CompileUnit(ТекущийМодуль.КомпилированыеВInterface, ИмяUsesФайла);
Если (ТекушийМодуль.Состояние == Compiled) то
СписокМодулей.Добавить(ТекушийМодуль);
Выход;
6.ТекущийМодуль.СемантическоеДерево = КонверторДерева.КонвертироватьInterfaceЧасть(
ТекущийМодуль.СинтаксическоеДерево,
ТекущийМодуль.КомпилированыеВInterface,
ТекущийМодуль.СписокОшибок);
СписокМодулей.Добавить(ТекущийМодуль);
СинтаксическийСписокМодулей = ТекущийМодуль.СинтаксическоеДерево.Implementation.usesList;
Для(i = СинтаксическийСписокМодулей.Количество - 1; i >= 0; i--)
Если (ТаблицаМодулей[СинтаксическийСписокМодулей[i].ИмяФайла].Состояние == BeginCompilation)
СписокОтложенойКомпиляции.Добавить(ТаблицаМодулей[СинтаксическийСписокМодулей[i].ИмяФайла]);
иначе
CompileUnit(ТекущийМодуль.КомпилированыеВImplementation, СинтаксическийСписокМодулей[i].ИмяФайла);
Если(ДобавлялиХотябыОдинВСписокОтложенойКомпиляции)
СписокОтложенойКомпиляции.Добавить(ТекущийМодуль);
выход;
иначе
КонверторДерева.КонвертироватьImplementationЧасть(
ТекущийМодуль.СинтаксическоеДерево,
ТекущийМодуль.СемантическоеДерево,
ТекущийМодуль.КомпилированыеВImplementation
ТекущийМодуль.СписокОшибок);
ТекущийМодуль.Состояние = Compiled;
СохранитьPCU(ТекущийМодуль);
```
- Краткая версия (меньше технических подробностей)
```
CompileUnit(ИмяФайла)
1.CompileUnit(new СписокМодулей, ИмяФайла)
2.Докомпилировать модули из СписокОтложенойКомпиляции;
CompileUnit(СписокМодулей, ИмяФайла);
1.Если у этого модуля откомпилирован хотябы интерфейс то
добавить его в СписокМодулей
выход
2.Если это DLL то
считать
добавить его в СписокМодулей
выход
3.Если это PCU то
считать
добавить его в СписокМодулей
выход
4.создать новый компилируемыйМодуль
РаспарситьТекст(ИмяФайла)
Состояние компилируемогоМодуля установить на BeginCompilation
5.Для всех модулей из Interface части компилируемогоМодуля справа налево
Если мы уже начаинали компилировать этот модуль
Если состояние модуля BeginCompilation
Если состояние последнего компилируемого им модуля BeginCompilation
ошибка("Циклическая связь модулей")
выход
CompileUnit(Список из Interface части компилируемогоМодуля,модуль.имя)
Если компилируемыйМодуль.Состояние Compiled то
добавить его в СписокМодулей
выход
6.Откомпилировать Interface часть компилируемогоМодуля
Для всех модулей из Implementation части компилируемогоМодуля справа налево
Если состояние очередного модуля BeginCompilation то
добавить его в список отложеной компиляции;
иначе
CompileUnit(Список из Implementation части компилируемогоМодуля,модуль.имя)
Если Добавляли Хотя бы Один В Список Отложеной Компиляции то
добавить компилируемыйМодуль в список отложеной компиляции
выход
Откомпилировать Implementation часть компилируемогоМодуля
Состояние компилируемогоМодуля установить на Compiled
добавить его в СписокМодулей
Сохранить компилируемыйМодуль в виде PCU файла на диск
```
### Словесное описание алгоритма
Модули компилируются в порядке, соответствующем порядку **обхода в глубину** графа зависимостей. Обход в глубину реализуется рекурсивными вызовами функции CompileUnit.
Сначала рекурсивно обходятся все зависимости интерфейсных частей модулей, причем в порядке **обратном** порядку в списке uses. Ясно, что может существовать **циклическая зависимость интерфейсных частей** модулей. Она запрещается (во время компиляции выдается соотв. ошибка). Цикл обнаруживается, если мы встречаем модуль с не откомпилированным интерфейсом, в котором мы уже были.
<aside>
💡 Однако после обнаружения требуется еще проверка на то, что цикл образован целиком из “интерфейсных связей”, поскольку в случае присутствия в нем связи через implementation часть модуля он не представляет опасности благодаря механизму отложенной компиляции, про которую читайте далее.
</aside>
**Пример допустимого цикла**
![[допустимая циклическая зависимость.png]]
---
Если все зависимости интерфейса скомпилированы, то в том же порядке начинают рекурсивно обходиться зависимости части реализации (implementation). **Отложенная компиляция** происходит сразу после окончания первого обхода всех модулей, когда абсолютно все интерфейсные части модулей уже скомпилированы. Это условие гарантирует отсутствие новых циклических интерфейсных зависимостей. Модули добавляются в список отложенной компиляции в случае, если в некотором модуле в списке uses части реализации есть ранее встреченный модуль с еще не откомпилированным интерфейсом (в список добавляются оба этих модуля).
<aside>
💡 Может возникнуть вопрос, что происходит в случае циклической зависимости частей реализации. В этой ситуации мы в какой-то момент попадем во второй раз в модуль с откомпилированным интерфейсом и проверка, приведенная далее, в начале CompileUnit приведет к выходу из функции. А данный модуль докомпилируется позже в другом рекурсивном вызове.
</aside>
```csharp
if (currentUnit.State != UnitState.BeginCompilation || currentUnit.SemanticTree != null)
{
AddCurrentUnitAndItsReferencesToUsesLists(unitsFromUsesSection, directUnitsFromUsesSection,
currentUnitNode, currentUnit, GetReferences(currentUnit));
return currentUnit;
}
```

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,22 @@
Owner: Alexander Zemlyak
Tags: Codebase
Dll-библиотека для ускорения времени выполнения “простых” программ без пользовательских uses (допускается использование пр-в имен и стандартных модулей Паскаля из самой RTL). Для использования требуется включить флажок *“Ускорять запуск из под оболочки”.*
---
На картинке можно видеть стандартные модули из библиотеки:
![Untitled](Untitled.png)
---
> [!NOTE] Особые стандартные модули. 1-я группа
>  Некоторые стандартные модули не входят в PABCRtl.dll, поскольку она 64-битная, а они используют нативные 32-битные библиотеки. Таковыми являются модули из следующего списка: PT4, CRT, Arrays, MPI, Collections, Core. При использовании их в программе PABCRtl.dll не загружается. *2-я группа* Если стандартные модули не входят в RTL и одновременно не входят в список исключения RTL, о котором говорилось выше, то RTL загружается. Но при этом возникает ошибка компиляции и компиляция запускается заново без использования RTL.
> [!NOTE] Как сделано так, что при явном использовании стандартных модулей из RTL их pcu-файлы не используются?
> Стандартные модули в этом случае воспринимаются как пространства имен и даже добавляются в соответствующие списки, такие как `possibleNamespaces` (см. [[Uses lists]]). Метод `CompileUnit` для них вообще не вызывается. Конечно, хотелось бы выделить логику обработки стандартных модулей из rtl в отдельный блок кода.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,39 @@
Owner: Alexander Zemlyak
- **Получение списков зависимостей из синтаксического дерева юнита**
1. *interfaceUsesList* может содержать юниты, пространства имен .NET и явные пространства имен (узлы SyntaxTree.unit_or_namepace)
2. *references* содержит библиотеки dll (узлы TreeRealization.unit_node)
3. *namespaces* содержит явные пользовательские пространства имен, подключенные с помощью директивы {$includenamespace} (узлы SyntaxTree.syntax_namespace_node)
```csharp
CreateDependencyListsForCurrentUnit(currentUnit, currentDirectory, out var interfaceUsesList, out var references, out var namespaces);
```
- **Списки зависимостей, содержащиеся в CompilationUnit**
1. *possibleNamespaces* может содержать пространства имен .NET, стандартные паскалевские юниты из папки Lib, а также явные пространства имен, если они есть в секции uses
2. *InterfaceUsedUnits* содержит все, что есть в *possibleNamespaces +* пользовательские юниты и dll, т.е., грубо говоря, все виды программных единиц, являющиеся зависимостями CompilationUnit
3. *InterfaceUsedDirectUnits* содержит только пользовательские юниты
4. *ImplementationUsedUnits* и *ImplementationUsedDirectUnits* устроены аналогично предыдущим двум
```csharp
public class CompilationUnit
{
...
internal List<SyntaxTree.unit_or_namespace> possibleNamespaces = new List<PascalABCCompiler.SyntaxTree.unit_or_namespace>();
public Dictionary<unit_node, CompilationUnit> InterfaceUsedDirectUnits { get; } = new Dictionary<unit_node, CompilationUnit>();
public unit_node_list InterfaceUsedUnits { get; } = new unit_node_list();
public Dictionary<unit_node, CompilationUnit> ImplementationUsedDirectUnits { get; } = new Dictionary<unit_node, CompilationUnit>();
public unit_node_list ImplementationUsedUnits { get; } = new unit_node_list();
...
}
```
<aside>
💡 Узел using_list является устаревшим, так как он относился к ключевому слову using.
</aside>

View file

@ -0,0 +1,41 @@
#### Подсказка при наведении мыши
На верхнем уровне реализуется в классе `TooltipServiceManager`. Основная логика реализована в методе `GetPopupHintText`. Вначале вызывается `FindExpressionFromAnyPosition` для получения нужного `expression` в виде строки.
Затем вызывается нужный парсер для получения дерева выражения (используется часть грамматики, связанная с нетерминалом parts). Если не получается распарсить, то делается попытка с решением неоднозначности, связанной с шаблонными параметрами (&<).
После этого еще попытка с удалением разыменования, пока непонятно зачем это.
Еще удаление разыменования было найдено в методе `GetDefinitionPosition` из `CodeCompletionActions`.
Затем еще попытка с тем же выражением без скобочек. При этом, если без скобочек распарсить не удается, то вне зависимости от успешности парсинга полного `expression` возвращается `null`.
В конце проверяется был ли успешен обход дерева программы визитором (`DomSyntaxTreeVisitor`). Если это так, то в словаре откомпилированных модулей будет данный модуль и соответствующий ему `DomConverter`.
Тогда мы сможем вызвать метод `GetDescription` из этого класса, который выдаст нам описание выражения.
#### Подсказка параметров функции
На нажатие клавиш (скобки, квадратной скобки или запятой) реагирует `TextAreaKeyEventHandler` из класса `CodeCompletionKeyHandler`. Там создается экземпляр `DefaultInsightDataProvider`, который загружается в стек провайдеров. Информация для подсказки формируется в методе `SetupDataProvider` инсайт-провайдера. После выяснения кода на текущей подстроке вызывается `FindExpressionForMethod` из `LanguageInformation`.
Далее делается попытка парсинга стандартным парсером языка и наконец из `DomConverter` вызывается `GetNameOfMethod`, либо `GetIndex` в зависимости от нажатой клавиши.
#### Подсказка по нажатию клавиш (по точке или пробелу)
`CodeCompletionKeyHandler` реагирует на нажатие вызовом метода `ShowCompletionWindow`. В нем есть обращение к `GenerateCompletionDataWithKeyword` из класса `CodeCompletionProvider`. В этом методе выделяется нужная подстрока и есть обращение к методу `GetCompletionData`.
**Логика этого метода следующая:**
1) если был нажат `ctrl + space` (`shift + space` входит в это понятие тоже)
Вызывается `FindPattern` из `LanguageInformation` для получения "токена" до каретки. Если в процессе встретится точка, то вызывается `FindExpression`. Ниже если pattern пустой, то в итоговый список `ICompletionData` добавляются ключевые слова языка.
2) если был нажат пробел после `new`
Вызывается SkipNew для пропуска `new` и поиска expression перед ним.
3) иначе, если не было введено `uses`
вызывается `FindExpression` (c текущей позиции)
Далее, если [**не** `ctrl + space` или была найдена цепочка имен в выражении] и выражение не пусто, то делается попытка вызова функции `GetTypeAsExpression` стандартного парсера. Если это ничего не дало далее попытка вызова `GetExpression`. Если снова ничего не удалось и мы не в случае пробела после `new`, то возврат.
Далее, если Intellisense успешно не обошла текущий модуль, то для случая `uses` мы будем рассматривать SymInfo стандартных модулей (взятых из DomConverter). Если же у нас не `uses`случай и не случай ctrl + space, мы возвращаем пустой массив.
Иначе, если у нас есть `DomConverter`:
- Если пробел после `new`, то вызываем `GetTypes`
- Иначе, если `uses`, то работаем со списком стандартных модулей + пр-в имен в случае семантического Intellisense.
- Иначе, если не `ctrl + space` (случай, когда мы поставили точку), то вызываем `GetName` для получения всех возможных имен после точки
- Иначе, если `ctrl + space`, то либо ищем имена после точки - `GetName` (если точка была раньше), либо ищем имя по его началу - `GetNameByPattern`
> [!NOTE] Замечание
>Также на нажатие реагирует Execute из CodeCompletionAllNamesAction. key подменяется на доллар если это было нажатие `ctrl + space` сразу после точки.
В конце по полученным SymInfo строится результирующий список CompletionData (без повторений).
#### Переход к определению символа
В `CodeCompletionActoins` есть специальный класс GotoAction, в котором вызывается метод `GoToDefinition`. В этом методе используется метод GetDefinitionPosition, который в свою очередь (тоже косвенно) использует `FindExpressionFromAnyPosition` из `LanguageInformation` и `GetDefinition` из `CodeCompletionProvider`. Последний вышеупомянутый метод является оберткой на `GetDefinition` из DomConverter.