Antlr4语法解析器(下)
Antlr4 的两种AST遍历⽅式:Visitor⽅式和Listener⽅式。
Antlr4规则⽂法:
注释:和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
基于IDEA调试Antlr4语法规则(⽂法可视化)
基于IDEA调试Antlr4语法⼀般步骤:
1) 创建⼀个调试⼯程,并创建⼀个g4⽂件
这⾥,我⾃⼰测试⽤Java开发,所以创建的是⼀个Maven⼯程,g4⽂件放在了src/main/resources ⽬录下,取名 Test.g4
2)写⼀个简单的语法结构
这⾥我们参考写⼀个加减乘除操作的表达式,然后在赋值操作对应的Rule上右键,可选择测试:
grammar Test;
@header {
package com.chaplinthink.antlr;
}
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);
看我们 3/ 4 是可以识别出来的语法中 channel(HIDDEN) (代表隐藏通道) 中的 Token,不会被语法解析阶段处理,但是可以通过Token遍历获取到。
Antlr4⽣成并遍历AST
1. 通过命令⾏如上篇⽂章
java -jar antlr-4.7.2--complete.jar -Dlanguage=Python3 -visitor Test.g4
这样就可以⽣成Python3 target的源码,如果不希望⽣成Listener,可以添加参数 -no-listener
2. Maven Antlr4插件⾃动⽣成(针对Java⼯程,也可以⽤于Gradle)
此处使⽤第⼀种⽅式
访问者模式遍历Antlr4语法树
java -jar  /usr/local/lib/antlr-4.7.2-complete.jar  -visitor -no-listener  Test.g4
⽣成源码⽂件:
通过代码展⽰访问者模式在Antlr4中使⽤:
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 = pr();
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: " + pr(0).getText());
System.out.println("second arg: " + pr(1).getText());
return super.visitAdd(ctx);
}
}
}
⼀般来说,⾯向程序静态分析时,都是使⽤访问者模式的,很少使⽤模式(⽆法主动控制遍历AST的顺序,不⽅便在不同节点遍历之间传递数据)Antlr4词法解析和语法解析
如前⾯的语法定义,分为Lexer和Parser,实际上表⽰了两个不同的阶段:
词法分析阶段:对应于Lexer定义的词法规则,解析结果为⼀个⼀个的Token;
解析阶段:根据词法,构造出来⼀棵解析树或者语法树。
如下图所⽰:
Spark & Antlr4
parse error怎么解决Spark SQL /DataFrame 执⾏过程是这样⼦的:
我们看下在 Spark SQL 中是如何使⽤Antlr4的.
当你调⽤spark.sql的时候, 会调⽤下⾯的⽅法:
def sql(sqlText: String): DataFrame = {
Dataset.ofRows(self, sessionState.sqlParser.parsePlan(sqlText))
}
parse sql阶段主要是parsePlan(sqlText)这⼀部分。⽽这⾥⼜会辗转去org.apache.spark.sql.catalyst.parser.AbstractSqlParser调⽤parse⽅法:
protected def parse[T](command: String)(toResult: SqlBaseParser => T): T = {
logDebug(s"Parsing command: $command")
val lexer = new SqlBaseLexer(new UpperCaseCharStream(CharStreams.fromString(command)))
lexer.addErrorListener(ParseErrorListener)
val tokenStream = new CommonTokenStream(lexer)
val parser = new SqlBaseParser(tokenStream)
parser.addParseListener(PostProcessor)
parser.addErrorListener(ParseErrorListener)
try {
try {
// first, try parsing with potentially faster SLL mode
toResult(parser)
}
catch {
case e: ParseCancellationException =>
/
/ if we fail, parse with LL mode
tokenStream.seek(0) // rewind input stream
// Try Again.
toResult(parser)
}
}
catch {
case e: ParseException if emand.isDefined =>
throw e
case e: ParseException =>
throw e.withCommand(command)
case e: AnalysisException =>
val position = Origin(e.line, e.startPosition)
throw new ParseException(Option(command), e.message, position, position)
}
}
这⾥SqlBaseLexer 、SqlBaseParser都是Antlr4的东西,包括最后的toResult(parser)也是调⽤访问者模式的类去遍历语法树来⽣成Logical Plan
spark提供了⼀个.g4⽂件,编译的时候会使⽤Antlr根据这个.g4⽣成对应的词法分析类和语法分析类,同时还使⽤了访问者模式,⽤以构建Logical Plan(语法树)。
访问者模式简单说就是会去遍历⽣成的语法树(针对语法树中每个节点⽣成⼀个visit⽅法),以及返
回相应的值。我们接下来看看⼀条简单的select语句⽣成的树是什么样⼦:
这个sqlBase.g4⽂件我们也可以直接复制出来,⽤antlr相关⼯具就可以⽣成⼀个⽣成⼀个解析SQL的图
将SELECT A.B FROM A,转换成⼀棵语法树。我们可以看到这颗语法树⾮常复杂,这是因为SQL解析中,要适配这种SELECT语句之外,还有很多其他类型的语句,⽐如INSERT,ALERT等等。Spark SQL这个模块的最终⽬标,就是将这样的⼀棵语法树转换成⼀个可执⾏的Dataframe(RDD)
Spark使⽤Antlr4的访问者模式,⽣成Logical Plan. 我们继承SqlBaseBaseVisitor,⾥⾯提供了默认的访问各个节点的触发⽅法。我们可以通过继承这个类,重写对应节点的visit⽅法,实现⾃⼰的访问逻辑,Spark SQL中这个继承的类就是org.apache.spark.sql.catalyst.parser.AstBuilder
通过观察这棵树,我们可以发现针对我们的SELECT语句,⽐较重要的⼀个节点,是querySpecification节点,实际上,在AstBuilder类中,visitQuerySpecification也是⽐较重要的⼀个⽅法(访问对应节点时触发),正是在这个⽅法中⽣成主要的Logical Plan的。
以下是querySpecification在Spark SQL 中实现的代码:
/**
* Create a logical plan using a query specification.
*/
override def visitQuerySpecification(
ctx: QuerySpecificationContext): LogicalPlan = withOrigin(ctx) {
val from = OneRowRelation().optional(ctx.fromClause) {
visitFromClause(ctx.fromClause)
}
withQuerySpecification(ctx, from)
}
先判断是否有FROM⼦语句,有的话会去⽣成对应的Logical Plan,再调⽤withQuerySpecification()⽅法,withQuerySpecification是逻辑计划核⼼⽅法, 根据不同的⼦语句⽣成不同的Logical Plan.
参考: