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'
# Specify your gem's dependencies in n65.gemspec
# Specify gem dependencies in n65.gemspec
gemspec
Gem 'mintest'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,73 +1,63 @@
# frozen_string_literal: true
require 'optparse'
require_relative '../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
####
## Initialize with ARGV commandline
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
end
####
## Run the assembler
# Run the assembler
def run
## First use the option parser
parser = create_option_parser
parser.parse!(@argv)
## Whatever is leftover in argv the input files
if @argv.size.zero?
STDERR.puts("No input files")
warn('No input files')
exit(1)
end
## Only can assemble one file at once for now
# Only can assemble one file at once for now
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)
end
input_file = @argv.shift
## Make sure the input file exists
unless File.exists?(input_file)
STDERR.puts "Input file #{input_file} does not exist"
# Make sure the input file exists
unless File.exist?(input_file)
warn("Input file #{input_file} does not exist")
exit(1)
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?
ext = File.extname(input_file)
@options[:output_file] = input_file.gsub(ext, '') + '.nes'
@options[:output_file] = "#{input_file.gsub(ext, '')}.nes"
end
if @options.values.any?(&:nil?)
STDERR.puts "Missing options try --help"
warn('Missing options try --help')
exit(1)
end
N65::Assembler.from_file(input_file, @options)
end
private
####
## Create a commandline option parser
def create_option_parser
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|
@options[:output_file] = output_file;
@options[:output_file] = output_file
end
opts.on('-s', '--symbols', 'Outputs a symbol map') do
@ -91,11 +81,7 @@ module N65
puts opts
exit
end
end
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module N65
require_relative 'instruction'
require_relative 'directives/ines_header'
require_relative 'directives/org'
@ -15,71 +15,53 @@ module N65
require_relative 'directives/exit_scope'
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
#### Custom Exceptions
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)
sanitized = sanitize_line(line)
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)
unless label.nil?
return label
end
return label unless label.nil?
## Now check if we have a directive
# Now check if we have a directive
directive = parse_directive(sanitized)
unless directive.nil?
return directive
end
return directive unless directive.nil?
## Now, surely it is an asm instruction?
# Now, surely it is an asm instruction?
instruction = Instruction.parse(sanitized)
unless instruction.nil?
return instruction
end
return instruction unless instruction.nil?
## Guess not, we have no idea
fail(CannotParse, sanitized)
# Guess not, we have no idea
raise(CannotParse, sanitized)
end
private
####
## Sanitize one line of program source
# Sanitize one line of program source
def self.sanitize_line(line)
code = line.split(';').first || ""
code = line.split(';').first || ''
code.strip.chomp
end
private_class_method :sanitize_line
####
## Try to Parse a directive
def self.parse_directive(line)
if line.start_with?('.')
Directives.each do |directive|
DIRECTIVES.each do |directive|
object = directive.parse(line)
return object unless object.nil?
end
end
nil
end
private_class_method :parse_directive
end
end

View File

@ -1,15 +1,14 @@
# frozen_string_literal: true
module N65
####
## All the regexes used to parse in one module
# All the regexes used to parse in one module
module Regexes
## Mnemonics
# rubocop:disable Naming/ConstantName
# Mnemonics
Mnemonic = '([A-Za-z]{3})'
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})'
Hex16 = '\$([A-Fa-f0-9]{3,4})'
@ -17,17 +16,17 @@ module N65
Bin16 = '%([01]{9,16})'
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}"
## 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+)*)'
## The X or Y register
# The X or Y register
XReg = '[Xx]'
YReg = '[Yy]'
end
# rubocop:enable Naming/ConstantName
end
end

View File

