一.ANTRL 是什么
当我们实现一种语言时,我们需要构建读取句子(sentence)的应用,并对输入中的元素做出反应。如果应用计算或执行句子,我们就叫它解释器(interpreter),包括计算器、配置文件读取器、Python解释器都属于解释器。如果我们将句子转换成另一种语言,我们就叫它翻译器(translator),像Java到C#的翻译器和编译器都属于翻译器。不管是解释器还是翻译器,应用首先都要识别出所有有效的句子、词组、字词组等,识别语言的程序就叫解析器(parser)或语法分析器(syntax analyzer)。我们学习的重点就是如何实现自己的解析器,去解析我们的目标语言,像DSL语言、配置文件、自定义SQL等等。
手动编写解析器是非常繁琐的,所以我们有了ANTLR。只需编写ANTLR的语法文件,描述我们要解析的语言的语法,之后ANTLR就会自动生成能解析这种语言的解析器。也就是说,ANTLR是一种能写出程序的程序。而用来声明我们语言的ANTLR语言的语法,就是元语言(meta-language)。
二.ANTRL 语法
文件结构
/** Optional javadoc style comment */
grammar Name;
options {...}
import ... ;
tokens {...}
channels {...} // lexer only
@actionName {...}
rule1 // parser and lexer rules, possibly intermingled
...
ruleN
grammar 声明语法头,类似于java类的定义
grammar SPL;
options 选项,如语言选项,输出选项,回溯选项,记忆选项等等
options { output=AST; language=Java; }
options { tokenVocab=MySqlLexer; }
rule 表示规则,以 “:” 开始, “;” 结束, 多规则以 "|" 分隔
ID : [a-zA-Z0-9|'_']+ ; //数字
STR:'\'' ('\'\'' | ~('\''))* '\'';
WS: [ \t\n\r]+ -> skip ; // 系统级规则 ,即忽略换行与空格
sqlStatement
: ddlStatement
| dmlStatement | transactionStatement
| replicationStatement | preparedStatement
| administrationStatement | utilityStatement
;
注释
- 单行、多行、javadoc风格
- javadoc风格只能在开头使用
/**
* This grammar is an example illustrating the three kinds
* of comments.
*/
grammar T;
/* a multi-line
comment
*/
/** This rule matches a declarator for my language */
decl : ID ; // match a variable name
标识符
- 符号(Token)名大写开头
- 解析规则(Parser rule)名小写开头,后面可以跟字母、数字、下划线
ID, LPAREN, RIGHT_CURLY // token names
expr, simpleDeclarator, d2, header_file // rule names
ANTLR 语法识别一般分为二个阶段:
1.词法分析阶段 (lexical analysis)
对应的分析程序叫做 lexer ,负责将符号(token)分组成符号类(token class or token type)
2.解析阶段
根据词法,构建出一棵分析树(parse tree)或叫语法树(syntax tree)
3.递归下降解析器
ANTLR生成的解析器叫做递归下降解析器(recursive-descent parser),属于自顶向下解析器(top-down parser)的一种。递归下降指的就是解析过程是从语法树的根开始向叶子(token)递归.
。还是以前面的赋值表达式解析为例,其递归下降解析器的代码大概是下面这个样子:
Assign很简单,直接顺序读取输入字符,不用做任何选择。相比之下,根结点Stat要复杂一些,因为它有多种选择。解析时,要向前看(lookahead)一些字符才能确认走哪个分支代码,有时甚至要读取完所有输入才能预测出,而ANTLR默默为我们处理了一切!
三. 解析树上的应用
如下图所示,解析树的叶子节点指向Token流中的Token,而Token中的起止字符索引指向字符流,而非拷贝子字符串。而像空格这种不与任何Token相关的字符会直接被Lexer丢弃掉。
ANTLR为每个Rule都会生成一个Context对象,它会记录识别时的所有信息。
四.树遍历
ANTLR提供了Listener和Visitor两种遍历机制。
Listener是全自动化的,ANTLR会主导深度优先遍历过程,我们只需处理各种事件就可以了。而Visitor则提供了可控的遍历方式,我们可以自行决定是否显示地调用子结点的visit方法。
String sql = "DELETE FROM T1 WHERE COL1 = TRUE AND (COL2 - COL3 <= (SELECT COUNT(*) FROM T2) OR MAINCOL/2 > 100.2);".toUpperCase();
final MySqlLexer mySqlLexer = new MySqlLexer(CharStreams.fromString(sql));
//字符组成单词(token)
final CommonTokenStream commonTokenStream = new CommonTokenStream(mySqlLexer);
//词法分析:将负责将符号(token)分组成符号类
final MySqlParser mySqlParser = new MySqlParser(commonTokenStream);
//根据词法,构建出一棵分析树(parse tree)或叫语法树(syntax tree)
final ParseTree tree = mySqlParser.root();
//遍历树节
ParseTreeWalker walker = new ParseTreeWalker(); // create standard walker
ExtractInterfaceListener extractor = new ExtractInterfaceListener(parser);
walker.walk(extractor, tree); // initiate walk of tree with listener
String sql = "DELETE FROM T1 WHERE COL1 = TRUE AND (COL2 - COL3 <= (SELECT COUNT(*) FROM T2) OR MAINCOL/2 > 100.2);".toUpperCase();
final MySqlLexer mySqlLexer = new MySqlLexer(CharStreams.fromString(sql));
//字符组成单词(token)
final CommonTokenStream commonTokenStream = new CommonTokenStream(mySqlLexer);
//词法分析:将负责将符号(token)分组成符号类
final MySqlParser mySqlParser = new MySqlParser(commonTokenStream);
//根据词法,构建出一棵分析树(parse tree)或叫语法树(syntax tree)
final MySqlParser.RootContext selectStatementContext = mySqlParser.root();
//遍历树节点
MySqlParserBaseVisitor visitor = new MySqlParserBaseVisitor();
visitor.visit(selectStatementContext);
四.实践
准备工作
1 安装IDE插件
我这里使用的是Intellij IDEA,所以就去Plugins中搜“ANTLR v4 grammar plugin”插件,重启IDEA即可使用。如果想在IDE外使用,需要下载ANTLR包,是JAVA写成的,后面在IDEA中的各种操作都可以手动执行命令来完成。
2 编写.g4文件
创建一个文件,后缀名是g4,只有这样在文件上点右键才能看到ANTLR插件的菜单。
/*
这个grammar的名称为ArrayInt ,必须与文件名相同
*/
grammar ArrayInt;
/* 一条rule 符号(token)分组*/
init : '{' value (',' value)* '}' ;
/* 一条rule*/
value : init
| INT
;
/* lexer 词法分析规则*/
INT : [0-9]+ ;
WS : [ \t\r\n]+ -> skip ;
3.自动生成代码
在.g4文件上右键就能看到ANTLR插件的两个菜单,分别用来配置ANTLR生成工具的参数(在命令行中都有对应)和触发生成文件。首先选配置菜单,将目录选择到main/java或test/java。注意:ANTLR会自动根据Package/namespace的配置,生成出包的文件夹,不用预先创建出来。
之后就点生成菜单,于是就在我们配置的目录下,自动生成出的如下代码:
4 构建应用代码
有了生成好的解析器,我们就可以在它上面构建出好玩的应用了。
在开始编写应用代码之前,我们要引入ANTLR运行时。因为我们的解析器其实只是一堆回调hook,真正的通用解析流程实现是在ANTLR runtime包中。所以,以Maven为例ANTLR v4的依赖是:
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
<version>4.8</version>
</dependency>
String arrayInt = "{1,2,3}";
final ArrayIntLexer arrayIntLexer = new ArrayIntLexer(CharStreams.fromString(arrayInt));
//字符组成单词(token)
final CommonTokenStream commonTokenStream = new CommonTokenStream(arrayIntLexer);
//词法分析:将负责将符号(token)分组成符号类
final ArrayIntParser arrayIntParser = new ArrayIntParser(commonTokenStream);
//根据词法,构建出一棵分析树(parse tree)或叫语法树(syntax tree)
final ArrayIntParser.InitContext selectStatementContext = arrayIntParser.init();
//遍历树节点
ArrayIntBaseVisitor visitor = new ArrayIntBaseVisitor();
visitor.visit(selectStatementContext);