【ANTLR學習筆記】3:數組初始化列表的解析和翻譯

這節跟着書上第三章學習解析例如{val,val,{val,val},val}這樣的數組初始化列表,其中每個val都是一個無符號整數。它可以用來將Java中

static short[] data = {1,2,3};

轉化成等價的Unicode字符串形式:

static String data = "\u0001\u0002\u0003";

因爲Java中的char就是unsigned short,可以用Unicode字符(四個16進製表示的16位字符)來表達。

這樣轉換的目的是使數組初始化不受編譯出的class文件的限制。因爲class文件會將數組初始化顯式存儲爲賦值:

data[0] = 1;
data[1] = 2;
data[2] = 3;

而JVM規範規定,包括cinit方法在內,一個方法的字節碼長度不能超過65535字節。這樣如果需要初始化的數組很長就會超過這個限制。

但是如果使用轉換後的字符串就沒有這個限制,因爲字符串常量存儲在class文件的常量池中,沒有長度限制。

1 從語法規則文件生成程序

這裏需要注意語法規則必須小寫字母開頭,一般直接用全小寫。而詞法符號規則必須大寫字母開頭,一般直接用全大寫

這裏還有一個技巧,因爲生成的程序類名總是這個語法名+後綴拼接的方式,爲了符合Java類命名規範,這裏的語法名(和對應文件名) 也使用大寫字母開頭的駝峯式命名

// 和文件名相同的語法名
grammar ArrayInit;

// ----------語法規則的定義(必須小寫字母開頭)----------

// 頂層規則
init:
    '{' value (',' value)* '}' // 花括號內若干value用逗號隔開
    ;

// value的定義
value:
    init    // 可以是嵌套的花括號
    | INT   // 也可以是單獨一個INT詞法符號
    ;

// ----------詞法符號的定義(必須大寫字母開頭)----------

// 詞法符號INT的定義
INT : [0-9]+; // 一至多個數字
// 空白符號定義
WS  : [ \t\r\n]+ -> skip; // 匹配到時將其丟棄

然後用筆記1中的插件方法自動生成程序,暫時不勾選生成visitor,書上是直接用命令行(antlr4 xxx.g4)。

如果生成的程序中@Override下面有紅線報錯,是IDEA的項目Language level設置得太低,在模塊上F4打開設置,然後將Language level設置爲6就可以了。

2 生成的程序文件概覽

2.1 ArrayInitParser.java

這個文件中存放語法分析器類:

public class ArrayInitParser extends Parser { ... }

用來解析ArrayInit這個語法文件中的語法規則,每條語法規則都對應其中的一個內部類(對應筆記2中學習的語法樹上的非葉子結點)和一個訪問方法,即:

public static class InitContext extends ParserRuleContext { ... }
public final InitContext init() throws RecognitionException { ... }

public static class ValueContext extends ParserRuleContext { ... }
public final ValueContext value() throws RecognitionException { ... }

另外還有一些輔助代碼。

2.2 ArrayInitLexer.java

這個文件中存放詞法分析器類:

public class ArrayInitLexer extends Lexer { ... }

它是通過分析.g4文件中的詞法規則INTWS)以及語法規則中的字面值{},)生成的,用於將輸入的字符序列分解成詞法符號。

2.3 ArrayInit.tokens

這個文件用於存儲詞法符號到一個數字值的對應關係,每個詞法符號都會對應一個數字值,使用它可以在多個小型語法之間同步全部的詞法符號類型,以將一個大型語法切分成多個小型語法。

2.4 ArrayInitListener.java

該文件用於存儲監聽器接口:

public interface ArrayInitListener extends ParseTreeListener { ... }

在遍歷語法分析樹時,遍歷到指定的結點會觸發一系列事件回調,該接口即存放這些回調方法的定義。具體地,對於這個ArrayInit語法的語法樹而言,這包括:

InitContext結點的進入和退出:

void enterInit(ArrayInitParser.InitContext ctx);
void exitInit(ArrayInitParser.InitContext ctx);

ValueContext結點的進入和退出:

void enterValue(ArrayInitParser.ValueContext ctx);
void exitValue(ArrayInitParser.ValueContext ctx);

進入/退出哪個結點,就會調用上面相應的方法,並且把那個結點的上下文對象作爲參數傳進去。

2.5 ArrayInitBaseListener.java

該文件用於存儲監聽器接口的默認實現類:

public class ArrayInitBaseListener implements ArrayInitListener { ... }

也就是ANTLR提供的ArrayInitListener接口的默認實現,可以看到裏面的回調方法都是空的,只需要按照需求覆蓋感興趣的回調方法,就能用監聽器的方式來訪問語法分析樹了。

3 構建從數組到字符串的翻譯程序

這裏通過繼承前面生成的監聽器類ArrayInitBaseListener,在語法規則回調時翻譯對應的部分,然後直接打印出來:
在這裏插入圖片描述
實現監聽器:

import antlr.ArrayInitBaseListener;
import antlr.ArrayInitParser;

// 繼承監聽器類,覆蓋回調函數
public class ShortToUnicodeString extends ArrayInitBaseListener {
    // 在init結點開始遍歷之前,把'{'翻譯的'"'輸出
    @Override
    public void enterInit(ArrayInitParser.InitContext ctx) {
        System.out.print('"');
    }

    // 在init結點結束遍歷之後,把'}'翻譯的'"'輸出
    @Override
    public void exitInit(ArrayInitParser.InitContext ctx) {
        System.out.print('"');
    }

    // 在value結點開始遍歷之前(之後也可以),把數字翻譯出的Unicode輸出
    @Override
    public void enterValue(ArrayInitParser.ValueContext ctx) {
        // 假設不存在嵌套,value只走INT的分支
        // 這裏ctx.INT().getText()從上下文對象中獲取這個結點的文本
        int value = Integer.parseInt(ctx.INT().getText());
        // 輸出Unicode(數字的4位16進製表示)
        System.out.printf("\\u%04x", value);
    }
}

在主類調用:

import antlr.ArrayInitLexer;
import antlr.ArrayInitParser;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTreeWalker;

public class Main {
    public static void main(String[] args) {
        // 詞法分析、語法分析並得到語法樹(的根節點上下文對象)
        ArrayInitLexer lexer = new ArrayInitLexer(CharStreams.fromString("{1,2,3,4}"));
        ArrayInitParser parser = new ArrayInitParser(new CommonTokenStream(lexer));
        ArrayInitParser.InitContext tree = parser.init();
        System.out.println(tree.toStringTree(parser));
        // 創建一個能觸發回調函數的語法分析樹遍歷器
        ParseTreeWalker parseTreeWalker = new ParseTreeWalker();
        // 將監聽器和語法樹傳入walk方法,該方法會遍歷語法樹觸發回調
        parseTreeWalker.walk(new ShortToUnicodeString(), tree);
    }
}

運行結果:

(init { (value 1) , (value 2) , (value 3) , (value 4) })
"\u0001\u0002\u0003\u0004"

這樣就完成了最初設想的,從一維數組到Unicode字符串的翻譯器。最後記錄一個坑點,如果在Java程序的註釋裏面打\u字樣就會在build的時候報錯"非法的Unicode轉義"。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章