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