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.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +28 -0
  4. data/.gitignore +1 -1
  5. data/.rubocop.yml +125 -0
  6. data/Gemfile +3 -1
  7. data/README.md +3 -20
  8. data/Rakefile +15 -1
  9. data/bin/n65 +2 -0
  10. data/data/opcodes.yaml +39 -39
  11. data/everdrive_transfer/everdrive.rb +175 -0
  12. data/examples/pulse_chord.asm +1 -1
  13. data/examples/scales.asm +182 -0
  14. data/lib/n65/directives/ascii.rb +4 -19
  15. data/lib/n65/directives/bytes.rb +20 -35
  16. data/lib/n65/directives/dw.rb +22 -36
  17. data/lib/n65/directives/enter_scope.rb +14 -30
  18. data/lib/n65/directives/exit_scope.rb +7 -17
  19. data/lib/n65/directives/inc.rb +14 -30
  20. data/lib/n65/directives/incbin.rb +6 -24
  21. data/lib/n65/directives/ines_header.rb +66 -27
  22. data/lib/n65/directives/label.rb +8 -21
  23. data/lib/n65/directives/org.rb +9 -19
  24. data/lib/n65/directives/segment.rb +5 -19
  25. data/lib/n65/directives/space.rb +6 -18
  26. data/lib/n65/front_end.rb +36 -39
  27. data/lib/n65/instruction.rb +123 -159
  28. data/lib/n65/instruction_base.rb +6 -18
  29. data/lib/n65/memory_space.rb +51 -71
  30. data/lib/n65/opcodes.rb +3 -5
  31. data/lib/n65/parser.rb +20 -38
  32. data/lib/n65/regexes.rb +20 -21
  33. data/lib/n65/symbol_table.rb +58 -89
  34. data/lib/n65/version.rb +3 -1
  35. data/lib/n65.rb +120 -121
  36. data/n65.gemspec +17 -12
  37. data/nes_lib/nes.sym +2 -2
  38. data/spec/.rubocop.yml +4 -0
  39. data/spec/assembler_spec.rb +84 -0
  40. data/spec/lib/n65/memory_space_spec.rb +147 -0
  41. data/spec/lib/n65/symbol_table_spec.rb +291 -0
  42. data/utils/opcode_table_to_yaml.rb +65 -67
  43. data.tar.gz.sig +0 -0
  44. metadata +84 -41
  45. metadata.gz.sig +0 -0
  46. data/examples/music_driver.asm +0 -202
  47. data/test/test_memory_space.rb +0 -82
  48. data/test/test_symbol_table.rb +0 -238
  49. data/utils/midi/Makefile +0 -3
  50. data/utils/midi/c_scale.mid +0 -0
  51. data/utils/midi/convert +0 -0
  52. data/utils/midi/guitar.mid +0 -0
  53. data/utils/midi/include/event.h +0 -93
  54. data/utils/midi/include/file.h +0 -57
  55. data/utils/midi/include/helpers.h +0 -14
  56. data/utils/midi/include/track.h +0 -45
  57. data/utils/midi/lil_melody.mid +0 -0
  58. data/utils/midi/mi_feabhra.mid +0 -0
  59. data/utils/midi/midi_to_nes.rb +0 -204
  60. data/utils/midi/source/convert.cpp +0 -16
  61. data/utils/midi/source/event.cpp +0 -96
  62. data/utils/midi/source/file.cpp +0 -37
  63. data/utils/midi/source/helpers.cpp +0 -46
  64. data/utils/midi/source/track.cpp +0 -37
@@ -1,35 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../instruction_base'
2
4
 
3
5
  module N65
4
-
5
-
6
- ####
7
- ## This directive to include bytes
6
+ # This directive to include bytes
8
7
  class ExitScope < InstructionBase
9
-
10
-
11
- ####
12
- ## Try to parse an incbin directive
13
8
  def self.parse(line)
14
9
  match_data = line.match(/^\.$/)
15
10
  return nil if match_data.nil?
11
+
16
12
  ExitScope.new
17
13
  end
18
14
 
19
-
20
- ####
21
- ## Execute on the assembler
15
+ # Execute on the assembler
22
16
  def exec(assembler)
23
17
  assembler.symbol_table.exit_scope
24
18
  end
25
19
 
26
-
27
- ####
28
- ## Display
20
+ # Display
29
21
  def to_s
30
- "."
22
+ '.'
31
23
  end
32
-
33
24
  end
34
-
35
25
  end
@@ -1,67 +1,51 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../instruction_base'
2
4
 
3
5
  module N65
