ANTLR4權威指南 - 第5章 設計語法


第5章

設計語法

在第I部分,我們熟悉了ANTLR,並在一個比較高的層次上了解了語法以及語言程序。現在,我們將要放慢速度來學習下實現更實用任務的一些細節上的技巧,例如建立內部數據結構,提取信息,生成輸入對應的翻譯內容等。在我們開始的第一步,首先,就是需要學習怎樣建立語法。在這一章,我們會着眼於語言學結構中最通用的詞法和句法,並且學習怎樣用ANTLR來描述這些詞法和句法。以這些ANTLR建立的結構為基礎,在下一章我們會將它們組合起來並建立一些實際的語法。

在學習簡歷語法的時候,我們不能僅僅是從頭到尾學一遍眾多的ANTLR結構就可以了。首先,我們需要學習一些通用的語言模式,並了解如何從一些計算機語言的句子中識別它們。這也是我們需要一個總體的語言結構的原因。(語言模式是一個反復出現的語法結構,比如英語中的句子中的主謂賓的結構,或者是日語中的主賓謂的結構。)最終,我們需要從一系列的代表性輸入文件中分析出語言學上的結構,分析出來之后,我們就可以用ANTLR的語法來描述這個語言了。

值得慶幸的是,雖然在過去的50年里有數不清的語言被創造了出來,但是只有極少數的幾種語言模式是我們需要處理的。這個現象是因為我們在設計語言的時候,都會傾向於受到我們大腦中所了解的自然語言的限制。我們希望符號按照有效的順序排列,並且符號之間擁有着特定的依賴關系。舉個例子,{(}) 就是不符合語法的,因為符號的順序不對。而 (1+2 這樣的寫法會讓我們發瘋,因為我們總是會想去給它補上一個 ) 。大多數語言看上去都差不多,因為設計者在設計的時候通常都是用數學上的通用符號來設計的。就算是在詞法這個層次上來說,語言更偏向於重復利用同樣的結構,比如標識符,整數,字符串等。

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

l  序列:是由許多元素組成的一個序列,比如數組初始化句法中的數值。

l  選項:是指在多個可選的短語中的選擇,比如編程語言中的不同語句。

l  單詞約束:一個單詞的出現需要對應另一個其它短語的出現,比如左括號和右括號。

l  嵌套短語:這是一種自相似的語言結構,例如編程語言中的嵌套算術表達式或嵌套語句塊。

為了實現這些模式,我們只需要由可選項、符號引用和規則引用組成的語法規則(巴克斯范式,BNF)就可以了。為了表述起來更加方便,我們通常會使用子規則來組織這些元素。子規則就是用括號括起來的內部規則。比如,我們可以利用標記子規則(例如,可選的(?),重復零次到多次的(*),或重復一次到多次的(+))來識別一些重復多次的相似的語法片段(擴展的巴克斯范式,EBNF)。

理論上說,許多人已經在語法描述或者最起碼在正則表達式上已經了解過這種表現形式了,無論如何,為了考慮到更多的讀者,我們還是從非常開始的地方說起。

5.1 從語言樣本中派生語法

編寫一個語法和編寫一個軟件差不多,不同的是我們需要處理的是規則而不是函數或者處理過程。(記住,ANTLR會給每一個你的語法中的規則生成一個函數。)然而,在我們深入研究規則之前,很有必要討論下語法的整體結構以及如何形成一個初步的語法框架。這是任何一個語言程序的至關重要的第一步,所以,這一節主要討論的內容就是這個。如果你着急着想建立並執行第一個語法分析器,你可以重新翻閱下第4章,或者跳到下一章的第1個例子。但是上一章或者下一章的例子的基本原理都將在這里討論。

語法是由一個命名語法的頭信息和一系列的規則組成的,這些規則可以互相調用。

grammar MyG;

rule1 : «stuff» ;

rule2 : «more stuff» ;

...

就跟寫軟件一樣,我們必須指出哪些規則是我們的語法所需要的,必須說明<<stuff>>的內容是什么,並且必須說明哪一條規則是起始規則(就好像main()函數一樣)。

要寫出給定語言的所有規則,我們要么必須把這個語言掌握得非常得透,要么擁有一系列代表性的輸入樣例。當然,從一個參考手冊或者另一個編譯器生成器的格式中生成一個語言的語法會非常方便。不過現在,我們假定並沒有現成的可以參考的語法。

在編程的世界中,正確的語法設計應該能反映功能分解的思想或者自頂向下的設計思想。這句話的意思就是我們的工作順序應該是從粗糙到精細,即從識別語言結構到將其編碼成語法規則。所以,第一步工作應該是找一個最宏觀的語法結構的名字,這就是我們的起始規則。在英語中,我們的起始規則叫做句子。對於一個XML文件,我們把起始規則叫做文檔(document)。對於一個Java文件,我們的起始規則叫做可編譯單元(compilationUnit)。

設計起始規則的內容將相當於用英語偽代碼描述所有可能的輸入形式,就像我們寫軟件的時候那樣。例如,“逗點分割文件(CSV)是一個由一系列以換行符結束的行組成。”這句話中,“逗點分割文件(comma-separated-value file)”就是規則的名稱(用file表示),而剩下的部分都是這個規則的定義。

file : «sequence of rows that areterminated by newlines» ;

然后,我們繼續向下走一層,描述我們的定義項:由一系列以換行符結束的行。定義中的名詞一般要引用另一個符號或者是一個將被定義的規則。符號是指那些我們大腦能直接聯想成單詞、標點符號或運算符的元素。就像單詞是英語句子中最根本的元素一樣,符號也是一個分析器語法的最根本的元素。可以參考其它語言結構的規則引用需要被進一步向下分解,比如“行”。

向下走一層,我們描述所謂“行”就是一系列由逗號分隔開的字段。進一步的,字段要么是一個數字,要么是一個字符串。所以,我們的偽代碼看起來應該是這個樣子的:

file  : «sequence of rows that are terminated by newlines» ;

row      : «sequence of fields separated by commas» ;

field : «number or string» ;

我們過一遍需要定義的規則之后,就會有一個粗略的語法的草案。

讓我們看看這個過程在描述Java文件的一些關鍵結構中是怎樣工作的。(我們會把規則名稱突出表示出來。)在最上面的一層,我們定義,一個Java可編譯單元是由一個后面跟着一個或多個類定義包說明符組成。往下走一層,一個類定義是由關鍵字class,標識符,一個可選的父類說明符,一個可選的實現分句,以及一個類主體組成的。一個類主體是由花括號括起來的一系列成員組成。成員可以是一個嵌套的類定義,或者是一個字段,或者是一個函數。從這里開始,我們就該開始描述什么是字段以及什么是函數,更進一步的,在描述函數之后,我們還將描述什么是語句。現在,你應該有個整體的思路了,從最高的層次開始不斷往下描述,將每一個大塊的子短語(例如Java類定義)作為一個規則,並在后面描述它。這個過程用偽代碼描述大致如下:

