Abstract
Some time ago I created a simple programming language that was compiled to its own assembly language, a file in this assembly language could be executed in a Virtual Machine or be compiled to an executable form. Today I will present an idea how to use Domain Specific Language (DSL) created in Ruby to load and execute the assembly file just as a regular Ruby script.
Implementation
A sample of an assembly file is presented below, it's intuitive except the "int" mnemonic, it means "interruption", int 0 pops element from the stack and prints it on STDOUT. There aren't any registers, everything is done by using a stack.
bar:
int 0
ret
foo:
call bar
ret
main:
push 13
call foo
ret
Some of the syntax isn't valid in Ruby so before the assembly file is loaded, it's parsed by a small preprocessor (DSLPreprocesor class). Parsed assembly file is presented below:
@bar = Proc.new{
int 0
}
@foo = Proc.new{
call @bar
}
@main = Proc.new{
push 13
call @foo
}
One of cool things in the syntax of Ruby is the possibility to invoke method without parenthesis, it was also used here (e.g. to it enables constructions like "push 0" to be executed directly by Ruby interpreter).
Below is presented a complete source code of the mentioned virtual machine, it can be also cloned from GitHub (DSLInRuby directory).
#!/usr/bin/env ruby
class DSLVirtualMachine
def initialize(file_name = String.new)
@preprocesor = DSLPreprocesor.new file_name
@stack = Array.new
@memmory = Array.new
@stack_ptr = [0]
@memmory_ptr = [0]
end
def run()
eval @preprocesor.parse()
@main.call()
end
private
def alu(operation)
parser = { 'add' => lambda { @stack.pop + @stack.pop },
'sub' => lambda { @stack.pop - @stack.pop },
'div' => lambda { @stack.pop / @stack.pop },
'mul' => lambda { @stack.pop * @stack.pop },
'gt' => lambda { @stack.pop > @stack.pop }, }
push parser[operation].call()
end
def call(function)
@memmory_ptr.push @memmory.length
function.call()
@memmory_ptr.pop
end
def push(element)
@stack.push element
end
def load(offset)
memmory_ptr = @memmory_ptr.last
@stack.push @memmory[offset+memmory_ptr]
end
def store(offset)
memmory_ptr = @memmory_ptr.last
@memmory[offset+memmory_ptr] = @stack.pop
end
def get_if_condition()
return @stack.pop
end
def int(interrupt)
parser = [ lambda { p @stack.pop },
lambda { push gets.to_i } ]
parser[interrupt].call()
end
end
class DSLPreprocesor
attr_accessor :fname
def initialize(fname = String.new)
@fname = fname
@parser = {
# remove comments
/^([^;]*);.*$/ =>
lambda { |u| "#{u}" },
/^[\s]*jz ([a-z0-9]*)$/ =>
lambda { |u| "if (get_if_condition)" },
/^a[0-9]:*$/ =>
lambda { |u| "end" },
/^([a-z]*[0-9]*):$/ =>
lambda { |u| "@#{u} = Proc.new{" },
/^[\s]*ret$/ =>
lambda { |u| "}" },
/^[\s]*call ([a-z0-9]*)$/ =>
lambda { |u| "call @#{u}" },
/^[\s]*(add|sub|mul|div|gt|lt|eq)$/ =>
lambda { |u| "alu '#{u}'" },
}
end
def parse()
file = File.readlines(self.fname)
return file.map { |l| parse_mnemonic(l) }.join('')
end
private
def parse_mnemonic(mnemonic)
@parser.map { |k,v| mnemonic.gsub!(k){ v.call($1) } }
return mnemonic
end
end
vm = DSLVirtualMachine.new ARGV[0]
vm.run()
Usage presented on mentioned above assembly file:
bash-3.2$ ruby vm.rb sample.asm 13 bash-3.2$
Results
Originally, I made in Python a regular Virtual Machine for this assembly language, later czarodziej made his own version in C. Version in Python has ~250LOC, in C ~600LOC, implementation in Ruby (+DSL) has ~110LOC. I think that it' a really good result. It could be even smaller, if the assembly language would be designed from the beginning to be executed in Virtual Machine
Ruby is great for making a DSL, we use it in our company for this task too.
ReplyDeleteWhat was that DSL?
Delete