Class: N65::Assembler

Inherits:
Object
  • Object
show all
Defined in:
lib/n65.rb

Defined Under Namespace

Classes: AddressOutOfRange, FileNotFound, INESHeaderAlreadySet, InvalidSegment, State, WriteOutOfBounds

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeAssembler

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_bankObject

Returns the value of attribute current_bank.



10
11
12
# File 'lib/n65.rb', line 10

def current_bank
  @current_bank
end

#current_segmentObject

Returns the value of attribute current_segment.



10
11
12
# File 'lib/n65.rb', line 10

def current_segment
  @current_segment
end

#program_counterObject

Returns the value of attribute program_counter.



10
11
12
# File 'lib/n65.rb', line 10

def program_counter
  @program_counter
end

#promisesObject (readonly)

Returns the value of attribute promises.



10
11
12
# File 'lib/n65.rb', line 10

def promises
  @promises
end

#symbol_tableObject (readonly)

Returns the value of attribute symbol_table.



10
11
12
# File 'lib/n65.rb', line 10

def symbol_table
  @symbol_table
end

#virtual_memoryObject (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

Raises:



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, options)
  raise(FileNotFound, infile) unless File.exist?(infile)

  assembler = new
  program = File.read(infile)
  output_file = options[:output_file]

  puts "Building #{infile}" unless options[: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 options[:quiet]
  end
  puts unless options[:quiet]

  # Second pass to resolve any missing symbols.
  print 'Second pass, resolving symbols...' unless options[:quiet]
  assembler.fulfill_promises
  puts ' Done.' unless options[:quiet]

  # Optionally write out a symbol map
  if options[:write_symbol_table]
    print "Writing symbol table to #{output_file}.yaml..." unless options[:quiet]
    File.open("#{output_file}.yaml", 'w') do |fp|
      fp.write(assembler.symbol_table.export_to_yaml)
    end
    puts 'Done.' unless options[:quiet]
  end

  # Optionally write out cycle count for subroutines
  if options[:cycle_count]
    print "Writing subroutine cycle counts to #{output_file}.cycles.yaml..." unless options[:quiet]
    File.open("#{output_file}.cycles.yaml", 'w') do |fp|
      fp.write(assembler.symbol_table.export_cycle_count_yaml)
    end
    puts 'Done.' unless options[: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 options[: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_romObject



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_promisesObject

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_stateObject

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

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