compilationUnit : «optional packageSpecthen classDefinitions»;

packageSpec     : 'package' identifier ';' ;

classDefinition :

'class' «optional superclassSpec optionalimplementsClause classBody»;

superclassSpec  : 'super' identifier ;

implementsClause:

'implements' «one or moreidentifiers separated by comma»;

classBody       : '{' «zero-or-more members»'}';

member          : «nestedclassDefinition or field or method» ;

...

如果我們對一個大型語言(比如Java)比較熟悉的話,以這樣的語言作為參考設計一個新的語法會輕松很多。但是,你也必須要小心,盲目地引用一個現有的語法會將你帶入歧途,我們會在接下來仔細討論這個問題。

5.2 使用現有的語法作為參考

研究一個非ANTLR格式的現有的語法,我們可以掌握別人是怎樣將語言中的短語進行拆分的。最起碼的,一個現有的語法可以給出所有的我們需要用到的規則名稱作為參考。但是,你仍然需要小心。我反對直接從一個語法的參考手冊中直接將語法復制粘貼到ANTLR中,應該將其看作是一種參考,而不是一段現成的代碼。

為了將語法表現清晰,參考手冊通常描述的非常寬松,這意味着其語法可能會識別很多不再語言中的句子,或者,語法是具有歧義的,存在多種方法對同樣的輸入進行理解。例如,一個語法表述,一個表達式可以是調用一個構造函數或者是調用一個函數。問題在於,像T(i)這樣的輸入可以同時匹配構造函數調用和函數調用。理想的情況下,我們設計的語法中不存在這樣的歧義。我們對特定的輸入句子只有唯一的規則去翻譯這個輸入或者去執行這個輸入。

 

從另一個方面去考慮,參考手冊上的語法也可能會過分約束規則。有一些約束是需要我們在完成輸入分析的時候去檢查的,而不是在分析時從語法結構上去檢查。舉個例子,我們考慮下第12.4節上面的那個關於XML的例子,我過了一遍W3C XML的語法定義,里面的語法細節讓我頭疼不已。舉個普通的例子,XML語法嚴格地指出了標簽中什么地方必須要有空格以及什么地方空格又是可以省略的。這樣對於讀懂XML大有好處,但是我們的詞法分析器只是簡單地把輸入流中標簽間的空格剔除之后才傳給語法分析器。實際上我們的語法並不需要測試空格。

XML的語法也規定了“<?xml…>”標簽要有兩個特殊的屬性:encoding和standalone。我們需要知道這個約束,但是更簡單的做法是,我們在分析語法的時候允許讓這個位置出現任何屬性,在語法分析完成之后,我們可以通過分析語法樹再來檢查這個約束是否被滿足。最后,XML一些嵌套的文本標簽,因此它的語法結構是相當簡單的。最大的難點在於,怎樣分別處理什么在標簽的內部什么在標簽的外部。我們會在12.3節中詳細介紹這些。

怎樣准確地識別語法規則,並將其用偽代碼准確地表述出來,對於剛開始來說是一個很有挑戰性的工作,但是隨着你建立的語法數量的增加,這個過程也會變得越來越簡單。你可以在這本書中找到很多例子。

我們寫出偽代碼之后,我們需要將其翻譯成ANTLR能夠識別的語法。在下一節,我們將所有定義語言中都會經常出現的四種語言模式,並了解怎樣用ANTLR來表述這四種模式。在那之后,我們將學習怎樣定義我們設計的語法中會使用到的符號,比如整數或標識符。記住,這一章我們的重點在於語言程序開發的基礎部分。這會給我們在下一章建立一些實際應用的例子的時候打下堅實的基礎。

5.3 用ANTLR語法來識別通用語言模式

現在,我們已經知道了怎樣用自頂向下的方法來獲得一個語法的輪廓了。現在,我們需要着眼於一些通用的語言模式:序列,選項,符號約束和嵌套短語。在之前的章節中我們曾經見過一些例子,但是現在我們會從眾多的語言中見到更多的例子。在學習這個的同時,我們會學到一些基本的ANTLR語法,並用這個語法來作為表達這些特殊模式的正式語法規則。那么,就讓我們從最通用的語言模式開始吧。

模式:序列

計算機語言中的這種結構最常見的例子就是元素序列,比如類定義中的函數列表。就算是一些簡單的語言,比如HTTP,POP和SMTP網絡協議,也能找到這種序列模式。協議中最常出現的是命令序列。例如,下面是登錄到一個POP服務器並獲得第一條消息的命令序列:

USER parrt

PASS secret

RETR 1

而命令本身也往往是一個序列。大部分的命令都是由以下部分組成一個序列的:關鍵字(保留的標識符,比如USER和RETR),一個操作數,然后是一個換行符。舉個例子,在語法中,我們規定,一個獲取命令是由一個關鍵字加上一個整數加上一個換行符組成的。要識別這樣的語法,我們只需要按照順序列出這些元素就可以了。在ANTLR中,獲取命令的序列表示為‘RETR’ INT ‘\n’,其中,INT代表了整數符號類型。

retr :'RETR' INT '\n';// match keywordinteger newline sequence

注意,在我們的簡單序列中能夠包含語法中的任何字母,比如關鍵字或標點符號,或者直接的一個字符串,像“RETR”。(我們會在5.5節進一步探索諸如INT這樣關鍵字的詞法結構。)

類似於我們在編程語言中用函數來標記語句塊,我們使用語法規則來標記語言結構。因而,我們將RETR序列標記為retr規則。這樣,在語法的其它地方,我們可以通過規則名稱來快速引用RETR序列。

考慮下任意長的序列,比如簡單的整數列表,像matlab中的向量,諸如[1 23]之類的。對於有限序列,我們希望這些元素一個接着一個出現,但是,我們又不可能像INT INT INT INT INT INT INT INT INT…這樣列出所有可能出現的情況。

要表示一個序列中的某個元素需要出現一次或多次,我們可以使用“+”子規則操作符。例如,(INT)+代表一個由整數組成的任意長的序列。為了書寫方便,寫成INT+也是可以的。如果希望這個序列也可以是空的,也就是某個元素可以出現零次或多次,我們可以使用“*”操作符:INT*。這個操作符類似於編程語言中的循環結構,當然,ANTLR生成編譯器的時候就是用循環結構實現的。

