Antlr4入门(三)如何编写语法文件
本章我們將會(huì)學(xué)習(xí)詞法及語法規(guī)則,以及四種抽象的計(jì)算機(jī)語言模式。因?yàn)锳NTLR的語法規(guī)則跟正則表達(dá)式是很類似的,所以還是推薦先閱讀下正則表達(dá)式的相關(guān)內(nèi)容,這樣在編寫語法文件時(shí)可以事半功倍。
一、四種語言模式
雖然在過去的50年里人們發(fā)明了許許多多的編程語言,但是,相對(duì)而言,基本的語言模式種類并不多。之所以如此,是因?yàn)槿藗冊(cè)谠O(shè)計(jì)編程語言的時(shí)候,傾向于將它們?cè)O(shè)計(jì)成與腦海中的自然語言相類似。我們希望符號(hào)按照有效的順序排列,并且符號(hào)之間擁有著特定的依賴關(guān)系。舉個(gè)例子,{(}) 就是不符合語法的,因?yàn)榉?hào)的順序不對(duì)。
單詞之間的順序和依賴性約束是來自于自然語言的,基本上可以總結(jié)成四種抽象的計(jì)算機(jī)語言模式。
二、通配符(更多見正則表達(dá)式)
常用的通配符如下所示:
下面通過識(shí)別一些常見的詞法符號(hào)來學(xué)習(xí)下通配符的用法:
1. 關(guān)鍵字、運(yùn)算符和標(biāo)點(diǎn)符號(hào):對(duì)于關(guān)鍵字、運(yùn)算符和標(biāo)點(diǎn)符號(hào),我們無須聲明詞法規(guī)則,只需在語法規(guī)則中直接使用單引號(hào)將他們括起來即可,比如 'while'、'+'。
2. 標(biāo)識(shí)符:一個(gè)基本的標(biāo)識(shí)符就是一個(gè)由大小寫字母組成的字符序列。需要注意的是,下面的ID規(guī)則也能夠匹配關(guān)鍵字(比如‘while’)等,上章中我們查看了Parser代碼,知道ANTLR是如何處理這種歧義性的——選擇所有匹配的備選分支中的第一條。因此,ID標(biāo)識(shí)符應(yīng)該放在關(guān)鍵字等定義之后。
// 匹配一個(gè)或者多個(gè)大小寫字母 ID : [a-zA-Z]+;3. 整數(shù):整數(shù)是包括正數(shù)和負(fù)數(shù)的不以零開頭的數(shù)字。
// 匹配一個(gè)整數(shù) INTEGER : '-'?[1-9][0-9]*| '0';4. 浮點(diǎn)數(shù):一個(gè)浮點(diǎn)數(shù)以一列數(shù)字為開頭,后面跟著一個(gè)小數(shù)點(diǎn),然后是可選的小數(shù)部分。浮點(diǎn)數(shù)的另外一個(gè)格式是,以小數(shù)點(diǎn)開頭,后面是一串?dāng)?shù)字?;谝陨隙x,我們可以得到以下詞法規(guī)則
FLOAT : DIGIT+ '.' DIGIT* // 1.39、3.14159等| '.' DIGIT+ // .12 (表示0.12);fragment DIGIT : [0-9]; // 匹配單個(gè)數(shù)字這里我們使用了一條輔助規(guī)則DIGIT,將一條規(guī)則聲明為fragment可以告訴ANTLR,該規(guī)則本身不是一個(gè)詞法符號(hào),它只會(huì)被其他的詞法規(guī)則使用。這意味著在語法規(guī)則中不能引用它。這也是一條片段規(guī)則(fragment rule)。
5. 字符串常量:一個(gè)字符串就是兩個(gè)雙引號(hào)之間的任意字符序列。
// 匹配"……"之間的任意文本 STRING : '"' .*? '"';點(diǎn)號(hào)通配符(.)匹配任意的單個(gè)字符,.* 表示匹配零個(gè)或多個(gè)字符組成的任意字符序列。顯然,這是個(gè)貪婪匹配,它會(huì)一直匹配到文件結(jié)束,為解決這個(gè)問題,ANTLR通過標(biāo)準(zhǔn)正則表達(dá)式的標(biāo)記(?后綴)提供了對(duì)非貪婪匹配子規(guī)則(nongreedy subrule)的支持。
非貪婪匹配的基本含義是:獲取一些字符,直到發(fā)現(xiàn)匹配后續(xù)子規(guī)則的字符為止。更準(zhǔn)確的描述是,在保證整個(gè)父規(guī)則完成匹配的前提下,非貪婪的子規(guī)則匹配數(shù)量最少的字符。
回到我們的字符串常量定義中來,這里的定義其實(shí)并不完善,因?yàn)樗辉试S其中出現(xiàn)雙引號(hào)。為了解決這個(gè)問題,很多語言都定義了以 \ 開頭的轉(zhuǎn)義序列,因此我們可以使用 \" 來對(duì)字符串中的雙引號(hào)進(jìn)行轉(zhuǎn)義。
STRING : '"' (ESC|.)*? '"'; // 表示\" 或者 \\ fragment ESC : '\\"' | '\\\\';其中,ANTLR語法本身需要對(duì)轉(zhuǎn)義字符 \ 進(jìn)行轉(zhuǎn)義,因此我們需要 \\ 來表示單個(gè)反斜杠字符。
6. 注釋和空白字符:對(duì)于注釋和空白字符,大多數(shù)情況下對(duì)于語法分析器是無用的(Python是一個(gè)例外,它的換行符表示一條命令的終止,特定數(shù)量的縮進(jìn)指明嵌套的層級(jí)),因此我們可以使用ANTLR的skip指令來通知詞法分析器將它們丟棄。
// 單行注釋(以//開頭,換行結(jié)束) LINE_COMMENT : '//' .*? '\r'?'\n' -> skip; // 多行注釋(/* */包裹的所有字符) COMMENT : '/*' .*? '*/' -> skip;詞法分析器可以接受許多 -> 操作符之后的指令,skip只是其中之一。例如,如果我們需要在語法分析器中對(duì)注釋做一定處理,我們可以使用channel指令將某些詞法符號(hào)送入一個(gè)“隱藏的通道”并輸送給語法分析器。
大多數(shù)編程語言將空白符看成是詞法符號(hào)間的分隔符,并將他們忽略。
// 匹配一個(gè)或者多個(gè)空白字符并將他們丟棄 WS : [ \t\r\n]+ -> skip;至此,我們已經(jīng)學(xué)會(huì)了通配符的用法和如何編寫常見的詞法規(guī)則,下面我們將學(xué)習(xí)如何編寫語法規(guī)則。
三、語法
語法(grammar)包含了一系列描述語言結(jié)構(gòu)的規(guī)則。這些規(guī)則不僅包括描述語法結(jié)構(gòu)的規(guī)則,也包括描述標(biāo)識(shí)符和整數(shù)之類的詞匯符號(hào)(詞法符號(hào)Token)的規(guī)則,即包含詞法規(guī)則和語法規(guī)則。注意:語法分析器的規(guī)則必須以小寫字母開頭,詞法分析器的規(guī)則必須以大寫字母開頭。
1. 語法文件聲明
語法由一個(gè)為該語法命名的頭部定義和一系列可以互相引用的語言規(guī)則組成。grammar關(guān)鍵字用于語法文件命名,需要注意的是,命名須與文件名一致。
2. 語法導(dǎo)入
前兩章的例子中,我們都是將詞法規(guī)則和語法規(guī)則放在一個(gè)語法文件中,然而一個(gè)優(yōu)雅的寫法是將詞法規(guī)則和語法規(guī)則進(jìn)行拆分。lexer grammar關(guān)鍵字用于聲明一個(gè)詞法規(guī)則文件。如下是一個(gè)通用的詞法規(guī)則文件定義。
// 通用的詞法規(guī)則,注意是 lexer grammar lexer grammar CommonLexerRules; // 匹配標(biāo)識(shí)符(+表示匹配一次或者多次) ID : [a-zA-Z]+; // 匹配整數(shù) INT : [0-9]+; // 匹配換行符(?表示匹配零次或者一次) NEWLINE : '\r'?'\n'; // 丟棄空白字符 WS : [ \t]+ -> skip;然后我們只需要import關(guān)鍵字,就可以輕松的將詞法規(guī)則進(jìn)行導(dǎo)入。如下是一個(gè)計(jì)算器的語法文件。
grammar LibExpr; // 引入 CommonLexerRules.g4 中全部的詞法規(guī)則 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;// 為上訴語法中使用的算術(shù)符命名 MUL : '*'; DIV : '/'; ADD : '+'; SUB : '-';3. 備選分支命名(標(biāo)簽)
如果備選分支上面沒有標(biāo)簽,ANTLR就只會(huì)為每條規(guī)則生成一個(gè)方法(監(jiān)聽器和訪問器中的方法,用于對(duì)不同的輸入進(jìn)行不同的操作)。為備選分支添加一個(gè)標(biāo)簽,我們只需要在備選分支的右側(cè),以?#?開頭,后面跟上任意的標(biāo)識(shí)符即可。如上所示。需要注意的是,為一個(gè)規(guī)則的備選分支添加標(biāo)簽,要么全部添加,要么全部不添加。
4. 優(yōu)先級(jí)
在第二章中我們講述了ANTLR是如何處理歧義性語句(二義性文法)的:選擇所有匹配的備選分支中的第一條。即ANTLR通過優(yōu)先選擇位置靠前的備選分支來解決歧義性問題,這也隱式地允許我們指定運(yùn)算符優(yōu)先級(jí)。例如,在上訴的例子中,乘除的優(yōu)先級(jí)會(huì)比加減高。因此,ANTLR在解決1+2*3的歧義問題時(shí),會(huì)優(yōu)先處理乘法。
5. 結(jié)合性
默認(rèn)情況下,ANTLR是左結(jié)合的,即將運(yùn)算符從左到右地進(jìn)行結(jié)合。但是有些情況下,比如指數(shù)運(yùn)算符是從右向左結(jié)合的。1^2^3應(yīng)該是3^(2^1)而不是(3^2)^1。我們可以使用assoc來手動(dòng)指定結(jié)合性。
expr : expr '^' <assoc=right> expr // ^ 是右結(jié)合的| INT;注意,在ANTLR4.2之后,<assoc=right>需要放在備選分支的最左側(cè),否則會(huì)收到警告。
expr : <assoc=right> expr '^' expr // ^ 是右結(jié)合的| INT;6. 詞法分析器與語法分析器的界限
由于ANTLR的詞法規(guī)則可以使用遞歸,因此從技術(shù)角度上看,詞法分析器可以和語法分析器一樣強(qiáng)大。這意味著我們甚至可以在詞法分析器中匹配語法結(jié)構(gòu)?;蛘?#xff0c;在另一個(gè)極端,我們可以把字符當(dāng)作詞法符號(hào),然后使用語法分析器去分析整個(gè)字符流(這種被稱為無掃描的語法分析器scannerless parser)。因此,我們需要去界定詞法分析器和語法分析器具體需要處理的界限。
- 在詞法分析器中匹配并丟棄任何語法分析器無須知曉的東西。例如,需要在詞法分析器中識(shí)別和扔掉像空格和注釋諸如此類的東西。否則,語法分析器必須經(jīng)常查看是否有空格或注釋在詞法符號(hào)之間。
- 在詞法分析器中匹配諸如標(biāo)志符、關(guān)鍵字、字符串和數(shù)字這樣的常用記號(hào)。語法分析器比詞法分析器有更多的開銷,因此我們不必讓語法分析器承受把數(shù)字放在一起識(shí)別成整數(shù)的負(fù)擔(dān)。
- 將語法分析器不需要區(qū)分的詞法結(jié)構(gòu)歸為同一個(gè)詞法符號(hào)類型。例如,如果我們的應(yīng)用把整數(shù)和浮點(diǎn)數(shù)當(dāng)作同一事物對(duì)待,那就把它們合并成詞法符號(hào)類型NUMBER。
- 將任何語法分析器可以以相同方式處理的實(shí)體歸為一類。例如,如果語法分析器不在乎XML標(biāo)簽里的內(nèi)容,詞法分析器可以把尖括號(hào)中的所有東西合并成一個(gè)單獨(dú)的名為TAG的詞法符號(hào)類型。
- 另一方面,如果語法分析器需要把一種類型的文本拆開處理,那么詞法分析器就應(yīng)該將它的各個(gè)組成部分作為獨(dú)立的詞法符號(hào)輸送給語法分析器。例如,如果語法分析器需要處理IP地址中的元素,那么詞法分析器應(yīng)該將IP的各個(gè)組成部分(整數(shù)和點(diǎn))作為獨(dú)立的詞法符號(hào)送入語法分析器。
后記
本章我們學(xué)習(xí)了如何編寫語法文件,但是單獨(dú)的語法并沒有用處,而與其相關(guān)的語法分析器僅能告訴我們輸入的語句是否遵循該語言的規(guī)范。為了構(gòu)建一個(gè)語言類應(yīng)用程序,這是不夠的,我們還需要相應(yīng)的“動(dòng)作”去執(zhí)行語法規(guī)則。而這就是下一章的內(nèi)容——監(jiān)聽器和訪問器。
?
總結(jié)
以上是生活随笔為你收集整理的Antlr4入门(三)如何编写语法文件的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阿里巴巴总裁马云语录
- 下一篇: matlab 读二值图片,二值图像