Этот пакет позволяет вам использовать грамматики ANTLR и использовать выходные данные синтаксического анализатора для создания абстрактного синтаксического дерева (AST).
pip install antlr-ast
Примечание: этот пакет не совместим с Python2.
# may need:
# pip install pytest
py.test
Использование antlr-ast
включает в себя четыре шага:
parse
для получения выходных данных среды выполнения ANTLR на основе сгенерированных файлов грамматики.process_tree
на выходе предыдущего шагаBaseAstVisitor
(настраиваемый путем предоставления подкласса) преобразует выходные данные ANTLR в сериализуемое дерево BaseNode
, динамически создаваемое на основе правил грамматики ANTLR.BaseNodeTransformer
можно использовать для преобразования узлов любого типа.В следующих разделах эти шаги рассматриваются более подробно.
Чтобы визуализировать процесс создания и преобразования этих деревьев разбора, вы можете использовать этот ast-viewer.
Примечание. Для этой части руководства вам необходимо знать, как анализировать код.
См. руководство по началу работы с ANTLR, если вы никогда не устанавливали ANTLR.
Мега-учебник ANTLR содержит полезные примеры Python.
На этой странице объясняется, как писать правила синтаксического анализатора ANTLR.
Приведенное ниже определение правила представляет собой пример с описательными именами для важных элементов грамматики парсера ANTLR:
rule_name : rule_element? rule_element_label= ' literal ' #RuleAlternativeLabel
| TOKEN + #RuleAlternativeLabel
;
Элемент правила и альтернативные метки являются необязательными.
+
, *
, ?
, |
и ()
имеют то же значение, что и в RegEx.
Ниже мы воспользуемся простой грамматикой, чтобы объяснить, как работает antlr-ast
. Эту грамматику можно найти в /tests/Expr.g4
.
grammar Expr;
// parser
expr : left=expr op=( ' + ' | ' - ' ) right=expr #BinaryExpr
| NOT expr #NotExpr
| INT #Integer
| ' ( ' expr ' ) ' #SubExpr
;
// lexer
INT : [0-9]+ ; // match integers
NOT : ' not ' ;
WS : [ t]+ -> skip ; // toss out whitespace
ANTLR может использовать приведенную выше грамматику для создания синтаксического анализатора на нескольких языках. Чтобы создать парсер Python, вы можете использовать следующую команду.
antlr4 -Dlanguage=Python3 -visitor /tests/Expr.g4
Это создаст несколько файлов в каталоге /tests/
, включая лексер ( ExprLexer.py
), анализатор ( ExprParser.py
) и посетителя ( ExprVisitor.py
).
Вы можете использовать и импортировать их непосредственно в Python. Например, из корня этого репо:
from tests import ExprVisitor
Чтобы упростить использование сгенерированных файлов, они помещаются в пакет antlr_py
. Файл __init__.py
экспортирует сгенерированные файлы под псевдонимом, который не включает имя грамматики.
Подкласс BaseNode
имеет поля для всех элементов правил и метки для всех меток элементов правил в соответствующем правиле грамматики. И поля, и метки доступны как свойства экземпляров BaseNode
. Метки имеют приоритет над полями, если имена могут конфликтовать.
Имя BaseNode
— это имя соответствующего грамматического правила ANTLR, но начинающееся с символа верхнего регистра. Если для правила ANTLR указаны альтернативные метки, они используются вместо имени правила.
Обычно между правилами ANTLR и концепциями языка не существует сопоставления 1-к-1: иерархия правил более вложена. Преобразования можно использовать, чтобы сделать исходное дерево BaseNodes на основе правил ANTLR более похожим на AST.
BaseNodeTransformer
будет проходить по дереву от корневого узла к листовым узлам. При посещении узла есть возможность его трансформировать. Дерево обновляется преобразованным узлом, прежде чем продолжить обход дерева.
Чтобы определить преобразование узла, добавьте статический метод в подкласс BaseNodeTransformer
переданный process_tree
.
visit_<BaseNode>
, где <BaseNode>
следует заменить именем подкласса BaseNode
для преобразования.Это простой пример:
class Transformer ( BaseNodeTransformer ):
@ staticmethod
def visit_My_antlr_rule ( node ):
return node . name_of_part
Пользовательский узел может представлять часть анализируемого языка, тип узла, присутствующего в AST.
Чтобы упростить возврат пользовательского узла, вы можете определить подклассы AliasNode
. Обычно поля AliasNode
похожи на символические ссылки для навигации по дереву BaseNode
.
Экземпляры пользовательских узлов создаются из BaseNode
. Поля и метки исходного BaseNode
также доступны в AliasNode
. Если имя поля AliasNode
конфликтует с ними, оно имеет приоритет при доступе к этому свойству.
Вот как выглядит пользовательский узел:
class NotExpr ( AliasNode ):
_fields_spec = [ "expr" , "op=NOT" ]
Этот код определяет пользовательский узел NotExpr
с expr
и полем op
.
Свойство класса _fields_spec
представляет собой список, определяющий поля, которые должен иметь пользовательский узел.
Вот как спецификация поля в этом списке используется при создании пользовательского узла из BaseNode
(исходного узла):
None
None
AliasNode
, а правая сторона — это путь, который следует пройти, начиная с исходного узла, чтобы получить узел, который должен быть значением для поля в пользовательский узел. Части этого пути разделяются с помощью .
Чтобы использовать этот пользовательский узел, добавьте к преобразователю метод:
class Transformer ( BaseNodeTransformer ):
# ...
# here the BaseNode name is the same as the custom node name
# but that isn't required
@ staticmethod
def visit_NotExpr ( node ):
return NotExpr . from_spec ( node )
Вместо определения методов класса преобразователя для использования пользовательских узлов это можно сделать автоматически:
Transformer . bind_alias_nodes ( alias_nodes )
Чтобы это работало, классы AliasNode
в списке должны иметь свойство класса _rules
со списком имен BaseNode
которые оно должно преобразовать.
Вот результат:
class NotExpr ( AliasNode ):
_fields_spec = [ "expr" , "op=NOT" ]
_rules = [ "NotExpr" ]
class Transformer ( BaseNodeTransformer ):
pass
alias_nodes = [ NotExpr ]
Transformer . bind_alias_nodes ( alias_nodes )
Элемент в _rules
также может быть кортежем. В этом случае первый элемент кортежа — это имя BaseNode
, а второй элемент — имя метода класса пользовательского узла.
В приведенном выше примере это бесполезно, но эквивалентно этому:
class NotExpr ( AliasNode ):
_fields_spec = [ "expr" , "op=NOT" ]
_rules = [( "NotExpr" , "from_not" )]
@ classmethod
def from_not ( cls , node ):
return cls . from_spec ( node )
class Transformer ( BaseNodeTransformer ):
pass
alias_nodes = [ NotExpr ]
Transformer . bind_alias_nodes ( alias_nodes )
Легко использовать дерево, состоящее из смеси AliasNode
и динамических BaseNode
: все дерево представляет собой просто вложенный объект Python.
При поиске узлов в дереве может учитываться приоритет узлов. По умолчанию BaseNode
имеют приоритет 3, а AliasNode
— приоритет 2.
При написании кода для работы с деревьями на него могут влиять изменения в грамматике, преобразованиях и пользовательских узлах. Грамматика, скорее всего, изменится.
Чтобы обновления грамматики не влияли на ваш код, не полагайтесь на BaseNode
. Вы по-прежнему можете проверить, установлены ли в родительском узле AliasNode
BaseNode
правильные поля, и выполнить поиск вложенных AliasNode
в поддереве.
Если вы полагаетесь на BaseNode
, код может сломаться из-за добавления AliasNode
, которые заменяют некоторые из них, если имя поля конфликтует с именем поля в используемом BaseNode
.