mirror of
https://github.com/safiire/n65.git
synced 2025-04-09 00:37:29 +00:00
Merge pull request #1 from safiire/reformat-code
Installed Rubocop and corrected many linter errors.
This commit is contained in:
commit
1d03529b29
125
.rubocop.yml
Normal file
125
.rubocop.yml
Normal 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
|
6
Gemfile
6
Gemfile
@ -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'
|
||||
|
7
Rakefile
7
Rakefile
@ -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
|
||||
|
2
bin/n65
2
bin/n65
@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
###############################################################################
|
||||
## 6502 Assembler for the NES's 2A03
|
||||
##
|
||||
|
197
lib/n65.rb
197
lib/n65.rb
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module N65
|
||||
VERSION ||= "1.5.3"
|
||||
VERSION ||= '1.5.3'
|
||||
end
|
||||
|
29
n65.gemspec
29
n65.gemspec
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user