你好 Racc

2007 年 5 月 30 日

當我說 HelloCup 時,我正在研究一種基於 yacc 的解析器,它使用一種不需要我處理髒指標的語言。另一個可以使用的替代方案是 Ruby,它現在有一個內建於標準函式庫中的 yaccish 解析器,不可避免地稱為 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 的檔案。

解析器語法類別是一種特殊格式,但對於 yaccish 使用者來說相當熟悉。對於這個簡單範例,它看起來像這樣

#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 的範例程式碼會對字串進行正規表示式比對。在大部分情況下,較好的選擇是使用標準 Ruby 函式庫中的 StringScanner

我可以使用這個掃描器將字串轉換為詞彙陣列。

#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 以取得下一個詞彙,因此我們需要實作該方法並將其包含在內部區段中。

#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

除了提供更好的規則定義方式之外,這也允許語法控制詞法分析器,因為它一次只擷取一個詞彙,這會提供一個機制,讓我可以在稍後實作詞彙狀態。

總的來說,如果您知道 yacc,racc 非常容易設定和使用。文件在簡略的一面。網站上有一個簡單的手冊和一些範例程式碼。還有一個非常有用的 簡報 介紹 racc。我也從我們的 Mingle 團隊那裡獲得了一些提示,他們在 Mingle 內部使用它作為一個靈巧的客製化語言。