snes_utils 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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