|
 |
RSS 源 : |
Martin Fowler's Bliki |
 |
投递时间: |
2007年5月31日星期四 3:47 |
 |
作 者: |
Martin Fowler |
 |
主 题: |
HelloRacc |
|
 |
整篇文章链接:http://martinfowler.com/bliki/HelloRacc.html |
|
|
当我讨论HelloCup的时候,我正在通过一种不需要我处理脏指针的语言看一个基于yacc的语法分析器。另外一个选择是Ruby,它现在有一个yaccish语法分析器,并构建到单独的库中 - 当然叫做racc。 |
|
|
Racc在ruby与语法规则之间存在着一种有趣的相互影响。你使用一个racc文件定义了语法规则,这个文件将会生成一个语法解析器类。 |
|
|
需要说明的是,我还是使用一个简单的hello world例子。输入的文本是: |
|
|
item camera item laser |
|
|
我将使用下面的model类,把item对象封装在一个catalog里。 |
|
|
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的文件。 |
|
|
语法分析器类是一种特殊的格式,但是它对于熟悉yaccish的人来说并不陌生。对于这个简单的例子来说,它将是下面这样的形式: |
|
|
#file item.y.rb... class ItemParser token 'item' WORD rule catalog: item | item catalog; item: 'item' WORD {@result << Item.new(val[1])}; end |
|
|
Taken从句声明了我们从lexer获得的token。我使用了一个字符串'item'和一个WORD作为标记。Rule为yacc从句开始了常用的BNF产生式规则。正如你预料到的那样,我可以在curlies中编写action。为了引用rule元素,我使用了一个val数据,因此valu[1]就等于yacc中的$2(ruby使用下标基于0的数组索引,但是我都给忘了)。我是否应当从那个我赋给rusult值的rule(等于yacc中的$$)返回一个值呢? |
|
|
使用racc最为复杂的部分是找出lexer。Racc期望调用一个生成token的方法,其中每个token都是一个包含两个元素的数组,其中第一个元素是token的类型(与token的声明相匹配),而第二个元素是值(它将在val中显示 - 通常是文本形式)。你使用[false,false]来标记token的结束。Racc的示例代码使用正则表达式来匹配一个字符串。在大多数情况下,一个更好的选择是使用StringScanner,它存在于标准的ruby库当中。 |
|
|
我可以使用这个scanner来将一个字符串转换为一个token数组。 |
|
|
#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 |
|
|
为了将scanner集成到语法分析器当中,racc允许你代码放置在生成的parser类当中。你可以将代码添加到语法文件当中。声明 ―――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来获取下一个token,因此我们需要实现这个方法,并且将它包含在内部。 |
|
|
#file item.y.rb.... ---- inner def next_token @tokens.shift end |
|
|
这样,racc就可以处理文件了。但是,当我调试的时候,我发现scanner比我想象的更加糟糕。我实际上只希望告诉lexer匹配什么模式,并且通过它们返回。就像下面这样: |
|
|
#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提供的功能编写了我自己的lexer包装。下面是设置lexer并处理上面的configuration的代码 |
|
|
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 |
|
|
然后,我就可以将语法分析器当中的代码改为调用lexer了。 |
|
|
#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 |
|
|
它不但使我通过一种更好的方式定义了规则,而且允许语法控制lexer,因为它一次只拿到一个token - 这将为我提供一种实现辞汇上表述的机制。 |
|
|
总体来说,racc可以非常轻松的安装并使用 - 只要你熟悉yacc。文档还不是很全面。在网站上还有一些简单的手册和一些示例代码。另外,在还有一个关于racc的非常有帮助的演示。我也从我们的Mingle团队(他们使用它作为一种Mingle内部的自定义语言)得到了一些小技巧。 |
|
|
>> 查看文章 |