Rubyで8ビットCPUを作る

Perl で 8ビット CPU を作る - naoyaのはてなダイアリー
octopusをRubyで実装してみました。IO関係、オプションまわりの処理は省略しています。できるだけRubyらしいプログラムを目指してみました。あんまりトリッキーな実装はしていません。
ファイル: octopus3.rb

#!/usr/bin/env ruby
# -*- compile-command: "ruby -Ks octopus3.rb" -*-

class OctopusVm
  REG_PC  = 7
  REG_RET = 6
  REG_SP  = 5
  ZFLAG   = 1

  OCT_INST = {
    0  => 'nop', 
    1  => 'mov', 
    2  => 'in',  
    3  => 'out', 
    4  => 'movi',
    5  => 'addi',
    6  => 'subi',
    7  => 'muli',
    8  => 'divi',
    9  => 'andi',
    10 => 'ori', 
    11 => 'testi',
    12 => 'jmp', 
    13 => 'jz',  
    14 => 'jnz', 
    15 => 'jsr', 
    16 => 'push',
    17 => 'pop', 
    18 => 'call',
    19 => 'ret', 
    31 => 'hlt'
  }

  def initialize(text)
    @text = Array.new(256, 0)
    @text[0, text.size + 1] = text + [0xf8]
    @reg = Array.new(16, 0)
    @port = Array.new(256, 0xff)
    @flag = 0
    @steps = 0

    msg(dump_format(@text[ 0, 16], '  Memory = ', "\n"))
    msg(dump_format(@text[16, 16], '         = ', "\n"))
    msg("----------------------------------------------------------\n")
  end

  def execute
    begin
      loop do
        case execute_op
        when :OCT_OK
          @steps += 1
        when :OCT_HALTED
          msg("### System is halted.\n")
          break
        when :OCT_ILLCOD
          msg("### Illegal operation code!\n")
          break
        when :OCT_ILLMOD
          msg("### Illegal operation mode!\n")
          break
        when :OCT_OVRRUN
          msg("### Segment over-run!\n")
          break
        else
          msg("### Internal error!\n")
          break
        end
      end
    ensure
      terminate
    end
  end

  private

  def msg(*args)
    printf *args
  end

  def imsg(*args)
    printf *args
  end

  def dump_format(vals, prefix, suffix)
    vals.inject(prefix) {|acc, val| acc + '%02x ' % val } + suffix
  end

  def terminate
    msg("----------------------------------------------------------\n")
    msg("            0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15\n")
    msg(dump_format(@text[ 0, 16], '  Memory = ', "\n"))
    msg(dump_format(@text[16, 16], '         = ', "\n"))
    msg(dump_format(@port[ 0, 16], '    Port = ', "\n"))
    msg(dump_format(@port[16, 16], '         = ', "\n"))
    msg(dump_format(@reg,          'Register = ', "\n"))
    msg("    Flag = #{(@flag & ZFLAG > 0) ? "Z\n" : "NZ\n"}")
    msg("   Steps = %d\n", @steps)
 end

  def execute_op
    ins = @text[@reg[REG_PC]]
    @reg[REG_PC] += 1
    op = OCT_INST[(ins & 0xf8) >> 3]
    return :OCT_BREAK if op == 30 # OPEC_BREAK
    return :OCT_OVRRUN if @reg[REG_PC] > 0xff
    return :OCT_ILLCOD unless op
    imsg('%02x: %s ', @reg[REG_PC] - 1, op)
    ret = send('oct_' + op, ins & 7)
    imsg("\n")
    ret
  end

  def oct_nop(mod)
    sleep(0.1)
    :OCT_OK
  end

  def oct_mov(mod)
    case mod
    when 3 # Register -> Register
      src = (@text[@reg[REG_PC]] & 0xf0) >> 4
      dst = @text[@reg[REG_PC]] & 0x0f
      @reg[REG_PC] += 1
      imsg('r%d, r%d', src, dst)
      @reg[dst] = @reg[src]
      :OCT_OK

    when 4 # Register -> Memory
      addr = @text[@reg[REG_PC]]
      @reg[REG_PC] += 1
      imsg('r0, [ 0x%02x ]', addr)
      @text[addr] = @reg[0]
      :OCT_OK

    when 5 # Memory -> Register
      addr = @text[@reg[REG_PC]]
      @reg[REG_PC] += 1
      imsg('[ 0x%02x ], r0', addr)
      @reg[0] = @text[addr]
      :OCT_OK

    when 6 # Reg indirect -> Register
      src = (@text[@reg[REG_PC]] & 0xf0) >> 4
      dst = @text[@reg[REG_PC]] & 0x0f
      @reg[REG_PC] += 1
      imsg('[ r%d ], r%d', src, dst)
      @reg[dst] = @text[@reg[src]]
      :OCT_OK

    when 7 # Register -> Reg indirect
      src = (@text[@reg[REG_PC]] & 0xf0) >> 4
      dst = @text[@reg[REG_PC]] & 0x0f
      @reg[REG_PC] += 1
      imsg('r%d, [ r%d ]', src, dst)
      @text[@reg[dst]] = @reg[src]
      :OCT_OK
      
    else
      imsg('???')
      :OCT_ILLMOD
    end
  end
  
  def oct_in(mod)
    num = @text[@reg[REG_PC]]
    @reg[REG_PC] += 1
    @reg[0] = @port[num]
    imsg('[ 0x%02x ], r0', num)
    :OCT_OK
  end
  
  def oct_out(mod)
    num = @text[@reg[REG_PC]]
    @reg[REG_PC] += 1
    @port[num] = @reg[0]
    imsg('r0, [ 0x%02x ]', num)
    :OCT_OK
  end

  def calc_base(dreg)
    val = @text[@reg[REG_PC]]
    @reg[REG_PC] += 1
    imsg('0x%02x, r%d', val, dreg)
    yield(val)
    if @reg[dreg].zero?
      @flag |= ZFLAG
    else
      @flag &= ~ZFLAG
    end
    :OCT_OK
  end
  
  def oct_movi(dreg)
    calc_base(dreg) do |val|
      @reg[dreg] = val
    end
  end

  def oct_addi(dreg)
    calc_base(dreg) do |val|
      @reg[dreg] += val
      @reg[dreg] -= 0x100 if @reg[dreg] > 0xff    
    end
  end

  def oct_subi(dreg)
    calc_base(dreg) do |val|
      @reg[dreg] -= val
      @reg[dreg] += 0x100 if @reg[dreg] < 0
    end
  end

  def oct_muli(dreg)
    calc_base(dreg) do |val|
      @reg[dreg] *= val
      @reg[dreg] %= 0x100 if @reg[dreg] > 0xff
    end
  end

  def oct_divi(dreg)
    calc_base(dreg) do |val|
      @reg[dreg] /= val
    end
  end

  def oct_andi(dreg)
    calc_base(dreg) do |val|
      @reg[dreg] &= val
    end
  end

  def oct_ori(dreg)
    calc_base(dreg) do |val|
      @reg[dreg] |= val
    end
  end

  def oct_testi(dreg)
    calc_base(dreg) do |val|
    end
  end

  def jmp_base(mod)
    if mod.zero?
      dst = @text[@reg[REG_PC]]
      @reg[REG_PC] += 1
      imsg('0x%02x', dst)
    else
      dst = @reg[mod]
      imsg('r%d', mod)
    end
    yield(dst)
    :OCT_OK
  end

  def oct_jmp(mod)
    jmp_base(mod) do |dst|
      @reg[REG_PC] = dst
    end
  end    

  def oct_jz(mod)
    jmp_base(mod) do |dst|
      @reg[REG_PC] = dst if @flag & ZFLAG != 0
    end
  end

  def oct_jnz(mod)
    jmp_base(mod) do |dst|
      @reg[REG_PC] = dst if @flag & ZFLAG == 0
    end
  end

  def oct_jsr(mod)
    jmp_base(mod) do |dst|
      @reg[REG_RET] = @reg[REG_PC]
      @reg[REG_PC] = dst
    end
  end

  def oct_push(mod)
    jmp_base(mod) do |data|
      @reg[REG_SP] -= 1
      @text[@reg[REG_SP]] = data
    end
  end

  def oct_pop(mod)
    if mod.zero?
      imsg('???')
      :OCT_ILLMOD
    else
      imsg('r%d', mod)
      @reg[mod] = @text[@reg[REG_SP]]
      @reg[REG_SP] += 1
      :OCT_OK
    end
  end

  def oct_call(mod)
    jmp_base(mod) do |dst|
      @reg[REG_SP] -= 1
      @text[@reg[REG_SP]] = @reg[REG_PC]
      @reg[REG_PC] = dst
    end
  end

  def oct_ret(mod)
    if mod.zero?
      @reg[REG_PC] = @text[@reg[REG_SP]]
      @reg[REG_SP] += 1
      :OCT_OK
    else
      imsg('???')
      :OCT_ILLMOD
    end
  end

  def oct_hlt(mod)
    :OCT_HALTED
  end
end

if $0 == __FILE__
  OctopusVm.new(ARGV.map {|a| a.to_i }).execute
end

以下、実行例です。これしか動かしていないので、まだまだバグがありそうです。そのうち暇を見付けてテストコードを書きたいところです。

$ ruby octopus3.rb 37 32 33 16 144 7 248 41 1 144 12 152 41 2 152
  Memory = 25 20 21 10 90 07 f8 29 01 90 0c 98 29 02 98 f8
         = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
----------------------------------------------------------
00: movi 0x20, r5
02: movi 0x10, r1
04: call 0x07
07: addi 0x01, r1
09: call 0x0c
0c: addi 0x02, r1
0e: ret
0b: ret
06: hlt
### System is halted.
----------------------------------------------------------
            0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
  Memory = 25 20 21 10 90 07 f8 29 01 90 0c 98 29 02 98 f8
         = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0b 06
    Port = ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
         = ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
Register = 00 13 00 00 00 20 00 07 00 00 00 00 00 00 00 00
    Flag = NZ
   Steps = 8

(追記) うわー、やっぱりバグってました。id:miura1729さん、どうもありがとうございます。上のソースは修正しておきました。