Antlr4入門(三)如何編寫語法文件

本章我們將會學習詞法及語法規則,以及四種抽象的計算機語言模式。因爲ANTLR的語法規則跟正則表達式是很類似的,所以還是推薦先閱讀下正則表達式的相關內容,這樣在編寫語法文件時可以事半功倍。

一、四種語言模式

雖然在過去的50年裏人們發明了許許多多的編程語言,但是,相對而言,基本的語言模式種類並不多。之所以如此,是因爲人們在設計編程語言的時候,傾向於將它們設計成與腦海中的自然語言相類似。我們希望符號按照有效的順序排列,並且符號之間擁有着特定的依賴關係。舉個例子,{(}) 就是不符合語法的,因爲符號的順序不對。

單詞之間的順序和依賴性約束是來自於自然語言的,基本上可以總結成四種抽象的計算機語言模式。

  1. 序列(sequence):一列元素,比如一行命令
  2. 選擇(choice):在多種可選方案中做選擇(備選分支),比如 if else
  3. 詞法符號依賴(token dependency):符號總是成對出現,比如左右括號()
  4. 嵌套關係(nested phrase):嵌套的詞組是一種自相似的語言結構,即它的子詞組也遵循相同的結構。即遞歸調用本身定義的語法規則,這就是遞歸規則(自引用規則)。遞歸規則包括直接遞歸(directly recursive)和間接遞歸(indirectly recursive)。

