snes_utils 0.2.0 → 0.3.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.
data/lib/vas/vas.rb CHANGED
@@ -4,19 +4,24 @@ module SnesUtils
4
4
  class Vas
5
5
  WDC65816 = :wdc65816
6
6
  SPC700 = :spc700
7
+ SUPERFX = :superfx
7
8
 
8
9
  DIRECTIVE = [
9
- '.65816', '.spc700', '.org', '.base', '.db', '.rb', '.incbin', '.incsrc', '.define'
10
+ '.65816', '.spc700', '.superfx', '.org', '.base', '.db', '.rb', '.incbin'
10
11
  ]
11
12
 
12
13
  LABEL_OPERATORS = ['@', '!', '<', '>', '\^']
13
14
 
14
- def initialize(filename)
15
+ def initialize(filename, outfile)
15
16
  raise "File not found: #{filename}" unless File.file?(filename)
16
17
 
17
18
  @filename = filename
18
- @file = {}
19
+ @outfile = outfile
20
+ @file = []
19
21
  @label_registry = []
22
+ @reading_macro = false
23
+ @current_macro = nil
24
+ @macros_registry = {}
20
25
  @define_registry = {}
21
26
  @incbin_list = []
22
27
  @byte_sequence_list = []
@@ -38,7 +43,7 @@ module SnesUtils
38
43
  write_label_registry
39
44
  insert_bytes
40
45
  incbin
41
- write
46
+ write(@outfile)
42
47
  end
43
48
 
44
49
  def construct_file(filename = @filename)
@@ -47,27 +52,101 @@ module SnesUtils
47
52
  next if line.empty?
48
53
 
49
54
  if line.start_with?('.include')
55
+ raise "can't include file within macro" if @reading_macro
56
+
50
57
  directive = line.split(' ')
51
58
  inc_filename = directive[1].to_s.strip.chomp
59
+ dir = File.dirname(filename)
52
60
 
53
- construct_file(inc_filename)
61
+ construct_file(File.join(dir, inc_filename))
54
62
  elsif line.start_with?('.define')
63
+ raise "can't define variable within macro" if @reading_macro
64
+
55
65
  args = line.split(' ')
56
66
  key = "#{args[1]}"
57
- val = args[2..-1].join(' ').split(';').first.strip.chomp
67
+ raw_val = args[2..-1].join(' ').split(';').first
68
+ raise "Missing value for : #{key}" if raw_val.nil?
69
+
70
+ val = raw_val.strip.chomp
58
71
 
59
72
  raise "Already defined: #{key}" unless @define_registry[key].nil?
60
73
  @define_registry[key] = val
74
+ elsif line.start_with?('.call')
75
+ raise "can't call macro within macro" if @reading_macro
76
+
77
+ args = line.split(' ')
78
+ macro_name = args[1]
79
+ macro_args = args[2..-1].join.split(',')
80
+ call_macro(macro_name, macro_args, line_no + 1)
81
+ elsif line.start_with?('.macro')
82
+ raise "can't have nested macro" if @reading_macro
83
+
84
+ args = line.split(' ')
85
+ macro_name = args[1]
86
+ macro_args = args[2..-1].join.split(',')
87
+ init_macro(macro_name, macro_args)
61
88
  else
62
89
  new_line = replace_define(line)
90
+ line_info = { line: new_line, orig_line: line, line_no: line_no + 1, filename: filename }
63
91
 
64
- @file[SecureRandom.uuid] = { line: new_line, orig_line: line, line_no: line_no + 1, filename: filename }
92
+ if @reading_macro
93
+ line.start_with?('.endm') ? save_macro : @current_macro[:lines] << line_info
94
+ else
95
+ @file << line_info
96
+ end
65
97
  end
66
98
  end
99
+ end
67
100
 