4
-
5
-
6
- ####
7
- ## This directive instruction can include another asm file
6
+ # This directive instruction can include another asm file
8
7
  class Inc < InstructionBase
8
+ SYSTEM_INCLUDE = "#{File.dirname(__FILE__)}/../../../nes_lib"
9
9
 
10
- #### System include directory
11
- SystemInclude = File.dirname(__FILE__) + "/../../../nes_lib"
12
-
13
- #### Custom Exceptions
14
10
  class FileNotFound < StandardError; end
15
11
 
16
-
17
- ####
18
- ## Try to parse an incbin directive
12
+ # Try to parse an incbin directive
19
13
  def self.parse(line)
20
- ## Do We have a system directory include?
14
+ # Do We have a system directory include?
21
15
  match_data = line.match(/^\.inc <([^>]+)>$/)
22
16
  unless match_data.nil?
23
- filename = File.join(SystemInclude, match_data[1])
17
+ filename = File.join(SYSTEM_INCLUDE, match_data[1])
24
18
  return Inc.new(filename)
25
19
  end
26
20
 
27
- ## Do We have a project relative directory include?
21
+ # Do We have a project relative directory include?
28
22
  match_data = line.match(/^\.inc "([^"]+)"$/)
29
23
  unless match_data.nil?
30
24
  filename = File.join(Dir.pwd, match_data[1])
31
25
  return Inc.new(filename)
32
26
  end
33
27
 
34
- ## Nope, not an inc directive
28
+ # Nope, not an inc directive
35
29
  nil
36
30
  end
37
31
 
38
-
39
- ####
40
- ## Initialize with filename
32
+ # Initialize with filename
41
33
  def initialize(filename)
42
34
  @filename = filename
43
35
  end
44
36
 
45
-
46
- ####
47
- ## Execute on the assembler
37
+ # Execute on the assembler
48
38
  def exec(assembler)
49
- unless File.exists?(@filename)
50
- fail(FileNotFound, ".inc can't find #{@filename}")
51
- end
39
+ raise(FileNotFound, ".inc can't find #{@filename}") unless File.exist?(@filename)
40
+
52
41
  File.read(@filename).split(/\n/).each do |line|
53
42
  assembler.assemble_one_line(line)
54
43
  end
55
44
  end
56
45
 
57
-
58
- ####
59
- ## Display
46
+ # Display
60
47
  def to_s
61
48
  ".inc \"#{@filename}\""
62
49
  end
63
-
64
-
65
50
  end
66
-
67
51
  end
@@ -1,51 +1,33 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../instruction_base'
2
4
 
3
5
  module N65
4
-
5
-
6
- ####
7
- ## This directive instruction can include a binary file
6
+ # This directive instruction can include a binary file
8
7
  class IncBin < InstructionBase
9
-
10
- #### Custom Exceptions
11
8
  class FileNotFound < StandardError; end
12
9
 
13
-
14
- ####
15
- ## Try to parse an incbin directive
16
10
  def self.parse(line)
17
11
  match_data = line.match(/^\.incbin "([^"]+)"$/)
18
12
  return nil if match_data.nil?
13
+
19
14
  filename = match_data[1]
20
15
  IncBin.new(filename)
21
16
  end
22
17
 
23
-
24
- ####
25
- ## Initialize with filename
26
18
  def initialize(filename)
27
19
  @filename = filename
28
20
  end
29
21
 
30
-
31
- ####
32
- ## Execute on the assembler
33
22
  def exec(assembler)
34
- unless File.exists?(@filename)
35
- fail(FileNotFound, ".incbin can't find #{@filename}")
36
- end
23
+ raise(FileNotFound, ".incbin can't find #{@filename}") unless File.exist?(@filename)
24
+
37
25
  data = File.read(@filename).unpack('C*')
38
26
  assembler.write_memory(data)
39
27
  end
40
28
 
41
-
42
- ####
43
- ## Display
44
29
  def to_s
45
30
  ".incbin \"#{@filename}\""
46
31
  end
47
-
48
-
49
32
  end
50
-
51
33
  end
@@ -1,53 +1,92 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require_relative '../instruction_base'
3
5
 
4
6
  module N65
5
-
6
-
7
- ####
8
- ## This directive instruction can setup an ines header
9
7
  class INESHeader < InstructionBase
10
- attr_reader :prog, :char, :mapper, :mirror
8
+ attr_reader :prog, :char, :mapper, :mirror, :battery_backed, :fourscreen_vram, :prog_ram, :tv
9
+
10
+ DEFAULTS = {
11
+ prog: 1,
12
+ char: 0,
13
+ mapper: 0,
14
+ mirror: 0,
15
+ battery_backed: 0,
16
+ fourscreen_vram: 0,
17
+ prog_ram: 0,
18
+ tv: 0
19
+ }.freeze
11
20
 
