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,29 @@
1
+
2
+ module N65
3
+
4
+ class InstructionBase
5
+
6
+
7
+ #####
8
+ ## Sort of a "pure virtual" class method, not really tho.
9
+ def self.parse(line)
10
+ fail(NotImplementedError, "#{self.class.name} must implement self.parse")
11
+ end
12
+
13
+
14
+ ####
15
+ ## Does this instruction have unresolved symbols?
16
+ def unresolved_symbols?
17
+ false
18
+ end
19
+
20
+
21
+ ####
22
+ ## Another method subclasses will be expected to implement
23
+ def exec(assembler)
24
+ fail(NotImplementedError, "#{self.class.name} must implement exec")
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,150 @@
1
+
2
+ module N65
3
+
4
+ ####
5
+ ## Let's use this to simulate a virtual address space
6
+ ## Either a 16kb prog rom or 8kb char rom space.
7
+ ## It can also be used to create arbitrary sized spaces
8
+ ## for example to build the final binary ROM in.
9
+ class MemorySpace
10
+
11
+ #### Custom exceptions
12
+ class AccessOutsideProgRom < StandardError; end
13
+ class AccessOutsideCharRom < StandardError; end
14
+ class AccessOutOfBounds < StandardError; end
15
+
16
+
17
+ #### Some constants, the size of PROG and CHAR ROM
18
+ BankSizes = {
19
+ :ines => 0x10, # 16b
20
+ :prog => 0x4000, # 16kb
21
+ :char => 0x2000, # 8kb
22
+ }
23
+
24
+
25
+ ####
26
+ ## Create a new PROG ROM
27
+ def self.create_prog_rom
28
+ self.create_bank(:prog)
29
+ end
30
+
31
+
32
+ ####
33
+ ## Create a new CHAR ROM
34
+ def self.create_char_rom
35
+ self.create_bank(:char)
36
+ end
37
+
38
+
39
+ ####
40
+ ## Create a new bank
41
+ def self.create_bank(type)
42
+ self.new(BankSizes[type], type)
43
+ end
44
+
45
+
46
+ ####
47
+ ## Create a completely zeroed memory space
48
+ def initialize(size, type)
49
+ @type = type
50
+ @memory = Array.new(size, 0x0)
51
+ end
52
+
53
+
54
+ ####
55
+ ## Normalized read from memory
56
+ def read(address, count)
57
+ from_normalized = normalize_address(address)
58
+ to_normalized = normalize_address(address + (count - 1))
59
+ ensure_addresses_in_bounds!([from_normalized, to_normalized])
60
+
61
+ @memory[from_normalized..to_normalized]
62
+ end
63
+
64
+
65
+ ####
66
+ ## Normalized write to memory
67
+ def write(address, bytes)
68
+ from_normalized = normalize_address(address)
69
+ to_normalized = normalize_address(address + bytes.size - 1)
70
+ ensure_addresses_in_bounds!([from_normalized, to_normalized])
71
+
72
+ bytes.each_with_index do |byte, index|
73
+ @memory[from_normalized + index] = byte
74
+ end
75
+ bytes.size
76
+ end
77
+
78
+
79
+ ####
80
+ ## Return the memory as an array of bytes to write to disk
81
+ def emit_bytes
82
+ @memory
83
+ end
84
+
85
+
86
+ private
87
+
88
+ ####
89
+ ## Are the given addresses in bounds? If not blow up.
90
+ def ensure_addresses_in_bounds!(addresses)
91
+ addresses.each do |address|
92
+ unless address >= 0 && address < @memory.size
93
+ fail(AccessOutOfBounds, sprintf("Address $%.4X is out of bounds in this #{@type} bank"))
94
+ end
95
+ end
96
+ true
97
+ end
98
+
99
+
100
+ ####
101
+ ## Since prog rom can be loaded at either 0x8000 or 0xC000
102
+ ## We should normalize the addresses to fit properly into
103
+ ## these banks, basically it acts like it is mirroring addresses
104
+ ## in those segments. Char rom doesn't need this. This will also
105
+ ## fail if you are accessing outside of the address space.
106
+ def normalize_address(address)
107
+ case @type
108
+ when :prog
109
+ if address_inside_prog_rom1?(address)
110
+ return address - 0x8000
111
+ end
112
+ if address_inside_prog_rom2?(address)
113
+ return address - 0xC000
114
+ end
115
+ fail(AccessOutsideProgRom, sprintf("Address $%.4X is outside PROG ROM", address))
116
+ when :char
117
+ unless address_inside_char_rom?(address)
118
+ fail(AccessOutsideCharRom, sprintf("Address $%.4X is outside CHAR ROM", address))
119
+ end
120
+ address
121
+ else
122
+ address
123
+ end
124
+ end
125
+
126
+
127
+ ####
128
+ ## Is this address inside the prog rom 1 area?
129
+ def address_inside_prog_rom1?(address)
130
+ address >= 0x8000 && address < 0xC000
131
+ end
132
+
133
+
134
+ ####
135
+ ## Is this address inside the prog rom 2 area?
136
+ def address_inside_prog_rom2?(address)
137
+ address >= 0xC000 && address <= 0xffff
138
+ end
139
+
140
+
141
+ ####
142
+ ## Is this address inside the char rom area?
143
+ def address_inside_char_rom?(address)
144
+ address >= 0x0000 && address <= 0x1fff
145
+ end
146
+
147
+ end
148
+
149
+ end
150
+
@@ -0,0 +1,9 @@
1
+ require 'yaml'
2
+
3
+ module N65
4
+
5
+ ## Load OpCode definitions into this module
6
+ MyDirectory = File.expand_path(File.dirname(__FILE__))
7
+ OpCodes = YAML.load_file("#{MyDirectory}/../../data/opcodes.yaml")
8
+
9
+ end
@@ -0,0 +1,85 @@
1
+
2
+ module N65
3
+
4
+ require_relative 'instruction'
5
+ require_relative 'directives/ines_header'
6
+ require_relative 'directives/org'
7
+ require_relative 'directives/segment'
8
+ require_relative 'directives/incbin'
9
+ require_relative 'directives/inc'
10
+ require_relative 'directives/dw'
11
+ require_relative 'directives/bytes'
12
+ require_relative 'directives/ascii'
13
+ require_relative 'directives/label'
14
+ require_relative 'directives/enter_scope'
15
+ require_relative 'directives/exit_scope'
16
+ require_relative 'directives/space'
17
+
18
+
19
+
20
+ ####
21
+ ## This class determines what sort of line of code we
22
+ ## are dealing with, parses one line, and returns an
23
+ ## object deriving from InstructionBase
24
+ class Parser
25
+
26
+ #### Custom Exceptions
27
+ class CannotParse < StandardError; end
28
+
29
+
30
+ Directives = [INESHeader, Org, Segment, IncBin, Inc, DW, Bytes, ASCII, EnterScope, ExitScope, Space]
31
+
32
+ ####
33
+ ## Parses a line of program source into an object
34
+ ## deriving from base class InstructionBase
35
+ def self.parse(line)
36
+ sanitized = sanitize_line(line)
37
+ return nil if sanitized.empty?
38
+
39
+ ## First check to see if we have a label.
40
+ label = Label.parse(sanitized)
41
+ unless label.nil?
42
+ return label
43
+ end
44
+
45
+ ## Now check if we have a directive
46
+ directive = parse_directive(sanitized)
47
+ unless directive.nil?
48
+ return directive
49
+ end
50
+
51
+ ## Now, surely it is an asm instruction?
52
+ instruction = Instruction.parse(sanitized)
53
+ unless instruction.nil?
54
+ return instruction
55
+ end
56
+
57
+ ## Guess not, we have no idea
58
+ fail(CannotParse, sanitized)
59
+ end
60
+
61
+
62
+ private
63
+ ####
64
+ ## Sanitize one line of program source
65
+ def self.sanitize_line(line)
66
+ code = line.split(';').first || ""
67
+ code.strip.chomp
68
+ end
69
+
70
+
71
+ ####
72
+ ## Try to Parse a directive
73
+ def self.parse_directive(line)
74
+ if line.start_with?('.')
75
+ Directives.each do |directive|
76
+ object = directive.parse(line)
77
+ return object unless object.nil?
78
+ end
79
+ end
80
+ nil
81
+ end
82
+
83
+ end
84
+
85
+ end
@@ -0,0 +1,33 @@
1
+
2
+
3
+ module N65
4
+
5
+ ####
6
+ ## All the regexes used to parse in one module
7
+ module Regexes
8
+ ## Mnemonics
9
+ Mnemonic = '([A-Za-z]{3})'
10
+ Branches = '(BPL|BMI|BVC|BVS|BCC|BCS|BNE|BEQ|bpl|bmi|bvc|bvs|bcc|bcs|bne|beq)'
11
+
12
+ ## Numeric Literals
13
+ Hex8 = '\$([A-Fa-f0-9]{1,2})'
14
+ Hex16 = '\$([A-Fa-f0-9]{3,4})'
15
+
16
+ Bin8 = '%([01]{1,8})'
17
+ Bin16 = '%([01]{9,16})'
18
+
19
+ Num8 = Regexp.union(Regexp.new(Hex8), Regexp.new(Bin8)).to_s
20
+ Num16 = Regexp.union(Regexp.new(Hex16),Regexp.new(Bin16)).to_s
21
+
22
+ Immediate = "\##{Num8}"
23
+
24
+ ## Symbols, must begin with a letter, and supports dot syntax
25
+ Sym = '([a-zA-Z][a-zA-Z\d_\.]*)'
26
+
27
+
28
+ ## The X or Y register
29
+ XReg = '[Xx]'
30
+ YReg = '[Yy]'
31
+ end
32
+
33
+ end
@@ -0,0 +1,198 @@
1
+
2
+ module N65
3
+
4
+ class SymbolTable
5
+ attr_accessor :scope_stack
6
+
7
+ ##### Custom Exceptions
8
+ class InvalidScope < StandardError; end
9
+ class UndefinedSymbol < StandardError; end
10
+ class CantExitScope < StandardError; end
11
+
12
+
13
+ ####
14
+ ## Initialize a symbol table that begins in global scope
15
+ def initialize
16
+ @symbols = {
17
+ :global => {}
18
+ }
19
+ @anonymous_scope_number = 0
20
+ @scope_stack = [:global]
21
+ end
22
+
23
+
24
+ ####
25
+ ## Define a new scope, which can be anonymous or named
26
+ ## and switch into that scope
27
+ def enter_scope(name = nil)
28
+ name = generate_name if name.nil?
29
+ name = name.to_sym
30
+ scope = current_scope
31
+ if scope.has_key?(name)
32
+ #path_string = generate_scope_path(path_ary)
33
+ fail(InvalidScope, "Scope: #{name} already exists")
34
+ end
35
+ scope[name] = {}
36
+ @scope_stack.push(name)
37
+ end
38
+
39
+
40
+ ####
41
+ ## Exit the current scope
42
+ def exit_scope
43
+ if @scope_stack.size == 1
44
+ fail(CantExitScope, "You cannot exit global scope")
45
+ end
46
+ @scope_stack.pop
47
+ end
48
+
49
+
50
+ ####
51
+ ## Define a symbol in the current scope
52
+ def define_symbol(symbol, value)
53
+ scope = current_scope
54
+ scope[symbol.to_sym] = value
55
+ end
56
+
57
+
58
+ =begin
59
+ ####
60
+ ## Resolve symbol to a value, for example:
61
+ ## scope1.scope2.variable
62
+ ## It is not nessessary to specify the root scope :global
63
+ ## You can just address anything by name in the current scope
64
+ ## To go backwards in scope you need to write the full path
65
+ ## like global.sprite.x or whatever
66
+ def resolve_symbol_old(name)
67
+
68
+ value = if name.include?('.')
69
+ path_ary = name.split('.').map(&:to_sym)
70
+ symbol = path_ary.pop
71
+ path_ary.shift if path_ary.first == :global
72
+ scope = retreive_scope(path_ary)
73
+ ## We also try to look up the address associated with the scope
74
+ root = "-#{symbol}".to_sym
75
+ v = scope[symbol]
76
+ v.kind_of?(Hash) ? v[root] : v
77
+ else
78
+ root = "-#{name}".to_sym
79
+ scope = current_scope
80
+ ## We also try to look up the address associated with the scope
81
+ v = scope[name.to_sym] || scope[root]
82
+ v.kind_of?(Hash) ? v[root] : v
83
+ end
84
+
85
+ if value.nil?
86
+ fail(UndefinedSymbol, name)
87
+ end
88
+ value
89
+ end
90
+ =end
91
+
92
+
93
+ ####
94
+ ##
95
+ def resolve_symbol(name)
96
+ method = name.include?('.') ? :resolve_symbol_dot_syntax : :resolve_symbol_scoped
97
+ value = self.send(method, name)
98
+
99
+ fail(UndefinedSymbol, name) if value.nil?
100
+ value
101
+ end
102
+
103
+
104
+ ####
105
+ ## Resolve symbol by working backwards through each
106
+ ## containing scope. Similarly named scopes shadow outer scopes
107
+ def resolve_symbol_scoped(name)
108
+ root = "-#{name}".to_sym
109
+ stack = @scope_stack.dup
110
+ loop do
111
+ scope = retreive_scope(stack)
112
+
113
+ ## We see if there is a key either under this name, or root
114
+ v = scope[name.to_sym] || scope[root]
115
+ v = v.kind_of?(Hash) ? v[root] : v
116
+
117
+ return v unless v.nil?
118
+
119
+ ## Pop the stack so we can decend to the parent scope, if any
120
+ stack.pop
121
+ return nil if stack.empty?
122
+ end
123
+ end
124
+
125
+
126
+ ####
127
+ ## Dot syntax means to check an absolute path to the symbol
128
+ ## :global is ignored if it is provided as part of the path
129
+ def resolve_symbol_dot_syntax(name)
130
+ path_ary = name.split('.').map(&:to_sym)
131
+ symbol = path_ary.pop
132
+ root = "-#{symbol}".to_sym
133
+ path_ary.shift if path_ary.first == :global
134
+
135
+ scope = retreive_scope(path_ary)
136
+
137
+ ## We see if there is a key either under this name, or root
138
+ v = scope[symbol]
139
+ v.kind_of?(Hash) ? v[root] : v
140
+ end
141
+
142
+
143
+ ####
144
+ ## Export the symbol table as YAML
145
+ def export_to_yaml
146
+ @symbols.to_yaml.gsub(/(\d+)$/) do |match|
147
+ integer = match.to_i
148
+ sprintf("0x%.4X", integer)
149
+ end
150
+ end
151
+
152
+
153
+ private
154
+
155
+ ####
156
+ ## A bit more clearly states to get the current scope
157
+ def current_scope
158
+ retreive_scope
159
+ end
160
+
161
+
162
+ ####
163
+ ## Retrieve a reference to a scope, current scope by default
164
+ def retreive_scope(path_ary = @scope_stack)
165
+ path_ary = path_ary.dup
166
+ path_ary.unshift(:global) unless path_ary.first == :global
167
+
168
+ path_ary.inject(@symbols) do |scope, path_component|
169
+ new_scope = scope[path_component.to_sym]
170
+
171
+ if new_scope.nil?
172
+ path_string = generate_scope_path(path_ary)
173
+ message = "Resolving scope: #{path_string} failed at #{path_component}"
174
+ fail(InvalidScope, message) if new_scope.nil?
175
+ end
176
+
177
+ new_scope
178
+ end
179
+ end
180
+
181
+
182
+ ####
183
+ ## Generate a scope path from an array
184
+ def generate_scope_path(path_ary)
185
+ path_ary.join('.')
186
+ end
187
+
188
+
189
+ ####
190
+ ## Generate an anonymous scope name
191
+ def generate_name
192
+ @anonymous_scope_number += 1
193
+ "anonymous_#{@anonymous_scope_number}"
194
+ end
195
+
196
+ end
197
+
198
+ end