序列模式有很多中情況,其中包括兩個常見的情況,帶終結符的序列帶分隔符的序列。CSV文件很好地展示了這兩種情況。下面是我們將上一節中描述的CSV偽代碼用ANTLR語法編寫的代碼:

file : (row '\n')* ;       // sequence with a '\n' terminator

row : field (',' field)*; // sequence with a ',' separator

field: INT ;             // assume fields are just integers

file規則使用終結符模式來匹配零個或多個“row ‘\n’”序列列表。符號’\n’是每個序列結束的標志。row規則使用分隔符模式來匹配由零個或多個’,’隔開field序列列表。符號’,’分隔開了所有的field。row能夠識別像1以及1,2以及1,2,3這樣的序列。

我們能在編程語言中找到類似的結構。例如,下面是一個識別編程語言中語句序列的例子,像Java語言那樣,每條語句以分號作為結束符:

stats : (stat ';')* ; // match zero or more ';'-terminatedstatements

下面展示了如何指定一個逗號分割開的表達式列表,這種結構在函數的調用參數列表中會被用到:

exprList : expr (',' expr)* ;

不僅如此,連ANTLR的元語言也是用序列模式。下面展示了ANTLR是怎樣用自己的語法來規定ANTLR的規則定義語法的:

// match 'rule-name :' followed by at leastone alternative followed

// by zero or more alternatives separatedby '|' symbols followed by ';'

rule : ID ':' alternative('|'alternative )*';' ;

最后,再介紹一種特殊的出現零次或一次的序列模式,用符號“?”表示,這種模式通常用來表示可選項結構。例如,在Java的語法中,我們可以發現可以使用(‘extends’ identifier)? 這樣的序列來識別可選的父類繼承說明。類似的,要匹配變量聲明的同時可選擇的初始化,我們可以使用 (‘=’ expr)? 來實現。可選操作符就是選擇有或者沒有。下一節將會提到,(‘=’ expr)? 就等價於 (‘=’ expr |) 。

模式:選項(替換項)

一個只有一種句式的語言會顯得很枯燥無味。就算是最簡單的語言,比如網絡協議,也會有很多不同的句式,比如POP協議中的USER命令和RETR命令。這時我們就得選擇選項模式。我們在Java的語法偽代碼描述中見過這種模式,比如member規則中的“嵌套類定義或字段或方法”。

為了表達語言中選項這個概念,在ANTLR規則中,我們使用“|”操作符來表示“或”這個語法選擇概念,我們稱這種語法選擇概念為選項。語法中到處都可以看到選項。

回到我們的CSV語法中,我們可以定義一個更加靈活的field規則,讓其可以選擇是整數還是字符串。

field : INT | STRING ;

試着翻看下下一章中的語法,我們可以發現很多選項模式的例子,比如6.4節中提到的一個type規則中列舉的類型名稱。

type: 'float' 'int'|'void' // user-defined types

在6.3節,我們還會看到一個圖表描述中的所有可能的語句列表。

stmt: node_stmt

   | edge_stmt

   | attr_stmt

   | id '=' id

   | subgraph

   ;

不管什么時候,當你描述“語言結構x可以是這樣也可以是那樣”的時候,你其實是在描述一個選擇模式。那么,在你的規則x中使用“|”操作符吧。

語法中的序列和選項模式已經能夠讓我們實現大多數的語言結構了,但是,仍然有兩個很關鍵的模式需要我們留意:符號約束和嵌套短語。他們通常會在語法中混合使用,所以,讓我們開始繼續學習符號約束吧。

模式:符號約束

在前面,我們使用INT+來表示matlab向量中的非空的整數序列,比如[1 2 3]。要識別一個方括號括起來的向量,我們還需要表述符號之間的依賴關系。這種依賴關系是指,一旦我們在句子中看到一個符號,我們就必須能在句子的其它地方能夠找到其匹配對應的符號。要表示這樣的語法,我們可以在序列中同時指定這些對應的符號,通常情況下,這些對應符號會包含或分開一些其它元素。基於這些,我們能夠完成向量的識別:

vector : '[' INT+ ']';// [1], [1 2], [1 2 3], ...

回顧一下所有你熟悉的語言,你會發現,幾乎所有的分組符號都是成對出現的:(…),[…]以及{…}。在6.4節中,我們可以看到表述函數調用和數組下標時候使用到的括號符號約束關系。

expr: expr '(' exprList?')'  // func call like f(), f(x), f(1,2)

   | expr '[' expr ']'      // array index like a[i], a[i][j]

   ...

   ;

我們在函數的聲明中也能夠看到左右括號之間的這種符號約束關系。

examples/Cymbol.g4

functionDecl

   : type ID '(' formalParameters?')'block //"void f(int x) {...}"

   ;

formalParameters

   : formalParameter(',' formalParameter)*

   ;

formalParameter

   : type ID

   ;

在6.2節的例子中介紹的JSON語法,則匹配了大括號包含的元素定義語法,例如 {“name”:”part”, ”passwd”:”secret”} 。

examples/JSON.g4

object

   : '{' pair (','pair)*'}'

   | '{' '}' // empty object

   ;

pair: STRING ':' value;

在6.5節中可以看到更多這樣的匹配對應符號的例子。

還有一點必須引起你的注意,並不是所有的依賴性符號都是像括號那樣相互對應的。C風格的語言中,有一種像 a?b:c 這樣的三目運算符,其中就要求遇到了“?”符號之后必須要有一個“:”和其組成完整的運算符。

到目前為止,因為我們只是單純地匹配符號,所以沒有涉及到嵌套短語。比如,一個向量是不允許內部嵌套一個向量的。但是更一般地來看,被成對符號(比如括號)括起來的短語通常都可以是嵌套的。比如我們通常可以見到這樣的結構:a[(i)],{while(b){i=1;}}。這個時候,我們就需要用到最后一個語言模式了。

模式:嵌套短語

嵌套短語有一種自相似的語言結構,即其子短語和其本身有着同樣的結構。表達式是一種典型的自相似語言結構,表達式是由運算符分隔開的嵌套子表達式組成的。類似的,while語句塊也是一個嵌套在其它外層語句塊中的語句塊。我們在語法上用遞歸規則來表示自相似的語言結構。所以,當我們的偽代碼在表述一個規則的時候引用到了自己,我們就需要使用遞歸規則(自引用)。

 