@ -1,128 +1,108 @@
# frozen_string_literal: true
module N65
class SymbolTable
attr_accessor :scope_stack
##### Custom Exceptions
# Custom Exceptions
class InvalidScope < StandardError; end
class UndefinedSymbol < 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
@symbols = {
:global => {}
global: {}
}
@anonymous_scope_number = 0
@scope_stack = [:global]
@subroutine_cycles = {}
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)
cycles ||= 0
top_level_subroutine = @scope_stack[1]
unless top_level_subroutine.nil?
@subroutine_cycles[top_level_subroutine] ||= 0
@subroutine_cycles[top_level_subroutine] += cycles
end
return if top_level_subroutine.nil?
@subroutine_cycles[top_level_subroutine] ||= 0
@subroutine_cycles[top_level_subroutine] += cycles
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)
name = generate_name if name.nil?
name = name.to_sym
scope = current_scope
if scope.has_key?(name)
fail(InvalidScope, "Scope: #{name} already exists")
end
raise(InvalidScope, "Scope: #{name} already exists") if scope.key?(name)
scope[name] = {}
@scope_stack.push(name)
end
####
## Exit the current scope
# Exit the current scope
def exit_scope
if @scope_stack.size == 1
fail(CantExitScope, "You cannot exit global scope")
end
raise(CantExitScope, 'You cannot exit global scope') if @scope_stack.size == 1
@scope_stack.pop
end
####
## Define a symbol in the current scope
# Define a symbol in the current scope
def define_symbol(symbol, value)
scope = current_scope
scope[symbol.to_sym] = value
end
####
## Separate arithmetic from scope name
# Separate arithmetic from scope name
def find_arithmetic(name)
last_name = name.split('.').last
md = last_name.match(/([\+\-\*\/])(\d+)$/)
f = lambda{|v| v}
md = last_name.match(%r{([+\-*/])(\d+)$})
f = ->(v) { v }
unless md.nil?
full_match, operator, argument = md.to_a
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
[name, f]
end
####
## Resolve a symbol to its value
# Resolve a symbol to its value
def resolve_symbol(name)
name, arithmetic = find_arithmetic(name)
method = name.include?('.') ? :resolve_symbol_dot_syntax : :resolve_symbol_scoped
value = self.send(method, name)
value = send(method, name)
value = arithmetic.call(value)
raise(UndefinedSymbol, name) if value.nil?
fail(UndefinedSymbol, name) if value.nil?
value
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)
root = "-#{name}".to_sym
stack = @scope_stack.dup
loop do
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 = v.kind_of?(Hash) ? v[root] : v
v = v.is_a?(Hash) ? v[root] : v
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
return nil if stack.empty?
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)
path_ary = name.split('.').map(&:to_sym)
symbol = path_ary.pop
@ -131,40 +111,32 @@ module N65
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.kind_of?(Hash) ? v[root] : v
v.is_a?(Hash) ? v[root] : v
end
####
## Export the symbol table as YAML
# Export the symbol table as YAML
def export_to_yaml
@symbols.to_yaml.gsub(/(\d+)$/) do |match|
integer = match.to_i
sprintf("0x%.4X", integer)
format('0x%.4X', integer)
end
end
####
## Export a cycle count for top level subroutines
# Export a cycle count for top level subroutines
def export_cycle_count_yaml
@subroutine_cycles.to_yaml
end
private
####
## A bit more clearly states to get the current scope
# A bit more clearly states to get the current scope
def current_scope
retreive_scope
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)
path_ary = path_ary.dup
path_ary.unshift(:global) unless path_ary.first == :global
@ -175,28 +147,22 @@ module N65
if new_scope.nil?
path_string = generate_scope_path(path_ary)
message = "Resolving scope: #{path_string} failed at #{path_component}"
fail(InvalidScope, message) if new_scope.nil?
raise(InvalidScope, message) if new_scope.nil?
end
new_scope
end
end
####
## Generate a scope path from an array
# Generate a scope path from an array
def generate_scope_path(path_ary)
path_ary.join('.')
end
####
## Generate an anonymous scope name
# Generate an anonymous scope name
def generate_name
@anonymous_scope_number += 1
"anonymous_#{@anonymous_scope_number}"
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

