自研 DSL 神器:萬(wàn)字拆解 ANTLR 4 核心原理與高級(jí)應(yīng)用
DSL(領(lǐng)域特定語(yǔ)言) 是一種為解決特定領(lǐng)域的問(wèn)題而專門設(shè)計(jì)的計(jì)算機(jī)語(yǔ)言,它不同于通用編程語(yǔ)言(如 Python、Java)。它通常具有高度定制化的語(yǔ)法和結(jié)構(gòu),聚焦于某個(gè)特定任務(wù)或領(lǐng)域(如數(shù)據(jù)庫(kù)查詢、硬件配置、報(bào)表生成),通過(guò)提供更簡(jiǎn)潔、直觀且貼近領(lǐng)域術(shù)語(yǔ)的表達(dá)方式,大幅提升該領(lǐng)域人員的工作效率和生產(chǎn)力,降低復(fù)雜性。
通俗來(lái)說(shuō),DSL 就像是為某個(gè)專業(yè)領(lǐng)域量身定做的“行話”工具。
說(shuō)到構(gòu)建自定義 DSL,高效且靈活的語(yǔ)法解析至關(guān)重要,ANTLR 正是解決這一核心挑戰(zhàn)的利器。
簡(jiǎn)介
- 官方地址:https://www.antlr.org/
- GitHub:https://github.com/antlr/antlr4
- 在線調(diào)試:http://lab.antlr.org/
- IDEA插件:ANTLR V4
ANTLR 4(ANother Tool for Language Recognition,版本4)是一個(gè)開(kāi)源的解析器生成器工具,用于構(gòu)建語(yǔ)言識(shí)別程序。它能夠根據(jù)用戶定義的語(yǔ)法規(guī)則,自動(dòng)生成詞法分析器(Lexer)和語(yǔ)法分析器(Parser),從而實(shí)現(xiàn)對(duì)結(jié)構(gòu)化文本(如編程語(yǔ)言、配置文件、數(shù)據(jù)格式等)的解析、轉(zhuǎn)換或翻譯。
ANTLR 4 最大的核心價(jià)值就是降低語(yǔ)言處理的門檻。在ANTRL 4沒(méi)有出現(xiàn)之前,語(yǔ)言處理主要依賴正則表達(dá)式、手工編寫解析器以及早期的解析器生成工具(如Lex/Yacc)。

ANTLR 4 的使用很簡(jiǎn)單,因?yàn)槠浯嬖诘谋旧淼囊饬x就是為了加快語(yǔ)言類應(yīng)用程序的編寫速度,就是為了非專業(yè)人員對(duì)語(yǔ)言類應(yīng)用程序快速開(kāi)發(fā)而生的。
首先我們要進(jìn)行ANTLR 4元語(yǔ)言的編寫,也就是需要我們根據(jù)我們自己的需要來(lái)編寫一份語(yǔ)法文件,一份后綴為 .g4 的文件,這份文件是我們構(gòu)建ANTLR 4語(yǔ)言類應(yīng)用程序的基礎(chǔ),目前ANTLR 4已經(jīng)支持了數(shù)十種編程語(yǔ)言的生成,可以滿足不同語(yǔ)言的開(kāi)發(fā)需求。
官方也提供了相關(guān)的文件,GitHub:https://github.com/antlr/grammars-v4。
有了這些 Java 文件,語(yǔ)言類應(yīng)用程序的開(kāi)發(fā)人員就不需要再去思考如何手動(dòng)編寫解析語(yǔ)法樹(shù)的程序,因?yàn)锳NTLR 4已經(jīng)幫我們把這些事情都做了,ANTLR 4自帶的jar 包和自動(dòng)生成的這些語(yǔ)法分析器以及之后所提到的監(jiān)聽(tīng)器 Listener 和訪問(wèn)器 Visitor 都能夠完美的幫我們來(lái)處理任何語(yǔ)言類應(yīng)用程序的自定義需求,從而真正達(dá)到即使你沒(méi)學(xué)過(guò)編譯原理也能自己開(kāi)發(fā)應(yīng)用程序的效果。
ANTLR 是用 Java 編寫的,因此你需要首先安裝 Java,哪怕你的目標(biāo)是使用 ANTLR 來(lái)生成其他語(yǔ)言(如C#和C++)的解析器。
下圖是我使用 IDEA 中的 ANTLR 4 插件,以及我自己編寫的語(yǔ)法,自動(dòng)生成的語(yǔ)法解析樹(shù),這一切都是ANTLR 4幫我們自動(dòng)完成的。

簡(jiǎn)而言之,ANTLR 工具將語(yǔ)法文件轉(zhuǎn)換成可以識(shí)別該語(yǔ)法文件所描述的語(yǔ)言的程序。例如,給定一個(gè)識(shí)別 JSON 的語(yǔ)法,ANTLR工具將會(huì)根據(jù)該語(yǔ)法生成一個(gè)程序,此程序可以通過(guò) ANTLR 運(yùn)行庫(kù)來(lái)識(shí)別輸入的 JSON。
基礎(chǔ)概念
文件聲明
以下是一個(gè)包含完整頭部聲明的 ANTLR 4 語(yǔ)法文件示例,涵蓋所有關(guān)鍵字的解釋:
// =========== ANTLR4 語(yǔ)法文件頭部聲明示例 ===========
grammar MathParser; // [1] 主聲明
// [2] 導(dǎo)入聲明(組合語(yǔ)法)
import TrigParser, VectorParser; // 導(dǎo)入其他語(yǔ)法模塊
// [3] 選項(xiàng)配置
options {
language = Java; // 目標(biāo)生成語(yǔ)言
tokenVocab = CoreTokens; // 從外部語(yǔ)法導(dǎo)入詞法符號(hào)
superClass = MathBase; // 自定義基類
contextSuperClass = MyCtx; // 自定義上下文基類
}
// [4] 輔助符號(hào)聲明
tokens {
// 顯式定義新token
PI = 'π'; // 帶字面量的token
FUNCTION_CALL, // 無(wú)字面量的抽象token
VECTOR_DOT_PRODUCT // 用于語(yǔ)法樹(shù)節(jié)點(diǎn)的標(biāo)簽
}
// [5] 頭部注入 (生成文件頂部的代碼)
@header {
package com.company.math;
importstatic com.company.math.TrigUtil.*;
}
// [6] 成員注入 (向解析器類添加代碼)
@members {
privateboolean debug = true;
privateint errorCount = 0;
@Override
publicvoidreportError(RecognitionException e){
errorCount++;
super.reportError(e);
}
publicintgetErrorCount(){
return errorCount;
}
}
// [7] 規(guī)則定義區(qū)
expression: /* 規(guī)則內(nèi)容 */;
// ========================================- grammar:定義語(yǔ)法名稱(必須匹配文件名),聲明完整/詞法/解析語(yǔ)法類型。
- import:導(dǎo)入外部語(yǔ)法文件實(shí)現(xiàn)規(guī)則復(fù)用,支持模塊化開(kāi)發(fā)。語(yǔ)法導(dǎo)入允許你將語(yǔ)法分解成可復(fù)用的邏輯單元。ANTLR 處理被導(dǎo)入的語(yǔ)法的方式和面向?qū)ο笳Z(yǔ)言中的父類非常相似。一個(gè)語(yǔ)法會(huì)從其導(dǎo)入的語(yǔ)法中繼承所有的規(guī)則、詞法符號(hào)聲明和具名的動(dòng)作。位于“主語(yǔ)法”中的規(guī)則將會(huì)覆蓋其導(dǎo)入的語(yǔ)法中的規(guī)則,以此來(lái)實(shí)現(xiàn)繼承機(jī)制。ANTLR將被導(dǎo)入的規(guī)則放置在主語(yǔ)法的詞法規(guī)則列表末尾。這意味著,主語(yǔ)法中的詞法規(guī)則具有比被導(dǎo)入語(yǔ)法中的規(guī)則更高的優(yōu)先級(jí)。
- options:配置代碼生成選項(xiàng)(目標(biāo)語(yǔ)言/基類/符號(hào)表等)。
- tokens:聲明輔助符號(hào)(抽象Token/別名/語(yǔ)法樹(shù)標(biāo)簽)。tokens 區(qū)域存在的意義在于,它定義了一份語(yǔ)法所需,但卻未在本語(yǔ)法中列出對(duì)應(yīng)規(guī)則的詞法符號(hào)。大多數(shù)情況下,tokens 區(qū)域用于定義本語(yǔ)法中動(dòng)作所需的詞法符號(hào)類型。
- @header:向生成文件頂部注入代碼(包聲明/導(dǎo)入語(yǔ)句)。用于將代碼注入生成的識(shí)別類中的類聲明之前。用于將代碼注入為識(shí)別類的字段和方法。
- @members:向解析器類添加自定義成員(字段/方法/狀態(tài)管理)。
關(guān)于 @header 和 @members,其中 @header 用于當(dāng) ANTLR 4 工具生成詞法分析器和語(yǔ)法分析器時(shí),將 @header 中的內(nèi)容原封不動(dòng)的復(fù)制到生成的 Java 文件的頂部,而 @members 用于將代碼插入到生成的 Java 類當(dāng)中,其中可以包含字段聲明,自定義方法等內(nèi)容。

