mirror of
https://github.com/safiire/n65.git
synced 2024-12-12 00:29:03 +00:00
2c938f7312
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.
203 lines
5.5 KiB
Ruby
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
|