n65 0.5.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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +340 -0
  5. data/README.md +126 -0
  6. data/Rakefile +2 -0
  7. data/bin/n65 +11 -0
  8. data/data/opcodes.yaml +1030 -0
  9. data/examples/beep.asm +24 -0
  10. data/examples/mario2.asm +260 -0
  11. data/examples/mario2.char +0 -0
  12. data/examples/music_driver.asm +202 -0
  13. data/examples/noise.asm +93 -0
  14. data/examples/pulse_chord.asm +213 -0
  15. data/images/assembler_demo.png +0 -0
  16. data/lib/n65.rb +243 -0
  17. data/lib/n65/directives/ascii.rb +42 -0
  18. data/lib/n65/directives/bytes.rb +102 -0
  19. data/lib/n65/directives/dw.rb +86 -0
  20. data/lib/n65/directives/enter_scope.rb +55 -0
  21. data/lib/n65/directives/exit_scope.rb +35 -0
  22. data/lib/n65/directives/inc.rb +67 -0
  23. data/lib/n65/directives/incbin.rb +51 -0
  24. data/lib/n65/directives/ines_header.rb +53 -0
  25. data/lib/n65/directives/label.rb +46 -0
  26. data/lib/n65/directives/org.rb +47 -0
  27. data/lib/n65/directives/segment.rb +45 -0
  28. data/lib/n65/directives/space.rb +46 -0
  29. data/lib/n65/front_end.rb +90 -0
  30. data/lib/n65/instruction.rb +308 -0
  31. data/lib/n65/instruction_base.rb +29 -0
  32. data/lib/n65/memory_space.rb +150 -0
  33. data/lib/n65/opcodes.rb +9 -0
  34. data/lib/n65/parser.rb +85 -0
  35. data/lib/n65/regexes.rb +33 -0
  36. data/lib/n65/symbol_table.rb +198 -0
  37. data/lib/n65/version.rb +3 -0
  38. data/n65.gemspec +23 -0
  39. data/nes_lib/nes.sym +105 -0
  40. data/test/test_memory_space.rb +82 -0
  41. data/test/test_symbol_table.rb +238 -0
  42. data/utils/midi/Makefile +3 -0
  43. data/utils/midi/c_scale.mid +0 -0
  44. data/utils/midi/convert +0 -0
  45. data/utils/midi/guitar.mid +0 -0
  46. data/utils/midi/include/event.h +93 -0
  47. data/utils/midi/include/file.h +57 -0
  48. data/utils/midi/include/helpers.h +14 -0
  49. data/utils/midi/include/track.h +45 -0
  50. data/utils/midi/lil_melody.mid +0 -0
  51. data/utils/midi/mi_feabhra.mid +0 -0
  52. data/utils/midi/midi_to_nes.rb +204 -0
  53. data/utils/midi/source/convert.cpp +16 -0
  54. data/utils/midi/source/event.cpp +96 -0
  55. data/utils/midi/source/file.cpp +37 -0
  56. data/utils/midi/source/helpers.cpp +46 -0
  57. data/utils/midi/source/track.cpp +37 -0
  58. data/utils/opcode_table_to_yaml.rb +91 -0
  59. metadata +133 -0