從圖中我們可以看到我們預(yù)先在語(yǔ)法文件中進(jìn)行了 @header 和 @members 的定義和編寫,然后利用 ANTLR 4 工具自動(dòng)生成我們所需要的詞法解析器和語(yǔ)法分析器等相關(guān)的 Java 文件,后續(xù)生成的這些 Java 文件中的相關(guān)位置包含了我們?cè)?@header 和 @members 中所定義的相關(guān)內(nèi)容。
不帶前綴的語(yǔ)法聲明是混合語(yǔ)法,可以同時(shí)包含詞法規(guī)則和語(yǔ)法規(guī)則。欲創(chuàng)建一份只允許語(yǔ)法規(guī)則出現(xiàn)的文件,使用如下聲明:
parser grammar Name;同理,純?cè)~法的文件如下所示:
lexer grammar Name;詞法規(guī)則
詞法文件的規(guī)則以大寫字母開(kāi)頭。
將字符聚集為單詞或者符號(hào)(詞法符號(hào),token)的過(guò)程稱為詞法分析(lexicalanalysis)或者詞法符號(hào)化(tokenizing)。我們把可以將輸入文本轉(zhuǎn)換為詞法符號(hào)的程序稱為詞法分析器(lexer)。詞法分析器可以將相關(guān)的詞法符號(hào)歸類,例如INT(整數(shù))、ID(標(biāo)識(shí)符)、FLOAT(浮點(diǎn)數(shù))等。當(dāng)語(yǔ)法分析器不關(guān)心單個(gè)符號(hào),而僅關(guān)心符號(hào)的類型時(shí),詞法分析器就需要將詞匯符號(hào)歸類。詞法符號(hào)包含至少兩部分信息:詞法符號(hào)的類型(從而能夠通過(guò)類型來(lái)識(shí)別詞法結(jié)構(gòu))和該詞法符號(hào)對(duì)應(yīng)的文本。

Java 詞法規(guī)則示例:

接下來(lái)介紹一下詞法規(guī)則是如何編寫的。

如上圖所示詞法規(guī)則以大寫的字母開(kāi)頭,或者以冒號(hào)開(kāi)頭后跟大寫字母,這樣做是為了與之后所要介紹的語(yǔ)法規(guī)則做區(qū)分。例如上圖中我們就給出了一些示例的規(guī)則,定義了INT,ID,STRING類型的詞法單元,冒號(hào)后面是對(duì)這些詞法單元的描述。
這種詞法規(guī)則的類型被稱之為標(biāo)準(zhǔn)詞法符號(hào)類型,這一類詞法規(guī)則必須用大寫字母開(kāi)頭,經(jīng)過(guò)ANTLR 4工具處理會(huì)生成可直接在解析器中引用的符號(hào),其規(guī)則匹配的優(yōu)先級(jí)由在語(yǔ)法文件中聲明詞法規(guī)則的順序和詞法規(guī)則的長(zhǎng)度來(lái)決定。
其中有很多符號(hào),比如“+”代表著 INTEGER 這一詞法規(guī)則使用出現(xiàn)至少一次的自然數(shù)組成的,而 IDENTIFIER 這一規(guī)則中的“*”則代表著 IDENTIFIER 這一詞法規(guī)則是由大小寫字母或下劃線加上至少出現(xiàn)0次的單詞字符組成的。而 STRING 詞法規(guī)則中單引號(hào)中間的內(nèi)容則代表著中間的內(nèi)容直接匹配,是固定的。

第二類詞法規(guī)則被稱之為片段規(guī)則,通過(guò)關(guān)鍵字 fragment 來(lái)定義。
片段規(guī)則具有以下特點(diǎn):首先片段規(guī)則是不能獨(dú)立匹配的,fragment 規(guī)則不能直接用于匹配輸入文本。它們只能被其他非片段的詞法規(guī)則所引用。
將一條規(guī)則聲明為 fragment 可以告訴 ANTLR,該規(guī)則本身不是一個(gè)詞法符號(hào),它只會(huì)被其他的詞法規(guī)則使用。這意味著我們不能在文法規(guī)則中引用 HEX_DIGIT。
通常使用片段規(guī)則是為了提高可讀性和重用性,通過(guò)將常用的字符模式提取為片段規(guī)則,可以使詞法規(guī)則更加簡(jiǎn)潔和易于維護(hù)。例如,可以將字母或數(shù)字的模式定義為片段規(guī)則,然后在多個(gè)詞法規(guī)則中引用它們。

