灵活的 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。我使用要处理的目录初始化对象,它会找出正确的目标目录以及是否需要生成遍历器。

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 版本更快地完成了干净的构建。也许一旦我获得更多语法文件来构建,这种情况就会改变,但目前看来,我似乎成了过早优化的受害者。(而且我不值得弄清楚原因,两种构建现在都足够快了。)