Brainf*ck 括弧の対応
さて、Brainf*ck で括弧の対応関係は必須なんだろうか。要は、こんなのはエラーになるべきなのだろうか。
]
構文木版では、パースエラーになる、括弧の対応をもって文法規則にしているんだから当たり前だ。
一方その場実行版ではエラーにならない、「]」実行時点でテープ値は(何もしてないので)「0」、対応する括弧を探しに行くジャンプは行われないのだ。だから
+]
はエラーになる、corresponding_bracket の中でポインタ位置の不正をチェックしてる所に引っ掛かる。
どちらの動作があるべき姿なんだろうか。
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番目に相当する引数の数の調整。ソースの対応する括弧を探すメソッドは前と一緒。
Racc で Brainf*ck 、その場で実行の為に
対応する括弧を行き来する感覚を大事にする為にその場で実行する方向で考える。ソースを行き来できるようにするわけだ。その為に、ソースとソース上の位置を示すポインタを導入する。
class BrainF_ckParser rule# class BrainF_ckParser expression : | expression TERMINAL { send val[1] } end # class BrainF_ckParser ---- inner def initialize(source) @source = source.split // @source_pointer = 0 @tape = [] @pointer = 0 end # def initialize(source) def parse(opts = nil) @yydebug = opts.yydebug if opts.respond_to? :yydebug yyparse self, :scan end # def parse(opts = nil) def right @pointer += 1 end # def right def left @pointer -= 1 end # def left def plus @tape[@pointer] = (@tape[@pointer] ||= 0) + 1 end # def plus def minus @tape[@pointer] = (@tape[@pointer] ||= 0) - 1 end # def minus def put print (@tape[@pointer] ||= 0).chr end # def put def get c = $stdin.getc @tape[@pointer] = c.respond_to?(:ord) ? c.ord : c end # def get def bra if (@tape[@pointer] ||= 0) == 0 then @source_pointer = corresponding_bracket(@source_pointer) - 1 end # if (@tape[@pointer] or 0) == 0 end # def bra def ket unless (@tape[@pointer] ||= 0) == 0 then @source_pointer = corresponding_bracket(@source_pointer) - 1 end # unless (@tape[@pointer] or 0) == 0 end # def ket private def scan while c = @source[@source_pointer] do case c when '>' then yield [:TERMINAL, :right] when '<' then yield [:TERMINAL, :left] when '+' then yield [:TERMINAL, :plus] when '-' then yield [:TERMINAL, :minus] when '.' then yield [:TERMINAL, :put] when ',' then yield [:TERMINAL, :get] when '[' then yield [:TERMINAL, :bra] when ']' then yield [:TERMINAL, :ket] end # case c @source_pointer += 1 end # while c = @source[@source_pointer] do yield nil end # def scan Correspondings = { '[' => ['[', ']', +1 ], ']' => [']', '[', -1 ], } def corresponding_bracket(s_pointer) bra_ket, ket_bra, sign = Correspondings[@source[s_pointer]] s_pointer += sign corr = 0 until corr == 0 and @source[s_pointer] == ket_bra do raise unless (0...@source.size).include? s_pointer corr += 1 if @source[s_pointer] == bra_ket corr -= 1 if @source[s_pointer] == ket_bra s_pointer += sign end # until corr == 0 and @source[s_pointer] == ket_bra do s_pointer end # def corresponding_bracket(s_pointer) ---- header # -*- coding: utf-8; -*- Version = '$Id: brainf_ck_source_pointer.y 3298 2009-03-07 02:50:25Z hs9587 $' # $Date: 2009-03-07 11:50:25 +0900 (土, 07 3 2009) $ ---- footer 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' 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 で分岐している。
ソース @source は配列にしてみた、文字列のままでも良かったのだけど、1.8系と 1.9系で文字列の要素へのアクセスが変わっているのでちょっと避けた('String'[n])。
corresponding_bracket はちょっと複雑かな、まあ対応する括弧を探してるだけといえばそれだけ、括弧のネストを数えるのが少し煩雑。この中ではテープからはみ出るのチェックしている。あとはこういったソース位置の計算と、スキャナ内でのソース位置の変更(ひとつ先に進む)のタイミングを合わせないといけなかったり。
Brainf*ck の Hello, wold!
そうそう、Brainf*ck のテスト用にちょっと
A 一文字表示
++++++ [> ++++++++++ < -] > +++++.
Hello, world!
>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++. [-]>+++++++[<++++++>-]<++.>++[<------>-]<. >+++++++++[<+++++++++>-]<++++++.>++[<---->-]<.+++.------.--------. [-]>++++++++[<++++>-]<+.[-]++++++++++.
何箇所か出てくる「[-]」は、そのテープ位置の値をゼロクリアするもの。
Racc で Brainf*ck 、構文木
取り敢えずルールはこんな感じかな。
program : expression expression : | expression bracket | expression TERM bracket : '[' expression ']'
終端記号は、「TERM」テープの扱いや入出力と括弧 '['、']'。一方「program」はまあ、最終的な左辺値として全体を代表する。それと同じものなのに更に「expression」を作ってるのは、「'['」「']'」で挟まれるものに「program」は無いだろうという思いもある。その「expression」は空を含む「bracket」と「TERM」の列。「bracket」はその「expression」を囲むもの。
その「bracket」のだけど
- 「[」ポインタ位置の数値が 0 以外なら何もしない、0 なら対応する「]」まで行き、その「]」の次からまた始める
- 「]」ポインタ位置の数値が 0 なら何もしない、0 以外なら対応する「[」まで戻り、その「[」の次からまた始める
対応する処理を考えると要するに while ループ(0 以外なら繰り返しを続ける) なわけだ。それが分かっているから次のようになる。だが、対応する括弧を行き来する気分が無くなってしまうのだけど。
class BrainF_ckParser rule# class BrainF_ckParser program : expression { result = RootNode.new val[0] } expression : { result = NullNode.new } | expression bracket { result = ExpressionNode.new val[0], val[1] } | expression TERMINAL { result = ExpressionNode.new val[0], TerminalNode.new(val[1]) } bracket : '[' expression ']' { result = BracketNode.new val[1] } end # class BrainF_ckParser ---- inner def initialize(source) @source = source @tape = [] @pointer = 0 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 @source.each_char do |c| case c when '>' then yield [:TERMINAL, :right] when '<' then yield [:TERMINAL, :left] when '+' then yield [:TERMINAL, :plus] when '-' then yield [:TERMINAL, :minus] when '.' then yield [:TERMINAL, :put] when ',' then yield [:TERMINAL, :get] when '[' then yield ['[', nil] when ']' then yield [']', nil] end # case c end # @source.each_char do |c| yield nil end # def scan ---- header # -*- coding: utf-8; -*- Version = '$Id: brainf_ck.y 3300 2009-03-07 05:29:42Z hs9587 $' # $Date: 2009-03-07 14:29:42 +0900 (土, 07 3 2009) $ ---- footer require 'singleton' class Tape include Singleton def initialize @tape = [] @pointer = 0 end # def initialize def value @tape[@pointer] ||= 0 end # def value def right @pointer += 1 end # def right def left @pointer -= 1 end # def left def plus @tape[@pointer] = value + 1 end # def plus def minus @tape[@pointer] = value - 1 end # def minus def put $stdout.print value.chr end # def put def get c = $stdin.getc @tape[@pointer] = c.respond_to?(:ord) ? c.ord : c end # def get end # class Tape class Node def initialize(*args) end # def initialize(*args) def evaluate(opts, *args) $stderr.puts Tape.instance.inspect if opts.yydebug end # def evaluate(opts, *args) end # class Node class NullNode < Node; end class RootNode < Node def initialize(expression) super @expression = expression end # def initialize def evaluate(opts, *args) super @expression.evaluate(opts, *args).flatten end # def evaluate(opts, *args) end # class RootNode < Node class ExpressionNode < Node def initialize(expression, other_expression) super @expression = expression @other_expression = other_expression end # def initialize(expression, otherexpression) def evaluate(opts, *args) super [@expression.evaluate(opts, *args), @other_expression.evaluate(opts, *args)] end # def evaluate(opts, *args) end # class ExpressionNode < Node class TerminalNode < Node def initialize(term) super @term = term end # def initialize(term) def evaluate(opts, *args) super Tape.instance.send @term end # def evaluate(opts, *args) end # class TerminalNode < Node class BracketNode < Node def initialize(expression) super @expression = expression end # def initialize(expression, other_expression) def evaluate(opts, *args) super while Tape.instance.value != 0 do @expression.evaluate(opts, *args) end # while Tape.instance.value != 0 do end # def evaluate(opts, *args) end # class BracketNode < Node 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 = 'Node tree' 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) \ .tap{ |tree| tree.inspect.display $stderr if opts.yydebug }.evaluate(opts)
Nodeクラスの定義群が入ってきて、そろそろファイルの分割を考えたくなってきた。そしてこれだけクラス定義をするとなると、テストやスペックを書きたくもなって来る。
それから、Singleton を使って Tape を抽象化してみた。
さらに命名について。Node類が出てきたので、パーサにも Parser と接尾辞をつけよう。各 Node(継承)クラス名は各記号の名前文字列に対応したものにしているが、RootNode だけは ProgramNode では気分がでないのでそうした。また、NullNode は作らないで単に何もしないでよかったかも知れないが、一応作ってみた。
Racc で Brainf*ck
そして Brainf*ck、「奇妙な言語」p.46 数値データ格納用のテープの存在(先頭位置 0 から始まる不定長一次元配列)とテープ上の位置を示すポインタを想定した上で
- 「>」ポインタを右に一つ動かす
- 「<」ポインタを左に一つ動かす
- 「+」ポインタ位置の数値を一つ増やす
- 「-」ポインタ位置の数値を一つ減らす
- 「.」ポインタ位置の数値を ASCIIコードと見做してその文字を出力
- 「,」標準入力から一文字読み込みその文字の ASCIIコードをポインタ位置に書き込む
- 「[」ポインタ位置の数値が 0 以外なら何もしない、0 なら対応する「]」まで行き、その「]」の次からまた始める
- 「]」ポインタ位置の数値が 0 なら何もしない、0 以外なら対応する「[」まで戻り、その「[」の次からまた始める
取り敢えず、今までと同じ方針で「[」「]」(制御構造、ジャンプ)なしのもの。
class BrainF_ckParser rule# class BrainF_ckParser expression : | expression TERM { send val[1] } end # class BrainF_ckParser ---- inner def initialize(source) @source = source @tape = [] @pointer = 0 end # def initialize(source) def scan @source.each_char do |c| case c when '>' then yield [:TERM, :right] when '<' then yield [:TERM, :left] when '+' then yield [:TERM, :plus] when '-' then yield [:TERM, :minus] when '.' then yield [:TERM, :put] when ',' then yield [:TERM, :get] end # case c end # @source.each_char do |c| yield nil end # def scan def parse(opts = nil) @yydebug = opts.yydebug if opts.respond_to? :yydebug yyparse self, :scan end # def parse(opts = nil) private def right @pointer += 1 end # def right def left @pointer -= 1 end # def left def plus @tape[@pointer] = (@tape[@pointer] ? @tape[@pointer] : 0) + 1 end # def plus def minus @tape[@pointer] = (@tape[@pointer] ? @tape[@pointer] : 0) - 1 end # def minus def put print (@tape[@pointer] ? @tape[@pointer] : 0).chr end # def put def get c = $stdin.getc @tape[@pointer] = c.respond_to?(:ord) ? c.ord : c end # def get ---- header # -*- coding: utf-8; -*- Version = '$Id: brainf_ck_without_bracket.y 3298 2009-03-07 02:50:25Z hs9587 $' # $Date: 2009-03-07 11:50:25 +0900 (土, 07 3 2009) $ ---- footer 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 = 'start' 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)
今までと同じように全部その場で実行する。テープとポインター位置はインスタンス変数に持ってる、ソースを initilarize する時同時に初期化。scan で六つの命令を case分けする、記号の種類は一つだけにして、メソッド名を value に持たせることで処理を振り分ける。ポインタがテープの先頭を越えて左に行くときとか、値の範囲チェックとかしていない、真面目に作るならその辺もきちんとしないといけない(「奇妙な言語」pp.47, 55 参照)。
さて、これに条件付きジャンプ命令を追加しようとすると、全部その場で実行してそれを忘れてしまうという今の方針ではなかなか上手くいかない。素直に構文木を作りましょう。
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(//) にせよ、一文字ずつ与えている事が確かなのでそこまでしていない。