讓我們看看對於代碼塊語法,嵌套是怎么工作的。一個while語句是由一個關鍵字“while”,跟一個括號括起來的條件表達式,跟一個語句組成的。當然,我們把用大括號括起來的多條語句看成是一條語句。那么,要表達這樣的語法,就應該像這樣:

stat: 'while' '(' expr')' stat // match WHILE statement

   | '{' stat* '}'         // match block of statements in curlies

   ...                   // and other kinds of statements

   ;

while語句的循環體可以是一條語句也可以是用大括號括起來的多條語句。stat規則是一個直接遞歸的規則,因為它在其第一個選項和第二個選項中都直接引用了自己。如果我們把stat的第二個選項改動一下,讓第二個選項單獨成為一個規則block,那么規則stat和block之間就形成了非直接遞歸

stat: 'while' '(' expr')' stat // match WHILE statement

   |block                    // match a block of statements

   ...                        // and other kinds of statements

   ;

block: '{' stat* '}'  // match block of statements in curlies

大多數平凡語言都有許多自相似的結構,進而會產生許多遞歸規則。我們以一個簡單的語言例子為例,這個例子中表達式只有三種類型:下標數組,括號表達式,整數。下面是我們如何在ANTLR中表述這種表達式語法:

expr: ID '[' expr ']'    // a[1], a[b[1]], a[(2*b[1])]

   '('expr ')'     // (1), (a[1]), (((1))), (2*a[1])

   | INT             // 1, 94117

   ;

仔細觀察遞歸都是怎么產生的。下標數組中,下標本身就是一個表達式,所以我們直接用expr來替換下標元素就可以了。數組的下標本身就是一個表達式,這應該並不難理解。語言的結構自然而然地就決定了規則引用正好是遞歸的。

下面展示了兩個輸入樣例的語法樹:

 

正如我們在2.1節看到的那樣,語法樹的內部節點就是規則引用,而葉子節點就是符號引用。從樹的根節點開始到任何節點的路徑都表示了那個節點所表示的元素的調用棧(或者叫做ANTLR生成的遞歸下降分析器的調用棧)。路徑代表了遞歸,對於同樣的規則,不同嵌套的子樹會有不同的引用。我個人喜歡把規則節點看成對其下方子樹的標簽。例如根節點是expr,所以整棵樹所表示的就是一個表達式。而數字1上面的那個expr子樹則表示這個整數1是一個表達式。

並不是每種語言都含有表達式,比如數據格式語言就沒有,但是大多數你可以用來寫程序的語言基本上都帶有非常復雜的表達式語法(詳見6.5節)。此外,識別表達式語法並不都是一件顯而易見的事情,所以非常有必要花點時間好好研究下怎樣識別表達式。我們接下來會好好討論這個的。

首先,下表列出了ANTLR核心語法的一個小總結:

語法

描述

x

匹配一個符號,規則或子規則x

x y … z

匹配一個規則序列

(…|…|…)

帶有多個選項的子規則

x?

匹配零次或一次x

x*

匹配零次或多次x

x+

匹配一次或多次x

r:…;

定義規則r

r:…|…|…;

定義一個帶有多個選項的規則r

表1 — ANTLR核心語法

下表總結了到目前為止我們所學習過的所有計算機語言模式:

模式名稱

描述

序列

這是由符號和子短語組成的任意長的有限的序列,例如變量聲明語法(類型后面加上標識符)以及整數列表等。下面是一些實現這種模式的例子:

x y ... z       // x followed by y, ..., z

'[' INT+ ']'   // Matlab vector of integers

帶終結符的序列

這是由符號和子短語組成的任意長的,可能是空的序列,以一個符號結束,通常情況系這個符號是分號或換行符,例如C風格的編程語言中的語句以及以換行符終結的數據行。下面是一些實現這種模式的例子:

(statement ';')*   // Java statement list

(row '\n')*        // Lines of data

帶分隔符的序列

這是由符號的子短語組成的任意長的非空的序列,用一個特定的符號分隔開,通常這個符號是逗號,分號或句號。例如函數參數列表,函數調用列表,或者是分開卻不終止的程序語句。下面是一些實現這種模式的例子:

expr (',' expr)*      // function call arguments

( expr (',' expr)* )?

// optional function call arguments

'/'? name ('/'name)*  // simplified directory name

stat ('.' stat)*      // SmallTalk statement list

選項

這是由一系列可選擇的短語組成的,例如類型說明,語句,表達式或者XML的標簽。下面是一些實現這種模式的例子:

type : 'int' 'float';

stat : ifstat | whilestat | 'return'expr ';' ;

expr : '(' expr ')'| INT | ID ;

tag : '<' Name attribute* '>''<' '/' Name '>';

符號約束

一個符號的出現需要另一個或多個子序列符號的出現來對應,例如小括號,中括號,大括號,尖括號的匹配等。下面是一些實現這種模式的例子:

'(' expr ')'         // nested expression

ID '[' expr ']'         // array index

'{' stat* '}'  // statements grouped in curlies

'<' ID (','ID)* '>' // generic type specifier

遞歸短語

這是一種自相似的語言結構,例如表達式結構,Java類嵌套,代碼塊嵌套以及Python中的函數嵌套定義等。下面是一些實現這種模式的例子:

expr : '(' expr ')'| ID ;

classDef : 'class' ID '{'(classDef|method|field) '}' ;

5.4 處理優先級,左遞歸以及相關性

在自頂向下的語法中,利用遞歸下降分析器來識別表達式一直都是一件很麻煩的事情,首先,因為很多自然語法都是模糊不清的,其次,大部分自然定義都使用一種特殊的遞歸——左遞歸。我們會在后面仔細地討論左遞歸,但是現在,我們只需要知道在傳統模式下,自頂向下的語法分析器是無法處理左遞歸的。

為了更好地表述這個問題,想象一個簡單的數學表達式語言,其只包含乘法和加法操作符,且只有整數作為原子項。表達式是自相似的,所以我們很自然能夠想到一個乘法表達式是由乘號(*)連接兩個子表達式組成的。類似的,一個加法表達式是由加號(+)連接兩個子表達式組成的。我們也可以把一個單獨的整數算作一個表達式。從字面上來將這個想法轉換成語法規則,那么這個規則看起來應該像下面這樣:

expr : expr '*' expr // match subexpressions joined with '*' operator

    | expr '+' expr  // match subexpressions joined with '+' operator

    | INT             // matches simple integer atom

    ;

但問題是,這樣的語法定義對於一些特定的輸入是具有歧義的。換句話說,這樣的規則可能存在對同一條輸入的多種理解方式,具體看以參閱2.3節。對於只有一個整數或者只有一個操作符的表達式,例如1+2和1*2這種,這個規則可以正常工作。例如,這個規則可以匹配1+2,只需要使用第二條規則就行,如圖3的左邊所示。

 