12
-
13
- ####
14
- ## Implementation of the parser for this directive
15
21
  def self.parse(line)
16
22
  match_data = line.match(/^\.ines (.+)$/)
17
23
  return nil if match_data.nil?
18
24
 
19
25
  header = JSON.parse(match_data[1])
20
- INESHeader.new(header['prog'], header['char'], header['mapper'], header['mirror'])
26
+ header = header.each_with_object({}) do |(key, val), hash|
27
+ hash[key.to_sym] = val
28
+ end
29
+
30
+ header = DEFAULTS.merge(header)
31
+
32
+ INESHeader.new(
33
+ header[:prog],
34
+ header[:char],
35
+ header[:mapper],
36
+ header[:mirror],
37
+ header[:battery_backed],
38
+ header[:fourscreen_vram],
39
+ header[:prog_ram],
40
+ header[:tv]
41
+ )
21
42
  end
22
43
 
23
-
24
- ####
25
- ## Construct a header
26
- def initialize(prog, char, mapper, mirror)
27
- @prog, @char, @mapper, @mirror = prog, char, mapper, mirror
44
+ # Construct a header
45
+ def initialize(prog, char, mapper, mirror, battery_backed, fourscreen_vram, prog_ram, tv)
46
+ @prog = prog
47
+ @char = char
48
+ @mapper = mapper
49
+ @mirror = mirror
50
+ @battery_backed = battery_backed
51
+ @fourscreen_vram = fourscreen_vram
52
+ @prog_ram = prog_ram
53
+ @tv = tv
28
54
  end
29
55
 
30
-
31
- ####
32
- ## Exec function the assembler will call
56
+ # Exec function the assembler will call
33
57
  def exec(assembler)
34
58
  assembler.set_ines_header(self)
35
59
  end
36
60
 
37
-
38
- ####
39
- ## Emit the header bytes, this is not exactly right, but it works for now.
61
+ # Emit the header bytes
40
62
  def emit_bytes
41
- [0x4E, 0x45, 0x53, 0x1a, @prog, @char, @mapper, @mirror, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]
63
+ mapper_lo_nybble = (@mapper & 0x0f)
64
+ mapper_hi_nybble = (@mapper & 0xf0) >> 4
65
+
66
+ flag6 = 0
67
+ flag6 |= 0x1 if @mirror == 1
68
+ flag6 |= 0x2 if @battery_backed == 1
69
+ flag6 |= 0x8 if @fourscreen_vram == 1
70
+ flag6 |= (mapper_lo_nybble << 4)
71
+
72
+ flag7 = 0
73
+ flag7 |= (mapper_hi_nybble << 4)
74
+
75
+ [0x4E, 0x45, 0x53, 0x1a,
76
+ @prog & 0xff,
77
+ @char & 0xff,
78
+ flag6 & 0xff,
79
+ flag7 & 0xff,
80
+ @prog_ram & 0xff,
81
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]
42
82
  end
43
83
 
44
-
45
- ####
46
- ## Display
84
+ # Display
47
85
  def to_s
48
- ".ines {\"prog\": #{@prog}, \"char\": #{@char}, \"mapper\": #{@mapper}, \"mirror\": #{@mirror}}"
86
+ [".ines {\"prog\": #{@prog}, \"char\": #{@char}, \"mapper\": #{@mapper}, ",
87
+ "\"mirror\": #{@mirror}}, \"battery_backed\": #{@battery_backed}, ",
88
+ "\"fourscreen_vram\": #{@fourscreen_vram}, \"prog_ram\": #{@prog_ram}, ",
89
+ "\"tv\": #{@tv}"].join
49
90
  end
50
-
51
91
  end
52
-
53
92
  end
@@ -1,46 +1,33 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  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.
4
+ # This class represents a label, and will create
5
+ # an entry in the symbol table associated with
6
+ # the address it appears at.
8
7
  class Label
9
-
10
- ####
11
- ## Try to parse as a label
12
8
  def self.parse(line)
13
9
  match_data = line.match(/^([a-zA-Z][a-zA-Z0-9_]+):$/)
14
10
  unless match_data.nil?
15
11
  label = match_data[1].to_sym
16
- return self.new(label)
12
+ return new(label)
17
13
  end
18
14
  nil
19
15
  end
20
16
 