第三類詞法規(guī)則被稱之為指令規(guī)則。
- 第一種被稱之為跳過(guò)指令,ANTLR 4在詞法分析過(guò)程中會(huì)忽略這些匹配的空白字符,不會(huì)將它們作為(token)傳遞給語(yǔ)法分析器;
- 第二種被稱之為通道指令,使用 -> channel(HIDDEN) 指令,ANTLR 將這些注釋標(biāo)記發(fā)送到一個(gè)隱藏通道,使得它們不會(huì)被默認(rèn)的語(yǔ)法分析器處理,但仍然可以在需要時(shí)訪問(wèn);
- 第三種被稱之為模式指令,使用 -> pushMode(XML_MODE) 指令,ANTLR 會(huì)切換到 XML_MODE 模式,這允許在不同的上下文中使用不同的詞法規(guī)則集;
- 最后一種被稱之為類型指令,使用 -> type(DOLLAR_SIGN) 指令,ANTLR 會(huì)將匹配的標(biāo)記類型動(dòng)態(tài)設(shè)置為 DOLLAR_SIGN,這可以用于在語(yǔ)法分析中對(duì)不同類型的標(biāo)記進(jìn)行區(qū)分和處理。
語(yǔ)法規(guī)則
語(yǔ)法文件的規(guī)則以小寫字母開(kāi)頭。
首先我們來(lái)介紹語(yǔ)法規(guī)則的規(guī)則組成元素。

以上名為 assignment 的語(yǔ)法規(guī)則中所包含的大寫字母序列 IDENTIFIER 被稱之終結(jié)符,它來(lái)自詞法分析器,我們?cè)谠~法規(guī)則中會(huì)對(duì)其進(jìn)行定義。

與此相對(duì)的是非終結(jié)符,比如以上 expression 語(yǔ)法規(guī)則中的 term,這些非終結(jié)符,由小寫字母命名,并且由其他規(guī)則所定義。

除了之前介紹的終結(jié)符和非終結(jié)符兩種元素之外,還有帶參數(shù)的規(guī)則和帶返回值的規(guī)則。因此,參數(shù)和返回值也是語(yǔ)法規(guī)則的重要元素。
[String className],表示這個(gè)規(guī)則接受一個(gè)參數(shù) className,類型為 String。在解析過(guò)程中,可以將外部傳入的類名用于匹配。[Object value],表示這個(gè)規(guī)則在匹配成功后會(huì)返回一個(gè) Object 類型的值,存儲(chǔ)在 value 中。
ANTLR 4的語(yǔ)法規(guī)則的核心語(yǔ)法構(gòu)造分為四種模式,分別是序列模式、選擇模式、分組模式、循環(huán)模式。
序列模式
sqlSelect : SELECT column FROM table WHERE condition;元素必須嚴(yán)格按順序出現(xiàn)(如 SQL 語(yǔ)句結(jié)構(gòu))。
選擇模式
dataType : INT | STRING | BOOL;多選一匹配(如數(shù)據(jù)類型只能為三者之一)。
分組模式
functionCall : ID '(' (arg (',' arg)*)? ')';括號(hào)強(qiáng)制組合子規(guī)則(如函數(shù)參數(shù)列表的逗號(hào)分隔結(jié)構(gòu))。
循環(huán)模式
emailList : address (',' address)+;
后綴運(yùn)算符控制重復(fù)次數(shù)(如至少一個(gè)郵箱地址的逗號(hào)分隔列表)。
規(guī)則標(biāo)簽
在 ANTLR 4 中,規(guī)則標(biāo)簽(Rule Labels)是提升語(yǔ)法可讀性、精確控制解析樹(shù)生成的關(guān)鍵機(jī)制,我們可以使用 # 給最外層的備選分支添加標(biāo)簽,以獲得更加精確的語(yǔ)法分析器監(jiān)聽(tīng)器事件。一條規(guī)則中的備選分支要么全部帶上標(biāo)簽,要么全部不帶標(biāo)簽。標(biāo)簽主要有兩種應(yīng)用形式:
分支備選標(biāo)簽(Alternative Labels)
在規(guī)則的選擇分支(|)中標(biāo)注備選項(xiàng):
expression
: left=expr '+' right=expr # AddExpr // # 定義標(biāo)簽
| left=expr '*' right=expr # MulExpr
| NUMBER # NumLiteral
;作用:
為每個(gè)分支生成獨(dú)立的上下文類(如
AddExprContext),在監(jiān)聽(tīng)器/訪問(wèn)器中提供類型精確的訪問(wèn)方法
生成代碼優(yōu)勢(shì):
// 自動(dòng)生成精確的進(jìn)入/退出方法
@Override
public void enterAddExpr(MyParser.AddExprContext ctx) {
// 直接訪問(wèn)帶標(biāo)簽的元素
ExprContext left = ctx.left; // 無(wú)需遍歷子節(jié)點(diǎn)
ExprContext right = ctx.right;
}元素標(biāo)簽(Element Labels)
在規(guī)則中標(biāo)記特定子元素:
funcCall : func=ID '(' args+=expr (',' args+=expr)* ')';三種標(biāo)記方式:
標(biāo)簽語(yǔ)法 | 適用對(duì)象 | 返回值類型 | 訪問(wèn)示例 |
| 詞法符號(hào) |
|
|
| 規(guī)則引用 |
子類 |
|
| 重復(fù)元素 |
|
|
實(shí)戰(zhàn)應(yīng)用場(chǎng)景
- 場(chǎng)景1:四則運(yùn)算精確解析
expr
: left=expr op=('*'|'/') right=expr # MulDiv
| left=expr op=('+'|'-') right=expr # AddSub
| NUM # Number
| '(' expr ')' # Parens
;生成的監(jiān)聽(tīng)器接口:
voidenterMulDiv(ExprParser.MulDivContext ctx);
voidenterAddSub(ExprParser.AddSubContext ctx);
voidexitMulDiv(ExprParser.MulDivContext ctx);
// ...- 場(chǎng)景2:函數(shù)調(diào)用語(yǔ)義分析
functionCall
: func=ID '('
(firstArg=expr (',' otherArgs+=expr)*)?
')' # FuncCall
;在訪問(wèn)器中直接獲取元素:
public Object visitFuncCall(FuncCallContext ctx){
String funcName = ctx.func.getText();
List<ExprContext> args = new ArrayList<>();
if(ctx.firstArg != null) {
args.add(ctx.firstArg);
args.addAll(ctx.otherArgs);
}
// ...處理函數(shù)調(diào)用
}TokenStream
詞法分析器處理字符序列并將生成的詞法符號(hào)提供給語(yǔ)法分析器,語(yǔ)法分析器隨即根據(jù)這些信息來(lái)檢查語(yǔ)法的正確性并建造出一棵語(yǔ)法分析樹(shù)。這個(gè)過(guò)程對(duì)應(yīng)的ANTLR 類是 CharStream、Lexer、Token、Parser,以及 ParseTree。連接詞法分析器和語(yǔ)法分析器的“管道”就是 TokenStream。下圖展示了這些類型的對(duì)象在內(nèi)存中的交互方式。

