1
0
mirror of https://github.com/safiire/n65.git synced 2024-06-02 05:41:36 +00:00

Merge pull request #1 from safiire/reformat-code

Installed Rubocop and corrected many linter errors.
This commit is contained in:
Saf 2020-08-30 15:06:35 -07:00 committed by GitHub
commit 1d03529b29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 741 additions and 986 deletions

125
.rubocop.yml Normal file
View File

@ -0,0 +1,125 @@
Layout/LineLength:
Max: 120
Naming/MethodParameterName:
MinNameLength: 1
Style/FormatStringToken:
Enabled: false
Style/Documentation:
Enabled: false
Lint/MissingSuper:
Enabled: false
Style/ParallelAssignment:
Enabled: false
Layout/EmptyLinesAroundAttributeAccessor:
Enabled: true
Layout/SpaceAroundMethodCallOperator:
Enabled: true
Lint/BinaryOperatorWithIdenticalOperands:
Enabled: true
Lint/DeprecatedOpenSSLConstant:
Enabled: true
Lint/DuplicateElsifCondition:
Enabled: true
Lint/DuplicateRescueException:
Enabled: true
Lint/EmptyConditionalBody:
Enabled: true
Lint/FloatComparison:
Enabled: true
Lint/MixedRegexpCaptureTypes:
Enabled: true
Lint/OutOfRangeRegexpRef:
Enabled: true
Lint/RaiseException:
Enabled: true
Lint/SelfAssignment:
Enabled: true
Lint/StructNewOverride:
Enabled: true
Lint/TopLevelReturnWithArgument:
Enabled: true
Lint/UnreachableLoop:
Enabled: true
Style/AccessorGrouping:
Enabled: true
Style/ArrayCoercion:
Enabled: true
Style/BisectedAttrAccessor:
Enabled: true
Style/CaseLikeIf:
Enabled: true
Style/ExplicitBlockArgument:
Enabled: true
Style/ExponentialNotation:
Enabled: true
Style/GlobalStdStream:
Enabled: true
Style/HashAsLastArrayItem:
Enabled: true
Style/HashEachMethods:
Enabled: true
Style/HashLikeCase:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
Style/OptionalBooleanParameter:
Enabled: true
Style/RedundantAssignment:
Enabled: true
Style/RedundantFetchBlock:
Enabled: true
Style/RedundantFileExtensionInRequire:
Enabled: true
Style/RedundantRegexpCharacterClass:
Enabled: true
Style/RedundantRegexpEscape:
Enabled: true
Style/SingleArgumentDig:
Enabled: true
Style/SlicingWithRange:
Enabled: true
Style/StringConcatenation:
Enabled: true

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
source 'https://rubygems.org' source 'https://rubygems.org'
# Specify your gem's dependencies in n65.gemspec # Specify gem dependencies in n65.gemspec
gemspec gemspec
Gem 'mintest'

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'bundler/gem_tasks' require 'bundler/gem_tasks'
require 'rake/testtask' require 'rake/testtask'
@ -5,8 +7,7 @@ Rake::TestTask.new do |t|
t.pattern = 'test/test*.rb' t.pattern = 'test/test*.rb'
end end
## Check the syntax of all ruby files ## Check the syntax of all ruby files
task :syntax do |t| task :syntax do
sh "find . -name *.rb -type f -exec ruby -c {} \\; -exec echo {} \\;" sh 'find . -name *.rb -type f -exec ruby -c {} \; -exec echo {} \;'
end end

View File

@ -1,4 +1,6 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
# frozen_string_literal: true
############################################################################### ###############################################################################
## 6502 Assembler for the NES's 2A03 ## 6502 Assembler for the NES's 2A03
## ##

View File

@ -1,83 +1,80 @@
# frozen_string_literal: true
require_relative 'n65/version' require_relative 'n65/version'
require_relative 'n65/symbol_table' require_relative 'n65/symbol_table'
require_relative 'n65/memory_space' require_relative 'n65/memory_space'
require_relative 'n65/parser' require_relative 'n65/parser'
module N65 module N65
class Assembler class Assembler
attr_reader :program_counter, :current_segment, :current_bank, :symbol_table, :virtual_memory, :promises attr_reader :program_counter, :current_segment, :current_bank, :symbol_table, :virtual_memory, :promises
##### Custom exceptions
class AddressOutOfRange < StandardError; end class AddressOutOfRange < StandardError; end
class InvalidSegment < StandardError; end class InvalidSegment < StandardError; end
class WriteOutOfBounds < StandardError; end class WriteOutOfBounds < StandardError; end
class INESHeaderAlreadySet < StandardError; end class INESHeaderAlreadySet < StandardError; end
class FileNotFound < StandardError; end class FileNotFound < StandardError; end
# Assemble from an asm file to a nes ROM
#### # TODO: This reall needs a logger instead of all these unless quiet conditions
## Assemble from an asm file to a nes ROM
def self.from_file(infile, options) def self.from_file(infile, options)
fail(FileNotFound, infile) unless File.exists?(infile) raise(FileNotFound, infile) unless File.exist?(infile)
assembler = self.new assembler = new
program = File.read(infile) program = File.read(infile)
output_file = options[:output_file] output_file = options[:output_file]
puts "Building #{infile}" unless options[:quiet] puts "Building #{infile}" unless options[:quiet]
## Process each line in the file # Process each line in the file
program.split(/\n/).each_with_index do |line, line_number| program.split(/\n/).each_with_index do |line, line_number|
begin begin
assembler.assemble_one_line(line) assembler.assemble_one_line(line)
rescue StandardError => e rescue StandardError => e
STDERR.puts("\n\n#{e.class}\n#{line}\n#{e}\nOn line #{line_number}") warn("\n\n#{e.class}\n#{line}\n#{e}\nOn line #{line_number}")
exit(1) exit(1)
end end
print '.' unless options[:quiet] print '.' unless options[:quiet]
end end
puts unless options[:quiet] puts unless options[:quiet]
## Second pass to resolve any missing symbols. # Second pass to resolve any missing symbols.
print "Second pass, resolving symbols..." unless options[:quiet] print 'Second pass, resolving symbols...' unless options[:quiet]
assembler.fulfill_promises assembler.fulfill_promises
puts " Done." unless options[:quiet] puts ' Done.' unless options[:quiet]
## Optionally write out a symbol map # Optionally write out a symbol map
if options[:write_symbol_table] if options[:write_symbol_table]
print "Writing symbol table to #{output_file}.yaml..." unless options[:quiet] print "Writing symbol table to #{output_file}.yaml..." unless options[:quiet]
File.open("#{output_file}.yaml", 'w') do |fp| File.open("#{output_file}.yaml", 'w') do |fp|
fp.write(assembler.symbol_table.export_to_yaml) fp.write(assembler.symbol_table.export_to_yaml)
end end
puts "Done." unless options[:quiet] puts 'Done.' unless options[:quiet]
end end
## Optionally write out cycle count for subroutines # Optionally write out cycle count for subroutines
if options[:cycle_count] if options[:cycle_count]
print "Writing subroutine cycle counts to #{output_file}.cycles.yaml..." unless options[:quiet] print "Writing subroutine cycle counts to #{output_file}.cycles.yaml..." unless options[:quiet]
File.open("#{output_file}.cycles.yaml", 'w') do |fp| File.open("#{output_file}.cycles.yaml", 'w') do |fp|
fp.write(assembler.symbol_table.export_cycle_count_yaml) fp.write(assembler.symbol_table.export_cycle_count_yaml)
end end
puts "Done." unless options[:quiet] puts 'Done.' unless options[:quiet]
end end
## Emit the complete binary ROM # Emit the complete binary ROM
rom = assembler.emit_binary_rom rom = assembler.emit_binary_rom
File.open(output_file, 'w') do |fp| File.open(output_file, 'w') do |fp|
fp.write(rom) fp.write(rom)
end end
unless options[:quiet] return if options[:quiet]
rom_size = rom.size
rom_size_hex = "%x" % rom_size rom_size = rom.size
assembler.print_bank_usage rom_size_hex = format('%x', rom_size)
puts "Total size: $#{rom_size_hex}, #{rom_size} bytes" assembler.print_bank_usage
end puts "Total size: $#{rom_size_hex}, #{rom_size} bytes"
end end
# Initialize with a bank 1 of prog space for starters
####
## Initialize with a bank 1 of prog space for starters
def initialize def initialize
@ines_header = nil @ines_header = nil
@program_counter = 0x0 @program_counter = 0x0
@ -86,141 +83,119 @@ module N65
@symbol_table = SymbolTable.new @symbol_table = SymbolTable.new
@promises = [] @promises = []
@virtual_memory = { @virtual_memory = {
:prog => [MemorySpace.create_prog_rom], prog: [MemorySpace.create_prog_rom],
:char => [] char: []
} }
end end
# Return an object that contains the assembler's current state
####
## Return an object that contains the assembler's current state
def get_current_state def get_current_state
saved_program_counter, saved_segment, saved_bank = @program_counter, @current_segment, @current_bank saved_program_counter, saved_segment, saved_bank = @program_counter, @current_segment, @current_bank
saved_scope = symbol_table.scope_stack.dup saved_scope = symbol_table.scope_stack.dup
OpenStruct.new(program_counter: saved_program_counter, segment: saved_segment, bank: saved_bank, scope: saved_scope) OpenStruct.new(
program_counter: saved_program_counter,
segment: saved_segment,
bank: saved_bank,
scope: saved_scope
)
end end
# Set the current state from an OpenStruct
####
## Set the current state from an OpenStruct
def set_current_state(struct) def set_current_state(struct)
@program_counter, @current_segment, @current_bank = struct.program_counter, struct.segment, struct.bank @program_counter, @current_segment, @current_bank = struct.program_counter, struct.segment, struct.bank
symbol_table.scope_stack = struct.scope.dup symbol_table.scope_stack = struct.scope.dup
end end
# This is the main assemble method, it parses one line into an object
#### # which when given a reference to this assembler, controls the assembler
## This is the main assemble method, it parses one line into an object # itself through public methods, executing assembler directives, and
## which when given a reference to this assembler, controls the assembler # emitting bytes into our virtual memory spaces. Empty lines or lines
## itself through public methods, executing assembler directives, and # with only comments parse to nil, and we just ignore them.
## emitting bytes into our virtual memory spaces. Empty lines or lines
## with only comments parse to nil, and we just ignore them.
def assemble_one_line(line) def assemble_one_line(line)
parsed_object = Parser.parse(line) parsed_object = Parser.parse(line)
return if parsed_object.nil?
unless parsed_object.nil? exec_result = parsed_object.exec(self)
exec_result = parsed_object.exec(self)
## TODO # TODO: I could perhaps keep a tally of cycles used per top level scope here
## I could perhaps keep a tally of cycles used per top level scope here if parsed_object.respond_to?(:cycles)
if parsed_object.respond_to?(:cycles) # puts "Line: #{line}"
#puts "Line: #{line}" # puts "Cycles #{parsed_object.cycles}"
#puts "Cycles #{parsed_object.cycles}" # puts "Sym: #{@symbol_table.scope_stack}"
#puts "Sym: #{@symbol_table.scope_stack}" @symbol_table.add_cycles(parsed_object.cycles)
@symbol_table.add_cycles(parsed_object.cycles)
end
## If we have returned a promise save it for the second pass
@promises << exec_result if exec_result.kind_of?(Proc)
end end
# If we have returned a promise save it for the second pass
@promises << exec_result if exec_result.is_a?(Proc)
end end
# This will empty out our promise queue and try to fullfil operations
#### # that required an undefined symbol when first encountered.
## This will empty out our promise queue and try to fullfil operations
## that required an undefined symbol when first encountered.
def fulfill_promises def fulfill_promises
while promise = @promises.pop while (promise = @promises.pop)
promise.call promise.call
end end
end end
# This rewinds the state of the assembler, so a promise can be
#### # executed with a previous state, for example if we can't resolve
## This rewinds the state of the assembler, so a promise can be # a symbol right now, and want to try during the second pass
## executed with a previous state, for example if we can't resolve
## a symbol right now, and want to try during the second pass
def with_saved_state(&block) def with_saved_state(&block)
## Save the current state of the assembler ## Save the current state of the assembler
old_state = get_current_state old_state = get_current_state
lambda do lambda do
# Set the assembler state back to the old state and run the block like that
## Set the assembler state back to the old state and run the block like that
set_current_state(old_state) set_current_state(old_state)
block.call(self) block.call(self)
end end
end end
# Write to memory space. Typically, we are going to want to write
#### # to the location of the current PC, current segment, and current bank.
## Write to memory space. Typically, we are going to want to write # Bounds check is inside MemorySpace#write
## to the location of the current PC, current segment, and current bank.
## Bounds check is inside MemorySpace#write
def write_memory(bytes, pc = @program_counter, segment = @current_segment, bank = @current_bank) def write_memory(bytes, pc = @program_counter, segment = @current_segment, bank = @current_bank)
memory_space = get_virtual_memory_space(segment, bank) memory_space = get_virtual_memory_space(segment, bank)
memory_space.write(pc, bytes) memory_space.write(pc, bytes)
@program_counter += bytes.size @program_counter += bytes.size
end end
# Set the iNES header
####
## Set the iNES header
def set_ines_header(ines_header) def set_ines_header(ines_header)
fail(INESHeaderAlreadySet) unless @ines_header.nil? raise(INESHeaderAlreadySet) unless @ines_header.nil?
@ines_header = ines_header @ines_header = ines_header
end end
# Set the program counter
####
## Set the program counter
def program_counter=(address) def program_counter=(address)
fail(AddressOutOfRange) unless address_within_range?(address) raise(AddressOutOfRange) unless address_within_range?(address)
@program_counter = address @program_counter = address
end end
# Set the current segment, prog or char.
####
## Set the current segment, prog or char.
def current_segment=(segment) def current_segment=(segment)
segment = segment.to_sym segment = segment.to_sym
unless valid_segment?(segment) raise(InvalidSegment, "#{segment} is not a valid segment. Try prog or char") unless valid_segment?(segment)
fail(InvalidSegment, "#{segment} is not a valid segment. Try prog or char")
end
@current_segment = segment @current_segment = segment
end end
# Set the current bank, create it if it does not exist
####
## Set the current bank, create it if it does not exist
def current_bank=(bank_number) def current_bank=(bank_number)
memory_space = get_virtual_memory_space(@current_segment, bank_number) memory_space = get_virtual_memory_space(@current_segment, bank_number)
if memory_space.nil? @virtual_memory[@current_segment][bank_number] = MemorySpace.create_bank(@current_segment) if memory_space.nil?
@virtual_memory[@current_segment][bank_number] = MemorySpace.create_bank(@current_segment)
end
@current_bank = bank_number @current_bank = bank_number
end end
####
## Emit a binary ROM
def emit_binary_rom def emit_binary_rom
progs = @virtual_memory[:prog] progs = @virtual_memory[:prog]
chars = @virtual_memory[:char] chars = @virtual_memory[:char]
rom_size = 0x10 rom_size = 0x10
rom_size += MemorySpace::BankSizes[:prog] * progs.size rom_size += MemorySpace::BANK_SIZES[:prog] * progs.size
rom_size += MemorySpace::BankSizes[:char] * chars.size rom_size += MemorySpace::BANK_SIZES[:char] * chars.size
rom = MemorySpace.new(rom_size, :rom) rom = MemorySpace.new(rom_size, :rom)
@ -228,22 +203,20 @@ module N65
offset += rom.write(0x0, @ines_header.emit_bytes) offset += rom.write(0x0, @ines_header.emit_bytes)
progs.each do |prog| progs.each do |prog|
offset += rom.write(offset, prog.read(0x8000, MemorySpace::BankSizes[:prog])) offset += rom.write(offset, prog.read(0x8000, MemorySpace::BANK_SIZES[:prog]))
end end
chars.each do |char| chars.each do |char|
offset += rom.write(offset, char.read(0x0, MemorySpace::BankSizes[:char])) offset += rom.write(offset, char.read(0x0, MemorySpace::BANK_SIZES[:char]))
end end
rom.emit_bytes.pack('C*') rom.emit_bytes.pack('C*')
end end
# TODO: Use StringIO to build output
####
## Display information about the bank sizes and total usage
def print_bank_usage def print_bank_usage
puts puts
puts "ROM Structure {" puts 'ROM Structure {'
puts " iNES 1.0 Header: $10 bytes" puts ' iNES 1.0 Header: $10 bytes'
@virtual_memory[:prog].each_with_index do |prog_rom, bank_number| @virtual_memory[:prog].each_with_index do |prog_rom, bank_number|
puts " PROG ROM bank #{bank_number}: #{prog_rom.usage_info}" puts " PROG ROM bank #{bank_number}: #{prog_rom.usage_info}"
@ -252,33 +225,21 @@ module N65
@virtual_memory[:char].each_with_index do |char_rom, bank_number| @virtual_memory[:char].each_with_index do |char_rom, bank_number|
puts " CHAR ROM bank #{bank_number}: #{char_rom.usage_info}" puts " CHAR ROM bank #{bank_number}: #{char_rom.usage_info}"
end end
puts "}" puts '}'
end end
private private
####
## Get virtual memory space
def get_virtual_memory_space(segment, bank_number) def get_virtual_memory_space(segment, bank_number)
@virtual_memory[segment][bank_number] @virtual_memory[segment][bank_number]
end end
####
## Is this a 16-bit address within range?
def address_within_range?(address) def address_within_range?(address)
address >= 0 && address < 2**16 address >= 0 && address < 2**16
end end
####
## Is this a valid segment?
def valid_segment?(segment) def valid_segment?(segment)
[:prog, :char].include?(segment) %i[prog char].include?(segment)
end end
end end
end end

