溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

什么是Antlr4

發(fā)布時(shí)間:2021-10-19 16:53:53 來源:億速云 閱讀:311 作者:iii 欄目:編程語言

本篇內(nèi)容主要講解“什么是Antlr4”,感興趣的朋友不妨來看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“什么是Antlr4”吧!

1. Antlr4簡(jiǎn)單介紹

Antlr4(Another Tool for Language Recognition)是一款基于Java開發(fā)的開源的語法分析器生成工具,能夠根據(jù)語法規(guī)則文件生成對(duì)應(yīng)的語法分析器,廣泛應(yīng)用于DSL構(gòu)建,語言詞法語法解析等領(lǐng)域?,F(xiàn)在在非常多的流行的框架中都用使用,例如,在構(gòu)建特定語言的AST方面,CheckStyle工具,就是基于Antlr來解析Java的語法結(jié)構(gòu)的(當(dāng)前Java Parser是基于JavaCC來解析Java文件的,據(jù)說有規(guī)劃在下個(gè)版本改用Antlr來解析),還有就是廣泛應(yīng)用在DSL構(gòu)建上,著名的Eclipse Xtext就有使用Antlr。

Antlr可以生成不同target的AST(https://www.antlr.org/download.html),包括Java、C++、JS、Python、C#等,可以滿足不同語言的開發(fā)需求。當(dāng)前Antlr最新穩(wěn)定版本為4.9,Antlr4官方github倉(cāng)庫中,已經(jīng)有數(shù)十種語言的grammer(https://github.com/antlr/grammars-v4,不過雖然這么多語言的規(guī)則文法定義都在一個(gè)倉(cāng)庫中,但是每種語言的grammer的license是不一樣的,如果要使用,需要參考每種語言自己的語法結(jié)構(gòu)的license)。

本文將首先介紹Antlr4 grammer的定義方式(簡(jiǎn)單介紹語法結(jié)構(gòu),并介紹如何基于IDEA Antlr4插件進(jìn)行調(diào)試),然后介紹如何通過Antlr4 grammer生成對(duì)應(yīng)的AST,最后介紹Antlr4 的兩種AST遍歷方式:Visitor方式和Listener方式。

2. Antlr4規(guī)則文法

下面簡(jiǎn)單介紹一部分Antlr4的g4(grammar)文件的寫法 (主要參考Antlr4官方wiki:https://github.com/antlr/antlr4/blob/master/doc/index.md)。 最有效的學(xué)習(xí)Antlr4的規(guī)則文法的寫法的方法,就是參考已有的規(guī)則文法,大家在學(xué)習(xí)中,可以參考已有語言的文法。而且Antlr4已經(jīng)實(shí)現(xiàn)了數(shù)十種語言的文法,如果需要自己定義,可以參考和自己的語言最接近的文法來開發(fā)。

2.1 Antlr4規(guī)則基本語法和關(guān)鍵字

首先,如果有一點(diǎn)兒C或者Java基礎(chǔ),對(duì)上手Antlr4 g4的文法非??臁V饕邢旅娴囊恍┪姆ńY(jié)構(gòu):

  • 注釋:和Java的注釋完全一致,也可參考C的注釋,只是增加了JavaDoc類型的注釋;

  • 標(biāo)志符:參考Java或者C的標(biāo)志符命名規(guī)范,針對(duì)Lexer 部分的 Token 名的定義,采用全大寫字母的形式,對(duì)于parser rule命名,推薦首字母小寫的駝峰命名;

  • 不區(qū)分字符和字符串,都是用單引號(hào)引起來的,同時(shí),雖然Antlr g4支持 Unicode編碼(即支持中文編碼),但是建議大家盡量還有英文;

  • Action,行為,主要有@header 和@members,用來定義一些需要生成到目標(biāo)代碼中的行為,例如,可以通過@header設(shè)置生成的代碼的package信息,@members可以定義額外的一些變量到Antlr4語法文件中;

  • Antlr4語法中,支持的關(guān)鍵字有:import, fragment, lexer, parser, grammar, returns, locals, throws, catch, finally, mode, options, tokens。

2.2 Antlr4語法介紹

2.2.1語法文件的整體結(jié)構(gòu)及寫法示例

Antlr4整體結(jié)構(gòu)如下:

/** Optional javadoc style comment */

grammar Name;

options {...}

import ... ;

 

tokens {...}

channels {...} // lexer only

@actionName {...}

 

rule1 // parser and lexer rules, possibly intermingled

...

ruleN

一般如果語法非常復(fù)雜,會(huì)基于Lexer和Parser寫到兩個(gè)不同的文件中(例如Java,可參考:https://github.com/antlr/grammars-v4/tree/master/java/java8),如果語法比較簡(jiǎn)單,可以只寫到一個(gè)文件中(例如Lua,可參考:https://github.com/antlr/grammars-v4/blob/master/lua/Lua.g4)。

下面我們結(jié)合Lua.g4中的一部分語法結(jié)構(gòu),介紹使用方法。寫Antlr4的文法,需要依據(jù)源碼的結(jié)構(gòu)來決定。定義時(shí),依據(jù)源碼文件的寫法,從上到下開始構(gòu)造語法結(jié)構(gòu)。例如,下面是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)*
    ;

如上語法中,整個(gè)文件被表示成一個(gè)chunk,chunk表示為一個(gè)block和一個(gè)文件結(jié)束符(EOF);block又被表示為一系列的語句的集合,而每一種語句又有特定的語法結(jié)構(gòu),包含了特定的表達(dá)式、關(guān)鍵字、變量、常量等信息,然后遞歸表達(dá)式的文法組成,變量的寫法等,最終全部都?xì)w結(jié)到Lexer(Token)上,遞歸樹結(jié)束。

上面其實(shí)已經(jīng)可以看到Antlr4規(guī)則的寫法,下面介紹一部分比較重要的規(guī)則的寫法。

2.2.2 替代標(biāo)簽

首先,如2.2.1節(jié)的代碼所示,stat可以有非常多的類型,例如變量定義、函數(shù)定義、if、while等,這些都沒有進(jìn)行區(qū)分,這樣解析出來語法樹時(shí),會(huì)很不清晰,需要結(jié)合很多的標(biāo)記完成具體語句的識(shí)別,這種情況下,我們可以結(jié)合替代標(biāo)簽完成區(qū)分,如下代碼:

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
    ;

通過在語句后面,添加 #替代標(biāo)簽,可以將語句轉(zhuǎn)換為這些替代標(biāo)簽,從而加以區(qū)分。

2.2.3 操作符優(yōu)先級(jí)處理

默認(rèn)情況下,ANTLR從左到右結(jié)合運(yùn)算符,然而某些像指數(shù)群這樣的運(yùn)算符則是從右到左??梢允褂眠x項(xiàng)assoc手動(dòng)指定運(yùn)算符記號(hào)上的相關(guān)性。如下面的操作:

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

^ 表示指數(shù)運(yùn)算,增加 assoc=right,表示該運(yùn)算符是右結(jié)合。

實(shí)際上,Antlr4 已經(jīng)對(duì)一些常用的操作符的優(yōu)先級(jí)進(jìn)行了處理,例如加減乘除等,這些就不需要再特殊處理。

2.2.4 隱藏通道

很多信息,例如注釋、空格等,是結(jié)果信息生成不需要處理的,但是我們又不適合直接丟棄,安全地忽略掉注釋和空格的方法是把這些發(fā)送給語法分析器的記號(hào)放到一個(gè)“隱藏通道”中,語法分析器僅需要調(diào)協(xié)到單個(gè)通道即可。我們可以把任何我們想要的東西傳遞到其它通道中。在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,不會(huì)被語法解析階段處理,但是可以通過Token遍歷獲取到。

2.2.5 常見詞法結(jié)構(gòu)

Antlr4采用BNF范式,用’|’表示分支選項(xiàng),’*’表示匹配前一個(gè)匹配項(xiàng)0次或者多次,’+’ 表示匹配前一個(gè)匹配項(xiàng)至少一次。下面介紹幾種常見的詞法舉例(均來自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) 數(shù)字

INT
    : Digit+
    ;

Digit
    : [0-9]
    ;

3) ID(命名)

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

