這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文在此。
————翻譯分隔線————
編譯器(8)-抽象文法樹
第一部分:介紹
第二部分:編譯、轉譯和解釋
第三部分:編譯器設計概覽
第四部分:語言設計概述
第五部分:Calc 1 語言規格說明書
第六部分:標識符
第七部分:掃描
在構建解析器之前,首先應當談談如何處理目標資料。
需要用某種抽象資料類型來儲存所有需要解析的資料。樹形資料結構很好的滿足了我們的需求。這個樹描述了程式設計語言的文法結構,它被很恰當的叫做抽象文法樹(AST)。
AST
樹形資料結構總是從一個根開始,我們的也是一樣。通常,在一個成熟的編譯器中,你通常會有一個代表包或程式的對象。在我們的例子中,我們只有一個檔案,因此我們將有一個叫做 File 的對象。
這個對象的其他部分可以在我們的文法藍圖中找到。再次提醒,回顧我們建立文法規則的第五部分。它告訴我們到底需要什麼。你是不是對於已經建立了標準而感到開心?當我們開始解析和文法分析的時候,它還是會提供有力的支援。
其他需要建立的對象:運算式和整數。
Go 介面
在構造需要的對象之前,需要考慮一下它們看起來應當是什麼形態。對於它我們知道什嗎?
我們知道掃描器會為我們提供三個東西:
我們掃描到的字串。可能是數值“42”。
表示元素的標識符。token.LPAREN 是一個左括弧。
元素開始的位置。在檔案的基數和基數加上檔案的大小之間的數值。
在樹中的所有對象都中共存的東西就是它們的位置。它們從哪裡開始,到哪裡結束。例如,錯誤報表不會關心它是什麼類型的對象。只需要瞭解它的位置是多少。因此我們建立了一個 Node 的介面,有兩個方法 用於開始位置的 Pos,和用於結束位置的 End。
接下來,在我們的文法規則中有一個經常出現的元素:運算式。運算式可以是一個整數,或是一個由括弧包裹的一串元素。它的介面將叫做 Expr。任何 Expr 對象必須實現 exprNode 同時完整匹配 Node 的介面。
現在就可以在編譯器的不同部分之間通過各個介面傳遞對象,並且如果需要的話可以使用類型斷言來得到它們真正的類型。當我們到達解析步驟的時候,這應該會變得更清晰一些。
對象
我們需要實現三個對象和一個輔助對象:
我們將從 Integer 開始。之前我們已經考慮過整數類型其實與其他對象沒有什麼不同。可以花大量時間為語言的每個類型來建立對象,也可以只建立一個對象來儲存所有的基本類型。
BasicLit 就是這樣一個對象。不論我們有整數、浮點數、字串還是字元都沒有關係。這個對象可以滿足以上所有需求。當然,當前我們只需要整數,不過最後我們總會作一些添加。這個對象儲存了可以告訴我們物件類型的標識符、文法串開始的位置和字串標識的文法串。
先跳過運算式,先看看 File。這個對象也是類似的。它作為起點,有一個欄位:Root。回顧文法規則,可以發現,一個檔案儲存了一個運算式,而這個運算式又作為其他所有東西的根出現。
現在運算式是一個特殊的類型了。從數學角度來說,它們是雙值運算式。有若干元素需要跟蹤:一個運算子、左右括弧和兩個或以上數量的運算對象,運算對象又可以是運算式。
雙值運算式不會永遠是我們唯一的運算式類型。最終,我們會需要更多類型。只有左右括弧在我們的語言中的每個運算式都會有的。這也就是通用運算式能帶來協助的地方。它唯一的作用就是處理括弧。這個對象會嵌入所有的子運算式中去。
Go 的魅力就在於任何對象都可以通過嵌入的方式“繼承”被嵌入的對象的方法。這不是真正的繼承,不過編譯器已經足夠聰明了。無論如何,對於我們的目的,結果是一樣的。
已經提到,BinaryExpr 需要有一個運算子和一組運算對象。我們有一個欄位來儲存運算對象和其在代碼中的位置。運算對象儲存在一個 slice 中,這些對象都完全滿足 Expr 介面。
AST 工具
如果需要查詢 AST,一個遍曆這個樹的的方法可以提供協助。這個簡單的 Walk 函數就是為此提供的。
通過 Print 方法可以列印出 AST,只需要遍曆整個樹並且按順序列印每個一節點的資訊。
繼續前進
我們很快就要進行解析步驟了,不過我希望確保 AST 的代碼清晰。嵌套定義的運算式容易混淆,還有一些方法並沒有提供使用它們的上下文。請在繼續前回顧本文和前面的文章。
感謝上帝,將所有的部分組裝起來進行解析,應當會讓事情變得更加清晰一些。