View File

@ -1,42 +1,28 @@
# frozen_string_literal: true
require_relative '../instruction_base' require_relative '../instruction_base'
module N65 module N65
# This directive to include bytes
####
## This directive to include bytes
class ASCII < InstructionBase class ASCII < InstructionBase
####
## Try to parse a .ascii directive
def self.parse(line) def self.parse(line)
match_data = line.match(/^\.ascii\s+"([^"]+)"$/) match_data = line.match(/^\.ascii\s+"([^"]+)"$/)
return nil if match_data.nil? return nil if match_data.nil?
ASCII.new(match_data[1]) ASCII.new(match_data[1])
end end
####
## Initialize with a string
def initialize(string) def initialize(string)
super
@string = string @string = string
end end
####
## Execute on the assembler
def exec(assembler) def exec(assembler)
assembler.write_memory(@string.bytes) assembler.write_memory(@string.bytes)
end end
####
## Display
def to_s def to_s
".ascii \"#{@string}\"" ".ascii \"#{@string}\""
end end
end end
end end

View File

@ -1,27 +1,21 @@
# frozen_string_literal: true
require_relative '../instruction_base' require_relative '../instruction_base'
require_relative '../regexes.rb' require_relative '../regexes'
module N65 module N65
# This directive to include bytes
####
## This directive to include bytes
class Bytes < InstructionBase class Bytes < InstructionBase
#### Custom Exceptions
class InvalidByteValue < StandardError; end class InvalidByteValue < StandardError; end
# Try to parse an incbin directive
####
## Try to parse an incbin directive
def self.parse(line) def self.parse(line)
match_data = line.match(/^\.bytes\s+(.+)$/) match_data = line.match(/^\.bytes\s+(.+)$/)
return nil if match_data.nil? return nil if match_data.nil?
bytes_array = match_data[1].split(',').map do |byte_string| bytes_array = match_data[1].split(',').map do |byte_string|
# Does byte_string represent a numeric literal, or is it a symbol?
## Does byte_string represent a numeric literal, or is it a symbol? # In numeric captures $2 is always binary, $1 is always hex
## In numeric captures $2 is always binary, $1 is always hex
case byte_string.strip case byte_string.strip
when Regexp.new("^#{Regexes::Num8}$") when Regexp.new("^#{Regexes::Num8}$")
@ -30,32 +24,28 @@ module N65
when Regexp.new("^#{Regexes::Num16}$") when Regexp.new("^#{Regexes::Num16}$")
value = $2.nil? ? $1.to_i(16) : $2.to_i(2) value = $2.nil? ? $1.to_i(16) : $2.to_i(2)
## Break value up into two bytes # Break value up into two bytes
high = (0xff00 & value) >> 8 high = (0xff00 & value) >> 8
low = (0x00ff & value) low = (0x00ff & value)
[low, high] [low, high]
when Regexp.new("^#{Regexes::Sym}$") when Regexp.new("^#{Regexes::Sym}$")
$1 $1
else else
fail(InvalidByteValue, byte_string) raise(InvalidByteValue, byte_string)
end end
end.flatten end.flatten
Bytes.new(bytes_array) Bytes.new(bytes_array)
end end
# Initialize with a byte array
####
## Initialize with a byte array
def initialize(bytes_array) def initialize(bytes_array)
super
@bytes_array = bytes_array @bytes_array = bytes_array
end end
# Execute on the assembler
####
## Execute on the assembler
def exec(assembler) def exec(assembler)
promise = assembler.with_saved_state do |saved_assembler| promise = assembler.with_saved_state do |saved_assembler|
@bytes_array.map! do |byte| @bytes_array.map! do |byte|
case byte case byte
@ -64,7 +54,7 @@ module N65
when String when String
saved_assembler.symbol_table.resolve_symbol(byte) saved_assembler.symbol_table.resolve_symbol(byte)
else else
fail(InvalidByteValue, byte) raise(InvalidByteValue, byte)
end end
end end
saved_assembler.write_memory(@bytes_array) saved_assembler.write_memory(@bytes_array)
@ -73,8 +63,8 @@ module N65
begin begin
promise.call promise.call
rescue SymbolTable::UndefinedSymbol rescue SymbolTable::UndefinedSymbol
## Write the bytes but assume a zero page address for all symbols # Write the bytes but assume a zero page address for all symbols
## And just write 0xDE for a placeholder # And just write 0xDE for a placeholder
placeholder_bytes = @bytes_array.map do |byte| placeholder_bytes = @bytes_array.map do |byte|
case bytes case bytes
when Integer when Integer
@ -82,21 +72,17 @@ module N65
when String when String
0xDE 0xDE
else else
fail(InvalidByteValue, byte) raise(InvalidByteValue, byte)
end end
end end
assembler.write_memory(placeholder_bytes) assembler.write_memory(placeholder_bytes)
return promise promise
end end
end end
# Display, I don't want to write all these out
####
## Display, I don't want to write all these out
def to_s def to_s
".bytes (#{@bytes_array.length})" ".bytes (#{@bytes_array.length})"
end end
end end
end end

View File

@ -1,25 +1,21 @@
# frozen_string_literal: true
require_relative '../instruction_base' require_relative '../instruction_base'
module N65 module N65
# This directive instruction can include a binary file
####
## This directive instruction can include a binary file
class DW < InstructionBase class DW < InstructionBase
# Try to parse a dw directive
####
## Try to parse a dw directive
def self.parse(line) def self.parse(line)
# Maybe it is a straight up bit of hex
## Maybe it is a straight up bit of hex
match_data = line.match(/^\.dw\s+\$([0-9A-F]{1,4})$/) match_data = line.match(/^\.dw\s+\$([0-9A-F]{1,4})$/)
unless match_data.nil? unless match_data.nil?
word = match_data[1].to_i(16) word = match_data[1].to_i(16)
return DW.new(word) return DW.new(word)
end end
## Or maybe it points to a symbol # Or maybe it points to a symbol
match_data = line.match(/^\.dw\s+([A-Za-z_][A-Za-z0-9_\.]+)/) match_data = line.match(/^\.dw\s+([A-Za-z_][A-Za-z0-9_.]+)/)
unless match_data.nil? unless match_data.nil?
symbol = match_data[1] symbol = match_data[1]
return DW.new(symbol) return DW.new(symbol)
@ -27,30 +23,24 @@ module N65
nil nil
end end
# Initialize with filename
####
## Initialize with filename
def initialize(value) def initialize(value)
@value = value @value = value
end end
# Execute on the assembler, now in this case value may
#### # be a symbol that needs to be resolved, if so we return
## Execute on the assembler, now in this case value may # a lambda which can be executed later, with the promise
## be a symbol that needs to be resolved, if so we return # that that symbol will have then be defined
## a lambda which can be executed later, with the promise # This is a little complicated, I admit.
## that that symbol will have then be defined
## This is a little complicated, I admit.
def exec(assembler) def exec(assembler)
promise = assembler.with_saved_state do |saved_assembler| promise = assembler.with_saved_state do |saved_assembler|
value = saved_assembler.symbol_table.resolve_symbol(@value) value = saved_assembler.symbol_table.resolve_symbol(@value)
bytes = [value & 0xFFFF].pack('S').bytes bytes = [value & 0xFFFF].pack('S').bytes
saved_assembler.write_memory(bytes) saved_assembler.write_memory(bytes)
end end
# Try to execute it now, or setup the promise to return
## Try to execute it now, or setup the promise to return
case @value case @value
when Integer when Integer
bytes = [@value & 0xFFFF].pack('S').bytes bytes = [@value & 0xFFFF].pack('S').bytes
@ -59,28 +49,24 @@ module N65
begin begin
promise.call promise.call
rescue SymbolTable::UndefinedSymbol rescue SymbolTable::UndefinedSymbol
## Must still advance PC before returning promise, so we'll write # Must still advance PC before returning promise, so we'll write
## a place holder value of 0xDEAD # a place holder value of 0xDEAD
assembler.write_memory([0xDE, 0xAD]) assembler.write_memory([0xDE, 0xAD])
return promise promise
end end
else else
fail("Uknown argument in .dw directive") raise('Uknown argument in .dw directive')
end end
end end
# Display
####
## Display
def to_s def to_s
case @value case @value
when String when String
".dw #{@value}" ".dw #{@value}"
when Fixnum when Integer
".dw $%4.X" % @value '.dw $%4.X' % @value
end end
end end
end end
end end

View File

@ -1,55 +1,39 @@
# frozen_string_literal: true
require_relative '../instruction_base' require_relative '../instruction_base'
module N65 module N65
# This directive to include bytes
####
## This directive to include bytes
class EnterScope < InstructionBase class EnterScope < InstructionBase
####
## Try to parse an incbin directive
def self.parse(line) def self.parse(line)
## Anonymous scope # Anonymous scope
match_data = line.match(/^\.scope$/) match_data = line.match(/^\.scope$/)
unless match_data.nil? return EnterScope.new unless match_data.nil?
return EnterScope.new
end
## Named scope # Named scope
match_data = line.match(/^\.scope\s+([a-zA-Z][a-zA-Z0-9_]+)$/) match_data = line.match(/^\.scope\s+([a-zA-Z][a-zA-Z0-9_]+)$/)
return nil if match_data.nil? return nil if match_data.nil?
EnterScope.new(match_data[1]) EnterScope.new(match_data[1])
end end
# Initialize with filename
####
## Initialize with filename
def initialize(name = nil) def initialize(name = nil)
@name = name @name = name
end end
# Execute on the assembler, also create a symbol referring to
#### # the current pc which contains a hyphen, and is impossible for
## Execute on the assembler, also create a symbol referring to # the user to create. This makes a scope simultaneously act as
## the current pc which contains a hyphen, and is impossible for # a label to the current PC. If someone tries to use a scope
## the user to create. This makes a scope simultaneously act as # name as a label, it can return the address when the scope opened.
## a label to the current PC. If someone tries to use a scope
## name as a label, it can return the address when the scope opened.
def exec(assembler) def exec(assembler)
assembler.symbol_table.enter_scope(@name) assembler.symbol_table.enter_scope(@name)
unless @name.nil? assembler.symbol_table.define_symbol("-#{@name}", assembler.program_counter) unless @name.nil?
assembler.symbol_table.define_symbol("-#{@name}", assembler.program_counter)
end
end end
####
## Display
def to_s def to_s
".scope #{@name}" ".scope #{@name}"
end end
end end
end end

View File

@ -1,35 +1,25 @@
# frozen_string_literal: true
require_relative '../instruction_base' require_relative '../instruction_base'
module N65 module N65
# This directive to include bytes
####
## This directive to include bytes
class ExitScope < InstructionBase class ExitScope < InstructionBase
####
## Try to parse an incbin directive
def self.parse(line) def self.parse(line)
match_data = line.match(/^\.$/) match_data = line.match(/^\.$/)
return nil if match_data.nil? return nil if match_data.nil?
ExitScope.new ExitScope.new
end end
# Execute on the assembler
####
## Execute on the assembler
def exec(assembler) def exec(assembler)
assembler.symbol_table.exit_scope assembler.symbol_table.exit_scope
end end
# Display
####
## Display
def to_s def to_s
"." '.'
end end
end end
end end

View File

@ -1,67 +1,51 @@
# frozen_string_literal: true
require_relative '../instruction_base' require_relative '../instruction_base'
module N65 module N65
# This directive instruction can include another asm file
####
## This directive instruction can include another asm file
class Inc < InstructionBase class Inc < InstructionBase
SYSTEM_INCLUDE = "#{File.dirname(__FILE__)}/../../../nes_lib"
#### System include directory
SystemInclude = File.dirname(__FILE__) + "/../../../nes_lib"
#### Custom Exceptions
class FileNotFound < StandardError; end class FileNotFound < StandardError; end
# Try to parse an incbin directive
####
## Try to parse an incbin directive
def self.parse(line) def self.parse(line)
## Do We have a system directory include? # Do We have a system directory include?
match_data = line.match(/^\.inc <([^>]+)>$/) match_data = line.match(/^\.inc <([^>]+)>$/)
unless match_data.nil? unless match_data.nil?
filename = File.join(SystemInclude, match_data[1]) filename = File.join(SYSTEM_INCLUDE, match_data[1])
return Inc.new(filename) return Inc.new(filename)
end end
## Do We have a project relative directory include? # Do We have a project relative directory include?
match_data = line.match(/^\.inc "([^"]+)"$/) match_data = line.match(/^\.inc "([^"]+)"$/)
unless match_data.nil? unless match_data.nil?
filename = File.join(Dir.pwd, match_data[1]) filename = File.join(Dir.pwd, match_data[1])
return Inc.new(filename) return Inc.new(filename)
end end
## Nope, not an inc directive # Nope, not an inc directive
nil nil
end end
# Initialize with filename
####
## Initialize with filename
def initialize(filename) def initialize(filename)
@filename = filename @filename = filename
end end
# Execute on the assembler
####
## Execute on the assembler
def exec(assembler) def exec(assembler)
unless File.exists?(@filename) raise(FileNotFound, ".inc can't find #{@filename}") unless File.exist?(@filename)
fail(FileNotFound, ".inc can't find #{@filename}")
end
File.read(@filename).split(/\n/).each do |line| File.read(@filename).split(/\n/).each do |line|
assembler.assemble_one_line(line) assembler.assemble_one_line(line)
end end
end end
# Display
####
## Display
def to_s def to_s
".inc \"#{@filename}\"" ".inc \"#{@filename}\""
end end
end end
end end

View File

@ -1,51 +1,33 @@
# frozen_string_literal: true
require_relative '../instruction_base' require_relative '../instruction_base'
module N65 module N65
# This directive instruction can include a binary file
####
## This directive instruction can include a binary file
class IncBin < InstructionBase class IncBin < InstructionBase
#### Custom Exceptions
class FileNotFound < StandardError; end class FileNotFound < StandardError; end
####
## Try to parse an incbin directive
def self.parse(line) def self.parse(line)
match_data = line.match(/^\.incbin "([^"]+)"$/) match_data = line.match(/^\.incbin "([^"]+)"$/)
return nil if match_data.nil? return nil if match_data.nil?
filename = match_data[1] filename = match_data[1]
IncBin.new(filename) IncBin.new(filename)
end end
####
## Initialize with filename
def initialize(filename) def initialize(filename)
@filename = filename @filename = filename
end end
####
## Execute on the assembler
def exec(assembler) def exec(assembler)
unless File.exists?(@filename) raise(FileNotFound, ".incbin can't find #{@filename}") unless File.exist?(@filename)
fail(FileNotFound, ".incbin can't find #{@filename}")
end
data = File.read(@filename).unpack('C*') data = File.read(@filename).unpack('C*')
assembler.write_memory(data) assembler.write_memory(data)
end end
####
## Display
def to_s def to_s
".incbin \"#{@filename}\"" ".incbin \"#{@filename}\""
end end
end end
end end

View File

@ -1,28 +1,33 @@
# frozen_string_literal: true
require 'json' require 'json'
require_relative '../instruction_base' require_relative '../instruction_base'
module N65 module N65
####
## This directive instruction can setup an iNES header
class INESHeader < InstructionBase class INESHeader < InstructionBase
attr_reader :prog, :char, :mapper, :mirror, :battery_backed, :fourscreen_vram, :prog_ram, :tv attr_reader :prog, :char, :mapper, :mirror, :battery_backed, :fourscreen_vram, :prog_ram, :tv
Defaults = {prog: 1, char: 0, mapper: 0, mirror: 0, battery_backed: 0, fourscreen_vram: 0, prog_ram: 0, tv: 0} DEFAULTS = {
prog: 1,
char: 0,
mapper: 0,
mirror: 0,
battery_backed: 0,
fourscreen_vram: 0,
prog_ram: 0,
tv: 0
}.freeze
####
## Implementation of the parser for this directive
def self.parse(line) def self.parse(line)
match_data = line.match(/^\.ines (.+)$/) match_data = line.match(/^\.ines (.+)$/)
return nil if match_data.nil? return nil if match_data.nil?
header = JSON.parse(match_data[1]) header = JSON.parse(match_data[1])
header = header.inject({}) do |hash, (key, val)| header = header.each_with_object({}) do |(key, val), hash|
hash[key.to_sym] = val hash[key.to_sym] = val
hash
end end
header = Defaults.merge(header) header = DEFAULTS.merge(header)
INESHeader.new( INESHeader.new(
header[:prog], header[:prog],
@ -32,12 +37,11 @@ module N65
header[:battery_backed], header[:battery_backed],
header[:fourscreen_vram], header[:fourscreen_vram],
header[:prog_ram], header[:prog_ram],
header[:tv]) header[:tv]
)
end end
# Construct a header
####
## Construct a header
def initialize(prog, char, mapper, mirror, battery_backed, fourscreen_vram, prog_ram, tv) def initialize(prog, char, mapper, mirror, battery_backed, fourscreen_vram, prog_ram, tv)
@prog = prog @prog = prog
@char = char @char = char
@ -49,18 +53,13 @@ module N65
@tv = tv @tv = tv
end end
# Exec function the assembler will call
####
## Exec function the assembler will call
def exec(assembler) def exec(assembler)
assembler.set_ines_header(self) assembler.set_ines_header(self)
end end
# Emit the header bytes
####
## Emit the header bytes
def emit_bytes def emit_bytes
mapper_lo_nybble = (@mapper & 0x0f) mapper_lo_nybble = (@mapper & 0x0f)
mapper_hi_nybble = (@mapper & 0xf0) >> 4 mapper_hi_nybble = (@mapper & 0xf0) >> 4
@ -82,16 +81,12 @@ module N65
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0] 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]
end end
# Display
####
## Display
def to_s def to_s
[".ines {\"prog\": #{@prog}, \"char\": #{@char}, \"mapper\": #{@mapper}, ", [".ines {\"prog\": #{@prog}, \"char\": #{@char}, \"mapper\": #{@mapper}, ",
"\"mirror\": #{@mirror}}, \"battery_backed\": #{@battery_backed}, ", "\"mirror\": #{@mirror}}, \"battery_backed\": #{@battery_backed}, ",
"\"fourscreen_vram\": #{@fourscreen_vram}, \"prog_ram\": #{@prog_ram}, ", "\"fourscreen_vram\": #{@fourscreen_vram}, \"prog_ram\": #{@prog_ram}, ",
"\"tv\": #{@tv}"].join "\"tv\": #{@tv}"].join
end end
end end
end end

