您好,Racc

2007年5月30日

当我提到 HelloCup 时,我正在看一种基于 yacc 的解析器,它使用一种不需要我处理脏指针的语言。另一个可以尝试的替代方案是 Ruby,它现在在标准库中内置了一个 yacc 式解析器 - 不可避免地被称为 racc.

Racc 在 ruby 和语法之间有着有趣的互动。您可以使用 racc 文件定义语法,该文件将生成一个解析器类。

我将再次使用我的简单 hello world 示例。输入文本是

item camera
item laser

我将使用以下模型类在目录中填充项目对象。

class Item
  attr_reader :name
  def initialize name
    @name = name
  end
end

class Catalog 
  extend Forwardable
  def initialize
    @items = []
  end
  def_delegators :@items, :size, :<<, :[] 
end

Forwardable 是一个方便的库,它允许我将方法委托给实例变量。在本例中,我将许多方法委托给 @items 列表。

我用这个测试我读到的内容。

class Tester < Test::Unit::TestCase
  def testReadTwo
    parser = ItemParser.new
    parser.parse "item camera\nitem laser\n"
    assert_equal 2, parser.result.size
    assert_equal 'camera', parser.result[0].name
    assert_equal 'laser', parser.result[1].name
  end
  def testReadBad
    parser = ItemParser.new
    parser.parse "xitem camera"
    fail
    rescue #expected
  end   
end

为了构建文件并运行测试,我使用了一个简单的 rake 文件。

# rakefile...
task :default => :test

file 'item.tab.rb' => 'item.y.rb' do
  sh 'racc item.y.rb'
end

task :test => 'item.tab.rb' do 
  require 'rake/runtest'
  Rake.run_tests 'test.rb'
end

racc 命令需要安装在您的系统上。我在 Ubuntu 上使用 apt-get 以最简单的方式完成了它。它接受输入文件并创建一个名为 inputFileName.tab.rb 的文件。

解析器语法类是一种特殊的格式,但对于 yacc 式的人来说很熟悉。对于这个简单的例子,它看起来像这样

#file item.y.rb...
class ItemParser
  token 'item'  WORD
  rule
    catalog: item | item catalog;
    item: 'item' WORD {@result << Item.new(val[1])};
end

tokens 子句声明我们从词法分析器获得的标记。我使用字符串 'item'WORD 作为符号。rule 子句开始生成规则,这些规则采用 yacc 的通常 BNF 形式。正如您所料,我可以在花括号内编写操作。要引用规则的元素,我使用 val 数组,因此 val[1] 等效于 yacc 中的 $2(ruby 使用基于 0 的数组索引,但我已经原谅它)。如果我想从规则中返回值(等效于 yacc 的 $$),我将其分配给变量 result

使用 racc 最复杂的部分是整理词法分析器。Racc 期望调用一个生成标记的方法,其中每个标记都是一个包含两个元素的数组,第一个元素是标记类型(与标记声明匹配),第二个元素是值(在 val 中显示的内容 - 通常是文本)。您使用 [false, false] 标记标记流的结束。带有 racc 的示例代码对字符串使用正则表达式匹配。对于大多数情况来说,更好的选择是使用 StringScanner,它位于标准 ruby 库中。

我可以使用此扫描器将字符串转换为标记数组。

#file item.y.rb....
---- inner
def make_tokens str
  require 'strscan'
  result = []
  scanner = StringScanner.new str
  until scanner.empty?
    case
      when scanner.scan(/\s+/)
        #ignore whitespace
      when match = scanner.scan(/item/)
        result << ['item', nil]
      when match = scanner.scan(/\w+/)
        result << [:WORD, match]
      else
        raise "can't recognize  <#{scanner.peek(5)}>"
    end
  end
  result << [false, false]
  return result
end

为了将扫描器集成到解析器中,racc 允许您将代码放置到生成的解析器类中。您可以通过在语法文件中添加代码来实现。声明 ---- inner 标记要放在生成的类中的代码(您也可以将代码放在生成的文件的开头和结尾)。我在测试中调用了一个 parse 方法,因此我需要实现该方法。

#file item.y.rb....
---- inner
attr_accessor :result

def parse(str)
  @result = Catalog.new
  @tokens = make_tokens str
  do_parse
end

do_parse 方法启动生成的解析器。这将调用 next_token 来获取下一个标记,因此我们需要实现该方法并将其包含在 inner 部分中。

#file item.y.rb....
---- inner
def next_token
  @tokens.shift
end

这足以使 racc 与文件一起工作。但是,当我使用它时,我发现扫描器比我想象的要乱。我真的只想让它告诉词法分析器要匹配哪些模式以及用它们返回什么。就像这样。

#file item.y.rb....
---- inner
def make_lexer aString
  result = Lexer.new
  result.ignore /\s+/
  result.keyword 'item'
  result.token /\w+/, :WORD
  result.start aString
  return result
end

为了使它工作,我在 StringScanner 提供的基本功能之上编写了自己的词法分析器包装器。以下是设置词法分析器和处理上述配置的代码。

class Lexer...
  require 'strscan'
  def initialize 
    @rules = []
  end
  def ignore pattern
    @rules << [pattern, :SKIP]
  end
  def token pattern, token
    @rules << [pattern, token]
  end
  def keyword aString
    @rules << [Regexp.new(aString), aString]
  end
  def start aString
    @base = StringScanner.new aString
  end

为了执行扫描,我需要使用 StringScanner 将规则与输入流进行比较。

class Lexer...
  def next_token
    return [false, false] if @base.empty?
    t = get_token
    return (:SKIP == t[0]) ? next_token : t
  end
  def get_token
    @rules.each do |key, value|
      m = @base.scan(key)
      return [value, m] if m
    end 
    raise  "unexpected characters  <#{@base.peek(5)}>"
  end  

然后,我可以更改解析器中的代码以调用此词法分析器。

#file item.y.rb....
---- inner
def parse(arg)
  @result = Catalog.new
  @lexer = make_lexer arg
  do_parse
end

def next_token
  @lexer.next_token
end

除了为我提供一种更好的定义规则的方法之外,这也允许语法控制词法分析器,因为它一次只获取一个标记 - 这将为我提供一种机制来以后实现词法状态。

总的来说,racc 设置和使用起来非常容易 - 只要您了解 yacc。文档处于草率的一面。网站上有一个简单的手册和一些示例代码。还有一个非常有用的 演示文稿 关于 racc。我还从我们的 Mingle 团队那里获得了一些提示,他们已经在 Mingle 中使用它来实现一个漂亮的自定义语言。