語法解析及Antlr

目錄

 

1 語法解析

1.1 語法解析器

1.1.1 執行流程

1.1.2  語法樹好處

1.1.3 解析方法LL與LR

1.1.4 抽象語法樹(AST)

1.2  語法規則文件

2 Antlr

2.1  解析方法

2.1.1  遞歸下降語法分析器

2.1.2 ALL(*)解析器

2.2  語法解析樹

2.3 使用方法

2.3.1 使用流程

3 Listener 和 Visitor 機制

3.1 Listener模式

3.2 Visitor模式

3.3 相關問題釋疑

3.9 實踐問題

4 規則命中策略

5 antlr的應用

5.1 在Twitter中的應用

5.2  大數據產品中的應用 

9 參考資料


1 語法解析

1.1 語法解析器

1.1.1 執行流程

第一個階段是詞法分析,主要負責將符號文本分組成符號類tokens

第二個階段就是真正的語法解析,目標就是構建一個語法解析樹。

 

語法解析的輸入是tokens,輸出就是一顆語法解析樹。

1.1.2  語法樹好處

  1. 樹形結構易於遍歷和處理,並且容易被程序員理解,方便了應用代碼做進一步處理。

  2. 多種解釋或翻譯的應用代碼都可以重用一個解析器。但 ANTLR 也支持像傳統解析器生成器那樣,將應用處理代碼直接嵌入到語法中。

  3. 對於因爲計算依賴而需要多趟處理的翻譯器來說,比起多次調用解析器去解析,遍歷語法樹多次更加高效。

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 使用流程

http://www.antlr.org/

自定義文法--->詞法分析器(Lexical)------>語法分析器(Parse)—→Grammer

draw.io evaluation version

Lexical

文法文件

Parse

處理後的文件

輸入

g4文件

Parse

輸出

antlr

  1. 自定義詞法分析器 (g4後綴)文件

  2. 用工具生成Java文件

  3. 繼承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 參考資料

  1. [官網 看 antlr 4 reference手冊,權威 必看http://www.antlr.org/

  2. [進階] https://liangshuang.name/2017/08/20/antlr/

  3. [基礎] http://kyonhuang.top/ANTLR-learning-notes-1/

  4.  [visitor] https://dohkoos.gitbooks.io/antlr4-short-course/content/calculator-visitor.html

  5. https://toutiao.io/posts/2a6f21/preview

  6. [使用語法錯誤異常做補全] https://www.liangshuang.name/2017/08/20/antlr/

  7. [各種語法文件] https://github.com/antlr/grammars-v4

發佈了87 篇原創文章 · 獲贊 20 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章