從定義到AST及其遍歷方式,一文帶你搞懂Antlr4

摘要:本文將首先介紹Antlr4 grammer的定義方式,如何經過Antlr4 grammer生成對應的AST,以及Antlr4 的兩種AST遍歷方式:Visitor方式和Listener方式。

1. Antlr4簡單介紹

Antlr4(Another Tool for Language Recognition)是一款基於Java開發的開源的語法分析器生成工具,可以根據語法規則文件生成對應的語法分析器,普遍應用於DSL構建,語言詞法語法解析等領域。如今在很是多的流行的框架中都用使用,例如,在構建特定語言的AST方面,CheckStyle工具,就是基於Antlr來解析Java的語法結構的(當前Java Parser是基於JavaCC來解析Java文件的,聽說有規劃在下個版本改用Antlr來解析),還有就是普遍應用在DSL構建上,著名的Eclipse Xtext就有使用Antlr。html

Antlr能夠生成不一樣target的AST(https://www.antlr.org/download.html),包括Java、C++、JS、Python、C#等,能夠知足不一樣語言的開發需求當前Antlr最新穩定版本爲4.9Antlr4官方github倉庫中,已經有數十種語言的grammerhttps://github.com/antlr/grammars-v4,不過雖然這麼多語言的規則文法定義都在一個倉庫中,可是每種語言的grammerlicense是不同的,若是要使用,須要參考每種語言本身的語法結構的license)。java

本文將首先介紹Antlr4 grammer的定義方式(簡單介紹語法結構,並介紹如何基於IDEA Antlr4插件進行調試),而後介紹如何經過Antlr4 grammer生成對應的AST,最後介紹Antlr4 的兩種AST遍歷方式:Visitor方式和Listener方式。git

2. Antlr4規則文法

下面簡單介紹一部分Antlr4的g4(grammar)文件的寫法 (主要參考Antlr4官方wikihttps://github.com/antlr/antlr4/blob/master/doc/index.md)。 最有效的學習Antlr4的規則文法的寫法的方法,就是參考已有的規則文法,你們在學習中,能夠參考已有語言的文法。並且Antlr4已經實現了數十種語言的文法,若是須要本身定義,能夠參考和本身的語言最接近的文法來開發。github

2.1 Antlr4規則基本語法和關鍵字

首先,若是有一點兒C或者Java基礎,對上手Antlr4 g4的文法很是快。主要有下面的一些文法結構:設計模式

  • 註釋:和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。

2.2 Antlr4語法介紹

2.2.1語法文件的總體結構及寫法示例

Antlr4總體結構以下:api

/** Optional javadoc style comment */

grammar Name;

options {...}

import ... ;

 

tokens {...}

channels {...} // lexer only

@actionName {...}

 

rule1 // parser and lexer rules, possibly intermingled

...

ruleN

通常若是語法很是複雜,會基於LexerParser寫到兩個不一樣的文件中(例如Java,可參考:https://github.com/antlr/grammars-v4/tree/master/java/java8),若是語法比較簡單,能夠只寫到一個文件中(例如Lua,可參考:https://github.com/antlr/grammars-v4/blob/master/lua/Lua.g4)。安全

下面咱們結合Lua.g4中的一部分語法結構,介紹使用方法。寫Antlr4的文法,須要依據源碼的結構來決定。定義時,依據源碼文件的寫法,從上到下開始構造語法結構。例如,下面是Lua.g4的一部分:數據結構

chunk
    : block EOF
    ;

block
    : stat* retstat?
    ;

stat
    : ';'
    | varlist '=' explist
    | functioncall
    | label
    | 'break'
    | 'goto' NAME
    | 'do' block 'end'
    | 'while' exp 'do' block 'end'
    | 'repeat' block 'until' exp
    | 'if' exp 'then' block ('elseif' exp 'then' block)* ('else' block)? 'end'
    | 'for' NAME '=' exp ',' exp (',' exp)? 'do' block 'end'
    | 'for' namelist 'in' explist 'do' block 'end'
    | 'function' funcname funcbody
    | 'local' 'function' NAME funcbody
    | 'local' attnamelist ('=' explist)?
    ;

attnamelist
    : NAME attrib (',' NAME attrib)*
    ;

如上語法中,整個文件被表示成一個chunk,chunk表示爲一個block和一個文件結束符(EOF);block又被表示爲一系列的語句的集合,而每一種語句又有特定的語法結構,包含了特定的表達式、關鍵字、變量、常量等信息,而後遞歸表達式的文法組成,變量的寫法等,最終所有都歸結到Lexer(Token)上,遞歸樹結束。框架

上面其實已經能夠看到Antlr4規則的寫法,下面介紹一部分比較重要的規則的寫法。maven

2.2.2 替代標籤

首先,如2.2.1節的代碼所示,stat能夠有很是多的類型,例如變量定義、函數定義、if、while等,這些都沒有進行區分,這樣解析出來語法樹時,會很不清晰,須要結合不少的標記完成具體語句的識別,這種狀況下,咱們能夠結合替代標籤完成區分,以下代碼:

stat
    : ';'
    | varlist '=' explist  #varListStat
    | functioncall  #functionCallStat
    | label  #labelStat
    | 'break'  #breakStat
    | 'goto' NAME  #gotoStat
    | 'do' block 'end'  #doStat
    | 'while' exp 'do' block 'end'  #whileStat
    | 'repeat' block 'until' exp  #repeatStat
    | 'if' exp 'then' block ('elseif' exp 'then' block)* ('else' block)? 'end'  #ifStat
    | 'for' NAME '=' exp ',' exp (',' exp)? 'do' block 'end'  #forStat
    | 'for' namelist 'in' explist 'do' block 'end'  #forInStat
    | 'function' funcname funcbody  #functionDefStat
    | 'local' 'function' NAME funcbody  #localFunctionDefStat
    | 'local' attnamelist ('=' explist)?  #localVarListStat
    ;

經過在語句後面,添加 #替代標籤,能夠將語句轉換爲這些替代標籤,從而加以區分。

2.2.3 操做符優先級處理

默認狀況下,ANTLR從左到右結合運算符,然而某些像指數羣這樣的運算符則是從右到左。可使用選項assoc手動指定運算符記號上的相關性。以下面的操做:

expr : expr '^'<assoc=right> expr

^ 表示指數運算,增長 assoc=right,表示該運算符是右結合。

實際上,Antlr4 已經對一些經常使用的操做符的優先級進行了處理,例如加減乘除等,這些就不須要再特殊處理。

2.2.4 隱藏通道

不少信息,例如註釋、空格等,是結果信息生成不須要處理的,可是咱們又不適合直接丟棄,安全地忽略掉註釋和空格的方法是把這些發送給語法分析器的記號放到一個「隱藏通道」中,語法分析器僅須要調協到單個通道便可。咱們能夠把任何咱們想要的東西傳遞到其它通道中。在Lua.g4中,這類信息的處理以下:

COMMENT
    : '--[' NESTED_STR ']' -> channel(HIDDEN)
    ;
LINE_COMMENT
    : '--'
    (                                               // --
    | '[' '='*                                      // --[==
    | '[' '='* ~('='|'['|'\r'|'\n') ~('\r'|'\n')*   // --[==AA
    | ~('['|'\r'|'\n') ~('\r'|'\n')*                // --AAA
    ) ('\r\n'|'\r'|'\n'|EOF)
    -> channel(HIDDEN)
    ;
WS
    : [ \t\u000C\r\n]+ -> skip
    ;
SHEBANG
    : '#' '!' ~('\n'|'\r')* -> channel(HIDDEN)
    ;

放到 channel(HIDDEN) 中的 Token,不會被語法解析階段處理,可是能夠經過Token遍歷獲取到。

2.2.5 常見詞法結構

Antlr4採用BNF範式,用’|’表示分支選項,’*’表示匹配前一個匹配項0次或者屢次,’+’ 表示匹配前一個匹配項至少一次。下面介紹幾種常見的詞法舉例(均來自Lua.g4文件):

1) 註釋信息

COMMENT
    : '--[' NESTED_STR ']' -> channel(HIDDEN)
    ;
LINE_COMMENT
    : '--'
    (                                               // --
    | '[' '='*                                      // --[==
    | '[' '='* ~('='|'['|'\r'|'\n') ~('\r'|'\n')*   // --[==AA
    | ~('['|'\r'|'\n') ~('\r'|'\n')*                // --AAA
    ) ('\r\n'|'\r'|'\n'|EOF)
    -> channel(HIDDEN)
    ;

2) 數字

INT
    : Digit+
    ;

Digit
    : [0-9]
    ;

3) ID(命名)

