Antlr4入門(二)基本概念解析

在上一章中(Antlr4入門(一)IDEA中Antlr的安裝與使用),我們安裝了Antlr,並編寫運行了第一個程序“Hello world”。而在本章中,我們將學習語言類應用程序相關的重要過程、術語和數據結構。

一. 術語

語言(language)是由一系列有意義的語句組成,語句(sentence)由詞組組成,詞組(phrase)是由更小的子詞組(subphrase)和詞彙符號(vocabulary symbols)組成。

舉個例子,英語就是一種語言,“Keep on going never give up.”這句話就是語句,語句是由詞組(keep on、give up等)組成,而詞組又是由詞彙符號(即單詞)組成。

解釋器(interpreter)是指能夠分析計算或者“執行”語句的程序,比如計算器、讀取配置文件的程序、python解釋器等。

翻譯器(translator)是指能夠將一門語言的語句轉換成另外一門語言的語句的程序,比如普通的編譯器和Java到C#的轉換器。

現在,想象下我們的大腦是怎麼樣分析理解 “Keep on going never give up.” 這句話的,答對了,我們並不是一個字符一個字符的去閱讀一個句子,而是將句子看作是一系列的單詞的集合,我們在識別整個句子的語法結構之前,大腦會先將字符聚集成單詞,然後獲取每個單詞的意義。

將字符聚集成單詞或者詞彙符號(token)的過程稱爲詞法分析(lexical analysis)或者詞法符號化(tokenizing)。把可以將輸入文本轉換成詞法符號(token)的程序稱爲詞法分析器(lexer)。詞法符號包含至少兩部分信息:詞法符號的類型(從而能夠通過類型來識別詞法結構)和該詞法符號對應的文本。

當我們將字符聚集成單詞後,大腦就會根據語法規則(比如主謂賓,時態等)去分析語句。

語法(grammar)是一系列規則的集合,每條規則表述出一種詞彙結構。而句法(syntax)是指約束語言中的各個組成部分之間關係的規則。能夠識別語言的程序稱爲語法分析器(parser)或者句法分析器(syntax analysis)。而antlr在進行語法分析的時候,還會生成一種名爲語法分析樹(parse tree)或者句法樹(syntax tree)的數據結構,用來記錄語法分析器識別出輸入語句結構的過程,以及該結構的各組成部分。

二、Antlr語法識別過程

下面我們將通過編寫一個簡單 “賦值語句” 語法來深入瞭解Antlr是如何進行語法分析的。

1. 按照上一章內容新建一個Maven項目。如下是我們 “賦值語句” 的語法文件

// 語法文件通常以grammar關鍵字開頭,並且與文件名同名
grammar Assign;
stat : assign;
// 一條名爲assign的規則
assign : ID '=' expr ';' ;
// 語法分析器的規則必須以小寫字母開頭
// 詞法分析器的規則必須以大寫字母開頭
ID : [a-z]+ ;
expr : NUMBER ;
NUMBER : [1-9][0-9]*|[0]|([0-9]+[.][0-9]+) ;
WS : [ \t\r\n]+ -> skip ;

現在,讓我們使用Test Rule來測試 賦值語句 語法

可以看到該賦值語句語法成功生成了一棵語法分析樹。下面,讓我們使用antlr工具來生成詞法分析器和語法分析器等代碼。

下面簡單介紹下生成的文件:

  1. AssignParser.java:該文件包含一個語法分析器類的定義,在該類中,每條規則都有對應的方法,除此之外,還有一些輔助方法
  2. AssignLexer.java:該文件包含一個詞法分析器的定義,作用是將輸入的字符序列聚集成詞彙符號。
  3. Assign.tokens:ANTLR會給每個我們定義的詞法符號指定一個數字形式的類型,然後將他們的對應關係存儲於該文件中。通過它,ANTLR可以在多個小型語法間同步全部的詞法符號類型。
  4. AssignListener.java,AssignBaseListener.java:默認情況下,ANTLR生成的語法分析器能將輸入文本轉換成一棵語法分析樹。在遍歷語法分析樹時,遍歷器能夠觸發一系列“事件”(回調),並通知我們提供的監聽器對象。AssignListener是個接口,存放了這些回調方法的定義,我們可以通過實現它來完成自定義的功能。而AssignBaseListener是該接口默認的實現類,爲其中的每個方法提供了空實現。
  5. AssignVisitor.java,AssignBaseVisitor.java:ANTLR提供了兩種遍歷樹的機制,一種是4中所提到的監聽器模式,另外一種就是visitor訪問者模式。

接着,我們來編寫main函數,將生成的語法分析器與Java程序進行集成。

