Racc で HQ9+ 、ちょっと

というわけで、HQ9+言語の処理系を作ったのだけど、二点ほどちょっとした事を試してみよう

スキャナの yiled nil

スキャナ

def scan
  @source.each_char{ |hq9p| yield [hq9p, nil] if /[HQ9\+]/ =~ hq9p }
  yield nil
end # def scan

この、スキャンの終了を送る「yield nil」がちょっと唐突に感じる訳だ、たとえ「yield [false, nil]」にした所でその感じは否めない。なんとかならないだろうか。
@source本体の字句解析が全体として nil を返すとしたらどうだろう。each_char は元の文字列を返すだけなので、split で配列に分割して考える。

def scan
  yield @source.split(//).inject(nil){ |hq9p| yield [hq9p, nil] if /[HQ9\+]/ =~ hq9p }
end # def scan

ブロック内のこの yield は nil を返すし、この Array#inject は結果として nil を返す、inject初期値の nil は該当文字が全然無かったとき対策。
injectブロック内でのスキャンと yield が全部終わると、左辺の yield にその nil が与えられて、トークン列の終了を指図する。
終了は nil 又は [fals, <何か>] であって、false 単独ではエラーになるの注意

 :in `_racc_yyparse_c': scan() yielded FalseClass (must be Array[2]) (TypeError)

それはそれとして、何やってるのか実にわかりくい。左辺の yield に必ず nil が渡るというのが明確でない。というか、必ず nil を渡すんだから明記しよう。
という訳でこの案は不採用、元のソースの方が良い。

トークンの記号を Symbol で

さて、ルールセクションでの記号の取り扱い。前に挙げたソースでは引用符で囲むことで文字列で取り扱っていたが、Racc では引用符を使わず Symbol として取り扱う用法もある(というかその方が多いかもしれない)。
それでやってみよう。そのとき引用符なしで 9(数字) や + をルールセクションに書くと「unexpected token」とか言われてしまう。まあ、NINE とか PLUS とかそれっぽい終端記号を使うことにしよう。

class HQ9Plus
rule# class HQ9Plus

  program :
          | program H {puts "Hello world!\n"}
          | program Q {puts @source}
          | program NINE {puts ninety9_bottles_beer val[1]}
          | program PLUS {@count += 1; @racc_debug_out.puts @count if @yydebug}

end # class HQ9Plus

---- inner
def initialize(source)
  @source = source
  @count = 0
end # def initialize(source)

def scan
  @source.each_char do |hq9p|
    case hq9p                                               # (A)
      when /[HQ]/ then yield [hq9p.intern, nil]
      when /[1..9]/ then yield [:NINE, hq9p.to_i]
      when '+' then yield [:PLUS, nil]
    end # case hq9p
  end # @source.each_char do |hq9p|
  yield nil
end # def scan

def parse(opts)
  @yydebug = opts.yydebug if opts.respond_to? :yydebug
  yyparse self, :scan
end # def parse(opts)

private
def ninety9_bottles_beer(n_ty = 9)
  (0..(n_ty*11)).to_a.reverse.inject('') do |nnbb, b|
    case b
      when 0 then before, after = 'no more bottles', "{n_ty*11} bottles"
      when 1 then before, after = '1 bottle', 'no more bottles'
      when 2 then before, after = '2 bottles', '1 bottle'
      else before, after = "#{b} bottles", "#{b-1} bottles"
    end # case b

    action = (b == 0 \
      ? 'Go to the store and buy some more' \
      : 'Take one down and pass it around'
      )

    nnbb << "#{before.capitalize} of beer on the wall, #{before} of beer.\n"
    nnbb << "#{action}, #{after} of beer on the wall.\n"
    nnbb << "\n" unless b == 0
    nnbb
  end # (1..(n_ty*11)).to_a.reverse.inject('') do |nnbb, b|
end # def ninety9_bottles_beer(n_ty = 9)

---- header
# -*- coding: utf-8; -*-
Version = '$Id: hq9plus_digit.y 3261 2009-02-06 10:31:01Z hs9587 $'
# $Date: 2009-02-06 19:31:01 +0900 (金, 06 2 2009) $

---- footer
require 'optparse'
require 'ostruct'
opts = OpenStruct.new
opts.yydebug = false

ARGV.options do |opt|
  opt.on('--[no-]yydebug', 'debug mode (from racc -g) on/off'){ |v| 
opts.yydebug = v }
  opt.release = 'H'
  opt.banner += "\n\tHQ9+ interpreter.\nOptions:"
  opt.parse!
end # ARGV.options do |opt|

HQ9Plus.new((ARGV.size > 0 ? ARGF : $stdin).read).parse(opts)

その結果ルールセクションだけではなく、スキャナも分岐処理になってしまった (A)、単純な case文による分岐ではある。
折角なので、99本のビールだけじゃなくて、n十n本のビール文書を作れるようにしてみた、yield に与える配列の二項目トークンの値をルールセクションでどう拾うのか(val[1])、使ってみたかったのです。
それから、その辺の正規表現、「/[HQ9\+]/」「/[HQ]/」「/[1..9]/」なんだけど安全のためには「\A」「\z」で囲って前後に他の文字が無いことを保障すべきだろうという話しもある。ここでは、each_char にせよ、split(//) にせよ、一文字ずつ与えている事が確かなのでそこまでしていない。