View File

@ -1,46 +1,33 @@
# frozen_string_literal: true
module N65 module N65
# This class represents a label, and will create
#### # an entry in the symbol table associated with
## This class represents a label, and will create # the address it appears at.
## an entry in the symbol table associated with
## the address it appears at.
class Label class Label
####
## Try to parse as a label
def self.parse(line) def self.parse(line)
match_data = line.match(/^([a-zA-Z][a-zA-Z0-9_]+):$/) match_data = line.match(/^([a-zA-Z][a-zA-Z0-9_]+):$/)
unless match_data.nil? unless match_data.nil?
label = match_data[1].to_sym label = match_data[1].to_sym
return self.new(label) return new(label)
end end
nil nil
end end
# Create a new label object
####
## Create a new label object
def initialize(symbol) def initialize(symbol)
@symbol = symbol @symbol = symbol
end end
# Create an entry in the symbol table for this label
####
## Create an entry in the symbol table for this label
def exec(assembler) def exec(assembler)
program_counter = assembler.program_counter program_counter = assembler.program_counter
assembler.symbol_table.define_symbol(@symbol, program_counter) assembler.symbol_table.define_symbol(@symbol, program_counter)
end end
# Display
####
## Display
def to_s def to_s
"#{@symbol}:" "#{@symbol}:"
end end
end end
end end

View File

@ -1,47 +1,37 @@
# frozen_string_literal: true
require_relative '../instruction_base' require_relative '../instruction_base'
module N65 module N65
# This is an .org directive
####
## This is an .org directive
class Org < InstructionBase class Org < InstructionBase
attr_reader :address attr_reader :address
####
## Try to parse an .org statement
def self.parse(line) def self.parse(line)
match_data = line.match(/^\.org\s+\$([0-9A-Fa-f]{4})$/) match_data = line.match(/^\.org\s+\$([0-9A-Fa-f]{4})$/)
return nil if match_data.nil? return nil if match_data.nil?
address = match_data[1].to_i(16) address = match_data[1].to_i(16)
Org.new(address) Org.new(address)
end end
# Initialized with address to switch to
####
## Initialized with address to switch to
def initialize(address) def initialize(address)
@address = address @address = address
end end
# Exec this directive on the assembler
####
## Exec this directive on the assembler
def exec(assembler) def exec(assembler)
assembler.program_counter = address assembler.program_counter = address
end end
# Display
####
## Display
def to_s def to_s
if @address <= 0xff if @address <= 0xff
".org $%2.X" % @address '.org $%2.X' % @address
else else
".org $%4.X" % @address '.org $%4.X' % @address
end end
end end
end end
end end

