snes_utils 0.1.1 → 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 ADDED
@@ -0,0 +1,609 @@
1
+ require 'securerandom'
2
+
3
+ module SnesUtils
4
+ class Vas
5
+ WDC65816 = :wdc65816
6
+ SPC700 = :spc700
7
+ SUPERFX = :superfx
8
+
9
+ DIRECTIVE = [
10
+ '.65816', '.spc700', '.superfx', '.org', '.base', '.db', '.rb', '.incbin'
11
+ ]
12
+
13
+ LABEL_OPERATORS = ['@', '!', '<', '>', '\^']
14
+
15
+ def initialize(filename, outfile)
16
+ raise "File not found: #{filename}" unless File.file?(filename)
17
+
18
+ @filename = filename
19
+ @outfile = outfile
20
+ @file = []
21
+ @label_registry = []
22
+ @reading_macro = false
23
+ @current_macro = nil
24
+ @macros_registry = {}
25
+ @define_registry = {}
26
+ @incbin_list = []
27
+ @byte_sequence_list = []
28
+ @memory = []
29
+ end
30
+
31
+ def assemble
32
+ construct_file
33
+
34
+ 2.times do |pass|
35
+ @program_counter = 0
36
+ @origin = 0
37
+ @base = 0
38
+ @cpu = WDC65816
39
+
40
+ assemble_file(pass)
41
+ end
42
+
43
+ write_label_registry
44
+ insert_bytes
45
+ incbin
46
+ write(@outfile)
47
+ end
48
+
49
+ def construct_file(filename = @filename)
50
+ File.open(filename).each_with_index do |raw_line, line_no|
51
+ line = raw_line.split(';').first.strip.chomp
52
+ next if line.empty?
53
+
54
+ if line.start_with?('.include')
55
+ raise "can't include file within macro" if @reading_macro
56
+
57
+ directive = line.split(' ')
58
+ inc_filename = directive[1].to_s.strip.chomp
59
+ dir = File.dirname(filename)
60
+
61
+ construct_file(File.join(dir, inc_filename))
62
+ elsif line.start_with?('.define')
63
+ raise "can't define variable within macro" if @reading_macro
64
+
65
+ args = line.split(' ')
66
+ key = "#{args[1]}"
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
71
+
72
+ raise "Already defined: #{key}" unless @define_registry[key].nil?
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)
88
+ else
89
+ new_line = replace_define(line)
90
+ line_info = { line: new_line, orig_line: line, line_no: line_no + 1, filename: filename }
91
+
92
+ if @reading_macro
93
+ line.start_with?('.endm') ? save_macro : @current_macro[:lines] << line_info
94
+ else
95
+ @file << line_info
96
+ end
97
+ end
98
+ end
99
+ end
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
145
+ end
146
+
147
+ def replace_define(line)
148
+ # TODO also support multiple define on same line
149
+ # hint : generalize replace_eval_label ?
150
+ found = nil
151
+
152
+ @define_registry.keys.each do |key|
153
+ if line.match(/\b#{key}\b/)
154
+ found = key
155
+ break
156
+ end
157
+ end
158
+
159
+ return line if found.nil?
160
+
161
+ val = @define_registry[found]
162
+
163
+ line.gsub(/\b#{found}\b/, val)
164
+ end
165
+
166
+ def assemble_file(pass)
167
+ @file.each do |line|
168
+ @line = line[:line]
169
+
170
+ if @line.include?(':')
171
+ arr = @line.split(':')
172
+ label = arr[0].strip.chomp
173
+ unless /^\w+$/ =~ label
174
+ raise "Invalid label: #{label}"
175
+ end
176
+ register_label(label, pass) # if pass == 0
177
+ next unless arr[1]
178
+ instruction = arr[1].strip.chomp
179
+ else
180
+ instruction = @line
181
+ end
182
+
183
+ next if instruction.empty?
184
+
185
+ if instruction.start_with?(*DIRECTIVE)
186
+ process_directive(instruction, pass, line)
187
+ next
188
+ end
189
+
190
+ begin
191
+ bytes = LineAssembler.new(instruction, **options).assemble
192
+ rescue => e
193
+ puts "Error at line #{line[:filename]}##{line[:line_no]} - (#{line[:orig_line]}) : #{e}"
194
+ exit(1)
195
+ end
196
+
197
+ insert(bytes) if pass == 1
198
+ @program_counter += bytes.size
199
+ end
200
+ end
201
+
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
210
+ end
211
+
212
+ def insert(bytes, insert_at = insert_index)
213
+ @memory[insert_at..insert_at + bytes.size - 1] = bytes
214
+ end
215
+
216
+ def insert_index
217
+ @program_counter + @base
218
+ end
219
+
220
+ def write(filename)
221
+ if filename.nil?
222
+ dir = File.dirname(@filename)
223
+ filename = File.join(dir, 'out.sfc')
224
+ end
225
+
226
+ File.open(filename, 'w+b') do |file|
227
+ file.write([@memory.map { |i| Vas::hex(i) }.join].pack('H*'))
228
+ end
229
+
230
+ filename
231
+ end
232
+
233
+ def self.hex(num, rjust_len = 2)
234
+ (num || 0).to_s(16).rjust(rjust_len, '0').upcase
235
+ end
236
+
237
+ def options
238
+ {
239
+ program_counter: @program_counter,
240
+ origin: @origin,
241
+ cpu: @cpu,
242
+ label_registry: @label_registry
243
+ }
244
+ end
245
+
246
+ def process_directive(instruction, pass, line_info)
247
+ directive = instruction.split(' ')
248
+
249
+ case directive[0]
250
+ when '.65816'
251
+ @cpu = WDC65816
252
+ when '.spc700'
253
+ @cpu = SPC700
254
+ when '.superfx'
255
+ @cpu = SUPERFX
256
+ when '.org'
257
+ update_origin(directive[1].to_i(16))
258
+ when '.base'
259
+ @base = directive[1].to_i(16)
260
+ when '.incbin'
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)
264
+ when '.db'
265
+ raw_line = directive[1..-1].join.to_s.strip.chomp
266
+ line = LineAssembler.new(raw_line, **options).replace_labels(raw_line)
267
+
268
+ @program_counter += define_bytes(line, pass)
269
+ when '.rb'
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)
302
+ end
303
+ end
304
+
305
+ def update_origin(param)
306
+ @origin = param
307
+ @program_counter = 0
308
+
309
+ update_base_from_origin
310
+ end
311
+
312
+ def update_base_from_origin
313
+ # TODO: automatically update base
314
+ # lorom/hirom scheme
315
+ # spc700 scheme
316
+ end
317
+
318
+ def prepare_incbin(filename, pass)
319
+ raise "Incbin: file not found: #{filename}" unless File.file?(filename)
320
+
321
+ @incbin_list << [filename, insert_index] if pass == 0
322
+ File.size(filename) || 0
323
+ end
324
+
325
+ def incbin
326
+ @incbin_list.each do |filename, index|
327
+ file = File.open(filename)
328
+ bytes = file.each_byte.to_a
329
+ @line = filename
330
+ insert(bytes, index)
331
+ end
332
+ end
333
+
334
+ def define_bytes(raw_bytes, pass)
335
+ bytes = raw_bytes.split(',').map { |rb| rb.scan(/.{2}/).reverse }.flatten.map do |b|
336
+ bv = b.to_i(16)
337
+ raise "Invalid byte: #{b} : #{@line}" if bv < 0 || bv > 0xff
338
+ bv
339
+ end
340
+
341
+ @byte_sequence_list << [bytes, insert_index] if pass == 0
342
+ bytes.size
343
+ end
344
+
345
+ def insert_bytes
346
+ @byte_sequence_list.each do |bytes, index|
347
+ insert(bytes, index)
348
+ end
349
+ end
350
+
351
+ def write_label_registry
352
+ longest = @label_registry.map{|r| r[0] }.max_by(&:length)
353
+
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|
361
+ @label_registry.each do |label|
362
+ adjusted_label = label[0].ljust(longest.length, ' ')
363
+ raw_address = Vas::hex(label[1], 6)
364
+ address = "#{raw_address[0..1]}/#{raw_address[2..-1]}"
365
+ file.write "#{adjusted_label} #{address}\n"
366
+ end
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
383
+ end
384
+ end
385
+
386
+ class LineAssembler
387
+ def initialize(raw_line, **options)
388
+ @line = raw_line.split(';').first.strip.chomp
389
+ @current_address = (options[:program_counter] + options[:origin])
390
+ @cpu = options[:cpu]
391
+ @label_registry = options[:label_registry]
392
+ end
393
+
394
+ def assemble
395
+ instruction = @line.split(' ')
396
+ mnemonic = instruction[0].upcase
397
+ raw_operand = instruction[1].to_s
398
+
399
+ raw_operand = Vas.replace_eval_label(@label_registry, raw_operand)
400
+ raw_operand = replace_label(raw_operand).downcase # TODO -> generalize replace_label_eval
401
+
402
+ opcode_data = detect_opcode(mnemonic, raw_operand)
403
+ raise "Invalid syntax #{@line}" unless opcode_data
404
+
405
+ opcode = opcode_data[:opcode]
406
+ @mode = opcode_data[:mode]
407
+ @length = opcode_data[:length]
408
+
409
+ operand_data = detect_operand(raw_operand)
410
+
411
+ return process_fx_instruction(opcode_data, operand_data) if special_fx_instruction?(opcode_data[:alt])
412
+
413
+ operand = process_operand(operand_data)
414
+
415
+ return [opcode, *operand]
416
+ end
417
+
418
+ def contains_label?(operand)
419
+ Vas::LABEL_OPERATORS.any? { |s| operand.include?(s[-1,1]) }
420
+ end
421
+
422
+ def replace_labels(operand)
423
+ while contains_label?(operand)
424
+ operand = replace_label(operand)
425
+ end
426
+
427
+ operand
428
+ end
429
+
430
+ def replace_label(operand)
431
+ return operand unless contains_label?(operand)
432
+
433
+ unless matches = /(#{Vas::LABEL_OPERATORS.join('|')})(\w+)(\+(\d+))?/.match(operand)
434
+ raise "Invalid label syntax: #{operand}"
435
+ end
436
+
437
+ mode = matches[1]
438
+ label = matches[2]
439
+ offset = matches[4].to_i
440
+
441
+ label_data = @label_registry.detect { |l| l[0] == label }
442
+
443
+ value = label_data ? label_data[1] : @current_address
444
+
445
+ value += offset
446
+
447
+ case mode
448
+ when '@'
449
+ value = value & 0x00ffff
450
+ new_value = Vas::hex(value, 4)
451
+ when '!'
452
+ value = value # | (((@current_address >> 16) & 0xff) << 16) # BUG HERE
453
+ new_value = Vas::hex(value, 6)
454
+ when '<'
455
+ value = value & 0x0000ff
456
+ new_value = Vas::hex(value)
457
+ when '>'
458
+ value = (value & 0x00ff00) >> 8
459
+ new_value = Vas::hex(value)
460
+ when '^'
461
+ mode = '\^'
462
+ value = (value & 0xff0000) >> 16
463
+ new_value = Vas::hex(value)
464
+ else
465
+ raise "Mode error: #{mode}"
466
+ end
467
+
468
+ operand.gsub(/(#{mode})\w+(\+(\d+))?/, new_value)
469
+ end
470
+
471
+ def detect_opcode(mnemonic, operand)
472
+ SnesUtils.const_get(@cpu.capitalize)::Definitions::OPCODES_DATA.detect do |row|
473
+ mode = row[:mode]
474
+ regex = SnesUtils.const_get(@cpu.capitalize)::Definitions::MODES_REGEXES[mode]
475
+ row[:mnemonic] == mnemonic && regex =~ operand
476
+ end
477
+ end
478
+
479
+ def detect_operand(raw_operand)
480
+ SnesUtils.const_get(@cpu.capitalize)::Definitions::MODES_REGEXES[@mode].match(raw_operand)
481
+ end
482
+
483
+ def process_operand(operand_data)
484
+ if double_operand_instruction?
485
+ process_double_operand_instruction(operand_data)
486
+ else
487
+ operand = operand_data[1]&.to_i(16)
488
+ rel_instruction? ? process_rel_operand(operand) : little_endian(operand, @length - 1)
489
+ end
490
+ end
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
+
534
+ def process_double_operand_instruction(operand_data)
535
+ if bit_instruction?
536
+ process_bit_operand(operand_data)
537
+ else
538
+ operands = [operand_data[1], operand_data[2]].map { |o| o.to_i(16) }
539
+ operand_2 = rel_instruction? ? process_rel_operand(operands[1]) : operands[1]
540
+
541
+ rel_instruction? ? [operands[0], operand_2] : [operand_2, operands[0]]
542
+ end
543
+ end
544
+
545
+ def process_bit_operand(operand_data)
546
+ m = operand_data[1].to_i(16)
547
+ raise "Out of range: m > 0x1fff: #{m}" if m > 0x1fff
548
+
549
+ b = operand_data[2].to_i(16)
550
+ raise "Out of range: b > 7: #{b}" if b > 7
551
+
552
+ little_endian(m << 3 | b, 2)
553
+ end
554
+
555
+ def process_rel_operand(operand)
556
+ relative_addr = operand - (@current_address & 0x00ffff) - @length
557
+
558
+ if @cpu == Vas::WDC65816 && @mode == :rell
559
+ raise "Relative address out of range: #{relative_addr}" if relative_addr < -32_768 || relative_addr > 32_767
560
+
561
+ relative_addr += 0x10000 if relative_addr < 0
562
+ little_endian(relative_addr, 2)
563
+ else
564
+ raise "Relative address out of range: #{relative_addr}" if relative_addr < -128 || relative_addr > 127
565
+
566
+ relative_addr += 0x100 if relative_addr < 0
567
+ relative_addr
568
+ end
569
+ end
570
+
571
+ def little_endian(operand, length)
572
+ if length > 2
573
+ [((operand >> 0) & 0xff), ((operand >> 8) & 0xff), ((operand >> 16) & 0xff)]
574
+ elsif length > 1
575
+ [((operand >> 0) & 0xff), ((operand >> 8) & 0xff)]
576
+ else
577
+ operand
578
+ end
579
+ end
580
+
581
+ def double_operand_instruction?
582
+ SnesUtils.const_get(@cpu.capitalize)::Definitions::DOUBLE_OPERAND_INSTRUCTIONS.include?(@mode)
583
+ end
584
+
585
+ def bit_instruction?
586
+ SnesUtils.const_get(@cpu.capitalize)::Definitions::BIT_INSTRUCTIONS.include?(@mode)
587
+ end
588
+
589
+ def rel_instruction?
590
+ SnesUtils.const_get(@cpu.capitalize)::Definitions::REL_INSTRUCTIONS.include?(@mode)
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
608
+ end
609
+ end