圖3 語法樹解析

問題在於,當識別輸入像1+2*3這樣的表達式時,這條規則會存在2種不同的方式來解析,如圖3中的中間和右邊的語法樹所示。這兩種解析是不一樣的,因為中間的語法樹的表示先計算2*3的結果再加上1,而右邊的語法樹表示先計算1+2的結果再乘以3。這涉及到一個算符優先級的問題,傳統的語法將無法識別這種優先級。許多語法工具,比如Bison,使用額外的符號來指定這種算符優先級。

與此不同的是,ANTLR通過這些算符規則定義的順序來解決這種歧義,允許我們通過這種方式來隱式地指定算符優先級。expr規則先定義了乘法選項后定義了加法選項,所以ANTLR在處理類似於1+2*3這樣的語法歧義的時候會先選擇乘法選項。

默認情況下,ANTLR是從左到右來結合運算符的,這正好符合*和+的運算規律(從左到右計算)。但是,一些運算符(比如冪運算符)卻是需要從右到左結合的,這種情況下,我們就需要用到assoc選項來手動來指定運算符的結合方向。例如,下面是一個冪運算表達式規則的例子,能夠正確地將2^3^4這樣的輸入識別成2^(3^4):

expr : expr '^'<assoc=right>expr// ^ operator is right associative

   | INT

   ;

下面的語法分析樹展示了“^”運算符的左結合和右結合這兩種結合方式的差別。右邊的語法樹才是我們通常對冪運算的理解方式,但是作為語言設計者而言,可以根據自己的設計指定任意一種結合方式。

 

將冪運算也加入我們的表達式規則中,我們需要將“^”規則選項放在“*”和“+”的前面,因為它的優先級比乘號和加號要高(例如,1+2^3的結果應該是9)。

expr : expr '^'<assoc=right>expr// ^ operator is right associative

   | expr '*'expr // match subexpressions joined with '*'operator

   | expr '+'expr // match subexpressions joined with '+'operator

   | INT // matches simple integer atom

   ;

看到這里,很多熟悉ANTLR v3的讀者就應該迫不及待地要向我指出,ANTLR應該和傳統的自頂向下分析器生成器一樣,是無法處理左遞歸的。然而,ANTLR v4的一個很重要的改進就是它能支持直接左遞歸。左遞歸是指在規則的最左邊能夠直接或者間接調用自身的規則。例如,expr規則就是一個直接左遞歸規則,因為除了INT選項外,每一個選項一開始都是直接調用expr自身。(expr同樣也是一個右遞歸規則,因為expr在一個選項的右邊也直接調用了自己。)

雖然ANTLR v4能夠支持直接左遞歸,但是目前還不能處理間接左遞歸。這意味着如果我們想講expr因子化為一些等價的規則會出問題。

expr : expo // indirectlyinvokes expr left recursively via expo

   | ...

   ;

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

優先爬山算法表達式分析器

很多有經驗的編譯器開發員在自己手寫編譯器的時候總是想盡一切辦法提升程序性能,並且嘗試能夠去完全實現對錯誤恢復的控制。相比於編寫老長老長的表達式規則分析代碼,他們更加偏向於使用算符優先文法分析器。

ANTLR使用一個和算符優先類似但是更具有效率策略,這種策略最初是Keith Clarke在1986年提出來的。隨后,Theodore Norvell使用術語“優先爬山”(precedence climbing)來命名這種方法。與之類似的,ANTLR使用預先定義好的循環來替換直接左遞歸,從而進行前后運算符的優先級比較。詳情參見第11章。

 

用ANTLR v3來處理表達式的時候,我們需要將expr規則拆分成多個規則來解決左遞歸的問題,每條規則都代表一個優先級。例如,在之前,我們需要像下面這樣定義從而正確識別乘法操作符和加法操作符:

expr     : addExpr ;

addExpr : multExpr ('+' multExpr)* ;

multExpr: atom ('*' atom)* ;

atom     : INT ;

像C語言或Java中的表達式至少包括15條這樣的規則,如果我們自己去編寫自頂向下語法或者遞歸下降分析器的話,這么多規則處理起來是一件很麻煩的事情。

ANTLR v4中簡化了直接左遞歸規則的處理過程,這不僅僅是一個更有效率的特性,更重要的是表達式規則也將變得更簡單且更容易理解。例如,在Java語法中,關於表達式預測的語法行數幾乎減少了一半(從172行減少到91行)。

實際上,我們幾乎能處理所有我們關心的左遞歸的語言結構。例如,下面的例子能夠識別C語言聲明語句的子集,包括像“*(*a)[][]”這樣的輸入:

decl : decl '[' ']' // match [] suffixes using direct left-recursion

   | '*' decl      // *x, *x[], **x

   | '(' decl ')'  // (x), (x[]), (*x)[]

   | ID

   ;

要了解更多的關於ANTLR支持的左遞歸(使用語法轉換實現)結構,請參見第14章。

到這里為止,我們已經學習了計算機語言中通用的語言模式,並知道了怎樣用ANTLR的語法來表示這些語言結構。但是,在我們真正可以開始實現一些完整的例子之前,我們還需要掌握怎樣在我們的語法規則中描述符號引用,同時,我們還將學習一些通用到極點的詞法結構。創建一個完整的語法主要就是將這一節學習到語法規則和下一節要學習的詞法規則結合起來的過程。

5.5 識別通用詞法結構