View File

@ -1,13 +1,8 @@
# frozen_string_literal: true
require_relative '../instruction_base' require_relative '../instruction_base'
module N65 module N65
####
## This directive instruction can include a binary file
class Segment < InstructionBase class Segment < InstructionBase
####
## Try to parse a dw directive
def self.parse(line) def self.parse(line)
match_data = line.match(/^.segment (prog|char) (\d+)$/i) match_data = line.match(/^.segment (prog|char) (\d+)$/i)
unless match_data.nil? unless match_data.nil?
@ -17,29 +12,20 @@ module N65
nil nil
end end
# Initialize with filename
####
## Initialize with filename
def initialize(segment, bank) def initialize(segment, bank)
@bank = bank @bank = bank
@segment = segment @segment = segment
end end
# Execute the segment and bank change on the assembler
####
## Execute the segment and bank change on the assembler
def exec(assembler) def exec(assembler)
assembler.current_segment = @segment assembler.current_segment = @segment
assembler.current_bank = @bank assembler.current_bank = @bank
end end
####
## Display
def to_s def to_s
".segment #{@segment} #{@bank}" ".segment #{@segment} #{@bank}"
end end
end end
end end

View File

@ -1,46 +1,34 @@
# frozen_string_literal: true
require_relative '../instruction_base' require_relative '../instruction_base'
module N65 module N65
####
## This directive gives a symbolic name for memory and creates space for a variable in RAM ## This directive gives a symbolic name for memory and creates space for a variable in RAM
class Space < InstructionBase class Space < InstructionBase
####
## Try to parse a .space directive
def self.parse(line) def self.parse(line)
match_data = line.match(/^.space\s+([a-zA-Z]?[a-zA-Z0-9_]+?)\s+([0-9]+)$/) match_data = line.match(/^.space\s+([a-zA-Z]?[a-zA-Z0-9_]+?)\s+([0-9]+)$/)
return nil if match_data.nil? return nil if match_data.nil?
_, name, size = match_data.to_a
_, name, size = match_data.to_a
Space.new(name, size.to_i) Space.new(name, size.to_i)
end end
# Initialize some memory space with a name
####
## Initialize some memory space with a name
def initialize(name, size) def initialize(name, size)
@name = name @name = name
@size = size @size = size
end end
# .space creates a symbol at the current PC, and then advances PC by size
####
## .space creates a symbol at the current PC, and then advances PC by size
def exec(assembler) def exec(assembler)
program_counter = assembler.program_counter program_counter = assembler.program_counter
assembler.symbol_table.define_symbol(@name, program_counter) assembler.symbol_table.define_symbol(@name, program_counter)
assembler.program_counter += @size assembler.program_counter += @size
end end
# Display
####
## Display
def to_s def to_s
".space #{@name} #{@size}" ".space #{@name} #{@size}"
end end
end end
end end

View File

@ -1,73 +1,63 @@
# frozen_string_literal: true
require 'optparse' require 'optparse'
require_relative '../n65' require_relative '../n65'
module N65 module N65
# This class handles the front end aspects,
#### # parsing the commandline options and running the assembler
## This class handles the front end aspects,
## parsing the commandline options and running the assembler
class FrontEnd class FrontEnd
####
## Initialize with ARGV commandline
def initialize(argv) def initialize(argv)
@options = {output_file: nil, write_symbol_table: false, quiet: false, cycle_count: false} @options = { output_file: nil, write_symbol_table: false, quiet: false, cycle_count: false }
@argv = argv.dup @argv = argv.dup
end end
# Run the assembler
####
## Run the assembler
def run def run
## First use the option parser
parser = create_option_parser parser = create_option_parser
parser.parse!(@argv) parser.parse!(@argv)
## Whatever is leftover in argv the input files
if @argv.size.zero? if @argv.size.zero?
STDERR.puts("No input files") warn('No input files')
exit(1) exit(1)
end end
## Only can assemble one file at once for now # Only can assemble one file at once for now
if @argv.size != 1 if @argv.size != 1
STDERR.puts "Can only assemble one input file at once, but you can use .inc and .incbin directives" warn('Can only assemble one input file at once, but you can use .inc and .incbin directives')
exit(1) exit(1)
end end
input_file = @argv.shift input_file = @argv.shift
## Make sure the input file exists # Make sure the input file exists
unless File.exists?(input_file) unless File.exist?(input_file)
STDERR.puts "Input file #{input_file} does not exist" warn("Input file #{input_file} does not exist")
exit(1) exit(1)
end end
## Maybe they didn't provide an output file name, so we'll guess # Maybe they didn't provide an output file name, so we'll guess
if @options[:output_file].nil? if @options[:output_file].nil?
ext = File.extname(input_file) ext = File.extname(input_file)
@options[:output_file] = input_file.gsub(ext, '') + '.nes' @options[:output_file] = "#{input_file.gsub(ext, '')}.nes"
end end
if @options.values.any?(&:nil?) if @options.values.any?(&:nil?)
STDERR.puts "Missing options try --help" warn('Missing options try --help')
exit(1) exit(1)
end end
N65::Assembler.from_file(input_file, @options) N65::Assembler.from_file(input_file, @options)
end end
private private
####
## Create a commandline option parser
def create_option_parser def create_option_parser
OptionParser.new do |opts| OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [options] <input_file.asm>" opts.banner = "Usage: #{$PROGRAM_NAME} [options] <input_file.asm>"
opts.on('-o', '--outfile filename', 'outfile') do |output_file| opts.on('-o', '--outfile filename', 'outfile') do |output_file|
@options[:output_file] = output_file; @options[:output_file] = output_file
end end
opts.on('-s', '--symbols', 'Outputs a symbol map') do opts.on('-s', '--symbols', 'Outputs a symbol map') do
@ -91,11 +81,7 @@ module N65
puts opts puts opts
exit exit
end end
end end
end end
end end
end end

View File

