Brainf*ck on Ruby

仕事でRubyの資格取得目指して勉強中なので、復習ついで?にBrainf*ckのインタプリタを作ってみた。
Wikipediaで仕様を読んで、サンプル2つほど(helloworld、ハノイの塔)でデバッグ。
ちなみにWikipediaのBrainf*ckページにはRubyインタプリタへのリンク(しかもURLを見ると今年の5/14と新しい)があるけど、あえて全く見ないでフルスクラッチで書いた。


module Brainfuck
  class BFEngine    
    def initialize
      @bfcodearray = []
      @bfcodestr = ""
      @ip = 0
      @dp = 0
      @bfdataarray = [0] * 30000
      @datalength = 30000
      @bracketlevel = 0
      return true
    end

    def setscript scr
      if scr.class != String
        return false
      end
      if scr.count("[") != scr.count("]")
        return false
      end
      @bfcodestr = scr
      @bfcodearray = scr.split("")
      @ip = 0
      return true
    end

    def exec(start = -1,step = false)
      if @bfcodearray.length < 1
        return false
      end
      if start >= 0
        @ip = start
      end
      while @ip < @bfcodearray.length
        op = @bfcodearray[@ip]
        step ? (print "code<#{@ip}> ") : nil
        case op
        when "<"
          step ? (puts "exec #{op}") : nil
          self.l
        when ">"
          step ? (puts "exec #{op}") : nil
          self.r
        when "+"
          step ? (puts "exec #{op}") : nil
          self.i
        when "-"
          step ? (puts "exec #{op}") : nil
          self.d
        when "["
          step ? (puts "exec #{op}") : nil
          self.s
        when "]"
          step ? (puts "exec #{op}") : nil
          self.e
        when ","
          step ? (puts "exec #{op}") : nil
          self.g
        when "."
          step ? (puts "exec #{op}") : nil
          self.p
        else
        end
        @ip += 1
        if step then
          print "mem<#{@dp}> "
          Kernel::p @bfdataarray[0,20]
          STDIN.gets
        end
      end
      puts ""
      puts "END exec."
      begin
        while true
          STDIN.readchar
        end
      rescue EOFError
      end
      true
    end

    def clearscript
      @bfcodestr = ""
      @bfcodearray.clear
      @ip = 0
      return true
    end

    def cleardata
      @bfdataarray.fill(0)
      @dp = 0
      return true
    end

    def l
      @dp -= 1
      if @dp < 0
        @dp = 0
      end
      return @dp
    end

    def r
      @dp += 1
      if @dp >= @bfdataarray.length
        @bfdataarray.push(0)
      end
      return @dp
    end

    def i
      @bfdataarray[@dp] += 1
      if @bfdataarray[@dp] > 255
        @bfdataarray[@dp] = 0
      end
      return @bfdataarray[@dp]
    end

    def d
      @bfdataarray[@dp] -= 1
      if @bfdataarray[@dp] < 0
        @bfdataarray[@dp] = 255
      end
      return @bfdataarray[@dp]
    end

    def s
      if @bfcodearray.length < 1
        return false
      end
      if @bfdataarray[@dp] != 0
        return true
      end
      @bracketlevel = 1
      @ip += 1  
      while @bracketlevel > 0
        if @bfcodearray[@ip] == "["
          @bracketlevel += 1
        end
        if @bfcodearray[@ip] == "]"
          @bracketlevel -= 1
        end
        @ip += 1
      end
      @ip -= 1
      return @ip
    end

    def e
      if @bfcodearray.length < 1
        return false
      end            
      if @bfdataarray[@dp] == 0
        return true
      end
      @bracketlevel = 1
      @ip -= 1
      while @bracketlevel > 0
        if @bfcodearray[@ip] == "]"
          @bracketlevel += 1
        end
        if @bfcodearray[@ip] == "["
          @bracketlevel -= 1
        end
        @ip -= 1
      end
      @ip += 1
      return @ip
    end
    
    def g
      c = nil
      begin
        begin
          c = STDIN.readchar
        rescue EOFError
          retry
          
        end
      rescue EOFError
      end
      if c.ord < 0 || c.ord > 255
        return false
      end
      @bfdataarray[@dp] = c.ord
      return c.ord
    end
    
    def p
      print @bfdataarray[@dp].chr
      return @bfdataarray[@dp].chr
    end
  end
end

まあ、いろいろと突っ込みどころ満載だとは思うけど、とりあえずシンプル(?)に。
もっと最適化とかコード短縮の余地はあると思う。
メモリはunsigned char*相当になるようにしてあるので、0に-すると255、255に+すると0。

使い方としては、上記のコードを「brainfuck.rb」としてカレントディレクトリに保存し、ローカル変数sにBrainf*ckのコードを文字列で読み込んでおき、irbのコンソールから

irb(main):xxx:0> load "brainfuck.rb" ; bfe = Brainfuck::BFEngine.new ; bfe.setscript(s);bfe.exec

と実行するとBrainf*ckコードが実行される。
ちなみにBrainfuck::BFEngine#execの第1引数に開始IP(命令ポインタ、デフォルト0)、第2引数にステップ実行フラグ(デフォルトfalse)を指定できる。

さらに、[と]以外のコードはRuby上から直接インタラクティブ実行できて、

irb(main):xxx:0> bfe.cleardata ; "A".ord.times { bfe.i } ; 26.times { bfe.p ; bfe.i } ; nil
ABCDEFGHIJKLMNOPQRSTUVWXYZ=> nil

のような結果が得られる。

…とまあ、いろいろ書いたけど、実際のところ実用性なんてないよ。
ただちょっとコード書きたい気分だっただけ。もう0時近くなったし、明日は叛逆3回目鑑賞に行くからもうお風呂入って寝る。