Brainf*ckインタプリタをRubyで実装してみる

実用性ゼロのシンプルな言語Brainf*ckのインタプリタRubyで実装してみました。Googleで検索すると他の人もいっぱいRubyで実装していますが、まぁそれはそれということで。
Brainf*ckの言語仕様はこちらです。Brainfuck - Wikipedia
このインタプリタの動作確認は、Brainf*ckのサンプルを使いました。
ファイル: bf.rb

#!/usr/bin/env ruby
# -*- ruby -*-

require 'ostruct' 

$opts = OpenStruct.new(:debug => 0)

class String
  def each_byte_with_index
    index = -1
    self.each_byte do |byte|
      index += 1
      yield(byte, index)
    end
  end
end

module Enumerable
  def inject_with_index(init)
    index = -1
    inject(init) do |result, item|
      index += 1
      yield(result, item, index)
    end
  end
end

=begin
BrainFuck Language Specification
  > ... ++ptr;
  < ... --ptr;
  + ... ++(*ptr);
  - ... --(*ptr);
  . ... putchar(*ptr);
  , ... *ptr = getchar();
  [ ... while(*ptr) {
  ] ... }
=end
class BrainFuck
  DISPATCH_TABLE = {
    ?> => :increment_pointer,
    ?< => :decrement_pointer,
    ?+ => :increment_byte,
    ?- => :decrement_byte,
    ?. => :output_byte,
    ?, => :input_byte,
    ?[ => :while_begin,
    ?] => :while_end
  }
  EXPATTERN = '^' + DISPATCH_TABLE.keys.map {|c| '\\' + c.chr }.join

  def initialize(memsize = 64 * 1024)
    @program = ''
    @memory = Array.new(memsize, 0)
    @jump_table = {}
    @p = 0
    @pc = 0
  end

  def run(input)
    @program = input.read.tr(EXPATTERN, '')
    parse
    while @pc < @program.size
      trace if $opts.debug > 0
      send(DISPATCH_TABLE[@program[@pc]])
      break if @pc < 0
      @pc += 1
    end
    puts # for zsh.
  end
  
  def parse
    stack = []
    @program.each_byte_with_index do |b, i|
      case DISPATCH_TABLE[b]
      when :while_begin
        stack.push i
      when :while_end
        pc = stack.pop
        @jump_table[i] = pc - 1
        @jump_table[pc] = i
      end
    end
  end

  def trace(offset = 0, size = 10)
    mem = @memory[offset, size].inject_with_index('') do |s, b, i|
      s + ((i == @p) ? "[%02x]" : " %02x ") % b
    end
    printf "%08x:%c: %s\n", @pc, @program[@p], mem
  end

  def increment_pointer; @p += 1 end
  def decrement_pointer; @p -= 1 end
  def increment_byte; @memory[@p] += 1 end
  def decrement_byte; @memory[@p] -= 1 end

  def output_byte
    print @memory[@p].chr
  end

  def input_byte
    c = $stdin.getc
    if c
      @memory[@p] = c
    else
      @pc = -1
    end
  end
  
  def while_begin
    @pc = @jump_table[@pc] if @memory[@p] == 0
  end
  
  def while_end
    @pc = @jump_table[@pc]
  end
end

if __FILE__ == $0
  require 'optparse'
  
  Version = '0.1a'
  OptionParser.new do |opt|
    opt.on('-d', '--debug', 'enable debug mode.') do
      $opts.debug += 1
    end
    opt.parse!(ARGV)
  end

  begin
    BrainFuck.new.run(ARGF)
  rescue Interrupt
    # ignore
  end
end

実行例:

$ cat hello.bf 
>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++
++>-]<.>+++++++++++[<+++++>-]<.>++++++++[<+++>-]<.+++.------.--------.[-]>
$ ./bf.rb hello.bf 
Hello World!

$ 

さて次はWhitespaceかな。Whitespace - Wikipedia