101
+ def init_macro(name, args)
102
+ @current_macro = { name: name, args: args, lines: [] }
103
+ @reading_macro = true
104
+ end
105
+
106
+ def save_macro
107
+ name = @current_macro[:name]
108
+ raise "macro `#{name}` already defined" unless @macros_registry[name].nil?
109
+ @macros_registry[name] = @current_macro
110
+ @reading_macro = false
111
+ end
112
+
113
+ def call_macro(name, raw_args, line_no)
114
+ macro = @macros_registry[name]
115
+ uuid = SecureRandom.uuid
116
+ raise "line #{line_no}: call of undefined macro `#{name}`" if macro.nil?
117
+
118
+ args_names = @macros_registry[name][:args]
119
+ if args_names.count != raw_args.count
120
+ raise "line #{line_no}: wrong number of arguments for macro `#{name}` expected : #{args_names.count}, given: #{raw_args.count}"
121
+ end
122
+ args = {}
123
+ args_names.count.times do |i|
124
+ args[args_names[i]] = raw_args[i]
125
+ end
126
+
127
+ macro[:lines].each_with_index do |line_info|
128
+ line = line_info[:line]
129
+
130
+ if line.include?('%')
131
+ # replace variable with arg
132
+ matches = line.match(/%(\w+)%?/)
133
+ if matches[1] == 'MACRO_ID'
134
+ value = uuid.delete('-')
135
+ else
136
+ value = args[matches[1]]
137
+ end
138
+ raise "line #{line_no}: undefined variable `#{matches[1]}` for macro `#{name}`" if value.nil?
139
+ replaced_line = line.gsub(/#{matches[0]}/, value)
140
+ @file << line_info.merge(line: replace_define(replaced_line))
141
+ else
142
+ @file << line_info
143
+ end
144
+ end
68
145
  end
69
146
 
70
147
  def replace_define(line)
148
+ # TODO also support multiple define on same line
149
+ # hint : generalize replace_eval_label ?
71
150
  found = nil
72
151
 
73
152
  @define_registry.keys.each do |key|
@@ -77,17 +156,16 @@ module SnesUtils
77
156
  end
78
157
  end
79
158
 
80
-
81
159
  return line if found.nil?
82
160
 
83
161
  val = @define_registry[found]
84
162
 
85
- line.gsub(/\b#{found}/, val)
163
+ line.gsub(/\b#{found}\b/, val)
86
164
  end
87
165
 
88
166
  def assemble_file(pass)
89
- @file.each do |key, val|
90
- @line = val[:line]
167
+ @file.each do |line|
168
+ @line = line[:line]
91
169
 
92
170
  if @line.include?(':')
93
171
  arr = @line.split(':')
@@ -95,7 +173,7 @@ module SnesUtils
95
173
  unless /^\w+$/ =~ label
96
174
  raise "Invalid label: #{label}"
97
175
  end
98
- register_label(label) if pass == 0
176
+ register_label(label, pass) # if pass == 0
99
177
  next unless arr[1]
100
178
  instruction = arr[1].strip.chomp
101
179
  else
@@ -105,14 +183,14 @@ module SnesUtils
105
183
  next if instruction.empty?
106
184
 
107
185
  if instruction.start_with?(*DIRECTIVE)
108
- process_directive(instruction, pass)
186
+ process_directive(instruction, pass, line)
109
187
  next
110
188
  end
111
189
 
112
190
  begin
113
191
  bytes = LineAssembler.new(instruction, **options).assemble
114
192
  rescue => e
115
- puts "Error at line #{val[:filename]}##{val[:line_no]} - (#{val[:orig_line]}) : #{e}"
193
+ puts "Error at line #{line[:filename]}##{line[:line_no]} - (#{line[:orig_line]}) : #{e}"
116
194
  exit(1)
117
195
  end
118
196
 
@@ -121,9 +199,14 @@ module SnesUtils
121
199
  end
122
200
  end
123
201
 
124
- def register_label(label)
125
- raise "Label already defined: #{label}" if @label_registry.detect { |l| l[0] == label }
126
- @label_registry << [label, @program_counter + @origin]
202
+ def register_label(label, pass)
203
+ if pass == 0
204
+ raise "Label already defined: #{label}" if @label_registry.detect { |l| l[0] == label }
205
+ @label_registry << [label, @program_counter + @origin]
206
+ else
207
+ index = @label_registry.index { |l| l[0] == label }
208
+ @label_registry[index][1] = @program_counter + @origin
209
+ end
127
210
  end
128
211
 
129
212
  def insert(bytes, insert_at = insert_index)
@@ -134,7 +217,12 @@ module SnesUtils
134
217
  @program_counter + @base
135
218
  end
136
219
 
137
- def write(filename = 'out.smc')
220
+ def write(filename)
221
+ if filename.nil?
222
+ dir = File.dirname(@filename)
223
+ filename = File.join(dir, 'out.sfc')
224
+ end
225
+
138
226
  File.open(filename, 'w+b') do |file|
139
227
  file.write([@memory.map { |i| Vas::hex(i) }.join].pack('H*'))
140
228
  end
@@ -155,7 +243,7 @@ module SnesUtils
155
243
  }
156
244
  end
157
245
 
158
- def process_directive(instruction, pass)
246
+ def process_directive(instruction, pass, line_info)
159
247
  directive = instruction.split(' ')
160
248
 
161
249
  case directive[0]
@@ -163,21 +251,54 @@ module SnesUtils
163
251
  @cpu = WDC65816
164
252
  when '.spc700'
165
253
  @cpu = SPC700
254
+ when '.superfx'
255
+ @cpu = SUPERFX
166
256
  when '.org'
167
257
  update_origin(directive[1].to_i(16))
168
258
  when '.base'
169
259
  @base = directive[1].to_i(16)
170
260
  when '.incbin'
171
- @program_counter += prepare_incbin(directive[1].to_s.strip.chomp, pass)
261
+ inc_filename = directive[1].to_s.strip.chomp
262
+ dir = File.dirname(line_info[:filename])
263
+ @program_counter += prepare_incbin(File.join(dir, inc_filename), pass)
172
264
  when '.db'
173
265
  raw_line = directive[1..-1].join.to_s.strip.chomp
174
266
  line = LineAssembler.new(raw_line, **options).replace_labels(raw_line)
175
267
 
176
268
  @program_counter += define_bytes(line, pass)
177
269
  when '.rb'
178
- @program_counter += directive[1].to_i(16)
179
- when '.define'
180
- # TODO
270
+ arg = directive[1..-1].join
271
+
272
+ count = self.class.replace_eval_label(@label_registry, arg)
273
+ @program_counter += count.is_a?(String) ? count.to_i(16) : count
274
+ end
275
+ end
276
+
277
+ def self.replace_eval_label(registry, arg)
278
+ return arg unless matches = /({(.*)})(w)?$/.match(arg)
279
+
280
+ found = {}
281
+
282
+ registry.each do |key, val|
283
+ if arg.match(/\b#{key}\b/)
284
+ found[key] = val
285
+ end
286
+ end
287
+
288
+ return arg.to_i(16) if found.empty?
289
+
290
+ new_arg = matches[2]
291
+
292
+ found.each do |key, val|
293
+ new_arg = new_arg.gsub(/\b#{key}\b/, val.to_s)
294
+ end
295
+
296
+ if matches[3] == 'w'
297
+ res = eval(new_arg).to_s(16).rjust(4, '0')[-4..-1]
298
+ arg[0..-2].gsub(matches[1], res)
299
+ else
300
+ res = eval(new_arg).to_s(16).rjust(2, '0')[-2..-1]
301
+ arg.gsub(matches[1], res)
181
302
  end
182
303
  end
183
304
 
@@ -230,7 +351,13 @@ module SnesUtils
230
351
  def write_label_registry
231
352
  longest = @label_registry.map{|r| r[0] }.max_by(&:length)
232
353
 
233
- File.open('labels.txt', 'w+b') do |file|
354
+ if @outfile.nil?
355
+ dir = File.dirname(@filename)
356
+ else
357
+ dir = File.dirname(@outfile)
358
+ end
359
+
360
+ File.open(File.join(dir, 'labels.txt'), 'w+b') do |file|
234
361
  @label_registry.each do |label|
235
362
  adjusted_label = label[0].ljust(longest.length, ' ')
236
363
  raw_address = Vas::hex(label[1], 6)
@@ -238,6 +365,21 @@ module SnesUtils
238
365
  file.write "#{adjusted_label} #{address}\n"
239
366
  end
240
367
  end
368
+ File.open(File.join(dir, 'labels.msl'), 'w+b') do |file|
369
+ @label_registry.each do |label|
370
+ if label[1] >= 0x7e0000 && label[1] <= 0x7fffff
371
+ bank = label[1] & 0xff0000
372
+ address = "WORK:#{Vas::hex(label[1] - bank)}:#{label[0]}:"
373
+ else
374
+ bank = label[1] & 0xff0000
375
+ bank_i = bank >> 16 & 0xf
376
+ # low rom only for now
377
+ prg_addr = label[1] - bank - 0x8000 + bank_i * 0x8000
378
+ address = "PRG:#{Vas::hex(prg_addr)}:#{label[0]}:"
379
+ end
380
+ file.write "#{address}\n"
381
+ end
382
+ end
241
383
  end
242
384
  end
243
385
 
@@ -254,10 +396,11 @@ module SnesUtils
254
396
  mnemonic = instruction[0].upcase
255
397
  raw_operand = instruction[1].to_s
256
398
 
257
- raw_operand = replace_label(raw_operand)
399
+ raw_operand = Vas.replace_eval_label(@label_registry, raw_operand)
400
+ raw_operand = replace_label(raw_operand).downcase # TODO -> generalize replace_label_eval
258
401
 
259
402
  opcode_data = detect_opcode(mnemonic, raw_operand)
260
- raise "Invalid syntax" unless opcode_data
403
+ raise "Invalid syntax #{@line}" unless opcode_data
261
404
 
262
405
  opcode = opcode_data[:opcode]
263
406
  @mode = opcode_data[:mode]
@@ -265,6 +408,8 @@ module SnesUtils
265
408
 
266
409
  operand_data = detect_operand(raw_operand)
267
410
 
411
+ return process_fx_instruction(opcode_data, operand_data) if special_fx_instruction?(opcode_data[:alt])
412
+
268
413
  operand = process_operand(operand_data)
269
414
 
270
415
  return [opcode, *operand]
@@ -304,7 +449,7 @@ module SnesUtils
304
449
  value = value & 0x00ffff
305
450
  new_value = Vas::hex(value, 4)
306
451
  when '!'
307
- value = value | (((@current_address >> 16) & 0xff) << 16)
452
+ value = value # | (((@current_address >> 16) & 0xff) << 16) # BUG HERE
308
453
  new_value = Vas::hex(value, 6)
309
454
  when '<'
310
455
  value = value & 0x0000ff
@@ -344,6 +489,48 @@ module SnesUtils
344
489
  end
345
490
  end
346
491
 
492
+ def process_fx_instruction(opcode_data, operand_data)
493
+ alt = opcode_data[:alt]
494
+
495
+ return [alt, opcode_data[:opcode]].compact if @mode == :imp
496
+
497
+ if fx_mov_instruction?
498
+ reg1 = operand_data[1].to_i(10)
499
+ raise "Invalid register R#{reg1}" if reg1 > 15
500
+ reg2 = operand_data[2].to_i(10)
501
+ raise "Invalid register R#{reg2}" if reg2 > 15
502
+
503
+ raw_opcode = opcode_data[:opcode]
504
+ tmp_opcode = raw_opcode | (reg2 << 8) | reg1
505
+ opcode = [((tmp_opcode >> 8) & 0xff), ((tmp_opcode >> 0) & 0xff)]
506
+
507
+ operand = nil
508
+ else
509
+ base = @mode == :imm4 ? 16 : 10
510
+ index = inverted_dest_instruction? ? 2 : 1
511
+
512
+ opcode_suffix = operand_data[index].to_i(base)
513
+ opcode_prefix = opcode_data[:opcode]
514
+ opcode = (opcode_prefix << 4) | opcode_suffix
515
+
516
+ operand = process_fx_operand(operand_data, alt.nil? ? 0 : 1)
517
+ end
518
+
519
+ [alt, *opcode, *operand].compact
520
+ end
521
+
522
+ def process_fx_operand(operand_data, alt)
523
+ return unless double_operand_instruction?
524
+
525
+ index = inverted_dest_instruction? ? 1 : 2
526
+ operand = operand_data[index]&.to_i(16)
527
+
528
+ raise 'Invalid address, must be multiple of 2' if short_addr_instruction? && operand.odd?
529
+ operand /= 2 if short_addr_instruction?
530
+
531
+ little_endian(operand, @length - 1 - alt)
532
+ end
533
+
347
534
  def process_double_operand_instruction(operand_data)
348
535
  if bit_instruction?
349
536
  process_bit_operand(operand_data)
@@ -379,7 +566,6 @@ module SnesUtils
379
566
  relative_addr += 0x100 if relative_addr < 0
380
567
  relative_addr
381
568
  end
382
-
383
569
  end
384
570
 
385
571
  def little_endian(operand, length)
@@ -403,5 +589,21 @@ module SnesUtils
403
589
  def rel_instruction?
404
590
  SnesUtils.const_get(@cpu.capitalize)::Definitions::REL_INSTRUCTIONS.include?(@mode)
405
591
  end
592
+
593
+ def special_fx_instruction?(alt)
594
+ @cpu == Vas::SUPERFX && (SnesUtils.const_get(@cpu.capitalize)::Definitions::SFX_INSTRUCTIONS.include?(@mode) || !alt.nil?)
595
+ end
596
+
597
+ def fx_mov_instruction?
598
+ @cpu == Vas::SUPERFX && SnesUtils.const_get(@cpu.capitalize)::Definitions::MOV_INSTRUCTIONS.include?(@mode)
599
+ end
600
+
601
+ def short_addr_instruction?
602
+ @cpu == Vas::SUPERFX && SnesUtils.const_get(@cpu.capitalize)::Definitions::SHORT_ADDR_INSTRUCTIONS.include?(@mode)
603
+ end
604
+
605
+ def inverted_dest_instruction?
606
+ @cpu == Vas::SUPERFX && SnesUtils.const_get(@cpu.capitalize)::Definitions::INV_DEST_INSTRUCTIONS.include?(@mode)
607
+ end
406
608
  end
407
609
  end
@@ -0,0 +1,136 @@
1
+ require 'spec_helper'
2
+ require 'snes_utils'
3
+
4
+ describe SnesUtils::MiniAssembler do
5
+ let(:mini_asm) { SnesUtils::MiniAssembler.new }
6
+
7
+ describe '#parse_address' do
8
+ subject { mini_asm.parse_address(line) }
9
+
10
+ context 'when address is given' do
11
+ let(:line) { '300: lda #$12' }
12
+
13
+ it 'returns the address given' do
14
+ expect(subject).to eq(0x300)
15
+ end
16
+ end
17
+
18
+ context 'when address is implied' do
19
+ let(:line) { 'lda #$12' }
20
+
21
+ it 'returns the current address' do
22
+ expect(subject).to eq(0)
23
+ end
24
+ end
25
+ end
26
+
27
+ describe '#parse_instruction' do
28
+ subject { mini_asm.parse_instruction(line) }
29
+ context 'imm8' do
30
+ let(:line) { 'ora #17' }
31
+
32
+ it do
33
+ expect(subject).to eq [['09', '17'], 2, 0]
34
+ end
35
+ end
36
+
37
+ context 'abs' do
38
+ let(:line) { 'lda $1234' }
39
+
40
+ it do
41
+ expect(subject).to eq [['AD','34','12'], 3, 0]
42
+ end
43
+ end
44
+
45
+ context 'imp' do
46
+ let(:line) { '300:xce' }
47
+
48
+ it do
49
+ expect(subject).to eq [['FB'], 1, 0x300]
50
+ end
51
+ end
52
+
53
+ context 'idly' do
54
+ let(:line) { 'ADC [$12],Y' }
55
+
56
+ it do
57
+ expect(subject).to eq [['77', '12'], 2, 0]
58
+ end
59
+ end
60
+
61
+ context 'bm' do
62
+ let(:line) { 'MVN $12,$34' }
63
+
64
+ it do
65
+ expect(subject).to eq [['54', '34', '12'], 3, 0]
66
+ end
67
+ end
68
+
69
+ describe 'relative addressing' do
70
+ context '8 bit rel postivie' do
71
+ let(:line) { '300:BMI 305' }
72
+
73
+ it { expect(subject).to eq [['30', '03'], 2, 0x300] }
74
+ end
75
+
76
+ context '8 bit rel negative' do
77
+ let(:line) { '30a:bcc 300' }
78
+
79
+ it { expect(subject).to eq [['90', 'F4'], 2, 0x30a] }
80
+ end
81
+
82
+ context '16 bit rel postivie' do
83
+ let(:line) { '304:BRL $03A0' }
84
+
85
+ it { expect(subject).to eq [['82', '99', '00'], 3, 0x304] }
86
+ end
87
+
88
+ context '16 bit rel negative' do
89
+ let(:line) { '3bf:per 37a' }
90
+
91
+ it { expect(subject).to eq [['62', 'B8', 'FF'], 3, 0x3bf] }
92
+ end
93
+ end
94
+ end
95
+
96
+ describe '#disassemble_range' do
97
+ subject { mini_asm.disassemble_range(0, count) }
98
+
99
+ let(:count) { 1 }
100
+
101
+ before { mini_asm.instance_variable_set(:@memory, memory) }
102
+
103
+ context 'when cpu is 65816' do
104
+ let(:memory) { ['82', '99', '00'] }
105
+
106
+ it { expect(subject).to eq ['00/0000: 82 99 00 BRL 009C {+0099}'] }
107
+ end
108
+
109
+ context 'when cpu is spc700' do
110
+ let(:memory) { ['00'] }
111
+
112
+ before { mini_asm.instance_variable_set(:@cpu, :spc700) }
113
+
114
+ it { expect(subject).to eq ['00/0000: 00 NOP'] }
115
+ end
116
+ end
117
+
118
+ describe '#read' do
119
+ let(:file) { 'spec/fixtures/test.asm' }
120
+ before { mini_asm.read(file) }
121
+ let(:memory) do
122
+ ["A9", "12", "78", "18", "C9", "14", "3A", "D0",
123
+ "FD", "00", "00", "80", "02", "42", "12", "AD",
124
+ "34", "12", "80", "F2", nil, nil, nil, nil,
125
+ nil, nil, nil, nil, nil, nil, nil, nil,
126
+ "E4", "12", "4F", "12", "50", "FC", "53", "12",
127
+ "F9", "FA", "22", "11", nil, nil, nil, nil,
128
+ "11", "22", "33", "44", "55", "66", "77", "88",
129
+ "DE", "AF", "FA", "CE", "DE", "CA", "FE", "12"]
130
+ end
131
+
132
+ it 'sets memory correctly' do
133
+ expect(mini_asm.instance_variable_get(:@memory)).to eq memory
134
+ end
135
+ end
136
+ end