您好,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 中使用它来实现一个漂亮的自定义语言。