你好 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 內部使用它作為一個靈巧的客製化語言。