柔軟な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*など)は私の命名規則には十分ではないため、より正確な正規表現で結果をフィルター処理する必要があります。実際のディレクトリを入手したら、それらを処理するコマンドオブジェクトを作成します。

この作業をしているときに、Antlrを(コマンドライン経由で)呼び出す通常のrubyと、Antlrコマンドファサードを直接呼び出すJRubyの両方でスクリプトを実行できるようにしたいと思いました。そうすれば、JRubyがインストールされていないマシンでもスクリプトを実行できます。そうするのは非常に簡単で、JRubyのビットを分離するだけです。

Antlrクラスは、何をする必要があるかをすべて判断し、内部エンジンに委譲して、2つの異なるスタイルで実際にAntlrを呼び出します。私は処理するディレクトリでオブジェクトを初期化すると、正しいターゲットディレクトリと、ウォーカーを生成する必要があるかどうかを判断します。

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

エンジンには2つの実装があります。最も単純なのは、コマンドライン呼び出しを行うだけです。

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}は、javaでは非常に困難な、ディレクトリ内のすべてのjarをロードします。

最後のトリックは、ジョブに適切なエンジンを使用するようにすることです。私はこれを、Antlrコマンドクラス内のいくつかのインラインコードで行います。

class Antlr...
  tool_class = (RUBY_PLATFORM =~ /java/) ? AntlrJruby : AntlrCommandLine
  @@engine = tool_class.new

非常にシンプルで、通常のrubyまたはJRubyで実行できるのが素晴らしいです。

しかし、オチがあり、冗談は私にかかっています。私はJVMの起動時間がC ruby​​から実行するのを遅くするのを恐れてJRubyを使用するためにこれをすべて設定しました。しかし、C ruby​​は実際にはJRubyバージョンよりも速くクリーンビルドを実行します。ビルドする文法ファイルが増えれば、これは変わるかもしれませんが、今のところ、私は早すぎる最適化の犠牲になったようです。(そして、なぜそうなのかを解明する価値はありません。両方のビルドは今のところ十分に高速です。)