3. 基于IDEA調(diào)試Antlr4語法規(guī)則(文法可視化)

如果要安裝Antlr4,選擇 File -> Settings -> Plugins,然后在搜索框搜索 Antlr安裝即可,可以選擇安裝搜索出來的最新版本,下圖是剛剛安裝的ANTLR v4,版本是v1.15,支持最新的Antlr 4.9版本。

什么是Antlr4

基于IDEA調(diào)試Antlr4語法一般步驟:

1) 創(chuàng)建一個(gè)調(diào)試工程,并創(chuàng)建一個(gè)g4文件

這里,我自己測(cè)試用Java開發(fā),所以創(chuàng)建的是一個(gè)Maven工程,g4文件放在了src/main/resources 目錄下,取名 Test.g4

2)寫一個(gè)簡(jiǎn)單的語法結(jié)構(gòu)

這里我們參考寫一個(gè)加減乘除操作的表達(dá)式,然后在賦值操作對(duì)應(yīng)的Rule上右鍵,可選擇測(cè)試:

什么是Antlr4

如上圖,expr 表示的是一個(gè)乘法操作,所以我們?nèi)缦聹y(cè)試:

什么是Antlr4

但是,如果改成一個(gè)加法操作,則無法識(shí)別,只能識(shí)別到第一個(gè)數(shù)字。

