В этом посте мы увидим, как обрабатывают и преобразуют информацию, полученную от парсера. Синтаксический анализатор ANTLR распознает элементы, присутствующие в исходном коде, и создает дерево анализа . Из дерева разбора мы получим Абстрактное синтаксическое дерево, которое мы будем использовать для проверки и создания скомпилированного кода.
Обратите внимание, что терминология может варьироваться: многие называют дерево, полученное из ANTLR, абстрактным синтаксическим деревом. Я предпочитаю отмечать разницу от этих двух шагов. Для меня дерево синтаксического анализа — это информация, значимая для синтаксического анализатора, абстрактное синтаксическое дерево — это информация, реорганизованная для лучшей поддержки следующих шагов.
Серия по созданию собственного языка
Предыдущие сообщения:
- Построение лексера
- Сборка парсера
- Создание редактора с подсветкой синтаксиса
- Создайте редактор с автозаполнением
Код доступен на GitHub под тегом 05_ ast
Приправить наш язык
В этой серии статей мы работали над очень простым языком выражений. Пришло время сделать наш язык немного более сложным, введя:
- например, 10 : как десятичное или (1 * 2,45) как Int
- печать заявления например: печать (3 + 11)
Для этого нам нужно пересмотреть нашу грамматику лексера и синтаксического анализатора. Подсветка синтаксиса и автозаполнение, которые мы встроили в предыдущие посты, будут продолжать работать.
Новая грамматика лексера:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | lexer grammar SandyLexer; //WhitespaceNEWLINE            : '\r\n'| 'r'| '\n';WS                 : [\t ]+ -> skip ; //KeywordsVAR                : 'var';PRINT              : 'print';AS                 : 'as';INT                : 'Int';DECIMAL            : 'Decimal'; //LiteralsINTLIT             : '0'|[1-9][0-9]* ;DECLIT             : '0'|[1-9][0-9]* '.'[0-9]+ ; //OperatorsPLUS               : '+';MINUS              : '-';ASTERISK           : '*';DIVISION           : '/';ASSIGN             : '=';LPAREN             : '(';RPAREN             : ')'; //IdentifiersID                 : [_]*[a-z][A-Za-z0-9_]* ; | 
И новый синтаксический анализатор:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | parser grammar SandyParser; options { tokenVocab=SandyLexer; } sandyFile : lines=line+ ; line      : statement (NEWLINE | EOF) ; statement : varDeclaration # varDeclarationStatement          | assignment     # assignmentStatement          | print          # printStatement ; print : PRINT LPAREN expression RPAREN ; varDeclaration : VAR assignment ; assignment : ID ASSIGN expression ; expression : left=expression operator=(DIVISION|ASTERISK) right=expression # binaryOperation           | left=expression operator=(PLUS|MINUS) right=expression        # binaryOperation           | value=expression AS targetType=type# typeConversion           | LPAREN expression RPAREN                                      # parenExpression           | ID                                                            # varReference           | MINUS expression                                              # minusExpression           | INTLIT                                                        # intLiteral           | DECLIT                                                        # decimalLiteral ; type: INT     # integer     | DECIMAL # decimal ; | 
Метамодель абстрактного синтаксического дерева
Метамодель Абстрактного синтаксического дерева — это просто структура данных, которые мы хотим использовать для нашего Абстрактного синтаксического дерева (AST). В этом случае мы определяем его, определяя классы, которые мы будем использовать для нашего AST.
Метамодель AST будет выглядеть в достаточной степени похожей на метамодель дерева разбора, т. Е. На набор классов, генерируемых ANTLR для содержания узлов.
Там будет несколько ключевых отличий:
- он будет иметь более простой и приятный API, чем классы, сгенерированные ANTLR (поэтому классы, составляющие дерево разбора). В следующих разделах мы увидим, как этот API может позволить выполнять преобразования в AST
- мы удалим элементы, которые имеют смысл только при разборе, но которые логически бесполезны: например, выражение в скобках или узел строки
- некоторые узлы, для которых у нас есть отдельные экземпляры в дереве разбора, могут соответствовать одному экземпляру в AST. Это случай ссылок на типы Int и Decimal, которые в AST определяются с помощью одноэлементных объектов.
- мы можем определить общие интерфейсы для связанных типов узлов, таких как BinaryExpression
- чтобы определить, как анализировать объявление переменной, мы повторно используем правило присваивания. В AST две концепции полностью разделены
- некоторые операции имеют одинаковый тип узла в дереве разбора, но разделены в AST. Это случай различных типов бинарных выражений
Давайте посмотрим, как мы можем определить нашу метамодель AST, используя Kotlin.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | interfaceNode //// Sandy specific part// data classSandyFile(val statements : List) : Node interfaceStatement : Node { } interfaceExpression : Node { } interfaceType : Node { } //// Types// object IntType : Type object DecimalType : Type //// Expressions// interfaceBinaryExpression : Expression {    val left: Expression    val right: Expression} data classSumExpression(override val left: Expression, override val right: Expression) : BinaryExpression data classSubtractionExpression(override val left: Expression, override val right: Expression) : BinaryExpression data classMultiplicationExpression(override val left: Expression, override val right: Expression) : BinaryExpression data classDivisionExpression(override val left: Expression, override val right: Expression) : BinaryExpression data classUnaryMinusExpression(val value: Expression) : Expression data classTypeConversion(val value: Expression, val targetType: Type) : Expression data classVarReference(val varName: String) : Expression data classIntLit(val value: String) : Expression data classDecLit(val value: String) : Expression //// Statements// data classVarDeclaration(val varName: String, val value: Expression) : Statement data classAssignment(val varName: String, val value: Expression) : Statement data classPrint(val value: Expression) : Statement | 
Начнем с определения узла . Узел представляет каждый возможный узел AST, и он является общим. Это может быть повторно использовано и для других языков. Все остальное вместо этого специфично для языка (Сэнди в нашем случае). На нашем конкретном языке нам нужны три важных интерфейса:
- утверждение
- выражение
- Тип
Каждый из этих интерфейсов расширяет узел .
Затем мы объявляем два типа, которые мы используем в нашем языке. Они определены как одноэлементные объекты. Это означает, что у нас есть только один экземпляр этих классов.
Затем у нас есть интерфейс BinaryExpression , который расширяет Expression . Для классов реализует его, по одному для каждого из основных арифметических выражений.
Большинство выражений имеют в качестве дочерних узлов другие узлы. У некоторых есть вместо этого простые значения. Это VarReference (у которого есть свойство varName типа String ), а также Intlit и DecLit (оба имеют значение свойства типа String ).
Наконец, у нас есть три класса, реализующие Statement .
Обратите внимание, что мы используем классы данных, чтобы мы могли бесплатно использовать методы hashCode, equals и toString. Котлин порождает для нас также конструкторов и добытчиков. Попробуйте представить, сколько кода будет в Java.
Отображение дерева разбора на абстрактное синтаксическое дерево
Давайте посмотрим, как мы можем получить дерево разбора, созданное ANTLR, и отобразить его в наши классы AST.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | fun SandyFileContext.toAst() : SandyFile = SandyFile(this.line().map { it.statement().toAst() }) fun StatementContext.toAst() : Statement = when (this) {    is VarDeclarationStatementContext -> VarDeclaration(varDeclaration().assignment().ID().text, varDeclaration().assignment().expression().toAst())    is AssignmentStatementContext -> Assignment(assignment().ID().text, assignment().expression().toAst())    is PrintStatementContext -> Print(print().expression().toAst())    else-> throwUnsupportedOperationException(this.javaClass.canonicalName)} fun  ExpressionContext.toAst() : Expression = when (this) {    is BinaryOperationContext -> toAst()    is IntLiteralContext -> IntLit(text)    is DecimalLiteralContext -> DecLit(text)    is ParenExpressionContext -> expression().toAst()    is VarReferenceContext -> VarReference(text)    is TypeConversionContext -> TypeConversion(expression().toAst(), targetType.toAst())    else-> throwUnsupportedOperationException(this.javaClass.canonicalName)} fun TypeContext.toAst() : Type = when (this) {    is IntegerContext -> IntType    is DecimalContext -> DecimalType    else-> throwUnsupportedOperationException(this.javaClass.canonicalName)} fun  BinaryOperationContext.toAst() : Expression = when (operator.text) {    "+"-> SumExpression(left.toAst(), right.toAst())    "-"-> SubtractionExpression(left.toAst(), right.toAst())    "*"-> MultiplicationExpression(left.toAst(), right.toAst())    "/"-> DivisionExpression(left.toAst(), right.toAst())    else-> throwUnsupportedOperationException(this.javaClass.canonicalName)} | 
Чтобы реализовать это, мы воспользовались тремя очень полезными функциями Kotlin:
- методы расширения: мы добавили метод toAst к нескольким существующим классам
- конструкция when , которая является более мощной версией switch
- умные приведения: после того, как мы проверим, что объект имеет определенный класс, компилятор неявно приводит его к этому типу, чтобы мы могли использовать конкретные методы этого класса
Мы могли бы придумать механизм для автоматического получения этого отображения для большинства правил и просто настроить его там, где отличается дерево разбора и AST. Чтобы не использовать слишком много отражающей черной магии, мы пока не будем этого делать. Если бы я использовал Java, я бы просто пошел по пути размышлений, чтобы избежать необходимости писать вручную много избыточного и скучного кода. Однако, используя Kotlin, этот код компактен и понятен.
Тестирование картирования
Конечно, нам нужно проверить это. Давайте посмотрим, является ли AST, который мы получаем для определенного фрагмента кода, именно тем, который мы ожидаем.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | classMappingTest {     @testfun mapSimpleFile() {        val code = """var a = 1+ 2                     |a = 7* (2/ 3)""".trimMargin("|")        val ast = SandyParserFacade.parse(code).root!!.toAst()        val expectedAst = SandyFile(listOf(                VarDeclaration("a", SumExpression(IntLit("1"), IntLit("2"))),                Assignment("a", MultiplicationExpression(                        IntLit("7"),                        DivisionExpression(                                IntLit("2"),                                IntLit("3"))))))        assertEquals(expectedAst, ast)    }     @testfun mapCastInt() {        val code = "a = 7 as Int"        val ast = SandyParserFacade.parse(code).root!!.toAst()        val expectedAst = SandyFile(listOf(Assignment("a", TypeConversion(IntLit("7"), IntType))))        assertEquals(expectedAst, ast)    }     @testfun mapCastDecimal() {        val code = "a = 7 as Decimal"        val ast = SandyParserFacade.parse(code).root!!.toAst()        val expectedAst = SandyFile(listOf(Assignment("a", TypeConversion(IntLit("7"), DecimalType))))        assertEquals(expectedAst, ast)    }     @testfun mapPrint() {        val code = "print(a)"        val ast = SandyParserFacade.parse(code).root!!.toAst()        val expectedAst = SandyFile(listOf(Print(VarReference("a"))))        assertEquals(expectedAst, ast)    } } | 
Учитывая позиции
Все было бы хорошо: у нас есть чистая модель информации, представленной в коде. Метамодель и код отображения выглядят очень просто и понятно. Однако нам нужно добавить немного подробностей: положение узлов в исходном коде. Это необходимо при отображении ошибок пользователю. Мы хотим иметь возможность указывать позиции наших узлов AST, но мы не хотим, чтобы нас заставляли это делать. Таким образом, в зависимости от операций, которые нам нужно сделать, мы можем игнорировать или нет позиции. Рассмотрим тесты, которые мы написали до сих пор: не будет ли громоздким и раздражающим указание ложных позиций для всех узлов? Я так думаю.
Это новое определение узла и несколько вспомогательных классов:
| 1 2 3 4 5 6 7 8 9 | interfaceNode {    val position: Position?} data classPoint(val line: Int, val column: Int) data classPosition(val start: Point, val end: Point) fun pos(startLine:Int, startCol:Int, endLine:Int, endCol:Int) = Position(Point(startLine,startCol),Point(endLine,endCol)) | 
Нам также необходимо добавить позицию как необязательный параметр для всех классов. Это будет иметь значение по умолчанию null . Например, вот как выглядит SandyFile :
| 1 | data class SandyFile(val statements : List<Statement>, override val position: Position? = null) : Node | 
Отображение стало немного сложнее:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | fun SandyFileContext.toAst(considerPosition: Boolean = false) : SandyFile = SandyFile(this.line().map { it.statement().toAst(considerPosition) }, toPosition(considerPosition)) fun Token.startPoint() = Point(line, charPositionInLine) fun Token.endPoint() = Point(line, charPositionInLine + text.length) fun ParserRuleContext.toPosition(considerPosition: Boolean) : Position? {    returnif(considerPosition) Position(start.startPoint(), stop.endPoint()) elsenull} fun StatementContext.toAst(considerPosition: Boolean = false) : Statement = when (this) {    is VarDeclarationStatementContext -> VarDeclaration(varDeclaration().assignment().ID().text, varDeclaration().assignment().expression().toAst(considerPosition), toPosition(considerPosition))    is AssignmentStatementContext -> Assignment(assignment().ID().text, assignment().expression().toAst(considerPosition), toPosition(considerPosition))    is PrintStatementContext -> Print(print().expression().toAst(considerPosition), toPosition(considerPosition))    else-> throw UnsupportedOperationException(this.javaClass.canonicalName)} fun  ExpressionContext.toAst(considerPosition: Boolean = false) : Expression = when (this) {    is BinaryOperationContext -> toAst(considerPosition)    is IntLiteralContext -> IntLit(text, toPosition(considerPosition))    is DecimalLiteralContext -> DecLit(text, toPosition(considerPosition))    is ParenExpressionContext -> expression().toAst(considerPosition)    is VarReferenceContext -> VarReference(text, toPosition(considerPosition))    is TypeConversionContext -> TypeConversion(expression().toAst(considerPosition), targetType.toAst(considerPosition), toPosition(considerPosition))    else-> throw UnsupportedOperationException(this.javaClass.canonicalName)} fun TypeContext.toAst(considerPosition: Boolean = false) : Type = when (this) {    is IntegerContext -> IntType(toPosition(considerPosition))    is DecimalContext -> DecimalType(toPosition(considerPosition))    else-> throw UnsupportedOperationException(this.javaClass.canonicalName)} fun  BinaryOperationContext.toAst(considerPosition: Boolean = false) : Expression = when (operator.text) {    "+"-> SumExpression(left.toAst(considerPosition), right.toAst(considerPosition), toPosition(considerPosition))    "-"-> SubtractionExpression(left.toAst(considerPosition), right.toAst(considerPosition), toPosition(considerPosition))    "*"-> MultiplicationExpression(left.toAst(considerPosition), right.toAst(considerPosition), toPosition(considerPosition))    "/"-> DivisionExpression(left.toAst(considerPosition), right.toAst(considerPosition), toPosition(considerPosition))    else-> throw UnsupportedOperationException(this.javaClass.canonicalName)} | 
На этом этапе все предыдущие тесты продолжают проходить, но мы хотим добавить тест, чтобы убедиться, что позиция определена правильно:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @testfun mapSimpleFileWithPositions() {        val code = """var a = 1+ 2                     |a = 7* (2/ 3)""".trimMargin("|")        val ast = SandyParserFacade.parse(code).root!!.toAst(considerPosition = true)        val expectedAst = SandyFile(listOf(                VarDeclaration("a",                        SumExpression(                                IntLit("1", pos(1,8,1,9)),                                IntLit("2", pos(1,12,1,13)),                                pos(1,8,1,13)),                        pos(1,0,1,13)),                Assignment("a",                        MultiplicationExpression(                            IntLit("7", pos(2,4,2,5)),                            DivisionExpression(                                    IntLit("2", pos(2,9,2,10)),                                    IntLit("3", pos(2,13,2,14)),                                    pos(2,9,2,14)),                            pos(2,4,2,15)),                        pos(2,0,2,15))),                pos(1,0,2,15))        assertEquals(expectedAst, ast)    } | 
Выводы
Дерево синтаксического анализа содержит информацию, организованную наиболее удобным для синтаксического анализатора способом. Обычно это не самый удобный способ для последующих шагов. Подумайте о реализуемом правиле объявления переменных, используя повторно правило присваивания: конечно, это делает грамматику короче и имеет смысл для дерева разбора. Однако с логической точки зрения эти два элемента разделены, и в AST они действительно есть.
Большинство остальных наших инструментов будут работать на AST, поэтому лучше потратить некоторое время на работу с AST, что имеет смысл.
| Ссылка: | Создание компилятора для вашего собственного языка: от дерева разбора до абстрактного синтаксического дерева от нашего партнера по JCG |