計算機語言在詞法上看起來非常相似。舉個例子,如果加上一些特定的語法信息來規定順序,“)”“10”“(”“f”這幾個符號幾乎能從最原始的語言到最新的語言中都能組成有效的短語。15年前,我們可以在LISP中看到“(f10)”這樣的寫法,也可以在Algol中看到“f(10)”這種寫法。當然,“f(10)”這種寫法幾乎從Prolog到Java再到最新的Go語言都支持。從詞法上來看,函數式的、過程式的、聲明式的以及面向對象的語言看起來都非常得一致。多么令人驚訝!

這一點意味着我們只需要學習怎樣描述標識符和整數,然后做一點小改動,就可以將其應用到大多數的編程語言中了。從分析器的角度來看,詞法分析器也是使用規則來描述各種各樣的語言結構的。在描述中,我們幾乎使用完全一樣的符號和規則。語法分析器和詞法分析器的唯一差別在於,語法分析器是從符號流中識別語法結構的,而詞法分析器是從字符流中識別語法結構的。

鑒於詞法規則和語法規則擁有着類似的結構,ANTLR中允許將這兩個規則寫在同一個語法文件中。但是,畢竟詞法分析和語法分析在語言識別中是兩個完全不同的階段,所以我們必須告訴ANTLR哪條規則應該屬於哪個階段。我們通過規則名稱的第一個字符來識別這兩種規則,第一個字符是大寫字母的是詞法過則,第一個字符是小寫的是語法規則。例如,ID是一個詞法規則名稱,而expr則是一個語法規則名稱。

當我開始編寫一個新的語法的時候,我習慣從現有的語法中復制規則,比如從Java的規則中復制一些最通用的詞法結構:標識符,數字,字符串,注釋以及空白字符。然后,通過一些巧妙的調整,就可以直接建立並使用了。幾乎所有的語言,包括那些非編程的語言,比如XML和JSON,都擁有很多這樣詞法符號。例如,一個C語言的詞法規則同樣也可以用來識別下面這段JSON代碼:

{

   "title":"Catwrestling",

   "chapters":[{"Intro":"..."}, ... ]

}

另一個例子,我們看一下塊注釋。在Java中,塊注釋是用“/*…*/”表示的,而在XML則是使用“<!--…-->”表示的。但是它們除了起始和終止符號不一樣意外幾乎是完全相同的。

再看關鍵字,運算符和標點符號,我們並不需要為這些創建規則,因為我們能夠在語法規則中直接通過用單引號將這些符號引起來的方式來直接匹配。比如’while’,’*’,’++’等。一些程序員更喜歡用詞法規則來創建引用,比如用MULT來代替符號’*’。這么做的好處就是當需要修改乘法符號時,只需要修改MULT的規則定義就可以了。這兩種方式都可以使用,它們最終都會生成同樣的符號類型。

為了說明詞法規則的樣子,我們來看一些通用符號的簡化版本,就從我們的老朋友“標識符”開始吧。

匹配標識符

在語法的偽代碼中,一個基本的標識符被描述為一個非空的,由大寫和小寫字母組成的字母序列。使用一下我們之前剛學到的新技能,我們知道可以使用“(…)+”的方式來表示這樣的序列模式。因為組成標識符的元素可以是小寫字母也可以是大寫字母,所以這里應該有一個選擇操作符來描述這種模式。

ID : ('a'..'z'|'A'..'Z')+ ; // match 1-or-more upper or lowercaseletters

在這里我們遇到了一個新的ANTLR表述語法,即區間運算符:’a’.. ‘z’,這個短語代表了一個’a’到’z’之間的任意字符。這個區間也就是ASCII碼上面的97到122。如果需要用到Unicode來指定字符,我們需要用到’\uXXXX’這種形式,其中的XXXX就是十六進制的Unicode字符編碼。

作為字符集的縮寫形式,ANTLR也支持一些類似於正則表達式的寫法。

ID : [a-zA-Z]+ ; // match 1-or-more upper or lowercase letters

但是,像ID這樣的規則有時候會和其它的詞法規則或者符號引用發生沖突,比如ID會和引用’enum’沖突。

grammar KeywordTest;

enumDef : 'enum' '{' ... '}' ;

...

FOR : 'for' ;

...

ID : [a-zA-Z]+ ; // does NOT match 'enum' or 'for'

規則ID同時也能匹配上一些關鍵字,比如enum和for,換句話說,同一個輸入串可能會被多條規則同時匹配。為了讓這個問題更加清楚,我們來考慮下ANTLR是怎樣來處理這種混合的詞法/語法規則的。首先,ANTLR會將所有的詞法規則(包括詞匯引用)和語法規則,然后,像’enum’這樣的詞匯引用會轉換成詞法規則,並插入到語法規則之后,明確定義的詞法規則之前。

ANTLR的詞法分析器是通過詞法規則出現的順序來解決這種歧義的。也就是說,你的ID規則必須定義在所有的關鍵字規則后面,就像上面例子中定義在for后面那樣。ANTLR會將所有的詞匯引用隱含地生成詞法規則並將這些規則放在明確定義的詞法規則之前,所以直接詞匯引用一般都有較高的優先級。在這種情況下,’enum’將自動比ID的優先級要高。

由於ANTLR自動將所有詞法規則都放在語法規則后面,像下面的例子這樣重新調整順序依然能夠獲得同樣的解析結果:

grammar KeywordTestReordered;

FOR : 'for' ;

ID : [a-zA-Z]+ ; // does NOT match 'enum' or 'for'

...

enumDef : 'enum' '{' ... '}' ;

...

到目前為止,我們的標識符的定義中還不允許出現數字,你可以在6.3節和6.5節看到完整的關於ID的定義。

匹配數字

要描述像10這樣的整數是一件非常容易的事情,因為它僅僅是數碼的序列。

INT : '0'..'9'+ ; // match 1 or more digits

INT : [0-9]+ ; // match 1 or more digits

然而,浮點數的表達方式就顯得非常復雜了。但是,我們可以通過忽略指數形式來簡化浮點數的表示。(在6.5節中可以看到完整的浮點數表示,甚至還能看到像3.2i這樣的復數的表示。)浮點數就是一個數碼的序列,后面跟上一個小數點,然后跟上一個可選擇的小數部分,或者是由小數點開始,后面跟上一連串的數碼序列。只有一個小數點這樣的表示方法是非法的。所以,我們使用選擇模式和序列模式來表示浮點數規則。

FLOAT: DIGIT+ '.' DIGIT*    // match 1. 39. 3.14159 etc...

   | '.' DIGIT+          // match .1 .14159

   ;

 

fragment

DIGIT : [0-9] ;             // match single digit

這里,我們使用了一個協助規則DIGIT,這樣我們只需要寫一遍[0-9] 就可以了。使用fragment前綴來定義,ANTLR就會知道這條規則僅僅用來組成其它詞法規則,而不會單獨使用。DIGIT本身並不是我們需要的符號,也就是說,我們在語法分析器中是看不到DIGIT這個符號的。

匹配字符串

計算機語言中最經常出現的下一個通用結構就是字符串,比如”Hello”。大多數情況下都是使用雙引號,但是也有些語言會使用單引號,甚至像Python這樣的語言兩種符號都使用。不管使用什么符號來分割字符串,我們使用同樣的規則來表示字符串內部。在偽代碼中,字符串就是在雙引號之間由任意字符組成的序列。

STRING : '"' .*? '"' // match anything in "..."

其中,通配符“.”匹配任意一個字符。所以,“.*”就能匹配任意長的可以為空的字符串。當然,這種匹配也可以直接匹配到文件末尾,但是這么做往往是沒有意義的。所以,ANTLR使用標准正則表達式符號(后綴:?)來表示采用非貪婪子規則的策略。非貪婪的意思是“不斷匹配字符,直到匹配上詞法規則中子規則的后面跟着的元素”。更精確地說,非貪婪子規則值匹配盡可能少的字符,盡管這條規則有可能匹配更多的字符。更多細節查看第15.6節。相比之下,“.*”就被認為是貪婪子規則,因為它會匹配掉所有循環內部的字符(這種情況下,作為通配符使用)。如果你現在對“.*?”不太理解,也沒有關系,現在你只需要知道這種寫法是用來表示值匹配雙引號引起來的內部所有字符就可以了。我們會在后面討論注釋的時候再次討論非貪婪循環的。

我們的STRING規則還是不完整的,因為目前我們的規則中不允許出現雙引號。為了支持字符串中出現雙引號,許多語言都會定義一種以反斜杠開頭的轉義序列。要讓雙引號引起來的字符串中也出現雙引號,我們使用“\””。我們需要如下定義來支持轉義字符:

STRING: '"' (ESC|.)*?'"' ;

fragment

ESC : '\\"' '\\\\' // 2-char sequences \" and \\

ANTLR本身支持轉義字符,所以需要對反斜杠進行轉義,所以我們可以看到上面使用“\\”來代表一個反斜杠。

現在,我們的STRING規則的循環中可以通過片段規則ESC來匹配轉義序列,以及通過“.”通配符匹配任意單個字符了。而“*?”子規則操作符會在匹配規則中后跟元素(沒有轉義的雙引號)時結束匹配“(ESC|.)*?”的循環。

匹配注釋和空白字符

當詞法分析器匹配了我們定義的符號之后,就會通過符號流將識別到的符號提交給語法分析器。然后,語法分析器就會根據語法結構來檢查這個符號流。但是,當詞法分析器遇到注釋和空白字符時,我們希望詞法分析器直接忽略它們,這樣的話,語法分析器就不用考慮怎么處理這些到處都可能會出現的注釋和空白字符了。例如,WS代表是一個詞法規則中的空白字符,像下面這么定義語法規則的話是一件非常可怕的事情:

assign : ID (WS|COMMENT)? '=' (WS|COMMENT)? expr (WS|COMMENT)? ;

定義這些需要丟棄的符號和定義非丟棄的符號是一樣的,我們只需要簡單地使用skip命令來指明詞法分析器需要將這些符號忽略就行了。例如,下面是匹配類C語言中單行注釋和多行注釋的規則:

LINE_COMMENT  : '//' .*? '\r''\n' -> skip ; // Match"//" stuff '\n'

COMMENT       : '/*' .*? '*/' ->skip ;       // Match "/*" stuff "*/"

在LINE_COMMENT規則中,“.*?”匹配“//”后面直到第一個出現的換行符(回車符前面的那個換行符是為了匹配Windows下的換行符)之前的所有字符。在COMMENT規則中,“.*?”匹配了“/*”和“*/”之間的所有字符。

詞法分析器通過“->”操作符來接收命令,skip只是諸多命令中的一種。例如,我們還可以通過channel命令將這些符號傳遞給語法分析器的“隱藏通道”。關於符號通道請查閱第12.1節。

下面來看看怎樣處理我們這一解中的最后一個通用符號,空白字符。一些編程語言把空白字符作為符號的分隔符,而其他語言則是直接忽略空白字符。(Python是一個例外,因為Python將空白字符作為語法結構的一部分了:換行符作為命令的結束符,縮進(Tab或者空格)作為指定嵌套結構的符號。)下面是讓ANTLR忽略空白字符的例子:

WS : (''|'\t'|'\r'|'\n')+ -> skip ; // match 1-or-more whitespace but discard

或者

WS : [\t\r\n]+ -> skip ; // match 1-or-more whitespace but discard

當換行符既是需要忽略的空白字符又是作為命令的結束符時,我們就會遇到一個問題。換行符是上下文相關的。在一個語法結構中,我們需要忽略換行符,但是再另外一個語法結構中,我們又需要將換行符傳遞給語法分析器,這樣語法分析器才能知道一條命令是否結束。例如,在Python中,f()后面跟一個換行符意味着我們需要執行指令,調用f()。但是我們又可以在圓括號中插入一個額外的換行符。Python會等到遇到“)”后面的換行符才會執行函數調用。