21
-
22
- ####
23
- ## Create a new label object
17
+ # Create a new label object
24
18
  def initialize(symbol)
25
19
  @symbol = symbol
26
20
  end
27
21
 
28
-
29
- ####
30
- ## Create an entry in the symbol table for this label
22
+ # Create an entry in the symbol table for this label
31
23
  def exec(assembler)
32
24
  program_counter = assembler.program_counter
33
25
  assembler.symbol_table.define_symbol(@symbol, program_counter)
34
26
  end
35
27
 
36
-
37
- ####
38
- ## Display
28
+ # Display
39
29
  def to_s
40
30
  "#{@symbol}:"
41
31
  end
42
-
43
-
44
32
  end
45
-
46
33
  end
@@ -1,47 +1,37 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../instruction_base'
2
4
 
3
5
  module N65
4
-
5
- ####
6
- ## This is an .org directive
6
+ # This is an .org directive
7
7
  class Org < InstructionBase
8
8
  attr_reader :address
9
9
 
10
- ####
11
- ## Try to parse an .org statement
12
10
  def self.parse(line)
13
11
  match_data = line.match(/^\.org\s+\$([0-9A-Fa-f]{4})$/)
14
12
  return nil if match_data.nil?
13
+
15
14
  address = match_data[1].to_i(16)
16
15
  Org.new(address)
17
16
  end
18
17
 
19
-
20
- ####
21
- ## Initialized with address to switch to
18
+ # Initialized with address to switch to
22
19
  def initialize(address)
23
20
  @address = address
24
21
  end
25
22
 
26
-
27
- ####
28
- ## Exec this directive on the assembler
23
+ # Exec this directive on the assembler
29
24
  def exec(assembler)
30
25
  assembler.program_counter = address
31
26
  end
32
27
 
33
-
34
- ####
35
- ## Display
28
+ # Display
36
29
  def to_s
37
30
  if @address <= 0xff
38
- ".org $%2.X" % @address
31
+ '.org $%2.X' % @address
39
32
  else
40
- ".org $%4.X" % @address
33
+ '.org $%4.X' % @address
41
34
  end
42
35
  end
43
-
44
36
  end
45
-
46
37
  end
47
-
@@ -1,13 +1,8 @@
1
- require_relative '../instruction_base'
2
- module N65
3
-
1
+ # frozen_string_literal: true
4
2
 
5
- ####
6
- ## This directive instruction can include a binary file
3
+ require_relative '../instruction_base'
4
+ module N65
7
5
  class Segment < InstructionBase
8
-
9
- ####
10
- ## Try to parse a dw directive
11
6
  def self.parse(line)
12
7
  match_data = line.match(/^.segment (prog|char) (\d+)$/i)
13
8
  unless match_data.nil?
@@ -17,29 +12,20 @@ module N65
17
12
  nil
18
13
  end
19
14
 
20
-
21
- ####
22
- ## Initialize with filename
15
+ # Initialize with filename
23
16
  def initialize(segment, bank)
24
17
  @bank = bank
25
18
  @segment = segment
26
19
  end
27
20
 
28
-
29
- ####
30
- ## Execute the segment and bank change on the assembler
21
+ # Execute the segment and bank change on the assembler
31
22
  def exec(assembler)
32
23
  assembler.current_segment = @segment
33
24
  assembler.current_bank = @bank
34
25
  end
35
26
 
36
-
37
- ####
38
- ## Display
39
27
  def to_s
40
28
  ".segment #{@segment} #{@bank}"
41
29
  end
42
-
43
30
  end
44
-
45
31
  end
@@ -1,46 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../instruction_base'
2
4
 
3
5
  module N65
4
-
5
-
6
- ####
7
6
  ## This directive gives a symbolic name for memory and creates space for a variable in RAM
8
7
  class Space < InstructionBase
9
-
10
- ####
11
- ## Try to parse a .space directive
12
8
  def self.parse(line)
13
9
  match_data = line.match(/^.space\s+([a-zA-Z]?[a-zA-Z0-9_]+?)\s+([0-9]+)$/)
14
10
  return nil if match_data.nil?
15
- _, name, size = match_data.to_a
16
11
 
12
+ _, name, size = match_data.to_a
17
13
  Space.new(name, size.to_i)
18
14
  end
19
15
 
20
-
21
- ####
22
- ## Initialize some memory space with a name
16
+ # Initialize some memory space with a name
23
17
  def initialize(name, size)
24
18
  @name = name
25
19
  @size = size
26
20
  end
27
21
 
