|
|||||||
Абстрактное синтаксическое дерево - Abstract Syntax Tree
Время создания: 13.05.2021 11:34
Текстовые метки: компилятор, абстрактоное синтаксическое дерево, abstract syntax tree, AST
Раздел: Компьютер - Программирование - Теория программирования - Теория компиляции
Запись: xintrea/mytetra_syncro/master/base/1620894860y9tpfqmtw3/text.html на raw.github.com
|
|||||||
|
|||||||
Сердце современных фронтендов компиляторов — абстрактное синтаксическое дерево (Abstract Syntax Tree, AST). Оно создаётся на стадии синтаксического разбора, обрабатывается путём обхода при проверке семантических правил и проверке/определении типов, а затем также путём обхода AST выполняется генерация кода. Зачем нужен ASTAST - это Abstract Syntax Tree, то есть дерево, которое в абстрактном виде представляет структуру программы. AST создаётся парсером по мере синтаксического разбора программы. В современных компиляторах AST и список диагностик (ошибок, предупреждений) - это два результата вызова модуля синтаксического разбора. AST содержит полную синтаксическую модель программы без лишних деталей (таких, как пробельные символы или комментарии). Чтобы понять, как это работает, рассмотрим, из чего состоит типичный язык программирования. Типовой императивный язык программирования (такой как C, C++, PASCAL, Java, JavaScript, C#) состоит из трёх синтаксических элементов:
AST создаётся парсером по мере синтаксического разбора программы. В современных компиляторах AST и список диагностик (ошибок, предупреждений) - это два результата вызова модуля разбора (или функции разбора). ВыраженияВыражения - это выражение формулы в исходном коде. Выражение может иметь побочные эффекты: например, при вызове функции внутри выражения функция может что-то напечатать. В типичном языке программирования есть как минимум следующие типы выражений:
ИнструкцииИнструкции - это действия в исходном коде. Инструкция всегда имеет побочный эффект (печать, присваивание переменной, смена потока управления и т.д.), поэтому в некоторых языках инструкций не существует - например, в LISP или Haskell. Примеры инструкций, характерных для процедурных языков:
ОбъявлениеОбъявление - это создание новой именованной сущности, такой как функция или тип. Объявления типов бывают разнообразными: различные языки могут позволять объявлять новый тип как синоним старого типа, как структуру, как класс, как интерфейс или иным образом. Спорные вопросыНекоторые сущности трудно отнести к той или иной категории. Например:
Спорные вопросы обычно решаются в сторону удобства для создателя языка или компилятора. Что такое AST?AST - это Abstract Syntax Tree, т.е. представление структуры программы в виде дерева объявлений, инструкций и выражений.
Удобный способ реализовать AST в коде - это иерархия классов и интерфейсов. Например, можно ввести три базовых интерфейса:
Если в языке нет типов, то DeclarationAST можно для удобства превратить в FunctionAST и считать программу списком FunctionAST. Все остальные классы из иерархии наследуются от базовых интерфейсов и реализуют объявленные ими методы. Какие методы находятся в базовых интерфейсах - решать автору компилятора/интерпретатора. В любом случае, методы должны выстраиваться в единую модель генерации кода или интерпретации. Суть дерева - в возможности обойти его (слева направо в глубину или другим способом). При обходе можно выполнять осмысленные действия, при этом возникает проблема двойной диспетчеризации: мы должны выбирать действие в зависимости как от алгоритма, которым мы обрабатываем дерево, так и от типа узла дерева, который мы сейчас обходим. Рассмотрим пути решения проблемы:
Конструирование AST в парсереДля конструирования узлов AST потребуется выделять память, а затем запоминать указатель в стеке парсера. Напомним, что любой реальный язык содержит рекурсивные синтаксические правила (например, выражения всегда определяются рекурсивно), значит, парсер языка не может быть реализован без стека или без рекурсии (которая эквивалентна стеку). Поэтому способ работы с AST зависит от метода создания парсера. Рекурсивный спускЕсли вы используете рекурсивный спуск, то вы скорее всего
Таким образом, типичная функция парсинга выглядит примерно так: bool Parser::parseExpr() { // Если следующий токен - ID, то это вызов функции или обращение к переменной if (NextToken().type == Token.ID) { if (NextToken(2).type == Token.OpenParen) { return parseCallExpr(); } return parseVariableAccess(); } // ... разбираем иные варианты ... // Ожидалось выражение, но его нет throw ParseException("expected expression, got " + NextToken().ToString(), NextToken()); } Для добавления конструирования AST следуйте простым правилам:
Восходящий разбор методом свёртки (LL, LR, SLR, LALR)Если вы используете табличный недетерминированный конечный автомат со стеком для восходящего разбора методом свёртки, то вы можете следовать нескольким правилам:
Пример декларативного описания правила грамматики с конструированием AST (для генератора парсеров Lemon): statement ::= PRINT expression(A). { auto stmt = pParse->MakeAST<CPrintAst>(A.expr); pParse->AddStatement(stmt); } Обработка готового ASTПутём обработки готового AST можно
Способы обхода ASTВ глубину слева направо function visit(node) { actionBeforeVisit(node) for child in node.children() child.accept(this) actionAfterVisit(node) } В глубину справа налево function visit(node) { actionBeforeVisit(node) for child in reverse(node.children()) child.accept(this) actionAfterVisit(node) } В ширину слева направо (влечёт значительный расход памяти): function visit(node_list) { new_node_list = [] for node in node_list { actionOnVisit(node) for child in node.children() new_node_list << child } visit(new_node_list) } В ширину справа налево (влечёт значительный расход памяти). Expression problem
Решение 1: case-распознаваниеПодходит для процедурных и функциональных языков. Варианты: if/elseif/else, switch/case или pattern matching. // Печатает поддерево, начиная с узла node function print(node) { case node of { AddOperator => print(node.left) + '+' + print(node.right) NotOperator => '!' + print(node) Variable => print(variables.get(node.name).value) Literal => print(node.value) } } // Вычисляет значение, начиная с узла node function eval(node) { case node of { AddOperator => return eval(node.left) + eval(node.right) NotOperator => return !eval(node) Variable => return variables.get(node.name).value Literal => return node.value } } Решение 2: полиморфные методыПодходит для объектно-ориентированных и некоторых функциональных языков. В языке должно быть наследование либо утиная типизация, а также иерархия классов. class AddOperator extends Node { let left: Node = null let right: Node = null function print() { left.print() print(' + ') right.print() } function eval() { return left.eval() + right.eval() } } class NotOperator extends Node { let child: Node = null function print() { print('!') child.print() } function eval() { return not child.eval() } } Решение 3: Visitor (Посетитель)Объектно-ориентированные языки не имеют встроенного решения, но зато есть паттерн проектирования Visitor (Посетитель).
Реализовать паттерн Visitor в C++ можно с помощью полиморфизма (virtual-методы и классы) или с помощью шаблонов (Curiously recurring template pattern). Реализация на виртуальных методах: struct VaribleAst; struct LiteralAst; struct IVisitor { virtual ~IVisitor() = default; virtual void visit(VaribleAst &ast) = 0; virtual void visit(LiteralAst &ast) = 0; }; struct IExpressionAst { virtual ~IExpressionAst() = default; virtual void accept(IVisitor &visitor) = 0; } struct VariableAst : IExpressionAst { void accept(IVisitor &visitor) override { visitor.visit(*this); } } struct LiteralAst : IExpressionAst { void accept(IVisitor &visitor) override { visitor.visit(*this); } } Реализация на CRTP (Curiously recurring template pattern), которую не рекомендуется использовать из-за бессмысленной сложности: Объектно-ориентированный подход не снимает Expression Problem, но позволяет выбирать, что будет простым: добавление новых типов данных или новых операций
Решение 4: Обход дерева и case-распознаваниеПсевдокод: function (this *EvaluationVisitor) visit(node) { case node of: AddOperator => print(node.left) + '+' + print(node.right) NotOperator => '!' + print(node) } function eval(ast) { var visitor EvaluationVisitor ast.walk(visitor) }
Семантическая постобработка дереваСемантическая постобработка дерева позволяет добавить в язык правила, выходящие за рамки контекстно-свободных грамматик. Например, путём постобработки можно:
Подобные шаги постобработки дерева называются “процедурами семантической проверки”, их часто выделяют в собственный модуль - модуль семантики языка. Генерация кода по ASTО методах генерации кода на базе собранного AST вы можете прочитать в статье Стековые и регистровые машины. |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|