$ python

>>> def f(): print "hi"

<...

>>> f()

<hi

>>> f(

... )

<hi

關於這個問題的詳細討論,請參見第12.2節。

那么,現在我們知道了怎樣實現最基本的通用詞法結構:標識符,數字,字符串,注釋,以及空白字符。信不信由你,這對於一個大語言程序的詞法部分而言,是一個很好的開始。下面是一個詞法新人工具包,我們以后可以將其作為參考:

符號類別

描述和例子

標點符號

對標點符號和運算符最簡單的處理就是直接在語法規則中引用它們。

call : ID '(' exprList ')' ;

當然一些程序員更喜歡定義符號的標簽規則,例如定義LP來代表左括號。

call : ID LP exprList RP ;

LP : '(' ;

RP : ')' ;

關鍵字

關鍵字就是保留的標識符,和標點符號的處理一樣,我們可以直接引用也可以定義標簽規則。

returnStat : 'return' expr ';'

標識符

標識符在幾乎所有語言中看起來都差不多,可以再加一些改動,比如規定首字符以及設定是否可以使用Unicode字符。

ID : ID_LETTER (ID_LETTER | DIGIT)* ; // From C language

fragment ID_LETTER : 'a'..'z'|'A'..'Z'|'_' ;

fragment DIGIT : '0'..'9' ;

數字

例子中是整數和簡單浮點數的定義。

INT : DIGIT+ ;

FLOAT

    : DIGIT+ '.' DIGIT*

    | '.' DIGIT+

    ;

字符串

匹配使用雙引號引起來的字符串。

STRING : '"' ( ESC | . )*? '"' ;

