Class: N65::Assembler
- Inherits:
-
Object
- Object
- N65::Assembler
- Defined in:
- lib/n65.rb
Defined Under Namespace
Classes: AddressOutOfRange, FileNotFound, INESHeaderAlreadySet, InvalidSegment, State, WriteOutOfBounds
Instance Attribute Summary collapse
-
#current_bank ⇒ Object
Returns the value of attribute current_bank.
-
#current_segment ⇒ Object
Returns the value of attribute current_segment.
-
#program_counter ⇒ Object
Returns the value of attribute program_counter.
-
#promises ⇒ Object
readonly
Returns the value of attribute promises.
-
#symbol_table ⇒ Object
readonly
Returns the value of attribute symbol_table.
-
#virtual_memory ⇒ Object
readonly
Returns the value of attribute virtual_memory.
Class Method Summary collapse
-
.from_file(infile, options) ⇒ Object
Assemble from an asm file to a NES ROM TODO: This really needs a logger instead of all these unless quiet conditions.
Instance Method Summary collapse
-
#assemble_one_line(line) ⇒ Object
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.
-
#assemble_string(string) ⇒ Object
Assemble the given string.
- #emit_binary_rom ⇒ Object
-
#fulfill_promises ⇒ Object
This will empty out our promise queue and try to fullfil operations that required an undefined symbol when first encountered.
-
#get_current_state ⇒ Object
Return an object that contains the assembler's current state.
-
#initialize ⇒ Assembler
constructor
Initialize with a bank 1 of prog space for starters.
-
#print_bank_usage ⇒ Object
TODO: Use StringIO to build output.
-
#set_current_state(state) ⇒ Object
Set the current state.
-
#set_ines_header(ines_header) ⇒ Object
Set the iNES header.
-
#with_saved_state(&block) ⇒ Object
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.
-
#write_memory(bytes, pc = @program_counter, segment = @current_segment, bank = @current_bank) ⇒ Object
Write to memory space.
Constructor Details
#initialize ⇒ Assembler
Initialize with a bank 1 of prog space for starters
80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/n65.rb', line 80 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 |
Instance Attribute Details
#current_bank ⇒ Object
Returns the value of attribute current_bank.
10 11 12 |
# File 'lib/n65.rb', line 10 def current_bank @current_bank end |
#current_segment ⇒ Object
Returns the value of attribute current_segment.
10 11 12 |
# File 'lib/n65.rb', line 10 def current_segment @current_segment end |
#program_counter ⇒ Object
Returns the value of attribute program_counter.
10 11 12 |
# File 'lib/n65.rb', line 10 def program_counter @program_counter end |
#promises ⇒ Object (readonly)
Returns the value of attribute promises.
10 11 12 |
# File 'lib/n65.rb', line 10 def promises @promises end |
#symbol_table ⇒ Object (readonly)
Returns the value of attribute symbol_table.
10 11 12 |
# File 'lib/n65.rb', line 10 def symbol_table @symbol_table end |
#virtual_memory ⇒ Object (readonly)
Returns the value of attribute virtual_memory.
10 11 12 |
# File 'lib/n65.rb', line 10 def virtual_memory @virtual_memory end |
Class Method Details
.from_file(infile, options) ⇒ Object
Assemble from an asm file to a NES ROM TODO: This really needs a logger instead of all these unless quiet conditions
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/n65.rb', line 22 def self.from_file(infile, ) raise(FileNotFound, infile) unless File.exist?(infile) assembler = new program = File.read(infile) output_file = [:output_file] puts "Building #{infile}" unless [:quiet] # Process each line in the file program.each_line.each_with_index do |line, line_number| begin assembler.assemble_one_line(line) rescue StandardError => e warn("\n\n#{e.class}\n#{line}\n#{e}\nOn line #{line_number}") exit(1) end print '.' unless [:quiet] end puts unless [:quiet] # Second pass to resolve any missing symbols. print 'Second pass, resolving symbols...' unless [:quiet] assembler.fulfill_promises puts ' Done.' unless [:quiet] # Optionally write out a symbol map if [:write_symbol_table] print "Writing symbol table to #{output_file}.yaml..." unless [:quiet] File.open("#{output_file}.yaml", 'w') do |fp| fp.write(assembler.symbol_table.export_to_yaml) end puts 'Done.' unless [:quiet] end # Optionally write out cycle count for subroutines if [:cycle_count] print "Writing subroutine cycle counts to #{output_file}.cycles.yaml..." unless [:quiet] File.open("#{output_file}.cycles.yaml", 'w') do |fp| fp.write(assembler.symbol_table.export_cycle_count_yaml) end puts 'Done.' unless [:quiet] end # Emit the complete binary ROM rom = assembler.emit_binary_rom File.open(output_file, 'w') do |fp| fp.write(rom) end return if [:quiet] rom_size = rom.size rom_size_hex = format('%x', rom_size) assembler.print_bank_usage puts "Total size: $#{rom_size_hex}, #{rom_size} bytes" end |
Instance Method Details
#assemble_one_line(line) ⇒ Object
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.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/n65.rb', line 111 def assemble_one_line(line) parsed_object = Parser.parse(line) return if parsed_object.nil? exec_result = parsed_object.exec(self) # TODO: I could perhaps keep a tally of cycles used per top level scope here if parsed_object.respond_to?(:cycles) @symbol_table.add_cycles(parsed_object.cycles) end # If we have returned a promise save it for the second pass @promises << exec_result if exec_result.is_a?(Proc) end |
#assemble_string(string) ⇒ Object
Assemble the given string
127 128 129 130 131 132 133 |
# File 'lib/n65.rb', line 127 def assemble_string(string) string.each_line do |line| assemble_one_line(line) end fulfill_promises self end |
#emit_binary_rom ⇒ Object
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/n65.rb', line 189 def emit_binary_rom progs = @virtual_memory[:prog] chars = @virtual_memory[:char] rom_size = 0x10 rom_size += MemorySpace::BANK_SIZES[:prog] * progs.size rom_size += MemorySpace::BANK_SIZES[:char] * chars.size 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::BANK_SIZES[:prog])) end chars.each do |char| offset += rom.write(offset, char.read(0x0, MemorySpace::BANK_SIZES[:char])) end rom.emit_bytes.pack('C*') end |
#fulfill_promises ⇒ Object
This will empty out our promise queue and try to fullfil operations that required an undefined symbol when first encountered.
137 138 139 |
# File 'lib/n65.rb', line 137 def fulfill_promises @promises.pop.call while @promises.any? end |
#get_current_state ⇒ Object
Return an object that contains the assembler's current state
94 95 96 97 98 |
# File 'lib/n65.rb', line 94 def get_current_state saved_program_counter, saved_segment, saved_bank = @program_counter, @current_segment, @current_bank saved_scope = symbol_table.scope_stack.dup State.new(saved_program_counter, saved_segment, saved_bank, saved_scope) end |
#print_bank_usage ⇒ Object
TODO: Use StringIO to build output
213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
# File 'lib/n65.rb', line 213 def print_bank_usage puts puts 'ROM Structure {' puts ' iNES 1.0 Header: $10 bytes' @virtual_memory[:prog].each_with_index do |prog_rom, bank_number| puts " PROG ROM bank #{bank_number}: #{prog_rom.usage_info}" end @virtual_memory[:char].each_with_index do |char_rom, bank_number| puts " CHAR ROM bank #{bank_number}: #{char_rom.usage_info}" end puts '}' end |
#set_current_state(state) ⇒ Object
Set the current state
101 102 103 104 |
# File 'lib/n65.rb', line 101 def set_current_state(state) @program_counter, @current_segment, @current_bank = state.program_counter, state.segment, state.bank symbol_table.scope_stack = state.scope.dup end |
#set_ines_header(ines_header) ⇒ Object
Set the iNES header
161 162 163 164 165 |
# File 'lib/n65.rb', line 161 def set_ines_header(ines_header) raise(INESHeaderAlreadySet) unless @ines_header.nil? @ines_header = ines_header end |
#with_saved_state(&block) ⇒ Object
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
144 145 146 147 148 149 150 |
# File 'lib/n65.rb', line 144 def with_saved_state(&block) saved_state = get_current_state lambda do set_current_state(saved_state) block.call(self) end end |
#write_memory(bytes, pc = @program_counter, segment = @current_segment, bank = @current_bank) ⇒ Object
Write to memory space. Typically, we are going to want to write to the location of the current PC, current segment, and current bank.
154 155 156 157 158 |
# File 'lib/n65.rb', line 154 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 |