二、通配符(更多見正則表達式

常用的通配符如下所示:

  1. | 表示或(備選分支)
  2. * 表示出現0次或以上
  3. ? 表示出現0次或1次
  4. + 表示出現1次或以上
  5. ~ 表示取反
  6. 範圍運算符:.. 或者 -,比如小寫字母的表示:'a'..'z' 或者 [a-z]

下面通過識別一些常見的詞法符號來學習下通配符的用法:

1. 關鍵字、運算符和標點符號:對於關鍵字、運算符和標點符號,我們無須聲明詞法規則,只需在語法規則中直接使用單引號將他們括起來即可,比如 'while'、'+'。

2. 標識符:一個基本的標識符就是一個由大小寫字母組成的字符序列。需要注意的是,下面的ID規則也能夠匹配關鍵字(比如‘while’)等,上章中我們查看了Parser代碼,知道ANTLR是如何處理這種歧義性的——選擇所有匹配的備選分支中的第一條。因此,ID標識符應該放在關鍵字等定義之後。

// 匹配一個或者多個大小寫字母
ID : [a-zA-Z]+;

3. 整數:整數是包括正數和負數的不以零開頭的數字。

// 匹配一個整數
INTEGER : '-'?[1-9][0-9]*
        | '0'
        ;

4. 浮點數:一個浮點數以一列數字爲開頭,後面跟着一個小數點,然後是可選的小數部分。浮點數的另外一個格式是,以小數點開頭,後面是一串數字。基於以上定義,我們可以得到以下詞法規則

FLOAT : DIGIT+ '.' DIGIT*    // 1.39、3.14159等
      | '.' DIGIT+           // .12 (表示0.12)
      ;

fragment DIGIT : [0-9];    // 匹配單個數字

這裏我們使用了一條輔助規則DIGIT,將一條規則聲明爲fragment可以告訴ANTLR,該規則本身不是一個詞法符號,它只會被其他的詞法規則使用。這意味着在語法規則中不能引用它。這也是一條片段規則(fragment rule)。

5. 字符串常量:一個字符串就是兩個雙引號之間的任意字符序列。

// 匹配"……"之間的任意文本
STRING : '"' .*? '"';

點號通配符(.)匹配任意的單個字符,.* 表示匹配零個或多個字符組成的任意字符序列。顯然,這是個貪婪匹配,它會一直匹配到文件結束,爲解決這個問題,ANTLR通過標準正則表達式的標記(?後綴)提供了對非貪婪匹配子規則(nongreedy subrule)的支持。

非貪婪匹配的基本含義是:獲取一些字符,直到發現匹配後續子規則的字符爲止。更準確的描述是,在保證整個父規則完成匹配的前提下,非貪婪的子規則匹配數量最少的字符。

回到我們的字符串常量定義中來,這裏的定義其實並不完善,因爲它不允許其中出現雙引號。爲了解決這個問題,很多語言都定義了以 \ 開頭的轉義序列,因此我們可以使用 \" 來對字符串中的雙引號進行轉義。

STRING : '"' (ESC|.)*? '"';
// 表示\" 或者 \\
fragment ESC : '\\"' | '\\\\';

其中,ANTLR語法本身需要對轉義字符 \ 進行轉義,因此我們需要 \\ 來表示單個反斜槓字符。

6. 註釋和空白字符:對於註釋和空白字符,大多數情況下對於語法分析器是無用的(Python是一個例外,它的換行符表示一條命令的終止,特定數量的縮進指明嵌套的層級),因此我們可以使用ANTLR的skip指令來通知詞法分析器將它們丟棄。

// 單行註釋(以//開頭,換行結束)
LINE_COMMENT : '//' .*? '\r'?'\n' -> skip;
// 多行註釋(/* */包裹的所有字符)
COMMENT : '/*' .*? '*/' -> skip;

詞法分析器可以接受許多 -> 操作符之後的指令,skip只是其中之一。例如,如果我們需要在語法分析器中對註釋做一定處理,我們可以使用channel指令將某些詞法符號送入一個“隱藏的通道”並輸送給語法分析器。

大多數編程語言將空白符看成是詞法符號間的分隔符,並將他們忽略。

// 匹配一個或者多個空白字符並將他們丟棄
WS : [ \t\r\n]+ -> skip;

至此,我們已經學會了通配符的用法和如何編寫常見的詞法規則,下面我們將學習如何編寫語法規則。

三、語法

語法(grammar)包含了一系列描述語言結構的規則。這些規則不僅包括描述語法結構的規則,也包括描述標識符和整數之類的詞彙符號(詞法符號Token)的規則,即包含詞法規則和語法規則。注意:語法分析器的規則必須以小寫字母開頭,詞法分析器的規則必須以大寫字母開頭。

1. 語法文件聲明

語法由一個爲該語法命名的頭部定義和一系列可以互相引用的語言規則組成。grammar關鍵字用於語法文件命名,需要注意的是,命名須與文件名一致。

2. 語法導入

前兩章的例子中,我們都是將詞法規則和語法規則放在一個語法文件中,然而一個優雅的寫法是將詞法規則和語法規則進行拆分。lexer grammar關鍵字用於聲明一個詞法規則文件。如下是一個通用的詞法規則文件定義。

// 通用的詞法規則,注意是 lexer grammar
lexer grammar CommonLexerRules;
// 匹配標識符(+表示匹配一次或者多次)
ID : [a-zA-Z]+;
// 匹配整數
INT : [0-9]+;
// 匹配換行符(?表示匹配零次或者一次)
NEWLINE : '\r'?'\n';
// 丟棄空白字符
WS : [ \t]+ -> skip;

然後我們只需要import關鍵字,就可以輕鬆的將詞法規則進行導入。如下是一個計算器的語法文件。

grammar LibExpr;
// 引入 CommonLexerRules.g4 中全部的詞法規則
import CommonLexerRules;

prog : stat+;
stat : expr NEWLINE             # printExpr
    | ID '=' expr NEWLINE       # assign
    | NEWLINE                   # blank
    ;
expr : expr op=('*' | '/') expr    # MulDiv
    | expr op=('+' | '-') expr     # AddSub
    | INT                       # int
    | ID                        # id
    | '(' expr ')'              # parens
    | 'clear'                   # clear
    ;

// 爲上訴語法中使用的算術符命名
MUL : '*';
DIV : '/';
ADD : '+';
SUB : '-';

3. 備選分支命名(標籤)

如果備選分支上面沒有標籤,ANTLR就只會爲每條規則生成一個方法(監聽器和訪問器中的方法,用於對不同的輸入進行不同的操作)。爲備選分支添加一個標籤,我們只需要在備選分支的右側,以 開頭,後面跟上任意的標識符即可。如上所示。需要注意的是,爲一個規則的備選分支添加標籤,要麼全部添加,要麼全部不添加。

4. 優先級

在第二章中我們講述了ANTLR是如何處理歧義性語句(二義性文法)的:選擇所有匹配的備選分支中的第一條。即ANTLR通過優先選擇位置靠前的備選分支來解決歧義性問題,這也隱式地允許我們指定運算符優先級。例如,在上訴的例子中,乘除的優先級會比加減高。因此,ANTLR在解決1+2*3的歧義問題時,會優先處理乘法。

5. 結合性

默認情況下,ANTLR是左結合的,即將運算符從左到右地進行結合。但是有些情況下,比如指數運算符是從右向左結合的。1^2^3應該是3^(2^1)而不是(3^2)^1。我們可以使用assoc來手動指定結合性。

expr : expr '^' <assoc=right> expr    // ^ 是右結合的
     | INT
     ;

注意,在ANTLR4.2之後,<assoc=right>需要放在備選分支的最左側,否則會收到警告。

expr : <assoc=right> expr '^' expr    // ^ 是右結合的
     | INT
     ;

6. 詞法分析器與語法分析器的界限

由於ANTLR的詞法規則可以使用遞歸,因此從技術角度上看,詞法分析器可以和語法分析器一樣強大。這意味着我們甚至可以在詞法分析器中匹配語法結構。或者,在另一個極端,我們可以把字符當作詞法符號,然後使用語法分析器去分析整個字符流(這種被稱爲無掃描的語法分析器scannerless parser)。因此,我們需要去界定詞法分析器和語法分析器具體需要處理的界限。

  • 在詞法分析器中匹配並丟棄任何語法分析器無須知曉的東西。例如,需要在詞法分析器中識別和扔掉像空格和註釋諸如此類的東西。否則,語法分析器必須經常查看是否有空格或註釋在詞法符號之間。
  • 在詞法分析器中匹配諸如標誌符、關鍵字、字符串和數字這樣的常用記號。語法分析器比詞法分析器有更多的開銷,因此我們不必讓語法分析器承受把數字放在一起識別成整數的負擔。
  • 將語法分析器不需要區分的詞法結構歸爲同一個詞法符號類型。例如,如果我們的應用把整數和浮點數當作同一事物對待,那就把它們合併成詞法符號類型NUMBER。
  • 將任何語法分析器可以以相同方式處理的實體歸爲一類。例如,如果語法分析器不在乎XML標籤裏的內容,詞法分析器可以把尖括號中的所有東西合併成一個單獨的名爲TAG的詞法符號類型。
  • 另一方面,如果語法分析器需要把一種類型的文本拆開處理,那麼詞法分析器就應該將它的各個組成部分作爲獨立的詞法符號輸送給語法分析器。例如,如果語法分析器需要處理IP地址中的元素,那麼詞法分析器應該將IP的各個組成部分(整數和點)作爲獨立的詞法符號送入語法分析器。

後記

本章我們學習瞭如何編寫語法文件,但是單獨的語法並沒有用處,而與其相關的語法分析器僅能告訴我們輸入的語句是否遵循該語言的規範。爲了構建一個語言類應用程序,這是不夠的,我們還需要相應的“動作”去執行語法規則。而這就是下一章的內容——監聽器和訪問器。

 

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