Search code examples
rubygoto

How to implement goto and label methods in Ruby?


As a homework on the topic DSL, I need to write an inline assembler in Ruby. I am aware of The Joke Is On Us: How Ruby 1.9 Supports the Goto Statement, but I must not use it. It is a very simple implementation with the assembler having four registers - ax, bx, cx, dx, holding integer values, on which I can do some operations such as setting their value (mov), comparing two registers (cmp), incrementing a register (inc), jumping to a particular place (jmp) and a few others of the sorts. The interface will be something like this:

Asm.asm do
  mov cx, 1
  jmp l1
  mov ax, 1
  label l1
  mov dx, 1
end

The jmp method will accept either a label name or the sequential number of one of the other functions. So my question is: in the block:

{
mov cx, 1
jmp l1
mov ax, 1
label l1
mov dx, 1
}

how do I keep track of the current number of a function. My implementation looks roughly like this:

module Asm
  def self.asm(&block)
    memory = Memory.new
    memory.instance_eval(&block)
    memory.table.values
  end

  class Memory
    attr_reader :table

    def initialize
      @table = { ax: 0, bx: 0, cx: 0, dx: 0 }
      ...
    end

    ...

    def mov(destination_register, source)
      ...
    end

    def inc(destination_register, value = 1)
      ...
    end

    def dec(destination_register, value = 1)
      ...
    end

    ...
  end
end

I am stuck in implementing the jmp aka goto method. One idea I had was to use a hash holding all called methods and their arguments or to loop the block, containing the instructions and execute or not execute methods based on conditions saved in global variables, but I couldn't do much with that. So, for example, is there a way to break the block and save each instruction in an array/hash and then execute it based on its index or something similar. Any help is appreciated. Thank you very much in advance.


Solution

  • Here's an idea: pre "parse" your assembly code and then execute it. This consists of changing a few things. Here would be my implementation of the Memory class:

    class Memory
      attr_reader :table
      def initialize
        @table = { ax: 0, bx: 0, cx: 0, dx: 0 }
        @program = []
        @labels = {}
      end
      OPS = {
        "mov" => lambda {|dest, src| @table[dest] = (src.is_a?(Symbol) ? @table[src] : src); nil},
        "inc" => lambda {|dest, incval = 1| @table[dest] += incval; nil},
        "jmp" => lambda {|lblname| @labels[lblname]}
        #and so on
      }
      def method_missing(name, *args)
        if(OPS.keys.include?(name.to_s)) 
          @program << [name.to_s, args]
        elsif(name.to_s == "label")
          @labels[args.first] = @program.length
        else
          return name
        end
      end
      def load_program(&block)
        self.instance_exec(&block)
      end
      def run_program
        pc = 0
        until(pc == @program.length)
          instruction = @program[pc]
          retval = self.instance_exec(*instruction[1], &OPS[instruction[0]])
          if(retval)
            pc = retval
          else
            pc += 1
          end
        end
      end
      def asm(&block)
        load_program(&block)
        run_program
        @table
      end
    end
    

    Let's go over this step by step. Instead of having a method for each instruction, I use a hash of lambdas. Then, I use method_missing to do three things:

    1. If the method name is just a random symbol (not an instruction name), I just return the symbol. So now, cx is equivilent to :cx.
    2. If it's an instruction name, add it and the arguments to the program array
    3. If it's a label, add the index of the next instruction to the labels hash under that name.

    The return value (if not nil) of an instruction lambda is used as the new value of the program counter (so that more jumps like jump if zero can be added).