2011-04-29 92 views
25

我目前正在構建用PHP編寫的PHP解析器,因爲沒有現有解析器出現在my previous question中。 parser itself工作得很好。將AST編譯回源代碼

現在很明顯,解析器本身並沒有什麼好處(除了靜態分析)。我想將轉換應用於AST,然後將其編譯回源代碼。應用轉換不是一個問題,一個正常的訪問者模式應該做。

我現在的問題是如何編譯AST回源。基本上有兩種可能性,我看到:

  1. 編譯使用一些預定義的方案
  2. 保留原始代碼的格式,並只對已更改節點申請1中的代碼。

現在我想專注於1.作爲2.似乎很難完成(但如果你有關於這方面的提示,我想聽聽他們)。

但我不確定可以使用哪種設計模式來編譯代碼。我看到實現這個最簡單的方法是將->compile方法添加到所有節點。我在這裏看到的缺點是要更改生成的輸出的格式非常困難。人們需要改變節點本身才能做到這一點。因此我正在尋找不同的解決方案。

我聽說Visitor模式也可以用於此,但我無法真正想象這應該如何工作。據我瞭解訪問者模式,你有一些NodeTraverser遞歸遍歷所有節點,並調用一個->visit方法Visitor。這聽起來很有希望用於節點操作,其中Visitor->visit方法可以簡單地更改它傳遞的節點,但我不知道它如何用於編譯。一個顯而易見的想法是將節點樹從葉子迭代到根,並用源代碼替換訪問的節點。但這似乎不是一個非常乾淨的解決方案?

+0

我想你給了我50分。恩,謝謝! – 2011-10-06 21:20:05

回答

51

將AST轉換回源代碼的問題通常稱爲「漂亮打印」。有兩個微妙的變化:儘可能重新生成與原始文本匹配的文本(我稱之爲「保真度打印」)和(漂亮)漂亮打印,這會生成很好的格式化文本。以及如何根據編碼人員是否將在重新生成的代碼上工作(他們通常希望保真度打印)來打印問題 ,或者您唯一的意圖是編譯它(此時任何合法的漂亮打印都可以)。

要做好漂亮的打印需要的信息通常比經典的解析器收集的信息要多,而且大多數解析器生成器都不支持這種額外的信息收集。我打電話給解析器,收集足夠的信息來做到這一點,「重新設計解析器」。以下更多細節。

漂亮打印的基本方法是通過行走AST(「放置它」時的「訪問者模式」),並根據AST節點內容生成文本。基本技巧是:從左到右調用子節點(假定這是原始文本的順序)以生成它們表示的文本,併爲此AST節點類型散佈其他文本。要打印一段語句,您可能需要以下僞代碼:

PrettyPrintBlock: 
    Print("{"}; PrintNewline(); 
    Call PrettyPrint(Node.children[1]); // prints out statements in block 
    Print("}"); PrintNewline(); 
    return; 


PrettyPrintStatements: 
    do i=1,number_of_children 
     Call PrettyPrint(Node.children[i]); Print(";"); PrintNewline(); // print one statement 
    endo 
    return; 

請注意,當您訪問樹時,會隨即彈出文本。