28
-
29
- ####
30
- ## .space creates a symbol at the current PC, and then advances PC by size
22
+ # .space creates a symbol at the current PC, and then advances PC by size
31
23
  def exec(assembler)
32
24
  program_counter = assembler.program_counter
33
25
  assembler.symbol_table.define_symbol(@name, program_counter)
34
26
  assembler.program_counter += @size
35
27
  end
36
28
 
37
-
38
- ####
39
- ## Display
29
+ # Display
40
30
  def to_s
41
31
  ".space #{@name} #{@size}"
42
32
  end
43
-
44
33
  end
45
-
46
34
  end
data/lib/n65/front_end.rb CHANGED
@@ -1,81 +1,80 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'optparse'
2
4
  require_relative '../n65'
3
5
 
4
6
  module N65
5
-
6
- ####
7
- ## This class handles the front end aspects,
8
- ## parsing the commandline options and running the assembler
7
+ # This class handles the front end aspects,
8
+ # parsing the commandline options and running the assembler
9
9
  class FrontEnd
10
-
11
- ####
12
- ## Initialize with ARGV commandline
13
10
  def initialize(argv)
14
- @options = {:output_file => nil}
11
+ @options = { output_file: nil, write_symbol_table: false, quiet: false, cycle_count: false }
15
12
  @argv = argv.dup
16
13
  end
17
14
 
18
-
19
- ####
20
- ## Run the assembler
15
+ # Run the assembler
21
16
  def run
22
- ## First use the option parser
23
17
  parser = create_option_parser
24
18
  parser.parse!(@argv)
25
19
 
26
- ## Whatever is leftover in argv the input files
27
20
  if @argv.size.zero?
28
- STDERR.puts("No input files")
21
+ warn('No input files')
29
22
  exit(1)
30
23
  end
31
24
 
32
- ## Only can assemble one file at once for now
25
+ # Only can assemble one file at once for now
33
26
  if @argv.size != 1
34
- STDERR.puts "Can only assemble one input file at once :("
27
+ warn('Can only assemble one input file at once, but you can use .inc and .incbin directives')
35
28
  exit(1)
36
29
  end
37
30
 
38
31
  input_file = @argv.shift
39
32
 
40
- ## Make sure the input file exists
41
- unless File.exists?(input_file)
42
- STDERR.puts "Input file #{input_file} does not exist"
33
+ # Make sure the input file exists
34
+ unless File.exist?(input_file)
35
+ warn("Input file #{input_file} does not exist")
43
36
  exit(1)
44
37
  end
45
-
46
- ## Maybe they didn't provide an output file name, so we'll guess
38
+
39
+ # Maybe they didn't provide an output file name, so we'll guess
47
40
  if @options[:output_file].nil?
48
41
  ext = File.extname(input_file)
49
- @options[:output_file] = input_file.gsub(ext, '') + '.nes'
42
+ @options[:output_file] = "#{input_file.gsub(ext, '')}.nes"
50
43
  end
51
44
 
52
45
  if @options.values.any?(&:nil?)
53
- STDERR.puts "Missing options try --help"
46
+ warn('Missing options try --help')
54
47
  exit(1)
55
48
  end
56
49
 
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
50
+ N65::Assembler.from_file(input_file, @options)
67
51
  end
68
52
 
69
53
  private
70
54
 
71
- ####
72
- ## Create a commandline option parser
73
55
  def create_option_parser
74
56
  OptionParser.new do |opts|
75
- opts.banner = "Usage: #{$0} [options] <input_file.asm>"
57
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options] <input_file.asm>"
76
58
 
77
59
  opts.on('-o', '--outfile filename', 'outfile') do |output_file|
78
- @options[:output_file] = output_file;
60
+ @options[:output_file] = output_file
61
+ end
62
+
63
+ opts.on('-s', '--symbols', 'Outputs a symbol map') do
64
+ @options[:write_symbol_table] = true
65
+ end
66
+
67
+ opts.on('-c', '--cycles', 'Outputs a cycle count yaml document') do
68
+ @options[:cycle_count] = true
69
+ end
70
+
71
+ opts.on('-q', '--quiet', 'No output on success') do
72
+ @options[:quiet] = true
73
+ end
74
+
75
+ opts.on('-v', '--version', 'Displays Version') do
76
+ puts "N65 Assembler Version #{N65::VERSION}"
77
+ exit
79
78
  end
80
79
 
81
80
  opts.on('-h', '--help', 'Displays Help') do
@@ -84,7 +83,5 @@ module N65
84
83
  end
85
84
  end
86
85
  end
87
-
88
86
  end
89
-
90
87
  end