从定义到AST及其遍历方式,一文带你搞懂Antlr4

摘要:本文将首先介绍Antlr4 grammer的定义方式,如何经过Antlr4 grammer生成对应的AST,以及Antlr4 的两种AST遍历方式:Visitor方式和Listener方式。

1. Antlr4简单介绍

Antlr4(Another Tool for Language Recognition)是一款基于Java开发的开源的语法分析器生成工具,可以根据语法规则文件生成对应的语法分析器,普遍应用于DSL构建,语言词法语法解析等领域。如今在很是多的流行的框架中都用使用,例如,在构建特定语言的AST方面,CheckStyle工具,就是基于Antlr来解析Java的语法结构的(当前Java Parser是基于JavaCC来解析Java文件的,听说有规划在下个版本改用Antlr来解析),还有就是普遍应用在DSL构建上,著名的Eclipse Xtext就有使用Antlr。html

Antlr能够生成不一样target的AST(https://www.antlr.org/download.html),包括Java、C++、JS、Python、C#等,能够知足不一样语言的开发需求当前Antlr最新稳定版本为4.9Antlr4官方github仓库中,已经有数十种语言的grammerhttps://github.com/antlr/grammars-v4,不过虽然这么多语言的规则文法定义都在一个仓库中,可是每种语言的grammerlicense是不同的,若是要使用,须要参考每种语言本身的语法结构的license)。java

本文将首先介绍Antlr4 grammer的定义方式(简单介绍语法结构,并介绍如何基于IDEA Antlr4插件进行调试),而后介绍如何经过Antlr4 grammer生成对应的AST,最后介绍Antlr4 的两种AST遍历方式:Visitor方式和Listener方式。git

2. Antlr4规则文法

下面简单介绍一部分Antlr4的g4(grammar)文件的写法 (主要参考Antlr4官方wikihttps://github.com/antlr/antlr4/blob/master/doc/index.md)。 最有效的学习Antlr4的规则文法的写法的方法,就是参考已有的规则文法,你们在学习中,能够参考已有语言的文法。并且Antlr4已经实现了数十种语言的文法,若是须要本身定义,能够参考和本身的语言最接近的文法来开发。github

2.1 Antlr4规则基本语法和关键字

首先,若是有一点儿C或者Java基础,对上手Antlr4 g4的文法很是快。主要有下面的一些文法结构:设计模式

  • 注释:和Java的注释彻底一致,也可参考C的注释,只是增长了JavaDoc类型的注释;
  • 标志符:参考Java或者C的标志符命名规范,针对Lexer 部分的 Token 名的定义,采用全大写字母的形式,对于parser rule命名,推荐首字母小写的驼峰命名;
  • 不区分字符和字符串,都是用单引号引发来的,同时,虽然Antlr g4支持 Unicode编码(即支持中文编码),可是建议你们尽可能还有英文;
  • Action,行为,主要有@header 和@members,用来定义一些须要生成到目标代码中的行为,例如,能够经过@header设置生成的代码的package信息,@members能够定义额外的一些变量到Antlr4语法文件中;
  • Antlr4语法中,支持的关键字有:import, fragment, lexer, parser, grammar, returns, locals, throws, catch, finally, mode, options, tokens。

2.2 Antlr4语法介绍

2.2.1语法文件的总体结构及写法示例

Antlr4总体结构以下:api

/** Optional javadoc style comment */

grammar Name;

options {...}

import ... ;

 

tokens {...}

channels {...} // lexer only

@actionName {...}

 

rule1 // parser and lexer rules, possibly intermingled

...

ruleN

通常若是语法很是复杂,会基于LexerParser写到两个不一样的文件中(例如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规则的写法,下面介绍一部分比较重要的规则的写法。maven

2.2.2 替代标签

首先,如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
    ;

经过在语句后面,添加 #替代标签,能够将语句转换为这些替代标签,从而加以区分。

2.2.3 操做符优先级处理

默认状况下,ANTLR从左到右结合运算符,然而某些像指数群这样的运算符则是从右到左。可使用选项assoc手动指定运算符记号上的相关性。以下面的操做:

expr : expr '^'<assoc=right> expr

^ 表示指数运算,增长 assoc=right,表示该运算符是右结合。

实际上,Antlr4 已经对一些经常使用的操做符的优先级进行了处理,例如加减乘除等,这些就不须要再特殊处理。

2.2.4 隐藏通道

不少信息,例如注释、空格等,是结果信息生成不须要处理的,可是咱们又不适合直接丢弃,安全地忽略掉注释和空格的方法是把这些发送给语法分析器的记号放到一个“隐藏通道”中,语法分析器仅须要调协到单个通道便可。咱们能够把任何咱们想要的东西传递到其它通道中。在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遍历获取到。

2.2.5 常见词法结构

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]*
    ;

3. 基于IDEA调试Antlr4语法规则(文法可视化)

若是要安装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 : '-';

INT : Digit+;
Digit : [0-9];

WS : [ \t\u000C\r\n]+ -> skip;

SHEBANG : '#' '!' ~('\n'|'\r')* -> channel(HIDDEN);

4. Antlr4生成并遍历AST

4.1 生成源码文件

这一步介绍两种生成解析语法树的两种方法,供参考:

  • Maven Antlr4插件自动生成(针对Java工程,也能够用于Gradle)

pom.xml设置Antlr4 Maven插件,能够经过执行 mvn generate-sources自动生成须要的代码(参考连接: https://www.antlr.org/api/maven-plugin/latest/antlr4-mojo.html,主要的意义在于,代码入库的时候,不须要再将生成的这些语法文件入库,减小库里面的代码冗余,只包含本身开发的代码,不会有自动生成的代码,也不须要作clean code整改),下面是一个示例:

<build>
    <plugins>
      <plugin>
        <groupId>org.antlr</groupId>
        <artifactId>antlr4-maven-plugin</artifactId>
        <version>4.3</version>
        <executions>
          <execution>
            <id>antlr</id>
            <goals>
              <goal>antlr4</goal>
            </goals>
            <phase>generate-sources</phase>
          </execution>
        </executions>
        <configuration>
          <sourceDirectory>${basedir}/src/main/resources</sourceDirectory>
          <outputDirectory>${project.build.directory}/generated-sources/antlr4/zmj/test/antlr4/parser</outputDirectory>
          <listener>true</listener>
          <visitor>true</visitor>
          <treatWarningsAsErrors>true</treatWarningsAsErrors>
        </configuration>
      </plugin>
    </plugins>
  </build>

按照上面设置后,只须要执行 mvn generate-sources 便可在maven工程中自动生成代码。

  • 命令行方式

主要参考连接(https://www.antlr.org/download.html),有每种语言的语法配置,咱们这里考虑下载Antlr4完整jar

下载好后(antlr-4.9-complete.jar),可使用以下命令来生成须要的信息:

java -jar antlr-4.9-complete.jar -Dlanguage=Python3 -visitor Test.g4

这样就能够生成Python3 target的源码,支持的源码能够从上面连接查看,若是不但愿生成Listener,能够添加参数 -no-listener

4.2 访问者模式遍历Antlr4语法树

Antlr4在AST遍历时,支持两种设计模式:访问者设计模式 和 监听器模式。

对于 访问者设计模式,咱们须要本身定义对 AST 的访问(https://xie.infoq.cn/article/5f80da3c014fd69f8dbe09b28,这是一篇针对访问者设计模式的介绍,你们能够参考)。下面直接经过代码展现访问者模式在Antlr4中使用(基于第3章的例子):

import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import zmj.test.antlr4.parser.TestBaseVisitor;
import zmj.test.antlr4.parser.TestLexer;
import zmj.test.antlr4.parser.TestParser;

public class App {
    public static void main(String[] args) {
        CharStream input = CharStreams.fromString("12*2+12");
        TestLexer lexer=new TestLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        TestParser parser = new TestParser(tokens);
        TestParser.ExprContext tree = parser.expr();
        TestVisitor tv = new TestVisitor();
        tv.visit(tree);
    }

    static class TestVisitor extends TestBaseVisitor<Void> {
        @Override
        public Void visitAdd(TestParser.AddContext ctx) {
            System.out.println("========= test add");
            System.out.println("first arg: " + ctx.expr(0).getText());
            System.out.println("second arg: " + ctx.expr(1).getText());
            return super.visitAdd(ctx);
        }
    }
}

如上,main方法中,解析出了表达式的AST结构,同时在源码中也定义了一个Visitor:TestVisitor,访问AddContext,而且打印该加表达式的先后两个表达式,上面例子的输出为:

========= test add
first arg: 12*2
second arg: 12

4.2 监听器模式(观察者模式)

对于监听器模式,就是经过监听某对象,若是该对象上有特定的事件发生,则触发该监听行为执行。好比有个监控(监听器),监控的是大门(事件对象),若是发生了闯门的行为(事件源),则进行报警(触发操做行为)。

在Antlr4中,若是使用监听器模式,首先须要开发一个监听器,该监听器能够监听每一个AST节点(例如表达式、语句等)的不一样的行为(例如进入该节点、结束该节点)。在使用时,Antlr4会对生成的AST进行遍历(ParseTreeWalker),若是遍历到某个具体的节点,而且执行了特定行为,就会触发监听器的事件。

监听器方法是没有返回值的(即返回类型是void)。所以须要一种额外的数据结构(能够经过Map或者栈)来存储当次的计算结果,供下一次计算调用。

通常来讲,面向程序静态分析时,都是使用访问者模式的,不多使用监听器模式(没法主动控制遍历AST的顺序,不方便在不一样节点遍历之间传递数据),用法对我们也不友好,因此本文不介绍监听器模式,若是有兴趣,能够本身搜索测试使用。

5. Antlr4词法解析和语法解析

这部分实际上,算是Antlr4最基础的内容,可是放到最后一部分来说,有特定的目的,就是探讨一下词法解析和语法解析的界限,以及Antlr4的结果的处理。

5.1 Antlr4执行阶段

如前面的语法定义,分为Lexer和Parser,实际上表示了两个不一样的阶段:

  • 词法分析阶段:对应于Lexer定义的词法规则,解析结果为一个一个的Token;
  • 解析阶段:根据词法,构造出来一棵解析树或者语法树。

以下图所示:

5.2 词法解析和语法解析的调和

首先,咱们应该有个广泛的认知:语法解析相对于词法解析,会产生更多的开销,因此,应该尽可能将某些可能的处理在词法解析阶段完成,减小语法解析阶段的开销,主要下面的这些例子:

  • 合并语言不关心的标记,例如,某些语言(例如js)不区分int、double,只有 number,那么在词法解析阶段,就不须要将int和double区分开,统一合并为一个number;
  • 空格、注释等信息,对于语法解析并没有大的帮助,能够在词法分析阶段剔除掉;
  • 诸如标志符、关键字、字符串和数字这样的经常使用记号,均应该在词法解析时完成,而不要到语法解析阶段再进行。

可是,这样的操做在节省了语法分析的开销以外,其实对咱们也产生了一些影响:

  • 虽然语言不区分类型,例如只有 number,没有 int 和 double 等,可是面向静态代码分析,咱们可能须要知道确切的类型来帮助分析特定的缺陷;
  • 虽然注释对代码帮助不大,可是咱们有时候也须要解析注释的内容来进行分析,若是没法在语法解析的时候获取,那么就须要遍历Token,从而致使静态代码分析开销更大等;

这样的一些问题该如何处理呢?

5.3 解析树vs语法树

大部分的资料中,都把Antlr4生成的树状结构,称为解析树或者是语法树,可是,若是咱们细究的话,可能说成是解析树更加准确,由于Antlr4的结果,只是简单的文法解析,不能称之为语法树(语法树应该是可以体现出来语法特性的信息),如上面的那些问题,就很难在Antlr4生成的解析树上获取到。

因此,如今不少工具,基于Antlr4进行封装,而后进行了更进一步地处理,从而获取到了更加丰富的语法树,例如CheckStyle。所以,若是经过Antlr4解析语言简单使用,能够直接基于Antlr4的结果开发,可是若是要进行更加深刻的处理,就须要对Antlr4的结果进行更进一步的处理,以更符合咱们的使用习惯(例如,Java Parser格式的Java的AST,Clang格式的C/C++的AST),而后才能更好地在上面进行开发。

本文分享自华为云社区《Antlr4简明使用教程》,原文做者:maijun 。

 

点击关注,第一时间了解华为云新鲜技术~

相关文章
相关标签/搜索