class SnesUtils::MiniAssembler

Public Class Methods

new(filename = nil) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 10
def initialize(filename = nil)
  if filename && File.file?(filename)
    file = File.open(filename)
    @memory = file.each_byte.map { |b| hex(b) }
  else
    @memory = []
  end

  @cpu = :wdc65816 # :spc700
  @mem_map = :lorom # :hirom

  @normal_mode = true

  @current_addr = 0
  @current_bank_no = 0
  @accumulator_flag = 1
  @index_flag = 1
  @label_registry = {}

  @next_addr_to_list = 0
end

Public Instance Methods

address_human(addr = nil, cur_bank = @current_bank_no) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 248
def address_human(addr = nil, cur_bank = @current_bank_no)
  address = full_address(addr || @current_addr, cur_bank)
  bank = address >> 16
  addr = (((address >> 8) & 0xFF) << 8) | (address & 0xFF)
  "#{hex(bank)}/#{hex(addr, 4)}"
end
assemble_file(filename, outfile = 'out.smc') click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 66
def assemble_file(filename, outfile = 'out.smc')
  return 0 unless File.file?(filename)

  res = read(filename)
  write(outfile)

  res
end
auto_update_flags(opcode, operand) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 545
def auto_update_flags(opcode, operand)
  if opcode == 0xc2
    @index_flag = 0 if (operand & 0x10) == 0x10
    @accumulator_flag = 0 if (operand & 0x20) == 0x20
  elsif opcode == 0xe2
    @index_flag = 1 if (operand & 0x10) == 0x10
    @accumulator_flag = 1 if (operand & 0x20) == 0x20
  end
end
contains_label?(op) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 402
def contains_label?(op)
  op.include?('@') | op.include?('%') | op.include?('&')
end
detect_label_type(address) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 391
def detect_label_type(address)
  address = address[1..-1] if address.start_with?('#')
  if address.start_with?('@')
    :relative
  elsif address.start_with?('%')
    :absolute16
  elsif address.start_with?('&')
    :absolute24
  end
end
detect_opcode_data_from_mnemonic(mnemonic, operand) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 206
def detect_opcode_data_from_mnemonic(mnemonic, operand)
  SnesUtils.const_get(@cpu.capitalize)::Definitions::OPCODES_DATA.detect do |row|
    mode = row[:mode]
    regex = SnesUtils.const_get(@cpu.capitalize)::Definitions::MODES_REGEXES[mode]
    row[:mnemonic] == mnemonic && regex =~ operand
  end
end
detect_opcode_data_from_opcode(opcode, force_length) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 214
def detect_opcode_data_from_opcode(opcode, force_length)
  SnesUtils.const_get(@cpu.capitalize)::Definitions::OPCODES_DATA.detect do |row|
    if @cpu == :spc700
      row[:opcode] == opcode
    else
      if row[:m]
        accumulator_flag = force_length ? 0 : @accumulator_flag
        row[:opcode] == opcode && row[:m] == accumulator_flag
      elsif row[:x]
        index_flag = force_length ? 0 : @index_flag
        row[:opcode] == opcode && row[:x] == index_flag
      else
        row[:opcode] == opcode
      end
    end
  end
