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番目に相当する引数の数の調整。ソースの対応する括弧を探すメソッドは前と一緒。