1
0
mirror of https://github.com/safiire/n65.git synced 2024-12-12 00:29:03 +00:00
n65/lib/assembler.rb
Safiire 2c938f7312 This is s a big rewrite including: A scoped symbol table, segment and
bank management, Use of promises to resolve symbols that are used before
they are defined.  A base class for all instructions and assembler
directives.  Hopefully my scoped symbols can be used to create C like
data structures in the zero page, ie sprite.x   New code to prodce the
final ROM.  Basically everything was rewritten.
2015-03-05 12:33:56 -08:00

203 lines
5.5 KiB
Ruby

require_relative 'symbol_table'
require_relative 'memory_space'
require_relative 'parser'
module Assembler6502
class Assembler
attr_reader :program_counter, :current_segment, :current_bank, :symbol_table, :virtual_memory, :promises
##### Custom exceptions
class AddressOutOfRange < StandardError; end
class InvalidSegment < StandardError; end
class WriteOutOfBounds < StandardError; end
class INESHeaderAlreadySet < StandardError; end
class FileNotFound < StandardError; end
####
## Assemble from an asm file to a nes ROM
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 do |line|
assembler.assemble_one_line(line)
print '.'
end
puts
## Second pass to resolve any missing symbols.
print "Second pass, resolving symbols..."
assembler.fulfill_promises
puts " Done."
## Let's export the symbol table to a file
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
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
####
## 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.
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
####
## This will empty out our promise queue and try to fullfil operations
## that required an undefined symbol when first encountered.
def fulfill_promises
while promise = @promises.pop
promise.call
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
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
####
## Set the iNES header
def set_ines_header(ines_header)
fail(INESHeaderAlreadySet) unless @ines_header.nil?
@ines_header = ines_header
end
####
## Set the program counter
def program_counter=(address)
fail(AddressOutOfRange) unless address_within_range?(address)
@program_counter = address
end
####
## Set the current segment, prog or char.
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
####
## Set the current bank, create it if it does not exist
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
####
## Emit a binary ROM
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
private
####
## Get virtual memory space
def get_virtual_memory_space(segment, bank_number)
@virtual_memory[segment][bank_number]
end
####
## Is this a 16-bit address within range?
def address_within_range?(address)
address >= 0 && address < 2**16
end
####
## Is this a valid segment?
def valid_segment?(segment)
[:prog, :char].include?(segment)
end
end
end