NAME
    : [a-zA-Z_][a-zA-Z_0-9]*
    ;

3. 基於IDEA調試Antlr4語法規則(文法可視化)

若是要安裝Antlr4,選擇 File -> Settings -> Plugins,而後在搜索框搜索 Antlr安裝便可,能夠選擇安裝搜索出來的最新版本,下圖是剛剛安裝的ANTLR v4,版本是v1.15,支持最新的Antlr 4.9版本。

基於IDEA調試Antlr4語法通常步驟:

1) 建立一個調試工程,並建立一個g4文件

這裏,我本身測試用Java開發,因此建立的是一個Maven工程,g4文件放在了src/main/resources 目錄下,取名 Test.g4

2)寫一個簡單的語法結構

這裏咱們參考寫一個加減乘除操做的表達式,而後在賦值操做對應的Rule上右鍵,可選擇測試:

如上圖,expr 表示的是一個乘法操做,因此咱們以下測試:

可是,若是改爲一個加法操做,則沒法識別,只能識別到第一個數字。

這種狀況下,就須要繼續擴充 expr的定義,豐富不一樣的語法,來繼續支持其餘的語法,以下:

還能夠繼續擴充其餘類型的支持,這樣一步步將整個語言的語法都支持完整。這裏,咱們造成的一個完整的格式以下(表示整形數字的加減乘除):

