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.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/Gemfile +4 -0
- data/LICENSE +340 -0
- data/README.md +126 -0
- data/Rakefile +2 -0
- data/bin/n65 +11 -0
- data/data/opcodes.yaml +1030 -0
- data/examples/beep.asm +24 -0
- data/examples/mario2.asm +260 -0
- data/examples/mario2.char +0 -0
- data/examples/music_driver.asm +202 -0
- data/examples/noise.asm +93 -0
- data/examples/pulse_chord.asm +213 -0
- data/images/assembler_demo.png +0 -0
- data/lib/n65.rb +243 -0
- data/lib/n65/directives/ascii.rb +42 -0
- data/lib/n65/directives/bytes.rb +102 -0
- data/lib/n65/directives/dw.rb +86 -0
- data/lib/n65/directives/enter_scope.rb +55 -0
- data/lib/n65/directives/exit_scope.rb +35 -0
- data/lib/n65/directives/inc.rb +67 -0
- data/lib/n65/directives/incbin.rb +51 -0
- data/lib/n65/directives/ines_header.rb +53 -0
- data/lib/n65/directives/label.rb +46 -0
- data/lib/n65/directives/org.rb +47 -0
- data/lib/n65/directives/segment.rb +45 -0
- data/lib/n65/directives/space.rb +46 -0
- data/lib/n65/front_end.rb +90 -0
- data/lib/n65/instruction.rb +308 -0
- data/lib/n65/instruction_base.rb +29 -0
- data/lib/n65/memory_space.rb +150 -0
- data/lib/n65/opcodes.rb +9 -0
- data/lib/n65/parser.rb +85 -0
- data/lib/n65/regexes.rb +33 -0
- data/lib/n65/symbol_table.rb +198 -0
- data/lib/n65/version.rb +3 -0
- data/n65.gemspec +23 -0
- data/nes_lib/nes.sym +105 -0
- data/test/test_memory_space.rb +82 -0
- data/test/test_symbol_table.rb +238 -0
- data/utils/midi/Makefile +3 -0
- 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 +93 -0
- data/utils/midi/include/file.h +57 -0
- data/utils/midi/include/helpers.h +14 -0
- data/utils/midi/include/track.h +45 -0
- data/utils/midi/lil_melody.mid +0 -0
- data/utils/midi/mi_feabhra.mid +0 -0
- data/utils/midi/midi_to_nes.rb +204 -0
- data/utils/midi/source/convert.cpp +16 -0
- data/utils/midi/source/event.cpp +96 -0
- data/utils/midi/source/file.cpp +37 -0
- data/utils/midi/source/helpers.cpp +46 -0
- data/utils/midi/source/track.cpp +37 -0
- data/utils/opcode_table_to_yaml.rb +91 -0
- 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
|
+
|
data/lib/n65/opcodes.rb
ADDED
data/lib/n65/parser.rb
ADDED
@@ -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
|
data/lib/n65/regexes.rb
ADDED
@@ -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
|