end
disassemble_range(start, count, force_length = false) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 555
def disassemble_range(start, count, force_length = false)
  next_idx = start
  instructions = []
  count.times do
    byte = memory_loc(next_idx)
    break unless byte

    opcode = byte.to_i(16)

    opcode_data = detect_opcode_data_from_opcode(opcode, force_length)

    mnemonic = opcode_data[:mnemonic]
    mode = opcode_data[:mode]
    length = opcode_data[:length]

    format = SnesUtils.const_get(@cpu.capitalize)::Definitions::MODES_FORMATS[mode]

    operand = memory_range(next_idx + 1, next_idx + length - 1).reverse.join.to_i(16)

    hex_encoded_instruction = memory_range(next_idx, next_idx + length - 1)
    prefix = ["#{address_human(next_idx)}:", *hex_encoded_instruction].join(' ')

    auto_update_flags(opcode, operand) if @cpu == :wdc65816

    if SnesUtils.const_get(@cpu.capitalize)::Definitions::DOUBLE_OPERAND_INSTRUCTIONS.include?(mode)
      if SnesUtils.const_get(@cpu.capitalize)::Definitions::BIT_INSTRUCTIONS.include?(mode)
        m = operand >> 3
        b = operand & 0b111
        operand = [m, b]
      else
        operand = hex(operand, 4).scan(/.{2}/).map { |op| op.to_i(16) }
        if @cpu == :spc700 && SnesUtils.const_get(@cpu.capitalize)::Definitions::REL_INSTRUCTIONS.include?(mode)
          r = operand.first
          r_operand = relative_operand(r, next_idx + length)

          operand = [operand.last, *r_operand]
        end
      end
    elsif SnesUtils.const_get(@cpu.capitalize)::Definitions::SINGLE_OPERAND_INSTRUCTIONS.include?(mode)
      if SnesUtils.const_get(@cpu.capitalize)::Definitions::REL_INSTRUCTIONS.include?(mode)
        if @cpu == :wdc65816 && SnesUtils.const_get(@cpu.capitalize)::Definitions::REL_INSTRUCTIONS.include?(mode)
          limit = mode == :rel ? 0x7f : 0x7fff
          offset = mode == :rel ? 0x100 : 0x10000
          rjust_len = mode == :rel ? 2 : 4
          operand = relative_operand(operand, next_idx + length, limit, offset, rjust_len)
        else
          operand = relative_operand(operand, next_idx + length)
        end
      end
    end

    instructions << "#{prefix.ljust(30)} #{format(format, mnemonic, *operand)}"
    next_idx += length
  end

  @next_addr_to_list = next_idx
  instructions
end
dump_label_registry() click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 193
def dump_label_registry
  dump = ['label,snes addr,rom addr']
  @label_registry.each do |k, v|
    next if k.start_with?('@')

    dump << "#{k},#{address_human(v[:mapped_addr], v[:mapped_bank])},#{hex(v[:rom_address], 6)}"
  end

  open('labels.csv', 'w') do |f|
    f << dump.join("\n")
  end
end
full_address(address, bank_no = @current_bank_no) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 232
def full_address(address, bank_no = @current_bank_no)
  (bank_no << 16) | address
end
getline() click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 279
def getline
  prompt = @normal_mode ? "(#{@accumulator_flag}=m #{@index_flag}=x)*" : "(#{address_human})!"
  Readline.readline(prompt, true)
end
hex(num, rjust_len = 2) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 41
def hex(num, rjust_len = 2)
  num.to_s(16).rjust(rjust_len, '0').upcase
end
inc_addr(address, length) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 236
def inc_addr(address, length)
  @current_addr = address + length
  initial_bank_no = @current_bank_no

  while @current_addr > 0xffff
    @current_addr -= 0x10000
    @current_bank_no += 1
  end

  @current_bank_no != initial_bank_no
end
incbin(filename, addr) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 55
def incbin(filename, addr)
  return 0 unless File.file?(filename)

  file = File.open(filename)
  bytes = file.each_byte.map { |b| hex(b) }

  replace_memory_range(addr, addr + bytes.size - 1, bytes)

  bytes.size
end
mapped_address(address, absolute = false) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 371
def mapped_address(address, absolute = false)
  return hex(address, 4) unless absolute

  if @mem_map == :lorom
    bank_offset = address / 0x8000
    mapped_addr = address - (bank_offset * 0x8000)
    mapped_addr += 0x8000 if mapped_addr < 0x8000
    mapped_bank = @current_bank_no * 2
    mapped_bank += bank_offset
    mapped_bank += 0x80 if mapped_bank < 0x7f

    { mapped_bank: mapped_bank, mapped_addr: mapped_addr, rom_address: full_address(address) }
  elsif @mem_map == :hirom
    mapped_bank = @current_bank_no + 0xc0 if @current_bank_no < 0x3f
    { mapped_bank: mapped_bank, mapped_addr: address, rom_address: full_address(address) }
  else
    {}
  end
end
memory_loc(address) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 255
def memory_loc(address)
  @memory[full_address(address)]
end
memory_range(start_addr, end_addr) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 259
def memory_range(start_addr, end_addr)
  start_full_addr = full_address(start_addr)
  end_full_addr = full_address(end_addr)

  if start_full_addr > end_full_addr || start_full_addr >= @memory.length
    return []
  end

  @memory[start_full_addr..end_full_addr]
