class N65::Assembler

Attributes

current_bank[R]
current_segment[R]
program_counter[R]
promises[R]
symbol_table[R]
virtual_memory[R]

Public Class Methods

from_file(infile, outfile) click to toggle source

Assemble from an asm file to a nes ROM

# File lib/n65.rb, line 21
def self.from_file(infile, outfile)
  fail(FileNotFound, infile) unless File.exists?(infile)

  assembler = self.new
  program = File.read(infile)

  puts "Building #{infile}"
  ##  Process each line in the file
  program.split(/\n/).each_with_index do |line, line_number|
    begin
      assembler.assemble_one_line(line)
    rescue StandardError => e
      STDERR.puts("\n\n#{e.class}\n#{line}\n#{e}\nOn line #{line_number}")
      exit(1)
    end
    print '.'
  end
  puts

  ##  Second pass to resolve any missing symbols.
  print "Second pass, resolving symbols..."
  assembler.fulfill_promises
  puts " Done."

  ##  Let's not export the symbol table to a file anymore
  ##  Will add an option for this later.
  #print "Writing symbol table to #{outfile}.yaml..."
  #File.open("#{outfile}.yaml", 'w') do |fp|
    #fp.write(assembler.symbol_table.export_to_yaml)
  #end
  #puts "Done."

  ##  For right now, let's just emit the first prog bank
  File.open(outfile, 'w') do |fp|
    fp.write(assembler.emit_binary_rom)
  end
  puts "All Done :)"
end
new() click to toggle source

Initialize with a bank 1 of prog space for starters

# File lib/n65.rb, line 63
def initialize
  @ines_header = nil
  @program_counter = 0x0
  @current_segment = :prog
  @current_bank = 0x0
  @symbol_table = SymbolTable.new
  @promises = []
  @virtual_memory = {
    :prog => [MemorySpace.create_prog_rom],
    :char => []
  }
end

Public Instance Methods

assemble_one_line(line) click to toggle source

This is the main assemble method, it parses one line into an object which when given a reference to this assembler, controls the assembler itself through public methods, executing assembler directives, and emitting bytes into our virtual memory spaces. Empty lines or lines with only comments parse to nil, and we just ignore them.

# File lib/n65.rb, line 100
def assemble_one_line(line)
  parsed_object = Parser.parse(line)

  unless parsed_object.nil?
    exec_result = parsed_object.exec(self)

    ##  If we have returned a promise save it for the second pass
    @promises << exec_result if exec_result.kind_of?(Proc)
  end
end
current_bank=(bank_number) click to toggle source

Set the current bank, create it if it does not exist

# File lib/n65.rb, line 179
def current_bank=(bank_number)
  memory_space = get_virtual_memory_space(@current_segment, bank_number)
  if memory_space.nil?
    @virtual_memory[@current_segment][bank_number] = MemorySpace.create_bank(@current_segment)
  end
  @current_bank = bank_number
end
current_segment=(segment) click to toggle source

Set the current segment, prog or char.

# File lib/n65.rb, line 168
def current_segment=(segment)
  segment = segment.to_sym
  unless valid_segment?(segment)
    fail(InvalidSegment, "#{segment} is not a valid segment.  Try prog or char")
  end
  @current_segment = segment
end
emit_binary_rom() click to toggle source

Emit a binary ROM

# File lib/n65.rb, line 190
def emit_binary_rom
  progs = @virtual_memory[:prog]
  chars = @virtual_memory[:char]
  puts "iNES Header"
  puts "+ #{progs.size} PROG ROM bank#{progs.size != 1 ? 's' : ''}"
  puts "+ #{chars.size} CHAR ROM bank#{chars.size != 1 ? 's' : ''}"

  rom_size  = 0x10 
  rom_size += MemorySpace::BankSizes[:prog] * progs.size
  rom_size += MemorySpace::BankSizes[:char] * chars.size

  puts "= Output ROM will be #{rom_size} bytes"
  rom = MemorySpace.new(rom_size, :rom)

  offset = 0x0
  offset += rom.write(0x0, @ines_header.emit_bytes)

  progs.each do |prog|
    offset += rom.write(offset, prog.read(0x8000, MemorySpace::BankSizes[:prog]))
  end

  chars.each do |char|
    offset += rom.write(offset, char.read(0x0, MemorySpace::BankSizes[:char]))
  end
  rom.emit_bytes.pack('C*')
end
fulfill_promises() click to toggle source

This will empty out our promise queue and try to fullfil operations that required an undefined symbol when first encountered.

# File lib/n65.rb, line 115
def fulfill_promises
  while promise = @promises.pop
    promise.call
  end
end
get_current_state() click to toggle source

Return an object that contains the assembler’s current state

# File lib/n65.rb, line 79
def get_current_state
  saved_program_counter, saved_segment, saved_bank = @program_counter, @current_segment, @current_bank
  saved_scope = symbol_table.scope_stack.dup
  OpenStruct.new(program_counter: saved_program_counter, segment: saved_segment, bank: saved_bank, scope: saved_scope)
end
program_counter=(address) click to toggle source

Set the program counter

# File lib/n65.rb, line 160
def program_counter=(address)
  fail(AddressOutOfRange) unless address_within_range?(address)
  @program_counter = address
end
set_current_state(struct) click to toggle source

Set the current state from an OpenStruct

# File lib/n65.rb, line 88
def set_current_state(struct)
  @program_counter, @current_segment, @current_bank = struct.program_counter, struct.segment, struct.bank
  symbol_table.scope_stack = struct.scope.dup
end
set_ines_header(ines_header) click to toggle source

Set the iNES header

# File lib/n65.rb, line 152
def set_ines_header(ines_header)
  fail(INESHeaderAlreadySet) unless @ines_header.nil?
  @ines_header = ines_header
end
with_saved_state(&block) click to toggle source

This rewinds the state of the assembler, so a promise can be executed with a previous state, for example if we can’t resolve a symbol right now, and want to try during the second pass

# File lib/n65.rb, line 126
def with_saved_state(&block)
  ##  Save the current state of the assembler
  old_state = get_current_state

  lambda do

    ##  Set the assembler state back to the old state and run the block like that
    set_current_state(old_state)
    block.call(self)
  end
end
write_memory(bytes, pc = @program_counter, segment = @current_segment, bank = @current_bank) click to toggle source

Write to memory space. Typically, we are going to want to write to the location of the current PC, current segment, and current bank. Bounds check is inside MemorySpace#write

# File lib/n65.rb, line 143
def write_memory(bytes, pc = @program_counter, segment = @current_segment, bank = @current_bank)
  memory_space = get_virtual_memory_space(segment, bank)
  memory_space.write(pc, bytes)
  @program_counter += bytes.size
end

Private Instance Methods

address_within_range?(address) click to toggle source

Is this a 16-bit address within range?

# File lib/n65.rb, line 230
def address_within_range?(address)
  address >= 0 && address < 2**16
end
get_virtual_memory_space(segment, bank_number) click to toggle source

Get virtual memory space

# File lib/n65.rb, line 223
def get_virtual_memory_space(segment, bank_number)
  @virtual_memory[segment][bank_number]
end
valid_segment?(segment) click to toggle source

Is this a valid segment?

# File lib/n65.rb, line 237
def valid_segment?(segment)
  [:prog, :char].include?(segment)
end