snes_utils 0.1.1 → 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 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