当前位置 博文首页 > 华为云开发者社区:从定义到AST及其遍历方式,一文带你搞懂Antlr
摘要:本文将首先介绍Antlr4 grammer的定义方式,如何通过Antlr4 grammer生成对应的AST,以及Antlr4 的两种AST遍历方式:Visitor方式和Listener方式。
Antlr4(Another Tool for Language Recognition)是一款基于Java开发的开源的语法分析器生成工具,能够根据语法规则文件生成对应的语法分析器,广泛应用于DSL构建,语言词法语法解析等领域。现在在非常多的流行的框架中都用使用,例如,在构建特定语言的AST方面,CheckStyle工具,就是基于Antlr来解析Java的语法结构的(当前Java Parser是基于JavaCC来解析Java文件的,据说有规划在下个版本改用Antlr来解析),还有就是广泛应用在DSL构建上,著名的Eclipse Xtext就有使用Antlr。
Antlr可以生成不同target的AST(https://www.antlr.org/download.html),包括Java、C++、JS、Python、C#等,可以满足不同语言的开发需求。当前Antlr最新稳定版本为4.9,Antlr4官方github仓库中,已经有数十种语言的grammer(https://github.com/antlr/grammars-v4,不过虽然这么多语言的规则文法定义都在一个仓库中,但是每种语言的grammer的license是不一样的,如果要使用,需要参考每种语言自己的语法结构的license)。
本文将首先介绍Antlr4 grammer的定义方式(简单介绍语法结构,并介绍如何基于IDEA Antlr4插件进行调试),然后介绍如何通过Antlr4 grammer生成对应的AST,最后介绍Antlr4 的两种AST遍历方式:Visitor方式和Listener方式。
下面简单介绍一部分Antlr4的g4(grammar)文件的写法(主要参考Antlr4官方wiki:https://github.com/antlr/antlr4/blob/master/doc/index.md)。最有效的学习Antlr4的规则文法的写法的方法,就是参考已有的规则文法,大家在学习中,可以参考已有语言的文法。而且Antlr4已经实现了数十种语言的文法,如果需要自己定义,可以参考和自己的语言最接近的文法来开发。
首先,如果有一点儿C或者Java基础,对上手Antlr4 g4的文法非常快。主要有下面的一些文法结构:
Antlr4整体结构如下:
/** Optional javadoc style comment */ grammar Name; options {...} import ... ; tokens {...} channels {...} // lexer only @actionName {...} rule1 // parser and lexer rules, possibly intermingled ... ruleN
一般如果语法非常复杂,会基于Lexer和Parser写到两个不同的文件中(例如Java,可参考:https://github.com/antlr/grammars-v4/tree/master/java/java8),如果语法比较简单,可以只写到一个文件中(例如Lua,可参考:https://github.com/antlr/grammars-v4/blob/master/lua/Lua.g4)。
下面我们结合Lua.g4中的一部分语法结构,介绍使用方法。写Antlr4的文法,需要依据源码的结构来决定。定义时,依据源码文件的写法,从上到下开始构造语法结构。例如,下面是Lua.g4的一部分:
chunk : block EOF ; block : stat* retstat? ; stat : ';' | varlist '=' explist | functioncall | label | 'break' | 'goto' NAME | 'do' block 'end' | 'while' exp 'do' block 'end' | 'repeat' block 'until' exp | 'if' exp 'then' block ('elseif' exp 'then' block)* ('else' block)? 'end' | 'for' NAME '=' exp ',' exp (',' exp)? 'do' block 'end' | 'for' namelist 'in' explist 'do' block 'end' | 'function' funcname funcbody | 'local' 'function' NAME funcbody | 'local' attnamelist ('=' explist)? ; attnamelist : NAME attrib (',' NAME attrib)* ;
如上语法中,整个文件被表示成一个chunk,chunk表示为一个block和一个文件结束符(EOF);block又被表示为一系列的语句的集合,而每一种语句又有特定的语法结构,包含了特定的表达式、关键字、变量、常量等信息,然后递归表达式的文法组成,变量的写法等,最终全部都归结到Lexer(Token)上,递归树结束。
上面其实已经可以看到Antlr4规则的写法,下面介绍一部分比较重要的规则的写法。
首先,如2.2.1节的代码所示,stat可以有非常多的类型,例如变量定义、函数定义、if、while等,这些都没有进行区分,这样解析出来语法树时,会很不清晰,需要结合很多的标记完成具体语句的识别,这种情况下,我们可以结合替代标签完成区分,如下代码:
stat : ';' | varlist '=' explist #varListStat | functioncall #functionCallStat | label #labelStat | 'break' #breakStat | 'goto' NAME #gotoStat | 'do' block 'end' #doStat | 'while' exp 'do' block 'end' #whileStat | 'repeat' block 'until' exp #repeatStat | 'if' exp 'then' block ('elseif' exp 'then' block)* ('else' block)? 'end' #ifStat | 'for' NAME '=' exp ',' exp (',' exp)? 'do' block 'end' #forStat | 'for' namelist 'in' explist 'do' block 'end' #forInStat | 'function' funcname funcbody #functionDefStat | 'local' 'function' NAME funcbody #localFunctionDefStat | 'local' attnamelist ('=' explist)? #localVarListStat ;
通过在语句后面,添加 #替代标签,可以将语句转换为这些替代标签,从而加以区分。
默认情况下,ANTLR从左到右结合运算符,然而某些像指数群这样的运算符则是从右到左。可以使用选项assoc手动指定运算符记号上的相关性。如下面的操作:
expr : expr '^'<assoc=right> expr
^ 表示指数运算,增加 assoc=right,表示该运算符是右结合。
实际上,Antlr4 已经对一些常用的操作符的优先级进行了处理,例如加减乘除等,这些就不需要再特殊处理。
很多信息,例如注释、空格等,是结果信息生成不需要处理的,但是我们又不适合直接丢弃,安全地忽略掉注释和空格的方法是把这些发送给语法分析器的记号放到一个“隐藏通道”中,语法分析器仅需要调协到单个通道即可。我们可以把任何我们想要的东西传递到其它通道中。在Lua.g4中,这类信息的处理如下:
COMMENT : '--[' NESTED_STR ']' -> channel(HIDDEN) ; LINE_COMMENT : '--' ( // -- | '[' '='* // --[== | '[' '='* ~('='|'['|'\r'|'\n') ~('\r'|'\n')* // --[==AA | ~('['|'\r'|'\n') ~('\r'|'\n')* // --AAA ) ('\r\n'|'\r'|'\n'|EOF) -> channel(HIDDEN) ; WS : [ \t\u000C\r\n]+ -> skip ; SHEBANG : '#' '!' ~('\n'|'\r')* -> channel(HIDDEN) ;
放到 channel(HIDDEN) 中的 Token,不会被语法解析阶段处理,但是可以通过Token遍历获取到。
Antlr4采用BNF范式,用’|’表示分支选项,’*’表示匹配前一个匹配项0次或者多次,’+’ 表示匹配前一个匹配项至少一次。下面介绍几种常见的词法举例(均来自Lua.g4文件):
1) 注释信息
COMMENT : '--[' NESTED_STR ']' -> channel(HIDDEN) ; LINE_COMMENT : '--' ( // -- | '[' '='* // --[== | '[' '='* ~('='|'['|'\r'|'\n') ~('\r'|'\n')* // --[==AA | ~('['|'\r'|'\n') ~('\r'|'\n')* // --AAA ) ('\r\n'|'\r'|'\n'|EOF) -> channel(HIDDEN) ;
2) 数字
INT : Digit+ ; Digit : [0-9] ;
3) ID(命名)
NAME : [a-zA-Z_][a-zA-Z_0-9]* ;
如果要安装Antlr4,选择 File -> Settings -> Plugins,然后在搜索框搜索 Antlr安装即可,可以选择安装搜索出来的最新版本,下图是刚刚安装的ANTLR v4,版本是v1.15,支持最新的Antlr 4.9版本。
基于IDEA调试Antlr4语法一般步骤:
1) 创建一个调试工程,并创建一个g4文件
这里,我自己测试用Java开发,所以创建的是一个Maven工程,g4文件放在了src/main/resources 目录下,取名 Test.g4
2)写一个简单的语法结构
这里我们参考写一个加减乘除操作的表达式,然后在赋值操作对应的Rule上右键,可选择测试:
如上图,expr 表示的是一个乘法操作,所以我们如下测试:
但是,如果改成一个加法操作,则无法识别,只能识别到第一个数字。
这种情况下,就需要继续扩充 expr的定义,丰富不同的语法,来继续支持其他的语法,如下:
还可以继续扩充其他类型的支持,这样一步步将整个语言的语法都支持完整。这里,我们形成的一个完整的格式如下(表示整形数字的加减乘除):
grammar Test; @header { package zmj.test.antlr4.parser; } stmt : expr; expr : expr NUL expr # Mul | expr ADD expr # Add | expr DIV expr # Div | expr MIN expr # Min | INT # Int ; NUL : '*'; ADD : '+'; DIV : '/'; MIN : '-'