@ -1,153 +1,150 @@
# frozen_string_literal: true
require_relative 'opcodes' require_relative 'opcodes'
require_relative 'regexes' require_relative 'regexes'
module N65 module N65
# Represents a single 6502 Instruction
####
## Represents a single 6502 Instruction
class Instruction class Instruction
attr_reader :op, :arg, :mode, :hex, :description, :length, :cycles, :boundry_add, :flags, :address attr_reader :op, :arg, :mode, :hex, :description, :length, :cycles, :boundry_add, :flags, :address
## Custom Exceptions # Custom Exceptions
class InvalidInstruction < StandardError; end class InvalidInstruction < StandardError; end
class UnresolvedSymbols < StandardError; end class UnresolvedSymbols < StandardError; end
class InvalidAddressingMode < StandardError; end class InvalidAddressingMode < StandardError; end
class AddressOutOfRange < StandardError; end class AddressOutOfRange < StandardError; end
class ArgumentTooLarge < StandardError; end class ArgumentTooLarge < StandardError; end
## Include Regexes # Include Regexes
include Regexes include Regexes
AddressingModes = { ADDRESSING_MODES = {
:relative => { relative: {
:example => 'B** my_label', example: 'B** my_label',
:display => '%s $%.4X', display: '%s $%.4X',
:regex => /$^/i, # Will never match this one regex: /$^/i,
:regex_label => /^#{Branches}\s+#{Sym}$/ regex_label: /^#{Branches}\s+#{Sym}$/
}, },
:immediate => { immediate: {
:example => 'AAA #$FF', example: 'AAA #$FF',
:display => '%s #$%.2X', display: '%s #$%.2X',
:regex => /^#{Mnemonic}\s+#{Immediate}$/, regex: /^#{Mnemonic}\s+#{Immediate}$/,
:regex_label => /^#{Mnemonic}\s+#(<|>)#{Sym}$/ regex_label: /^#{Mnemonic}\s+#(<|>)#{Sym}$/
}, },
:implied => { implied: {
:example => 'AAA', example: 'AAA',
:display => '%s', display: '%s',
:regex => /^#{Mnemonic}$/ regex: /^#{Mnemonic}$/
}, },
:zero_page => { zero_page: {
:example => 'AAA $FF', example: 'AAA $FF',
:display => '%s $%.2X', display: '%s $%.2X',
:regex => /^#{Mnemonic}\s+#{Num8}$/, regex: /^#{Mnemonic}\s+#{Num8}$/,
:regex_label => /^#{Mnemonic}\s+#{Sym}\s+zp$/ regex_label: /^#{Mnemonic}\s+#{Sym}\s+zp$/
}, },
:zero_page_x => { zero_page_x: {
:example => 'AAA $FF, X', example: 'AAA $FF, X',
:display => '%s $%.2X, X', display: '%s $%.2X, X',
:regex => /^#{Mnemonic}\s+#{Num8}\s?,\s?#{XReg}$/, regex: /^#{Mnemonic}\s+#{Num8}\s?,\s?#{XReg}$/,
:regex_label => /^#{Mnemonic}\s+#{Sym}\s?,\s?#{XReg}\s+zp$/ regex_label: /^#{Mnemonic}\s+#{Sym}\s?,\s?#{XReg}\s+zp$/
}, },
:zero_page_y => { zero_page_y: {
:example => 'AAA $FF, Y', example: 'AAA $FF, Y',
:display => '%s $%.2X, Y', display: '%s $%.2X, Y',
:regex => /^#{Mnemonic}\s+#{Num8}\s?,\s?#{YReg}$/, regex: /^#{Mnemonic}\s+#{Num8}\s?,\s?#{YReg}$/,
:regex_label => /^#{Mnemonic}\s+#{Sym}\s?,\s?#{YReg}\s+zp$/ regex_label: /^#{Mnemonic}\s+#{Sym}\s?,\s?#{YReg}\s+zp$/
}, },
:absolute => { absolute: {
:example => 'AAA $FFFF', example: 'AAA $FFFF',
:display => '%s $%.4X', display: '%s $%.4X',
:regex => /^#{Mnemonic}\s+#{Num16}$/, regex: /^#{Mnemonic}\s+#{Num16}$/,
:regex_label => /^#{Mnemonic}\s+#{Sym}$/ regex_label: /^#{Mnemonic}\s+#{Sym}$/
}, },
:absolute_x => { absolute_x: {
:example => 'AAA $FFFF, X', example: 'AAA $FFFF, X',
:display => '%s $%.4X, X', display: '%s $%.4X, X',
:regex => /^#{Mnemonic}\s+#{Num16}\s?,\s?#{XReg}$/, regex: /^#{Mnemonic}\s+#{Num16}\s?,\s?#{XReg}$/,
:regex_label => /^#{Mnemonic}\s+#{Sym}\s?,\s?#{XReg}$/ regex_label: /^#{Mnemonic}\s+#{Sym}\s?,\s?#{XReg}$/
}, },
:absolute_y => { absolute_y: {
:example => 'AAA $FFFF, Y', example: 'AAA $FFFF, Y',
:display => '%s $%.4X, Y', display: '%s $%.4X, Y',
:regex => /^#{Mnemonic}\s+#{Num16}\s?,\s?#{YReg}$/, regex: /^#{Mnemonic}\s+#{Num16}\s?,\s?#{YReg}$/,
:regex_label => /^#{Mnemonic}\s+#{Sym}\s?,\s?#{YReg}$/ regex_label: /^#{Mnemonic}\s+#{Sym}\s?,\s?#{YReg}$/
}, },
:indirect => { indirect: {
:example => 'AAA ($FFFF)', example: 'AAA ($FFFF)',
:display => '%s ($%.4X)', display: '%s ($%.4X)',
:regex => /^#{Mnemonic}\s+\(#{Num16}\)$/, regex: /^#{Mnemonic}\s+\(#{Num16}\)$/,
:regex_label => /^#{Mnemonic}\s+\(#{Sym}\)$/ regex_label: /^#{Mnemonic}\s+\(#{Sym}\)$/
}, },
:indirect_x => { indirect_x: {
:example => 'AAA ($FF, X)', example: 'AAA ($FF, X)',
:display => '%s ($%.2X, X)', display: '%s ($%.2X, X)',
:regex => /^#{Mnemonic}\s+\(#{Num8}\s?,\s?#{XReg}\)$/, regex: /^#{Mnemonic}\s+\(#{Num8}\s?,\s?#{XReg}\)$/,
:regex_label => /^#{Mnemonic}\s+\(#{Sym}\s?,\s?#{XReg}\)$/ regex_label: /^#{Mnemonic}\s+\(#{Sym}\s?,\s?#{XReg}\)$/
}, },
:indirect_y => { indirect_y: {
:example => 'AAA ($FF), Y)', example: 'AAA ($FF), Y)',
:display => '%s ($%.2X), Y', display: '%s ($%.2X), Y',
:regex => /^#{Mnemonic}\s+\(#{Num8}\)\s?,\s?#{YReg}$/, regex: /^#{Mnemonic}\s+\(#{Num8}\)\s?,\s?#{YReg}$/,
:regex_label => /^#{Mnemonic}\s+\(#{Sym}\)\s?,\s?#{YReg}$/ regex_label: /^#{Mnemonic}\s+\(#{Sym}\)\s?,\s?#{YReg}$/
} }
} }.freeze
#### # Parse one line of assembly, returns nil if the line
## Parse one line of assembly, returns nil if the line # is ultimately empty of asm instructions
## is ultimately empty of asm instructions # Raises SyntaxError if the line is malformed in some way
## Raises SyntaxError if the line is malformed in some way
def self.parse(line) def self.parse(line)
# Try to parse this line in each addressing mode
## Try to parse this line in each addressing mode ADDRESSING_MODES.each do |mode, parse_info|
AddressingModes.each do |mode, parse_info| # We have regexes that match each addressing mode
## We have regexes that match each addressing mode
match_data = parse_info[:regex].match(line) match_data = parse_info[:regex].match(line)
unless match_data.nil? unless match_data.nil?
## We must have a straight instruction without symbols, construct # We must have a straight instruction without symbols, construct
## an Instruction from the match_data, and return it # an Instruction from the match_data, and return it
_, op, arg_hex, arg_bin = match_data.to_a _, op, arg_hex, arg_bin = match_data.to_a
## Until I think of something better, it seems that the union regex # Until I think of something better, it seems that the union regex
## puts a hexidecimal argument in one capture, and a binary in the next # puts a hexidecimal argument in one capture, and a binary in the next
## This is annoying, but still not as annoying as using Treetop to parse # This is annoying, but still not as annoying as using Treetop to parse
if arg_hex != nil if !arg_hex.nil?
return Instruction.new(op, arg_hex.to_i(16), mode) return Instruction.new(op, arg_hex.to_i(16), mode)
elsif arg_bin != nil elsif !arg_bin.nil?
return Instruction.new(op, arg_bin.to_i(2), mode) return Instruction.new(op, arg_bin.to_i(2), mode)
else else
return Instruction.new(op, nil, mode) return Instruction.new(op, nil, mode)
end end
else else
## Can this addressing mode even use labels? # Can this addressing mode even use labels?
unless parse_info[:regex_label].nil? unless parse_info[:regex_label].nil?
## See if it does in fact have a symbolic argument # See if it does in fact have a symbolic argument
match_data = parse_info[:regex_label].match(line) match_data = parse_info[:regex_label].match(line)
unless match_data.nil? unless match_data.nil?
## We have found an assembly instruction containing a symbolic # We have found an assembly instruction containing a symbolic
## argument. We can resolve this symbol later by looking at the # argument. We can resolve this symbol later by looking at the
## symbol table in the #exec method # symbol table in the #exec method
match_array = match_data.to_a match_array = match_data.to_a
## If we have a 4 element array, this means we matched something # If we have a 4 element array, this means we matched something
## like LDA #<label, which is a legal immediate one byte value # like LDA #<label, which is a legal immediate one byte value
## by taking the msb. We need to make that distinction in the # by taking the msb. We need to make that distinction in the
## Instruction, by passing an extra argument # Instruction, by passing an extra argument
if match_array.size == 4 if match_array.size == 4
_, op, byte_selector, arg = match_array _, op, byte_selector, arg = match_array
return Instruction.new(op, arg, mode, byte_selector.to_sym) return Instruction.new(op, arg, mode, byte_selector.to_sym)
@ -160,60 +157,49 @@ module N65
end end
end end
## We just don't recognize this line of asm, it must be a Syntax Error # We just don't recognize this line of asm, it must be a Syntax Error
fail(SyntaxError, line) raise(SyntaxError, line)
end end
# Create an instruction. Having the instruction op a downcased symbol is nice
#### # because that can later be used to index into our opcodes hash in OpCodes
## Create an instruction. Having the instruction op a downcased symbol is nice # OpCodes contains the definitions of each OpCode
## because that can later be used to index into our opcodes hash in OpCodes
## OpCodes contains the definitions of each OpCode
def initialize(op, arg, mode, byte_selector = nil) def initialize(op, arg, mode, byte_selector = nil)
@byte_selector = byte_selector.nil? ? nil : byte_selector.to_sym @byte_selector = byte_selector.nil? ? nil : byte_selector.to_sym
fail(InvalidInstruction, "Bad Byte selector: #{byte_selector}") unless [:>, :<, nil].include?(@byte_selector) raise(InvalidInstruction, "Bad Byte selector: #{byte_selector}") unless [:>, :<, nil].include?(@byte_selector)
## Lookup the definition of this opcode, otherwise it is an invalid instruction ## Lookup the definition of this opcode, otherwise it is an invalid instruction
@op = op.downcase.to_sym @op = op.downcase.to_sym
definition = OpCodes[@op] definition = OpCodes[@op]
fail(InvalidInstruction, op) if definition.nil? raise(InvalidInstruction, op) if definition.nil?
@arg = arg @arg = arg
## Be sure the mode is an actually supported mode. # Be sure the mode is an actually supported mode.
@mode = mode.to_sym @mode = mode.to_sym
fail(InvalidAddressingMode, mode) unless AddressingModes.has_key?(@mode) raise(InvalidAddressingMode, mode) unless ADDRESSING_MODES.key?(@mode)
raise(InvalidInstruction, "#{op} cannot be used in #{mode} mode") if definition[@mode].nil?
if definition[@mode].nil?
fail(InvalidInstruction, "#{op} cannot be used in #{mode} mode")
end
@description, @flags = definition.values_at(:description, :flags) @description, @flags = definition.values_at(:description, :flags)
@hex, @length, @cycles, @boundry_add = definition[@mode].values_at(:hex, :len, :cycles, :boundry_add) @hex, @length, @cycles, @boundry_add = definition[@mode].values_at(:hex, :len, :cycles, :boundry_add)
end end
# Is this instruction a zero page instruction?
####
## Is this instruction a zero page instruction?
def zero_page_instruction? def zero_page_instruction?
[:zero_page, :zero_page_x, :zero_page_y].include?(@mode) %i[zero_page zero_page_x zero_page_y].include?(@mode)
end end
# Execute writes the emitted bytes to virtual memory, and updates PC
#### # If there is a symbolic argument, we can try to resolve it now, or
## Execute writes the emitted bytes to virtual memory, and updates PC # promise to resolve it later.
## If there is a symbolic argument, we can try to resolve it now, or
## promise to resolve it later.
def exec(assembler) def exec(assembler)
promise = assembler.with_saved_state do |saved_assembler| promise = assembler.with_saved_state do |saved_assembler|
@arg = saved_assembler.symbol_table.resolve_symbol(@arg) @arg = saved_assembler.symbol_table.resolve_symbol(@arg)
## If the instruction uses a byte selector, we need to apply that. # If the instruction uses a byte selector, we need to apply that.
@arg = apply_byte_selector(@byte_selector, @arg) @arg = apply_byte_selector(@byte_selector, @arg)
## If the instruction is relative we need to work out how far away it is # If the instruction is relative we need to work out how far away it is
@arg = @arg - saved_assembler.program_counter - 2 if @mode == :relative @arg = @arg - saved_assembler.program_counter - 2 if @mode == :relative
saved_assembler.write_memory(emit_bytes) saved_assembler.write_memory(emit_bytes)
@ -224,23 +210,22 @@ module N65
assembler.write_memory(emit_bytes) assembler.write_memory(emit_bytes)
when String when String
begin begin
## This works correctly now :) # This works correctly now :)
promise.call promise.call
rescue SymbolTable::UndefinedSymbol rescue SymbolTable::UndefinedSymbol
placeholder = [@hex, 0xDE, 0xAD][0...@length] placeholder = [@hex, 0xDE, 0xAD][0...@length]
## I still have to write a placeholder instruction of the right # I still have to write a placeholder instruction of the right
## length. The promise will come back and resolve the address. # length. The promise will come back and resolve the address.
assembler.write_memory(placeholder) assembler.write_memory(placeholder)
return promise promise
end end
end end
end end
# Apply a byte selector to an argument
####
## Apply a byte selector to an argument
def apply_byte_selector(byte_selector, value) def apply_byte_selector(byte_selector, value)
return value if byte_selector.nil? return value if byte_selector.nil?
case byte_selector case byte_selector
when :> when :>
high_byte(value) high_byte(value)
@ -249,47 +234,39 @@ module N65
end end
end end
# Emit bytes from asm structure
####
## Emit bytes from asm structure
def emit_bytes def emit_bytes
case @length case @length
when 1 when 1
[@hex] [@hex]
when 2 when 2
if zero_page_instruction? && @arg < 0 || @arg > 0xff if zero_page_instruction? && @arg.negative? || @arg > 0xff
fail(ArgumentTooLarge, "For #{@op} in #{@mode} mode, only 8-bit values are allowed") raise(ArgumentTooLarge, "For #{@op} in #{@mode} mode, only 8-bit values are allowed")
end end
[@hex, @arg] [@hex, @arg]
when 3 when 3
[@hex] + break_16(@arg) [@hex] + break_16(@arg)
else else
fail("Can't handle instructions > 3 bytes") raise("Can't handle instructions > 3 bytes")
end end
end end
private private
####
## Break an integer into two 8-bit parts # Break an integer into two 8-bit parts
def break_16(integer) def break_16(integer)
[integer & 0x00FF, (integer & 0xFF00) >> 8] [integer & 0x00FF, (integer & 0xFF00) >> 8]
end end
# Take the high byte of a 16-bit integer
####
## Take the high byte of a 16-bit integer
def high_byte(word) def high_byte(word)
(word & 0xFF00) >> 8 (word & 0xFF00) >> 8
end end
# Take the low byte of a 16-bit integer
####
## Take the low byte of a 16-bit integer
def low_byte(word) def low_byte(word)
word & 0xFF word & 0xFF
end end
end end
end end

View File

@ -1,29 +1,17 @@
# frozen_string_literal: true
module N65 module N65
class InstructionBase class InstructionBase
def self.parse(_line)
raise(NotImplementedError, "#{self.class.name} must implement self.parse")
#####
## Sort of a "pure virtual" class method, not really tho.
def self.parse(line)
fail(NotImplementedError, "#{self.class.name} must implement self.parse")
end end
####
## Does this instruction have unresolved symbols?
def unresolved_symbols? def unresolved_symbols?
false false
end end
def exec(_assembler)
#### raise(NotImplementedError, "#{self.class.name} must implement exec")
## Another method subclasses will be expected to implement
def exec(assembler)
fail(NotImplementedError, "#{self.class.name} must implement exec")
end end
end end
end end

View File

@ -1,59 +1,42 @@
# frozen_string_literal: true
module N65 module N65
# Let's use this to simulate a virtual address space
#### # Either a 16kb prog rom or 8kb char rom space.
## Let's use this to simulate a virtual address space # It can also be used to create arbitrary sized spaces
## Either a 16kb prog rom or 8kb char rom space. # for example to build the final binary ROM in.
## It can also be used to create arbitrary sized spaces
## for example to build the final binary ROM in.
class MemorySpace class MemorySpace
#### Custom exceptions
class AccessOutsideProgRom < StandardError; end class AccessOutsideProgRom < StandardError; end
class AccessOutsideCharRom < StandardError; end class AccessOutsideCharRom < StandardError; end
class AccessOutOfBounds < StandardError; end class AccessOutOfBounds < StandardError; end
# Some constants, the size of PROG and CHAR ROM
BANK_SIZES = {
ines: 0x10,
prog: 0x4000,
char: 0x2000
}.freeze
#### Some constants, the size of PROG and CHAR ROM
BankSizes = {
:ines => 0x10, # 16b
:prog => 0x4000, # 16kb
:char => 0x2000, # 8kb
}
####
## Create a new PROG ROM
def self.create_prog_rom def self.create_prog_rom
self.create_bank(:prog) create_bank(:prog)
end end
####
## Create a new CHAR ROM
def self.create_char_rom def self.create_char_rom
self.create_bank(:char) create_bank(:char)
end end
####
## Create a new bank
def self.create_bank(type) def self.create_bank(type)
self.new(BankSizes[type], type) new(BANK_SIZES[type], type)
end end
# Create a completely zeroed memory space
####
## Create a completely zeroed memory space
def initialize(size, type) def initialize(size, type)
@type = type @type = type
@memory = Array.new(size, 0x0) @memory = Array.new(size, 0x0)
@bytes_written = 0 @bytes_written = 0
end end
# Normalized read from memory
####
## Normalized read from memory
def read(address, count) def read(address, count)
from_normalized = normalize_address(address) from_normalized = normalize_address(address)
to_normalized = normalize_address(address + (count - 1)) to_normalized = normalize_address(address + (count - 1))
@ -62,9 +45,7 @@ module N65
@memory[from_normalized..to_normalized] @memory[from_normalized..to_normalized]
end end
# Normalized write to memory
####
## Normalized write to memory
def write(address, bytes) def write(address, bytes)
from_normalized = normalize_address(address) from_normalized = normalize_address(address)
to_normalized = normalize_address(address + bytes.size - 1) to_normalized = normalize_address(address + bytes.size - 1)
@ -77,87 +58,65 @@ module N65
bytes.size bytes.size
end end
# Return the memory as an array of bytes to write to disk
####
## Return the memory as an array of bytes to write to disk
def emit_bytes def emit_bytes
@memory @memory
end end
# Bank Usage information
####
## Bank Usage information
def usage_info def usage_info
percent_used = @bytes_written / @memory.size.to_f * 100 percent_used = @bytes_written / @memory.size.to_f * 100
percent_string = "%0.2f" % percent_used percent_string = format('%0.2f', percent_used)
bytes_written_hex = "$%04x" % @bytes_written bytes_written_hex = format('$%04x', @bytes_written)
memory_size_hex = "$%04x" % @memory.size memory_size_hex = format('$%04x', @memory.size)
"(#{bytes_written_hex} / #{memory_size_hex}) #{percent_string}%" "(#{bytes_written_hex} / #{memory_size_hex}) #{percent_string}%"
end end
private private
#### # Are the given addresses in bounds? If not blow up.
## Are the given addresses in bounds? If not blow up.
def ensure_addresses_in_bounds!(addresses) def ensure_addresses_in_bounds!(addresses)
addresses.each do |address| addresses.each do |address|
unless address >= 0 && address < @memory.size unless address >= 0 && address < @memory.size
fail(AccessOutOfBounds, sprintf("Address $%.4X is out of bounds in this #{@type} bank")) raise(AccessOutOfBounds, format("Address $%.4X is out of bounds in this #{@type} bank"))
end end
end end
true true
end end
# Since prog rom can be loaded at either 0x8000 or 0xC000
#### # We should normalize the addresses to fit properly into
## Since prog rom can be loaded at either 0x8000 or 0xC000 # these banks, basically it acts like it is mirroring addresses
## We should normalize the addresses to fit properly into # in those segments. Char rom doesn't need this. This will also
## these banks, basically it acts like it is mirroring addresses # fail if you are accessing outside of the address space.
## in those segments. Char rom doesn't need this. This will also
## fail if you are accessing outside of the address space.
def normalize_address(address) def normalize_address(address)
case @type case @type
when :prog when :prog
if address_inside_prog_rom1?(address) return (address - 0x8000) if address_inside_prog_rom1?(address)
return address - 0x8000 return (address - 0xC000) if address_inside_prog_rom2?(address)
end
if address_inside_prog_rom2?(address) raise(AccessOutsideProgRom, format('Address $%.4X is outside PROG ROM', address))
return address - 0xC000
end
fail(AccessOutsideProgRom, sprintf("Address $%.4X is outside PROG ROM", address))
when :char when :char
unless address_inside_char_rom?(address) unless address_inside_char_rom?(address)
fail(AccessOutsideCharRom, sprintf("Address $%.4X is outside CHAR ROM", address)) raise(AccessOutsideCharRom, format('Address $%.4X is outside CHAR ROM', address))
end end
return address
address
else else
return address address
end end
end end
####
## Is this address inside the prog rom 1 area?
def address_inside_prog_rom1?(address) def address_inside_prog_rom1?(address)
address >= 0x8000 && address < 0xC000 address >= 0x8000 && address < 0xC000
end end
####
## Is this address inside the prog rom 2 area?
def address_inside_prog_rom2?(address) def address_inside_prog_rom2?(address)
address >= 0xC000 && address <= 0xffff address >= 0xC000 && address <= 0xffff
end end
####
## Is this address inside the char rom area?
def address_inside_char_rom?(address) def address_inside_char_rom?(address)
address >= 0x0000 && address <= 0x1fff address >= 0x0000 && address <= 0x1fff
end end
end end
end end

View File

@ -1,9 +1,7 @@
# frozen_string_literal: true
require 'yaml' require 'yaml'
module N65 module N65
OpCodes = YAML.load_file(File.join(__dir__, '../../data/opcodes.yaml'))
## Load OpCode definitions into this module
MyDirectory = File.expand_path(File.dirname(__FILE__))
OpCodes = YAML.load_file("#{MyDirectory}/../../data/opcodes.yaml")
end end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module N65 module N65
require_relative 'instruction' require_relative 'instruction'
require_relative 'directives/ines_header' require_relative 'directives/ines_header'
require_relative 'directives/org' require_relative 'directives/org'
@ -15,71 +15,53 @@ module N65
require_relative 'directives/exit_scope' require_relative 'directives/exit_scope'
require_relative 'directives/space' require_relative 'directives/space'
# This class determines what sort of line of code we
# are dealing with, parses one line, and returns an
#### # object deriving from InstructionBase
## This class determines what sort of line of code we
## are dealing with, parses one line, and returns an
## object deriving from InstructionBase
class Parser class Parser
#### Custom Exceptions
class CannotParse < StandardError; end class CannotParse < StandardError; end
DIRECTIVES = [INESHeader, Org, Segment, IncBin, Inc, DW, Bytes, ASCII, EnterScope, ExitScope, Space].freeze
Directives = [INESHeader, Org, Segment, IncBin, Inc, DW, Bytes, ASCII, EnterScope, ExitScope, Space] # Parses a line of program source into an object
# deriving from base class InstructionBase
####
## Parses a line of program source into an object
## deriving from base class InstructionBase
def self.parse(line) def self.parse(line)
sanitized = sanitize_line(line) sanitized = sanitize_line(line)
return nil if sanitized.empty? return nil if sanitized.empty?
## First check to see if we have a label. # First check to see if we have a label.
label = Label.parse(sanitized) label = Label.parse(sanitized)
unless label.nil? return label unless label.nil?
return label
end
## Now check if we have a directive # Now check if we have a directive
directive = parse_directive(sanitized) directive = parse_directive(sanitized)
unless directive.nil? return directive unless directive.nil?
return directive
end
## Now, surely it is an asm instruction? # Now, surely it is an asm instruction?
instruction = Instruction.parse(sanitized) instruction = Instruction.parse(sanitized)
unless instruction.nil? return instruction unless instruction.nil?
return instruction
end
## Guess not, we have no idea # Guess not, we have no idea
fail(CannotParse, sanitized) raise(CannotParse, sanitized)
end end
# Sanitize one line of program source
private
####
## Sanitize one line of program source
def self.sanitize_line(line) def self.sanitize_line(line)
code = line.split(';').first || "" code = line.split(';').first || ''
code.strip.chomp code.strip.chomp
end end
private_class_method :sanitize_line
####
## Try to Parse a directive ## Try to Parse a directive
def self.parse_directive(line) def self.parse_directive(line)
if line.start_with?('.') if line.start_with?('.')
Directives.each do |directive| DIRECTIVES.each do |directive|
object = directive.parse(line) object = directive.parse(line)
return object unless object.nil? return object unless object.nil?
end end
end end
nil nil
end end
private_class_method :parse_directive
end end
end end

View File

@ -1,15 +1,14 @@
# frozen_string_literal: true
module N65 module N65
# All the regexes used to parse in one module
####
## All the regexes used to parse in one module
module Regexes module Regexes
## Mnemonics # rubocop:disable Naming/ConstantName
# Mnemonics
Mnemonic = '([A-Za-z]{3})' Mnemonic = '([A-Za-z]{3})'
Branches = '(BPL|BMI|BVC|BVS|BCC|BCS|BNE|BEQ|bpl|bmi|bvc|bvs|bcc|bcs|bne|beq)' Branches = '(BPL|BMI|BVC|BVS|BCC|BCS|BNE|BEQ|bpl|bmi|bvc|bvs|bcc|bcs|bne|beq)'
## Numeric Literals # Numeric Literals
Hex8 = '\$([A-Fa-f0-9]{1,2})' Hex8 = '\$([A-Fa-f0-9]{1,2})'
Hex16 = '\$([A-Fa-f0-9]{3,4})' Hex16 = '\$([A-Fa-f0-9]{3,4})'
@ -17,17 +16,17 @@ module N65
Bin16 = '%([01]{9,16})' Bin16 = '%([01]{9,16})'
Num8 = Regexp.union(Regexp.new(Hex8), Regexp.new(Bin8)).to_s Num8 = Regexp.union(Regexp.new(Hex8), Regexp.new(Bin8)).to_s
Num16 = Regexp.union(Regexp.new(Hex16),Regexp.new(Bin16)).to_s Num16 = Regexp.union(Regexp.new(Hex16), Regexp.new(Bin16)).to_s
Immediate = "\##{Num8}" Immediate = "\##{Num8}"
## Symbols, must begin with a letter, and supports dot syntax # Symbols, must begin with a letter, and supports dot syntax
Sym = '([a-zA-Z][a-zA-Z\d_\.]*(?:[\+\-\*\/]\d+)*)' Sym = '([a-zA-Z][a-zA-Z\d_\.]*(?:[\+\-\*\/]\d+)*)'
# The X or Y register
## The X or Y register
XReg = '[Xx]' XReg = '[Xx]'
YReg = '[Yy]' YReg = '[Yy]'
end
# rubocop:enable Naming/ConstantName
end
end end

View File

@ -1,128 +1,108 @@
# frozen_string_literal: true
module N65 module N65
class SymbolTable class SymbolTable
attr_accessor :scope_stack attr_accessor :scope_stack
##### Custom Exceptions # Custom Exceptions
class InvalidScope < StandardError; end class InvalidScope < StandardError; end
class UndefinedSymbol < StandardError; end class UndefinedSymbol < StandardError; end
class CantExitScope < StandardError; end class CantExitScope < StandardError; end
# Initialize a symbol table that begins in global scope
####
## Initialize a symbol table that begins in global scope
def initialize def initialize
@symbols = { @symbols = {
:global => {} global: {}
} }
@anonymous_scope_number = 0 @anonymous_scope_number = 0
@scope_stack = [:global] @scope_stack = [:global]
@subroutine_cycles = {} @subroutine_cycles = {}
end end
# Add a running cycle count to current top level scopes (ie subroutines)
####
## Add a running cycle count to current top level scopes (ie subroutines)
def add_cycles(cycles) def add_cycles(cycles)
cycles ||= 0 cycles ||= 0
top_level_subroutine = @scope_stack[1] top_level_subroutine = @scope_stack[1]
unless top_level_subroutine.nil? return if top_level_subroutine.nil?
@subroutine_cycles[top_level_subroutine] ||= 0
@subroutine_cycles[top_level_subroutine] += cycles @subroutine_cycles[top_level_subroutine] ||= 0
end @subroutine_cycles[top_level_subroutine] += cycles
end end
# Define a new scope, which can be anonymous or named
#### # and switch into that scope
## Define a new scope, which can be anonymous or named
## and switch into that scope
def enter_scope(name = nil) def enter_scope(name = nil)
name = generate_name if name.nil? name = generate_name if name.nil?
name = name.to_sym name = name.to_sym
scope = current_scope scope = current_scope
if scope.has_key?(name) raise(InvalidScope, "Scope: #{name} already exists") if scope.key?(name)
fail(InvalidScope, "Scope: #{name} already exists")
end
scope[name] = {} scope[name] = {}
@scope_stack.push(name) @scope_stack.push(name)
end end
# Exit the current scope
####
## Exit the current scope
def exit_scope def exit_scope
if @scope_stack.size == 1 raise(CantExitScope, 'You cannot exit global scope') if @scope_stack.size == 1
fail(CantExitScope, "You cannot exit global scope")
end
@scope_stack.pop @scope_stack.pop
end end
# Define a symbol in the current scope
####
## Define a symbol in the current scope
def define_symbol(symbol, value) def define_symbol(symbol, value)
scope = current_scope scope = current_scope
scope[symbol.to_sym] = value scope[symbol.to_sym] = value
end end
# Separate arithmetic from scope name
####
## Separate arithmetic from scope name
def find_arithmetic(name) def find_arithmetic(name)
last_name = name.split('.').last last_name = name.split('.').last
md = last_name.match(/([\+\-\*\/])(\d+)$/) md = last_name.match(%r{([+\-*/])(\d+)$})
f = lambda{|v| v} f = ->(v) { v }
unless md.nil? unless md.nil?
full_match, operator, argument = md.to_a full_match, operator, argument = md.to_a
name.gsub!(full_match, '') name.gsub!(full_match, '')
f = lambda {|value| value.send(operator.to_sym, argument.to_i) } f = ->(value) { value.send(operator.to_sym, argument.to_i) }
end end
[name, f] [name, f]
end end
# Resolve a symbol to its value
####
## Resolve a symbol to its value
def resolve_symbol(name) def resolve_symbol(name)
name, arithmetic = find_arithmetic(name) name, arithmetic = find_arithmetic(name)
method = name.include?('.') ? :resolve_symbol_dot_syntax : :resolve_symbol_scoped method = name.include?('.') ? :resolve_symbol_dot_syntax : :resolve_symbol_scoped
value = self.send(method, name) value = send(method, name)
value = arithmetic.call(value) value = arithmetic.call(value)
raise(UndefinedSymbol, name) if value.nil?
fail(UndefinedSymbol, name) if value.nil?
value value
end end
# Resolve symbol by working backwards through each
#### # containing scope. Similarly named scopes shadow outer scopes
## Resolve symbol by working backwards through each
## containing scope. Similarly named scopes shadow outer scopes
def resolve_symbol_scoped(name) def resolve_symbol_scoped(name)
root = "-#{name}".to_sym root = "-#{name}".to_sym
stack = @scope_stack.dup stack = @scope_stack.dup
loop do loop do
scope = retreive_scope(stack) scope = retreive_scope(stack)
## We see if there is a key either under this name, or root # We see if there is a key either under this name, or root
v = scope[name.to_sym] || scope[root] v = scope[name.to_sym] || scope[root]
v = v.kind_of?(Hash) ? v[root] : v v = v.is_a?(Hash) ? v[root] : v
return v unless v.nil? return v unless v.nil?
## Pop the stack so we can decend to the parent scope, if any # Pop the stack so we can decend to the parent scope, if any
stack.pop stack.pop
return nil if stack.empty? return nil if stack.empty?
end end
end end
# Dot syntax means to check an absolute path to the symbol
#### # :global is ignored if it is provided as part of the path
## Dot syntax means to check an absolute path to the symbol
## :global is ignored if it is provided as part of the path
def resolve_symbol_dot_syntax(name) def resolve_symbol_dot_syntax(name)
path_ary = name.split('.').map(&:to_sym) path_ary = name.split('.').map(&:to_sym)
symbol = path_ary.pop symbol = path_ary.pop
@ -131,40 +111,32 @@ module N65
scope = retreive_scope(path_ary) scope = retreive_scope(path_ary)
## We see if there is a key either under this name, or root # We see if there is a key either under this name, or root
v = scope[symbol] v = scope[symbol]
v.kind_of?(Hash) ? v[root] : v v.is_a?(Hash) ? v[root] : v
end end
# Export the symbol table as YAML
####
## Export the symbol table as YAML
def export_to_yaml def export_to_yaml
@symbols.to_yaml.gsub(/(\d+)$/) do |match| @symbols.to_yaml.gsub(/(\d+)$/) do |match|
integer = match.to_i integer = match.to_i
sprintf("0x%.4X", integer) format('0x%.4X', integer)
end end
end end
# Export a cycle count for top level subroutines
####
## Export a cycle count for top level subroutines
def export_cycle_count_yaml def export_cycle_count_yaml
@subroutine_cycles.to_yaml @subroutine_cycles.to_yaml
end end
private private
#### # A bit more clearly states to get the current scope
## A bit more clearly states to get the current scope
def current_scope def current_scope
retreive_scope retreive_scope
end end
# Retrieve a reference to a scope, current scope by default
####
## Retrieve a reference to a scope, current scope by default
def retreive_scope(path_ary = @scope_stack) def retreive_scope(path_ary = @scope_stack)
path_ary = path_ary.dup path_ary = path_ary.dup
path_ary.unshift(:global) unless path_ary.first == :global path_ary.unshift(:global) unless path_ary.first == :global
@ -175,28 +147,22 @@ module N65
if new_scope.nil? if new_scope.nil?
path_string = generate_scope_path(path_ary) path_string = generate_scope_path(path_ary)
message = "Resolving scope: #{path_string} failed at #{path_component}" message = "Resolving scope: #{path_string} failed at #{path_component}"
fail(InvalidScope, message) if new_scope.nil? raise(InvalidScope, message) if new_scope.nil?
end end
new_scope new_scope
end end
end end
# Generate a scope path from an array
####
## Generate a scope path from an array
def generate_scope_path(path_ary) def generate_scope_path(path_ary)
path_ary.join('.') path_ary.join('.')
end end
# Generate an anonymous scope name
####
## Generate an anonymous scope name
def generate_name def generate_name
@anonymous_scope_number += 1 @anonymous_scope_number += 1
"anonymous_#{@anonymous_scope_number}" "anonymous_#{@anonymous_scope_number}"
end end
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module N65 module N65
VERSION ||= "1.5.3" VERSION ||= '1.5.3'
end end

View File

@ -1,23 +1,28 @@
# coding: utf-8 # frozen_string_literal: true
lib = File.expand_path('../lib', __FILE__)
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'n65/version' require 'n65/version'
Gem::Specification.new do |spec| Gem::Specification.new do |spec|
spec.name = "n65" spec.name = 'n65'
spec.version = N65::VERSION spec.version = N65::VERSION
spec.authors = ["Safiire"] spec.authors = ['Safiire']
spec.email = ["safiire@irkenkitties.com"] spec.email = ['safiire@irkenkitties.com']
spec.summary = %q{An NES assembler for the 6502 microprocessor written in Ruby} spec.summary = 'An NES assembler for the 6502 microprocessor'
spec.description = %q{An NES assembler for the 6502 microprocessor written in Ruby} spec.description = 'An NES assembler for the 6502 microprocessor'
spec.homepage = "http://github.com/safiire/n65" spec.homepage = 'http://github.com/safiire/n65'
spec.license = "GPL2" spec.license = 'GPL2'
spec.files = `git ls-files -z`.split("\x0") spec.files = `git ls-files -z`.split("\x0")
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ["lib"] spec.require_paths = ['lib']
spec.add_development_dependency "bundler", "~> 1.7" spec.required_ruby_version = '>= 2.4.0'
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency 'bundler'
spec.add_development_dependency 'minitest'
spec.add_development_dependency 'rake'
spec.add_development_dependency 'rubocop'
end end

View File

@ -1,14 +1,11 @@
gem 'minitest' gem 'minitest'
require 'minitest/autorun' require 'minitest/autorun'
require 'minitest/unit' require 'minitest/unit'
require_relative '../lib/n65'
require_relative '../lib/n65.rb'
class TestArithmeticSymbols < MiniTest::Test class TestArithmeticSymbols < MiniTest::Test
include N65 include N65
def test_identify_plain_symbol def test_identify_plain_symbol
re = Regexp.new(Regexes::Sym) re = Regexp.new(Regexes::Sym)
assert_match(re, 'dog') assert_match(re, 'dog')
@ -16,14 +13,12 @@ class TestArithmeticSymbols < MiniTest::Test
assert_match(re, 'global.animal.dog') assert_match(re, 'global.animal.dog')
end end
def test_symbol_values def test_symbol_values
st = SymbolTable.new st = SymbolTable.new
st.define_symbol('variable', 0xff) st.define_symbol('variable', 0xff)
assert_equal(0xff, st.resolve_symbol('variable')) assert_equal(0xff, st.resolve_symbol('variable'))
end end
def test_perform_symbolic_arithmetic def test_perform_symbolic_arithmetic
st = SymbolTable.new st = SymbolTable.new
st.define_symbol('variable', 0x20) st.define_symbol('variable', 0x20)
@ -31,7 +26,6 @@ class TestArithmeticSymbols < MiniTest::Test
assert_equal(0x40, st.resolve_symbol('variable*2')) assert_equal(0x40, st.resolve_symbol('variable*2'))
end end
def test_symbol_addition def test_symbol_addition
program = <<-ASM program = <<-ASM
.ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0} .ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0}
@ -72,7 +66,4 @@ class TestArithmeticSymbols < MiniTest::Test
] ]
assert_equal(binary, correct) assert_equal(binary, correct)
end end
end end

View File

@ -1,82 +1,77 @@
# frozen_string_literal: true
gem 'minitest' gem 'minitest'
require 'minitest/autorun' require 'minitest/autorun'
require 'minitest/unit' require 'minitest/unit'
require_relative '../lib/n65/memory_space.rb' require_relative '../lib/n65/memory_space'
class TestMemorySpace < MiniTest::Test class TestMemorySpace < MiniTest::Test
include N65 include N65
def test_create_prog_rom def test_create_prog_rom
## First just try to read alll of it # First just try to read alll of it
space = MemorySpace.create_prog_rom space = MemorySpace.create_prog_rom
contents = space.read(0x8000, 0x4000) contents = space.read(0x8000, 0x4000)
assert_equal(contents.size, 0x4000) assert_equal(contents.size, 0x4000)
assert(contents.all?{|byte| byte.zero?}) assert(contents.all?(&:zero?))
## It is mirrored so this should also work # It is mirrored so this should also work
space = MemorySpace.create_prog_rom space = MemorySpace.create_prog_rom
contents = space.read(0xC000, 0x4000) contents = space.read(0xC000, 0x4000)
assert_equal(contents.size, 0x4000) assert_equal(contents.size, 0x4000)
assert(contents.all?{|byte| byte.zero?}) assert(contents.all?(&:zero?))
end end
def test_writing def test_writing
## Write some bytes into prog 2 area # Write some bytes into prog 2 area
space = MemorySpace.create_prog_rom space = MemorySpace.create_prog_rom
space.write(0xC000, "hi there".bytes) space.write(0xC000, 'hi there'.bytes)
## Read them back.. # Read them back..
contents = space.read(0xC000, 8) contents = space.read(0xC000, 8)
assert_equal('hi there', contents.pack('C*')) assert_equal('hi there', contents.pack('C*'))
## Should be mirrored in prog 1 # Should be mirrored in prog 1
contents = space.read(0x8000, 8) contents = space.read(0x8000, 8)
assert_equal('hi there', contents.pack('C*')) assert_equal('hi there', contents.pack('C*'))
end end
def test_reading_out_of_bounds def test_reading_out_of_bounds
space = MemorySpace.create_prog_rom space = MemorySpace.create_prog_rom
assert_raises(MemorySpace::AccessOutsideProgRom) do assert_raises(MemorySpace::AccessOutsideProgRom) do
space.read(0x200, 10) space.read(0x200, 10)
end end
## But that is valid char rom area, so no explody # But that is valid char rom area, so no explody
space = MemorySpace.create_char_rom space = MemorySpace.create_char_rom
space.read(0x200, 10) space.read(0x200, 10)
## But something like this should explode # But something like this should explode
space = MemorySpace.create_char_rom space = MemorySpace.create_char_rom
assert_raises(MemorySpace::AccessOutsideCharRom) do assert_raises(MemorySpace::AccessOutsideCharRom) do
space.read(0x8001, 10) space.read(0x8001, 10)
end end
end end
# There seem to be problems writing bytes right to
#### # the end of the memory map, specifically where the
## There seem to be problems writing bytes right to # vector table is in prog rom, so let's test that.
## the end of the memory map, specifically where the
## vector table is in prog rom, so let's test that.
def test_writing_to_end def test_writing_to_end
space = MemorySpace.create_prog_rom space = MemorySpace.create_prog_rom
bytes = [0xDE, 0xAD] bytes = [0xDE, 0xAD]
## Write the NMI address to FFFA # Write the NMI address to FFFA
space.write(0xFFFA, bytes) space.write(0xFFFA, bytes)
## Write the entry point to FFFC # Write the entry point to FFFC
space.write(0xFFFC, bytes) space.write(0xFFFC, bytes)
## Write the irq to FFFE, and this fails, saying # Write the irq to FFFE, and this fails, saying
## I'm trying to write to $10000 for some reason. # I'm trying to write to $10000 for some reason.
space.write(0xFFFE, bytes) space.write(0xFFFE, bytes)
## Write to the very first # Write to the very first
space.write(0x8000, bytes) space.write(0x8000, bytes)
end end
end end

View File