什么是Antlr4

這種情況下,就需要繼續(xù)擴(kuò)充 expr的定義,豐富不同的語法,來繼續(xù)支持其他的語法,如下:

什么是Antlr4

還可以繼續(xù)擴(kuò)充其他類型的支持,這樣一步步將整個(gè)語言的語法都支持完整。這里,我們形成的一個(gè)完整的格式如下(表示整形數(shù)字的加減乘除):

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插件自動(dòng)生成(針對(duì)Java工程,也可以用于Gradle)

pom.xml設(shè)置Antlr4 Maven插件,可以通過執(zhí)行 mvn generate-sources自動(dòng)生成需要的代碼(參考鏈接: https://www.antlr.org/api/maven-plugin/latest/antlr4-mojo.html,主要的意義在于,代碼入庫的時(shí)候,不需要再將生成的這些語法文件入庫,減少庫里面的代碼冗余,只包含自己開發(fā)的代碼,不會(huì)有自動(dòng)生成的代碼,也不需要做clean code整改),下面是一個(gè)示例:

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

按照上面設(shè)置后,只需要執(zhí)行 mvn generate-sources 即可在maven工程中自動(dòng)生成代碼。

  • 命令行方式

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

什么是Antlr4

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

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

這樣就可以生成Python3 target的源碼,支持的源碼可以從上面鏈接查看,如果不希望生成Listener,可以添加參數(shù) -no-listener

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

Antlr4在AST遍歷時(shí),支持兩種設(shè)計(jì)模式:訪問者設(shè)計(jì)模式 和 監(jiān)聽器模式。

對(duì)于 訪問者設(shè)計(jì)模式,我們需要自己定義對(duì) AST 的訪問(https://xie.infoq.cn/article/5f80da3c014fd69f8dbe09b28,這是一篇針對(duì)訪問者設(shè)計(jì)模式的介紹,大家可以參考)。下面直接通過代碼展示訪問者模式在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方法中,解析出了表達(dá)式的AST結(jié)構(gòu),同時(shí)在源碼中也定義了一個(gè)Visitor:TestVisitor,訪問AddContext,并且打印該加表達(dá)式的前后兩個(gè)表達(dá)式,上面例子的輸出為:

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

4.2 監(jiān)聽器模式(觀察者模式)

對(duì)于監(jiān)聽器模式,就是通過監(jiān)聽某對(duì)象,如果該對(duì)象上有特定的事件發(fā)生,則觸發(fā)該監(jiān)聽行為執(zhí)行。比如有個(gè)監(jiān)控(監(jiān)聽器),監(jiān)控的是大門(事件對(duì)象),如果發(fā)生了闖門的行為(事件源),則進(jìn)行報(bào)警(觸發(fā)操作行為)。

在Antlr4中,如果使用監(jiān)聽器模式,首先需要開發(fā)一個(gè)監(jiān)聽器,該監(jiān)聽器可以監(jiān)聽每個(gè)AST節(jié)點(diǎn)(例如表達(dá)式、語句等)的不同的行為(例如進(jìn)入該節(jié)點(diǎn)、結(jié)束該節(jié)點(diǎn))。在使用時(shí),Antlr4會(huì)對(duì)生成的AST進(jìn)行遍歷(ParseTreeWalker),如果遍歷到某個(gè)具體的節(jié)點(diǎn),并且執(zhí)行了特定行為,就會(huì)觸發(fā)監(jiān)聽器的事件。

監(jiān)聽器方法是沒有返回值的(即返回類型是void)。因此需要一種額外的數(shù)據(jù)結(jié)構(gòu)(可以通過Map或者棧)來存儲(chǔ)當(dāng)次的計(jì)算結(jié)果,供下一次計(jì)算調(diào)用。

一般來說,面向程序靜態(tài)分析時(shí),都是使用訪問者模式的,很少使用監(jiān)聽器模式(無法主動(dòng)控制遍歷AST的順序,不方便在不同節(jié)點(diǎn)遍歷之間傳遞數(shù)據(jù)),用法對(duì)咱們也不友好,所以本文不介紹監(jiān)聽器模式,如果有興趣,可以自己搜索測(cè)試使用。

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

這部分實(shí)際上,算是Antlr4最基礎(chǔ)的內(nèi)容,但是放到最后一部分來講,有特定的目的,就是探討一下詞法解析和語法解析的界限,以及Antlr4的結(jié)果的處理。

5.1 Antlr4執(zhí)行階段

如前面的語法定義,分為L(zhǎng)exer和Parser,實(shí)際上表示了兩個(gè)不同的階段:

  • 詞法分析階段:對(duì)應(yīng)于Lexer定義的詞法規(guī)則,解析結(jié)果為一個(gè)一個(gè)的Token;

  • 解析階段:根據(jù)詞法,構(gòu)造出來一棵解析樹或者語法樹。

如下圖所示:

什么是Antlr4

5.2 詞法解析和語法解析的調(diào)和

首先,我們應(yīng)該有個(gè)普遍的認(rèn)知:語法解析相對(duì)于詞法解析,會(huì)產(chǎn)生更多的開銷,所以,應(yīng)該盡量將某些可能的處理在詞法解析階段完成,減少語法解析階段的開銷,主要下面的這些例子:

  • 合并語言不關(guān)心的標(biāo)記,例如,某些語言(例如js)不區(qū)分int、double,只有 number,那么在詞法解析階段,就不需要將int和double區(qū)分開,統(tǒng)一合并為一個(gè)number;

  • 空格、注釋等信息,對(duì)于語法解析并無大的幫助,可以在詞法分析階段剔除掉;

  • 諸如標(biāo)志符、關(guān)鍵字、字符串和數(shù)字這樣的常用記號(hào),均應(yīng)該在詞法解析時(shí)完成,而不要到語法解析階段再進(jìn)行。

但是,這樣的操作在節(jié)省了語法分析的開銷之外,其實(shí)對(duì)我們也產(chǎn)生了一些影響:

  • 雖然語言不區(qū)分類型,例如只有 number,沒有 int 和 double 等,但是面向靜態(tài)代碼分析,我們可能需要知道確切的類型來幫助分析特定的缺陷;

  • 雖然注釋對(duì)代碼幫助不大,但是我們有時(shí)候也需要解析注釋的內(nèi)容來進(jìn)行分析,如果無法在語法解析的時(shí)候獲取,那么就需要遍歷Token,從而導(dǎo)致靜態(tài)代碼分析開銷更大等;

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

5.3 解析樹vs語法樹

大部分的資料中,都把Antlr4生成的樹狀結(jié)構(gòu),稱為解析樹或者是語法樹,但是,如果我們細(xì)究的話,可能說成是解析樹更加準(zhǔn)確,因?yàn)锳ntlr4的結(jié)果,只是簡(jiǎn)單的文法解析,不能稱之為語法樹(語法樹應(yīng)該是能夠體現(xiàn)出來語法特性的信息),如上面的那些問題,就很難在Antlr4生成的解析樹上獲取到。

所以,現(xiàn)在很多工具,基于Antlr4進(jìn)行封裝,然后進(jìn)行了更進(jìn)一步地處理,從而獲取到了更加豐富的語法樹,例如CheckStyle。因此,如果通過Antlr4解析語言簡(jiǎn)單使用,可以直接基于Antlr4的結(jié)果開發(fā),但是如果要進(jìn)行更加深入的處理,就需要對(duì)Antlr4的結(jié)果進(jìn)行更進(jìn)一步的處理,以更符合我們的使用習(xí)慣(例如,Java Parser格式的Java的AST,Clang格式的C/C++的AST),然后才能更好地在上面進(jìn)行開發(fā)。

到此,相信大家對(duì)“什么是Antlr4”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)
推薦閱讀:
  1. 什么是PHP
  2. 什么是python

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI