n65 0.5.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ci.yml +28 -0
- data/.gitignore +1 -1
- data/.rubocop.yml +125 -0
- data/Gemfile +3 -1
- data/README.md +3 -20
- data/Rakefile +15 -1
- data/bin/n65 +2 -0
- data/data/opcodes.yaml +39 -39
- data/everdrive_transfer/everdrive.rb +175 -0
- data/examples/pulse_chord.asm +1 -1
- data/examples/scales.asm +182 -0
- data/lib/n65/directives/ascii.rb +4 -19
- data/lib/n65/directives/bytes.rb +20 -35
- data/lib/n65/directives/dw.rb +22 -36
- data/lib/n65/directives/enter_scope.rb +14 -30
- data/lib/n65/directives/exit_scope.rb +7 -17
- data/lib/n65/directives/inc.rb +14 -30
- data/lib/n65/directives/incbin.rb +6 -24
- data/lib/n65/directives/ines_header.rb +66 -27
- data/lib/n65/directives/label.rb +8 -21
- data/lib/n65/directives/org.rb +9 -19
- data/lib/n65/directives/segment.rb +5 -19
- data/lib/n65/directives/space.rb +6 -18
- data/lib/n65/front_end.rb +36 -39
- data/lib/n65/instruction.rb +123 -159
- data/lib/n65/instruction_base.rb +6 -18
- data/lib/n65/memory_space.rb +51 -71
- data/lib/n65/opcodes.rb +3 -5
- data/lib/n65/parser.rb +20 -38
- data/lib/n65/regexes.rb +20 -21
- data/lib/n65/symbol_table.rb +58 -89
- data/lib/n65/version.rb +3 -1
- data/lib/n65.rb +120 -121
- data/n65.gemspec +17 -12
- data/nes_lib/nes.sym +2 -2
- data/spec/.rubocop.yml +4 -0
- data/spec/assembler_spec.rb +84 -0
- data/spec/lib/n65/memory_space_spec.rb +147 -0
- data/spec/lib/n65/symbol_table_spec.rb +291 -0
- data/utils/opcode_table_to_yaml.rb +65 -67
- data.tar.gz.sig +0 -0
- metadata +84 -41
- metadata.gz.sig +0 -0
- data/examples/music_driver.asm +0 -202
- data/test/test_memory_space.rb +0 -82
- data/test/test_symbol_table.rb +0 -238
- data/utils/midi/Makefile +0 -3
- data/utils/midi/c_scale.mid +0 -0
- data/utils/midi/convert +0 -0
- data/utils/midi/guitar.mid +0 -0
- data/utils/midi/include/event.h +0 -93
- data/utils/midi/include/file.h +0 -57
- data/utils/midi/include/helpers.h +0 -14
- data/utils/midi/include/track.h +0 -45
- data/utils/midi/lil_melody.mid +0 -0
- data/utils/midi/mi_feabhra.mid +0 -0
- data/utils/midi/midi_to_nes.rb +0 -204
- data/utils/midi/source/convert.cpp +0 -16
- data/utils/midi/source/event.cpp +0 -96
- data/utils/midi/source/file.cpp +0 -37
- data/utils/midi/source/helpers.cpp +0 -46
- data/utils/midi/source/track.cpp +0 -37
data/lib/n65.rb
CHANGED
|
@@ -1,65 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'n65/version'
|
|
2
4
|
require_relative 'n65/symbol_table'
|
|
3
5
|
require_relative 'n65/memory_space'
|
|
4
6
|
require_relative 'n65/parser'
|
|
5
7
|
|
|
6
8
|
module N65
|
|
7
|
-
|
|
8
9
|
class Assembler
|
|
9
10
|
attr_reader :program_counter, :current_segment, :current_bank, :symbol_table, :virtual_memory, :promises
|
|
10
11
|
|
|
11
|
-
##### Custom exceptions
|
|
12
12
|
class AddressOutOfRange < StandardError; end
|
|
13
13
|
class InvalidSegment < StandardError; end
|
|
14
14
|
class WriteOutOfBounds < StandardError; end
|
|
15
15
|
class INESHeaderAlreadySet < StandardError; end
|
|
16
16
|
class FileNotFound < StandardError; end
|
|
17
17
|
|
|
18
|
+
State = Data.define(:program_counter, :segment, :bank, :scope)
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def self.from_file(infile,
|
|
22
|
-
|
|
20
|
+
# Assemble from an asm file to a NES ROM
|
|
21
|
+
# TODO: This really needs a logger instead of all these unless quiet conditions
|
|
22
|
+
def self.from_file(infile, options)
|
|
23
|
+
raise(FileNotFound, infile) unless File.exist?(infile)
|
|
23
24
|
|
|
24
|
-
assembler =
|
|
25
|
+
assembler = new
|
|
25
26
|
program = File.read(infile)
|
|
27
|
+
output_file = options[:output_file]
|
|
26
28
|
|
|
27
|
-
puts "Building #{infile}"
|
|
28
|
-
|
|
29
|
-
program.
|
|
29
|
+
puts "Building #{infile}" unless options[:quiet]
|
|
30
|
+
# Process each line in the file
|
|
31
|
+
program.each_line.each_with_index do |line, line_number|
|
|
30
32
|
begin
|
|
31
33
|
assembler.assemble_one_line(line)
|
|
32
34
|
rescue StandardError => e
|
|
33
|
-
|
|
35
|
+
warn("\n\n#{e.class}\n#{line}\n#{e}\nOn line #{line_number}")
|
|
34
36
|
exit(1)
|
|
35
37
|
end
|
|
36
|
-
print '.'
|
|
38
|
+
print '.' unless options[:quiet]
|
|
37
39
|
end
|
|
38
|
-
puts
|
|
40
|
+
puts unless options[:quiet]
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
print
|
|
42
|
+
# Second pass to resolve any missing symbols.
|
|
43
|
+
print 'Second pass, resolving symbols...' unless options[:quiet]
|
|
42
44
|
assembler.fulfill_promises
|
|
43
|
-
puts
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
## For right now, let's just emit the first prog bank
|
|
54
|
-
File.open(outfile, 'w') do |fp|
|
|
55
|
-
fp.write(assembler.emit_binary_rom)
|
|
45
|
+
puts ' Done.' unless options[:quiet]
|
|
46
|
+
|
|
47
|
+
# Optionally write out a symbol map
|
|
48
|
+
if options[:write_symbol_table]
|
|
49
|
+
print "Writing symbol table to #{output_file}.yaml..." unless options[:quiet]
|
|
50
|
+
File.open("#{output_file}.yaml", 'w') do |fp|
|
|
51
|
+
fp.write(assembler.symbol_table.export_to_yaml)
|
|
52
|
+
end
|
|
53
|
+
puts 'Done.' unless options[:quiet]
|
|
56
54
|
end
|
|
57
|
-
puts "All Done :)"
|
|
58
|
-
end
|
|
59
55
|
|
|
56
|
+
# Optionally write out cycle count for subroutines
|
|
57
|
+
if options[:cycle_count]
|
|
58
|
+
print "Writing subroutine cycle counts to #{output_file}.cycles.yaml..." unless options[:quiet]
|
|
59
|
+
File.open("#{output_file}.cycles.yaml", 'w') do |fp|
|
|
60
|
+
fp.write(assembler.symbol_table.export_cycle_count_yaml)
|
|
61
|
+
end
|
|
62
|
+
puts 'Done.' unless options[:quiet]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Emit the complete binary ROM
|
|
66
|
+
rom = assembler.emit_binary_rom
|
|
67
|
+
File.open(output_file, 'w') do |fp|
|
|
68
|
+
fp.write(rom)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
return if options[:quiet]
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
rom_size = rom.size
|
|
74
|
+
rom_size_hex = format('%x', rom_size)
|
|
75
|
+
assembler.print_bank_usage
|
|
76
|
+
puts "Total size: $#{rom_size_hex}, #{rom_size} bytes"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Initialize with a bank 1 of prog space for starters
|
|
63
80
|
def initialize
|
|
64
81
|
@ines_header = nil
|
|
65
82
|
@program_counter = 0x0
|
|
@@ -68,176 +85,158 @@ module N65
|
|
|
68
85
|
@symbol_table = SymbolTable.new
|
|
69
86
|
@promises = []
|
|
70
87
|
@virtual_memory = {
|
|
71
|
-
:
|
|
72
|
-
:
|
|
88
|
+
prog: [MemorySpace.create_prog_rom],
|
|
89
|
+
char: []
|
|
73
90
|
}
|
|
74
91
|
end
|
|
75
92
|
|
|
76
|
-
|
|
77
|
-
####
|
|
78
|
-
## Return an object that contains the assembler's current state
|
|
93
|
+
# Return an object that contains the assembler's current state
|
|
79
94
|
def get_current_state
|
|
80
95
|
saved_program_counter, saved_segment, saved_bank = @program_counter, @current_segment, @current_bank
|
|
81
96
|
saved_scope = symbol_table.scope_stack.dup
|
|
82
|
-
|
|
97
|
+
State.new(saved_program_counter, saved_segment, saved_bank, saved_scope)
|
|
83
98
|
end
|
|
84
99
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
@program_counter, @current_segment, @current_bank = struct.program_counter, struct.segment, struct.bank
|
|
90
|
-
symbol_table.scope_stack = struct.scope.dup
|
|
100
|
+
# Set the current state
|
|
101
|
+
def set_current_state(state)
|
|
102
|
+
@program_counter, @current_segment, @current_bank = state.program_counter, state.segment, state.bank
|
|
103
|
+
symbol_table.scope_stack = state.scope.dup
|
|
91
104
|
end
|
|
92
105
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
## emitting bytes into our virtual memory spaces. Empty lines or lines
|
|
99
|
-
## with only comments parse to nil, and we just ignore them.
|
|
106
|
+
# This is the main assemble method, it parses one line into an object
|
|
107
|
+
# which when given a reference to this assembler, controls the assembler
|
|
108
|
+
# itself through public methods, executing assembler directives, and
|
|
109
|
+
# emitting bytes into our virtual memory spaces. Empty lines or lines
|
|
110
|
+
# with only comments parse to nil, and we just ignore them.
|
|
100
111
|
def assemble_one_line(line)
|
|
101
112
|
parsed_object = Parser.parse(line)
|
|
113
|
+
return if parsed_object.nil?
|
|
102
114
|
|
|
103
|
-
|
|
104
|
-
exec_result = parsed_object.exec(self)
|
|
115
|
+
exec_result = parsed_object.exec(self)
|
|
105
116
|
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
# TODO: I could perhaps keep a tally of cycles used per top level scope here
|
|
118
|
+
if parsed_object.respond_to?(:cycles)
|
|
119
|
+
@symbol_table.add_cycles(parsed_object.cycles)
|
|
108
120
|
end
|
|
109
|
-
end
|
|
110
121
|
|
|
122
|
+
# If we have returned a promise save it for the second pass
|
|
123
|
+
@promises << exec_result if exec_result.is_a?(Proc)
|
|
124
|
+
end
|
|
111
125
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
while promise = @promises.pop
|
|
117
|
-
promise.call
|
|
126
|
+
# Assemble the given string
|
|
127
|
+
def assemble_string(string)
|
|
128
|
+
string.each_line do |line|
|
|
129
|
+
assemble_one_line(line)
|
|
118
130
|
end
|
|
131
|
+
fulfill_promises
|
|
132
|
+
self
|
|
119
133
|
end
|
|
120
134
|
|
|
135
|
+
# This will empty out our promise queue and try to fullfil operations
|
|
136
|
+
# that required an undefined symbol when first encountered.
|
|
137
|
+
def fulfill_promises
|
|
138
|
+
@promises.pop.call while @promises.any?
|
|
139
|
+
end
|
|
121
140
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
## a symbol right now, and want to try during the second pass
|
|
141
|
+
# This rewinds the state of the assembler, so a promise can be
|
|
142
|
+
# executed with a previous state, for example if we can't resolve
|
|
143
|
+
# a symbol right now, and want to try during the second pass
|
|
126
144
|
def with_saved_state(&block)
|
|
127
|
-
|
|
128
|
-
old_state = get_current_state
|
|
129
|
-
|
|
145
|
+
saved_state = get_current_state
|
|
130
146
|
lambda do
|
|
131
|
-
|
|
132
|
-
## Set the assembler state back to the old state and run the block like that
|
|
133
|
-
set_current_state(old_state)
|
|
147
|
+
set_current_state(saved_state)
|
|
134
148
|
block.call(self)
|
|
135
149
|
end
|
|
136
150
|
end
|
|
137
151
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
## Write to memory space. Typically, we are going to want to write
|
|
141
|
-
## to the location of the current PC, current segment, and current bank.
|
|
142
|
-
## Bounds check is inside MemorySpace#write
|
|
152
|
+
# Write to memory space. Typically, we are going to want to write
|
|
153
|
+
# to the location of the current PC, current segment, and current bank.
|
|
143
154
|
def write_memory(bytes, pc = @program_counter, segment = @current_segment, bank = @current_bank)
|
|
144
155
|
memory_space = get_virtual_memory_space(segment, bank)
|
|
145
156
|
memory_space.write(pc, bytes)
|
|
146
157
|
@program_counter += bytes.size
|
|
147
158
|
end
|
|
148
159
|
|
|
149
|
-
|
|
150
|
-
####
|
|
151
|
-
## Set the iNES header
|
|
160
|
+
# Set the iNES header
|
|
152
161
|
def set_ines_header(ines_header)
|
|
153
|
-
|
|
162
|
+
raise(INESHeaderAlreadySet) unless @ines_header.nil?
|
|
163
|
+
|
|
154
164
|
@ines_header = ines_header
|
|
155
165
|
end
|
|
156
166
|
|
|
157
|
-
|
|
158
|
-
####
|
|
159
|
-
## Set the program counter
|
|
167
|
+
# Set the program counter
|
|
160
168
|
def program_counter=(address)
|
|
161
|
-
|
|
169
|
+
raise(AddressOutOfRange) unless address_within_range?(address)
|
|
170
|
+
|
|
162
171
|
@program_counter = address
|
|
163
172
|
end
|
|
164
173
|
|
|
165
|
-
|
|
166
|
-
####
|
|
167
|
-
## Set the current segment, prog or char.
|
|
174
|
+
# Set the current segment, prog or char.
|
|
168
175
|
def current_segment=(segment)
|
|
169
176
|
segment = segment.to_sym
|
|
170
|
-
unless valid_segment?(segment)
|
|
171
|
-
|
|
172
|
-
end
|
|
177
|
+
raise(InvalidSegment, "#{segment} is not a valid segment. Try prog or char") unless valid_segment?(segment)
|
|
178
|
+
|
|
173
179
|
@current_segment = segment
|
|
174
180
|
end
|
|
175
181
|
|
|
176
|
-
|
|
177
|
-
####
|
|
178
|
-
## Set the current bank, create it if it does not exist
|
|
182
|
+
# Set the current bank, create it if it does not exist
|
|
179
183
|
def current_bank=(bank_number)
|
|
180
184
|
memory_space = get_virtual_memory_space(@current_segment, bank_number)
|
|
181
|
-
if memory_space.nil?
|
|
182
|
-
@virtual_memory[@current_segment][bank_number] = MemorySpace.create_bank(@current_segment)
|
|
183
|
-
end
|
|
185
|
+
@virtual_memory[@current_segment][bank_number] = MemorySpace.create_bank(@current_segment) if memory_space.nil?
|
|
184
186
|
@current_bank = bank_number
|
|
185
187
|
end
|
|
186
188
|
|
|
187
|
-
|
|
188
|
-
####
|
|
189
|
-
## Emit a binary ROM
|
|
190
189
|
def emit_binary_rom
|
|
191
190
|
progs = @virtual_memory[:prog]
|
|
192
191
|
chars = @virtual_memory[:char]
|
|
193
|
-
puts "iNES Header"
|
|
194
|
-
puts "+ #{progs.size} PROG ROM bank#{progs.size != 1 ? 's' : ''}"
|
|
195
|
-
puts "+ #{chars.size} CHAR ROM bank#{chars.size != 1 ? 's' : ''}"
|
|
196
192
|
|
|
197
|
-
rom_size = 0x10
|
|
198
|
-
rom_size += MemorySpace::
|
|
199
|
-
rom_size += MemorySpace::
|
|
193
|
+
rom_size = 0x10
|
|
194
|
+
rom_size += MemorySpace::BANK_SIZES[:prog] * progs.size
|
|
195
|
+
rom_size += MemorySpace::BANK_SIZES[:char] * chars.size
|
|
200
196
|
|
|
201
|
-
puts "= Output ROM will be #{rom_size} bytes"
|
|
202
197
|
rom = MemorySpace.new(rom_size, :rom)
|
|
203
198
|
|
|
204
199
|
offset = 0x0
|
|
205
200
|
offset += rom.write(0x0, @ines_header.emit_bytes)
|
|
206
201
|
|
|
207
202
|
progs.each do |prog|
|
|
208
|
-
offset += rom.write(offset, prog.read(0x8000, MemorySpace::
|
|
203
|
+
offset += rom.write(offset, prog.read(0x8000, MemorySpace::BANK_SIZES[:prog]))
|
|
209
204
|
end
|
|
210
205
|
|
|
211
206
|
chars.each do |char|
|
|
212
|
-
offset += rom.write(offset, char.read(0x0, MemorySpace::
|
|
207
|
+
offset += rom.write(offset, char.read(0x0, MemorySpace::BANK_SIZES[:char]))
|
|
213
208
|
end
|
|
214
209
|
rom.emit_bytes.pack('C*')
|
|
215
210
|
end
|
|
216
211
|
|
|
212
|
+
# TODO: Use StringIO to build output
|
|
213
|
+
def print_bank_usage
|
|
214
|
+
puts
|
|
215
|
+
puts 'ROM Structure {'
|
|
216
|
+
puts ' iNES 1.0 Header: $10 bytes'
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
@virtual_memory[:prog].each_with_index do |prog_rom, bank_number|
|
|
219
|
+
puts " PROG ROM bank #{bank_number}: #{prog_rom.usage_info}"
|
|
220
|
+
end
|
|
219
221
|
|
|
222
|
+
@virtual_memory[:char].each_with_index do |char_rom, bank_number|
|
|
223
|
+
puts " CHAR ROM bank #{bank_number}: #{char_rom.usage_info}"
|
|
224
|
+
end
|
|
225
|
+
puts '}'
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
private
|
|
220
229
|
|
|
221
|
-
####
|
|
222
|
-
## Get virtual memory space
|
|
223
230
|
def get_virtual_memory_space(segment, bank_number)
|
|
224
231
|
@virtual_memory[segment][bank_number]
|
|
225
232
|
end
|
|
226
233
|
|
|
227
|
-
|
|
228
|
-
####
|
|
229
|
-
## Is this a 16-bit address within range?
|
|
230
234
|
def address_within_range?(address)
|
|
231
235
|
address >= 0 && address < 2**16
|
|
232
236
|
end
|
|
233
237
|
|
|
234
|
-
|
|
235
|
-
####
|
|
236
|
-
## Is this a valid segment?
|
|
237
238
|
def valid_segment?(segment)
|
|
238
|
-
[
|
|
239
|
+
%i[prog char].include?(segment)
|
|
239
240
|
end
|
|
240
|
-
|
|
241
241
|
end
|
|
242
|
-
|
|
243
242
|
end
|
data/n65.gemspec
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
5
|
require 'n65/version'
|
|
5
6
|
|
|
6
7
|
Gem::Specification.new do |spec|
|
|
7
|
-
spec.name =
|
|
8
|
+
spec.name = 'n65'
|
|
8
9
|
spec.version = N65::VERSION
|
|
9
|
-
spec.authors = [
|
|
10
|
-
spec.email = [
|
|
11
|
-
spec.summary =
|
|
12
|
-
spec.description =
|
|
13
|
-
spec.homepage =
|
|
14
|
-
spec.license =
|
|
10
|
+
spec.authors = ['Safiire']
|
|
11
|
+
spec.email = ['safiire@irkenkitties.com']
|
|
12
|
+
spec.summary = 'An NES assembler for the 6502 microprocessor'
|
|
13
|
+
spec.description = 'An NES assembler for the 6502 microprocessor in pure Ruby'
|
|
14
|
+
spec.homepage = 'http://github.com/safiire/n65'
|
|
15
|
+
spec.license = 'GPL-2.0-or-later'
|
|
15
16
|
|
|
16
17
|
spec.files = `git ls-files -z`.split("\x0")
|
|
17
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
|
-
spec.require_paths = [
|
|
20
|
+
spec.require_paths = ['lib']
|
|
21
|
+
|
|
22
|
+
spec.required_ruby_version = '>= 3.2.0'
|
|
20
23
|
|
|
21
|
-
spec.add_development_dependency
|
|
22
|
-
spec.add_development_dependency
|
|
24
|
+
spec.add_development_dependency 'bundler'
|
|
25
|
+
spec.add_development_dependency 'rake'
|
|
26
|
+
spec.add_development_dependency 'rspec'
|
|
27
|
+
spec.add_development_dependency 'rubocop'
|
|
23
28
|
end
|
data/nes_lib/nes.sym
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
2
2
|
; Let's start a little library for naming parts of memory in the NES
|
|
3
|
-
;
|
|
3
|
+
;
|
|
4
4
|
; Including this file will not emit any instructions or data into your binary
|
|
5
5
|
; It only defines symbols in the symbol table to name memory addresses
|
|
6
6
|
;
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
.space controller2 1 ; $4017
|
|
76
76
|
.scope mapper
|
|
77
77
|
.org $8000
|
|
78
|
-
.space unrom 1
|
|
78
|
+
.space unrom 1
|
|
79
79
|
.org $8000
|
|
80
80
|
.space cnrom 1
|
|
81
81
|
.org $8000
|
data/spec/.rubocop.yml
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest/sha2'
|
|
4
|
+
require_relative '../lib/n65'
|
|
5
|
+
|
|
6
|
+
RSpec.describe(N65::Assembler) do
|
|
7
|
+
describe 'assembling example files' do
|
|
8
|
+
let(:examples) do
|
|
9
|
+
File.join(__dir__, '..', 'examples')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
let(:source) do
|
|
13
|
+
File.read(filename)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
let(:assembler) do
|
|
17
|
+
described_class.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
let(:binary) do
|
|
21
|
+
Dir.chdir(examples) do
|
|
22
|
+
assembler.assemble_string(source).emit_binary_rom
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
let(:sha256) do
|
|
27
|
+
Digest::SHA256.hexdigest(binary)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context 'when assembling beep.asm' do
|
|
31
|
+
let(:filename) { 'beep.asm' }
|
|
32
|
+
let(:expected) { 'afce4dd85323d86dd6b2f8e4c9e7d704607b0480c3cf087bd8f66b37b8ddf269' }
|
|
33
|
+
|
|
34
|
+
it 'have the correct sha256 digest' do
|
|
35
|
+
expect(sha256).to eq(expected)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context 'when assembling demo.asm' do
|
|
40
|
+
let(:filename) { 'demo.asm' }
|
|
41
|
+
let(:expected) { 'ff687730a0a4022519b177c1cd92f18eaafaa2e9606f9a579371e9c62ffcb726' }
|
|
42
|
+
|
|
43
|
+
it 'have the correct sha256 digest' do
|
|
44
|
+
expect(sha256).to eq(expected)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context 'when assembling scales.asm' do
|
|
49
|
+
let(:filename) { 'scales.asm' }
|
|
50
|
+
let(:expected) { '2c6c586f3a9d35212a1a02e064a8bb71ec26ac857ed6572f217c10161f2de102' }
|
|
51
|
+
|
|
52
|
+
it 'have the correct sha256 digest' do
|
|
53
|
+
expect(sha256).to eq(expected)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
context 'when assembling pulse_chord.asm' do
|
|
58
|
+
let(:filename) { 'pulse_chord.asm' }
|
|
59
|
+
let(:expected) { '77712d504edbe9b30261939a9d72f51e14ee3029da04287b879dffbb36eb9b12' }
|
|
60
|
+
|
|
61
|
+
it 'have the correct sha256 digest' do
|
|
62
|
+
expect(sha256).to eq(expected)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context 'when assembling noise.asm' do
|
|
67
|
+
let(:filename) { 'noise.asm' }
|
|
68
|
+
let(:expected) { 'ea790d892ecd2bf76f711fbcb8a2f8e9b4dbaa5dffb56b691924c81666d23673' }
|
|
69
|
+
|
|
70
|
+
it 'have the correct sha256 digest' do
|
|
71
|
+
expect(sha256).to eq(expected)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'when assembling mario2.asm' do
|
|
76
|
+
let(:filename) { 'mario2.asm' }
|
|
77
|
+
let(:expected) { '0974b799eac7ffb8835fcbd518a5cdeeec734d5d9476cd11cb6b75491c6ef488' }
|
|
78
|
+
|
|
79
|
+
it 'have the correct sha256 digest' do
|
|
80
|
+
expect(sha256).to eq(expected)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../../lib/n65/memory_space'
|
|
4
|
+
|
|
5
|
+
RSpec.describe(N65::MemorySpace) do
|
|
6
|
+
describe '.new' do
|
|
7
|
+
context 'when provided a size and type' do
|
|
8
|
+
let(:bank) { described_class.new(size, type) }
|
|
9
|
+
let(:size) { 0x100 }
|
|
10
|
+
let(:type) { :prog }
|
|
11
|
+
|
|
12
|
+
it 'returns a zeroed bank' do
|
|
13
|
+
expect(bank.emit_bytes.all?(&:zero?)).to eq(true)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'returns the requested sized bank' do
|
|
17
|
+
expect(bank.emit_bytes.size).to be(size)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '.create_prog_rom' do
|
|
23
|
+
context 'when creating a new prog rom' do
|
|
24
|
+
let(:bank) { described_class.create_prog_rom }
|
|
25
|
+
|
|
26
|
+
it 'returns a zeroed bank' do
|
|
27
|
+
expect(bank.emit_bytes.all?(&:zero?)).to eq(true)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'returns the correct sized bank' do
|
|
31
|
+
expect(bank.emit_bytes.size).to eq(0x4000)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe '.create_char_rom' do
|
|
37
|
+
context 'when creating a new prog rom' do
|
|
38
|
+
let(:bank) { described_class.create_char_rom }
|
|
39
|
+
|
|
40
|
+
it 'returns a zeroed bank' do
|
|
41
|
+
expect(bank.emit_bytes.all?(&:zero?)).to eq(true)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'returns the correct sized bank' do
|
|
45
|
+
expect(bank.emit_bytes.size).to eq(0x2000)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#read prog rom' do
|
|
51
|
+
let(:address) { 0xC100 }
|
|
52
|
+
let(:mirroed_address) { 0x8100 }
|
|
53
|
+
let(:bank) { described_class.create_prog_rom }
|
|
54
|
+
let(:data) { 'hi there'.bytes }
|
|
55
|
+
|
|
56
|
+
before { bank.write(address, data) }
|
|
57
|
+
|
|
58
|
+
context 'when reading from the bank' do
|
|
59
|
+
it 'can read back from the 0xC000 base address' do
|
|
60
|
+
expect(bank.read(address, data.size)).to eq(data)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'can read back from the 0x8000 mirrored address' do
|
|
64
|
+
expect(bank.read(mirroed_address, data.size)).to eq(data)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
context 'when attempting to read out of bounds' do
|
|
69
|
+
it 'throws an error' do
|
|
70
|
+
expect { bank.read(0x0, 0x10) }.to raise_error(described_class::AccessOutsideProgRom)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'throws an error' do
|
|
74
|
+
expect { bank.read(0xffff, 0x10) }.to raise_error(described_class::AccessOutsideProgRom)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe '#read char rom' do
|
|
80
|
+
let(:address) { 0x100 }
|
|
81
|
+
let(:bank) { described_class.create_char_rom }
|
|
82
|
+
let(:data) { 'hi there'.bytes }
|
|
83
|
+
|
|
84
|
+
before { bank.write(address, data) }
|
|
85
|
+
|
|
86
|
+
context 'when reading from the bank' do
|
|
87
|
+
it 'can read from the bank' do
|
|
88
|
+
expect(bank.read(address, data.size)).to eq(data)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
context 'when attempting to read out of bounds' do
|
|
93
|
+
it 'throws an error' do
|
|
94
|
+
expect { bank.read(0x2000, 0x10) }.to raise_error(described_class::AccessOutsideCharRom)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe '#write prog rom' do
|
|
100
|
+
let(:address) { 0xC100 }
|
|
101
|
+
let(:mirroed_address) { 0x8100 }
|
|
102
|
+
let(:bank) { described_class.create_prog_rom }
|
|
103
|
+
let(:data) { 'hi there'.bytes }
|
|
104
|
+
|
|
105
|
+
before { bank.write(mirroed_address, data) }
|
|
106
|
+
|
|
107
|
+
context 'when reading from the bank' do
|
|
108
|
+
it 'can read back from the 0xC000 base address' do
|
|
109
|
+
expect(bank.read(address, data.size)).to eq(data)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'can read back from the 0x8000 mirrored address' do
|
|
113
|
+
expect(bank.read(mirroed_address, data.size)).to eq(data)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
context 'when attempting to write out of bounds' do
|
|
118
|
+
it 'throws an error' do
|
|
119
|
+
expect { bank.write(0x0, data) }.to raise_error(described_class::AccessOutsideProgRom)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'throws an error' do
|
|
123
|
+
expect { bank.write(0xffff, data) }.to raise_error(described_class::AccessOutsideProgRom)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
describe '#write char rom' do
|
|
129
|
+
let(:address) { 0x100 }
|
|
130
|
+
let(:bank) { described_class.create_char_rom }
|
|
131
|
+
let(:data) { 'hi there'.bytes }
|
|
132
|
+
|
|
133
|
+
before { bank.write(address, data) }
|
|
134
|
+
|
|
135
|
+
context 'when reading from the bank' do
|
|
136
|
+
it 'can read from the bank' do
|
|
137
|
+
expect(bank.read(address, data.size)).to eq(data)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
context 'when attempting to write out of bounds' do
|
|
142
|
+
it 'throws an error' do
|
|
143
|
+
expect { bank.write(0x2000, data) }.to raise_error(described_class::AccessOutsideCharRom)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|