摘要:本文将首先详情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,包括Java、C++、JS、Python、C#等,可以满足不同语言的开发需求。当前Antlr最新稳固版本为4.9,Antlr4官方github仓库中,已经有数十种语言的grammer,不过尽管这么多语言的规则文法定义都在一个仓库中,但是每种语言的grammer的license是不一样的,假如要使用,需要参考每种语言自己的语法结构的license)。
本文将首先详情Antlr4 grammer的定义方式(简单详情语法结构,并详情如何基于IDEA Antlr4插件进行调试),而后详情如何通过Antlr4 grammer生成对应的AST,最后详情Antlr4 的两种AST遍历方式:Visitor方式和Listener方式。
下面简单详情一部分Antlr4的g4(grammar)文件的写法(主要参考Antlr4官方wiki)。最有效的学习Antlr4的规则文法的写法的方法,就是参考已有的规则文法,大家在学习中,可以参考已有语言的文法。而且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。
Antlr4整体结构如下:
一般假如语法非常复杂,会基于Lexer和Parser写到两个不同的文件中(例如Java,可参考: antlr/gram...),假如语法比较简单,可以只写到一个文件中(例如Lua,可参考: antlr/gram...)。
下面我们结合Lua.g4中的一部分语法结构,详情使用方法。写Antlr4的文法,需要依据源码的结构来决定。定义时,依据源码文件的写法,从上到下开始构造语法结构。例如,下面是Lua.g4的一部分:
如上语法中,整个文件被表示成一个chunk,chunk表示为一个block和一个文件结束符(EOF);block又被表示为一系列的语句的集合,而每一种语句又有特定的语法结构,包含了特定的表达式、关键字、变量、常量等信息,而后递归表达式的文法组成,变量的写法等,最终一律都归结到Lexer(Token)上,递归树结束。
上面其实已经可以看到Antlr4规则的写法,下面详情一部分比较重要的规则的写法。
首先,如2.2.1节的代码所示,stat可以有非常多的类型,例如变量定义、函数定义、if、while等,这些都没有进行区分,这样解析出来语法树时,会很不清晰,需要结合很多的标记完成具体语句的识别,这种情况下,我们可以结合替代标签完成区分,如下代码:
通过在语句后面,增加 #替代标签,可以将语句转换为这些替代标签,从而加以区分。
默认情况下,ANTLR从左到右结合运算符,然而某些像指数群这样的运算符则是从右到左。可以使用选项assoc手动指定运算符记号上的相关性。如下面的操作:
^ 表示指数运算,添加 assoc=right,表示该运算符是右结合。
实际上,Antlr4 已经对少量常用的操作符的优先级进行了解决,例如加减乘除等,这些就不需要再特殊解决。
很多信息,例如注释、空格等,是结果信息生成不需要解决的,但是我们又不适合直接丢弃,安全地忽略掉注释和空格的方法是把这些发送给语法分析器的记号放到一个“隐藏通道”中,语法分析器仅需要调协到单个通道就可。我们可以把任何我们想要的东西传递到其它通道中。在Lua.g4中,这类信息的解决如下:
放到 channel(HIDDEN) 中的 Token,不会被语法解析阶段解决,但是可以通过Token遍历获取到。
Antlr4采用BNF范式,用’|’表示分支选项,’*’表示匹配前一个匹配项0次或者者屡次,’+’ 表示匹配前一个匹配项至少一次。下面详情几种常见的词法举例(均来自Lua.g4文件):
1) 注释信息
2) 数字
3) ID(命名)
假如要安装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的定义,丰富不同的语法,来继续支持其余的语法,如下:
还可以继续扩充其余类型的支持,这样一步步将整个语言的语法都支持完整。这里,我们形成的一个完整的格式如下(表示整形数字的加减乘除):
这一步详情两种生成解析语法树的两种方法,供参考:
Maven Antlr4插件自动生成(针对Java工程,也可以用于Gradle)
pom.xml设置Antlr4 Maven插件,可以通过执行 mvn generate-sources自动生成需要的代码(主要的意义在于,代码入库的时候,不需要再将生成的这些语法文件入库,减少库里面的代码冗余,只包含自己开发的代码,不会有自动生成的代码,也不需要做clean code整改),下面是一个示例:
按照上面设置后,只要要执行 mvn generate-sources 就可在maven工程中自动生成代码。
命令行方式
有每种语言的语法配置,我们这里考虑下载Antlr4完整jar:
下载好后(antlr-4.9-complete.jar),可以使用如下命令来生成需要的信息:
这样即可以生成Python3 target的源码,支持的源码可以从上面链接查看,假如不希望生成Listener,可以增加参数 -no-listener
Antlr4在AST遍历时,支持两种设计模式:访问者设计模式 和 监听器模式。
对于 访问者设计模式,我们需要自己定义对 AST 的访问(一篇针对访问者设计模式的详情,大家可以参考)。下面直接通过代码展现访问者模式在Antlr4中使用(基于第3章的例子):
如上,main方法中,解析出了表达式的AST结构,同时在源码中也定义了一个Visitor:TestVisitor,访问AddContext,并且打印该加表达式的前后两个表达式,上面例子的输出为:
对于监听器模式,就是通过监听某对象,假如该对象上有特定的事件发生,则触发该监听行为执行。比方有个监控(监听器),监控的是大门(事件对象),假如发生了闯门的行为(事件源),则进行报警(触发操作行为)。
在Antlr4中,假如使用监听器模式,首先需要开发一个监听器,该监听器可以监听每个AST节点(例如表达式、语句等)的不同的行为(例如进入该节点、结束该节点)。在使用时,Antlr4会对生成的AST进行遍历(ParseTreeWalker),假如遍历到某个具体的节点,并且执行了特定行为,就会触发监听器的事件。
监听器方法是没有返回值的(即返回类型是void)。因而需要一种额外的数据结构(可以通过Map或者者栈)来存储当次的计算结果,供下一次计算调用。
一般来说,面向程序静态分析时,都是使用访问者模式的,很少使用监听器模式(无法主动控制遍历AST的顺序,不方便在不同节点遍历之间传递数据),用法对咱们也不友好,所以本文不详情监听器模式,假如有兴趣,可以自己搜索测试使用。
这部分实际上,算是Antlr4最基础的内容,但是放到最后一部分来讲,有特定的目的,就是讨论一下词法解析和语法解析的界限,以及Antlr4的结果的解决。
如前面的语法定义,分为Lexer和Parser,实际上表示了两个不同的阶段:
词法分析阶段:对应于Lexer定义的词法规则,解析结果为一个一个的Token;
解析阶段:根据词法,构造出来一棵解析树或者者语法树。
如下图所示:
首先,我们应该有个普遍的认知:语法解析相对于词法解析,会产生更多的开销,所以,应该尽量将某些可能的解决在词法解析阶段完成,减少语法解析阶段的开销,主要下面的这些例子:
合并语言不关心的标记,例如,某些语言(例如js)不区分int、double,只有 number,那么在词法解析阶段,就不需要将int和double区分开,统一合并为一个number;
空格、注释等信息,对于语法解析并无大的帮助,可以在词法分析阶段剔除掉;
诸如标志符、关键字、字符串和数字这样的常用记号,均应该在词法解析时完成,而不要到语法解析阶段再进行。
但是,这样的操作在节省了语法分析的开销之外,其实对我们也产生了少量影响:
尽管语言不区分类型,例如只有 number,没有 int 和 double 等,但是面向静态代码分析,我们可能需要知道确切的类型来帮助分析特定的缺陷;
尽管注释对代码帮助不大,但是我们有时候也需要解析注释的内容来进行分析,假如无法在语法解析的时候获取,那么就需要遍历Token,从而导致静态代码分析开销更大等;
…
这样的少量问题该如何解决呢?
大部分的资料中,都把Antlr4生成的树状结构,称为解析树或者者是语法树,但是,假如我们细究的话,可能说成是解析树更加精确,由于Antlr4的结果,只是简单的文法解析,不能称之为语法树(语法树应该是能够表现出来语法特性的信息),如上面的那些问题,就很难在Antlr4生成的解析树上获取到。
所以,现在很多工具,基于Antlr4进行封装,而后进行了更进一步地解决,从而获取到了更加丰富的语法树,例如CheckStyle。因而,假如通过Antlr4解析语言简单使用,可以直接基于Antlr4的结果开发,但是假如要进行更加深入的解决,就需要对Antlr4的结果进行更进一步的解决,以更符合我们的使用习惯(例如,Java Parser格式的Java的AST,Clang格式的C/C++的AST),而后才能更好地在上面进行开发。
本文分享自华为云社区《Antlr4简明使用教程》,原文作者:maijun 。
你画我猜小程序【更新序列至1.4.5】【持续更新】【流量主/激励视频/广告赚钱】
多商家营销活动平台【更新序列至2.3.7】【开源版】
微信智慧外链接致富版【更新序列至1.8.0】【一键授权/流量主躺赚/跳转企微/跳转视频号/关注公众号/添加微信/跳转群聊/跳转公众号文章/敏感内容过滤等插件】
新畅美容美发平台v2.1.18-修复微信登录/全插件含取号-订单辅助-收银-门店/技师/服务/预约/产品/会员卡劵/上门/报名/评论/分销/抽奖/社区/问卷
闯关答题多行业适用小程序【更新序列至1.8.33】【持续更新】
云端猎手公海搜客【更新序列至1.1.8】【七蚁基础模块】