fragment ESC : '\\' [btnr"\\] ; // \b, \t, \n etc...

注釋

識別並丟棄注釋。

LINE_COMMENT : '//' .*? '\n' -> skip ;

COMMENT        : '/*' .*? '*/' -> skip ;

空白字符

匹配詞法中的空白字符並丟棄這些字符。

WS : [ \t\n\r]+ -> skip ;

現在,我們已經能夠將輸入文件傳遞給詞法和語法分析器並准備好來解決下一章中的一些例子了。在我們繼續之前,還需要考慮兩個重要的問題。首先,什么時候該用詞法匹配,什么時候改用語法匹配,這兩者之間的界限並沒有那么清晰。其次,ANTLR對於我們的語法規則有一些限制,我們最好了解下這些限制。

5.6 詞法分析和語法分析之間的界限

由於ANTLR的詞法分析規則也可以使用遞歸,所以詞法分析器幾乎和語法分析器一樣智能。換句話說,我們能夠在詞法分析器中實現語法結構。或者,從另一個極端上說,我們可以把字符當作符號,然后利用語法分析器將語法規則運用在字符流上。(這樣的語法分析器叫做無掃描語法分析器,可以參考一個匹配小型的C和SQL混合的語法,詳見code/extras/CSQL.g4)。

詞法分析和語法分析的界限部分依賴於語言的功能以及預期應用的功能。值得慶幸的是,有一些總結可以幫助我們區分。

l  應該在詞法分析器中匹配那些需要丟棄的元素,這樣語法分析器就無需關心這些內容了。比如編程語言中的空白字符和注釋就需要識別並剔除。否則的話,語法分析器就不得不花很大的功夫來檢查兩個符號之間的空白字符或注釋了。

l  應該在詞法分析中匹配通用符號,比如標識符,關鍵字,字符串,以及數字。語法分析器的開銷比詞法分析器要大,所以我們最好不要給語法分析器太大壓力,也就是說,把數碼組合在一起並識別出數字再傳給語法分析器。

l  將語法分析器不需要區分的詞法結構合並成一個詞法結構。舉個例子,如果我們的程序處理整數和浮點數的過程是一樣的話,那么我們最好就把整數和浮點數合並一個符號:數字。在這種情況下,將分的那么細的詞法規則傳給語法分析器沒有任何意義。

l  將所有語法分析器可以等同對待的內容合並成一個內容。例如,如果我們的語法分析器並不關心一個XML標簽的內容,那么詞法分析器就應該將兩個尖括號之間的所有內容都合並成一個單獨的符號類型:標簽。

l  從另一方面來說,如果語法分析器需要量一個文本分成幾部分來處理,那么詞法分析器就應該傳遞已經分好的各元素符號給語法分析器。例如,如果語法分析器需要解析IP地址的各個元素,詞法分析器就應該傳遞IP地址的各個部分(整數和點)。

我們所說的語法分析器不需要區分某些詞法結構,或者語法分析器並不關心一個詞法結構內部組成,我們實際上是指我們最終的程序不關心這些。即我們的程序對這些詞法結構有着同樣的處理或者翻譯。

為了更好地說明預期應用是怎樣影響我們的詞法和語法識別的,我們以處理一個Web服務器上的日志文件為例,這種文件每行一條記錄。我們將通過不斷增加程序的需求來觀察詞法分析和語法分析之間的界限是怎樣變動的。假設這個日志文件每一行由請求IP,HTTP協議命令以及返回代碼組成。下面是一行日志記錄的例子:

192.168.209.85 "GET /download/foo.html HTTP/1.0" 200

這時候,會有很多詞法結構出現在我們的腦海里。然而,如果我們的程序只需要計算這個文件中一共有多少條記錄,除了換行符為終結符的序列外,我們完全可以忽略其它所有詞法結構。

file : NL+ ; // parser rulematching newline (NL) sequence

STUFF : ~'\n'+-> skip ; // match and discard anything but a '\n'

NL : '\n' // return NL to parser or other invoking code

詞法分析器並不需要識別那么多類型的結構,並且語法分析器也只需要識別由換行符結束的序列就可以了。(“~x”操作符匹配除了x外所有符號。)

接下來,我們給程序加一個需要從日志文件中識別IP地址列表的功能。這意味着我們需要一條詞法規則來匹配IP地址,同時,我們也就需要一條規則來匹配一條記錄剩下來的部分。

IP : INT '.' INT '.' INT '.' INT ; // 192.168.209.85

INT : [0-9]+ ; // match IP octet or HTTP result code

STRING: '"' .*? '"' // matches the HTTP protocol command

NL : '\n' // match log file record terminator

WS : ' ' ->skip ; // ignore spaces

然后我們通過組合完整的符號組合來讓語法分析器識別日志文件中的一條記錄。

file : row+ ; // parser rule matching rows of log file

row : IP STRING INT NL ; // match log file record

再添加一點我們程序的功能,我們需要將文本的IP地址轉換為32位整數。使用一些很方便的庫函數,例如split(‘.’),我們可以將識別到的文本IP地址傳遞給語法分析器並在語法分析器中完成分離和轉換。但是,我們最好在詞法分析過程中識別IP地址的各個部分,讓后將這些部分作為符號傳給語法分析器。

file : row+ ; // parser rule matching rows of log file

row : ip STRING INT NL ; // match log file record

ip : INT '.' INT '.' INT '.' INT ; // match IPs in parser

INT : [0-9]+ ; // match IP octet or HTTP result code

STRING: '"' .*? '"' // matches the HTTP protocol command

NL : '\n' // match log file record terminator

WS : ' ' ->skip ; // ignore spaces

從我們將詞法規則IP改為語法規則ip可以看出,這兩者之間的分割線可以很容易發生變動。(將四個INT符號轉換成32位數字需要一些嵌入到語法中的程序代碼,目前我們還不需要了解這個,所以就先忽略之。)

繼續添加功能。如果程序需要處理HTTP協議命令字符串中的內容的時候,我們將遵循類似的思維過程。如果我們的程序並不關系字符串是由哪些部分組成的,那么我們直接將整個字符串作為一個單獨的符號傳遞給語法分析器就可以了。但是,我們如果需要分離出其中具體的各個部分,最好在我們的詞法分析器中就識別這些個部分再傳遞給語法分析器。

根據程序的需求以及語言的特征來區分詞法分析和語法分析的任務並不是一件特別困難的事情。下一章中的例子能夠幫你更好地掌握這一章學到的規則。有了這些堅實的基礎之后,我們就可以嘗試一些第12章中令人頭疼的詞法問題了。例如,Java編譯器既需要忽略又需要處理Javadoc注釋,以及,XML文件對於標簽的內部和外部有着不一樣的詞法結構。

在這一章中,我們學到了如何從一些具有代表性的語言或語言手冊來創建語法偽代碼,並用ANTLR語法來形成一個正式的語法。同時,我們也學習了一些通用的語言模式:蓄力,選項,符號依賴以及嵌套短語。在詞法層次,我們學習了最通用的詞法結構的實現:標識符,數字,注釋,以及空白字符。現在,是時候該將這些知識應用到一些實際的語言上去了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM