class N65::Assembler
Attributes
Public Class Methods
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
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
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
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
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 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
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
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
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 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 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
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 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
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
# File lib/n65.rb, line 223 def get_virtual_memory_space(segment, bank_number) @virtual_memory[segment][bank_number] end
Is this a valid segment?
# File lib/n65.rb, line 237 def valid_segment?(segment) [:prog, :char].include?(segment) end