這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文在此。
————翻譯分隔線————
編譯器(9)-解析
第一部分:介紹
第二部分:編譯、轉譯和解釋
第三部分:編譯器設計概覽
第四部分:語言設計概述
第五部分:Calc 1 語言規格說明書
第六部分:標識符
第七部分:掃描
第八部分:抽象文法樹
長征已經走了很遠。我們概覽了掃描和抽象文法樹的基本概念。現在終於可以向著解析前進。
如果你已經開始與概念點不停的鬥爭,那麼我需要警告你,從現在開始會變得越來越難。解析可能是你腦袋裡已有的概念中最難的部分。我們將處理掃描器發現的詞素,給它們提供一個含義,並且在 AST 中儲存結果對象。
在繼續前行前確保你已經理解了前面的資料。
解析對象
我們這個小語言的解析器可以說是相當的簡單。與掃描器一樣,我們有一個指向正在掃描的檔案相關資訊的指標。我們通過某種途徑跟蹤錯誤和掃描器對象。
剩下的三個欄位直接從掃描器返回的值對應過來:位置、標識符和詞法串。
入口
ParseFile 是魔法開始的地方。初始化解析器並且開始掃描。如果發生一個或多個錯誤,就停下來並且輸出錯誤資訊。否則,返回作為程式的入口的檔案對象。
與掃描器類似,init 進行啟動準備。任何對解析器函數的調用都會讓解析器和掃描器向前移動,並且無法後退。燃料就是那一行行代碼,那麼就讓我們發動引擎吧!
parseFile 是其導可出版本的夥伴。這個函數建立了 AST 的第一個對象。再次回顧第五部分(看到文法規則有多麼重要了嗎?)我們可以認為這個檔案對象是所有東西的根。這就是我們已經完成的,File 對象就是第一個對象。
主意,我們並未在處理尋找第一個運算式之前進行任何的檢查。這是因為我們的文法規則告訴我們應當如此。否則就是錯的。我們預期找到某種形式的運算式,如果沒有找到的話會是件另人煩躁的事情,不論如何,一無反顧的向前吧!
最後,我們需要確保在檔案中的最後一個標識符是檔案結束(EOF)。文法規則標識,在得到根運算式後,不應該還有其他任何內容。如果在期後還發現有其他任何東西的話,就報告一個錯誤。向每個人宣布,召集大家來看,並且一起嘲諷!
運算式
我們已經多次討論了運算式。現在,任務是用 parseGenExpr 找到一個運算式,只是一開始我們並不知道它是什麼類型。不過,第一個標識符會告訴我們所有需要的資訊。如果我們找到了一個左括弧,那麼就是一個二值運算式。如果找到了一個整數,那麼就是一個整數。否則就產生一個錯誤,然後繼續。
整數是最容易解析的元素。它沒有什麼需要關注的細節。代碼自己已經很能說明問題。
然而,二值運算式會更棘手一點。Calc 1 只有二值運算式,不過不久的將來,我們會添加更多類型的運算式。與 Expression 對象類似,我們需要一個更加通用的方式在特殊處理每一個之前,來進行通用的處理。parseExpr 就是用於這個目的的。
首先,期望找到一個左括弧。如果沒有找到,就是錯的。接著,需要明確這是哪種類型的運算式。我們知道,當前僅有的運算式類型就是二值運算式,因此接下來確定下一個標識符到底是什麼運算子。如果得到的不是預期的內容,需要報告一個錯誤。
BinaryExpr 展示了我們解析器不斷迴圈調用的過程。我們已經找到了左括弧和運算子,因此接下來需要尋找運算對象。這一過程是由遞迴的調用 parseGenExpr 來完成的。不斷的一層一層的構建整個樹,直到結束。
當我們找到了全部運算對象後,我們預期有一個右括弧來結束運算式。最後返回作為結果的 BinaryExpr 對象,並插入到樹中去。
Expect、Next 和 AddError
Expect 是一個超棒的小工具函數。我們告訴它預期得到什麼標識符。如果它在那,那很好。如果不是的話,就報告一個錯誤。不論怎樣,都會返回元素的位置。
Next 實際上不需要怎麼解釋。它只是擷取掃描器找到的內容,然後將其儲存在解析器的對象中。
AddError 向 ErrorList 增加一個錯誤。如果發現了超過十個錯誤,那就沒有必要繼續了。它會列印錯誤,並強制解析器用一個錯誤碼退出。
文法分析
我們的解析步驟在其工作時進行文法分析,這保證瞭解析的原始碼符合文法規則。
缺少任何東西都是無法接受的。真的!
完成
這就是這個步驟的全部內容。如果這部分你領會得不錯,那麼最後一步應該很容易。當我們的文法設計越來越深入的時候,解析器會變得越來越複雜。對此,Calc 2 應該是個好例子。