ANTLR 4權威指南(下部9-15章)_第1頁
ANTLR 4權威指南(下部9-15章)_第2頁
ANTLR 4權威指南(下部9-15章)_第3頁
ANTLR 4權威指南(下部9-15章)_第4頁
ANTLR 4權威指南(下部9-15章)_第5頁
已閱讀5頁,還剩127頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權,請進行舉報或認領

文檔簡介

ANTLR4權威指南下部9-15章目錄\h第三部分高級特性\h第9章錯誤報告與恢復\h9.1錯誤處理入門\h9.2修改和轉發(fā)ANTLR的錯誤消息\h9.3自動錯誤恢復機制\h9.4勘誤備選分支\h9.5修改ANTLR的錯誤處理策略\h第10章屬性和動作\h10.1使用帶動作的語法編寫一個計算器\h10.2訪問詞法符號和規(guī)則的屬性\h10.3識別關鍵字不固定的語言\h第11章使用語義判定修改語法分析過程\h11.1識別編程語言的多種方言\h11.2關閉詞法符號\h11.3識別歧義性文本\h第12章掌握詞法分析的“黑魔法”\h12.1將詞法符號送入不同通道\h12.2上下文相關的詞法問題\h12.3字符流中的孤島\h12.4對XML進行語法分析和詞法分析\h第四部分ANTLR參考文檔\h第13章探究運行時API\h13.1包結構概覽\h13.2識別器\h13.3輸入字符流和詞法符號流\h13.4詞法符號和詞法符號工廠\h13.5語法分析樹\h13.6錯誤監(jiān)聽器和監(jiān)聽策略\h13.7提高語法分析器的速度\h13.8無緩沖的字符流和詞法符號流\h13.9修改ANTLR的代碼生成機制\h第14章移除直接左遞歸\h14.1直接左遞歸備選分支模式\h14.2左遞歸規(guī)則轉換\h第15章語法參考\h15.1語法詞匯表\h15.2語法結構\h15.3文法規(guī)則\h15.4動作和屬性\h15.5詞法規(guī)則\h15.6通配符與非貪婪子規(guī)則\h15.7語義判定\h15.8選項\h15.9ANTLR命令行參數(shù)第三部分高級特性在第二部分中,我們學習了如何從范例代碼和參考手冊中提取一門語言的抽象結構(句法),并使用ANTLR對該句法進行正式表述。為了開發(fā)語言類應用程序,我們編寫了一些樹監(jiān)聽器和訪問器,用它們操縱自動生成的語法分析樹。這樣,我們就掌握了使用ANTLR來高效地處理大多數(shù)問題的關鍵技巧。第三部分的主要內(nèi)容是ANTLR的高級用法。首先,我們將會學習ANTLR的自動異常處理機制。其次,我們將會探究如何在語法中直接嵌入代碼片段,以便在解析過程中實時地產(chǎn)生輸出或者執(zhí)行計算。再次,我們將會看到如何基于運行時信息,通過語義判定來動態(tài)開啟或者關閉語法中的備選分支。最后,我們將會介紹一些詞法方面的“黑魔法”。第9章錯誤報告與恢復同絕大多數(shù)軟件一樣,在我們開發(fā)一門語法的過程中,需要修復很多的錯誤。直到我們編寫完(并調試完)語法之后,生成的語法分析器才能識別所有的有效輸入語句。在這個過程中,ANTLR的錯誤消息含有豐富的信息,有助于我們調試語法中產(chǎn)生的問題。一旦擁有了正確的語法,我們就必須處理不合語法的語句,這些語句可能來源于用戶輸入,甚至是其他程序在錯誤情況下自動生成的。在上述情況下,我們的語法分析器對非法輸入的響應就會大大影響生產(chǎn)力。換句話說,無論在我們的開發(fā)過程中,還是在用戶的實際使用過程中,一個只會輸出“呃,出錯了”并且一遇到語法錯誤就退出的語法分析器毫無用處。使用ANTLR的開發(fā)者將會無償獲得它提供的優(yōu)秀的錯誤報告功能和復雜的錯誤恢復機制。ANTLR生成的語法分析器能夠自動地在遇到句法錯誤時產(chǎn)生豐富的錯誤消息,并且能在大多數(shù)情況下成功地完成重新同步。這樣的語法分析器甚至能夠保證只為每個句法錯誤產(chǎn)生一條錯誤消息。在本章中,我們將會學習ANTLR自動生成的語法分析器使用的自動錯誤報告和恢復策略。我們還將了解如何修改默認的錯誤處理機制,使之符合一些典型情況下的需求,以及如何在特定的程序中定制錯誤消息。9.1錯誤處理入門描述ANTLR的錯誤恢復策略,最好的方法是觀察一個ANTLR自動生成的語法分析器對錯誤輸入產(chǎn)生的響應。下面讓我們看一個簡單的類Java語言的語法,它的類定義中包含字段和方法,其中的方法具有簡單的語句和表達式。該語法將成為本節(jié)和本章中其余各節(jié)的例子的核心。(注:ANTLR4.3之后,原先的$stat.text需要改成$ctx.getText()。——譯者注)其中的內(nèi)嵌動作會打印出語法分析器發(fā)現(xiàn)的相應元素。出于方便和簡潔的目的,我們使用內(nèi)嵌動作來代替語法分析樹監(jiān)聽器。我們將會在第10章中學習更多有關動作的知識。首先,我們運行語法分析器,給出一些正確的輸入,觀察正常情況下的輸出。我們沒有從語法分析器中得到任何錯誤,它正常地執(zhí)行了任務并使用打印結果報告,對變量i和類定義T的識別已經(jīng)成功完成?,F(xiàn)在,讓我們試著輸入一個類,它的方法定義中包含一個非法的賦值語句。在詞法符號4處,語法分析器沒有發(fā)現(xiàn)期望的“;”,因此報告了一個錯誤。輸出的line2:19表明,有誤的詞法符號位于第2行的第20個字符處(字符位置從0開始)。因為“-gui”選項的存在,我們還可以看到一棵將錯誤節(jié)點高亮顯示(稍后會講到)的語法分析樹,如圖9-1所示。圖9-1錯誤節(jié)點高亮顯示的語法分析樹在這個例子中,輸入包含兩個多余的詞法符號,因此,語法分析器針對這樣的錯誤給出了一個通用的錯誤信息。不過,如果輸入僅有一個多余的詞法符號,語法分析器就能夠表現(xiàn)得更加智能,指出存在一個多余的詞法符號。在下面的測試中,在類名和類定義體之間存在一個多余的“;”:語法分析器在“;”處報告了一個錯誤,并且給出了一個信息量更大的結果,因為它知道“;”后面的詞法符號是自己期望看到的。這個特性叫作單詞法符號移除(single-tokendeletion),實現(xiàn)這個特性只需要語法分析器假設多余的那個詞法符號不存在,然后繼續(xù)解析過程即可。同樣,在語法分析器檢測到詞法符號缺失的時候,它也可以完成單詞法符號補全(single-tokeninsertion)。下面讓我們?nèi)サ糇詈蟮摹皚”,看看會發(fā)生什么。與編程語言理論有關的幽默二則顯然,偉大的計算機科學家NiklausWirth極富幽默感。他曾經(jīng)開玩笑說,歐洲人以“傳引用”方式稱呼他(歐洲人通常能將他的名字正確讀作“Ni-klausVirt”),美國人以“傳值”方式稱呼他(將他的名字誤讀作“Nickle-lessWorth”)。在CompilerConstruction1994會議上,KristenNygaard(Simula的發(fā)明者)講了一個故事,有一次在一門編程語言的理論課上,他說,“強類型(strongtyping)是法西斯主義”,意指自己偏好弱類型的編程語言。后來,一個學生問他,為什么用力打字(strongtyping)是法西斯主義。語法分析器報告它未能找到結尾的“}”詞法符號。另外一種常見的句法錯誤發(fā)生在語法分析器做出決策的關鍵位置,剩余的輸入文本不符合規(guī)則的任意一個備選分支。例如,如果我們在字段聲明中遺漏了變量名,member規(guī)則的兩個備選分支就都無法匹配這樣的輸入。因此,語法分析器報告沒有找到可行的備選分支。錯誤報告中的“int”和“;”之間沒有空格,這是因為,我們令詞法分析器在空白符號的WS()規(guī)則中執(zhí)行了skip()指令。如果存在詞法錯誤,ANTLR也會給出一個錯誤消息,指明它無法將一個或者多個字符匹配為詞法符號。例如,如果輸入一個完全未知的字符,我們就會得到一個詞法符號識別錯誤。由于我們沒有給出一個有效的類名,單詞法符號補全機制生成了一個missingID作為類名,這樣,類名的詞法符號就不至于為空了。如果需要控制語法分析器對這樣的詞法符號的生成機制,覆蓋DefaultErrorStrategy類中的getMissingSymbol()方法即可(參見9.5節(jié))。你可能注意到了,本節(jié)中的示例代碼顯示,盡管發(fā)生了錯誤,語法分析過程還是照常進行。除了產(chǎn)生良好的錯誤消息和利用剩余的輸入進行重新同步之外,語法分析器還必須能夠移動到合適的位置繼續(xù)語法分析過程。例如,當通過classDef規(guī)則中的子規(guī)則member匹配類成員時,語法分析器不應該在遇到非法的成員定義時結束classDef。這就是語法分析器能夠跳過錯誤的原因——一個句法錯誤不應該讓語法分析器結束當前規(guī)則。語法分析器將會盡最大可能匹配到一個合法的類定義。我們將會在9.3節(jié)深入研究這個主題。不過,首先讓我們來看看如何修改標準的錯誤報告機制,以利于調試語法和為用戶提供更恰當?shù)南ⅰ?.2修改和轉發(fā)ANTLR的錯誤消息默認情況下,ANTLR將所有的錯誤消息送至標準錯誤(standarderror),不過我們可以通過實現(xiàn)接口ANTLRErrorListener來改變這些消息的目標輸出和內(nèi)容。該接口有一個同時應用于詞法分析器和語法分析器的syntaxError()方法。syntaxError()方法接收各式各樣的信息,無論是錯誤的位置還是錯誤的內(nèi)容。它還接收指向語法分析器的引用,因此我們能夠通過該引用來查詢識別過程的狀態(tài)。例如,下列錯誤監(jiān)聽器(errorlistener)來自于文件TestE_Listener.java,能夠在通常的帶有詞法符號信息的錯誤消息后面打印出規(guī)則的調用棧:使用這種方法,我們的程序就能在語法分析器調用起始規(guī)則之前,輕易地為其增加一個錯誤監(jiān)聽器。在我們增加自定義的錯誤監(jiān)聽器之前,我們需要移除輸出目標是控制臺的內(nèi)置錯誤監(jiān)聽器,以防出現(xiàn)重復的錯誤消息。讓我們輸入一個特殊的、包含多余的類名且缺失字段名的類定義,看看現(xiàn)在的錯誤消息。其中,棧內(nèi)容[prog,classDef]顯示,語法分析器當前正處于規(guī)則classDef中,該規(guī)則是由prog調用的。注意詞法符號的信息還包括對應的詞法符號在輸入字符流中的位置。這有助于在類似開發(fā)環(huán)境之類的輸入中對錯誤進行高亮顯示。例如,詞法符號[@2,8:8='T',<9>,1:8]顯示,它是詞法符號流中的第三個(索引是2,從0開始計數(shù)),包含的字符索引由8到8,詞法符號類型為9,位于第1行第8個字符處(從0開始計數(shù),tab符看作一個字符)。通過JavaSwing技術,我們可以非常容易地將這條消息使用一個對話框來顯示,只需要修改一下syntaxError()方法即可。使用輸入classT{intinti;}來測試TestE_Dialog,可以看到如圖9-2所示的對話框。圖9-2測試TestE_Dialog對話框請看下一個例子,構建一個錯誤監(jiān)聽器TestE_Listener2.java,用下劃線標示出有問題的詞法符號,如下所示:為簡單起見,我們將忽略tab符——charPositionInLine并不是實際的列數(shù),因為tab符并沒有統(tǒng)一的寬度。下面的錯誤監(jiān)聽器實現(xiàn)用下劃線標示出了錯誤的位置,正如之前我們所看到的那樣:我們還需要了解有關錯誤監(jiān)聽器的最后一件事情。當語法分析器檢測到有歧義的輸入序列時,它會通知錯誤監(jiān)聽器。默認的錯誤監(jiān)聽器ConsoleErrorListener不會向控制臺打印任何東西。正如我們在2.3節(jié)中所看到的那樣,有歧義的輸入可能意味著我們的語法存在錯誤,語法分析器不應該因此通知用戶。下面讓我們來回顧一下該節(jié)中有歧義的語法匹配“f();”的兩種不同方式。如果我們用這個語法進行測試,我們不會看到有關歧義的警告。當語法分析器檢測到歧義發(fā)生時,如果希望得到通知,請使用addErrorListener()方法添加一個DiagnosticErrorListener的實例來告知語法分析器。此外,你還應當告訴語法分析器,你對所有的歧義警告都感興趣,而不僅僅是那些可以快速檢測到的。出于效率方面的原因,ANTLR的決策機制并不是總能發(fā)現(xiàn)所有的歧義信息。下面是令語法分析器報告所有歧義的方法:如果你在用grun命令運行TestRig,加上選項“-diagnostics”令其使用DiagnosticError-Listener替代默認的控制臺錯誤監(jiān)聽器(并打開LL_EXACT_AMBIG_DETECTION選項)即可。輸出結果顯示語法分析器還調用了reportAttemptingFullContext()。ANTLR在SLL(*)分析失敗時調用此方法,語法分析器會啟用功能更加強大的完整ALL(*)機制。詳見13.7節(jié)。在開發(fā)過程中使用上面提到的診斷錯誤監(jiān)聽器(diagnosticserrorlistener)是個好主意,因為ANTLR工具(在生成語法分析器時)不會對歧義性語法結構提出靜態(tài)警告。在ANTLR4中,只有運行狀態(tài)的語法分析器才能檢測到歧義。這就像是Java中靜態(tài)類型機制和Python中動態(tài)類型機制的差別。ANTLR4的若干項改進在ANTLR4中,有兩個與錯誤處理相關的重大改進:ANTLR的內(nèi)置錯誤恢復機制更加優(yōu)秀,同時也讓開發(fā)者能夠更加容易地修改錯誤處理策略。當Sun公司使用ANTLR3編寫JavaFX的語法分析器時,他們注意到一個放錯位置的分號會導致語法分析器在搜尋一列元素(例如通過member+定義的類成員)的過程中提前終止。現(xiàn)在,ANTLR4的語法分析器會試圖在子規(guī)則的識別之前和識別過程中進行重新同步(resynchronize),而非草草丟棄詞法符號并退出當前規(guī)則。第二項改進允許開發(fā)者按照策略模式(Strategypattern)指定自定義的錯誤處理機制。現(xiàn)在,我們已經(jīng)深入了解了ANTLR語法分析器產(chǎn)生的消息類型,以及修改和轉發(fā)它們的方法,接下來,讓我們探索一下錯誤恢復方面的知識。9.3自動錯誤恢復機制錯誤恢復指的是允許語法分析器在發(fā)現(xiàn)語法錯誤后還能繼續(xù)的機制。原則上,最好的錯誤恢復來自人類在手工編寫的遞歸下降的語法分析器中進行的干預。盡管如此,按照我的經(jīng)驗,手工編寫一個優(yōu)秀的錯誤恢復機制非常難,因為這個過程過于枯燥乏味,極易出錯。在本書描述的ANTLR最新版中,我窮盡我畢生所學,基于多年的經(jīng)驗,來為ANTLR語法提供良好的錯誤恢復機制。ANTLR的錯誤恢復機制基于NiklausWirth的早期著作【Algorithms+DataStructures=Programs[Wir78]】中的思想(以及RodneyTopor的【ANoteonErrorRecoveryinRecursiveDescentParsers[Top82]】,同時也包含JosefGrosch在他的CoCo語法分析器生成器中的優(yōu)秀思想【EfficientandComfortableErrorRecoveryinRecursiveDescentParsers[Gro90]】。下面是ANTLR將這些思想糅合在一起的實現(xiàn)細節(jié):必要情況下,語法分析器在遇到無法匹配詞法符號的錯誤時,執(zhí)行單詞法符號補全和單詞法符號移除。如果這些方案不奏效,語法分析器將向后查找詞法符號,直到它遇到一個符合當前規(guī)則的后續(xù)部分的合理詞法符號為止,接著,語法分析器將會繼續(xù)語法分析過程,仿佛什么事情都沒有發(fā)生過一樣。在本節(jié)中,我們將會看到上述術語的含義,并探究ANTLR是如何在錯綜復雜的情況下從錯誤中恢復的。下面讓我們首先分析ANTLR使用的基本錯誤恢復策略。1.通過掃描后續(xù)詞法符號來恢復當面對真正的非法輸入時,當前的規(guī)則無法繼續(xù)下去,此時語法分析器將會向后查找詞法符號,直到它認為自己已經(jīng)完成重新同步時,它就返回原先被調用的規(guī)則。我們可以稱為同步-返回(sync-and-return)策略。有人稱為“應急模式”(panicmode),不過它的表現(xiàn)相當好。語法分析器知道自己無法使用當前規(guī)則匹配當前輸入。它會持續(xù)丟棄后續(xù)詞法符號,直至發(fā)現(xiàn)一個可以匹配本規(guī)則中斷位置之后的某條子規(guī)則的詞法符號。例如,如果在賦值語句中存在一個語法錯誤,那么語法分析器的做法就非常合理:丟棄后續(xù)的詞法符號,直到發(fā)現(xiàn)一個分號或者其他的語句終結符為止。這種策略較為激進,但是十分有效。我們下面將要看到,這種基本策略作為后備方案,在啟用之前,ANTLR會試圖在規(guī)則內(nèi)部進行恢復。每個ANTLR自動生產(chǎn)的規(guī)則方法都被包裹在一個try-catch塊內(nèi),它應對語法錯誤的措施是報告該錯誤,并試圖在返回之前從該錯誤中恢復。我們將會在9.5節(jié)中看到錯誤處理策略的更多細節(jié),不過,就現(xiàn)在而言,我們可以認為recover()會持續(xù)消費詞法符號,直到發(fā)現(xiàn)重新同步集合(resynchronizationset)中的詞法符號為止。重新同步集合是調用棧中所有規(guī)則的后續(xù)符號集合(followingset)的并集。一條規(guī)則引用(rulereference)的后續(xù)符號集合是能夠立即延續(xù)該規(guī)則,從而無須離開當前規(guī)則的詞法符號集合。例如,給定一個備選分支assign';',那么規(guī)則引用assign的后續(xù)符號集合就是{';'}。如果該備選分支是assign,那么后續(xù)符號集合就是空的。有必要通過一個例子來加深對重新同步集合的理解。請看下列語法,想象一下,在每條規(guī)則的調用過程中,語法分析器都會追蹤每次規(guī)則調用的后續(xù)符號集合。請看輸入文本[1^2]對應的如圖9-3左側所示的語法分析樹:圖9-3某語法分析樹當匹配規(guī)則atom中的詞法符號1時,調用棧是[group,expr,atom](這是因為group調用了expr,后者又調用了atom)。通過查看調用棧,我們就能清楚地知道語法分析器抵達此處時,緊跟在每條被其調用的規(guī)則后面的詞法符號的集合。后續(xù)符號集合只考慮那些在當前規(guī)則中出現(xiàn)的詞法符號,因此,在運行時,我們可以只把當前調用棧對應的后續(xù)符號集合組合在一起。換句話說,我們無法同時途徑group的兩個備選分支來到規(guī)則expr處。語法F中的注釋里給出了一些后續(xù)符號集合,將它們組合在一起,我們得到了上述輸入的重新同步集合{'^',']'}。為了證明該集合是符合預期的,讓我們看看當語法分析器遇到錯誤輸入[]時會發(fā)生些什么。此時,我們會得到圖9-3中右側的語法分析樹。在atom中,語法分析器發(fā)現(xiàn)當前詞法符號]不符合atom的任意兩個備選分支之一。為了完成重新同步,語法分析器將持續(xù)消費詞法符號,直到它發(fā)現(xiàn)重新同步集合中的詞法符號為止。在本例中,當前的詞法符號]正好是重新同步集合的成員之一,因此語法分析器實際上沒有消費任何詞法符號就完成了在atom中的重新同步。在完成atom規(guī)則中的恢復過程后,語法分析器返回expr規(guī)則,但是它立即發(fā)現(xiàn)缺少^詞法符號。上述恢復過程將會重復,語法分析器持續(xù)消費詞法符號,直到發(fā)現(xiàn)expr規(guī)則的重新同步集合中的元素為止。expr規(guī)則的重新同步集合,也就是group規(guī)則的第一個備選分支中引用的expr的后續(xù)符號集合,即{']'}。再一次,語法分析器沒有消費任何東西就退出了expr規(guī)則,返回到了group規(guī)則的第一個備選分支中。現(xiàn)在語法分析器知道自己找到了expr規(guī)則引用之后的內(nèi)容——它成功地匹配到了group規(guī)則中的']',這樣,語法分析器就成功地完成了重新同步。在恢復過程中,ANTLR語法分析器會避免輸出層疊的錯誤消息(從Grosch中借鑒的思想)。即,對于每個語法錯誤,直到成功從該錯誤中恢復,語法分析器才輸出一條錯誤消息。這件事情是通過一個簡單的布爾類型的變量完成的,若該變量被置為true,當遇到語法錯誤時,語法分析器就能避免輸出進一步的錯誤,直到語法分析器成功地匹配到一個詞法符號,或者該變量被置為false為止(參見DefaultErrorStrategy類中的errorRecoveryMode字段)。緊隨其后的符號集合(FOLLOWSet)vs.后續(xù)符號集合(FollowingSet)熟悉編程語言理論的讀者可能會有疑問,atom規(guī)則的重新同步集合是否應該是緊跟在atom之后的詞法符號(用FOLLOW(atom)表示)集合,即所有能在某種上下文中緊跟在atom之后的詞法符號集合?非常不幸的是,事情沒有那么簡單,要想用在特定上下文而非全部上下文中可能跟隨在某規(guī)則之后的詞法符號集合構建重新同步集合,必須通過動態(tài)計算。FOLLOW(expr)是{')',']'},它包含了在所有可能的上下文中(group的第一條和第二個備選分支)緊跟著expr規(guī)則引用的詞法符號。很顯然,盡管如此,在運行時,語法分析器同時只能從一個位置調用expr。注意到FOLLOW(atom)是'^',如果語法分析器使用這個詞法符號而非重新同步集合{'^',']'}來進行重新同步,它可能會持續(xù)消費詞法符號,直到文件的末尾,因為輸入內(nèi)容中并沒有^。在許多情況下,ANTLR能夠更加智能地完成恢復,而不僅僅是本節(jié)中提到的“尋找重新同步集合中的符號”和“從當前規(guī)則返回”。它會盡力嘗試“修復”輸入文本并繼續(xù)相同規(guī)則。在下面幾個小節(jié)中,我們將會看到語法分析器是如何從錯誤匹配的詞法符號和子規(guī)則的錯誤中恢復的。2.從不匹配的詞法符號中恢復在語法分析的過程中,最常見的操作之一就是“匹配詞法符號”。對于語法中的每個詞法符號T,語法分析器都會調用match(T)。如果當前的詞法符號不是T,match()方法就會通知錯誤監(jiān)聽器,并試圖重新同步。為完成同步,它有三種選擇:移除一個詞法符號、補全一個詞法符號,或者簡單地拋出一個異常以啟用基本的同步-返回機制。如果能夠成功的話,移除當前的詞法符號是重新同步最容易的方法。讓我們回顧一下之前Simple語法定義的“簡單類定義語言”里的classDef規(guī)則??紤]輸入文本class9T{inti;},語法分析器會刪除9,然后繼續(xù)進行同一條規(guī)則的語法分析過程——匹配類的定義體。圖9-4展示了語法分析器在分析完class時的狀態(tài)。LA(1)和LA(2)標示出了第一個和第二個(在當前詞法符號之后的)前瞻詞法符號。match(ID)期望LA(1)是一個ID,但是它不是。不過,下一個詞法符號LA(2)是一個ID。此時,我們只需移除當前的詞法符號(將它當作干擾項),然后按照預期匹配下一個ID并退出match()方法,即可完成恢復過程。如果語法分析器無法通過移除一個詞法符號的方式重新同步,它會轉而嘗試補全一個詞法符號。假設我們忘記輸入ID,那么classDef規(guī)則看到的輸入就是class{inti;}。在匹配完class后,輸出的狀態(tài)如圖9-5所示。圖9-4語法分析器分析完class時的狀態(tài)圖9-5匹配完class后的輸出狀態(tài)語法分析器調用了match(ID),期望發(fā)現(xiàn)一個標識符,但是實際上發(fā)現(xiàn)的卻是{。在這種情況下,語法分析器知道{是自己所期望的那個詞法符號的下一個,因為在classDef規(guī)則中它位于ID之后。此時match()方法可以假定標識符已經(jīng)被發(fā)現(xiàn)并返回,這樣,下一個match('{')的調用就會成功。在忽略內(nèi)嵌動作(例如打印出類名的語句)的前提下,這種方案表現(xiàn)得相當出色。但是,如果詞法符號是null,通過$ID.text引用了缺失詞法符號的打印語句就會引起一個異常。因此,錯誤處理器會創(chuàng)建一個詞法符號,而非簡單的假定該詞法符號存在,詳情參見DefaultErrorStrategy中的getMissingSymbol()方法。新創(chuàng)建的詞法符號具有語法分析器所期望的類型,以及和當前詞法符號LA(1)相同的行列位置信息。這個新創(chuàng)建的詞法符號阻止了監(jiān)聽器和訪問器中引用缺失詞法符號時引發(fā)的異常。分析語法分析過程最容易的方法是查看語法分析樹,它展示了語法分析器識別所有詞法符號的細節(jié)。一旦遇到錯誤,語法分析樹就會用紅色高亮標注那些詞法分析器在重新同步過程中移除或者補全的詞法符號。對于輸入文本class{inti;}和Simple語法,我們得到如圖9-6所示的語法分析樹。圖9-6輸入文本class{inti;}和Simple語法后的語法分析樹同時,語法分析器執(zhí)行了內(nèi)嵌動作,成功地完成了打印而沒有拋出異常,這是由于錯誤恢復機制為$ID創(chuàng)建了一個有效的Token對象。顯然,對我們的目的而言,一個<missingID>標識符沒有任何意義,不過,至少錯誤恢復機制不會引起一堆空指針異常了。現(xiàn)在,我們已經(jīng)知道了ANTLR針對簡單的詞法符號實施的規(guī)則內(nèi)恢復機制,接下來,讓我們進一步探索它在識別子規(guī)則之前以及子規(guī)則識別過程中的錯誤恢復機制。3.從子規(guī)則的錯誤中恢復許多年前,Sun公司的JavaFX小組向我反饋,他們使用的ANTLR自動生成的語法分析器在特定情況下無法很好地從錯誤中恢復。實際情況是,語法分析器在遇到第一個錯誤時就退出了類似member+的子規(guī)則循環(huán),從而強制將同步-返回機制作用于外圍規(guī)則。例如,“varwidthNumber;”(width后面缺少冒號)這樣一個有關成員聲明的小錯誤就會令語法分析器忽略后續(xù)的全部成員。JimIdle是一個ANTLR郵件組內(nèi)的貢獻者和顧問,他提出了一種我稱為“JimIdle的魔法同步”的錯誤恢復機制。他的解決方案是:在語法中手工插入一條空規(guī)則的引用,該規(guī)則包含特定的、能夠在必要時觸發(fā)錯誤恢復的動作?,F(xiàn)在,ANTLR4會在開始處和循環(huán)條件判定處自動插入同步檢查,以避免激進的恢復機制。該方案詳情如下:子規(guī)則起始位置在任意子規(guī)則的起始位置,語法分析器會嘗試進行單詞法符號移除。不過,和詞法符號匹配不同的是,語法分析器不會嘗試進行單詞法符號補全。創(chuàng)建一個詞法符號對ANTLR來說是很困難的,因為它必須猜測多個備選分支中的哪一個會最終勝出。子規(guī)則的循環(huán)條件判定位置如果子規(guī)則是一個循環(huán)結構,即(...)*或(...)+,在遇到錯誤時,語法分析器會嘗試進行積極的恢復,使得自己留在循環(huán)內(nèi)部。在成功地匹配到循環(huán)的某個備選分支之后,語法分析器會持續(xù)消費詞法符號,直到發(fā)現(xiàn)滿足下列條件之一的詞法符號為止:(a)循環(huán)的另一次迭代(b)緊跟在循環(huán)之后的內(nèi)容(c)當前規(guī)則的重新同步集合中的元素讓我們先看看在子規(guī)則前的單詞法符號移除??紤]Simple語法的classDef規(guī)則中的member+循環(huán)結構。如果我們手誤多輸入了{,member+子規(guī)則會在進入member之前移除掉多余的那個詞法符號,詳見如圖9-7所示的語法分析樹。圖9-7移除多余詞法符號的語法分析樹下面的命令行交互過程顯示,ANTLR成功地進行了錯誤恢復,因為它正確地識別出了變量i:接下來,讓我們試著輸入一些真正雜亂無章的文本,看看member+循環(huán)能否從錯誤中恢復,繼續(xù)尋找類成員。從中可知,語法分析器進行了重新同步,留在了循環(huán)內(nèi)部,因為它識別出了變量z。語法分析器丟棄了y;;;,然后它發(fā)現(xiàn)了另外一個member的開始(即上面的條件c),于是它回到了member循環(huán)。如果輸入文本不包含“intz;”,語法分析器就會丟棄到}(上面的條件b)為止,然后退出循環(huán)。語法分析樹高亮標記了被丟棄的詞法符號,并顯示出語法分析器仍然成功地將“intz;”解釋成了一個有效的類成員,如圖9-8所示。圖9-8被丟棄的詞法符號被高亮標記的語法分析樹如果用戶輸入了一個非法的member,同時遺漏了類定義最后的},我們不希望語法分析器一直掃描到它發(fā)現(xiàn)}為止。如果這樣的話,語法分析器的重新同步過程可能會丟棄后面的整整一個類定義,來尋找缺失的}。實際上,如果語法分析器發(fā)現(xiàn)了一個滿足條件c的詞法符號,它就會停止丟棄過程,如下所示:從圖9-9所示的語法分析樹中,我們可以看出,語法分析器在它發(fā)現(xiàn)關鍵字class的時候就停止了重新同步過程。圖9-9停止了重新同步過程的語法分析樹除了詞法符號匹配和子規(guī)則匹配中的失敗,語法分析器還可能在匹配語義判定的時候失敗。4.捕獲失敗的語義判定此時此刻,我們對語義判定的學習僅僅是淺嘗輒止,不過,由于本章的主題是錯誤處理機制,在這里討論語義判定失敗時發(fā)生的事情是非常合適的。我們將在第11章中深入研究語義判定。目前,讓我們暫時將語義判定看作斷言。它們指定了一些必須在運行時為真的條件,以使得語法分析器能夠通過這些條件的驗證。如果一個判定結果為假,語法分析器會拋出一個FailedPredicateException異常,該異常會被當前規(guī)則的catch語句捕獲。語法分析器隨即報告一個錯誤,并運行通用的同步-返回恢復機制。下面讓我們看一個使用語法判定來限制向量中整數(shù)數(shù)量的例子,它與4.4節(jié)“使用語義判定改變語法分析過程”部分中的語法非常相似。ints規(guī)則匹配最多max個整數(shù)。下列測試給出的整數(shù)過多,于是我們看到了一個錯誤消息,以及錯誤恢復的過程,在這個過程中,多余的逗號和整數(shù)被丟棄了:如圖9-10所示的語法分析樹顯示,語法分析器在第五個整數(shù)處檢測到了該錯誤。圖9-10在第五個整數(shù)處檢測到錯誤的語法分析樹作為語法設計者,其中的{$i<=$max}錯誤消息對我們很有幫助,但是顯然用戶很難讀懂它。我們可以修改這條消息,通過對語義判定使用fail選項,讓它從一堆代碼變成一些可讀的文字。例如,下面是ints的修改版,通過一個動作來動態(tài)生成可讀的字符串:現(xiàn)在,在相同的輸入下,我們獲得了更好的錯誤消息。fail選項接受兩種參數(shù):雙引號包圍的字符串常量或者一個可以得到字符串的動作。如果你希望在判定失敗時執(zhí)行一個函數(shù),使用動作是極其方便的,只需在動作中調用該函數(shù)即可,例如{...}?<fail={failedMaxTest()}>。關于使用語義判定來驗證輸入有效性這件事情,還有一些需要注意的地方。在上面的向量例子中,判定的強制性針對的是句法規(guī)則,所以拋出異常并嘗試恢復是沒問題的。但是,如果我們輸入的結構在語法上是有效的,但是在語義上是無效的,這時,語義判定就不適用了。假設存在一種語言,我們可以給一個變量賦予任何除零之外的值。這意味著“assignmentx=0;”在語法上是有效的,但是在語義上是無效的。顯然,這種情況下,我們需要向用戶輸出一個錯誤,但是不應該觸發(fā)錯誤處理機制。“x=0;”在句法上是完全合法的。在某種意義上,語法分析器將會自動地從錯誤中“恢復”。下列簡單語法展示了這個問題的處理方式:如果assign規(guī)則中的判定過程拋出了一個異常,同步-返回機制表現(xiàn)出的行為就會是丟棄判定后的“;”。這種行為可能能夠正常工作,但是我們面臨的風險是不完美的重新同步。更好的解決方案是手工輸出一個錯誤,然后令語法分析器按照正確的語法繼續(xù)進行匹配。所以,相比語義判定,我們應該使用一個帶條件語句的動作?,F(xiàn)在,我們已經(jīng)看到了所有會觸發(fā)錯誤恢復機制的場景,需要指出的是,這種機制存在一個缺點??紤]到有時語法分析器在一次錯誤恢復的嘗試中不會消費任何詞法符號,這可能帶來一個后果:整個恢復過程進入一個無限循環(huán)。如果語法分析器在恢復過程中沒有消費任何詞法符號,并且回到了相同的位置,那么我們就會面臨重新開始不消費詞法符號的恢復過程的窘境。在下一節(jié)中,我們將會看到ANTLR是如何避免這個缺陷的。5.錯誤恢復機制的防護措施ANTLR的語法分析器具有內(nèi)置的防護措施,以保證錯誤恢復過程正常結束。如果我們在相同的語法分析位置,遇到了相同的輸入情況,語法分析器會在嘗試進行恢復之前強制消費一個詞法符號?;氐奖菊麻_頭的簡單Simple語法,讓我們看一個能夠觸發(fā)防護措施的例子。如果我們在字段定義中加入一個多余的int,語法分析器就會檢測到錯誤,從而嘗試進行恢復。從下面的測試中我們可以看到,在正確的重新同步前,語法分析器會多次調用recover()并嘗試重新開始語法分析。如圖9-11中的右側語法分析樹所示,classDef規(guī)則調用了三次member。其中,第一個member沒有匹配到任何內(nèi)容,第二個member匹配到了多余的int。第三次匹配member的嘗試正確地匹配到了“intx;”序列。下面讓我們詳細分析這個過程。當語法分析器檢測到第一個錯誤時,它正位于member規(guī)則中。圖9-11正確和錯誤的語法對應的語法分析樹輸入intint不適用member的任何一個備選分支,因此語法分析器執(zhí)行了同步-返回錯誤恢復策略。它輸出了第一條錯誤消息,然后開始消費詞法符號,直到發(fā)現(xiàn)當前調用棧[prog,classDef,member]對應的重新同步集合中的詞法符號為止。因為語法中classDef+和member+循環(huán)的存在,計算重新同步集合的過程稍顯復雜。在member的調用之后,語法分析器可能回到循環(huán)開頭,再次匹配一個member,或者退出當前循環(huán),匹配類定義尾部的'}'。在classDef的調用之后,語法分析器可能回到循環(huán)開頭匹配另外一個類定義,或者簡單地退出prog規(guī)則。因此,調用棧[prog,classDef,member]對應的重新同步集合就是{'int','}','class'}。此時,語法分析器發(fā)現(xiàn),不需要消費詞法符號就可以完成恢復,因為當前的輸入詞法符號int位于重新同步集合中。因此,它返回到了調用者處:classDef規(guī)則的member+循環(huán)。該循環(huán)接著嘗試匹配另一個類成員。不幸的是,由于它沒有消費任何詞法符號,語法分析器隨即在返回member時再次檢測到了錯誤(受errorRecovery標志位影響,它隱藏了重復的錯誤消息)。在第二次錯誤的恢復中,語法分析器啟用了防護措施,因為它在相同的語法分析位置遇到了相同的輸入情況。在嘗試重新同步之前,防護措施強制消費了一個詞法符號。由于int位于重新同步集合中,它沒有繼續(xù)消費第二個詞法符號。幸運的是,現(xiàn)在的情況正好是我們所期望的,因為語法分析器已經(jīng)正確地完成了重新同步。接下來的三個詞法符號代表一個有效的成員定義:“intx;”。語法分析器的控制流再次從member回到了classDef中的循環(huán)。此時,我們第三次回到了member,不過,這一次語法分析已經(jīng)能夠順利進行了。這就是ANTLR自動錯誤恢復機制的全部細節(jié)。下面讓我們學習一種手工的錯誤恢復機制,在某些情況下,它能夠更好地完成恢復工作。9.4勘誤備選分支一些語法錯誤十分常見,以至于對它們進行特殊處理是值得的。例如,開發(fā)者經(jīng)常在嵌套的函數(shù)調用后寫錯括號的數(shù)量。為了對這些情況進行特殊處理,我們只需增加一些備選分支,匹配這些常見錯誤即可。下面的語法識別單參數(shù)的函數(shù)調用,其中參數(shù)中可能包含嵌套的括號。fcall規(guī)則具有兩個所謂的勘誤備選分支(erroralternative)。這些勘誤備選分支會給ANTLR自動生成的語法分析器帶來少量的額外工作,但是不會對它形成干擾。和其他的備選分支一樣,只要輸入文本與之相符,語法分析器就會匹配到它們。在下面的例子中,我們首先輸入了一個合法的函數(shù)調用,隨后輸入了一些匹配勘誤備選分支的文本。迄今為止,我們已經(jīng)學習了相當多錯誤處理方面的知識,它們包括ANTLR語法分析器能夠產(chǎn)生的錯誤消息,以及語法分析器在多種情況下的錯誤恢復機制。我們也看到了自定義錯誤消息和將錯誤消息轉發(fā)到不同錯誤監(jiān)聽器的方法。上述所有功能都由一個對象封裝和控制,該對象指定了ANTLR的錯誤處理策略。在下一節(jié)中,我們將會詳細了解該策略的細節(jié),以便深入學習如何自定義語法分析器對錯誤的處理行為。9.5修改ANTLR的錯誤處理策略默認的錯誤處理機制表現(xiàn)出色,不過我們還是會遇到一些非典型的、需要修改默認機制的場景。首先,我們希望關閉某些默認的錯誤處理功能,它們會帶來額外的運行負擔。其次,我們可能希望語法分析器在遇到第一個語法錯誤時就退出。這種情況的例子是,當處理類似bash的命令行輸入時,從錯誤中恢復是毫無意義的。我們不能一意孤行地執(zhí)行有風險的命令,因此語法分析器可以一遇到問題就退出。欲探究錯誤處理策略,不妨查看一下ANTLRErrorStrategy接口及實現(xiàn)類DefaultError-Strategy。該類完成了全部的默認錯誤處理工作。例如,下面的語句是每個ANTLR自動生成的規(guī)則函數(shù)中的catch中的內(nèi)容:_errHandler是一個指向DefaultErrorStrategy實例的變量。reportError()方法和recover()方法實現(xiàn)了錯誤的報告和同步-返回功能。reportError()方法根據(jù)拋出的異常類型,將報告錯誤的職責委托給了另外三個方法之一。對于之前提到的第一種非典型場景:減少錯誤處理機制給語法分析器帶來的運行負擔。請看下面的代碼,它是ANTLR根據(jù)Simple語法中的member+子規(guī)則自動生成的:在某些程序中,可以假定輸入在句法上是正確的,例如網(wǎng)絡協(xié)議。在這種情況下,我們最好避免錯誤檢查和恢復帶來的負荷。我們可以通過以下方法達到這個目的:繼承DefaultErrorStrategy類,并使用一個空方法覆蓋sync()。Java編譯器通常會在后續(xù)的優(yōu)化過程中將_errHandler.sync(this)調用內(nèi)聯(lián)化,并執(zhí)行無用代碼消除。在下一個例子中,我們將會看到如何令語法分析器采取不同的錯誤處理策略。另外一種非典型場景是令語法分析器在第一個語法錯誤處退出。為了達到這個目的,我們需要覆蓋三個關鍵方法,詳情如下:出于測試的目的,我們可以復用一些樣例代碼。除了創(chuàng)建和啟動語法分析器外,我們還需要創(chuàng)建一個新的BailErrorStrategy實例,并且令語法分析器使用它來替代默認的錯誤處理策略。隨后,我們應當令它在第一個詞法錯誤處報錯并退出。要達到這個目的,只需覆蓋Lexer類中的recover方法即可。讓我們先在輸入文本的開頭插入一個#字符,以構造一個詞法錯誤??梢姡~法分析器拋出了一個異常,接管了主程序中的控制流。同時,語法分析器在第一個語法錯誤(本例中的類名缺失)處就退出了。為展示ANTLRErrorStrategy接口的靈活性,讓我們通過一個例子來圓滿結束本章的學習:修改語法分析器的錯誤報告策略。如果希望修改標準的錯誤消息“在輸入X處沒有可行的備選分支”,我們可以覆蓋reportNoViableAlternative()方法,將錯誤消息改成其他內(nèi)容。不過,請記住,如果我們需要的僅僅是改變錯誤消息輸出的位置,我們可以像9.2節(jié)做的那樣,指定一個ANTLRErrorListener。欲了解如何完全覆蓋ANTLR生成的異常捕獲代碼,請閱讀15.3節(jié)“捕獲異?!辈糠?。在本章中,我們介紹了ANTLR中重要的錯誤報告和恢復機制的全部細節(jié)。利用ANTLRErrorListener和ANTLRErrorStategy接口,我們能夠非常靈活地指定錯誤消息的輸出位置、錯誤消息的內(nèi)容以及語法分析器從錯誤中恢復的方法。在下一章中,我們會學習如何在語法中直接嵌入被稱為動作(action)的代碼片段。第10章屬性和動作在之前的學習中,我們的程序邏輯代碼都是與語法分析樹遍歷器分離的,這意味著我們的代碼總是在語法分析完成之后執(zhí)行。在接下來的幾章中我們可以看到,一些語言類應用程序需要在語法分析的過程中執(zhí)行自身的邏輯代碼。為了達到這個目的,我們需要一種手段,將代碼片段——稱為動作——直接注入ANTLR生成的代碼中。本章的第一個目標是,學習如何在語法分析器和詞法分析器中嵌入動作,并弄清楚我們可以在這些動作中放置哪些內(nèi)容。請記住,通常我們應當避免將語法和應用程序的邏輯代碼糾纏在一起。不包含動作的語法更易閱讀,也不會綁定到特定的目標語言和程序上。盡管如此,內(nèi)嵌的動作仍然是有用的,原因有如下三個:·簡便:有時,使用少量的動作,避免創(chuàng)建一個監(jiān)聽器或者訪問器會使事情變得更加簡單。·效率:在資源緊張的程序中,我們可能不想把寶貴的時間和內(nèi)存浪費在建立語法分析樹上?!卸ǖ恼Z法分析過程:在某些罕見情況下,我們必須依賴從之前的輸入流中獲取的數(shù)據(jù)才能正常地進行語法分析過程。一些語法需要建立一個符號表,以便在未來根據(jù)情況(例如一個標識符是類型還是方法)差異化地識別輸入的文本。我們已經(jīng)在第11章中探究過這樣的例子。動作就是使用目標語言(即ANTLR生成的代碼的語言)編寫的、放置在{...}中的任意代碼塊。我們可以在動作中編寫任意代碼,只要它們是合法的目標語言語句。動作的典型用法是操縱詞法符號和規(guī)則引用的屬性(attribute)。例如,我們可以讀取一個詞法符號對應的文本或者整個規(guī)則匹配的文本。通過從詞法符號和規(guī)則引用中獲取的數(shù)據(jù),我們就可以打印結果或者執(zhí)行任意計算。規(guī)則允許參數(shù)和返回值,因此我們可以在規(guī)則之間傳遞數(shù)據(jù)。我們將會通過三個例子來學習編寫語法中的動作。第一,我們會編寫一個計算器,它的功能與7.4節(jié)中的計算器相同。第二,我們會為CSV語法(見6.1節(jié))增加一些動作,以此來探索規(guī)則和詞法符號的屬性。在第三個例子中,我們將會為一門在運行期才能確定關鍵字的語言編寫一個語法,以此來學習詞法規(guī)則中的動作。是動手的時候了,下面讓我們從一個基于動作的計算器實現(xiàn)開始。10.1使用帶動作的語法編寫一個計算器讓我們通過回顧4.2節(jié)中的表達式語法來學習編寫動作。在該節(jié)中,我們利用訪問器編寫了一個能夠對表達式求值的計算器,如下所示:我們的目標是在不使用訪問器,甚至不建立語法分析樹的前提下,重新編寫一個功能相同的計算器。此外,我們還會利用一個小技巧使其具備交互功能,這意味著我們會在敲回車時獲得結果,而非在輸入結束后。相比之下,之前的所有示例都是先讀取完整的輸入文本,然后處理生成的語法分析樹。通過本節(jié),我們會習得以下技能:將生成的語法分析器放入包中、定義語法分析器的字段和方法、在備選分支中插入動作、標記語法元素以便在動作中使用,以及定義規(guī)則的返回值。1.在語法規(guī)則之外使用動作在語法規(guī)則之外,我們希望將兩種東西注入自動生成的語法分析器和詞法分析器:package/import語句以及類似字段和方法這樣的類成員。下面是一份理想化的代碼生成模板,它展示了在語法分析器這樣的自動生成的代碼中,我們希望注入代碼片段的位置。我們可以在語法中使用@header{...}來指定一段header動作代碼,使用@members{...}向生成的代碼中注入字段或者方法。在一個聯(lián)合了文法和詞法的語法中,這些具名的動作會同時應用于語法分析器和詞法分析器(ANTLR選項-package允許我們直接設定包名,而無需使用header動作)。如果需要限制一段動作代碼只出現(xiàn)在語法分析器或者詞法分析器中,我們可以使用@parser::name或者@lexer::name。下面讓我們看看我們的計算器是如何使用上述特性的。和之前相同,計算器使用到的表達式語法以一個語法聲明開始,不過,現(xiàn)在我們打算將所有生成的代碼聲明于一個特定的Java包中。此外,我們還需要導入一些標準的Java工具類。之前的計算器的EvalVistor類有一個存儲鍵值對的memory字段,它用于實現(xiàn)變量的賦值和引用。在本次實現(xiàn)中,我們會將這個字段放入members功能里。為避免語法顯得凌亂,我們還定義了一個eval()方法,用于對兩個操作數(shù)執(zhí)行相關操作。下面是完整的members動作:完成上述定義之后,讓我們看看如何在規(guī)則內(nèi)的動作中使用這些類成員。2.在規(guī)則中嵌入動作在本節(jié)中,我們將會學習在語法中嵌入動作,這些動作可以生成輸出、更新數(shù)據(jù)結構,或者設置規(guī)則的返回值。我們還會看到ANTLR是如何將規(guī)則的參數(shù)、返回值,以及規(guī)則調用的其他屬性包裝成一個ParserRuleContext子類的實例的。(1)基礎知識stat規(guī)則用于識別表達式、變量賦值語句和空行。因為我們在發(fā)現(xiàn)空行時什么都不做,所以stat規(guī)則只需要兩個動作。動作被執(zhí)行的時機是它前面的語法元素之后、它后面的語法元素之前。在本例中,動作出現(xiàn)在備選分支的末尾,因此它們會在語法分析器匹配到整個語句之后被執(zhí)行。當stat發(fā)現(xiàn)一個后面跟著NEWLINE的表達式時,它應當打印出該表達式的值;當stat發(fā)現(xiàn)一個變量賦值語句時,它就應當將該鍵值對存儲到memory字段中。這些動作代碼中唯一陌生的語法是$e.v和$ID.text。通常,$x.y是指元素x的y屬性,其中x可以是詞法符號引用或者規(guī)則引用。在這里,$e.v指的是調用規(guī)則e的返回值(稍后我們會看到為什么它被稱為v)。$ID.text指的是ID詞法符號匹配到的文本。如果ANTLR無法識別y屬性,它就不會轉換該屬性。在本例中,text是一個詞法符號的已知屬性,所以ANTLR將它轉換為了getText()。我們還可以使用$ID.getText()來達到相同效果。有關規(guī)則和詞法符號的屬性的完整列表,請參閱15.4節(jié)。回到規(guī)則e,讓我們來看看內(nèi)嵌在其中的動作。我們的初衷是通過直接向語法中插入代碼片段,即動作,來模擬EvalVisitor的功能。(注:根據(jù)原書勘誤表,此處原文有誤,已修正。——譯者注)這個例子中有許多引人入勝的細節(jié)。我們發(fā)現(xiàn)的第一個細節(jié)是它指定了一個整數(shù)類型的返回值v。這就是之前stat的動作中引用$e.v的原因。ANTLR的返回值和Java的返回值的差異在于,我們需要為它們命名,并且可以有多個返回值。接下來,我們看到了規(guī)則引用e和運算符子規(guī)則上的標記,如op=('*'|'/')。標記可以指向一個詞法符號,也可以指向在匹配詞法符號或規(guī)則過程中生成的ParserRuleContext對象。在詳細分析動作的內(nèi)容之前,有必要了解一下ANTLR存儲諸如返回值和標記這樣的信息的位置。在進行源代碼級別的調試時(source-leveldebug),擁有這些知識會使得ANTLR自動生成的代碼更易理解。(2)將一切打包成一個規(guī)則上下文對象在2.4節(jié)中,我們已經(jīng)了解到,ANTLR通過規(guī)則上下文對象(rulecontextobject)來實現(xiàn)語法分析樹的節(jié)點。每次規(guī)則調用都會新建并返回一個規(guī)則上下文對象,它存儲了相應規(guī)則在輸入流的特定位置上進行識別工作的所有重要信息。例如,規(guī)則e新建并返回EContext對象。自然地,規(guī)則上下文對象非常適合放置與特定規(guī)則相關的數(shù)據(jù)實體。EContext的第一部分如下所示:標記總是會成為規(guī)則上下文對象的成員,但是ANTLR并不總是為類似ID、INT和e的備選分支元素生成字段。ANTLR只有在它們被語法中的動作引用時才為它們生成字段(例如e中的動作)。ANTLR會盡可能地減少上下文對象中字段的數(shù)量。現(xiàn)在,我們的計算器的各部分都已就緒,讓我們一起分析一下規(guī)則e的備選分支中動作的內(nèi)容。(3)計算返回值e中的所有動作都通過賦值語句“$v=...;”來設置返回值。該語句雖然設置了返回值,但是并不會導致對應的規(guī)則函數(shù)返回(不要在動作中使用return語句,它會使語法分析器崩潰)。下面是開頭兩個備選分支使用的動作:這段代碼計算子表達式的值并將其賦給了e的返回值。eval()方法的參數(shù)是兩個e引用的返回值$a.v和$b.v,以及當前備選分支匹配到的運算符類型$op.type。$op.type必然是某個算術運算符的詞法符號類型。注意我們可以重復使用同一個標記(只要它們指向相同類型的對象)。因此,第二個備選分支重復使用了標記a、b和op。第三個備選分支的動作中使用$INT.int來訪問INT詞法符號匹配到的文本對應的整數(shù)。它僅僅是Integer.valueOf($INT.text)的簡寫。這些內(nèi)嵌的動作比等價的訪問器方法visitInt()要簡單得多(代價是使程序的邏輯代碼和語法糾纏在了一起)。第四個備選分支識別一個變量的引用,如果在此之前它的值已經(jīng)被存儲過,就將e的返回值設置為該變量在memory中的值。這段動作代碼使用了Java的?:運算符,不過我們也能輕易地將它改寫成if-else的形式。我們可以在動作中放入任何東西,只要它們能在Java方法中正常工作即可。最后一個備選分支中的$v=$e.v;動作將返回值設為括號中的表達式的值。在這里,我們僅僅是傳遞了一個返回值而已。(3)的值就是3。以上就是全部的語法和動作。下面讓我們學習編寫計算器的交互部分。(4)編寫一個交互式的計算器在探索交互工具的細節(jié)之前,讓我們先熟悉一下該語法和Calc.java的構建和測試過程。由于在header動作中加入了語句“packagetools;”,我們需要將生成的Java代碼放入一個名為tools的目錄(它反映了Java標準下的包名和目錄結構之間的關系)。這意味著,我們需要在tools目錄中運行ANTLR,或者在它的上級目錄中指定路徑tools/Expr.g4來運行。下面我們使用Calc的全限定類名來試一下。你會注意到,當你敲回車的時候,計算器立刻給出了結果。在默認情況下,ANTLR會讀取全部的輸入(通常是讀入一個巨大的緩沖區(qū)),為達到上述目的,我們必須將輸入文本一行一行地傳遞給它,以使得這個過程變成交互式的。每行代表一個完整的表達式(如果需要處理可以分為多行的表達式,參見12.2節(jié)“有趣的Python換行符”部分)。下面的main()方法是我們獲得第一個表達式的方式:為在不同的表達式之間共享memory字段的值,我們需要用同一個語法分析器實例處理所有的輸入行。當我們讀入一行時,我們需要新建一個詞法符號流,將其傳給共享的語法分析器。現(xiàn)在,我們已經(jīng)掌握了編寫交互式工具的方法,并且清楚了如何編寫和使用內(nèi)嵌動作。我們的計算器使用一段header動作指定了包名,同時使用members動作為語法分析器定義了兩個類成員。我們將規(guī)則內(nèi)的動作用作處理詞法符號和規(guī)則屬性的函數(shù),從而能夠計算和返回子表達式的值。在下一節(jié)中,我們會看到更多的屬性,了解更多放置動作的可行位置。10.2訪問詞法符號和規(guī)則的屬性讓我們以6.1節(jié)中的CSV語法為基礎,學習一些與動作相關的特性。我們會編寫一個程序,解析并打印CSV文件中的數(shù)據(jù),它會為每行生成一個從列名到字段值的Map。我們的目的是學習更多有關規(guī)則動作和屬性的知識。首先,讓我們看看如何使用locals區(qū)域(section)定義局部變量。經(jīng)過定義參數(shù)和返回值后,locals區(qū)域中的聲明就會成為規(guī)則上下文對象的字段。由于我們在每次規(guī)則調用時都會獲得一個新的規(guī)則上下文,可以預料,我們同時也獲得了locals的一份新拷貝。下面這個版本的file規(guī)則帶有參數(shù),包含很多有趣的細節(jié),不過,讓我們首先重點關注一下locals到底可以做什么。file規(guī)則定義了一個局部變量i,并且使用動作代碼$i++來統(tǒng)計當前輸入的行數(shù)。引用局部變量時請不要忘了$前綴,否則編譯器會報告變量未定義的錯誤。ANTLR將$i轉換成_localctx.i;在file規(guī)則對應的規(guī)則函數(shù)中,實際上是不存在局部變量i的。接下來,讓我們看看對row規(guī)則的調用。規(guī)則調用row[$hdr.text.split(",")]顯示,我們使用方括號而非圓括號來向規(guī)則傳遞參數(shù)(圓括號已經(jīng)被ANTLR的子規(guī)則語法占用了)。參數(shù)表達式$hdr.text.split(",")將hdr規(guī)則匹配到的文本切分為一組row規(guī)則所需的字符串。讓我們分別理解這件事情。$hdr是對唯一的hdr規(guī)則調用的引用,它指向本次調用的HdrContext對象。在本例中,我們無需對hdr規(guī)則引用進行標記(如h=hdr)的原因是$hdr是獨一無二的。因此,$hdr.text就是標題行匹配到的文本。我們使用標準的Java方法String.split()將逗號分隔的標題列切分為一組字符串。我們稍后會看到row規(guī)則接收一個字符串數(shù)組作為參數(shù)。對row的調用也引入了一種新的標記,即+=而非=標記符。相比之下,=用于跟蹤單個值,而這里的標記rows是所有的row調用返回的RowContext對象的List。在打印出rows的數(shù)量后,file規(guī)則中最后的動作代碼通過一個循環(huán)遍歷了所有的RowContext對象。在循環(huán)的每次迭代中,它都打印出row規(guī)則調用匹配到的詞法符號的索引值范圍(使用getSourceInterval()方法)。循環(huán)使用了r,而非$r,這是因為r是一段Java代碼中的局部變量。ANTLR只能看到locals關鍵字定義的局部變量,而無法看到用戶編寫的任意內(nèi)嵌動作中的局部變量。它們之間的差別在于,file規(guī)則對應的語法分析樹節(jié)點只會定義字段i,而不會定義字段r?,F(xiàn)在轉到hdr規(guī)則,在該規(guī)則中,我們僅僅打印出標題行的內(nèi)容。我們可以通過$hdr.text來完成這項工作,它就是row規(guī)則引用匹配到的文本。另外,我們也可以直接用$text獲得當前的規(guī)則匹配到的文本。在本例中,它同時也是row規(guī)則匹配到的文本,因為它們二者包含的內(nèi)容是相同的?,F(xiàn)在讓我們使用row規(guī)則中的動作,將每行數(shù)據(jù)轉換成一個從列名到列值的Map。首先,row接收一組列名作為參數(shù),返回一個Map。其次,為了在列名組成的數(shù)組中移動,我們需要一個局部變量col。在解析該行數(shù)據(jù)之前,我們需要初始化返回的Map,另外,讓我們再找個樂子,row結束后打印出Map里的值。上述內(nèi)容組成了該規(guī)則的頭部。init動作發(fā)生在對應規(guī)則匹配過程開始之前,無論它有多少個備選分支。同樣,after動作發(fā)生在對應規(guī)則的備選分支之一完成匹配之后。在這個例子中,我們將打印語句置于row規(guī)則的最外層備選分支的末尾,以闡明after動作的功能。當一切就緒后,我們就可以提取數(shù)據(jù)并填充該Map了。這段動作的主要部分通過$values.put(...)將列名對應字段的值存儲了結果map中。這個方法的第一個參數(shù)是這樣得到的:獲得列名,將索引值增一,然后使用$columns[$col++].trim()移除列名兩側的空白。第二個參數(shù)通過$field.text.trim()移除掉最近一次匹配到的字段文本兩側的空白(row中的兩段動作代碼是完全相同的,所以最好將它們重構為members動作中的一個方法)。CSV.g4中的其他內(nèi)容我們都已經(jīng)很熟悉了,所以這里不再贅述,直接進行它的構建和測試過程。因為grun的存在,我們無須為其編寫特殊的測試,可以只生成語法分析器并編譯之。下面是我們使用的CSV數(shù)據(jù):下面是輸出結果:hdr規(guī)則打印出了上面的第一行輸出,然后三次對row的調用打印出了三行values=...。此時,程序的控制流程回到了file規(guī)則,它的動作打印出了總行數(shù)以及每行數(shù)據(jù)包含的詞法符號位置范圍。至此,我們已經(jīng)掌握了內(nèi)嵌動作的用法,無論是在規(guī)則內(nèi)部還是規(guī)則外部。此外,我們還學習了些許有關規(guī)則屬性的知識。不過,計算器和CSV處理器的例子都只在文法規(guī)則中使用了動作。實際上,動作在詞法規(guī)則中也可以大放光彩。我們將會在下一節(jié)中通過處理巨量和動態(tài)的關鍵字集合,來學習與之相關的知識。10.3識別關鍵字不固定的語言為探究內(nèi)嵌在詞法規(guī)則中的動作的相關知識,讓我們?yōu)橐婚T虛擬的、關鍵字會動態(tài)變化(每次運行都不同)的編程語言編寫一份語法。這件事情聽上去不可思議,但確實是可能的。例如,Java5新增了一個關鍵字enum,因此同一個編譯器必須能夠根據(jù)“-version”選項動態(tài)地開啟和關閉它。也許,更常見的應用是處理擁有巨量關鍵字集合的語言。我們可以令詞法分析器分別匹配所有的關鍵字(作為獨立的規(guī)則),也可以編寫一條ID規(guī)則作為分發(fā)器,然后在一個關鍵字列表中查找該規(guī)則匹配到的標識符。如果詞法分析器發(fā)現(xiàn)該標識符是一個關鍵字,我們可以它的詞法符號類型從原先通用的ID類型改成相應的關鍵字類型。在著手實現(xiàn)ID規(guī)則和關鍵字查找機制之前,讓我們先來編寫包含關鍵字引用的語句規(guī)則。雖然ANTLR會隱式地為每個關鍵字(BEGIN、END等)定義一個詞法符號類型。但是,它會警告我們,這些詞法符號類型沒有對應的詞法定義。為關閉這個警告,我們需要進行顯式定義。在生成的KeywordsParser中,ANTLR定義的詞法符號類型像是這樣:既然我們已經(jīng)完成了詞法符號類型的定義,讓我們看看這份語法的聲明和header動作,它導入了Map和HashMap。我們將會使用一個Map存放從關鍵字到其整數(shù)詞法符號類型的映射作為關鍵字表。另外,我們還使用內(nèi)聯(lián)的Java實例初始化語句(內(nèi)層的花括號中的代碼)定義了一個Map。這一切準備就緒之后,讓我們開始進行之前做過多次的匹配標識符的工作,不過,這次我們使用了一段動作代碼來將詞法符號類型設置為恰當?shù)闹怠T谶@里,我們使用了Lexer類的getText()方法來獲取當前詞法符號的文本內(nèi)容。我們根據(jù)它的文本內(nèi)容來確定它是否存在于keywords中。如果存在,那么我們就將該詞法符號的類型從ID重置為相應關鍵字的詞法符號類型。在和詞法分析器打交道時,我們需要清楚如何修改一個詞法符號的文本內(nèi)容。它可以用于剝離字符常量或者字符串常量兩側的引號。通常,一個語言類應用程序只需要引號中的文本。下面是使用setText()覆蓋一個詞法符號中的文本的方法:如果我們想要做一些真正瘋狂的事情,我們甚至可以用setToken()方法指定詞法分析器返回的Token對象。這是一種返回自定義的詞法符號的方式。另外一種方式是覆蓋Lexer的emit()方法。至此我們已經(jīng)準備就緒,可以開始體驗這門微型語言了。我們期望的行為是它能將關鍵字和常規(guī)的標識符區(qū)分開,換句話說,“x=34;”應是合法的,但是“if=34;”不是,因為if是一個關鍵字。讓我們運行ANTLR,編譯生成的代碼,然后使用合法的賦值語句測試它。沒問題,沒有任何錯誤發(fā)生。但是,當試圖輸入將if用作標識符的賦值語句時,語法分析器給出了一個語法錯誤。同時,它也能接受合法的if語句而不輸出任何錯誤。如果你很不幸,正在為一門在某些上下文中允許將關鍵字當作標識符的編程語言構建語法分析器,請參閱12.2節(jié)中“關鍵字作為標識符”部分。相比語法分析器,詞法分析器需要動作的情況較少,不過在諸如需要修改詞法符號類型或者文本的特定場景下,它們?nèi)匀幌喈斢杏?。除了在對輸入文本進行詞法分析時執(zhí)行動作之外,另一種修改詞法符號本身的方法是查看詞法分析后的詞法符號流。在本章中,我們學習了使用動作在語法中嵌入程序邏輯代碼的方法,這些動作可以位于規(guī)則內(nèi),也可以位于規(guī)則外,通過header和members發(fā)揮作用。我們也看到了如何定義和引用規(guī)則的參數(shù)和返回值。隨后,我們還使用了詞法符號的屬性,如text和type。合在一起,這些與動作相關的特性使我們能夠自定義ANTLR生成的代碼。再次提醒,應盡可能地避免使用語法中的動作,因為它將一份語法綁定到了特定的目標編程語言上。不僅如此,動作還將語法綁定到了一個特定的程序上。不過,你可能并不在乎這件事情,因為你的公司一直以來使用的都是同一門語言,你的語法也只適用于特定的程序。在這樣的情況下,基于簡便或者效率(省去了建立語法分析樹的開銷)方面的原因,在語法中直接嵌入動作就變得非常有意義了。最重要的是,一些語法分析問題需要運行時的測試才能正確識別輸入文本。在下一章中,我們將會研究一種名為語義判定的任意布爾表達式,它可以動態(tài)地開啟或者關閉某些備選分支。第11章使用語義判定修改語法分析過程在上一章中,我們學習了如何在語法中嵌入動作,以便在語法分析的過程中執(zhí)行應用的相關邏輯。無論如何,這些動作代碼都不會影響語法分析器的語法分析過程,就好像記錄日志的語句不會影響外圍程序一樣。我們的內(nèi)嵌動作僅僅是計算一些值或者打印結果。但是,在一些罕見情況下,使用內(nèi)嵌動作來修改語法分析過程是正確識別某些編程語言語句的唯一方法。在本章中,我們將會學習一種特殊的動作{...}?,稱為語義判定,它允許我們在運行時選擇性地關閉部分語法。判定本身是布爾表達式,它會減少語法分析器的在語法分析過程中可選項的數(shù)量。一個令人難以置信的事實是,適當?shù)販p少可選項的數(shù)量會增強語法分析器的性能!語義判定可以在兩種常見的情況下發(fā)揮作用。第一,我們可能需要語法分析器處理同一門編程語言稍有差異的多個版本(方言)。例如,數(shù)據(jù)庫供應商的SQL語法會隨著時間演進。為了為這樣的供應商編寫數(shù)據(jù)庫的前端模塊,我們需要支持同一種SQL語言的不同版本。與之相似,Gnu的C編譯器——gcc——需要處理ANSIC和自身提供的方言,這些方言提供了一些擴展,例如很好的“動態(tài)goto”特性。語義判定允許我們通過命令行參數(shù)或者其他動態(tài)機制,在運行時選擇所使用的方言。第二個應用場景包括處理語法的歧義性(已在2.2中討論過)。在某些編程語言中,相同的語法結構具有不同的含義,此時判定機制提供了一種方法,讓我們能夠在對相同輸入文本的不同解釋中做出選擇。例如,在古老的Fortran語言中,f(i)既可以是數(shù)組引用,也可以是函數(shù)調用,取決于f的定義是什么。這種情況下,兩種語義的語法是相同的。編譯器必須在符號表中查找該標識符,才能對輸入作出正確解釋。語義判定給我們提供了這樣一種途徑:我們能夠基于符號表來“關閉”對輸入文本作出的錯誤解釋。這使得語法分析器別無選擇,只能采用正確的解釋。我們將通過Java和C++中的一些例子學習語義判定。隨后,我們會深入研究它的細節(jié),你也可以閱讀15.7參考章節(jié),其中包含了對細節(jié)的討論。掌握了內(nèi)嵌動作和語義判定,我們就能夠胸有成竹地處理下一章的語言識別難題了。11.1識別編程語言的多種方言首先,我們會學習如何使用語義判定來關閉Java語法中的一

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經(jīng)權益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責。
  • 6. 下載文件中如有侵權或不適當內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論