On 8/17/07, Ruby Quiz <james / grayproductions.net> wrote:
> ...
> I'll leave the definition of "processes" intentionally vague.  Ruby doesn't have
> an equivalent to Erlang processes so we will just say that each process should
> represent a node where we could run some instructions concurrently.  Be
> creative.
>
I've been thinking about virtual machines recently, so I decided to
implement one in Ruby for this quiz.

My solution has 4 parts:
- a pair of programs in an as yet unnamed language.  Each program
sends a message to the next process. One program implements a counter
to stop the loop at the end.

- A Compiler for this language.  To keep the compiler simple, the
parser has no look-ahead.  One result of this is that all operators
have left associativity, so
'n=n+1', 'n=1+n', and 'n=(n+1)' all set n to different values, and
only the last one is unsuprising.   The compiler returns an array
containing 'assembly',  which is essentially bytecode, except the
codes are ruby symbols, not bytes.

- The InstructionSet, which contains a method for each VM instruction

- The virtual CPU.  Schedules and runs a set of Proceses.  A Process
executes assembly by sending each symbol to the instruction set.

This solution may be the slowest one submitted, but it was interesting to write.
-Adam

---BEGIN SOLUTION---
#processring.rb
# for Ruby Quiz #135
# Adam Shelly

#Implements a virtual machine and a compiler for a simple language
# language definition:
# types: ints, strings
# variables don't need to be declared in advance (works like ruby)
# only 3 keywords: 'exit', 'if' and 'while',
#   the latter two take the form 'keyword (condition) { body }'.
#   the parens and brackets are required
# only 2 operators: '+' and '-'.
# 4 builtin functions: '_peek' returns true if any messages waiting
for this proceses
#                             '_get' returns first pending message.
#                             '_send(id, message)' sends message to
process with given id
#                             '_puts(message)' writes message to stdout
# '%' before a name indicates process variable.
#    process variables include:  %id = current process id
#                                         %last = value of last expression
# strictly left associative, use parentheses to group.
#    be careful with assignments:  'n = 1+1' ==  '(n=1)+1'
#              you usually want to do 'n = (1+1)'

# Here are the two programs we will execute.
# this one just forwards any message to the next process
prog1 = <<PROG
while (0) { _get }
while (1) {
  if (_peek) {
    msg =_get
    _send ((%id+1),msg)
  }
}
PROG

# This one generates a message and sends it to process 0, n times.
# It will be the last process so we can close the ring.
prog2 = <<PROG
n = _get
_send (0,"chunky bacon")
while (n ) {
  if (_peek) {
    msg = _get
    _send (0,msg)
    n = ( n - 1)
    _puts ( n )
  }
}
_puts ( "done" )
exit
PROG

# The Compiler turns program text into "assembly"
class Compiler
    @@symbols = {}
    #register keywords
    %w{while if end exit}.each{|kw| @@symbols[kw]=kw.to_sym}
    #register builtins
    %w{_peek _get _send _puts}.each{|bi| @@symbols[bi] = :builtin}

  def self.compile code
    asm = []
    text = code
    text = text.dup #don't destroy original code
    token,name = parse text
    while (token)
      p token if $DEBUG
      case token
        when :while,:if,:exit,:end,:add,:subtract,:assign,:comma
          asm << token
        when :localvar,:procvar,:builtin,:num,:string
          asm << token
          asm << name
        when :startgroup,:startblock
          asm << token
          asm << 0                        #placeholder for size of group/block
        when :endgroup
          startgroup = asm.rindex(:startgroup)
          asm[startgroup] = :group
          asm[startgroup+1] = asm.size-startgroup-2  #store groupsize
        when :endblock
          startblock = asm.rindex(:startblock)
          asm[startblock] = :block
          asm[startblock+1] = asm.size-startblock  #store blocksize
          asm << :endblock
          asm << asm.size+1 #placeholder for looptarget (default is next inst.)
      end
      token,name = parse text
    end
    return asm
  end

private
  def self.parse text, vartype = :localvar
    pt = 0;
    p "parse: #{text}" if $DEBUG
    while (true)
      case (c = text[pt,1])
      when ''          #EOF
        return nil

      when /\s/         #skip whitespace
        pt+=1
        next

      when /\d/         #integers
        v = text[pt..-1].to_i
        text.slice!(0..pt+v.to_s.length-1) #remove number
        return :num,v

      when /\w/        #identifiers
        name = /\w*/.match(text[pt..-1])[0]
        text.slice!(0..pt+name.length-1) #remove name
        sym = @@symbols[name]
        sym = register_var(name,vartype) if !sym #unknown identifier is variable
        return sym,name

      when '"'       #strings
        name = /".*?[^\\]"/m.match(text[pt..-1])[0]
        text.slice!(0..pt+name.length-1) #remove name
        return :string, name

      when '%'       #processes variables
        text.slice!(0..pt)
        token,name = parse text,:procvar
        raise "invalid process variable" if token!= :procvar
        return token,name

      when '=':    #punctuation
        text.slice!(0..pt)
        return :assign, c
      when ',':
        text.slice!(0..pt)
        return :comma,c
      when '+'
          text.slice!(0..pt)
          return :add,'+'
      when '-'
          text.slice!(0..pt)
          return :subtract,'-'
      when '('
          text.slice!(0..pt)
          return :startgroup, c
      when ')'
          text.slice!(0..pt)
          return :endgroup, c
      when '{'
          text.slice!(0..pt)
          return :startblock, c
      when '}'
          text.slice!(0..pt)
          return :endblock, c
      end #case
    end #while
  end

  def self.register_var name,type
    @@symbols[name] = type
  end