有一些細節你需要管理:

  • 對於代表文字AST節點,你必須重新生成文本值。如果你想讓答案准確,這比看起來更難。在不損失任何精度的情況下打印浮點數是lot比看起來更難(當你損害Pi的值時,科學家討厭它)。對於字符串文字,您必須重新生成引號和字符串文字內容;您必須小心重新生成必須轉義的字符的轉義序列。 PHP雙引號字符串文字可能有點困難,因爲它們不是由AST中的單個標記表示的。 (我們的PHP Front End(重新編譯解析器/ prettyprinter將它們實質上表示爲連接字符串片段的表達式,使得變換可以在字符串「literal」內應用)

  • 間距:某些語言在關鍵位置需要空格。令牌ABC17 42最好不要打印爲ABC1742,但可以將令牌(ABC17)打印爲(ABC17)。解決此問題的一種方法是在合法的位置放置一個空間,但人們不會喜歡結果是:太多的空格,如果你只是編譯結果,不是問題

  • 換行符:允許任意空格的語言在技術上可以重新生成一行文本。你要編譯結果;有時你看看生成的代碼,這使得它不可能。因此,您需要一種方法來爲表示主要語言元素(語句,塊,方法,類等)的AST節點引入換行符。這通常不困難;當訪問代表這樣一個構造的節點時,打印構造並追加一個換行符。

  • 如果您希望用戶接受您的結果,您將會發現,您將不得不保留源文本的某些屬性,而這些屬性通常不會存儲爲 對於文字,您可能必須重新生成基數的字面意思;當您重新生成十進制等值時,即使它意味着完全相同的東西,輸入數字作爲十六進制文字的編碼器也不會很高興。同樣,字符串必須有「原始」引號;大多數語言都允許使用「或」作爲字符串引號字符,並且人們需要它們最初使用的字符。對於PHP,使用哪些引用來說明問題,並確定字符串字符串中的哪些字符必須轉義 有些語言允許使用大寫或小寫關鍵字(甚至是縮寫),大寫和小寫變量名稱表示同一個變量;原始作者通常希望他們的原始封套返回.PHP具有不同類型標識符中的有趣字符(例如「$」),但是您會發現它並不總是存在的(參見字符串中的$變量)通常人們都希望他們的原始佈局格式化;爲此,您必須存儲具體令牌的列號信息,並且有關何時使用該列的明確規則 - 數據數據,用於在可能的情況下將相同列中的相應打印文本放在哪個位置;如果在該列之後填充了那麼深的打印行,該如何處理?

  • 評論:大多數標準解析器(包括您使用Zend解析器實現的解析器,我敢肯定)完全拋棄註釋。再次,人們討厭這個問題,並且會拒絕一個評論丟失的印刷精美的答案。這是一些美麗的打印機試圖通過使用原始文本 重新生成代碼的主要原因(另一種是複製原始代碼佈局以進行保真度打印,如果您未捕獲到列號信息)。恕我直言,正確的訣竅是捕獲AST中的評論,以便AST轉換可以檢查/生成評論,但每個人都有自己的設計選擇。

所有這些「額外」信息都是由一個好的重新構造解析器收集的。傳統的解析器通常不收集任何信息,這使打印可接受的AST變得困難。

一個更原則的方法區分其打印目的是漂亮的格式,從保真度打印,其目的是重新生成文本,以最大限度地匹配原始來源。應該清楚的是,在終端級別,你幾乎要保真度打印。根據您的目的,您可以使用漂亮的格式或高保真打印進行漂亮的打印。我們使用的策略是當AST沒有改變時默認打印保真度打印,並且打印它的位置(因爲變更機器通常沒有關於列號或數字基數的信息等)。轉換會將新生成的AST節點標記爲「不存在保真度數據」。

一個有組織的方法,很好地打印漂亮的是要理解幾乎所有基於文本的編程語言在矩形塊文本方面很好地呈現。 (Knuth的TeX文檔生成器也有這個想法)。如果您有一些文本框代表重新生成的代碼片段(例如,直接爲終端令牌生成的原始框),您可以想象操作員編寫這些框:水平合成(在另一個框右側堆疊一個框),垂直(相互疊放盒,這實際上取代了印刷新行),縮進(用空格一盒水平線構圖),等等,那麼你可以通過建立和撰寫文本框中構建您的prettyprinter:

PrettyPrintBlock: 
    Box1=PrimitiveBox("{"); Box2=PrimitiveBox("}"); 
    ChildBox=PrettyPrint(Node.children[1]); // gets box for statements in block 
    ResultBox=VerticalBox(Box1,Indent(3,ChildBox),Box2); 
    return ResultBox; 

PrettyPrintStatements: 
    ResultBox=EmptyBox(); 
    do i=1,number_of_children 
     ResultBox=VerticalBox(ResultBox,HorizontalBox(PrettyPrint(Node.children[i]); PrimitiveBox(";") 
    endo 
    return; 

這裏的真正價值是任何節點都可以用任意的順序用任意的中間文本來組合由其子節點生成的文本框。您可以用這種方法重新排列大量文本塊(想象一下VBox在方法名順序中的類方法)。沒有文字被吐出來遇到;只有到達根目錄時,或者某個AST節點已知所有子項框已正確生成。

我們的DMS Software Reengineering Toolkit使用這種方法來清理它可以解析的所有語言(包括PHP,Java,C#等)。相反,通過遊客附着框計算到AST節點,我們重視箱計算在特定領域的文本框標記

  • H(...)臥式箱
  • V(.... )對於縱向盒
  • 我(...)爲縮進框)

直接到語法規則,使我們能夠簡潔地表達語法(解析器)和prettyprinter(「反解析」)一個地方。漂亮的打印機規則由DMS自動編譯到訪問者中。漂亮的打印機必須足夠聰明才能理解評論如何發揮到這一點,坦白說這有點神祕,但你只需要做一次。一個DMS例如:

block = '{' statements '}' ; -- grammar rule to recognize block of statements 
<<PrettyPrinter>>: { V('{',I(statements),'}'); }; 

你可以看到這是如何了Wirth's Oberon programming language PrettyPrinter展示如何將語法規則和漂亮的方式規則組合做了更爲明顯的例證。 PHP前端看起來像這樣,但它顯然很大。

執行漂亮打印的更復雜的方法是構建一個語法導向的轉換器(意思是,沿着樹形結構順序遍歷樹並構建文本或其他數據結構),以在特殊文本框中生成文本框AST。文本框AST然後被另一棵樹行走打印,但其行爲基本上是微不足道的:打印文本框。 看到這個技術文件:Pretty-printing for software reengineering

另外一點:你當然可以自己建立所有這些機械。但是,你選擇使用解析器生成器(它做了很多工作,並且這個工作並沒有以有趣的方式對你的目標做出貢獻)的原因與你想要選擇一個現成的解析器生成器的原因是一樣的,貨架漂亮打印機。周圍有很多解析器生成器。沒有很多漂亮的打印機。 [DMS是爲數不多的內置之一。]

+0

感謝您的廣泛描述,我會在接下來的幾天嘗試這些提示。 PS:Simlpe語言例子鏈接給了我404。 – NikiC 2011-04-29 17:05:47

+0

@nikic:在我第一次提交時是錯誤的,但我糾正了它。再試一次。 – 2011-04-29 17:09:20

+1

好的,謝謝你的幫助Ira :)我設法實現了漂亮的打印機(花了相當多的時間來擺脫許多邊緣案例的bug)。雖然它不保留任何空白或評論信息。我認爲這很難實施。你可以在github上找到結果包:https://github.com/nikic/PHP-Parser :)再次感謝! – NikiC 2011-06-04 14:00:05