ParseTree 的子類 RuleNode 和 TerminalNode ,二者分別是子樹(shù)的根節(jié)點(diǎn)和葉子節(jié)點(diǎn)。RuleNode 有一些令人熟悉的方法,例如 getChild() 和 getParent() ,但是,對(duì)于一個(gè)特定的語(yǔ)法,RuleNode 并不是確定不變的。為了更好地支持對(duì)特定節(jié)點(diǎn)的元素的訪問(wèn),ANTLR 會(huì)為每條規(guī)則生成一個(gè) RuleNode 的子類。如下圖所示,在我們的賦值語(yǔ)句的例子中,子樹(shù)根節(jié)點(diǎn)的類型實(shí)際上是:StatContext、AssignContext 以及 ExprContext。

因?yàn)檫@些根節(jié)點(diǎn)包含了使用規(guī)則識(shí)別詞組過(guò)程中的全部信息,它們被稱為上下文(context)對(duì)象。每個(gè)上下文對(duì)象都知道自己識(shí)別出的詞組中,開(kāi)始和結(jié)束位置處的詞法符號(hào),同時(shí)提供訪問(wèn)該詞組全部元素的途徑。例如,AssignContext 類提供了方法 ID() 和方法 expr() 來(lái)訪問(wèn)標(biāo)識(shí)符節(jié)點(diǎn)和代表表達(dá)式的子樹(shù)。
監(jiān)聽(tīng)器和訪問(wèn)器
ANTLR 的運(yùn)行庫(kù)提供了兩種遍歷樹(shù)的機(jī)制。默認(rèn)情況下,ANTLR 使用內(nèi)建的遍歷器訪問(wèn)生成的語(yǔ)法分析樹(shù),并為每個(gè)遍歷時(shí)可能觸發(fā)的事件生成一個(gè)語(yǔ)法分析樹(shù)監(jiān)聽(tīng)器接口(parse-tree listener interface)。監(jiān)聽(tīng)器非常類似于 XML 解析器生成的 SAX 文檔對(duì)象。SAX 監(jiān)聽(tīng)器接收類似 startDocument() 和 endDocument() 的事件通知。一個(gè)監(jiān)聽(tīng)器的方法實(shí)際上就是回調(diào)函數(shù),正如我們?cè)趫D形界面程序中響應(yīng)復(fù)選框點(diǎn)擊事件一樣。除了監(jiān)聽(tīng)器的方式,我們還將介紹另外一種遍歷語(yǔ)法分析樹(shù)的方式:訪問(wèn)者模式(vistor pattern)。
監(jiān)聽(tīng)器
為了將遍歷樹(shù)時(shí)觸發(fā)的事件轉(zhuǎn)化為監(jiān)聽(tīng)器的調(diào)用,ANTLR 運(yùn)行庫(kù)提供了 ParseTreeWalker 類。我們可以自行實(shí)現(xiàn) ParseTreeListener 接口,在其中填充自己的邏輯代碼(通常是調(diào)用程序的其他部分),從而構(gòu)建出我們自己的語(yǔ)言類應(yīng)用程序。ANTLR 為每個(gè)語(yǔ)法文件生成一個(gè) ParseTreeListener 的子類,在該類中,語(yǔ)法中的每條規(guī)則都有對(duì)應(yīng)的 enter 方法和 exit 方法。例如,當(dāng)遍歷器訪問(wèn)到 assign 規(guī)則對(duì)應(yīng)的節(jié)點(diǎn)時(shí),它就會(huì)調(diào)用 enterAssign() 方法,然后將對(duì)應(yīng)的語(yǔ)法分析樹(shù)節(jié)點(diǎn)——AssignContext 的實(shí)例——當(dāng)作參數(shù)傳遞給它。在遍歷器訪問(wèn)了 assign 節(jié)點(diǎn)的全部子節(jié)點(diǎn)之后,它會(huì)調(diào)用 exitAssign() 。下圖用粗虛線標(biāo)識(shí)了 ParseTreeWalker對(duì)語(yǔ)法分析樹(shù)進(jìn)行深度優(yōu)先遍歷的過(guò)程。

下圖顯示了在我們的賦值語(yǔ)句生成的語(yǔ)法分析樹(shù)中,ParseTreeWalker 對(duì)監(jiān)聽(tīng)器方法的完整的調(diào)用順序。

監(jiān)聽(tīng)器機(jī)制的優(yōu)秀之處在于,這一切都是自動(dòng)進(jìn)行的。我們不需要編寫對(duì)語(yǔ)法分析樹(shù)的遍歷代碼,也不需要讓我們的監(jiān)聽(tīng)器顯式地訪問(wèn)子節(jié)點(diǎn)。
訪問(wèn)器
有時(shí)候,我們希望控制遍歷語(yǔ)法分析樹(shù)的過(guò)程,通過(guò)顯式的方法調(diào)用來(lái)訪問(wèn)子節(jié)點(diǎn)。下圖是是使用常見(jiàn)的訪問(wèn)者模式對(duì)我們的語(yǔ)法分析樹(shù)進(jìn)行操作的過(guò)程。

其中,粗虛線顯示了對(duì)語(yǔ)法分析樹(shù)進(jìn)行深度優(yōu)先遍歷的過(guò)程。細(xì)虛線標(biāo)示出訪問(wèn)器方法的調(diào)用順序。我們可以在自己的程序代碼中實(shí)現(xiàn)這個(gè)訪問(wèn)器接口,然后調(diào)用visit() 方法來(lái)開(kāi)始對(duì)語(yǔ)法分析樹(shù)的一次遍歷。
ParseTree tree=...; // tree是語(yǔ)法分析得到的結(jié)果
MyVisitor v = new MyVisitor();
v.visit(tree);ANTLR 內(nèi)部為訪問(wèn)者模式提供的支持代碼會(huì)在根節(jié)點(diǎn)處調(diào)用 visitStat() 方法。接下來(lái),visitStat() 方法的實(shí)現(xiàn)將會(huì)調(diào)用 visit() 方法,并將所有子節(jié)點(diǎn)當(dāng)作參數(shù)傳遞給它,從而繼續(xù)遍歷的過(guò)程?;蛘?,visitMethod() 方法可以顯式調(diào)用 visitAssign() 方法等。ANTLR會(huì)提供訪問(wèn)器接口和一個(gè)默認(rèn)實(shí)現(xiàn)類,免去我們一切都要自行實(shí)現(xiàn)的麻煩。這樣,我們就可以專注于那些我們感興趣的方法,而無(wú)須覆蓋接口中的方法。
同時(shí)訪問(wèn)者機(jī)制支持泛型返回值,可以實(shí)現(xiàn)數(shù)據(jù)聚合。