end


#The cpu instruction set.
#each instruction is the equivalent of a VM bytecode.
class InstructionSet
  def initialize  cpu
    @cpu = cpu
  end

  def exit proc       #halt the cpu
    @cpu.halt
  end
  def end proc    #end the current process
    @cpu.end_process proc.id
  end

  def while proc
    loopp = proc.pc-1
    test = proc.exec
    blocksize = proc.exec
    if test && test != 0
      #if we are going to loop, store the loop start address at the
end of the block
      proc.pm[proc.pc+blocksize-1] = loopp
    else
      proc.pc += blocksize
    end
  end
  def if proc
    test = proc.exec
    blocksize = proc.exec
    if !test || test == 0
      proc.pc += blocksize
    end
  end

  def block proc
    blocksize = proc.pop
  end
  def endblock proc
    jumptarg = proc.pop   #after block, maybe jump somewhere
    proc.pc = jumptarg
  end

  def group proc
    groupsize = proc.pop
    endgroup = proc.pc+groupsize
    while (proc.pc < endgroup)
      val = proc.exec
    end
    return val
  end

  def num proc
    proc.pop
  end
  def string proc
    proc.pop
  end
  def builtin proc
    inst = proc.pop
    @cpu.send(inst,proc)
  end
  def localvar proc
    varname = proc.pop
    proc.getvar varname
  end
  def procvar proc
    varname = proc.pop
    proc.send varname
  end
  def assign proc
    proc.setvar(proc.exec)
  end
  def comma proc
    return :comma
  end
  def add proc
    return proc.last + proc.exec
  end
  def subtract proc
    return proc.last - proc.exec
  end

  #returns elements of group as array
  #used to evaluate arguments for function call
  def ungroup proc
    args = []
    proc.pop #ignore :group
    groupsize = proc.pop
    endgroup = proc.pc+groupsize
    while (proc.pc < endgroup)
      arg = proc.exec
      args << arg unless arg == :comma
    end
    return args
  end
end


#the CPU
# acts as process scheduler
# processes run for TIMESLICE instructions, or until they send or get a message.
# in the latter case, control switches to the process with a message
pending for the longest time
class CPU
  TIMESLICE = 10

  # CProcess is a process on our virtual machine
  # don't create directly, use CPU#add_process
  class CProcess
    attr_accessor :pm,:pc,:id,:last
    def initialize id, code, vm
      @id = id
      @pm = code    #program memory
      @pc = 0         #program counter
      @vars = {}
      @curvar = nil
      @vm = vm
    end

    #executes a VM instruction, advances program counter
    def exec
      inst = @pm[@pc]
      p to_s if $DEBUG
      @pc+=1
      @last = @vm.send(inst,self)
    end
    def pop
      @pc+=1
      @pm[@pc-1]
    end

    def getvar name
      @curvar = name
      @vars[name]||=0
    end
    def setvar value
      @vars[@curvar] = value
    end

    def to_s
      "#{@id}@#{@pc}: #{@pm[@pc]} (#{@pm[@pc+1]})"
    end
  end #class Process

  def initialize
    @processes = []
    @messages = []
    @i = InstructionSet.new self
    @queue=[[],[]]  #scheduling queues
  end

  def add_process code
    asm = code.dup
    asm << :end
    id = @processes.size
    @processes << CProcess.new(id, asm,@i)
    @messages[id] = []
    @queue[0] << id
    @cur_proc_id = id
  end

  #stop processes by swapping it out if it is running, and removing it
from queues.
  def end_process id
    taskswap 0 if @cur_proc_id == id
    @processes -= [id]
    @queue[0] -= [id]
    @queue[1] -= [id]
  end

  def start
    @running = true
    run
  end
  def halt
    @running = false
  end

  #inject a message into the system
  def send_msg proc_id,msg
    @messages[proc_id]<< msg
    @queue[1]<<proc_id
  end

private
  #run the scheduler
  def run
    @timeslice = 0
    while (@running)
      @processes[@cur_proc_id].exec
      @timeslice+=1
      if (@timeslice > TIMESLICE)
        taskswap 0
      end
    end
  end

  #switch to the next process waiting at this priority level
  def taskswap priority
    @cur_proc_id = @queue[priority].shift||@cur_proc_id
    (@queue[priority] << @cur_proc_id) if priority == 0
    @timeslice = 0
  end

  ## built-in messaging functions
  def _peek proc
    @messages[proc.id][0]
  end
  def _get proc
    retval = @messages[proc.id].shift
    taskswap 1
    return retval
  end
  def _send proc
    #send puts the target process on the high priority queue
    args = @i.ungroup proc
    @messages[args[0]] << args[1]
    @queue[1]<<args[0]
    taskswap 1
    args[1]
  end

  def _puts proc
    args = @i.ungroup proc
    puts args
  end
end

if __FILE__ == $0
puts "usage: #{$0} processes cycles" or exit if ARGV.size < 2
processes, cycles = ARGV.map { |n| n.to_i }

puts "Timer started."
start_time = Time.now
puts "Creating #{processes} processes"

code1 = Compiler.compile prog1
code2 = Compiler.compile prog2
cpu = CPU.new
(processes-1).times { cpu.add_process code1 }
last_proc = cpu.add_process code2

puts "Sending a message around the ring #{cycles} times..."
cpu.send_msg last_proc,cycles
cpu.start
puts "Time in seconds:  #{(Time.now - start_time)}"
end