目錄
1 語法解析
1.1 語法解析器
1.1.1 執行流程
第一個階段是詞法分析,主要負責將符號文本分組成符號類tokens
第二個階段就是真正的語法解析,目標就是構建一個語法解析樹。
語法解析的輸入是tokens,輸出就是一顆語法解析樹。
1.1.2 語法樹好處
-
樹形結構易於遍歷和處理,並且容易被程序員理解,方便了應用代碼做進一步處理。
-
多種解釋或翻譯的應用代碼都可以重用一個解析器。但 ANTLR 也支持像傳統解析器生成器那樣,將應用處理代碼直接嵌入到語法中。
-
對於因爲計算依賴而需要多趟處理的翻譯器來說,比起多次調用解析器去解析,遍歷語法樹多次更加高效。
1.1.3 解析方法LL與LR
根據推導策略的不同,語法解析分爲LL與LR兩種
LR自低向上(bottom-up)的語法分析方法
LL是自頂向下(top-down)的語法分析方法
舉個例子,比如如何描述一個Json字符串?
Json字符串是一個key:Value字符串,其中key是字符串,value可以是String、數字、或者又是一個Json。csv也是這樣,類似一種遞歸定義。
exp1 = x1; exp2 = x2; result = exp1 'operator' exp2 |
兩類分析器各有其優勢,適用不同的場景,很難說誰要更好一些。普遍的說法是LR可以解析的語法形式更多,LL的語法定義更簡單易懂。
1.1.4 抽象語法樹(AST)
抽象語法樹是將源代碼根據其語法結構,省略一些細節(比如:括號沒有生成節點),抽象成樹形表達。
抽象語法樹在計算機科學中有很多應用,比如編譯器、IDE、壓縮優化代碼等。
如配置文件讀取器,遺留代碼轉換器,wiki 標記渲染器和 JSON 解析器
[抽象語法樹] https://tech.meituan.com/abstract-syntax-tree.html 該文中也舉了一個遺留代碼轉換器的例子
Json解析器:Twitter
note:一些比較簡單的其實可以用正則來處理,但是規則容易誤中,表達能力也非常受限。
源文件--->抽象,結構化------->parse
1.2 語法規則文件
編寫語法規則文件的同時,其實就是一個抽象的過程。示例如下:
從上圖可以看出主要語法文件分爲四塊,第二塊一般可以不寫,最重要的就是語法規則。
語法規則是以小寫字母組成。如prog,stat。詞法規則由大寫字母組成。如ID:[a-zA-Z]+。
通過使用 | 運算符來將不同的規則分割,還可以使用括號構成子規則。例如(‘*’ | ‘/’)會匹配多個乘號或除號。註釋: // /* */其他空格連接符: 表示順序連接| 選擇符: 表示或者的關係重複次數: + *可選: ?
https://github.com/antlr/grammars-v4 這類有各種語法文件示例。
FAQ:
Q: 爲啥大家都是換行寫分號?這個是
A: 【TODO】
Q: stat+ 是必須的表示語句一次可以有多個嗎?
A:表示可以一次解析多個語句。比如 ANTLRInputStream inputStream = new ANTLRInputStream("ord=(3*4)/2 price=(4*3)/2"); 這兩個stat就可以解析。當然如果我們是每次只解析一個,也可以不寫+。
Q: 那麼多規則,規則內還有分支,誰先誰後?具體的邏輯。
A: 這個誰先誰後還是看具體場景的邏輯,整體就是希望先被識別成哪種模式,就把該標籤放在前面。
2 Antlr
2.1 解析方法
2.1.1 遞歸下降語法分析器
是自頂向下語法解析器的一種。
顧名思義,遞歸下降指的就是解析過程是從語法樹的根開始,向葉子(token)遞歸。還是以前面的賦值表達式解析爲例,其遞歸下降語法分析器的代碼大概是下面這個樣子:
stat()、assign()、expr()等方法調用所形成的調用棧能與語法分析樹的內部節點一一對應。match()的調用對應樹的葉子,而assign()方法直接順序讀取輸入字符,而不用做任何選擇。相比之下,stat()方法要複雜一些,因爲在解析時,它需要向前看一些字符才能確認走哪個代碼分支,有時甚至要讀取完所有輸入才能得出預測結果。
2.1.2 ALL(*)解析器
ANTLR從4.0開始生成的是ALL(*)解析器,其中A是自適應(Adaptive)的意思。對傳統的LL(*)解析器有很大的改進,ANTLR是目前唯一可以生成ALL(*)解析器的工具。
2.2 語法解析樹
在ANTLR4中已經不再提供生成AST的功能(在ANTLR3中可以通過Options指定output=AST來生成AST),而是生成了分析樹(Parse Tree)。
2.3 使用方法
2.3.1 使用流程
自定義文法--->詞法分析器(Lexical)------>語法分析器(Parse)—→Grammer
draw.io evaluation version
Lexical
文法文件
Parse
處理後的文件
輸入
g4文件
Parse
輸出
antlr
-
自定義詞法分析器 (g4後綴)文件
-
用工具生成Java文件
-
繼承xxBaseVisitor類實現自己的Visitor
在整個Antlr的使用過程中,最重要的就是編寫規則文件。
規則文件的編寫:
Grammer文件:
grammar Cal; prog: stat+; //一個程序由至少一條語句組成 /*爲了以後的運算的方便性,我們需要給每一步規則打上標籤,標籤以”#”開頭,出現在每一條規則的右邊。打上標籤後,antlr會爲每一個規則都生成一個事件;在沒有標籤的情況下,每個規則會生成一個Context以及一個事件。 如下述語句,如果沒有標籤,那麼只會生成一個StatContext和一個visitStat方法。 */ stat: ID '=' expr ';' #Assign //變量賦值語句 | 'print' '(' expr ')' ';' #printExpr //輸出語句 ; expr: expr op=('*'|'/') expr #MulDiv //表達式可以是表達式之間乘除 | expr op=('+'|'-') expr #AddSub //表達式可以是表達式之間加減 | NUM #NUM //表達式可以是一個數字 | ID #ID //表達式可以是一個變臉 | '(' expr ')' #parens //表達式可以被括號括起來 ; MUL:'*'; DIV:'/'; ADD:'+'; SUB:'-'; ID: [a-zA-Z][a-zA-Z0-9]*; //變量可以是數字和字母,但必須以字母開頭 //負數必須要用"()"括起來 NUM: [0-9]+ //正整數 | '(' '-' [0-9]+ ')' //負整數 | [0-9]+'.'[0-9]+ //正浮點數 | '(' '-' [0-9]+'.'[0-9]+ ')' //負浮點數 ; WS: [ \t\r\n] -> skip; //跳過空格、製表符、回車、換行 |
生成命令:
編輯完 Antlr 文件後,我們在安裝有 Antlr plugin 的 Intellij 上,可以通過右鍵語法規則對語法規則進行測試,並可以在配置生成中間代碼的包名、路徑等選項後,直接生成中間代碼。
生成的文件說明:
-
Hello.tokens和HelloLexver.tokens爲文法中用到的各種符號做了數字化編號,對於我們定義的每個 token,ANTLR 分配了一個 token 類型碼並將這些值保存在 ArrayInit.tokens。因爲這個文件的存在,當我們將較大規模的語法分割爲各種小型的語法表達時,ANTLR 能夠使同種 token 的類型碼保持一致。
-
HelloLexer.java 包含專用的詞法分析程序(lexer)類的定義
-
HelloParse.java 包含了專用於 ArrayInit 語法的解析器(parser)類的定義。
-
HelloVisitor 和 HelloBaseVisitor 分別是語法解析樹的vistor的接口和類,用於遍歷整個語法樹。一般情況下,我們通過繼承 HelloBaseVisitor 來實現自己對於語法樹遍歷的處理。
-
HelloListener和HelloBaseListener是用listen方式來遍歷樹的解析方法。
HelloParse說明:
ANTLR爲每個子規則創建一個同名函數,因此可以方便地取到其子規則的context。即每個規則對應一個Context。
Context對象包含以下內容:
語法分析時生成
-
起始Token,終止Token
-
children: 可以得到子語法規則中的內容。
-
異常信息: 可以得到解析失敗的信息。
查看語法解析樹:
3 Listener 和 Visitor 機制
在ANTLR 4以前,有兩種開發方式:一是將目標語言的代碼直接硬編碼到語法定義文件中,在生成分析器時會插入這些代碼到生成文件中,這也是大多數語法分析器生成工具的做法。
在上邊的語法判定與通道的例子中,就有將Java代碼硬編碼到語法定義的情況。將目標代碼和語法定義耦合在了一起,當需要生成不同目標語言的分析器時,就需要維護多份語法定義文件,
‘增加了維護成本,同時在編寫複雜邏輯時,由於IDE沒有對目標語言的支持,開發和測試都很幸苦。另一種方式是讓ANTLR生成語法分析樹,然後寫程序遍歷語法樹,對語法樹的遍歷是一個很複雜的工作。
ANTLR 4開始會生成監聽器(Listener)與訪問者(Visitor),將語法定義與目標代碼完全的解耦。監聽器可以被動的接受語法分析樹遍歷的事件,對於每一個語法節點,
都會生成進入enterSomeNodeName與退出exitSomeNodeName兩個方法。訪問者機制生成對語法節點的訪問方法visitSomeNodeName,在訪問方法中需要手動調用visit方法來對子節點進行遍歷,
使用訪問者模式可以控制語法樹的遍歷,略過某些的分枝。ANTLR默認只生成監聽器,生成訪問者類需要在生成時添加-visitor選項。
Visitor和Listener是antlr提供的兩種樹遍歷機制。Listener是默認的機制,可以被antlr提供的遍歷器對象調用;如果要用Visitor機制,在生成識別器時就需要顯式說明
antlr4 -no-listener -visitor Calc.g4,並且必須顯示的調用visitor方法遍歷它們的子節點,在一個節點的子節點上如果忘記調用visit方法,就意味着那些子數沒有得到訪問
核心區別:兩者各有優劣,Listener模式適合全局查找,默認是深度優先遍歷,而Visitor模式適合指定某個節點作遍歷。
listener默認是遍歷每個節點,對於每個節點都有enter和exit事件。對於visitor則是需要自己手動用寫代碼去進行指定節點遍歷。
3.1 Listener模式
示例如下
public class WlgsParseListener extends MerakDslBaseListener{
private ParseTreeProperty<String> property = new ParseTreeProperty<>();
@Override
public void exitArithmeticBinary(MerakDslParser.ArithmeticBinaryContext ctx) {
MerakDslParser.ExpressionContext right = ctx.right;
MerakDslParser.ExpressionContext left = ctx.left;
String formula = left.getText() + ctx.operator.getText() + right.getText();
property.put(ctx,formula);
super.exitArithmeticBinary(ctx);
}
}
Listener模式的exitXX方法的返回值都是空,在遍歷過程所有有臨時數據需要保存,可以存近一個自定義數據結構中或者使用antrl的ParseTreeProperty中或者自定義一個數據結果保存都是OK的。
3.2 Visitor模式
從上面可以看出,即使我們不使用super調用父類的同名方法,也不調用自定義baseListener中的其他方法,其實listen也是可以完成遍歷的。因此listener是被動進行遍歷的。
visitor不一樣,是手動進行遍歷,必須手動使用visit調用其他的visitor方法,或者使用super,父類中該方法是調用所有的子類visitor。
從以下源碼可以看出,super.visitXX都是一樣的,都是visit 參數Context的 Children。
@Override public T visitAssignment(MerakDslParser.AssignmentContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitCaseWhen(MerakDslParser.CaseWhenContext ctx) { return visitChildren(ctx); }
/**
整體流程就是,
visitProg(){
super.visitProg(ctx) //visit children就是所有的stat.accept(visitor)
stat.accept(visitor) ==> visitor.visitStat()
visitStat(ctx){super.visitStat} // visitStat的child
以此類推。。
}
Q:如果Prog下有多個stat,那麼用super visit所有的children的結果如何合併爲一個結果(該結果是visitStat的返回值)。
A: visitChildren源碼可以看出,多個child的結果是,後續的結果會覆蓋前面的結果,也就是visitProg的返回值就是最後一個visitStat的結果。
public T visitChildren(RuleNode node) {
T result = this.defaultResult();
int n = node.getChildCount();
for(int i = 0; i < n && this.shouldVisitNextChild(node, result); ++i) {
ParseTree c = node.getChild(i);
T childResult = c.accept(this);
result = this.aggregateResult(result, childResult);
}
return result;
}
比如對於(3*4)/2 ;方法visitParens,如果方法實現是使用super.visitParens,則會返回")"。 Parens的children數組元素有三個,(、exp、);最後的“)”會覆蓋前面兩個。這個時候我們自定義visit的作用就體現出來了,一般對於四則運算的visitParens我們會重寫如下
visit(ctx.expression());
重寫的visitParens中,我們就手動指定了節點的訪問。因此可以認爲visitor模式就是主動遍歷的模式。
接下來還需要明確下 Context的層級結構,比如
3.3 相關問題釋疑
1 語法文件中的#意味着什麼?和生成的Context結構如何對應?
#號的含義在上文語法文件說明部分已經解釋了,沒有#規則名對應一個Context,有#能以更細的分支來管理規則,#後面的別名對應着一個Context,也對應着一個訪問方法。
2 大部分時候都是有children,到底什麼節點下有children?
目前這個Expression的children下面沒有數組,而是left 和right的屬性。
因此猜測,凡是有 left = xx;有這種描述語句的時候都是沒有數組的。這個時候不能使用 super.xxx進行遍歷
3.9 實踐問題
其他細節:因爲 字符串和null用+號拼接的時候,就相當於字符串 + “null”因此可能會出現 (null)的現象。有兩種辦法,一種在遍歷括號節點的時候加判斷,或者解析公式之前就加個校驗,保證不出出現這種問題,
天璇V3Manager工程 物理公式解析就是前端控件進行了校驗,保證了不會出現這個問題。
if (ctx.expression() != null){
return visitExpression(ctx.expression());
} else{
return Double.parseDouble(ctx.INT().getText());
}
使用 Maven
Antlr4 提供了 Maven Plugin,可以通過配置來進行編譯。語法文件 g4 放置在 src/main/antlr4 目錄下即可,配置依賴的 antlr4 和 plugin 即可。生成 visitor 在 plugin 配置 visitor 參數爲 true 即可。
注意:antlr4 的庫版本要與 plugin 版本對應,antlr4 對生成文件用的版本與庫本身的版本會進行對照,不匹配會報錯。
...
<properties>
<antlr4.version>4.7.2</antlr4.version>
</properties>
<dependencies>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4</artifactId>
<version>${antlr4.version}</version>
</dependency>
</dependencies>
...
<build>
<plugins>
<plugin>
<groupId>org.antlr</groupId>
<artifactId>antlr4-maven-plugin</artifactId>
<version>${antlr4.version}</version>
<configuration>
<visitor>true</visitor>
</configuration>
<executions>
<execution>
<id>antlr</id>
<goals>
<goal>antlr4</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
4 規則命中策略
規則這麼多,也許一個表達式能夠命中多個規則分支,最終命中哪個是什麼策略決定的。以下進行說明。
【TODO】
5 antlr的應用
一般的處理對象有:
-
SQL語句
-
算數表達式
-
編程語言的源文件
可以看出來其實編寫grammar文件的過程其實就是對某種場景的抽象,定義一些基礎的元素,然後Parse能夠針對輸入生成某種輸入的樹結構,然後遍歷這種樹結構,在遍歷的過程中可以做一些處理,比如翻譯。
note:
[各種語言的antlr解析] https://blog.csdn.net/xiyue_ruoxi/article/details/38925091
關於SQL的antlr解析
5.1 在Twitter中的應用
將查詢參數解析成一個語法解析樹
5.2 大數據產品中的應用
antlr在hibernate、presto以及SparkSQL中都是使用了的。
antlr在presto及SparkSQL中作用是相似的,都是就是生成了生成一個SQL語法解析樹(一個未處理的邏輯執行計劃)。
後續是對邏輯執行計劃是進行了優化的。
也就是說presto和spark是從SQL到邏輯執行計劃;天璇的邏輯執行計劃是從查詢參數構建出來,也就是手動完成了antlr的過程。
9 參考資料
-
[官網 看 antlr 4 reference手冊,權威 必看] http://www.antlr.org/
-
[visitor] https://dohkoos.gitbooks.io/antlr4-short-course/content/calculator-visitor.html
-
[使用語法錯誤異常做補全] https://www.liangshuang.name/2017/08/20/antlr/
-
[各種語法文件] https://github.com/antlr/grammars-v4