grammar Test;

@header {
    package zmj.test.antlr4.parser;
}

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

4. Antlr4生成並遍歷AST

4.1 生成源碼文件

這一步介紹兩種生成解析語法樹的兩種方法,供參考:

  • Maven Antlr4插件自動生成(針對Java工程,也能夠用於Gradle)

pom.xml設置Antlr4 Maven插件,能夠經過執行 mvn generate-sources自動生成須要的代碼(參考連接: https://www.antlr.org/api/maven-plugin/latest/antlr4-mojo.html,主要的意義在於,代碼入庫的時候,不須要再將生成的這些語法文件入庫,減小庫裏面的代碼冗餘,只包含本身開發的代碼,不會有自動生成的代碼,也不須要作clean code整改),下面是一個示例:

<build>
    <plugins>
      <plugin>
        <groupId>org.antlr</groupId>
        <artifactId>antlr4-maven-plugin</artifactId>
        <version>4.3</version>
        <executions>
          <execution>
            <id>antlr</id>
            <goals>
              <goal>antlr4</goal>
            </goals>
            <phase>generate-sources</phase>
          </execution>
        </executions>
        <configuration>
          <sourceDirectory>${basedir}/src/main/resources</sourceDirectory>
          <outputDirectory>${project.build.directory}/generated-sources/antlr4/zmj/test/antlr4/parser</outputDirectory>
          <listener>true</listener>
          <visitor>true</visitor>
          <treatWarningsAsErrors>true</treatWarningsAsErrors>
        </configuration>
      </plugin>
    </plugins>
  </build>

按照上面設置後,只須要執行 mvn generate-sources 便可在maven工程中自動生成代碼。

  • 命令行方式

主要參考連接(https://www.antlr.org/download.html),有每種語言的語法配置,咱們這裏考慮下載Antlr4完整jar

下載好後(antlr-4.9-complete.jar),可使用以下命令來生成須要的信息:

java -jar antlr-4.9-complete.jar -Dlanguage=Python3 -visitor Test.g4

這樣就能夠生成Python3 target的源碼,支持的源碼能夠從上面連接查看,若是不但願生成Listener,能夠添加參數 -no-listener

4.2 訪問者模式遍歷Antlr4語法樹

Antlr4在AST遍歷時,支持兩種設計模式:訪問者設計模式 和 監聽器模式。

對於 訪問者設計模式,咱們須要本身定義對 AST 的訪問(https://xie.infoq.cn/article/5f80da3c014fd69f8dbe09b28,這是一篇針對訪問者設計模式的介紹,你們能夠參考)。下面直接經過代碼展現訪問者模式在Antlr4中使用(基於第3章的例子):

import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import zmj.test.antlr4.parser.TestBaseVisitor;
import zmj.test.antlr4.parser.TestLexer;
import zmj.test.antlr4.parser.TestParser;

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

如上,main方法中,解析出了表達式的AST結構,同時在源碼中也定義了一個Visitor:TestVisitor,訪問AddContext,而且打印該加表達式的先後兩個表達式,上面例子的輸出爲:

========= test add
first arg: 12*2
second arg: 12

4.2 監聽器模式(觀察者模式)

對於監聽器模式,就是經過監聽某對象,若是該對象上有特定的事件發生,則觸發該監聽行爲執行。好比有個監控(監聽器),監控的是大門(事件對象),若是發生了闖門的行爲(事件源),則進行報警(觸發操做行爲)。

在Antlr4中,若是使用監聽器模式,首先須要開發一個監聽器,該監聽器能夠監聽每一個AST節點(例如表達式、語句等)的不一樣的行爲(例如進入該節點、結束該節點)。在使用時,Antlr4會對生成的AST進行遍歷(ParseTreeWalker),若是遍歷到某個具體的節點,而且執行了特定行爲,就會觸發監聽器的事件。

監聽器方法是沒有返回值的(即返回類型是void)。所以須要一種額外的數據結構(能夠經過Map或者棧)來存儲當次的計算結果,供下一次計算調用。

通常來講,面向程序靜態分析時,都是使用訪問者模式的,不多使用監聽器模式(沒法主動控制遍歷AST的順序,不方便在不一樣節點遍歷之間傳遞數據),用法對我們也不友好,因此本文不介紹監聽器模式,若是有興趣,能夠本身搜索測試使用。

5. Antlr4詞法解析和語法解析

這部分實際上,算是Antlr4最基礎的內容,可是放到最後一部分來說,有特定的目的,就是探討一下詞法解析和語法解析的界限,以及Antlr4的結果的處理。

5.1 Antlr4執行階段

如前面的語法定義,分爲Lexer和Parser,實際上表示了兩個不一樣的階段:

  • 詞法分析階段:對應於Lexer定義的詞法規則,解析結果爲一個一個的Token;
  • 解析階段:根據詞法,構造出來一棵解析樹或者語法樹。

以下圖所示:

5.2 詞法解析和語法解析的調和

首先,咱們應該有個廣泛的認知:語法解析相對於詞法解析,會產生更多的開銷,因此,應該儘可能將某些可能的處理在詞法解析階段完成,減小語法解析階段的開銷,主要下面的這些例子:

  • 合併語言不關心的標記,例如,某些語言(例如js)不區分int、double,只有 number,那麼在詞法解析階段,就不須要將int和double區分開,統一合併爲一個number;
  • 空格、註釋等信息,對於語法解析並沒有大的幫助,能夠在詞法分析階段剔除掉;
  • 諸如標誌符、關鍵字、字符串和數字這樣的經常使用記號,均應該在詞法解析時完成,而不要到語法解析階段再進行。

可是,這樣的操做在節省了語法分析的開銷以外,其實對咱們也產生了一些影響:

  • 雖然語言不區分類型,例如只有 number,沒有 int 和 double 等,可是面向靜態代碼分析,咱們可能須要知道確切的類型來幫助分析特定的缺陷;
  • 雖然註釋對代碼幫助不大,可是咱們有時候也須要解析註釋的內容來進行分析,若是沒法在語法解析的時候獲取,那麼就須要遍歷Token,從而致使靜態代碼分析開銷更大等;

這樣的一些問題該如何處理呢?

5.3 解析樹vs語法樹

大部分的資料中,都把Antlr4生成的樹狀結構,稱爲解析樹或者是語法樹,可是,若是咱們細究的話,可能說成是解析樹更加準確,由於Antlr4的結果,只是簡單的文法解析,不能稱之爲語法樹(語法樹應該是可以體現出來語法特性的信息),如上面的那些問題,就很難在Antlr4生成的解析樹上獲取到。

因此,如今不少工具,基於Antlr4進行封裝,而後進行了更進一步地處理,從而獲取到了更加豐富的語法樹,例如CheckStyle。所以,若是經過Antlr4解析語言簡單使用,能夠直接基於Antlr4的結果開發,可是若是要進行更加深刻的處理,就須要對Antlr4的結果進行更進一步的處理,以更符合咱們的使用習慣(例如,Java Parser格式的Java的AST,Clang格式的C/C++的AST),而後才能更好地在上面進行開發。

本文分享自華爲雲社區《Antlr4簡明使用教程》,原文做者:maijun 。

 

點擊關注,第一時間瞭解華爲雲新鮮技術~

相關文章
相關標籤/搜索