module RubyLabs
=begin rdoc
== MarsLab
The MARSLab module has definitions of classes and methods used in the projects for Chapter 8
of Explorations in Computing.
The module has a Ruby implementation of the MARS
virtual machine used in the game of Core War.
The machine implemented here conforms as close as possible to the ICWS'88 standard,
as described by Mark Durham in his Introduction to Redcode (http://corewar.co.uk/guides.htm).
Instructions and data in a MARS program are represented by Word objects.
Each Word has an opcode and two operands (A and B). Integer data is represented by a Word where the opcode
field is DAT. Other Word objects have names of executable machine instructions for opcodes.
To test a single program users can make an instance of a class named MiniMARS, passing
it the name of a test program. The test machine will have a small memory, only big enough
to hold the test machine, and special versions of the step, dump and other methods.
The main machine that runs one, two, or three programs simultaneously is in the module
named MARS. Methods that assemble, load, and run programs are module methods, e.g.
to assemble a program call MARS.assemble(foo).
#--
TODO add [] and []= methods to Word, use it to extract/assign bits or ranges of bits
TODO define ranges for fields, e.g. @@opcode = 0..3; use to get opcode of Word, e.g. instr[@@opcode]
TODO make sure all code uses [] interface (e.g. no more instr.amode[0] == ?#)
TODO pack words into binary -- will kill several birds at once -- speed, trim values to 0..4095, ..
TODO color a cell black when a thread halts after executing an instruction in that cell
TODO MARS.dump
TODO pass load address to contest (which passes it on to Warrior.load)?
#++
=end
module MARSLab
# -- Initializations -- These are "global" vars in the outer MARSLab scope that are
# accessible to all the classes and modules defined inside MARSLab
@@marsDirectory = File.join(File.dirname(__FILE__), '..', 'data', 'mars')
@@opcodes = ["DAT", "MOV", "ADD", "SUB", "JMP", "JMZ", "JMN", "DJN", "CMP", "SPL", "END", "SLT", "EQU"]
@@modes = "@<#"
@@maxEntries = 3
@@displayMemSize = 4096
@@params = {
:maxRounds => 1000,
:memSize => @@displayMemSize,
:buffer => 100,
:tracing => false,
:pause => 0.01,
}
MARSView = Struct.new(:cells, :palettes, :options)
@@viewOptions = {
:cellSize => 8,
:cellRows => 32,
:cellCols => 128,
:padding => 20,
:traceSize => 10,
:cellColor => '#DDDDDD',
}
@@drawing = nil
=begin rdoc
A MARSRuntimeException is raised when MARS tries to execute an illegal instruction,
e.g. when a program contains an instruction with the wrong type of addressing mode for
its opcode.
=end
class MARSRuntimeException < StandardError
end
=begin rdoc
A RedcodeSyntaxError is raised when a source program contains an instruction that cannot
be translated into MARS machine language, e.g. it has an unknown opcode or a missing operand.
=end
class RedcodeSyntaxError < StandardError
end
=begin rdoc
== Word
An object of the Word class represents a single item from memory, either a machine
instruction or a piece of data. Attributes are the opcode, the A operand mode, and
the B operand (all strings). Instruction execution proceeds according to the description
in Durham's spec.
=end
class Word
attr_reader :op, :lineno
attr_accessor :a, :b
# Create a new Word from +op+, +a+, +b+, and +n+.
# :call-seq:
# Word.new(op, a, b, n) => Word
#
def initialize(*args)
@op, @a, @b, @lineno = args
end
# Return a string representation of this word.
def inspect
sprintf "%s %s %s", @op, @a, @b
end
alias to_s inspect
# Two Words are the same if the strings representing the opcode and
# both operands are the same.
def ==(other)
return (op == other.op && a == other.a && b == other.b)
end
# Make a new Word object with all the same attributes as this object.
def clone
Word.new(@op.clone, @a.clone, @b.clone, @lineno)
end
# Convert a field specification into an integer value, stripping off a leading
# addressing mode character if it's there
def field_value(field)
return @@modes.include?(field[0]) ? field[1..-1].to_i : field.to_i
end
# Return the address of an operand; note that for immediate operands the
# address is the address of the current instruction.
def dereference(field, pc, mem)
case field[0]
when ?#
return pc.current[:addr]
when ?@
ptrloc = (field_value(field) + pc.current[:addr]) % mem.size
ptr = mem.fetch(ptrloc)
return (field_value(ptr.b) + ptrloc) % mem.size
when ?<
ptrloc = (field_value(field) + pc.current[:addr]) % mem.size
ptr = mem.fetch(ptrloc)
newb = field_value(ptr.b) - 1
mem.store_field(ptrloc, (newb % mem.size), :b)
return (newb + ptrloc) % mem.size
else
return (field_value(field) + pc.current[:addr]) % mem.size
end
end
# Execute the instruction represented by this word. The first parameter is the program counter used
# to fetch the instruction, the second is the machine's memory array.
#
# The semantics for each type of instruction are defined by private methods that have the same
# name as the opcode, e.g. then the opcode is "ADD" execute calls +ADD+, sending it the +pc+ and
# +mem+ arguments so the instruction can dereference its arguments and carry out the specified operation.
def execute(pc, mem)
self.send(@op, pc, mem)
return (@op == "DAT") ? (:halt) : (:continue)
end
private
# The DAT instruction is effectively a "halt", but we still need to dereference
# both its operands to generate the side effects in auto-decrement modes.
def DAT(pc, mem) # :doc:
dereference(@a, pc, mem)
dereference(@b, pc, mem)
end
# Durham isn't clear on how to handle immediate moves -- does the immediate value
# go in the A or B field of the destination? Guess: B, in case the destination
# is a DAT.
def MOV(pc, mem) # :doc:
raise MARSRuntimeException, "MOV: immediate B-field not allowed" if @b[0] == ?#
src = dereference(@a, pc, mem)
val = mem.fetch(src)
dest = dereference(@b, pc, mem)
if @a[0] == ?#
mem.store_field(dest, field_value(val.a), :b)
else
mem.store(dest, val)
end
pc.log(src)
pc.log(dest)
end
# Ambiguity on how to handle immediate A values: add operand to the A or B
# field of the destination? Guess: B (for same reasons given for MOV)
def ADD(pc, mem) # :doc:
raise MARSRuntimeException, "ADD: immediate B-field not allowed" if @b[0] == ?#
src = dereference(@a, pc, mem)
left_operand = mem.fetch(src)
dest = dereference(@b, pc, mem)
right_operand = mem.fetch(dest)
if @a[0] == ?#
mem.store_field(dest, field_value(left_operand.a) + field_value(right_operand.b), :b)
else
mem.store_field(dest, field_value(left_operand.a) + field_value(right_operand.a), :a)
mem.store_field(dest, field_value(left_operand.b) + field_value(right_operand.b), :b)
end
pc.log(src)
pc.log(dest)
end
# See note for ADD, re immediate A operand.
def SUB(pc, mem) # :doc:
raise MARSRuntimeException, "SUB: immediate B-field not allowed" if @b[0] == ?#
src = dereference(@a, pc, mem)
right_operand = mem.fetch(src)
dest = dereference(@b, pc, mem)
left_operand = mem.fetch(dest)
if @a[0] == ?#
mem.store_field(dest, field_value(left_operand.b) - field_value(right_operand.a), :b)
else
mem.store_field(dest, field_value(left_operand.a) - field_value(right_operand.a), :a)
mem.store_field(dest, field_value(left_operand.b) - field_value(right_operand.b), :b)
end
pc.log(src)
pc.log(dest)
end
# Durham doesn't mention this explicitly, but since a B operand is allowed it implies
# we have to dereference it in case it has a side effect.
def JMP(pc, mem) # :doc:
raise MARSRuntimeException, "JMP: immediate A-field not allowed" if @a[0] == ?#
target = dereference(@a, pc, mem) % mem.size
dereference(@b, pc, mem)
pc.branch(target)
end
# Branch to address specified by A if the B-field of the B operand is zero.
def JMZ(pc, mem) # :doc:
raise MARSRuntimeException, "JMZ: immediate A-field not allowed" if @a[0] == ?#
target = dereference(@a, pc, mem) % mem.size
operand = mem.fetch(dereference(@b, pc, mem))
if field_value(operand.b) == 0
pc.branch(target)
end
end
# As in JMZ, but branch if operand is non-zero
def JMN(pc, mem) # :doc:
raise MARSRuntimeException, "JMN: immediate A-field not allowed" if @a[0] == ?#
target = dereference(@a, pc, mem) % mem.size
operand = mem.fetch(dereference(@b, pc, mem))
if field_value(operand.b) != 0
pc.branch(target)
end
end
# DJN combines the auto-decrement mode dereference logic with a branch -- take
# the branch if the new value of the B field of the pointer is non-zero.
def DJN(pc, mem) # :doc:
raise MARSRuntimeException, "DJN: immediate A-field not allowed" if @a[0] == ?#
target = dereference(@a, pc, mem)
operand_addr = dereference(@b, pc, mem)
operand = mem.fetch(operand_addr)
newb = field_value(operand.b) - 1
mem.store_field(operand_addr, (newb % mem.size), :b)
if newb != 0
pc.branch(target)
end
pc.log(operand_addr)
end
# Durham just says "compare two fields" if the operand is immediate. Since
# B can't be immediate, we just need to look at the A operand and, presumably,
# compare it to the A operand of the dereferenced operand fetched by the B field.
# If A is not immediate compare two full Words -- including op codes.
# The call to pc.next increments the program counter for this thread, which causes
# the skip.
def CMP(pc, mem) # :doc:
raise MARSRuntimeException, "CMP: immediate B-field not allowed" if @b[0] == ?#
right = mem.fetch(dereference(@b, pc, mem))
if @a[0] == ?#
left = field_value(@a)
right = field_value(right.a)
else
left = mem.fetch(dereference(@a, pc, mem))
end
if left == right
pc.next
end
end
# More ambiguity here -- what does it mean for a word A to be "less than"
# word B? First assumption, don't compare opcodes. Second, we're just going
# to implement one-field comparisons of B fields. Otherwise for full-word operands
# we'd need to do a lexicographic compare of A and B fields of both operands, skipping
# modes and just comparing values.
def SLT(pc, mem) # :doc:
raise MARSRuntimeException, "SLT: immediate B-field not allowed" if @b[0] == ?#
if @a[0] == ?#
left = field_value(@a)
else
left = field_value(mem.fetch(dereference(@a, pc, mem)).b)
end
right = field_value(mem.fetch(dereference(@b, pc, mem)).b)
if left < right
pc.next
end
end
# Fork a new thread at the address specified by A. The new thread goes at the end
# of the queue. Immediate operands are not allowed. Durham doesn't mention it, but
# implies only A is dereferenced, so ignore B.
def SPL(pc, mem) # :doc:
raise MARSRuntimeException, "SPL: immediate A-field not allowed" if @a[0] == ?#
target = dereference(@a, pc, mem)
pc.add_thread(target)
end
end # class Word
=begin rdoc
== Warrior
A Warrior is a core warrior -- an object of this class is a simple struct that
has the program name, assembled code, the starting location, and the symbol table.
A static method (Warrior.load) will load an assembled Warrior into memory, making
sure the max number of programs is not exceeded. The addr attribute is set to the
starting location of the program when it is loaded.
=end
class Warrior
attr_reader :name, :code, :symbols, :errors
# Create a new Warrior by reading and assembling the instructions in +filename+.
# The +code+ array is empty if there are any assembly errors.
def initialize(filename)
if filename.class == Symbol
filename = File.join(@@marsDirectory, filename.to_s + ".txt")
end
if ! File.exists?(filename)
raise "Can't find file: #{filename}"
end
@name, @code, @symbols, @errors = MARS.assemble( File.open(filename).readlines )
if @errors.length > 0
puts "Syntax errors in #{filename}:"
puts errors
@code.clear
end
end
# Create a string with essential information about this object.
def inspect
sprintf "Name: #{@name} Code: #{@code.inspect}"
end
alias to_s inspect
# Load the code for the Warrior object reference by +app+ into the MARS main memory.
# If an address is specified, load that code starting at that location, otherwise
# choose a random location. The memory addresses used by the program are recorded
# so programs aren't accidentally loaded on top of each other.
def Warrior.load(app, addr = :random)
if app.class != Warrior
puts "Argument must be a Warrior object, not a #{app.class}"
return nil
end
if @@entries.length == @@maxEntries
puts "Maximum #{@@maxEntries} programs can be loaded at one time"
return nil
end
if addr == :random
loop do
addr = rand(@@mem.size)
break if Warrior.check_loc(addr, addr + app.code.length - 1)
end
else
if ! Warrior.check_loc(addr, addr + app.code.length - 1)
puts "Address #{addr} too close to another program; #{app.name} not loaded"
return nil
end
end
loc = addr
app.code.each do |x|
@@mem.store(loc, x)
loc = (loc + 1) % @@mem.size
end
id = @@entries.length
@@pcs << PC.new(id, addr + app.symbols[:start])
@@entries << app
# save the range of memory locations reserved by this app
lb = addr - @@params[:buffer]
ub = addr + app.code.length + @@params[:buffer] - 1
if lb < 0
@@in_use << ( 0 .. (addr+app.code.length-1) )
@@in_use << ( (@@params[:memSize] + lb) .. (@@params[:memSize]-1) )
elsif ub > @@params[:memSize]
@@in_use << ( addr .. (@@params[:memSize]-1) )
@@in_use << ( 0 .. (ub - @@params[:memSize]) )
else
@@in_use << (lb..ub)
end
return addr
end
@@in_use = Array.new
# Clear the data structure that records which memory locations are in use, allowing
# new programs to be loaded into any location.
def Warrior.reset
@@in_use = Array.new
end
private
# Return true if a program loaded between lb and ub would not overlap another
# program already loaded (including a buffer surrounding loaded programs).
def Warrior.check_loc(lb, ub) # :doc:
@@in_use.each do |range|
return false if range.include?(lb) || range.include?(ub)
end
return true
end
end # class Warrior
=begin rdoc
== PC
The PC (program counter) class is used to keep track of the next instruction to execute when a program is running.
A PC object has an array of locations to hold the next instruction from each thread, plus the
index of the thread to use on the next instruction fetch cycle.
=end
class PC
attr_reader :id, :thread, :addrs, :current
@@hmax = 10 # see also @@threadMax in Draw -- allowed to be a different value
# Create a new program counter. The +id+ argument is a tag that can be used to
# identify which program is running at this location. The PC is intialized with
# one thread starting at location +addr+.
def initialize(id, addr)
@id = id
@addrs = [addr]
@history = [ Array.new ]
@thread = 0
@current = {:thread => nil, :addr => nil}
@first = addr
end
# Restore this program counter to its original state, a single thread starting
# at the location passed when the object was created.
def reset
@addrs = [@first]
@history.clear
@thread = 0
@current = {:thread => nil, :addr => nil}
return @first
end
# Create a string showing the name of the program and the current instruction
# for each thread.
def inspect
s = "[ "
@addrs.each_with_index do |x,i|
s << "*" if i == @thread
s << x.to_s
s << " "
end
s << "]"
end
alias to_s inspect
# Add a new thread, which will begin execution at location +addr+.
#--
# Not sure if this is right -- new thread appended to list, as per Durham, but
# the execution stays with the current thread (thread switch happens in 'next' method)
def add_thread(addr)
@addrs << addr
@history << Array.new
self
end
# Return the address of the next instruction to execute for this program.
# If more than one thread is active, make the next thread the current thread.
def next
return nil if @addrs.empty?
addr = @addrs[@thread]
@current[:thread] = @thread
@current[:addr] = addr
@addrs[@thread] = (@addrs[@thread] + 1) % @@mem.size
@thread = (@thread + 1) % @addrs.size
log(addr)
return addr
end
# Implement a branch instruction by setting the next instruction address for the
# current thread to +loc+.
def branch(loc)
@addrs[@current[:thread]] = loc
end
# Remove the current thread from the list of active threads. The return value
# is the number of remaining threads.
def kill_thread
return 0 if @addrs.empty?
@addrs.slice!(@current[:thread])
@history.slice!(@current[:thread])
@thread -= 1
return @addrs.length
end
# Record the location of a memory operation in the history vector for the current thread.
# The history vector is used by the methods that display the progress of a program on
# the RubyLabs canvas.
def log(loc)
a = @history[@current[:thread]]
a << loc
a.shift if a.length > @@hmax
end
# Return a reference to the set of history vectors for this Warrior object.
def history
return @history
end
end # class PC
=begin rdoc
== Memory
An object of the Memory class is a 1-D array of the specified size. Items in a Memory
are Word objects.
According to the Core War standard, memory should be initialized with DAT #0 instructions
before each contest. For efficiency, newly (re)initialized Memory objects have +nil+
at each location, but +nil+ is treated the same as DAT #0.
=end
class Memory
attr_reader :array
# Create a new Memory of the specified size.
def initialize(size)
@array = Array.new(size)
end
# Create a string describing this Memory object.
def to_s
sprintf "Memory [0..#{@array.size-1}]"
end
# Print the +n+ words in memory starting at +loc+.
def dump(loc, n)
(loc...(loc+n)).each do |x|
addr = x % @array.size
printf "%04d: %s\n", addr, self.fetch(addr)
end
end
# Return the size (number of words that can be stored) of this Memory object.
def size
return @array.size
end
# Return the Word object stored in location +loc+ in this Memory object.
def fetch(loc)
return ( @array[loc] || Word.new("DAT", "#0", "#0", nil) )
end
# Store object +val+ in location +loc+.
def store(loc, val)
@array[loc] = val.clone
end
# Same as +fetch+, but return only the designated field of the Word stored at location +loc+.
def fetch_field(loc, field)
instr = self.fetch(loc)
return field == :a ? instr.a : instr.b
end
# Same as +store+, but overwrite only the designated field of the Word in location +loc+.
def store_field(loc, val, field)
instr = self.fetch(loc)
part = (field == :a) ? instr.a : instr.b
mode = @@modes.include?(part[0]) ? part[0].chr : ""
part.replace(mode + val.to_s)
self.store(loc, instr)
end
end # class Memory
=begin rdoc
== MiniMARS
A miniature machine (MiniMARS) object is used to test a Redcode program. It is essentially
a MARS VM connected to a "thumb drive" that contains the assembled code for a single program.
By single-stepping through the program users can learn how the code works or debug a
program they are developing.
=end
class MiniMARS
attr_reader :mem, :pc, :state
# Create a VM that has a memory with the assembled form of the Redcode program in +file+.
# has been loaded into location 0. The optional second argument defines the size
# of the memory (otherwise the memory is just big enough to hold the program).
def initialize(file, size = nil)
w = Warrior.new(file)
@mem = size ? Memory.new(size) : Memory.new(w.code.length)
loc = 0
w.code.each do |x|
@mem.store(loc, x)
loc = loc + 1
end
@pc = PC.new(w.name, w.symbols[:start])
@state = :ready
end
# Create a string with a short description of this VM.
def inspect
return "#"
end
alias to_s inspect
# Print a string that describes the program status (running or halted, current
# instruction address, ...)
def status
s = "Run: #{@state}"
s += " PC: #{@pc}" unless @state == :halt
puts s
end
# Execute the next instruction in the program loaded into this VM. The
# return value is the Word object for the instruction just executed.
def step
return "machine halted" if @state == :halt
instr = @mem.fetch(@pc.next)
@state = instr.execute(@pc, @mem)
return instr
end
# Execute instructions in the program loaded into this VM until it hits
# a HALT (DAT) instruction. The return value is the number of instructions
# executed. The optional argument is a maximum number of steps
# to execute; afer executing this number of instructions the method will
# return, whether or not the program halts.
def run(nsteps = 1000)
count = 0
loop do
break if @state == :halt || nsteps <= 0
self.step
nsteps -= 1
count += 1
end
return count
end
# Reset the program counter to 0, so the program starts over. But the
# contents of memory are not restored, they are left as they were after the
# last instruction executed.
def reset
@pc.reset
puts "warning: memory may not be in initial state"
end
# Print the contents of the VM memory. If two arguments are passed they
# are used as the addresses of the first and last words to print.
def dump(*args)
if args.empty?
@mem.dump(0, @mem.array.length)
else
x = args[0]
y = args[1] || x
@mem.dump(x, y-x+1)
end
return nil
end
end # MiniMARS
=begin rdoc
== MARS
The MARS class defines a singleton object that has the methods used to assemble, load, and execute
up to three Redcode programs at a time.
=end
class MARS
# -------- Assembler ----------
# line format:
#