├── .gitignore ├── asm ├── noop.asm ├── includeme.asm ├── includer.asm ├── jsr.asm ├── fib.asm ├── prime.asm └── fact.asm ├── Gemfile ├── Rakefile ├── run.rb ├── benchmarks └── benchmark.rb ├── Gemfile.lock ├── spec ├── spec_helper.rb └── components │ ├── virtual_machine_spec.rb │ └── compiler_spec.rb ├── lib ├── action.rb ├── byte_code.rb ├── parser.rb ├── compiler.rb ├── actions.rb ├── node_extensions.rb ├── virtual_machine.rb └── assembler.treetop ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /asm/noop.asm: -------------------------------------------------------------------------------- 1 | mov eax, 0 2 | 3 | loop: 4 | nop 5 | inc eax 6 | cmp eax, 1000000 7 | jl loop -------------------------------------------------------------------------------- /asm/includeme.asm: -------------------------------------------------------------------------------- 1 | start_include: 2 | mov [1], 1 3 | prn [1] 4 | ret 5 | 6 | end_include: 7 | jmp end_include 8 | -------------------------------------------------------------------------------- /asm/includer.asm: -------------------------------------------------------------------------------- 1 | %include 'asm/includeme.asm' 2 | 3 | start: 4 | call start_include 5 | mov [1], 0 6 | prn [1] 7 | eof: 8 | jmp eof 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'rake' 4 | gem 'treetop' 5 | 6 | 7 | 8 | group :test do 9 | gem "minitest" 10 | gem 'mocha', '0.11.3', require: false 11 | end 12 | -------------------------------------------------------------------------------- /asm/jsr.asm: -------------------------------------------------------------------------------- 1 | print_eax: 2 | push ebp 3 | mov ebp, esp 4 | prn eax 5 | pop ebp 6 | ret 7 | 8 | start: 9 | mov eax, 42 10 | call print_eax 11 | 12 | mov eax, 23 13 | call print_eax -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | require 'rake/testtask' 3 | 4 | Bundler.setup 5 | Bundler.require(:test) 6 | 7 | 8 | Rake::TestTask.new do |t| 9 | t.libs.push "lib", "spec" 10 | t.test_files = FileList['spec/**/*_spec.rb'] 11 | t.verbose = true 12 | end -------------------------------------------------------------------------------- /run.rb: -------------------------------------------------------------------------------- 1 | require './lib/virtual_machine.rb' 2 | require './lib/compiler.rb' 3 | 4 | if ARGV.size < 1 5 | puts "Usage: #{$0} filename" 6 | exit(1) 7 | end 8 | 9 | code = Compiler.compile(File.read(ARGV[0])) 10 | vm = VirtualMachine.new(code) 11 | vm.run 12 | 13 | 14 | -------------------------------------------------------------------------------- /asm/fib.asm: -------------------------------------------------------------------------------- 1 | start: 2 | mov eax, 1 3 | mov ebx, 0 4 | mov ecx, 1 5 | 6 | loop: add eax, ebx 7 | add ebx, eax 8 | 9 | prn eax 10 | prn ebx 11 | 12 | inc ecx 13 | cmp ecx, 15 14 | je end 15 | 16 | cmp eax, 0 17 | jl end 18 | 19 | cmp ebx, 0 20 | jg loop 21 | 22 | 23 | end: 24 | -------------------------------------------------------------------------------- /benchmarks/benchmark.rb: -------------------------------------------------------------------------------- 1 | if ARGV[0].nil? then puts "Usage: ruby benchmark.rb "; exit 1 end 2 | 3 | require 'benchmark' 4 | require './lib/virtual_machine.rb' 5 | require './lib/compiler.rb' 6 | 7 | puts "Compiling..." 8 | puts Benchmark.measure { @code = Compiler.compile(File.read(ARGV[0])) } 9 | 10 | @vm = VirtualMachine.new( @code ) 11 | 12 | puts "Running..." 13 | puts Benchmark.measure { @vm.run } 14 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | metaclass (0.0.1) 5 | minitest (2.11.4) 6 | mocha (0.11.3) 7 | metaclass (~> 0.0.1) 8 | polyglot (0.3.3) 9 | rake (13.0.3) 10 | treetop (1.4.10) 11 | polyglot 12 | polyglot (>= 0.3.1) 13 | 14 | PLATFORMS 15 | ruby 16 | 17 | DEPENDENCIES 18 | minitest 19 | mocha (= 0.11.3) 20 | rake 21 | treetop 22 | 23 | BUNDLED WITH 24 | 1.17.2 25 | -------------------------------------------------------------------------------- /asm/prime.asm: -------------------------------------------------------------------------------- 1 | # Simplistic prime-finding algorithm 2 | 3 | start: mov eax, 2 # EAX is prime candidate 4 | 5 | checkPrime: mov ebx, 2 # EBX is factor candidate 6 | 7 | checkFactor: cmp eax, ebx 8 | je primeFound 9 | 10 | mod eax, ebx 11 | rem ecx 12 | cmp ecx, 0 13 | je nextPrime 14 | 15 | inc ebx 16 | jmp checkFactor # test comment 17 | 18 | primeFound: prn eax 19 | 20 | nextPrime: inc eax 21 | cmp eax, 100 22 | jl checkPrime 23 | -------------------------------------------------------------------------------- /asm/fact.asm: -------------------------------------------------------------------------------- 1 | # recursive factorial subroutine 2 | 3 | fact: 4 | push eax # save caller's EAX, ECX 5 | push ecx 6 | push ebp # call mechanism 7 | mov ebp, esp 8 | mov ebx, 1 # default value = 1 9 | cmp eax, 1 # n > 1 ? 10 | jle end_fact # no; leave with default = 1 11 | mov ecx, eax # yes; value = n*fact(n-1) 12 | dec eax 13 | call fact 14 | mul ebx, ecx 15 | end_fact: 16 | pop ebp # restore everything; leave 17 | pop ecx 18 | pop eax 19 | ret 20 | 21 | # print n! for 0 1 50 | 51 | if line[1].is_a?(Assembler::Offset) 52 | line[1] = line[1].value 53 | line[0] |= ByteCode.op_dest_offset 54 | else 55 | if line[1].is_a?(Assembler::Label) 56 | label = line[1].value 57 | label_uses[label] ||= [] 58 | label_uses[label] << (offset+1) 59 | line[1] = 0 60 | end 61 | 62 | line[0] |= ByteCode.op_dest_value 63 | end 64 | 65 | # Second parameter 66 | if line.size > 2 67 | if line[2].is_a?(Assembler::Offset) 68 | line[0] |= ByteCode.op_source_offset 69 | line[2] = line[2].value 70 | else 71 | line[0] |= ByteCode.op_source_value 72 | end 73 | end 74 | 75 | end 76 | buffer.concat(line) 77 | 78 | end 79 | 80 | # Second pass: Replace labels 81 | label_uses.each do |label, uses| 82 | offset = label_offsets[label] 83 | raise CompilerException, "Label #{label} was not defined" if offset.nil? 84 | uses.each do |idx| 85 | buffer[idx] = offset 86 | end 87 | end 88 | 89 | buffer.pack('V*') 90 | end 91 | 92 | end 93 | -------------------------------------------------------------------------------- /lib/actions.rb: -------------------------------------------------------------------------------- 1 | class VirtualMachine 2 | 3 | ACTIONS = { 4 | 5 | :mov => lambda { |vm| vm.src_to_dest {|dest, src| src} }, 6 | 7 | :cmp => lambda do |vm| 8 | vm.memory[ ByteCode.flags_location] = 0 9 | result = vm.dest_value - vm.src_value 10 | if (result == 0) 11 | vm.zf = true 12 | elsif (result < 0) 13 | vm.sf = true 14 | end 15 | vm.eip += 3 16 | end, 17 | 18 | :nop => lambda { |vm| vm.eip += 1}, 19 | 20 | :jmp => lambda { |vm| vm.eip = vm.dest_value }, 21 | 22 | :mod => lambda do |vm| 23 | vm.rem = vm.dest_value.to_i % vm.src_value.to_i 24 | vm.eip += 3 25 | end, 26 | 27 | :rem => lambda { |vm| vm.change_dest {vm.rem} }, 28 | :inc => lambda { |vm| vm.change_dest { |d| d+1 } } , 29 | :dec => lambda { |vm| vm.change_dest { |d| d-1 } } , 30 | :not => lambda { |vm| vm.change_dest { |d| ~d } } , 31 | :add => lambda { |vm| vm.src_to_dest { |d, s| d+s } }, 32 | :sub => lambda { |vm| vm.src_to_dest { |d, s| d-s } }, 33 | :mul => lambda { |vm| vm.src_to_dest { |d, s| d*s } }, 34 | :shl => lambda { |vm| vm.src_to_dest { |d, s| d< lambda { |vm| vm.src_to_dest { |d, s| d>>s } }, 36 | 37 | :div => lambda do |vm| 38 | vm.src_to_dest do |dest, src| 39 | raise DivideByZero.new if src == 0 40 | dest / src 41 | end 42 | end, 43 | 44 | :and => lambda { |vm| vm.src_to_dest { |d, s| d & s } }, 45 | :or => lambda { |vm| vm.src_to_dest { |d, s| d | s } }, 46 | :xor => lambda { |vm| vm.src_to_dest { |d, s| d ^ s } }, 47 | 48 | :je => lambda do |vm| 49 | vm.eip = vm.zf ? vm.dest_value : vm.eip + 2 50 | end, 51 | 52 | :jne => lambda do |vm| 53 | vm.eip = !vm.zf ? vm.dest_value : vm.eip + 2 54 | end, 55 | 56 | :jl => lambda do |vm| 57 | vm.eip = (vm.sf != vm.of) ? vm.dest_value : vm.eip + 2 58 | end, 59 | 60 | :jg => lambda do |vm| 61 | vm.eip = (!vm.zf and (vm.sf == vm.of)) ? vm.dest_value : vm.eip + 2 62 | end, 63 | 64 | :jle => lambda do |vm| 65 | vm.eip = (vm.zf or (vm.sf != vm.of)) ? vm.dest_value : vm.eip + 2 66 | end, 67 | 68 | :jge => lambda do |vm| 69 | vm.eip = (vm.sf == vm.of) ? vm.dest_value : vm.eip + 2 70 | end, 71 | 72 | :prn => lambda do |vm| 73 | vm.output(vm.dest_value.to_s) 74 | vm.eip += 2 75 | end , 76 | 77 | :push => lambda do |vm| 78 | vm.push(vm.dest_value) 79 | vm.eip += 2 80 | end, 81 | 82 | :pop => lambda do |vm| 83 | vm.esp += 1 84 | raise InvalidStack.new if vm.esp > vm.end_of_memory 85 | vm.change_dest { vm.memory[vm.esp] } 86 | end, 87 | 88 | :call => lambda do |vm| 89 | vm.push(vm.eip+2) 90 | vm.eip = vm.dest_value 91 | end, 92 | 93 | :ret => lambda do |vm| 94 | vm.esp +=1 95 | raise InvalidStack.new if vm.esp > vm.end_of_memory 96 | vm.eip = vm.memory[vm.esp] 97 | end 98 | 99 | } 100 | 101 | end 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rubycpu 2 | 3 | rubycpu is a virtualized CPU that's similar to x86 written in ruby. 4 | 5 | The goal of the project was to learn how to implement a bytecode compiler and interpreter, not performance. 6 | 7 | The project includes an assembler compiler and virtual machine that interprets the compiled bytecode, as well as a comprehensive test suite. 8 | 9 | ### Trying it out 10 | 11 | 1. `bundle install` to install necessary gems. 12 | 2. `ruby run.rb asm/fib.asm` to run the fibonacci sample program. 13 | 14 | ### CPU Design 15 | 16 | All values in the CPU are 32-bit, little endian and unsigned. 17 | 18 | You can configure how much memory the Virtual Machine has and its stack size using class methods: 19 | 20 | ```ruby 21 | VirtualMachine.memory_size = 1000 22 | VirtualMachine.stack_size = 200 23 | ``` 24 | 25 | There are 4 general purpose registers: *eax*, *ebx*, *ecx* and *edx*. 26 | 27 | The stack pointer and base pointer are available as *esp* and *ebp*. 28 | 29 | ### Instruction Set 30 | 31 | The instruction set is somewhat close to x86 assembly language. The [Wikibooks](http://en.wikibooks.org/wiki/X86_Assembly) articles on the language are excellent. Right now the following instructions are supported: 32 | 33 | General: 34 | 35 | * nop 36 | * mov 37 | * cmp 38 | * push 39 | * pop 40 | 41 | Arithmetic: 42 | 43 | * inc 44 | * add 45 | * sub 46 | * dec 47 | * mul 48 | * div 49 | * mod 50 | * rem 51 | 52 | Jumping/Calling: 53 | 54 | * jmp 55 | * call 56 | * ret 57 | * je 58 | * jl 59 | * jle 60 | * jg 61 | * jge 62 | * jne 63 | 64 | Bit Functions: 65 | 66 | * and 67 | * not 68 | * or 69 | * xor 70 | * shl 71 | * shr 72 | 73 | Special: 74 | 75 | * prn (outputs the value of the register or memory to STDOUT) 76 | 77 | ### Machine Layout 78 | 79 | All registers are mapped to addresses in memory and can be accessed directly. 80 | 81 | ``` 82 | +---+---+---+---+---+---+---+---+-----+---+---+---+---+---------+ 83 | |eax|ebx|ecx|edx|esp|ebp|eip|rem|flags|...|...|...|...|stack end| 84 | +---+---+---+---+---+---+---+---+-----+---+---+---+---+---------+ 85 | 0 1 2 3 4 5 6 7 8 ... mem_size 86 | ``` 87 | 88 | You can use square brackets around an integer to access any address of mapped memory. This means that: 89 | 90 | ``` 91 | mov ebx, 13 92 | mov edx, 20 93 | ``` 94 | 95 | is equivalent to: 96 | 97 | ``` 98 | mov [1], 13 99 | mov [3], 20 100 | ``` 101 | 102 | The stack begins at the end of memory and goes downwards until `VirtualMachine.stack_size` is exhausted. 103 | 104 | 105 | ### Running the Tests 106 | 107 | There are specs for compiling and running every command the CPU is capable of. Simply run `rake test` to execute the entire suite. 108 | 109 | 110 | ### Acknowledgments 111 | 112 | * The [tinyvm](https://github.com/GenTiradentes/tinyvm) project was a great inspiration, in fact many of the sample programs were lifted directly from them. Thanks! 113 | 114 | * [@notch](http://twitter.com/notch)'s tweets about designing a CPU that inspired the project. 115 | 116 | * My former co-worker [@aronmgough](http://twitter.com/aaronmgough)'s excellent [tutorial on treetop](http://thingsaaronmade.com/blog/a-quick-intro-to-writing-a-parser-using-treetop.html) which inspired my parser. 117 | 118 | ### License 119 | 120 | rubycpu is released under the MIT License. 121 | 122 | -------------------------------------------------------------------------------- /lib/node_extensions.rb: -------------------------------------------------------------------------------- 1 | module Assembler 2 | 3 | class Label 4 | attr_reader :value 5 | def initialize(value) 6 | @value = value.strip 7 | end 8 | end 9 | 10 | class Offset 11 | attr_reader :value 12 | def initialize(value) 13 | @value = value.to_i 14 | end 15 | end 16 | 17 | class Lines < Treetop::Runtime::SyntaxNode 18 | def to_array 19 | self.elements.map do |e| 20 | val = e.to_array 21 | val.flatten! if val.is_a?(Array) 22 | val 23 | end 24 | 25 | end 26 | end 27 | 28 | class Container < Treetop::Runtime::SyntaxNode 29 | def to_array 30 | self.elements.map {|x| x.to_array} 31 | end 32 | end 33 | 34 | # Instructions 35 | 36 | class Instruction < Treetop::Runtime::SyntaxNode 37 | def to_array 38 | name = self.class.name.gsub!(/Assembler\:\:/, '').gsub!(/Instruction$/, '').downcase!.to_sym 39 | 40 | if self.elements.nil? 41 | name 42 | else 43 | [name, self.elements.map {|x| x.to_array}].flatten 44 | end 45 | end 46 | end 47 | 48 | class NopInstruction < Instruction; end; 49 | class MovInstruction < Instruction; end; 50 | class ModInstruction < Instruction; end; 51 | class CmpInstruction < Instruction; end; 52 | class JeInstruction < Instruction; end; 53 | class JmpInstruction < Instruction; end; 54 | class JlInstruction < Instruction; end; 55 | class JleInstruction < Instruction; end; 56 | class JneInstruction < Instruction; end; 57 | class JgInstruction < Instruction; end; 58 | class JgeInstruction < Instruction; end; 59 | class IncInstruction < Instruction; end; 60 | class DecInstruction < Instruction; end; 61 | class MulInstruction < Instruction; end; 62 | class DivInstruction < Instruction; end; 63 | class RemInstruction < Instruction; end; 64 | class PrnInstruction < Instruction; end; 65 | class AddInstruction < Instruction; end; 66 | class SubInstruction < Instruction; end; 67 | class PushInstruction < Instruction; end; 68 | class PopInstruction < Instruction; end; 69 | class CallInstruction < Instruction; end; 70 | class RetInstruction < Instruction; end; 71 | 72 | class AndInstruction < Instruction; end; 73 | class NotInstruction < Instruction; end; 74 | class OrInstruction < Instruction; end; 75 | class XorInstruction < Instruction; end; 76 | class ShlInstruction < Instruction; end; 77 | class ShrInstruction < Instruction; end; 78 | 79 | class IncludeInstruction < Instruction 80 | def include 81 | self.elements.select { |n| n.is_a? AssemblerFile }.first.include 82 | end 83 | def to_array 84 | self 85 | end 86 | end 87 | 88 | class AssemblerFile < Treetop::Runtime::SyntaxNode 89 | def include 90 | self.text_value 91 | end 92 | end 93 | 94 | # Literals and Registers 95 | 96 | class IntegerLiteral < Treetop::Runtime::SyntaxNode 97 | def to_array 98 | self.text_value.to_i 99 | end 100 | end 101 | 102 | class LabelLiteral < Treetop::Runtime::SyntaxNode 103 | def to_array 104 | Label.new(self.text_value) 105 | end 106 | end 107 | 108 | class Register < Treetop::Runtime::SyntaxNode 109 | def to_array 110 | Offset.new(ByteCode.registers[self.text_value.to_sym]) 111 | end 112 | end 113 | 114 | class OffsetLiteral < Treetop::Runtime::SyntaxNode 115 | def to_array 116 | val = self.text_value.gsub(/\[|\]/, '') 117 | 118 | # If it's a hex value, convert it 119 | if val =~ /[0-9a-f]+h/ 120 | val = val[0..-2].to_i(16) 121 | else 122 | val = val.to_i 123 | end 124 | 125 | Offset.new(val) 126 | end 127 | end 128 | 129 | end 130 | -------------------------------------------------------------------------------- /lib/virtual_machine.rb: -------------------------------------------------------------------------------- 1 | require './lib/byte_code' 2 | require './lib/action' 3 | 4 | class VirtualMachine 5 | 6 | # Exceptions that can be raised by a program 7 | class InvalidProgram < Exception; end 8 | class InvalidStack < Exception; end 9 | class StackOverflow < Exception; end 10 | class DivideByZero < Exception; end 11 | 12 | # Operations executed 13 | attr_reader :op_count, :end_of_memory, :memory 14 | 15 | # Memory map 16 | # [eax, ebx, ecx, edx, esp, ebp] [eip] [rem] [flags] .. [stack] 17 | 18 | def initialize(program) 19 | @memory = Array.new(VirtualMachine.memory_size) 20 | @end_of_memory = @memory.size-1 21 | @buffer = program.unpack('V*') 22 | 23 | # Initialize registers to 0 24 | ByteCode.registers.each {|r, o| @memory[o] = 0 } 25 | @memory[ByteCode.flags_location] = 0 26 | self.eip = 0 27 | self.rem = 0 28 | 29 | self.esp = @end_of_memory 30 | @stack_end = self.esp - VirtualMachine.stack_size 31 | @op_count = 0 32 | 33 | @actions = ActionMap.new(self) 34 | @actions.setup!( VirtualMachine::ACTIONS ) 35 | end 36 | 37 | # Accessors for registers 38 | ByteCode.registers.each do |reg, offset| 39 | define_method(reg) { @memory[offset] } 40 | define_method("#{reg}=") {|val| @memory[offset] = val} 41 | end 42 | 43 | # Accessors for eip and rem 44 | %w(eip rem).each do |reg| 45 | class_eval %{ 46 | def #{reg}; @memory[ByteCode.#{reg}_location]; end 47 | def #{reg}=(val); @memory[ByteCode.#{reg}_location] = val; end 48 | } 49 | end 50 | 51 | # Accessors for flags 52 | %w(zf sf of).each do |f| 53 | class_eval %{ 54 | def #{f}; (@memory[ByteCode.flags_location] & ByteCode.#{f}_mask) == ByteCode.#{f}_mask; end 55 | def #{f}=(val) 56 | if val 57 | @memory[ByteCode.flags_location] |= ByteCode.#{f}_mask 58 | else 59 | @memory[ByteCode.flags_location] &= (~ByteCode.#{f}_mask) 60 | end 61 | end 62 | } 63 | end 64 | 65 | # For tuning the VM 66 | def self.memory_size=(sz) 67 | @memory_size = sz 68 | end 69 | 70 | def self.memory_size 71 | @memory_size || (1024 * 256) 72 | end 73 | 74 | # For tuning the VM 75 | def self.stack_size=(sz) 76 | @stack_size = sz 77 | end 78 | 79 | def self.stack_size 80 | @stack_size || (1024 * 4) 81 | end 82 | 83 | def src_value 84 | opcode = @buffer[eip] 85 | src = @buffer[eip+2] 86 | if opcode & ByteCode.op_source_offset == ByteCode.op_source_offset 87 | @memory[src] 88 | else 89 | src 90 | end 91 | end 92 | 93 | def stack_size 94 | (@end_of_memory-self.esp) 95 | end 96 | 97 | def dest_value 98 | opcode = @buffer[eip] 99 | dest = @buffer[eip+1] 100 | if opcode & ByteCode.op_dest_offset == ByteCode.op_dest_offset 101 | @memory[dest] 102 | else 103 | dest 104 | end 105 | end 106 | 107 | def output(value) 108 | puts value 109 | end 110 | 111 | def push(val) 112 | raise StackOverflow.new if self.esp < @stack_end 113 | @memory[self.esp] = val 114 | self.esp -= 1 115 | end 116 | 117 | # Perform an operation and store it in the dest 118 | def src_to_dest 119 | change_dest {|dest| yield(dest, src_value.to_i)} 120 | self.eip += 1 121 | end 122 | 123 | # Change the destination 124 | def change_dest 125 | @memory[@buffer[eip+1]] = yield(dest_value.to_i) 126 | self.eip += 2 127 | end 128 | 129 | def run 130 | while eip < @buffer.size 131 | opcode = @buffer[eip] 132 | operation = ByteCode.opcodes_inverted[ opcode & ByteCode.op_mask ] 133 | 134 | if @actions[ operation ].nil? 135 | puts "An Error: operation: \"#{operation}\", opcode: \"#{opcode}\"" 136 | raise VirtualMachine::InvalidProgram, "Don't understand the opcode %b" % opcode 137 | else 138 | @actions[ operation ].run() 139 | end 140 | 141 | # Keep track of how many operations we've done 142 | @op_count += 1 143 | 144 | end 145 | end 146 | 147 | def self.execute(code) 148 | @vm = VirtualMachine.new(Compiler.compile(code)) 149 | @vm.run 150 | @vm 151 | end 152 | 153 | end 154 | 155 | -------------------------------------------------------------------------------- /lib/assembler.treetop: -------------------------------------------------------------------------------- 1 | grammar Assembler 2 | 3 | rule syntax 4 | (line_end / label_line / labelled_command / command)* 5 | end 6 | 7 | rule label_line 8 | label ":" line_end 9 | end 10 | 11 | rule label 12 | [a-zA-Z\_0-9]+ 13 | end 14 | 15 | rule labelled_command 16 | whitespace? label ":" whitespace? command 17 | end 18 | 19 | rule command 20 | whitespace? instruction whitespace? line_end 21 | end 22 | 23 | rule comment 24 | whitespace? "#" [^\n]* EOL 25 | end 26 | 27 | rule instruction 28 | nop / 29 | mov / 30 | cmp / 31 | mod / 32 | rem / 33 | inc / 34 | jmp / 35 | jg / 36 | jge / 37 | je / 38 | jne / 39 | jl / 40 | jle / 41 | prn / 42 | add / 43 | sub / 44 | push / 45 | pop / 46 | call / 47 | ret / 48 | dec / 49 | mul / 50 | div / 51 | and_op / 52 | not_op / 53 | or_op / 54 | xor / 55 | shl / 56 | shr / 57 | include 58 | end 59 | 60 | rule whitespace 61 | [ \t\v]+ 62 | end 63 | 64 | rule line_end 65 | comment / 66 | ( whitespace? EOL )+ 67 | end 68 | 69 | rule EOL 70 | "\r\n" / "\r" / "\n" 71 | end 72 | 73 | rule param_sep 74 | whitespace? "," whitespace? 75 | end 76 | 77 | rule integer_literal 78 | [0-9]+ 79 | end 80 | 81 | rule non_zero_integer_literal 82 | "0"* [1-9]+ [0-9]* 83 | end 84 | 85 | rule to_label 86 | whitespace label 87 | end 88 | 89 | rule any 90 | destination / integer_literal 91 | end 92 | 93 | rule any_non_zero 94 | destination / non_zero_integer_literal 95 | end 96 | 97 | rule nop 98 | "nop" 99 | end 100 | 101 | rule mov 102 | "mov" whitespace destination param_sep any 103 | end 104 | 105 | rule cmp 106 | "cmp" whitespace any param_sep any 107 | end 108 | 109 | rule add 110 | "add" whitespace destination param_sep any 111 | end 112 | 113 | rule sub 114 | "sub" whitespace destination param_sep any 115 | end 116 | 117 | rule mul 118 | "mul" whitespace destination param_sep any 119 | end 120 | 121 | rule div 122 | "div" whitespace destination param_sep any_non_zero 123 | end 124 | 125 | rule mod 126 | "mod" whitespace any param_sep any 127 | end 128 | 129 | rule and_op 130 | "and" whitespace destination param_sep any 131 | end 132 | 133 | rule or_op 134 | "or" whitespace destination param_sep any 135 | end 136 | 137 | rule not_op 138 | "not" whitespace destination 139 | end 140 | 141 | rule xor 142 | "xor" whitespace destination param_sep any 143 | end 144 | 145 | rule shl 146 | "shl" whitespace destination param_sep any 147 | end 148 | 149 | rule shr 150 | "shr" whitespace destination param_sep any 151 | end 152 | 153 | 154 | rule rem 155 | "rem" whitespace destination 156 | end 157 | 158 | rule inc 159 | "inc" whitespace destination 160 | end 161 | 162 | rule dec 163 | "dec" whitespace destination 164 | end 165 | 166 | rule jmp 167 | "jmp" to_label 168 | end 169 | 170 | rule jle 171 | "jle" to_label 172 | end 173 | 174 | rule je 175 | "je" to_label 176 | end 177 | 178 | rule jl 179 | "jl" to_label 180 | end 181 | 182 | rule jg 183 | "jg" to_label 184 | end 185 | 186 | rule jge 187 | "jge" to_label 188 | end 189 | 190 | rule jne 191 | "jne" to_label 192 | end 193 | 194 | rule push 195 | "push" whitespace any 196 | end 197 | 198 | rule pop 199 | "pop" whitespace any 200 | end 201 | 202 | rule call 203 | "call" to_label 204 | end 205 | 206 | rule ret 207 | "ret" 208 | end 209 | 210 | rule prn 211 | "prn" whitespace any 212 | end 213 | 214 | rule destination 215 | "eax" / 216 | "ebx" / 217 | "ecx" / 218 | "edx" / 219 | "esp" / 220 | "ebp" / 221 | "[" [0-9]+ "]" / 222 | "[" [0-9a-f]+ "h]" 223 | end 224 | 225 | rule asm_file 226 | #[a-zA-Z\_0-9]+ '.asm' 227 | [a-zA-Z\_0-9]+ '/'? [a-zA-Z\_0-9]+ '.asm' 228 | end 229 | 230 | rule include 231 | "%include" whitespace "'" asm_file "'" 232 | end 233 | 234 | end 235 | -------------------------------------------------------------------------------- /spec/components/virtual_machine_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'virtual_machine' 3 | 4 | describe VirtualMachine do 5 | 6 | before do 7 | # We only need tiny memory sizes for these tests. So let's speed it up 8 | VirtualMachine.memory_size = 20 9 | VirtualMachine.stack_size = 10 10 | end 11 | 12 | 13 | describe 'nop' do 14 | it 'does nothing' do 15 | VirtualMachine.execute("nop").op_count.must_equal 1 16 | end 17 | end 18 | 19 | describe 'mov' do 20 | it 'sets a value to a register' do 21 | VirtualMachine.execute('mov ebx, 3').ebx.must_equal 3 22 | end 23 | 24 | it 'sets a register to the value of another register' do 25 | VirtualMachine.execute("mov eax, 7 26 | mov ecx, eax").ecx.must_equal 7 27 | end 28 | 29 | it 'can set offsets in memory' do 30 | VirtualMachine.execute("mov [100], 11 31 | mov eax, [100]").eax.must_equal 11 32 | end 33 | 34 | it 'can move memory to memory' do 35 | VirtualMachine.execute("mov [100], 10 36 | mov [101], [100] 37 | mov ebx, [101]").ebx.must_equal 10 38 | end 39 | 40 | it 'can address by hex or decimal address' do 41 | VirtualMachine.execute("mov [10h], 123 42 | mov eax, [16]").eax.must_equal 123 43 | end 44 | 45 | end 46 | 47 | describe 'add' do 48 | it 'adds 3 to ebx' do 49 | VirtualMachine.execute('mov ebx, 1 50 | add ebx, 3').ebx.must_equal 4 51 | end 52 | 53 | it 'adds edx to eax' do 54 | VirtualMachine.execute("mov eax, 2 55 | mov edx, 3 56 | add eax, edx").eax.must_equal 5 57 | end 58 | 59 | it 'adds eax to eax' do 60 | VirtualMachine.execute("mov ecx, 1 61 | add ecx, ecx").ecx.must_equal 2 62 | end 63 | end 64 | 65 | describe 'sub' do 66 | it 'subs 3 from ebx' do 67 | VirtualMachine.execute('mov ebx, 3 68 | sub ebx, 1').ebx.must_equal 2 69 | end 70 | 71 | it 'subs edx from eax' do 72 | VirtualMachine.execute("mov eax, 3 73 | mov edx, 2 74 | sub eax, edx").eax.must_equal 1 75 | end 76 | 77 | it 'subs eax from eax' do 78 | VirtualMachine.execute("mov ecx, 11 79 | sub ecx, ecx").ecx.must_equal 0 80 | end 81 | end 82 | 83 | describe 'mul' do 84 | it 'multiplies ebx by 3' do 85 | VirtualMachine.execute('mov ebx, 1 86 | mul ebx, 3').ebx.must_equal 3 87 | end 88 | 89 | it 'multiplies eax by edx' do 90 | VirtualMachine.execute("mov eax, 2 91 | mov edx, 3 92 | mul eax, edx").eax.must_equal 6 93 | end 94 | 95 | it 'squares ecx' do 96 | VirtualMachine.execute("mov ecx, 3 97 | mul ecx, ecx").ecx.must_equal 9 98 | end 99 | end 100 | 101 | 102 | describe 'div' do 103 | it 'divides ebx by 3' do 104 | VirtualMachine.execute('mov ebx, 9 105 | div ebx, 3').ebx.must_equal 3 106 | end 107 | 108 | it 'multiplies eax by edx' do 109 | VirtualMachine.execute("mov eax, 10 110 | mov edx, 5 111 | div eax, edx").eax.must_equal 2 112 | end 113 | 114 | it 'divides ecx by itself' do 115 | VirtualMachine.execute("mov ecx, 3 116 | div ecx, ecx").ecx.must_equal 1 117 | end 118 | 119 | it 'raises an error on divide by 0' do 120 | lambda { 121 | VirtualMachine.execute("mov eax, 10 122 | mov ebx, 0 123 | div eax, ebx") 124 | }.must_raise(VirtualMachine::DivideByZero) 125 | end 126 | end 127 | 128 | describe 'and' do 129 | it 'logical and of register to literal' do 130 | VirtualMachine.execute('mov ebx, 3 131 | and ebx, 6').ebx.must_equal 2 132 | end 133 | 134 | it 'logical and of eax to edx' do 135 | VirtualMachine.execute("mov eax, 4 136 | mov edx, 7 137 | and eax, edx").eax.must_equal 4 138 | end 139 | 140 | it 'ands itself' do 141 | VirtualMachine.execute("mov ecx, 3 142 | and ecx, ecx").ecx.must_equal 3 143 | end 144 | end 145 | 146 | describe 'or' do 147 | it 'logical or of register to literal' do 148 | VirtualMachine.execute('mov ebx, 3 149 | or ebx, 6').ebx.must_equal 7 150 | end 151 | 152 | it 'logical or of eax to edx' do 153 | VirtualMachine.execute("mov eax, 4 154 | mov edx, 7 155 | or eax, edx").eax.must_equal 7 156 | end 157 | 158 | it 'ors itself' do 159 | VirtualMachine.execute("mov ecx, 3 160 | or ecx, ecx").ecx.must_equal 3 161 | end 162 | end 163 | 164 | 165 | describe 'xor' do 166 | it 'logical xor of register to literal' do 167 | VirtualMachine.execute('mov ebx, 3 168 | xor ebx, 6').ebx.must_equal 5 169 | end 170 | 171 | it 'logical xor of eax to edx' do 172 | VirtualMachine.execute("mov eax, 4 173 | mov edx, 7 174 | xor eax, edx").eax.must_equal 3 175 | end 176 | 177 | it 'xor itself' do 178 | VirtualMachine.execute("mov ecx, 3 179 | xor ecx, ecx").ecx.must_equal 0 180 | end 181 | end 182 | 183 | describe 'not' do 184 | it 'logical not of register' do 185 | VirtualMachine.execute('mov ebx, 1 186 | not ebx').ebx.must_equal -2 187 | end 188 | end 189 | 190 | describe 'shr' do 191 | it 'shift right of register to literal' do 192 | VirtualMachine.execute('mov ebx, 30 193 | shr ebx, 2').ebx.must_equal 7 194 | end 195 | 196 | it 'shift right of register to register' do 197 | VirtualMachine.execute("mov eax, 64 198 | mov edx, 3 199 | shr eax, edx").eax.must_equal 8 200 | end 201 | 202 | it 'shifts itself right' do 203 | VirtualMachine.execute("mov ecx, 3 204 | shr ecx, ecx").ecx.must_equal 0 205 | end 206 | end 207 | 208 | describe 'shl' do 209 | it 'shift left of register to literal' do 210 | VirtualMachine.execute('mov ebx, 4 211 | shl ebx, 2').ebx.must_equal 16 212 | end 213 | 214 | it 'shift left of register to register' do 215 | VirtualMachine.execute("mov eax, 3 216 | mov edx, 4 217 | shl eax, edx").eax.must_equal 48 218 | end 219 | 220 | it 'shifts itself left' do 221 | VirtualMachine.execute("mov ecx, 3 222 | shl ecx, ecx").ecx.must_equal 24 223 | end 224 | end 225 | 226 | describe 'push' do 227 | it 'changes the esp' do 228 | VirtualMachine.execute('push eax').stack_size.must_equal 1 229 | end 230 | end 231 | 232 | describe 'pop' do 233 | 234 | it 'raises an error when ret when the stack is empty' do 235 | -> { VirtualMachine.execute("pop eax") }.must_raise(VirtualMachine::InvalidStack) 236 | end 237 | 238 | it 'lowers the stack size' do 239 | vm = VirtualMachine.execute("push eax 240 | push eax 241 | push eax 242 | pop ebx").stack_size.must_equal 2 243 | end 244 | 245 | it 'can recover the value via pop' do 246 | VirtualMachine.execute("mov eax, 10 247 | push eax 248 | mov eax, 2 249 | pop eax").eax.must_equal 10 250 | end 251 | 252 | it 'respects the push/pop order' do 253 | vm = VirtualMachine.execute("push 10 254 | push 15 255 | push 20 256 | pop eax 257 | pop ebx 258 | pop ecx") 259 | vm.eax.must_equal 20 260 | vm.ebx.must_equal 15 261 | vm.ecx.must_equal 10 262 | end 263 | 264 | end 265 | 266 | describe 'stack overflow' do 267 | it 'raises a stack overflow error' do 268 | 269 | lambda { 270 | VirtualMachine.execute("top: push eax 271 | jmp top") 272 | }.must_raise(VirtualMachine::StackOverflow) 273 | end 274 | end 275 | 276 | 277 | describe 'inc' do 278 | it 'increases the value in a register' do 279 | VirtualMachine.execute('mov ebx, 3 280 | inc ebx').ebx.must_equal 4 281 | end 282 | 283 | it 'increases the value in a register' do 284 | VirtualMachine.execute('mov eax, 0 285 | inc eax').eax.must_equal 1 286 | end 287 | end 288 | 289 | describe 'dec' do 290 | it 'decreases the value in a register' do 291 | VirtualMachine.execute('mov ecx, 3 292 | dec ecx').ecx.must_equal 2 293 | end 294 | end 295 | 296 | 297 | describe 'mod' do 298 | it 'divides two numbers and put the remainder in the remainder register' do 299 | VirtualMachine.execute('mod 10, 7').rem.must_equal 3 300 | end 301 | 302 | it 'has a modulo of 0' do 303 | VirtualMachine.execute('mod 5, 5').rem.must_equal 0 304 | end 305 | end 306 | 307 | describe 'rem' do 308 | it 'puts the remainder in ecx' do 309 | VirtualMachine.execute('mod 10, 3 310 | rem ecx').ecx.must_equal 1 311 | end 312 | 313 | it 'puts the remainder in ebx' do 314 | VirtualMachine.execute('mod 1, 1 315 | rem ebx').ebx.must_equal 0 316 | end 317 | end 318 | 319 | 320 | 321 | describe 'jmp' do 322 | 323 | before do 324 | @vm = VirtualMachine.execute("jmp skip 325 | mov eax, 5 326 | skip: nop") 327 | end 328 | 329 | it 'skips the mov' do 330 | @vm.eax.must_equal 0 331 | end 332 | 333 | it 'is not executed according to the operation count' do 334 | @vm.op_count.must_equal 2 335 | end 336 | 337 | end 338 | 339 | describe 'cmp' do 340 | 341 | describe 'value to value' do 342 | describe 'equal' do 343 | before do 344 | @vm = VirtualMachine.execute('cmp 11, 11') 345 | end 346 | 347 | it 'sets the zero flag' do 348 | @vm.zf.must_equal true 349 | end 350 | end 351 | 352 | describe 'greater than' do 353 | before do 354 | @vm = VirtualMachine.execute('cmp 8, 4') 355 | end 356 | 357 | it 'sets the zero flag to false' do 358 | @vm.zf.must_equal false 359 | end 360 | 361 | it 'sets the overflow flag to the sign flag' do 362 | @vm.sf.must_equal @vm.of 363 | end 364 | end 365 | 366 | describe 'less than' do 367 | before do 368 | @vm = VirtualMachine.execute('cmp 3, 6') 369 | end 370 | 371 | it 'sets the zero flag to false' do 372 | @vm.zf.must_equal false 373 | end 374 | 375 | it 'sets the overflow flag to the sign flag' do 376 | @vm.sf.wont_equal @vm.of 377 | end 378 | end 379 | 380 | end 381 | 382 | # Just make sure the basics work from registers too 383 | describe 'register to register' do 384 | it 'sets the zero flag to 1 when equal' do 385 | VirtualMachine.execute("mov eax, 1 386 | mov ebx, 1 387 | cmp eax, ebx").zf.must_equal true 388 | end 389 | 390 | it 'sets the zero flag to 0 when rinequal' do 391 | VirtualMachine.execute("mov eax, 1 392 | mov ebx, 2 393 | cmp eax, ebx").zf.must_equal false 394 | end 395 | end 396 | 397 | describe 'register to value' do 398 | it 'sets the zero flag to 1 when equal' do 399 | VirtualMachine.execute("mov edx, 2 400 | cmp edx, 2").zf.must_equal true 401 | end 402 | 403 | it 'sets the zero flag to 0 when inequal' do 404 | VirtualMachine.execute("mov edx, 2 405 | cmp edx, 3").zf.must_equal false 406 | end 407 | end 408 | 409 | end 410 | 411 | describe "je" do 412 | 413 | describe 'when equal' do 414 | before do 415 | @vm = VirtualMachine.execute "mov eax, 1 416 | cmp eax, 1 417 | je equal 418 | mov eax, 2 419 | equal: nop" 420 | end 421 | 422 | it 'leaves eax as 1' do 423 | @vm.eax.must_equal 1 424 | end 425 | 426 | it 'executes fewer operations' do 427 | @vm.op_count.must_equal 4 428 | end 429 | end 430 | 431 | describe 'when inequal' do 432 | before do 433 | @vm = VirtualMachine.execute "mov eax, 1 434 | cmp eax, 9 435 | je equal 436 | mov eax, 2 437 | equal: nop" 438 | end 439 | 440 | it 'changes eax to 2' do 441 | @vm.eax.must_equal 2 442 | end 443 | 444 | it 'executes all the operations' do 445 | @vm.op_count.must_equal 5 446 | end 447 | end 448 | 449 | end 450 | 451 | describe "jne" do 452 | 453 | describe 'when equal' do 454 | before do 455 | @vm = VirtualMachine.execute "mov eax, 1 456 | cmp eax, 1 457 | jne equal 458 | mov eax, 2 459 | equal: nop" 460 | end 461 | 462 | 463 | it 'changes eax to 2' do 464 | @vm.eax.must_equal 2 465 | end 466 | 467 | it 'executes all the operations' do 468 | @vm.op_count.must_equal 5 469 | end 470 | end 471 | 472 | describe 'when inequal' do 473 | before do 474 | @vm = VirtualMachine.execute "mov eax, 1 475 | cmp eax, 9 476 | jne equal 477 | mov eax, 2 478 | equal: nop" 479 | end 480 | 481 | it 'leaves eax as 1' do 482 | @vm.eax.must_equal 1 483 | end 484 | 485 | it 'executes fewer operations' do 486 | @vm.op_count.must_equal 4 487 | end 488 | 489 | end 490 | 491 | end 492 | 493 | 494 | describe 'jl' do 495 | 496 | describe 'when equal' do 497 | before do 498 | @vm = VirtualMachine.execute "mov eax, 3 499 | cmp eax, 3 500 | jl less 501 | mov eax, 2 502 | less: nop" 503 | end 504 | 505 | it 'changes eax to 2' do 506 | @vm.eax.must_equal 2 507 | end 508 | 509 | it 'executes all the operations' do 510 | @vm.op_count.must_equal 5 511 | end 512 | end 513 | 514 | describe 'when less than' do 515 | before do 516 | @vm = VirtualMachine.execute "mov eax, 1 517 | cmp eax, 5 518 | jl less 519 | mov eax, 2 520 | less: nop" 521 | end 522 | 523 | it 'leaves eax as 1' do 524 | @vm.eax.must_equal 1 525 | end 526 | 527 | it 'executes fewer operations' do 528 | @vm.op_count.must_equal 4 529 | end 530 | end 531 | 532 | describe 'when not less than' do 533 | before do 534 | @vm = VirtualMachine.execute "mov eax, 3 535 | cmp eax, 2 536 | jl less 537 | mov eax, 2 538 | less: nop" 539 | end 540 | 541 | it 'changes eax to 2' do 542 | @vm.eax.must_equal 2 543 | end 544 | 545 | it 'executes all the operations' do 546 | @vm.op_count.must_equal 5 547 | end 548 | end 549 | end 550 | 551 | describe 'jg' do 552 | 553 | describe 'when equal' do 554 | before do 555 | @vm = VirtualMachine.execute "mov eax, 3 556 | cmp eax, 3 557 | jg more 558 | mov eax, 2 559 | more: nop" 560 | end 561 | 562 | it 'changes eax to 2' do 563 | @vm.eax.must_equal 2 564 | end 565 | 566 | it 'executes all the operations' do 567 | @vm.op_count.must_equal 5 568 | end 569 | end 570 | 571 | describe 'when greater than' do 572 | before do 573 | @vm = VirtualMachine.execute "mov eax, 4 574 | cmp eax, 1 575 | jg more 576 | mov eax, 2 577 | more: nop" 578 | end 579 | 580 | it 'leaves eax as 4' do 581 | @vm.eax.must_equal 4 582 | end 583 | 584 | it 'executes fewer operations' do 585 | @vm.op_count.must_equal 4 586 | end 587 | end 588 | 589 | describe 'when not greater than' do 590 | before do 591 | @vm = VirtualMachine.execute "mov eax, 1 592 | cmp eax, 3 593 | jg more 594 | mov eax, 2 595 | more: nop" 596 | end 597 | 598 | it 'changes eax to 2' do 599 | @vm.eax.must_equal 2 600 | end 601 | 602 | it 'executes all the operations' do 603 | @vm.op_count.must_equal 5 604 | end 605 | end 606 | end 607 | 608 | 609 | describe 'jle' do 610 | 611 | describe 'when equal' do 612 | before do 613 | @vm = VirtualMachine.execute "mov eax, 3 614 | cmp eax, 3 615 | jle less 616 | mov eax, 2 617 | less: nop" 618 | end 619 | 620 | it 'leaves eax as 3' do 621 | @vm.eax.must_equal 3 622 | end 623 | 624 | it 'executes fewer operations' do 625 | @vm.op_count.must_equal 4 626 | end 627 | end 628 | 629 | describe 'when greater than' do 630 | before do 631 | @vm = VirtualMachine.execute "mov eax, 1 632 | cmp eax, 5 633 | jle less 634 | mov eax, 2 635 | less: nop" 636 | end 637 | 638 | it 'leaves eax as 1' do 639 | @vm.eax.must_equal 1 640 | end 641 | 642 | it 'executes fewer operations' do 643 | @vm.op_count.must_equal 4 644 | end 645 | end 646 | 647 | describe 'when not less than' do 648 | before do 649 | @vm = VirtualMachine.execute "mov eax, 3 650 | cmp eax, 2 651 | jle less 652 | mov eax, 2 653 | less: nop" 654 | end 655 | 656 | it 'changes eax to 2' do 657 | @vm.eax.must_equal 2 658 | end 659 | 660 | it 'executes all the operations' do 661 | @vm.op_count.must_equal 5 662 | end 663 | end 664 | 665 | end 666 | 667 | describe 'jge' do 668 | 669 | describe 'when equal' do 670 | before do 671 | @vm = VirtualMachine.execute "mov eax, 3 672 | cmp eax, 3 673 | jge more 674 | mov eax, 2 675 | more: nop" 676 | end 677 | 678 | it 'leaves eax as 3' do 679 | @vm.eax.must_equal 3 680 | end 681 | 682 | it 'executes fewer operations' do 683 | @vm.op_count.must_equal 4 684 | end 685 | end 686 | 687 | describe 'when greater than' do 688 | before do 689 | @vm = VirtualMachine.execute "mov eax, 5 690 | cmp eax, 1 691 | jge more 692 | mov eax, 2 693 | more: nop" 694 | end 695 | 696 | it 'leaves eax as 5' do 697 | end 698 | 699 | it 'executes fewer operations' do 700 | @vm.op_count.must_equal 4 701 | end 702 | end 703 | 704 | describe 'when not greater than' do 705 | before do 706 | @vm = VirtualMachine.execute "mov eax, 1 707 | cmp eax, 4 708 | jge more 709 | mov eax, 2 710 | more: nop" 711 | end 712 | 713 | it 'changes eax to 2' do 714 | @vm.eax.must_equal 2 715 | end 716 | 717 | it 'executes all the operations' do 718 | @vm.op_count.must_equal 5 719 | end 720 | end 721 | 722 | end 723 | 724 | describe 'call' do 725 | before do 726 | @vm = VirtualMachine.execute("call skip 727 | mov eax, 5 728 | skip: nop") 729 | end 730 | 731 | it 'skips the mov' do 732 | @vm.eax.must_equal 0 733 | end 734 | 735 | it 'pushes the return address onto the stack' do 736 | @vm.stack_size.must_equal 1 737 | end 738 | 739 | end 740 | 741 | describe 'ret' do 742 | 743 | describe 'working example' do 744 | before do 745 | @vm = VirtualMachine.execute("call skip 746 | mov eax, 5 747 | jmp end 748 | skip: mov eax, 4 749 | ret 750 | end: nop") 751 | end 752 | 753 | it 'skips the mov' do 754 | @vm.eax.must_equal 5 755 | end 756 | 757 | it 'has an empty stack' do 758 | @vm.stack_size.must_equal 0 759 | end 760 | end 761 | 762 | it 'raises an error when ret when the stack is empty' do 763 | -> { VirtualMachine.execute("ret") }.must_raise(VirtualMachine::InvalidStack) 764 | end 765 | 766 | end 767 | 768 | 769 | 770 | describe 'prn' do 771 | 772 | it 'calls output with the value' do 773 | @vm = VirtualMachine.new(Compiler.compile("prn 1")) 774 | @vm.expects(:output).with('1') 775 | @vm.run 776 | end 777 | 778 | end 779 | 780 | end 781 | 782 | -------------------------------------------------------------------------------- /spec/components/compiler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'compiler' 3 | 4 | describe Compiler do 5 | 6 | describe "nop" do 7 | it "compiles the instruction" do 8 | Compiler.compile_to_array("nop")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:nop]) 9 | end 10 | end 11 | 12 | describe "ret" do 13 | it "compiles the instruction" do 14 | Compiler.compile_to_array("ret")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:ret]) 15 | end 16 | end 17 | 18 | describe "mov" do 19 | it "compiles the mov instruction" do 20 | Compiler.compile_to_array("mov eax, 10")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:mov]) 21 | end 22 | 23 | it 'raises an exception without any parameters' do 24 | -> { Compiler.compile("mov") }.must_raise(Parser::ParserException) 25 | end 26 | 27 | it 'raises an exception without a second parameters' do 28 | -> { Compiler.compile("mov eax") }.must_raise(Parser::ParserException) 29 | end 30 | 31 | it 'raises an exception when the destination is a literal' do 32 | -> { Compiler.compile('mov 3, eax') }.must_raise(Parser::ParserException) 33 | end 34 | 35 | 36 | describe 'literal to offset' do 37 | before do 38 | @data = Compiler.compile_to_array("mov [100], 7") 39 | end 40 | 41 | it "has a register as the destination" do 42 | @data[0].must_have_mask(ByteCode.op_dest_offset) 43 | end 44 | 45 | it "has offset as the destination" do 46 | @data[1].must_equal 100 47 | end 48 | 49 | it "has a literal as the source" do 50 | @data[0].must_have_mask(ByteCode.op_source_value) 51 | end 52 | 53 | it "has 3 as the source literal" do 54 | @data[2].must_equal 7 55 | end 56 | 57 | end 58 | 59 | 60 | describe 'literal to register' do 61 | before do 62 | @data = Compiler.compile_to_array("mov ebx, 3") 63 | end 64 | 65 | it "has a register as the destination" do 66 | @data[0].must_have_mask(ByteCode.op_dest_offset) 67 | end 68 | 69 | it "has ebx as the dest register" do 70 | @data[1].must_equal ByteCode.registers[:ebx] 71 | end 72 | 73 | it "has a literal as the source" do 74 | @data[0].must_have_mask(ByteCode.op_source_value) 75 | end 76 | 77 | it "has 3 as the source literal" do 78 | @data[2].must_equal 3 79 | end 80 | end 81 | 82 | describe 'register to register' do 83 | before do 84 | @data = Compiler.compile_to_array("mov ecx, eax") 85 | end 86 | 87 | it "has a register as the destination" do 88 | @data[0].must_have_mask(ByteCode.op_dest_offset) 89 | end 90 | 91 | it "has ebx as the dest register" do 92 | @data[1].must_equal ByteCode.registers[:ecx] 93 | end 94 | 95 | it "has a literal as the source" do 96 | @data[0].must_have_mask(ByteCode.op_source_offset) 97 | end 98 | 99 | it "has 3 as the source register" do 100 | @data[2].must_equal ByteCode.registers[:eax] 101 | end 102 | end 103 | 104 | end 105 | 106 | describe "cmp" do 107 | 108 | it "compiles the cmp instruction" do 109 | Compiler.compile_to_array("cmp eax, ecx")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:cmp]) 110 | end 111 | 112 | it 'raises an exception without any parameters' do 113 | -> { Compiler.compile("cmp") }.must_raise(Parser::ParserException) 114 | end 115 | 116 | it 'raises an exception without a second parameters' do 117 | -> { Compiler.compile("cmp eax") }.must_raise(Parser::ParserException) 118 | end 119 | 120 | describe 'literal to register' do 121 | before do 122 | @data = Compiler.compile_to_array("cmp ecx, 1") 123 | end 124 | 125 | it "has a register as the destination" do 126 | @data[0].must_have_mask(ByteCode.op_dest_offset) 127 | end 128 | 129 | it "has ebx as the dest register" do 130 | @data[1].must_equal ByteCode.registers[:ecx] 131 | end 132 | 133 | it "has a literal as the source" do 134 | @data[0].must_have_mask(ByteCode.op_source_value) 135 | end 136 | 137 | it "has 3 as the source literal" do 138 | @data[2].must_equal 1 139 | end 140 | end 141 | 142 | describe 'register to register' do 143 | before do 144 | @data = Compiler.compile_to_array("cmp eax, ebx") 145 | end 146 | 147 | it "has a register as the destination" do 148 | @data[0].must_have_mask(ByteCode.op_dest_offset) 149 | end 150 | 151 | it "has ebx as the dest register" do 152 | @data[1].must_equal ByteCode.registers[:eax] 153 | end 154 | 155 | it "has a literal as the source" do 156 | @data[0].must_have_mask(ByteCode.op_source_offset) 157 | end 158 | 159 | it "has 3 as the source register" do 160 | @data[2].must_equal ByteCode.registers[:ebx] 161 | end 162 | end 163 | 164 | describe "register to literal" do 165 | before do 166 | @data = Compiler.compile_to_array("cmp 7, ecx") 167 | end 168 | 169 | it "has a register as the destination" do 170 | @data[0].must_have_mask(ByteCode.op_dest_value) 171 | end 172 | 173 | it "has ebx as the dest register" do 174 | @data[1].must_equal 7 175 | end 176 | end 177 | 178 | end 179 | 180 | describe "add" do 181 | it "compiles the add instruction" do 182 | Compiler.compile_to_array("add eax, 10")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:add]) 183 | end 184 | 185 | it 'raises an exception without any parameters' do 186 | -> { Compiler.compile("add") }.must_raise(Parser::ParserException) 187 | end 188 | 189 | it 'raises an exception without a second parameters' do 190 | -> { Compiler.compile("add eax") }.must_raise(Parser::ParserException) 191 | end 192 | 193 | it 'raises an exception when the destination is a literal' do 194 | -> { Compiler.compile('add 3, eax') }.must_raise(Parser::ParserException) 195 | end 196 | end 197 | 198 | describe "sub" do 199 | it "compiles the sub instruction" do 200 | Compiler.compile_to_array("sub eax, 10")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:sub]) 201 | end 202 | 203 | it 'raises an exception without any parameters' do 204 | -> { Compiler.compile("sub") }.must_raise(Parser::ParserException) 205 | end 206 | 207 | it 'raises an exception without a second parameters' do 208 | -> { Compiler.compile("sub eax") }.must_raise(Parser::ParserException) 209 | end 210 | 211 | it 'raises an exception when the destination is a literal' do 212 | -> { Compiler.compile('sub 3, eax') }.must_raise(Parser::ParserException) 213 | end 214 | end 215 | 216 | 217 | describe "mul" do 218 | it "compiles the mul instruction" do 219 | Compiler.compile_to_array("mul eax, 10")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:mul]) 220 | end 221 | 222 | it 'raises an exception without any parameters' do 223 | -> { Compiler.compile("mul") }.must_raise(Parser::ParserException) 224 | end 225 | 226 | it 'raises an exception without a second parameters' do 227 | -> { Compiler.compile("mul eax") }.must_raise(Parser::ParserException) 228 | end 229 | 230 | it 'raises an exception when the destination is a literal' do 231 | -> { Compiler.compile('mul 3, eax') }.must_raise(Parser::ParserException) 232 | end 233 | end 234 | 235 | describe "div" do 236 | it "compiles the div instruction" do 237 | Compiler.compile_to_array("div eax, 10")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:div]) 238 | end 239 | 240 | it 'raises an exception without any parameters' do 241 | -> { Compiler.compile("div") }.must_raise(Parser::ParserException) 242 | end 243 | 244 | it 'raises an exception without a second parameters' do 245 | -> { Compiler.compile("div eax") }.must_raise(Parser::ParserException) 246 | end 247 | 248 | it 'raises an exception when the destination is a literal' do 249 | -> { Compiler.compile('div 3, eax') }.must_raise(Parser::ParserException) 250 | end 251 | 252 | it 'raises an exception when the divisor is 0' do 253 | -> { Compiler.compile('div eax, 0') }.must_raise(Parser::ParserException) 254 | end 255 | 256 | it "doesn't raise an exception when the divisor is 0 padded" do 257 | Compiler.compile('div eax, 0001') 258 | end 259 | end 260 | 261 | describe "and" do 262 | it "compiles the and instruction" do 263 | Compiler.compile_to_array("and eax, 10")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:and]) 264 | end 265 | 266 | it 'raises an exception without any parameters' do 267 | -> { Compiler.compile("and") }.must_raise(Parser::ParserException) 268 | end 269 | 270 | it 'raises an exception without a second parameters' do 271 | -> { Compiler.compile("and eax") }.must_raise(Parser::ParserException) 272 | end 273 | 274 | it 'raises an exception when the destination is a literal' do 275 | -> { Compiler.compile('and 3, eax') }.must_raise(Parser::ParserException) 276 | end 277 | end 278 | 279 | describe "or" do 280 | it "compiles the or instruction" do 281 | Compiler.compile_to_array("or eax, 10")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:or]) 282 | end 283 | 284 | it 'raises an exception without any parameters' do 285 | -> { Compiler.compile("or") }.must_raise(Parser::ParserException) 286 | end 287 | 288 | it 'raises an exception without a second parameters' do 289 | -> { Compiler.compile("or eax") }.must_raise(Parser::ParserException) 290 | end 291 | 292 | it 'raises an exception when the destination is a literal' do 293 | -> { Compiler.compile('or 3, eax') }.must_raise(Parser::ParserException) 294 | end 295 | end 296 | 297 | 298 | describe "xor" do 299 | it "compiles the xor instruction" do 300 | Compiler.compile_to_array("xor eax, 10")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:xor]) 301 | end 302 | 303 | it 'raises an exception without any parameters' do 304 | -> { Compiler.compile("xor") }.must_raise(Parser::ParserException) 305 | end 306 | 307 | it 'raises an exception without a second parameters' do 308 | -> { Compiler.compile("xor eax") }.must_raise(Parser::ParserException) 309 | end 310 | 311 | it 'raises an exception when the destination is a literal' do 312 | -> { Compiler.compile('xor 3, eax') }.must_raise(Parser::ParserException) 313 | end 314 | end 315 | 316 | describe "not" do 317 | it "compiles the not instruction" do 318 | Compiler.compile_to_array("not eax")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:not]) 319 | end 320 | 321 | it 'raises an exception without any parameters' do 322 | -> { Compiler.compile("not") }.must_raise(Parser::ParserException) 323 | end 324 | 325 | it 'raises an exception when the destination is a literal' do 326 | -> { Compiler.compile('not 3') }.must_raise(Parser::ParserException) 327 | end 328 | end 329 | 330 | describe "shl" do 331 | it "compiles the shl instruction" do 332 | Compiler.compile_to_array("shl eax, 10")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:shl]) 333 | end 334 | 335 | it 'raises an exception without any parameters' do 336 | -> { Compiler.compile("shl") }.must_raise(Parser::ParserException) 337 | end 338 | 339 | it 'raises an exception without a second parameters' do 340 | -> { Compiler.compile("shl eax") }.must_raise(Parser::ParserException) 341 | end 342 | 343 | it 'raises an exception when the destination is a literal' do 344 | -> { Compiler.compile('shl 3, eax') }.must_raise(Parser::ParserException) 345 | end 346 | end 347 | 348 | describe "shr" do 349 | it "compiles the shr instruction" do 350 | Compiler.compile_to_array("shr eax, 10")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:shr]) 351 | end 352 | 353 | it 'raises an exception without any parameters' do 354 | -> { Compiler.compile("shr") }.must_raise(Parser::ParserException) 355 | end 356 | 357 | it 'raises an exception without a second parameters' do 358 | -> { Compiler.compile("shr eax") }.must_raise(Parser::ParserException) 359 | end 360 | 361 | it 'raises an exception when the destination is a literal' do 362 | -> { Compiler.compile('shr 3, eax') }.must_raise(Parser::ParserException) 363 | end 364 | end 365 | 366 | 367 | describe "mod" do 368 | 369 | it "compiles the mod instruction" do 370 | Compiler.compile_to_array("mod eax, ebx")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:mod]) 371 | end 372 | 373 | it 'raises an exception without any parameters' do 374 | -> { Compiler.compile("mod") }.must_raise(Parser::ParserException) 375 | end 376 | 377 | it 'raises an exception without a second parameters' do 378 | -> { Compiler.compile("mod eax") }.must_raise(Parser::ParserException) 379 | end 380 | 381 | describe 'literal to register' do 382 | before do 383 | @data = Compiler.compile_to_array("mod eax, 8") 384 | end 385 | 386 | it "has a register as the destination" do 387 | @data[0].must_have_mask(ByteCode.op_dest_offset) 388 | end 389 | 390 | it "has eax as the dest register" do 391 | @data[1].must_equal ByteCode.registers[:eax] 392 | end 393 | 394 | it "has a literal as the source" do 395 | @data[0].must_have_mask(ByteCode.op_source_value) 396 | end 397 | 398 | it "has 3 as the source literal" do 399 | @data[2].must_equal 8 400 | end 401 | end 402 | 403 | describe 'register to register' do 404 | before do 405 | @data = Compiler.compile_to_array("mod edx, eax") 406 | end 407 | 408 | it "has a register as the destination" do 409 | @data[0].must_have_mask(ByteCode.op_dest_offset) 410 | end 411 | 412 | it "has edx as the dest register" do 413 | @data[1].must_equal ByteCode.registers[:edx] 414 | end 415 | 416 | it "has a literal as the source" do 417 | @data[0].must_have_mask(ByteCode.op_source_offset) 418 | end 419 | 420 | it "has 3 as the source register" do 421 | @data[2].must_equal ByteCode.registers[:eax] 422 | end 423 | end 424 | 425 | describe "register to literal" do 426 | before do 427 | @data = Compiler.compile_to_array("mod 3, eax") 428 | end 429 | 430 | it "has a register as the destination" do 431 | @data[0].must_have_mask(ByteCode.op_dest_value) 432 | end 433 | 434 | it "has ebx as the dest register" do 435 | @data[1].must_equal 3 436 | end 437 | end 438 | 439 | end 440 | 441 | 442 | describe "rem" do 443 | 444 | it "compiles the rem instruction" do 445 | Compiler.compile_to_array("rem eax")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:rem]) 446 | end 447 | 448 | it 'raises an exception without any parameters' do 449 | -> { Compiler.compile("rem") }.must_raise(Parser::ParserException) 450 | end 451 | 452 | it 'raises an exception with a literal as the parameter' do 453 | -> { Compiler.compile("rem 8") }.must_raise(Parser::ParserException) 454 | end 455 | 456 | describe 'register' do 457 | before do 458 | @data = Compiler.compile_to_array("rem ecx") 459 | end 460 | 461 | it "has a register as the destination" do 462 | @data[0].must_have_mask(ByteCode.op_dest_offset) 463 | end 464 | 465 | it "has ecx as the dest register" do 466 | @data[1].must_equal ByteCode.registers[:ecx] 467 | end 468 | end 469 | 470 | end 471 | 472 | describe "inc" do 473 | 474 | it "compiles the inc instruction" do 475 | Compiler.compile_to_array("inc eax")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:inc]) 476 | end 477 | 478 | it 'raises an exception without any parameters' do 479 | -> { Compiler.compile("inc") }.must_raise(Parser::ParserException) 480 | end 481 | 482 | it 'raises an exception with a literal as the parameter' do 483 | -> { Compiler.compile("inc 8") }.must_raise(Parser::ParserException) 484 | end 485 | 486 | describe 'register' do 487 | before do 488 | @data = Compiler.compile_to_array("inc ebx") 489 | end 490 | 491 | it "has a register as the destination" do 492 | @data[0].must_have_mask(ByteCode.op_dest_offset) 493 | end 494 | 495 | it "has ebx as the dest register" do 496 | @data[1].must_equal ByteCode.registers[:ebx] 497 | end 498 | end 499 | 500 | end 501 | 502 | describe "dec" do 503 | 504 | it "compiles the dec instruction" do 505 | Compiler.compile_to_array("dec eax")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:dec]) 506 | end 507 | 508 | it 'raises an exception without any parameters' do 509 | -> { Compiler.compile("dec") }.must_raise(Parser::ParserException) 510 | end 511 | 512 | it 'raises an exception with a literal as the parameter' do 513 | -> { Compiler.compile("dec 8") }.must_raise(Parser::ParserException) 514 | end 515 | 516 | describe 'register' do 517 | before do 518 | @data = Compiler.compile_to_array("dec ebx") 519 | end 520 | 521 | it "has a register as the destination" do 522 | @data[0].must_have_mask(ByteCode.op_dest_offset) 523 | end 524 | 525 | it "has ebx as the dest register" do 526 | @data[1].must_equal ByteCode.registers[:ebx] 527 | end 528 | end 529 | 530 | end 531 | 532 | describe 'push' do 533 | it "compiles the push instruction" do 534 | Compiler.compile_to_array("push eax")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:push]) 535 | end 536 | 537 | it 'raises an exception without any parameters' do 538 | -> { Compiler.compile("push") }.must_raise(Parser::ParserException) 539 | end 540 | 541 | it 'allows us to push a literal' do 542 | Compiler.compile("push 8") 543 | end 544 | 545 | describe 'register' do 546 | before do 547 | @data = Compiler.compile_to_array("push ebp") 548 | end 549 | 550 | it "has a register as the destination" do 551 | @data[0].must_have_mask(ByteCode.op_dest_offset) 552 | end 553 | 554 | it "has ebp as the dest register" do 555 | @data[1].must_equal ByteCode.registers[:ebp] 556 | end 557 | end 558 | end 559 | 560 | describe 'pop' do 561 | it "compiles the pop instruction" do 562 | Compiler.compile_to_array("pop eax")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:pop]) 563 | end 564 | 565 | it 'raises an exception without any parameters' do 566 | -> { Compiler.compile("pop") }.must_raise(Parser::ParserException) 567 | end 568 | 569 | it 'allows us to pop a literal' do 570 | Compiler.compile("pop 8") 571 | end 572 | 573 | describe 'register' do 574 | before do 575 | @data = Compiler.compile_to_array("pop esp") 576 | end 577 | 578 | it "has a register as the destination" do 579 | @data[0].must_have_mask(ByteCode.op_dest_offset) 580 | end 581 | 582 | it "has esp as the dest register" do 583 | @data[1].must_equal ByteCode.registers[:esp] 584 | end 585 | end 586 | end 587 | 588 | 589 | describe "prn" do 590 | 591 | it "compiles the prn instruction" do 592 | Compiler.compile_to_array("prn eax")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:prn]) 593 | end 594 | 595 | it 'raises an exception without any parameters' do 596 | -> { Compiler.compile("prn") }.must_raise(Parser::ParserException) 597 | end 598 | 599 | describe 'register' do 600 | before do 601 | @data = Compiler.compile_to_array("prn eax") 602 | end 603 | 604 | it "has a register as the destination" do 605 | @data[0].must_have_mask(ByteCode.op_dest_offset) 606 | end 607 | 608 | it "has eax as the dest register" do 609 | @data[1].must_equal ByteCode.registers[:eax] 610 | end 611 | end 612 | 613 | describe 'literal' do 614 | before do 615 | @data = Compiler.compile_to_array("prn 5") 616 | end 617 | 618 | it "has a register as the destination" do 619 | @data[0].must_have_mask(ByteCode.op_dest_value) 620 | end 621 | 622 | it "has eax as the dest register" do 623 | @data[1].must_equal 5 624 | end 625 | end 626 | 627 | end 628 | 629 | describe "labels" do 630 | 631 | it "allows us to compile code with a label" do 632 | Compiler.compile_to_array("hello: mov eax, 3")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:mov]) 633 | end 634 | 635 | describe 'start label' do 636 | before do 637 | @code = Compiler.compile_to_array("mov eax, 3 638 | start: nop") 639 | end 640 | 641 | it 'inserts a jmp to the start label' do 642 | @code[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:jmp]) 643 | end 644 | 645 | it 'inserts a jmp to the start label' do 646 | @code[1].must_equal 5 647 | end 648 | end 649 | 650 | 651 | describe 'offsets' do 652 | it 'has the correct offset for jumping to the top' do 653 | Compiler.compile_to_array("hello: jmp hello")[1].must_equal 0 654 | end 655 | 656 | it 'has the offset of labels declared after the jmp statement' do 657 | Compiler.compile_to_array("jmp second\nsecond: nop")[1].must_equal 2 658 | end 659 | end 660 | 661 | describe 'jmp' do 662 | 663 | it 'should have the correct operation' do 664 | Compiler.compile_to_array("hello: jmp hello")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:jmp]) 665 | end 666 | 667 | it 'raises an exception without any parameters' do 668 | -> { Compiler.compile("jmp") }.must_raise(Parser::ParserException) 669 | end 670 | 671 | it "raises an exception when we try to jump to a label that doesn't exist" do 672 | -> { Compiler.compile("jmp doesnt_exist") }.must_raise(Compiler::CompilerException) 673 | end 674 | 675 | end 676 | 677 | 678 | describe 'call' do 679 | it 'should have the correct operation' do 680 | Compiler.compile_to_array("hello: call hello")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:call]) 681 | end 682 | 683 | it 'raises an exception without any parameters' do 684 | -> { Compiler.compile("call") }.must_raise(Parser::ParserException) 685 | end 686 | 687 | it "raises an exception when we try to jump to a label that doesn't exist" do 688 | -> { Compiler.compile("call doesnt_exist") }.must_raise(Compiler::CompilerException) 689 | end 690 | end 691 | 692 | 693 | describe 'je' do 694 | 695 | it 'should have the correct operation' do 696 | Compiler.compile_to_array("hello: je hello")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:je]) 697 | end 698 | 699 | it 'raises an exception without any parameters' do 700 | -> { Compiler.compile("je") }.must_raise(Parser::ParserException) 701 | end 702 | 703 | it "raises an exception when we try to jump to a label that doesn't exist" do 704 | -> { Compiler.compile("je doesnt_exist") }.must_raise(Compiler::CompilerException) 705 | end 706 | 707 | end 708 | 709 | describe 'jl' do 710 | 711 | it 'should have the correct operation' do 712 | Compiler.compile_to_array("hello: jl hello")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:jl]) 713 | end 714 | 715 | it 'raises an exception without any parameters' do 716 | -> { Compiler.compile("jl") }.must_raise(Parser::ParserException) 717 | end 718 | 719 | it "raises an exception when we try to jump to a label that doesn't exist" do 720 | -> { Compiler.compile("jl doesnt_exist") }.must_raise(Compiler::CompilerException) 721 | end 722 | 723 | end 724 | 725 | describe 'jle' do 726 | 727 | it 'should have the correct operation' do 728 | Compiler.compile_to_array("hello: jle hello")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:jle]) 729 | end 730 | 731 | it 'raises an exception without any parameters' do 732 | -> { Compiler.compile("jle") }.must_raise(Parser::ParserException) 733 | end 734 | 735 | it "raises an exception when we try to jump to a label that doesn't exist" do 736 | -> { Compiler.compile("jle doesnt_exist") }.must_raise(Compiler::CompilerException) 737 | end 738 | 739 | end 740 | 741 | describe 'jg' do 742 | 743 | it 'should have the correct operation' do 744 | Compiler.compile_to_array("hello: jg hello")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:jg]) 745 | end 746 | 747 | it 'raises an exception without any parameters' do 748 | -> { Compiler.compile("jg") }.must_raise(Parser::ParserException) 749 | end 750 | 751 | it "raises an exception when we try to jump to a label that doesn't exist" do 752 | -> { Compiler.compile("jg doesnt_exist") }.must_raise(Compiler::CompilerException) 753 | end 754 | 755 | end 756 | 757 | describe 'jge' do 758 | 759 | it 'should have the correct operation' do 760 | Compiler.compile_to_array("hello: jge hello")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:jge]) 761 | end 762 | 763 | it 'raises an exception without any parameters' do 764 | -> { Compiler.compile("jge") }.must_raise(Parser::ParserException) 765 | end 766 | 767 | it "raises an exception when we try to jump to a label that doesn't exist" do 768 | -> { Compiler.compile("jge doesnt_exist") }.must_raise(Compiler::CompilerException) 769 | end 770 | 771 | end 772 | 773 | describe 'jne' do 774 | 775 | it 'should have the correct operation' do 776 | Compiler.compile_to_array("hello: jne hello")[0].must_contain_bits(ByteCode.op_mask, ByteCode.opcodes[:jne]) 777 | end 778 | 779 | it 'raises an exception without any parameters' do 780 | -> { Compiler.compile("jne") }.must_raise(Parser::ParserException) 781 | end 782 | 783 | it "raises an exception when we try to jump to a label that doesn't exist" do 784 | -> { Compiler.compile("jne doesnt_exist") }.must_raise(Compiler::CompilerException) 785 | end 786 | 787 | end 788 | 789 | end 790 | 791 | 792 | end 793 | --------------------------------------------------------------------------------