訪問(wèn)器機(jī)制和監(jiān)聽(tīng)器機(jī)制的最大的區(qū)別在于,監(jiān)聽(tīng)器的方法會(huì)被 ANTLR 提供的遍歷器對(duì)象自動(dòng)調(diào)用,而在訪問(wèn)器的方法中,必須顯式調(diào)用 visit 方法來(lái)訪問(wèn)子節(jié)點(diǎn)。忘記調(diào)用visit() 的后果就是對(duì)應(yīng)的子樹(shù)將不會(huì)被訪問(wèn)。
語(yǔ)義判定
語(yǔ)義判定(Semantic Predicates)允許在語(yǔ)法規(guī)則中嵌入布爾表達(dá)式,從而在運(yùn)行時(shí)動(dòng)態(tài)控制解析過(guò)程。這使得 ANTLR4 能夠處理上下文相關(guān)的語(yǔ)法結(jié)構(gòu)。
基本語(yǔ)法:
ruleName
: {布爾表達(dá)式}? 規(guī)則元素 // 驗(yàn)證型判定
| {布爾表達(dá)式}?=> 規(guī)則元素 // 門控型判定
;判定類型
驗(yàn)證型判定
- 語(yǔ)法:
{布爾表達(dá)式}? - 行為:
嘗試匹配規(guī)則元素
如果匹配成功,評(píng)估布爾表達(dá)式
如果表達(dá)式為 false,放棄當(dāng)前分支并嘗試其他備選分支
expr
: {isType("int")}? ID // 只有當(dāng) isType("int") 為 true 時(shí)才匹配
| INT
;門控型判定
- 語(yǔ)法:
{布爾表達(dá)式}?=> - 行為:
在嘗試匹配規(guī)則元素前評(píng)估布爾表達(dá)式
如果表達(dá)式為 false,立即放棄整個(gè)分支
不會(huì)嘗試匹配規(guī)則元素
statement
: {inLoop()}?=> 'break'';' // 只有在循環(huán)中才允許 break
| 'continue'';'
;實(shí)現(xiàn)機(jī)制
在語(yǔ)法文件中聲明:
grammar ContextSensitive;
@parser::members {
private SymbolTable symbolTable = new SymbolTable();
privatebooleanisType(String id){
return symbolTable.isType(id);
}
}
expr
: {isType($ID.text)}? ID // 使用語(yǔ)義判定
| INT
;ANTLR 會(huì)將語(yǔ)義判定轉(zhuǎn)換為解析器代碼:
publicclassContextSensitiveParserextendsParser{
// ...
publicfinal ExprContext expr(){
// 嘗試第一個(gè)備選分支
if (isType(input.LT(1).getText())) {
// 創(chuàng)建上下文對(duì)象
// 匹配 ID
}
// 否則嘗試第二個(gè)分支
else {
// 匹配 INT
}
}
}Channel
在 ANTLR 4 中,通道(channels)是一種強(qiáng)大的機(jī)制,用于將詞法標(biāo)記(tokens)分類處理。ANTLR 4 有兩個(gè)預(yù)定義通道:
- 默認(rèn)通道 (Token.DEFAULT_CHANNEL),通道號(hào): 0,包含所有需要被解析器處理的標(biāo)記。
- 隱藏通道 (Token.HIDDEN_CHANNEL),通道號(hào): 1,包含所有不需要被解析器直接處理的標(biāo)記。
通道與 skip 的區(qū)別

自定義通道
// ===== 1. 聲明通道 =====
channels {
ERROR_CHANNEL, // 自定義錯(cuò)誤信息通道
HIDDEN_COMMENTS // 隱藏注釋通道
}
// ===== 2. 將詞法規(guī)則定向到通道 =====
ERROR_TOKEN : '<!' .*? '!>' -> channel(ERROR_CHANNEL); // 捕獲錯(cuò)誤標(biāo)記
LINE_COMMENT : '//' ~[\r\n]* -> channel(HIDDEN_COMMENTS); // 隱藏注釋
BLOCK_COMMENT : '/*' .*? '*/' -> channel(HIDDEN_COMMENTS);
// ===== 3. 保留傳統(tǒng)空白符處理 =====
WS : [ \t\r\n]+ -> skip; // 完全跳過(guò)空白符ANTLR 4 通過(guò) channels{} 聲明自定義通道,并用 -> channel(NAME) 將詞法規(guī)則輸出定向到指定通道,保留但隔離特殊內(nèi)容。
嵌入動(dòng)作
ANTLR 的嵌入動(dòng)作(Embedded Actions)是在語(yǔ)法規(guī)則中直接插入目標(biāo)語(yǔ)言代碼的機(jī)制,它允許開(kāi)發(fā)者在解析過(guò)程的關(guān)鍵節(jié)點(diǎn)執(zhí)行自定義邏輯。
語(yǔ)法規(guī)則 { 代碼塊 }ANTLR 在解析時(shí)會(huì)在對(duì)應(yīng)位置實(shí)時(shí)執(zhí)行這些代碼
執(zhí)行時(shí)機(jī)
- 元素匹配前:
{代碼} 規(guī)則元素 - 元素匹配后:
規(guī)則元素 {代碼} - 規(guī)則匹配完成:
規(guī)則元素 @after {代碼}
動(dòng)作類型與代碼示例
- 簡(jiǎn)單打印動(dòng)作(調(diào)試追蹤)
expression
: left=expression '+' { System.out.println("檢測(cè)到加號(hào)"); }
right=expression
{ System.out.println("完成加法: "+$left.value+"+"+$right.value); }
;輸出示例:
檢測(cè)到加號(hào)
完成加法: 5+3- 條件攔截動(dòng)作(語(yǔ)義檢查)
vectorOperation
: ID '=' (vec1=vector '×' vec2=vector
{
if($vec1.dimension != $vec2.dimension)
throw new RuntimeException("維度不匹配");
})
{ System.out.println("叉積運(yùn)算完成"); }
;- 動(dòng)態(tài)計(jì)算動(dòng)作(屬性傳遞)
number returns [int value]
: digits=INT { $value = Integer.parseInt($digits.text); }
| hex='0x' hexDigits=HEX
{ $value = Integer.parseInt($hexDigits.text,16); }
;- 集合構(gòu)造動(dòng)作(數(shù)據(jù)聚合)
jsonArray returns [List<Object> list = new ArrayList<>()]
: '['
(first=jsonValue { $list.add(first); }
(',' next=jsonValue { $list.add(next); })*
)? ']'
;- 符號(hào)表管理動(dòng)作(語(yǔ)義分析)
variableDecl
: type ID
{
Symbol sym = new Symbol($ID.text, $type.text);
currentScope.addSymbol(sym);
}
'=' expr ';'
;- 自動(dòng)代碼生成(DSL編譯)
sqlSelect
: 'SELECT' columns+=column (',' columns+=column)*
{ out.write("SELECT " + $columns.get(0).text);
for(int i=1; i<$columns.size(); i++) {
out.write("," + $columns.get(i).text);
}
}
'FROM' table=ID
{ out.write(" FROM " + $table.text); }
;注意:動(dòng)作會(huì)使語(yǔ)法與目標(biāo)語(yǔ)言耦合,優(yōu)先使用監(jiān)聽(tīng)器/訪問(wèn)器模式,避免過(guò)度使用。
處理優(yōu)先級(jí)、左遞歸和結(jié)合性
在自頂向下的語(yǔ)法和手工編寫的遞歸下降語(yǔ)法分析器中,處理表達(dá)式都是一件相當(dāng)棘手的事情,這首先是因?yàn)榇蠖鄶?shù)語(yǔ)法都存在歧義,其次是因?yàn)榇蠖鄶?shù)語(yǔ)言的規(guī)范使用了一種特殊的遞歸方式,稱為左遞歸(left recursion)。
自頂向下的語(yǔ)法和語(yǔ)法分析器的經(jīng)典形式無(wú)法處理左遞歸。為了闡明這個(gè)問(wèn)題,假設(shè)有一種簡(jiǎn)單的算術(shù)表達(dá)式語(yǔ)言,它包含乘法和加法運(yùn)算符,以及整數(shù)因子。表達(dá)式是自相似的,所以,很自然地,我們說(shuō),一個(gè)乘法表達(dá)式是由*連接的兩個(gè)子表達(dá)式,一個(gè)加法表達(dá)式是由+連接的兩個(gè)子表達(dá)式。另外單個(gè)整數(shù)也可以作為簡(jiǎn)單的表達(dá)式。這樣寫出的就是下列看上去非常合理的規(guī)則:

問(wèn)題在于,對(duì)于某些輸入文本而言,上面的規(guī)則存在歧義。換句話說(shuō),這條規(guī)則可以用不止一種方式匹配某種輸入的字符流,這個(gè)語(yǔ)法在簡(jiǎn)單的整數(shù)表達(dá)式和單運(yùn)算符表達(dá)式上工作得很好——例如1+2和1*2——是因?yàn)橹淮嬖谝环N方式去匹配它們。對(duì)于1+2,上述語(yǔ)法只能用第二個(gè)備選分支去匹配,如下圖左側(cè)的語(yǔ)法分析樹(shù)所示。

但是對(duì)于 1+2*3 這樣的輸入而言,上述規(guī)則能夠用兩種方式解釋它,如上圖中間和右側(cè)的語(yǔ)法分析樹(shù)所示。它們的差異在于,中間的語(yǔ)法分析樹(shù)表示將1加到2和3相乘的結(jié)果上去,而右側(cè)的語(yǔ)法分析樹(shù)表示將1和2相加的結(jié)果與3相乘。這就是運(yùn)算符優(yōu)先級(jí)帶來(lái)的問(wèn)題,傳統(tǒng)的語(yǔ)法無(wú)法指定優(yōu)先級(jí)。大多數(shù)語(yǔ)法工具,例如Bison,使用額外的標(biāo)記來(lái)指定運(yùn)算符優(yōu)先級(jí)。
與之不同的是,ANTLR 通過(guò)優(yōu)先選擇位置靠前的備選分支來(lái)解決歧義問(wèn)題,這隱式地允許我們指定運(yùn)算符優(yōu)先級(jí)。例如,expr 規(guī)則中,乘法規(guī)則在加法規(guī)則之前,所以ANTLR在解決歧義問(wèn)題時(shí)會(huì)優(yōu)先處理乘法。默認(rèn)情況下,ANTLR按照我們通常對(duì)*和+的理解,將運(yùn)算符從左向右地進(jìn)行結(jié)合。盡管如此,一些運(yùn)算符——例指數(shù)運(yùn)算符——是從右向左結(jié)合的,所以我們需要在這樣的運(yùn)算符上使用 assoc 選項(xiàng)手工指定結(jié)合性。這樣,輸入的 2^3^4 就能夠被正確解釋為2^(3^4):

注:在ANTLR 4.2之后,<assoc=right> 需要被放到備選分支的最左側(cè),否則會(huì)收到警告。在本例中,正確寫法是:

如下圖所示的語(yǔ)法分析樹(shù)展示了^符號(hào)的左結(jié)合版本和右結(jié)合版本在處理相同輸入時(shí)的差異。通常人們采用右側(cè)語(yǔ)法分析樹(shù)所代表的解釋方式,不過(guò),語(yǔ)言設(shè)計(jì)者可以自由地決定使用哪一種結(jié)合性。

若要將上述三種運(yùn)算符組合成為同一條規(guī)則,我們就必須把^放在最前面,因?yàn)樗膬?yōu)先級(jí)比*和+都要高(1+2^3的結(jié)果是9)。

ANTLR 4的一項(xiàng)重大改進(jìn)就是,它已經(jīng)可以處理直接左遞歸了。左遞歸規(guī)則是這樣的一種規(guī)則:在某個(gè)備選分支的最左側(cè)以直接或者間接方式調(diào)用了自身。上面的例子中的expr規(guī)則是直接左遞歸的,因?yàn)槌齀NT之外的所有備選分支都以expr規(guī)則本身開(kāi)頭(它同時(shí)也是右遞歸(rightrecursive)的,因?yàn)樗哪承﹤溥x分支在最右側(cè)引用了expr)。雖然ANTLR 4已經(jīng)能夠處理直接左遞歸,但是它還無(wú)法處理間接左遞歸。這意味著我們無(wú)法將expr規(guī)則分解為下列規(guī)則,盡管它們?cè)谡Z(yǔ)義上等價(jià):