import assign.AssignLexer;
import assign.AssignParser;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;

public class AssignMain {
    public static void run(String expr) throws Exception{
            // 新建一個CharStream,讀取數據
        ANTLRInputStream input = new ANTLRInputStream(expr);
        // 新建一個詞法分析器,處理輸入的字符流CharStream
        AssignLexer lexer = new AssignLexer(input);
        // 新建一個詞法符號的緩衝區,用於存儲詞法分析器生成的詞法符號(Token)
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        // 新建一個語法分析器,用於分析詞法符號緩衝區中的詞法符號
        AssignParser parser = new AssignParser(tokens);
        // 對assign規則進行語法分析,生成語法分析樹
        ParseTree tree = parser.assign();
        // 使用LISP風格打印生成的樹
        System.out.println(tree.toStringTree(parser));
    }

    public static void main(String[] args) throws Exception{
        run("sp = 100;");
    }
}

這裏可以看到,詞法分析器(Lexer)會處理字符流(Charstream)並將生成的詞法符號(token)提供給語法分析器,語法分析器(parser)隨即根據這些信息來檢查語法的正確性並建造出一棵語法分析樹(parse tree)。而連接詞法分析器和語法分析器的“管道”就是TokenStream。如下圖所示。

上圖中,語法分析樹(parse tree)中的RuleNodeTerminalNode分別代表子樹的根節點和葉子節點。可以看到,語法分析樹的葉子節點永遠是輸入的詞法符號,而子樹根節點因爲包含了使用規則識別詞組過程中的全部信息,它們又被稱爲上下文(context)對象

三、備選分支與歧義性文法

我們的賦值語句語法僅包含一種情況,即 ‘=’ 進行賦值,下面我們將學習通過備選分支來將 ‘:=’ 也表示爲賦值符號。

修改assign.g4語法規則文件

grammar Assign;
stat : assign;
assign : ID '=' expr ';'
 | ID ':=' expr ';' ;
ID : [a-z]+ ;
expr : NUMBER ;
NUMBER : [1-9][0-9]*|[0]|([0-9]+[.][0-9]+) ;
WS : [ \t\r\n]+ -> skip ;

‘|’ 符號是備選分支的分隔符(有關語法規則的更多寫法見下一章內容),備選分支(alternative)是指規則的右側定義的多個方案之一。現在,讓我們重新生成代碼,然後查看AssignParser的assign方法:

public final AssignContext assign() throws RecognitionException {
		AssignContext _localctx = new AssignContext(_ctx, getState());
		enterRule(_localctx, 2, RULE_assign);
		try {
			setState(18);
			_errHandler.sync(this);
			switch ( getInterpreter().adaptivePredict(_input,0,_ctx) ) {
			case 1:
				enterOuterAlt(_localctx, 1);
				{
				setState(8);
				match(ID);
				setState(9);
				match(T__0);
				setState(10);
				expr();
				setState(11);
				match(T__1);
				}
				break;
			case 2:
				enterOuterAlt(_localctx, 2);
				{
				setState(13);
				match(ID);
				setState(14);
				match(T__2);
				setState(15);
				expr();
				setState(16);
				match(T__1);
				}
				break;
			}
		}
		catch (RecognitionException re) {
			_localctx.exception = re;
			_errHandler.reportError(this, re);
			_errHandler.recover(this, re);
		}
		finally {
			exitRule();
		}
		return _localctx;
	}

可以看到,對assign規則的解析是通過switch語句。assign方法必須通過檢查下一個詞法符號來做出語法分析決策(parsing decision)或者預測(prediction)。做出決策的過程實際上就是判斷哪一個備選分支是正確的。而前瞻詞法符號(lookahead token)是指任何一個在被匹配和消費之前就由語法分析器嗅探出的詞法符號,其實也就是下一個輸入的詞法符號。

有時候,我們輸入的語句可能匹配到多個備選分支,這就是歧義性語句或者二義性文法。換句話說,就是歧義性語句中的單詞序列能夠匹配多種語法結構。最簡單的歧義性語句就是把同一條規則寫兩遍。

大多數情況下,歧義的表現更爲微妙。在下面的語法中,stat規則包含兩個備選分支,兩者都可以匹配一個函數調用語句。

下圖展示了stat規則對於輸入“f();”的兩種解析:

如果語法分析器檢測到該詞組存在歧義,那麼antlr就必須在多個備選分支中做出選擇。而antlr解決歧義性的方法是:選擇所有匹配的備選分支中的第一條(switch case 到就會break出去)

後記

在下一篇文章中,我們將編寫一個簡單的計算器,並學習Antlr相關的語法規則和如何遍歷語法分析樹,從而實現加減乘除功能。

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