@ -1,25 +1,23 @@
# frozen_string_literal: true
gem 'minitest' gem 'minitest'
require 'minitest/autorun' require 'minitest/autorun'
require 'minitest/unit' require 'minitest/unit'
require_relative '../lib/n65/symbol_table.rb' require_relative '../lib/n65/symbol_table'
require_relative '../lib/n65.rb' require_relative '../lib/n65'
class TestSymbolTable < MiniTest::Test class TestSymbolTable < MiniTest::Test
include N65 include N65
#### # Test that we can make simple global symbols
## Test that we can make simple global symbols
def test_define_global_symbols def test_define_global_symbols
st = SymbolTable.new st = SymbolTable.new
st.define_symbol('dog', 'woof') st.define_symbol('dog', 'woof')
assert_equal('woof', st.resolve_symbol('dog')) assert_equal('woof', st.resolve_symbol('dog'))
end end
# Test entering into a sub scope, and setting and retrieving values
####
## Test entering into a sub scope, and setting and retrieving values
def test_enter_scope def test_enter_scope
st = SymbolTable.new st = SymbolTable.new
st.enter_scope('animals') st.enter_scope('animals')
@ -27,9 +25,7 @@ class TestSymbolTable < MiniTest::Test
assert_equal('woof', st.resolve_symbol('dog')) assert_equal('woof', st.resolve_symbol('dog'))
end end
# Access something from an outer scope without dot syntax
####
## Access something from an outer scope without dot syntax
def test_outer_scope def test_outer_scope
st = SymbolTable.new st = SymbolTable.new
st.enter_scope('outer') st.enter_scope('outer')
@ -39,9 +35,7 @@ class TestSymbolTable < MiniTest::Test
assert_equal('woof', st.resolve_symbol('dog')) assert_equal('woof', st.resolve_symbol('dog'))
end end
# Access something from an outer scope without dot syntax
####
## Access something from an outer scope without dot syntax
def test_shadow def test_shadow
st = SymbolTable.new st = SymbolTable.new
st.enter_scope('outer') st.enter_scope('outer')
@ -55,9 +49,7 @@ class TestSymbolTable < MiniTest::Test
assert_equal('bark', st.resolve_symbol('outer.inner.dog')) assert_equal('bark', st.resolve_symbol('outer.inner.dog'))
end end
# Test exiting a sub scope, and seeing that the variable is unavailable by simple name
####
## Test exiting a sub scope, and seeing that the variable is unavailable by simple name
def test_exit_scope def test_exit_scope
st = SymbolTable.new st = SymbolTable.new
st.enter_scope('animals') st.enter_scope('animals')
@ -71,9 +63,7 @@ class TestSymbolTable < MiniTest::Test
end end
end end
# Test exiting a sub scope, and being able to access a symbol through a full path
####
## Test exiting a sub scope, and being able to access a symbol through a full path
def test_exit_scope_full_path def test_exit_scope_full_path
st = SymbolTable.new st = SymbolTable.new
st.enter_scope('animals') st.enter_scope('animals')
@ -85,9 +75,7 @@ class TestSymbolTable < MiniTest::Test
assert_equal('woof', st.resolve_symbol('animals.dog')) assert_equal('woof', st.resolve_symbol('animals.dog'))
end end
# Have two symbols that are the same but are in different scopes
####
## Have two symbols that are the same but are in different scopes
def test_two_scopes_same_symbol def test_two_scopes_same_symbol
st = SymbolTable.new st = SymbolTable.new
st.define_symbol('dog', 'woof') st.define_symbol('dog', 'woof')
@ -104,10 +92,7 @@ class TestSymbolTable < MiniTest::Test
assert_equal('woofwoof', st.resolve_symbol('animals.dog')) assert_equal('woofwoof', st.resolve_symbol('animals.dog'))
end end
# How do you get stuff out of the global scope when you are in a sub scope?
####
## How do you get stuff out of the global scope when you are in
## a sub scope?
def test_access_global_scope def test_access_global_scope
st = SymbolTable.new st = SymbolTable.new
st.define_symbol('dog', 'woof') st.define_symbol('dog', 'woof')
@ -117,13 +102,11 @@ class TestSymbolTable < MiniTest::Test
st.define_symbol('pig', 'oink') st.define_symbol('pig', 'oink')
assert_equal('oink', st.resolve_symbol('pig')) assert_equal('oink', st.resolve_symbol('pig'))
## Ok, now I want to access global.dog basically from the previous scope # Ok, now I want to access global.dog basically from the previous scope
assert_equal('woof', st.resolve_symbol('global.dog')) assert_equal('woof', st.resolve_symbol('global.dog'))
end end
# Now I want to just test making an anonymous scope
####
## Now I want to just test making an anonymous scope
def test_anonymous_scope def test_anonymous_scope
st = SymbolTable.new st = SymbolTable.new
st.define_symbol('dog', 'woof') st.define_symbol('dog', 'woof')
@ -133,19 +116,17 @@ class TestSymbolTable < MiniTest::Test
st.define_symbol('pig', 'oink') st.define_symbol('pig', 'oink')
assert_equal('oink', st.resolve_symbol('pig')) assert_equal('oink', st.resolve_symbol('pig'))
## Ok, now I want to access global.dog basically from the previous scope # Ok, now I want to access global.dog basically from the previous scope
assert_equal('woof', st.resolve_symbol('global.dog')) assert_equal('woof', st.resolve_symbol('global.dog'))
## Now exit the anonymous scope and get dog # Now exit the anonymous scope and get dog
st.exit_scope st.exit_scope
assert_equal('woof', st.resolve_symbol('global.dog')) assert_equal('woof', st.resolve_symbol('global.dog'))
assert_equal('woof', st.resolve_symbol('dog')) assert_equal('woof', st.resolve_symbol('dog'))
end end
# Now I want to test that I cannot exist the outer-most
#### # global scope by mistake
## Now I want to test that I cannot exist the outer-most
## global scope by mistake
def test_cant_exit_global def test_cant_exit_global
st = SymbolTable.new st = SymbolTable.new
assert_raises(SymbolTable::CantExitScope) do assert_raises(SymbolTable::CantExitScope) do
@ -153,10 +134,8 @@ class TestSymbolTable < MiniTest::Test
end end
end end
# I would like the name of the scope to take on the
#### # value of the program counter at that location.
## I would like the name of the scope to take on the
## value of the program counter at that location.
def test_scope_as_symbol def test_scope_as_symbol
program = <<-ASM program = <<-ASM
.ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0} .ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0}
@ -172,7 +151,7 @@ class TestSymbolTable < MiniTest::Test
jmp global.main jmp global.main
ASM ASM
#### There really should be an evaluate string method # There really should be an evaluate string method
assembler = Assembler.new assembler = Assembler.new
program.split(/\n/).each do |line| program.split(/\n/).each do |line|
assembler.assemble_one_line(line) assembler.assemble_one_line(line)
@ -181,9 +160,7 @@ class TestSymbolTable < MiniTest::Test
assert_equal(0x8000, assembler.symbol_table.resolve_symbol('global.main')) assert_equal(0x8000, assembler.symbol_table.resolve_symbol('global.main'))
end end
# Fix a bug where we can't see a forward declared symbol in a scope
####
## Fix a bug where we can't see a forward declared symbol in a scope
def test_foward_declaration_in_scope def test_foward_declaration_in_scope
program = <<-ASM program = <<-ASM
;;;; ;;;;
@ -208,7 +185,7 @@ class TestSymbolTable < MiniTest::Test
. .
ASM ASM
#### There really should be an evaluate string method # There really should be an evaluate string method
assembler = Assembler.new assembler = Assembler.new
program.split(/\n/).each do |line| program.split(/\n/).each do |line|
assembler.assemble_one_line(line) assembler.assemble_one_line(line)
@ -216,23 +193,22 @@ class TestSymbolTable < MiniTest::Test
puts YAML.dump(assembler.symbol_table) puts YAML.dump(assembler.symbol_table)
assembler.fulfill_promises assembler.fulfill_promises
#### The forward symbol should have been resolved to +3, and the ROM should look like this: # The forward symbol should have been resolved to +3, and the ROM should look like this:
correct_rom = [0x4e, 0x45, 0x53, 0x1a, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, correct_rom = [
0x78, # SEI 0x4e, 0x45, 0x53, 0x1a, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0xd8, # CLD 0x78, # SEI
0xa9, 0x0, # LDA immediate 0 0xd8, # CLD
0xd0, 0x3, # BNE +3 0xa9, 0x0, # LDA immediate 0
0xea, # NOP 0xd0, 0x3, # BNE +3
0xea, # NOP 0xea, # NOP
0xea, # NOP 0xea, # NOP
0x60 # RTS forward_symbol 0xea, # NOP
0x60 # RTS forward_symbol
] ]
#### Grab the first 26 bytes of the rom and make sure they assemble to the above # Grab the first 26 bytes of the rom and make sure they assemble to the above
emitted_rom = assembler.emit_binary_rom.bytes[0...26] emitted_rom = assembler.emit_binary_rom.bytes[0...26]
assert_equal(correct_rom, emitted_rom) assert_equal(correct_rom, emitted_rom)
#### Yup it is fixed now. # Yup it is fixed now.
end end
end end

View File

@ -1,91 +1,89 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
# frozen_string_literal: true
############################################################################### ##############################################################################
## http://www.6502.org/tutorials/6502opcodes.html # From http://www.6502.org/tutorials/6502opcodes.html
## This web page has information about each and every 6502 instruction # This web page has information about each and every 6502 instruction
## Specifically: # Specifically:
## #
## - Description of what each of the instructions do # - Description of what each of the instructions do
## - Which modes are supported by which instructions, immediate, zero page # - Which modes are supported by which instructions, immediate, zero page
## zero page x, and y, absolute, indirect, relative etc. # zero page x, and y, absolute, indirect, relative etc.
## - The hex codes each instruction assembles to, in each mode. # - The hex codes each instruction assembles to, in each mode.
## - The lengths in bytes of each instruction, by mode # - The lengths in bytes of each instruction, by mode
## - The possibly variable number of cycles each instruction takes. # - The possibly variable number of cycles each instruction takes.
## #
## There are 56 of them, and in my programmer laziness I just wrote this # There are 56 of them, and in my programmer laziness I just wrote this
## script to parse the page into the data structure that you see in # script to parse the page into the data structure that you see in
## opcodes.yaml. This really helped in creating the assembler, and # opcodes.yaml. This really helped in creating the assembler, and
## it had basically everything I needed to know, and sped up writing # it had basically everything I needed to know, and sped up writing
## this by huge factor. So, yay to this page, and this script! # this by huge factor. So, yay to this page, and this script!
require 'yaml' require 'yaml'
## Instruction name, and output structure to fill in. # Instruction name, and output structure to fill in.
name = :adc name = :adc
output = {name => {}} output = { name: {} }
## Copy paste the tables from that website into this heredoc: # Copy paste the tables from that website into this heredoc:
text =<<-TEXT text = <<~'TEXT'
Immediate ADC #$44 $69 2 2 Immediate ADC #$44 $69 2 2
Zero Page ADC $44 $65 2 3 Zero Page ADC $44 $65 2 3
Zero Page,X ADC $44,X $75 2 4 Zero Page,X ADC $44,X $75 2 4
Absolute ADC $4400 $6D 3 4 Absolute ADC $4400 $6D 3 4
Absolute,X ADC $4400,X $7D 3 4+ Absolute,X ADC $4400,X $7D 3 4+
Absolute,Y ADC $4400,Y $79 3 4+ Absolute,Y ADC $4400,Y $79 3 4+
Indirect,X ADC ($44,X) $61 2 6 Indirect,X ADC ($44,X) $61 2 6
Indirect,Y ADC ($44),Y $71 2 5+ Indirect,Y ADC ($44),Y $71 2 5+
TEXT TEXT
# And now iterate over each line to extract the info
## And now iterate over each line to extract the info
lines = text.split(/\n/) lines = text.split(/\n/)
lines.each do |line| lines.each do |line|
# Grab out the values we care about
## Grab out the values we care about
parts = line.split parts = line.split
cycles, len, hex = parts[-1], parts[-2], parts[-3] cycles, len, hex = parts[-1], parts[-2], parts[-3]
hex = "0x%X" % hex.gsub('$', '').to_i(16) hex = format('0x%X', hex.gsub('$', '').to_i(16))
match_data = cycles.match(/([0-9]+)(\+?)/) match_data = cycles.match(/([0-9]+)(\+?)/)
cycles = match_data[1] cycles = match_data[1]
boundary = match_data[2] boundary = match_data[2]
hash = {:hex => hex, :len => len, :cycles => cycles, :boundry_add => boundary != ""} hash = { hex: hex, len: len, cycles: cycles, boundry_add: boundary != '' }
## And now decide which mode the line belongs to, collecting each listed mode # And now decide which mode the line belongs to, collecting each listed mode
hash = case line hash = case line
when /^Accumulator/ when /^Accumulator/
{:accumulator => hash} { accumulator: hash }
when /^Immediate/ when /^Immediate/
{:immediate => hash} { immediate: hash }
when /^Zero Page,X/ when /^Zero Page,X/
{:zero_page_x => hash} { zero_page_x: hash }
when /^Zero Page,Y/ when /^Zero Page,Y/
{:zero_page_y => hash} { zero_page_y: hash }
when /^Zero Page/ when /^Zero Page/
{:zero_page => hash} { zero_page: hash }
when /^Absolute,X/ when /^Absolute,X/
{:absolute_x => hash} { absolute_x: hash }
when /^Absolute,Y/ when /^Absolute,Y/
{:absolute_y => hash} { absolute_y: hash }
when /^Absolute/ when /^Absolute/
{:absolute => hash} { absolute: hash }
when /^Indirect,X/ when /^Indirect,X/
{:indirect_x => hash} { indirect_x: hash }
when /^Indirect,Y/ when /^Indirect,Y/
{:indirect_y => hash} { indirect_y: hash }
when /^Indirect/ when /^Indirect/
{:indirect => hash} { indirect: hash }
when /^Implied/ when /^Implied/
{:implied => hash} { implied: hash }
else else
{} {}
end end
output[name].merge!(hash) output[name].merge!(hash)
end end
## Now output some yaml, and I only had to do this about 45 times # Now output some yaml, and I only had to do this about 45 times
## instead of laboriously and mistak-pronely doing it by hand. # instead of laboriously and mistak-pronely doing it by hand.
puts YAML.dump(output).gsub("'", '') puts YAML.dump(output).gsub("'", '')
## See opcodes.yaml # See opcodes.yaml