Racc で Brainf*ck 、趣味的な実装

その場実行で Tape と Source クラスを作って抽象化してみる。

class BrainF_ckParser
rule# class BrainF_ckParser

expression :
           | expression TERMINAL { send(val[1][0]).send val[1][1], send(val[1][2]) }

end # class BrainF_ckParser

---- inner
  attr_reader :tape, :source

  def initialize(source)
    @source = Source.new(source)
    @tape = Tape.new
  end # def initialize(source)

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

  private
  def scan
    while c = @source.char do
      case c
        when '>' then yield [:TERMINAL, [:tape, :right, :id]]
        when '<' then yield [:TERMINAL, [:tape, :left, :id]]
        when '+' then yield [:TERMINAL, [:tape, :plus, :id]]
        when '-' then yield [:TERMINAL, [:tape, :minus, :id]]
        when '.' then yield [:TERMINAL, [:tape, :put, :id]]
        when ',' then yield [:TERMINAL, [:tape, :get, :id]]
        when '[' then yield [:TERMINAL, [:source, :bra, :tape]]
        when ']' then yield [:TERMINAL, [:source, :ket, :tape]]
      end # case c
      @source.next
    end # while c = @source.char do
    yield nil
  end # def scan

---- header
# -*- coding: utf-8; -*-
Version = '$Id: brainf_ck_tape_source.y 3304 2009-03-07 06:39:01Z hs9587 $'
# $Date: 2009-03-07 15:39:01 +0900 (土, 07 3 2009) $

---- footer
class Tape
  def initialize
    @tape = []
    @pointer = 0
  end # def initialize

  def value
    @tape[@pointer] ||= 0
  end # def value

  def right(nil)
    @pointer += 1
  end # def right

  def left(nil)
    @pointer -= 1
  end # def left

  def plus(nil)
    @tape[@pointer] = value + 1
  end # def plus

  def minus(nil)
    @tape[@pointer] = value - 1
  end # def minus

  def put(nil)
    $stdout.print value.chr
  end # def put

  def get(nil)
    c = $stdin.getc
    @tape[@pointer] = c.respond_to?(:ord) ? c.ord : c
  end # def get
end # class Tape

class Source
  def initialize(source)
    @source = source.split //
    @pointer = 0
  end # def initialize(source)

  def char
    @source[@pointer]
  end # def char

  def next
    @pointer += 1
  end # def next

  def bra(tape)
    corresponding_bracket if tape.value == 0
  end # def bra

  def ket(tape)
    corresponding_bracket unless tape.value == 0
  end # def ket(tape)

  private
  Correspondings = {
    '[' => ['[', ']', +1 ],
    ']' => [']', '[', -1 ],
     }
  def corresponding_bracket
    bra_ket, ket_bra, sign = Correspondings[char]
    @pointer += sign
    corr = 0
    until corr == 0 and char == ket_bra do
      raise unless (0...@source.size).include? @pointer
      corr += 1 if char == bra_ket
      corr -= 1 if char == ket_bra
      @pointer += sign
    end # until corr == 0 and char == ket_bra do
    @pointer
  end # def corresponding_bracket
end # class Source

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

ARGV.options do |opt|
  opt.on('--[no-]yydebug', 'debug mode (from racc -t) on/off'){ |v| 
opts.yydebug = v }
  opt.release = 'TERMINAL acts directly with Tape and Source'
  opt.banner += "\n\tBrainf*ck interpreter.\nOptions:"
  opt.parse!
end # ARGV.options do |opt|

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

相変らず終端記号は一つだけ、ちょっと苦しかった。素直にブラケット系終端記号は別にした方が分かり易いだろう。なのでちょっと趣味的な実装という。
そういうわけでアクション「{ send(val[1][0]).send val[1][1], send(val[1][2]) }」が大変分かり難い。終端記号 TERMINAL の持ってくる値(val内)は項目数3の配列とした。0番目はテープ操作なのかソース操作なのかに応じて、それぞれのインスタンス変数を attr_reader 経由で取って来る、1番目はそのオブジェクトに与える操作メソッド名、2番目はソース操作系ならテープの値が必要なのでその辺、テープ操作系の場合は必要無いものなので単にオブジェクトIDを読み捨てる。いずれにしてもスキャナからはシンボル(の配列)がやってくるだけなので send の嵐になって大変分かり難い。
Tape と Source クラスの定義はまあ。テープ操作系メソッドが 1引数 nil を取ってそれを何も使わないのは上記分かり難いアクションの 2番目に相当する引数の数の調整。ソースの対応する括弧を探すメソッドは前と一緒。