@ -1,25 +1,23 @@
# frozen_string_literal: true
gem 'minitest'
require 'minitest/autorun'
require 'minitest/unit'
require_relative '../lib/n65/symbol_table.rb'
require_relative '../lib/n65.rb'
require_relative '../lib/n65/symbol_table'
require_relative '../lib/n65'
class TestSymbolTable < MiniTest::Test
include N65
####
## Test that we can make simple global symbols
# Test that we can make simple global symbols
def test_define_global_symbols
st = SymbolTable.new
st.define_symbol('dog', 'woof')
assert_equal('woof', st.resolve_symbol('dog'))
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
st = SymbolTable.new
st.enter_scope('animals')
@ -27,9 +25,7 @@ class TestSymbolTable < MiniTest::Test
assert_equal('woof', st.resolve_symbol('dog'))
end
####
## Access something from an outer scope without dot syntax
# Access something from an outer scope without dot syntax
def test_outer_scope
st = SymbolTable.new
st.enter_scope('outer')
@ -39,9 +35,7 @@ class TestSymbolTable < MiniTest::Test
assert_equal('woof', st.resolve_symbol('dog'))
end
####
## Access something from an outer scope without dot syntax
# Access something from an outer scope without dot syntax
def test_shadow
st = SymbolTable.new
st.enter_scope('outer')
@ -55,9 +49,7 @@ class TestSymbolTable < MiniTest::Test
assert_equal('bark', st.resolve_symbol('outer.inner.dog'))
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
st = SymbolTable.new
st.enter_scope('animals')
@ -71,9 +63,7 @@ class TestSymbolTable < MiniTest::Test
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
st = SymbolTable.new
st.enter_scope('animals')
@ -85,9 +75,7 @@ class TestSymbolTable < MiniTest::Test
assert_equal('woof', st.resolve_symbol('animals.dog'))
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
st = SymbolTable.new
st.define_symbol('dog', 'woof')
@ -104,10 +92,7 @@ class TestSymbolTable < MiniTest::Test
assert_equal('woofwoof', st.resolve_symbol('animals.dog'))
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
st = SymbolTable.new
st.define_symbol('dog', 'woof')
@ -117,13 +102,11 @@ class TestSymbolTable < MiniTest::Test
st.define_symbol('pig', 'oink')
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'))
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
st = SymbolTable.new
st.define_symbol('dog', 'woof')
@ -133,19 +116,17 @@ class TestSymbolTable < MiniTest::Test
st.define_symbol('pig', 'oink')
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'))
## Now exit the anonymous scope and get dog
# Now exit the anonymous scope and get dog
st.exit_scope
assert_equal('woof', st.resolve_symbol('global.dog'))
assert_equal('woof', st.resolve_symbol('dog'))
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
st = SymbolTable.new
assert_raises(SymbolTable::CantExitScope) do
@ -153,10 +134,8 @@ class TestSymbolTable < MiniTest::Test
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
program = <<-ASM
.ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0}
@ -172,7 +151,7 @@ class TestSymbolTable < MiniTest::Test
jmp global.main
ASM
#### There really should be an evaluate string method
# There really should be an evaluate string method
assembler = Assembler.new
program.split(/\n/).each do |line|
assembler.assemble_one_line(line)
@ -181,9 +160,7 @@ class TestSymbolTable < MiniTest::Test
assert_equal(0x8000, assembler.symbol_table.resolve_symbol('global.main'))
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
program = <<-ASM
;;;;
@ -208,7 +185,7 @@ class TestSymbolTable < MiniTest::Test
.
ASM
#### There really should be an evaluate string method
# There really should be an evaluate string method
assembler = Assembler.new
program.split(/\n/).each do |line|
assembler.assemble_one_line(line)
@ -216,23 +193,22 @@ class TestSymbolTable < MiniTest::Test
puts YAML.dump(assembler.symbol_table)
assembler.fulfill_promises
#### 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,
0x78, # SEI
0xd8, # CLD
0xa9, 0x0, # LDA immediate 0
0xd0, 0x3, # BNE +3
0xea, # NOP
0xea, # NOP
0xea, # NOP
0x60 # RTS forward_symbol
# 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,
0x78, # SEI
0xd8, # CLD
0xa9, 0x0, # LDA immediate 0
0xd0, 0x3, # BNE +3
0xea, # NOP
0xea, # NOP
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]
assert_equal(correct_rom, emitted_rom)
#### Yup it is fixed now.
# Yup it is fixed now.
end
end

View File

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