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 = 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);
}
}
}

一般來說,面向程序靜態分析時,都是使用訪問者模式的,很少使用監聽器模式(無法主動控制遍歷AST的順序,不方便在不同節點遍歷之間傳遞數據)
Antlr4詞法解析和語法解析
如前面的語法定義,分為Lexer和Parser,實際上表示了兩個不同的階段:
- 詞法分析階段:對應於Lexer定義的詞法規則,解析結果為一個一個的Token;
- 解析階段:根據詞法,構造出來一棵解析樹或者語法樹。
如下圖所示:

Spark & Antlr4
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.removeErrorListeners()
lexer.addErrorListener(ParseErrorListener)
val tokenStream = new CommonTokenStream(lexer)
val parser = new SqlBaseParser(tokenStream)
parser.addParseListener(PostProcessor)
parser.removeErrorListeners()
parser.addErrorListener(ParseErrorListener)
try {
try {
// first, try parsing with potentially faster SLL mode
parser.getInterpreter.setPredictionMode(PredictionMode.SLL)
toResult(parser)
}
catch {
case e: ParseCancellationException =>
// if we fail, parse with LL mode
tokenStream.seek(0) // rewind input stream
parser.reset()
// Try Again.
parser.getInterpreter.setPredictionMode(PredictionMode.LL)
toResult(parser)
}
}
catch {
case e: ParseException if e.command.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.
參考:
[1] Spark SQL: Relational Data Processing in Spark: https://amplab.cs.berkeley.edu/wp-content/uploads/2015/03/SparkSQLSigmod2015.pdf
[2] Antlr4簡明使用教程: https://bbs.huaweicloud.com/blogs/226877
