彈性 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 版本執行乾淨的建置更快。也許在我取得更多語法檔案來建置後,這會有所改變,但目前看來我似乎已成為過早最佳化的受害者。(而且找出原因對我來說並不值得,因為現在兩種建置都夠快了。)