你好 Sablecc
2007 年 2 月 11 日
我最近對 SableCC 進行了一些小嘗試。花了一點功夫才讓「Hello World」風格的剖析器運作起來,因此我想在此記錄一下我讓它運作的步驟。我不是說這是最好的方法,但如果你想嘗試看看,這可能會有幫助。
SableCC 是 Java 環境的編譯器編譯器工具。它處理 LALR(1) 文法(對於記得文法類別的人來說)。換句話說,它是一個自底向上的剖析器(不像 JavaCC 和 Antlr 是自頂向下的)。
與大多數編譯器編譯器工具一樣,你可以在文法檔案中定義語言的文法,並執行編譯器編譯器來產生此語言的剖析器。由於這是 hello-world 範例,我的語言是一個極簡語言,只是為了讓編譯器編譯器運作。語言只是一個項目清單,如下所示
item camera item laser
其中「item」是一個關鍵字,第二個字是一個名稱。我希望剖析器將它轉換成 Configuration 類別的實例,其中包含一個項目清單。
public class Configuration { private Map<String, Item> items = new HashMap<String, Item>(); public Item getItem(String key) { return items.get(key); } public void addItem(Item arg) { items.put(arg.getName(), arg); } public class Item { private String name; public Item(String name) { this.name = name; }
我先前展示的輸入應該被讀取,以通過以下測試。
@Test public void itemsAddedToItemList() { Reader input = null; try { input = new FileReader("rules.txt"); } catch (FileNotFoundException e) { throw new RuntimeException(e); } Configuration config = CatalogParser.parse(input); assertNotNull(config.getItem("camera")); assertNotNull(config.getItem("laser")); }
當然,對這種情況使用編譯器編譯器是完全殺雞用牛刀,但 hello-world 範例的重點是在簡單情況下讓環境運作,這樣你才能在基礎運作的情況下轉到有趣的案例。每當我嘗試新的技術時,我總是想先讓類似這樣的東西運作,然後再深入探討有趣的方面。
使用編譯器編譯器會讓建置程序變得更複雜一點。你必須先對語法檔案執行編譯器編譯器以產生剖析器,然後編譯自訂程式碼和產生的剖析器以建立整體程式,然後執行(並測試它)。因此,在這個時候,我無法只在 IntelliJ 中完成所有事情,實際上必須組合一個 ant 檔案。我已經有一段時間沒有組合 ant 檔案了,所以花了一點時間才重新記起如何使用 ant 語言。為了執行 SableCC,我使用了 Java 任務
<property name = "gendir" value = "gen"/> <target name = "gen" > <mkdir dir="${gendir}"/> <java jar = "lib/sablecc.jar" fork = "true" failonerror="true"> <arg value = "-d"/> <arg value = "${gendir}"/> <arg value = "catalog.sable"/> </java> </target>
我將程式碼產生到 gen
目錄中,以將它與我在 src
和 test
目錄中自行編寫的程式碼分開。然後使用 javac 任務將所有內容編譯在一起。
<property name = "builddir" value = "classes/production/sable"/> <path id="classpath"> <fileset dir = "lib"> <include name = "*.jar"/> </fileset> <pathelement path = "${builddir}"/> </path> <target name = "compile" depends = "gen, copyDats"> <mkdir dir="${builddir}"/> <javac destdir="${builddir}" classpathref="classpath"> <src path = "src"/> <src path = "${gendir}"/> <src path = "test"/> </javac> </target>
除了編譯之外,我還必須將兩個剖析器資料檔移到建置目錄中。資料包含用於剖析器和詞法分析器的表格。建置目錄以我擁有的方式嵌套,因此它將與 IntelliJ 完美地搭配運作。(我真的很應該將測試分隔到一個單獨的輸出目錄中,但我當時感到很懶惰。)
<target name = "copyDats"> <mkdir dir="${builddir}"/> <copy todir = "${builddir}"> <fileset dir = "${gendir}" includes = "**/lexer.dat"/> <fileset dir = "${gendir}" includes = "**/parser.dat"/> </copy> </target>
然後我有一個測試任務來執行測試。
<target name = "test" depends="compile"> <junit haltonfailure = "on"> <formatter type="brief"/> <classpath refid = "classpath"/> <batchtest todir="${builddir}" > <fileset dir = "test" includes = "**/*Tester.java"/> </batchtest> </junit> </target>
為了讓剖析器啟動並執行,我需要使用 SableCC 的語法語法定義簡單語言的語法。
Package catalogParser; Tokens itemdef = 'item'; string = ['a' .. 'z'] +; blank = (' ' | 13 | 10)+; Ignored Tokens blank; Productions configuration = item * ; item = itemdef string ;
與大多數編譯器編譯器一樣,SableCC 將工作拆分為詞法分析器和剖析器。詞法分析器讀入字元並將它們分塊為由語法檔案的 Tokens
區段定義的代碼。在這種情況下,它非常簡單:字串是小寫字母,關鍵字 item
是它自己的代碼。我也定義了空白是什麼,並在 Ignores
子句中告訴詞法分析器將它丟棄。
然後,詞法分析器會將 itemdef
和 string
代碼串流提供給剖析器。剖析器使用兩個產生式來處理這個問題。它將組態描述為多個項目,每個項目為一個 itemdef 和一個字串(用於其名稱)。
這定義了輸入的語法,但沒有說明如何從輸入取得我的組態和項目物件。為了執行這個動作,我需要撰寫一些程式碼來對應我已剖析的內容和我想建立的物件。在大多數編譯器編譯器中,我透過將動作嵌入語法中來執行這個動作。然而,SableCC 以另一種方式運作。它會自動建立剖析樹,然後提供一個訪客給我來遍歷這個剖析樹。然後,我可以對訪客進行子類別化以執行有趣的動作。在這種情況下,當我遍歷剖析樹時,我取得剖析樹上的每個項目節點,並將它轉換成模型中的真實項目。
public class CatalogParser extends DepthFirstAdapter { private Configuration result; public void outAItem(AItem itemNode) { System.out.println("found item"); result.addItem(new Item(itemNode.getString().toString().trim())); }
剖析樹使用命名慣例將語法繫結到在樹中建立的物件。因此,語法會建立稱為 AItem
的節點,以符合語法中的項目產生式。方法 outAItem
會在訪客離開項目節點時呼叫,並允許我存取項目上的任何內容,在這種情況下為基礎字串代碼。然後,我可以使用該字串作為名稱在我的模型中建立項目。
最後一段程式碼是用於執行檔案解析器,我透過將目錄解析器設為命令物件來執行。
public static Configuration parse(Reader input) { Configuration result = new Configuration(); new CatalogParser(input, result).run(); return result; } public CatalogParser(Reader input, Configuration result) { this.input = input; this.result = result; } public void run() { try { createParser(input).parse().apply(this); } catch (Exception e) { throw new RuntimeException(e); } } private Parser createParser(Reader input) { return new Parser(new Lexer(new PushbackReader(input))); }
這就是讓它運作的基本原理。到目前為止,我尚未深入探討,因此我對 SableCC 的想法有點初步,但這個部落格的重點就是寫出半成形的思考。
SableCC 的使用有點尷尬。除了作者的論文之外,幾乎沒有文件。幸運的是,這篇論文比我見過許多其他論文都容易理解,所以我能夠找出如何讓事情運作。在工作期間,我在語法中犯了一個錯誤,發現很難獲得診斷。錯誤訊息沒有提供太多資訊,我訴諸於偵錯程式和列印陳述式,這些陳述式位於產生的解析器程式碼中。幸運的是,問題出在標記化中,因此我透過查看詞法分析器的輸出,了解到我的錯誤。LALR 解析器出了名的難以理解,所以我很高興我不用深入探討。Antlr 在這方面表現得更好。遞迴下降解析器較容易理解,而且有一本Antlr 書籍正在製作中,這應該對我探索它很有幫助。
到目前為止,我還沒有被移除解析器動作並自動產生解析樹的方法說服。由於它是一個解析樹,您必須在它上面執行才能做任何有用的事情。Nat Pryce 讓我瞭解樹狀轉換規則在最新的 SableCC 中,由於它定義了抽象語法樹,而不是解析樹,因此看起來更有用。您仍然必須在它上面執行才能在網域模型中建立物件,但執行起來更容易。(最新版本的 Antlr 具有類似的功能。)樹狀執行的優點之一是,如果我對樹狀執行器進行變更,我就不需要重新產生,這讓我可以在 IntelliJ 中執行。然而,Antlr 具有 AntlrWorks,它可以插入 IntelliJ,看起來非常好。