非貪婪匹配
在 ANTLR 中,非貪婪匹配(Non-Greedy Matching) 是處理文本模式的特殊策略,它會(huì)盡可能少地匹配字符(即采用"最小匹配"原則)。這與默認(rèn)的貪婪匹配(盡可能多匹配)形成對(duì)比,是解決詞法歧義的關(guān)鍵技術(shù)。
貪婪匹配(默認(rèn)行為)
STRING : '"' .* '"'; // 匹配從第一個(gè)"到最后一個(gè)"非貪婪匹配
STRING_LAZY : '"' .*? '"'; // ? 啟用非貪婪通配符模式說(shuō)明:
模式 | 符號(hào) | 匹配策略 |
貪婪 |
| 最長(zhǎng)可能匹配 |
非貪婪 |
| 最短可能匹配 |
實(shí)戰(zhàn)應(yīng)用場(chǎng)景
- 場(chǎng)景1:注釋匹配
// 錯(cuò)誤:貪婪匹配會(huì)吃光所有內(nèi)容
DOC_COMMENT : '/*' .* '*/';
// 正確:非貪婪只匹配最近的一對(duì)
DOC_COMMENT_LAZY : '/*' .*? '*/';- 場(chǎng)景2:模板字符串
TEMPLATE : '`' ('\\`' | .)*? '`';正確處理帶轉(zhuǎn)義符的模板:
- 場(chǎng)景3:XML標(biāo)簽內(nèi)聯(lián)
TAG_CONTENT : '<' .*? '>';輔助類
ParseTreeProperty
ParseTreeProperty 是 ANTLR 4 中一個(gè)強(qiáng)大的輔助類,用于將自定義數(shù)據(jù)與解析樹(shù)(Parse Tree)中的節(jié)點(diǎn)關(guān)聯(lián)起來(lái)。它是實(shí)現(xiàn)屬性文法(Attribute Grammar)的核心工具,特別適用于需要在語(yǔ)法分析過(guò)程中計(jì)算和傳遞屬性的場(chǎng)景。
ParseTreeProperty 主要用于解決以下問(wèn)題:
- 存儲(chǔ)節(jié)點(diǎn)相關(guān)數(shù)據(jù):為每個(gè)解析樹(shù)節(jié)點(diǎn)關(guān)聯(lián)自定義屬性
- 實(shí)現(xiàn)屬性傳遞:在樹(shù)遍歷過(guò)程中收集和傳遞上下文信息
- 實(shí)現(xiàn)代碼生成:保存每個(gè)節(jié)點(diǎn)的代碼生成結(jié)果
- 類型檢查:記錄表達(dá)式的類型信息
- 符號(hào)表關(guān)聯(lián):將作用域和符號(hào)表與語(yǔ)法結(jié)構(gòu)關(guān)聯(lián)
/ 1. 創(chuàng)建數(shù)據(jù)容器
ParseTreeProperty<DataType> dataMap = new ParseTreeProperty<>();
// 2. 向節(jié)點(diǎn)注入數(shù)據(jù)(通常在監(jiān)聽(tīng)器/訪問(wèn)器中)
@Override
publicvoidexitAddExpr(CalcParser.AddExprContext ctx){
int left = dataMap.get(ctx.left); // 取左子樹(shù)數(shù)據(jù)
int right = dataMap.get(ctx.right);
int result = left + right;
dataMap.put(ctx, result); // 當(dāng)前節(jié)點(diǎn)存儲(chǔ)計(jì)算結(jié)果
}
// 3. 從根節(jié)點(diǎn)獲取最終結(jié)果
publicintgetResult(ParseTree tree){
return dataMap.get(tree); // 返回根節(jié)點(diǎn)存儲(chǔ)的計(jì)算結(jié)果
}TokenStreamRewriter
TokenStreamRewriter 是 ANTLR4 中一個(gè)強(qiáng)大的工具類,用于在不修改原始令牌流的情況下,對(duì)令牌流進(jìn)行非破壞性編輯。它特別適用于源代碼轉(zhuǎn)換、重構(gòu)和代碼生成等場(chǎng)景。
其中的關(guān)鍵之處在于,TokenStreamRewriter 對(duì)象實(shí)際上修改的是詞法符號(hào)流的“視圖”而非詞法符號(hào)流本身。它認(rèn)為所有對(duì)修改方法的調(diào)用都只是一個(gè)“指令”,然后將這些修改放入一個(gè)隊(duì)列;在未來(lái)詞法符號(hào)流被重新渲染為文本時(shí),這些修改才會(huì)被執(zhí)行。在每次我們調(diào)用 getText() 的時(shí)候,rewriter 對(duì)象都會(huì)執(zhí)行上述隊(duì)列中的指令。
簡(jiǎn)單使用示例:在方法調(diào)用前插入日志
publicclassRewriterExample{
publicstaticvoidmain(String[] args){
// 1. 創(chuàng)建輸入流
String input = "public class Test {\n" +
" public void method() {\n" +
" System.out.println(\"Hello\");\n" +
" }\n" +
"}";
CharStream charStream = CharStreams.fromString(input);
// 2. 創(chuàng)建詞法分析器和令牌流
JavaLexer lexer = new JavaLexer(charStream);
CommonTokenStream tokens = new CommonTokenStream(lexer);
// 3. 創(chuàng)建重寫器
TokenStreamRewriter rewriter = new TokenStreamRewriter(tokens);
// 4. 創(chuàng)建解析器
JavaParser parser = new JavaParser(tokens);
ParseTree tree = parser.compilationUnit();
// 5. 遍歷解析樹(shù)并修改
ParseTreeWalker walker = new ParseTreeWalker();
walker.walk(new InsertLogListener(rewriter), tree);
// 6. 獲取修改后的文本
System.out.println(rewriter.getText());
}
staticclassInsertLogListenerextendsJavaBaseListener{
privatefinal TokenStreamRewriter rewriter;
publicInsertLogListener(TokenStreamRewriter rewriter){
this.rewriter = rewriter;
}
@Override
publicvoidenterMethodCall(JavaParser.MethodCallContext ctx){
// 獲取方法名令牌
Token methodNameToken = ctx.Identifier().getSymbol();
// 在方法調(diào)用前插入日志語(yǔ)句
String logStmt = "\n System.out.println(\"Calling method: " +
methodNameToken.getText() + "\");";
rewriter.insertBefore(methodNameToken.getTokenIndex(), logStmt);
}
}
}輸出結(jié)果:
publicclassTest{
publicvoidmethod(){
System.out.println("Calling method: println");
System.out.println("Hello");
}
}錯(cuò)誤報(bào)告與恢復(fù)
ANTLR 的錯(cuò)誤報(bào)告與恢復(fù)機(jī)制是其生成健壯解析器的核心,它通過(guò)智能的錯(cuò)誤檢測(cè)、精確報(bào)告及自動(dòng)恢復(fù)策略,確保即使面對(duì)非法輸入也能進(jìn)行結(jié)構(gòu)化處理而非直接崩潰。
對(duì)于詞法錯(cuò)誤和語(yǔ)法錯(cuò)誤,ANTLR 4 會(huì)定位錯(cuò)誤的起始位置,向后刪除字符直到發(fā)現(xiàn)合法的 token 邊界,然后就會(huì)接著解析后續(xù)輸入。
// 自動(dòng)生成詳細(xì)的錯(cuò)誤診斷
line 5:8 missing '}' at '{'
line 10:22 mismatched input ';' expecting ','- 信息結(jié)構(gòu):
位置: 行號(hào):列號(hào)
類型: [missing|mismatched|extraneous]
詳情: 期望內(nèi)容/實(shí)際內(nèi)容自定義錯(cuò)誤處理器
重寫 BaseErrorListener:
publicclassVerboseListenerextendsBaseErrorListener{
@Override
publicvoidsyntaxError(Recognizer<?,?> recognizer,
Object offendingSymbol,
int line, int charPos,
String msg, RecognitionException e){
// 生成更友好的錯(cuò)誤提示
String error = String.format("[CUSTOM] Line %d:%d - %s", line, charPos, msg);
System.err.println(error);
}
}
// 注冊(cè)自定義監(jiān)聽(tīng)器
parser.removeErrorListeners();
parser.addErrorListener(new VerboseListener());性能優(yōu)化
提高語(yǔ)法分析器的速度
ANTLR 4 的自適應(yīng)語(yǔ)法分析策略功能比 ANTLR 3 更加強(qiáng)大,不過(guò)這是以少量的性能損失為代價(jià)的。如果你需要盡可能快的速度和盡可能少的內(nèi)存占用,你可以使用兩步語(yǔ)法分析策略。第一步使用功能稍弱的語(yǔ)法分析策略——SLL——在大多數(shù)情況下它已經(jīng)足夠了(它和ANTLR 3的策略相似,只是不需要回溯)。如果第一步的語(yǔ)法分析失敗,那么就必須使用全功能的 LL 語(yǔ)法分析。這是因?yàn)?,在第一步失敗后,我們無(wú)法知道原因究竟是真正的語(yǔ)法錯(cuò)誤,還是 SLL 的功能不夠強(qiáng)大
由于能夠通過(guò) SLL 的輸入一定能夠通過(guò)全功能的 LL,所以一旦第一步成功,就無(wú)須使用更昂貴的策略。
try {
parser.compilationUnit();
//如果抵達(dá)此處,證明沒(méi)有語(yǔ)法錯(cuò)誤,SLL(*)就夠了
//無(wú)需使用全功能的LL(*)
} catch (RuntimeException ex) {
if (ex.getClass() == RuntimeException.class &&
ex.getCause() instanceofRecognitionException) {
//BailErrorStrategy會(huì)將RecognitionExceptions封裝在
// RuntimeException中,所以這里需要檢查是不是
//一個(gè)真正的RecognitionException
tokenStream.reset();//回滾輸入流
//重新使用標(biāo)準(zhǔn)的錯(cuò)誤監(jiān)聽(tīng)器和錯(cuò)誤處理器
parser.addErrorListener(ConsoleErrorListener.INSTANCE);
parser.setErrorHandler(new DefaultErrorStrategy());
parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
parser.compilationUnit();
parser.addErrorListener(new SyntaxErrorListener());
ParseTree tree = parser.compilationUnit();
// 使用訪問(wèn)器轉(zhuǎn)換DSL
Map<String, Object> externalVarMaps = new HashMap<>();
externalVarMaps.put("features", Sets.newHashSet("test_tz_string_auto_test", "test_feature_999", "sys_attr5"));
ParentVisitor visitor = new ParentVisitor(123L, tokenStream, parser, externalVarMaps);
String dsl = visitor.visit(tree);
log.info("Generated DSL:\n{}", dsl);
}
}如果第二步失敗,那就意味著一個(gè)真正的語(yǔ)法錯(cuò)誤。
無(wú)緩沖的字符流和詞法符號(hào)流
因?yàn)?ANTLR 的識(shí)別器在默認(rèn)情況下會(huì)將輸入的完整字符流和全部詞法符號(hào)放入緩沖區(qū),所以它無(wú)法處理大小超過(guò)內(nèi)存的文件,也無(wú)法處理類似套接字(socket)連接之類的無(wú)限輸入流。為解決此問(wèn)題,你可以使用字符流和詞法符號(hào)流的無(wú)緩沖版本:UnbufferedCharStream 和 UnbufferedTokenStream,它們使用一個(gè)滑動(dòng)窗口來(lái)處理流。
為展示二者的實(shí)際應(yīng)用,下圖是一個(gè) CSV語(yǔ)法,它計(jì)算一個(gè)文件中兩列浮點(diǎn)數(shù)的和:

