Class: N65::Assembler

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

Defined Under Namespace

Classes: AddressOutOfRange, FileNotFound, INESHeaderAlreadySet, InvalidSegment, 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



63
64
65
66
67
68
69
70
71
72
73
74
# 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

Instance Attribute Details

#current_bankObject

Returns the value of attribute current_bank.



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

def current_bank
  @current_bank
end

#current_segmentObject

Returns the value of attribute current_segment.



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

def current_segment
  @current_segment
end

#program_counterObject

Returns the value of attribute program_counter.



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

def program_counter
  @program_counter
end

#promisesObject (readonly)

Returns the value of attribute promises.



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

def promises
  @promises
end

#symbol_tableObject (readonly)

Returns the value of attribute symbol_table.



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

def symbol_table
  @symbol_table
end

#virtual_memoryObject (readonly)

Returns the value of attribute virtual_memory.



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

def virtual_memory
  @virtual_memory
end

Class Method Details

.from_file(infile, outfile) ⇒ Object

Assemble from an asm file to a nes ROM



21
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
# 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

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.


100
101
102
103
104
105
106
107
108
109
# 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

#emit_binary_romObject

Emit a binary ROM



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# 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

#fulfill_promisesObject

This will empty out our promise queue and try to fullfil operations

that required an undefined symbol when first encountered.


115
116
117
118
119
# File 'lib/n65.rb', line 115

def fulfill_promises
  while promise = @promises.pop
    promise.call
  end
end

#get_current_stateObject

Return an object that contains the assembler’s current state



79
80
81
82
83
# 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_current_state(struct) ⇒ Object

Set the current state from an OpenStruct



88
89
90
91
# 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_ines_header(ines_header) ⇒ Object

Set the iNES header



152
153
154
155
# File 'lib/n65.rb', line 152

def set_ines_header(ines_header)
  fail(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


126
127
128
129
130
131
132
133
134
135
136
# 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_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.
Bounds check is inside MemorySpace#write


143
144
145
146
147
# 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