@@ -0,0 +1,46 @@
1
+
2
+ module N65
3
+
4
+ ####
5
+ ## This class represents a label, and will create
6
+ ## an entry in the symbol table associated with
7
+ ## the address it appears at.
8
+ class Label
9
+
10
+ ####
11
+ ## Try to parse as a label
12
+ def self.parse(line)
13
+ match_data = line.match(/^([a-zA-Z][a-zA-Z0-9_]+):$/)
14
+ unless match_data.nil?
15
+ label = match_data[1].to_sym
16
+ return self.new(label)
17
+ end
18
+ nil
19
+ end
20
+
21
+
22
+ ####
23
+ ## Create a new label object
24
+ def initialize(symbol)
25
+ @symbol = symbol
26
+ end
27
+
28
+
29
+ ####
30
+ ## Create an entry in the symbol table for this label
31
+ def exec(assembler)
32
+ program_counter = assembler.program_counter
33
+ assembler.symbol_table.define_symbol(@symbol, program_counter)
34
+ end
35
+
36
+
37
+ ####
38
+ ## Display
39
+ def to_s
40
+ "#{@symbol}:"
41
+ end
42
+
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,47 @@
1
+ require_relative '../instruction_base'
2
+
3
+ module N65
4
+
5
+ ####
6
+ ## This is an .org directive
7
+ class Org < InstructionBase
8
+ attr_reader :address
9
+
10
+ ####
11
+ ## Try to parse an .org statement
12
+ def self.parse(line)
13
+ match_data = line.match(/^\.org\s+\$([0-9A-Fa-f]{4})$/)
14
+ return nil if match_data.nil?
15
+ address = match_data[1].to_i(16)
16
+ Org.new(address)
17
+ end
18
+
19
+
20
+ ####
21
+ ## Initialized with address to switch to
22
+ def initialize(address)
23
+ @address = address
24
+ end
25
+
26
+
27
+ ####
28
+ ## Exec this directive on the assembler
29
+ def exec(assembler)
30
+ assembler.program_counter = address
31
+ end
32
+
33
+
34
+ ####
35
+ ## Display
36
+ def to_s
37
+ if @address <= 0xff
38
+ ".org $%2.X" % @address
39
+ else
40
+ ".org $%4.X" % @address
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+
@@ -0,0 +1,45 @@
1
+ require_relative '../instruction_base'
2
+ module N65
3
+
4
+
5
+ ####
6
+ ## This directive instruction can include a binary file
7
+ class Segment < InstructionBase
8
+
9
+ ####
10
+ ## Try to parse a dw directive
11
+ def self.parse(line)
12
+ match_data = line.match(/^.segment (prog|char) (\d+)$/i)
13
+ unless match_data.nil?
14
+ _, segment, bank = match_data.to_a
15
+ return Segment.new(segment, bank.to_i)
16
+ end
17
+ nil
18
+ end
19
+
20
+
21
+ ####
22
+ ## Initialize with filename
23
+ def initialize(segment, bank)
24
+ @bank = bank
25
+ @segment = segment
26
+ end
27
+
28
+
29
+ ####
30
+ ## Execute the segment and bank change on the assembler
31
+ def exec(assembler)
32
+ assembler.current_segment = @segment
33
+ assembler.current_bank = @bank
34
+ end
35
+
36
+
37
+ ####
38
+ ## Display
39
+ def to_s
40
+ ".segment #{@segment} #{@bank}"
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,46 @@
1
+ require_relative '../instruction_base'
2
+
3
+ module N65
4
+
5
+
6
+ ####
7
+ ## This directive gives a symbolic name for memory and creates space for a variable in RAM
8
+ class Space < InstructionBase
9
+
10
+ ####
11
+ ## Try to parse a .space directive
12
+ def self.parse(line)
13
+ match_data = line.match(/^.space\s+([a-zA-Z]?[a-zA-Z0-9_]+?)\s+([0-9]+)$/)
14
+ return nil if match_data.nil?
15
+ _, name, size = match_data.to_a
16
+
17
+ Space.new(name, size.to_i)
18
+ end
19
+
20
+
21
+ ####
22
+ ## Initialize some memory space with a name
23
+ def initialize(name, size)
24
+ @name = name
25
+ @size = size
26
+ end
27
+
28
+
29
+ ####
30
+ ## .space creates a symbol at the current PC, and then advances PC by size
31
+ def exec(assembler)
32
+ program_counter = assembler.program_counter
33
+ assembler.symbol_table.define_symbol(@name, program_counter)
34
+ assembler.program_counter += @size
35
+ end
36
+
37
+
38
+ ####
39
+ ## Display
40
+ def to_s
41
+ ".space #{@name} #{@size}"
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,90 @@
1
+ require 'optparse'
2
+ require_relative '../n65'
3
+
4
+ module N65
5
+
6
+ ####
7
+ ## This class handles the front end aspects,
8
+ ## parsing the commandline options and running the assembler
9
+ class FrontEnd
10
+
11
+ ####
12
+ ## Initialize with ARGV commandline
13
+ def initialize(argv)
14
+ @options = {:output_file => nil}
15
+ @argv = argv.dup
16
+ end
17
+
18
+
19
+ ####
20
+ ## Run the assembler
21
+ def run
22
+ ## First use the option parser
23
+ parser = create_option_parser
24
+ parser.parse!(@argv)
25
+
26
+ ## Whatever is leftover in argv the input files
27
+ if @argv.size.zero?
28
+ STDERR.puts("No input files")
29
+ exit(1)
30
+ end
31
+
32
+ ## Only can assemble one file at once for now
33
+ if @argv.size != 1
34
+ STDERR.puts "Can only assemble one input file at once :("
35
+ exit(1)
36
+ end
37
+
38
+ input_file = @argv.shift
39
+
40
+ ## Make sure the input file exists
41
+ unless File.exists?(input_file)
42
+ STDERR.puts "Input file #{input_file} does not exist"
43
+ exit(1)
44
+ end
45
+
46
+ ## Maybe they didn't provide an output file name, so we'll guess
47
+ if @options[:output_file].nil?
48
+ ext = File.extname(input_file)
49
+ @options[:output_file] = input_file.gsub(ext, '') + '.nes'
50
+ end
51
+
52
+ if @options.values.any?(&:nil?)
53
+ STDERR.puts "Missing options try --help"
54
+ exit(1)
55
+ end
56
+
57
+ #begin
58
+ N65::Assembler.from_file(input_file, @options[:output_file])
59
+ #rescue StandardError => error
60
+ #STDERR.puts("Assemble Failed!")
61
+ #STDERR.puts(error.class)
62
+ #if error.message
63
+ #STDERR.puts(error.message)
64
+ #end
65
+ #exit(1)
66
+ #end
67
+ end
68
+
69
+ private
70
+
71
+ ####
72
+ ## Create a commandline option parser
73
+ def create_option_parser
74
+ OptionParser.new do |opts|
75
+ opts.banner = "Usage: #{$0} [options] <input_file.asm>"
76
+
77
+ opts.on('-o', '--outfile filename', 'outfile') do |output_file|
78
+ @options[:output_file] = output_file;
79
+ end
80
+
81
+ opts.on('-h', '--help', 'Displays Help') do
82
+ puts opts
83
+ exit
84
+ end
85
+ end
86
+ end
87
+
88
+ end
89
+
90
+ end
@@ -0,0 +1,308 @@
1
+ require_relative 'opcodes'
2
+ require_relative 'regexes'
3
+
4
+ module N65
5
+
6
+ ####
7
+ ## Represents a single 6502 Instruction
8
+ class Instruction
9
+ attr_reader :op, :arg, :mode, :hex, :description, :length, :cycle, :boundry_add, :flags, :address
10
+
11
+ ## Custom Exceptions
12
+ class InvalidInstruction < StandardError; end
13
+ class UnresolvedSymbols < StandardError; end
14
+ class InvalidAddressingMode < StandardError; end
15
+ class AddressOutOfRange < StandardError; end
16
+ class ArgumentTooLarge < StandardError; end
17
+
18
+ ## Include Regexes
19
+ include Regexes
20
+
21
+ AddressingModes = {
22
+ :relative => {
23
+ :example => 'B** my_label',
24
+ :display => '%s $%.4X',
25
+ :regex => /$^/i, # Will never match this one
26
+ :regex_label => /^#{Branches}\s+#{Sym}$/
27
+ },
28
+
29
+ :immediate => {
30
+ :example => 'AAA #$FF',
31
+ :display => '%s #$%.2X',
32
+ :regex => /^#{Mnemonic}\s+#{Immediate}$/,
33
+ :regex_label => /^#{Mnemonic}\s+#(<|>)#{Sym}$/
34
+ },
35
+
36
+ :implied => {
37
+ :example => 'AAA',
38
+ :display => '%s',
39
+ :regex => /^#{Mnemonic}$/
40
+ },
41
+
42
+ :zero_page => {
43
+ :example => 'AAA $FF',
44
+ :display => '%s $%.2X',
45
+ :regex => /^#{Mnemonic}\s+#{Num8}$/,
46
+ :regex_label => /^#{Mnemonic}\s+#{Sym}\s+zp$/
47
+ },
48
+
49
+ :zero_page_x => {
50
+ :example => 'AAA $FF, X',
51
+ :display => '%s $%.2X, X',
52
+ :regex => /^#{Mnemonic}\s+#{Num8}\s?,\s?#{XReg}$/,
53
+ :regex_label => /^#{Mnemonic}\s+#{Sym}\s?,\s?#{XReg}\s+zp$/
54
+ },
55
+
56
+ :zero_page_y => {
57
+ :example => 'AAA $FF, Y',
58
+ :display => '%s $%.2X, Y',
59
+ :regex => /^#{Mnemonic}\s+#{Num8}\s?,\s?#{YReg}$/,
60
+ :regex_label => /^#{Mnemonic}\s+#{Sym}\s?,\s?#{YReg} zp$/
61
+ },
62
+
63
+ :absolute => {
64
+ :example => 'AAA $FFFF',
65
+ :display => '%s $%.4X',
66
+ :regex => /^#{Mnemonic}\s+#{Num16}$/,
67
+ :regex_label => /^#{Mnemonic}\s+#{Sym}$/
68
+ },
69
+
70
+ :absolute_x => {
71
+ :example => 'AAA $FFFF, X',
72
+ :display => '%s $%.4X, X',
73
+ :regex => /^#{Mnemonic}\s+#{Num16}\s?,\s?#{XReg}$/,
74
+ :regex_label => /^#{Mnemonic}\s+#{Sym}\s?,\s?#{XReg}$/
75
+ },
76
+
77
+ :absolute_y => {
78
+ :example => 'AAA $FFFF, Y',
79
+ :display => '%s $%.4X, Y',
80
+ :regex => /^#{Mnemonic}\s+#{Num16}\s?,\s?#{YReg}$/,
81
+ :regex_label => /^#{Mnemonic}\s+#{Sym}\s?,\s?#{YReg}$/
82
+ },
83
+
84
+ :indirect => {
85
+ :example => 'AAA ($FFFF)',
86
+ :display => '%s ($%.4X)',
87
+ :regex => /^#{Mnemonic}\s+\(#{Num16}\)$/,
88
+ :regex_label => /^#{Mnemonic}\s+\(#{Sym}\)$/
89
+ },
90
+
91
+ :indirect_x => {
92
+ :example => 'AAA ($FF, X)',
93
+ :display => '%s ($%.2X, X)',
94
+ :regex => /^#{Mnemonic}\s+\(#{Num8}\s?,\s?#{XReg}\)$/,
95
+ :regex_label => /^#{Mnemonic}\s+\(#{Sym}\s?,\s?#{XReg}\)$/
96
+ },
97
+
98
+ :indirect_y => {
99
+ :example => 'AAA ($FF), Y)',
100
+ :display => '%s ($%.2X), Y',
101
+ :regex => /^#{Mnemonic}\s+\(#{Num8}\)\s?,\s?#{YReg}$/,
102
+ :regex_label => /^#{Mnemonic}\s+\(#{Sym}\)\s?,\s?#{YReg}$/
103
+ }
104
+ }
105
+
106
+ ####
107
+ ## Parse one line of assembly, returns nil if the line
108
+ ## is ultimately empty of asm instructions
109
+ ## Raises SyntaxError if the line is malformed in some way
110
+ def self.parse(line)
111
+
112
+ ## Try to parse this line in each addressing mode
113
+ AddressingModes.each do |mode, parse_info|
114
+
115
+ ## We have regexes that match each addressing mode
116
+ match_data = parse_info[:regex].match(line)
117
+
118
+ unless match_data.nil?
119
+ ## We must have a straight instruction without symbols, construct
120
+ ## an Instruction from the match_data, and return it
121
+ _, op, arg_hex, arg_bin = match_data.to_a
122
+
123
+ ## Until I think of something better, it seems that the union regex
124
+ ## puts a hexidecimal argument in one capture, and a binary in the next
125
+ ## This is annoying, but still not as annoying as using Treetop to parse
126
+ if arg_hex != nil
127
+ return Instruction.new(op, arg_hex.to_i(16), mode)
128
+ elsif arg_bin != nil
129
+ return Instruction.new(op, arg_bin.to_i(2), mode)
130
+ else
131
+ return Instruction.new(op, nil, mode)
132
+ end
133
+
134
+ else
135
+ ## Can this addressing mode even use labels?
136
+ unless parse_info[:regex_label].nil?
137
+
138
+ ## See if it does in fact have a symbolic argument
139
+ match_data = parse_info[:regex_label].match(line)
140
+
141
+ unless match_data.nil?
142
+ ## We have found an assembly instruction containing a symbolic
143
+ ## argument. We can resolve this symbol later by looking at the
144
+ ## symbol table in the #exec method
145
+ match_array = match_data.to_a
146
+
147
+ ## If we have a 4 element array, this means we matched something
148
+ ## like LDA #<label, which is a legal immediate one byte value
149
+ ## by taking the msb. We need to make that distinction in the
150
+ ## Instruction, by passing an extra argument
151
+ if match_array.size == 4
152
+ _, op, byte_selector, arg = match_array
153
+ return Instruction.new(op, arg, mode, byte_selector.to_sym)
154
+ else
155
+ _, op, arg = match_array
156
+ return Instruction.new(op, arg, mode)
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ ## We just don't recognize this line of asm, it must be a Syntax Error
164
+ fail(SyntaxError, line)
165
+ end
166
+
167
+
168
+ ####
169
+ ## Create an instruction. Having the instruction op a downcased symbol is nice
170
+ ## because that can later be used to index into our opcodes hash in OpCodes
171
+ ## OpCodes contains the definitions of each OpCode
172
+ def initialize(op, arg, mode, byte_selector = nil)
173
+
174
+ ## Lookup the definition of this opcode, otherwise it is an invalid instruction
175
+ @byte_selector = byte_selector.nil? ? nil : byte_selector.to_sym
176
+ fail(InvalidInstruction, "Bad Byte selector: #{byte_selector}") unless [:>, :<, nil].include?(@byte_selector)
177
+
178
+ @op = op.downcase.to_sym
179
+ definition = OpCodes[@op]
180
+ fail(InvalidInstruction, op) if definition.nil?
181
+
182
+ @arg = arg
183
+
184
+ ## Be sure the mode is an actually supported mode.
185
+ @mode = mode.to_sym
186
+ fail(InvalidAddressingMode, mode) unless AddressingModes.has_key?(@mode)
187
+
188
+ if definition[@mode].nil?
189
+ fail(InvalidInstruction, "#{op} cannot be used in #{mode} mode")
190
+ end
191
+
192
+ @description, @flags = definition.values_at(:description, :flags)
193
+ @hex, @length, @cycles, @boundry_add = definition[@mode].values_at(:hex, :len, :cycles, :boundry_add)
194
+ end
195
+
196
+
197
+ ####
198
+ ## Return if this instruction is a zero page instruction
199
+ def zero_page_instruction?
200
+ [:zero_page, :zero_page_x, :zero_page_y].include?(@mode)
201
+ end
202
+
203
+
204
+ ####
205
+ ## Execute writes the emitted bytes to virtual memory, and updates PC
206
+ ## If there is a symbolic argument, we can try to resolve it now, or
207
+ ## promise to resolve it later.
208
+ def exec(assembler)
209
+
210
+ promise = assembler.with_saved_state do |saved_assembler|
211
+ @arg = saved_assembler.symbol_table.resolve_symbol(@arg)
212
+
213
+ ## If the instruction uses a byte selector, we need to apply that.
214
+ @arg = apply_byte_selector(@byte_selector, @arg)
215
+
216
+ ## If the instruction is relative we need to work out how far away it is
217
+ @arg = @arg - saved_assembler.program_counter - 2 if @mode == :relative
218
+
219
+ saved_assembler.write_memory(emit_bytes)
220
+ end
221
+
222
+ case @arg
223
+ when Fixnum, NilClass
224
+ assembler.write_memory(emit_bytes)
225
+ when String
226
+ begin
227
+ ## This is a bug, I don't believe it will ever get here.
228
+ ## I think it always resolves every symbol later.
229
+ promise.call
230
+ rescue SymbolTable::UndefinedSymbol
231
+ placeholder = [@hex, 0xDE, 0xAD][0...@length]
232
+ ## I still have to write a placeholder instruction of the right
233
+ ## length. The promise will come back and resolve the address.
234
+ assembler.write_memory(placeholder)
235
+ return promise
236
+ end
237
+ end
238
+ end
239
+
240
+
241
+ ####
242
+ ## Apply a byte selector to an argument
243
+ def apply_byte_selector(byte_selector, value)
244
+ return value if byte_selector.nil?
245
+ case byte_selector
246
+ when :>
247
+ high_byte(value)
248
+ when :<
249
+ low_byte(value)
250
+ end
251
+ end
252
+
253
+
254
+ ####
255
+ ## Emit bytes from asm structure
256
+ def emit_bytes
257
+ case @length
258
+ when 1
259
+ [@hex]
260
+ when 2
261
+ if zero_page_instruction? && @arg < 0 || @arg > 0xff
262
+ fail(ArgumentTooLarge, "For #{@op} in #{@mode} mode, only 8-bit values are allowed")
263
+ end
264
+ [@hex, @arg]
265
+ when 3
266
+ [@hex] + break_16(@arg)
267
+ else
268
+ fail("Can't handle instructions > 3 bytes")
269
+ end
270
+ end
271
+
272
+
273
+ ####
274
+ ## Pretty Print
275
+ def to_s
276
+ #display = AddressingModes[@mode][:display]
277
+ #if @arg.kind_of?(String)
278
+ #sprintf("#{display} (#{@mode}, #{@arg})", @op, 0x0)
279
+ #else
280
+ #sprintf("#{display} (#{@mode})", @op, @arg)
281
+ #end
282
+ end
283
+
284
+
285
+ private
286
+ ####
287
+ ## Break an integer into two 8-bit parts
288
+ def break_16(integer)
289
+ [integer & 0x00FF, (integer & 0xFF00) >> 8]
290
+ end
291
+
292
+
293
+ ####
294
+ ## Take the high byte of a 16-bit integer
295
+ def high_byte(word)
296
+ (word & 0xFF00) >> 8
297
+ end
298
+
299
+
300
+ ####
301
+ ## Take the low byte of a 16-bit integer
302
+ def low_byte(word)
303
+ word & 0xFF
304
+ end
305
+
306
+ end
307
+
308
+ end