彈性 Antlr 產生
2007 年 4 月 17 日
我一直在探索各種替代語言和語法,以用於外部 DSL。我的主要工具之一是 Antlr。有了這種探索,我有一個包含多個類似語法檔案的專案,我想使用不同的語法執行本質上相同的事情。雖然我目前只有少數幾個語法檔案,但我可能會最終得到幾十個。
在建置中使用這些檔案目前相當麻煩。到目前為止,我明確呼叫 Antlr 來建置每個語法檔案。檔案會完成,無論它最近是否有變更,這會減慢整個建置速度。我想要的是一種方法,可以自動找出語法檔案在哪裡建置,並在必要時建置它們。
我將語法檔案保存在目錄中,例如 src/parser1/Catalog.g, src/parser2/Catalog.g
,我想將它們產生到 gen/parser1, gen/parser2
。這樣,我可以將產生的 gen
目錄排除在原始碼控制之外(這是應該的)。有些目錄只有一個常規語法檔案(總是稱為 Catalog.g),如果我進行樹狀建置和遍歷,其他目錄也會有一個樹狀遍歷語法(稱為 CatalogWalker.g)。
Ant 可能可以做到這一點,但我的 Ant 已經過時了,坦白說,我很樂意保持這種狀態。我現在通常的建置流程是使用 Rake,但它在此處有一個問題 - 多次呼叫 Antlr 會導致多個 JVM 呼叫,由於 JVM 的啟動時間,這可能會很慢。在嘗試了一些替代方案後,我認為值得讓 JRuby 旋轉一下。
Ruby 可以輕鬆地找到並選出符合我的命名慣例的目錄
Dir['src/parser*']. select{|f| f =~ %r[src/parser\d+]}. collect{|f| Antlr.new(f)}. each {|g| g.run}
用於檔案球體的正規表示式(例如 src/parser*
不足以符合我的命名慣例,因此我必須使用更精確的正規表示式來篩選結果。一旦有了真正的目錄,我便建立一個命令物件來處理它們。
在我處理這個問題時,我決定希望能夠使用常規 Ruby(透過命令列呼叫 Antlr)和 JRuby(直接呼叫 Antlr 命令外觀)來執行指令碼。這樣,我可以在沒有安裝 JRuby 的機器上執行指令碼。這樣做相當容易,我只需要將 JRuby 位元組隔離即可。
Antlr
類別會找出所有需要執行的內容,並委派給內部引擎,以實際呼叫 Antlr 使用兩種不同的樣式。我使用要處理的目錄初始化物件,它會找出正確的目標目錄,以及是否需要產生一個 walker。
class Antlr... def initialize dir @dir = dir @grammarFile = File.join @dir, 'Catalog.g' raise "No Grammar file in #{dir}" unless File.exists? @grammarFile walker_name = File.join @dir, 'CatalogWalker.g' @walker = File.exists?(walker_name) ? walker_name : nil @dest = @dir.sub %r[src/], 'gen/' end
當我執行物件時,它會檢查是否需要在呼叫引擎之前執行。
class Antlr... def run return if current? puts "%s => %s " % [@grammarFile, @dest] mkdir_p @dest run_tool self end def current? return false unless File.exists? @dest output = File.join(@dest,'CatalogParser.java') sources = [@grammarFile] sources << @walker if @walker return uptodate?(output, sources) end
run_tool
方法會從欄位中取出資料,並將其放入 Antlr 的命令列引數中(我也會使用字串陣列引數呼叫門面)。
class Antlr... def run_tool args = [] args << '-o' << @dest args << "-lib" << @dest if @walker args << @grammarFile args << @walker if @walker @@engine.run_tool args end
對於引擎,我有兩個實作。最簡單的只會進行命令列呼叫。
class AntlrCommandLine def run_tool args classpath = Dir['lib/*.jar'].join(File::PATH_SEPARATOR) system "java -cp #{classpath} org.antlr.Tool #{args.join ' '}" end end
JRuby 版本比較複雜,因為它必須匯入 Antlr 門面檔案並整理類別路徑。
class AntlrJruby def initialize require 'java' Dir['lib/*.jar'].each{|j| require j} include_class 'org.antlr.Tool' end def run_tool args Tool.new(args.to_java(:string)).process end end
由於我花了很多時間在類別路徑上抓狂,所以我非常喜歡在這裡可以在執行階段只需要一個 jar。特別是因為程式碼 Dir['lib/*.jar'].each{|j| require j}
會載入目錄中的所有 jar,這是 Java 讓它變得非常困難的事情。
最後一個技巧是確保使用正確的引擎來執行工作。我使用 Antlr 命令類別內的內嵌程式碼來執行此操作。
class Antlr... tool_class = (RUBY_PLATFORM =~ /java/) ? AntlrJruby : AntlrCommandLine @@engine = tool_class.new
它在一般 Ruby 或 JRuby 中執行,非常簡單且簡潔。
但有一個笑點,而笑話就在我身上。我設定所有這些內容以使用 JRuby,因為我擔心 JVM 的啟動時間會讓從 C Ruby 執行它太慢。但 C Ruby 實際上比 JRuby 版本執行乾淨的建置更快。也許在我取得更多語法檔案來建置後,這會有所改變,但目前看來我似乎已成為過早最佳化的受害者。(而且找出原因對我來說並不值得,因為現在兩種建置都夠快了。)