end
parse_address(line, _register_label = false) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 406
def parse_address(line, _register_label = false)
  return @current_addr if line.index(':').nil?

  address = line.split(':').first.strip.chomp
  return -1 if address.to_i(16) > 0xffff
  return address.to_i(16) if detect_label_type(address).nil?

  label_type = detect_label_type(address)
  case label_type
  when :relative
    @label_registry[address] = mapped_address(@current_addr)
  when :absolute16
    @label_registry[address[1..-1]] = mapped_address(@current_addr, true)
  when :absolute24
    @label_registry[address[1..-1]] = mapped_address(@current_addr, true)
  else
    op
  end

  @current_addr
end
parse_instruction(line, register_label = false, resolve_label = false) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 428
def parse_instruction(line, register_label = false, resolve_label = false)
  current_address = parse_address(line, register_label)
  return if current_address < 0 || current_address > 0xffff

  instruction = line.split(':').last.split(' ')
  mnemonic = instruction[0].upcase
  raw_operand = instruction[1].to_s

  if register_label && contains_label?(raw_operand)
    raw_operand = raw_operand.split(',').map do |op|
      label_type = detect_label_type(op)
      case label_type
      when :relative
        mapped_address(@current_addr)
      when :absolute16
        dummy = mapped_address(@current_addr, true)
        dummy[:mapped_addr].to_s(16)
      when :absolute24
        dummy = mapped_address(@current_addr, true)
        full_address(dummy[:mapped_addr], dummy[:mapped_bank]).to_s(16)
      else
        op
      end
    end.join(',')
  end

  if resolve_label && contains_label?(raw_operand)
    raw_operand = raw_operand.split(',').map do |op|
      label_type = detect_label_type(op)
      case label_type
      when :relative
        if op.start_with?('#')
          @label_registry[op[1..-1]]
        else
          @label_registry[op]
        end
      when :absolute16
        if op.start_with?('#')
          "##{@label_registry[op[2..-1]][:mapped_addr].to_s(16)}"
        else
          @label_registry[op[1..-1]][:mapped_addr].to_s(16)
        end
      when :absolute24
        if op.start_with?('#')
          "##{full_address(@label_registry[op[2..-1]][:mapped_addr], @label_registry[op[1..-1]][:mapped_bank]).to_s(16)}"
        else
          full_address(@label_registry[op[1..-1]][:mapped_addr], @label_registry[op[1..-1]][:mapped_bank]).to_s(16)
        end
      else
        op
      end
    end.join(',')
  end

  opcode_data = detect_opcode_data_from_mnemonic(mnemonic, raw_operand)

  return unless opcode_data

  opcode = hex(opcode_data[:opcode])
  mode = opcode_data[:mode]
  length = opcode_data[:length]

  operand_matches = SnesUtils.const_get(@cpu.capitalize)::Definitions::MODES_REGEXES[mode].match(raw_operand)

  if SnesUtils.const_get(@cpu.capitalize)::Definitions::DOUBLE_OPERAND_INSTRUCTIONS.include?(mode)
    if SnesUtils.const_get(@cpu.capitalize)::Definitions::BIT_INSTRUCTIONS.include?(mode)
      m = operand_matches[1].to_i(16)
      return if m > 0x1fff

      b = operand_matches[2].to_i(16)
      return if b > 7

      operand = m << 3 | b
    else
      if SnesUtils.const_get(@cpu.capitalize)::Definitions::REL_INSTRUCTIONS.include?(mode)
        operand = [operand_matches[1], operand_matches[2]].map { |o| o.to_i(16) }
      else
        operand = "#{operand_matches[1]}#{operand_matches[2]}".to_i(16)
      end
    end
  else
    operand = operand_matches[1]&.to_i(16)
  end

  if operand
    if SnesUtils.const_get(@cpu.capitalize)::Definitions::REL_INSTRUCTIONS.include?(mode)
      if SnesUtils.const_get(@cpu.capitalize)::Definitions::DOUBLE_OPERAND_INSTRUCTIONS.include?(mode)
        relative_addr = operand[1] - current_address - length

        return if relative_addr < -128 || relative_addr > 127

        relative_addr = 0x100 + relative_addr if relative_addr < 0
        param_bytes = "#{hex(operand[0])}#{hex(relative_addr)}"
      else
        relative_addr = operand - current_address - length

        if @cpu == :wdc65816 && mode == :rell
          return if relative_addr < -32_768 || relative_addr > 32_767
        else
          return if relative_addr < -128 || relative_addr > 127
        end

        if relative_addr < 0
          relative_addr = (2**(8 * (length - 1))) + relative_addr
        end
        param_bytes = hex(relative_addr, 2 * (length - 1)).scan(/.{2}/).reverse.join
      end
    else
      param_bytes = hex(operand, 2 * (length - 1)).scan(/.{2}/).reverse.join
    end
  end

  encoded_result = "#{opcode}#{param_bytes}"

  [encoded_result.scan(/.{2}/), length, current_address]
end
parse_line(line) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 284
def parse_line(line)
  if @normal_mode
    if line == '!'
      @normal_mode = false
      nil
    elsif line == '.spc700'
      @cpu = :spc700
      'spc700'
    elsif line == '.65816'
      @cpu = :wdc65816
      '65816'
    elsif matches = Definitions::WRITE_REGEX.match(line)
      filename = matches[1].strip.chomp
      out_filename = write(filename)
      "Written #{@memory.size} bytes to file #{out_filename}"
    elsif matches = Definitions::READ_REGEX.match(line)
      start_addr = matches[2]&.to_i(16)
      filename = matches[3].strip.chomp

      read(filename, start_addr)
    elsif matches = Definitions::INCBIN_REGEX.match(line)
      start_addr = matches[1].to_i(16)
      filename = matches[2].strip.chomp
      nb_bytes = incbin(filename, start_addr)

      "Inserted #{nb_bytes} bytes at #{address_human(start_addr)}"
    elsif Definitions::BYTE_LOC_REGEX =~ line
      memory_loc(line.to_i(16))
    elsif matches = Definitions::BYTE_RANGE_REGEX.match(line)
      start_addr = matches[1].to_i(16)
      end_addr = matches[2].to_i(16)

      padding_count = start_addr % 8
      padding = (1..padding_count).map { |_b| '  ' }
      arr = memory_range(start_addr, end_addr)
      return if arr.empty?

      padded_arr = arr.insert(8 - padding_count, *padding).each_slice(8).to_a
      padded_arr.each_with_index.map do |row, idx|
        line_addr = if idx == 0
                      start_addr
                    else
                      start_addr - padding_count + idx * 8
                    end
        ["#{address_human(line_addr)}-", *row].join(' ')
      end.join("\n")
    elsif matches = Definitions::BYTE_SEQUENCE_REGEX.match(line)
      addr = matches[1].to_i(16)
      bytes = matches[2].delete(' ').scan(/.{1,2}/).map { |b| hex(b.to_i(16)) }
      replace_memory_range(addr, addr + bytes.length - 1, bytes)
      nil
    elsif matches = Definitions::DISASSEMBLE_REGEX.match(line)
      start = matches[1].empty? ? @next_addr_to_list : matches[1].to_i(16)
      disassemble_range(start, 20).join("\n")
    elsif matches = Definitions::SWITCH_BANK_REGEX.match(line)
      target_bank_no = matches[1].to_i(16)
      @current_bank_no = target_bank_no
      @current_addr = @current_bank_no << 16
      @next_addr_to_list = 0
      nil
    elsif matches = Definitions::FLIP_MX_REG_REGEX.match(line)
      val = matches[1]
      reg = matches[2]

      if reg.downcase == 'm'
        @accumulator_flag = val.to_i
      elsif reg.downcase == 'x'
        @index_flag = val.to_i
      end

      nil
    end
  else
    if line == ''
      @normal_mode = true
      nil
    else
      instruction, length, address = parse_instruction(line)
      return 'error' unless instruction

      replace_memory_range(address, address + length - 1, instruction)
      inc_addr(address, length)
      disassemble_range(address, 1, length > 2).join
    end
  end
end
read(filename, start_addr = nil) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 75
def read(filename, start_addr = nil)
  return 0 unless File.file?(filename)

  current_addr = start_addr || @current_addr
  current_bank_no = @current_bank_no
  instructions = []
  raw_bytes = []
  incbin_files = []

  cpu = @cpu

  2.times do |i|
    @current_addr = current_addr
    @current_bank_no = current_bank_no

    instructions = []
    File.open(filename).each_with_index do |raw_line, line_no|
      line = raw_line.split(';').first.strip.chomp
      next if line.empty?

      if line == '.spc700'
        @cpu = :spc700
        next
      elsif line == '.65816'
        @cpu = :wdc65816
        next
      elsif line == '.lorom'
        @mem_map = :lorom
        next
      elsif line == '.hirom'
        @mem_map = :hirom
        next
      end

      if matches = Definitions::READ_BYTE_SEQUENCE_REGEX.match(line)
        raw_addr = matches[1]
        addr = if raw_addr.start_with?('%')
                 parse_address(line, true)
               else
                 full_address(matches[1].to_i(16))
               end
        bytes = matches[2].delete(' ').scan(/.{1,2}/).map { |b| hex(b.to_i(16)) }

        raw_bytes << [addr, bytes] if i == 1

        inc_addr(@current_addr, bytes.size)
        next
      elsif matches = Definitions::READ_INCBIN_REGEX.match(line)
        raw_addr = matches[1]
        addr = if raw_addr.start_with?('%')
                 parse_address(line, true)
               else
                 full_address(matches[1].to_i(16))
               end
        target_filename = matches[2].strip.chomp
        incbin_files << [addr, target_filename] if i == 1

        inc_addr(@current_addr, File.size(target_filename))
        next
      elsif matches = Definitions::READ_INCSRC_REGEX.match(line)
        target_filename = matches[1].strip.chomp
        incsrc_res = read(target_filename)
        puts "incsrc: #{target_filename}, #{incsrc_res}" if i == 1
        next
      elsif matches = Definitions::READ_BANK_SWITCH.match(line)
        new_bank_no = matches[1].to_i(16)
        max_bank_no = @mem_map == :hirom ? 0x3f : 0x7f
        return "Error at line #{line_no + 1}" if new_bank_no > max_bank_no

        @current_bank_no = new_bank_no
        @current_addr = 0

        next
      elsif matches = Definitions::READ_ADDR_SWITCH.match(line)
        new_addr = matches[1].to_i(16)
        return "Error at line #{line_no + 1}" if new_addr > 0xffff

        @current_addr = new_addr

        next
      end

      instruction, length, address = parse_instruction(line, register_label = (i == 0), resolve_label = (i == 1))
      return "Error at line #{line_no + 1}" unless instruction

      instructions << [instruction, length, full_address(address)]
      bank_wrap = inc_addr(address, length)
      if bank_wrap && (i == 0)
        puts "Warning: bank wrap at line #{line_no + 1}"
      end
    end
  end

  @cpu = cpu

  total_bytes_read = 0
  @current_bank_no = current_bank_no

  instructions.map do |instruction_arr|
    instruction, length, address = instruction_arr
    total_bytes_read += replace_memory_range(address, address + length - 1, instruction)
  end

  raw_bytes.each do |raw_byte|
    addr, bytes = raw_byte
    total_bytes_read += replace_memory_range(addr, addr + bytes.length - 1, bytes)
  end

  incbin_files.each do |file|
    addr, filename = file
    total_bytes_read += incbin(filename, addr)
  end

  dump_label_registry

  "Read #{total_bytes_read} bytes"
end
relative_operand(operand, next_idx, limit = 0x7f, offset = 0x100, rjust_len = 2) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 614
def relative_operand(operand, next_idx, limit = 0x7f, offset = 0x100, rjust_len = 2)
  relative_addr = operand > limit ? operand - offset : operand
  relative_addr_s = "#{relative_addr.positive? ? '+' : '-'}#{hex(relative_addr.abs, rjust_len)}"
  absolute_addr = next_idx + relative_addr
  absolute_addr += 0x10000 if absolute_addr.negative?

  [absolute_addr, relative_addr_s]
end
replace_memory_range(start_addr, end_addr, bytes) click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 270
def replace_memory_range(start_addr, end_addr, bytes)
  start_full_addr = full_address(start_addr)
  end_full_addr = full_address(end_addr)

  @memory[start_full_addr..end_full_addr] = bytes

  bytes.size
end
run() click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 32
def run
  while line = getline
    result = parse_line(line.strip.chomp)
    puts result if result
  end

  puts
end
write(filename = 'out.smc') click to toggle source
# File lib/mini_assembler/mini_assembler.rb, line 45
def write(filename = 'out.smc')
  filename = filename.empty? ? 'out.smc' : filename

  File.open(filename, 'w+b') do |file|
    file.write([@memory.map { |i| i || '00' }.join].pack('H*'))
  end

  filename
end