如果你需要的只是每一列的和,你就應(yīng)該在內(nèi)存中只保留一個(gè)或兩個(gè)詞法符號(hào)用于記錄結(jié)果。欲關(guān)閉 ANTLR 的緩沖功能,需要完成三件事情。首先,使用無(wú)緩沖的流代替常見(jiàn)的 ANTLFileStream 和 CommonTokenStream。其次,傳給詞法分析器一個(gè)詞法符號(hào)工廠,將輸入流中的字符拷貝到生成的詞法符號(hào)中去。否則,詞法符號(hào)的 getTex() 方法就會(huì)嘗試訪問(wèn)可能已經(jīng)不再可用的字符流。最后,阻止語(yǔ)法分析器建立語(yǔ)法分析樹(shù)。如下圖標(biāo)記的關(guān)鍵代碼:

當(dāng)效率是首要目標(biāo)時(shí),無(wú)緩沖流是非常有用的。使用它們的缺點(diǎn)是你需要手工處理與緩沖區(qū)相關(guān)的事情。例如,你不能在規(guī)則的內(nèi)嵌動(dòng)作中使用 $text,因?yàn)樗鼈兪菑妮斎肓髦蝎@取文本的。
結(jié)尾
這篇關(guān)于 ANTLR 的技術(shù)指南到此告一段落。作為領(lǐng)域特定語(yǔ)言(DSL)構(gòu)建的利器,ANTLR 通過(guò)其強(qiáng)大的語(yǔ)法解析能力、靈活的監(jiān)聽(tīng)器/訪問(wèn)器機(jī)制,以及高效的錯(cuò)誤恢復(fù)策略,徹底革新了語(yǔ)言處理技術(shù)的開(kāi)發(fā)范式。
無(wú)論是設(shè)計(jì)數(shù)據(jù)庫(kù)查詢語(yǔ)言、配置文件解析器,還是實(shí)現(xiàn)復(fù)雜的領(lǐng)域?qū)S眠壿?,ANTLR 都提供了從詞法分析到語(yǔ)法樹(shù)遍歷的全套解決方案。其自動(dòng)生成的解析器代碼和直觀的規(guī)則定義方式,讓開(kāi)發(fā)者能專注于業(yè)務(wù)邏輯而非底層細(xì)節(jié),真正實(shí)現(xiàn)了"用語(yǔ)法驅(qū)動(dòng)開(kāi)發(fā)"的高效實(shí)踐。通過(guò)掌握 ANTLR,你已擁有了一把打開(kāi)自定義語(yǔ)言世界的鑰匙。

































