├── bench ├── cpu.prof ├── loop_specter.sh ├── loop_tvm.sh ├── xtime.sh ├── README.md ├── results │ ├── raw │ └── all_20130112 └── Makefile ├── .gitignore ├── cmd ├── examples │ ├── loop2.vm │ ├── loop.vm │ ├── loop3.vm │ ├── nop.vm │ ├── err_invalid_label.vm │ ├── err_jump_undefined.vm │ ├── err_label_after_inst.vm │ ├── err_too_many_args.vm │ ├── err_garbage.vm │ ├── jsr.vm │ ├── fib.vm │ ├── stack_bench.vm │ ├── euler1.vm │ ├── primes.vm │ ├── euler2.vm │ ├── euler7.vm │ ├── fact.vm │ └── euler1_nodiv.vm └── main.go ├── vm ├── prof_test.go ├── program.go ├── macro_test.go ├── memory.go ├── vm.go ├── codes.go ├── parser_test.go ├── parser.go └── vm_test.go ├── LICENSE ├── README.md └── SYNTAX /bench/cpu.prof: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mna/specter/HEAD/bench/cpu.prof -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-* 2 | .DS_Store 3 | cmd/cmd 4 | *.prof 5 | bench/results/tmp* 6 | -------------------------------------------------------------------------------- /cmd/examples/loop2.vm: -------------------------------------------------------------------------------- 1 | loop: inc eax 2 | cmp eax, 100|h 3 | prn eax 4 | 5 | jl loop 6 | -------------------------------------------------------------------------------- /cmd/examples/loop.vm: -------------------------------------------------------------------------------- 1 | loop: inc eax 2 | cmp eax, 10000|h 3 | prn eax 4 | 5 | jl loop 6 | -------------------------------------------------------------------------------- /cmd/examples/loop3.vm: -------------------------------------------------------------------------------- 1 | loop: inc eax 2 | cmp eax, 1000000|h 3 | prn eax 4 | 5 | jl loop 6 | -------------------------------------------------------------------------------- /cmd/examples/nop.vm: -------------------------------------------------------------------------------- 1 | mov eax, 0 2 | 3 | loop: 4 | nop 5 | inc eax 6 | cmp eax, 1000000 7 | jl loop 8 | -------------------------------------------------------------------------------- /cmd/examples/err_invalid_label.vm: -------------------------------------------------------------------------------- 1 | mov eax, 0 2 | 3 | ebx: 4 | nop 5 | inc eax 6 | cmp eax, 10 7 | jl loop 8 | -------------------------------------------------------------------------------- /cmd/examples/err_jump_undefined.vm: -------------------------------------------------------------------------------- 1 | mov eax, 0 2 | 3 | loop: 4 | nop 5 | inc eax 6 | cmp eax, 10 7 | jl none 8 | -------------------------------------------------------------------------------- /cmd/examples/err_label_after_inst.vm: -------------------------------------------------------------------------------- 1 | mov eax, 0 2 | 3 | nop loop: 4 | nop 5 | inc eax 6 | cmp eax, 10 7 | jl loop 8 | -------------------------------------------------------------------------------- /cmd/examples/err_too_many_args.vm: -------------------------------------------------------------------------------- 1 | mov eax, 0 2 | 3 | loop: 4 | nop 10, 12, ebx, 34 5 | inc eax 6 | cmp eax, 10 7 | jl loop 8 | -------------------------------------------------------------------------------- /bench/loop_specter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | for ((i=1; i <= $2 ; i++)) 3 | do 4 | $GOPATH/bin/cmd $GOPATH/src/github.com/mna/specter/cmd/examples/$1.vm 5 | done 6 | -------------------------------------------------------------------------------- /bench/loop_tvm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | for ((i=1; i <= $2 ; i++)) 3 | do 4 | $SUBWRKSPC/tinyvm/bin/tvmi $GOPATH/src/github.com/mna/specter/cmd/examples/$1.vm 5 | done 6 | -------------------------------------------------------------------------------- /cmd/examples/err_garbage.vm: -------------------------------------------------------------------------------- 1 | weuih wefh ehoi qef 2 | qef qiof qihfoq fqwd 3 | qwqwdqwdqwdqwdwqd qwdqwd qwd qwd qwd qwd qwd qwd 4 | # 5 | 6 | : 7 | , 8 | 234 9 | ef232 10 | -------------------------------------------------------------------------------- /cmd/examples/jsr.vm: -------------------------------------------------------------------------------- 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 14 | -------------------------------------------------------------------------------- /cmd/examples/fib.vm: -------------------------------------------------------------------------------- 1 | start: 2 | mov eax, 1 3 | mov ebx, 0 4 | 5 | loop: add eax, ebx 6 | add ebx, eax 7 | 8 | prn eax 9 | prn ebx 10 | 11 | cmp eax, 0 12 | jl end 13 | 14 | cmp ebx, 0 15 | jg loop 16 | 17 | end: 18 | -------------------------------------------------------------------------------- /cmd/examples/stack_bench.vm: -------------------------------------------------------------------------------- 1 | start: mov eax, 0xF 2 | mov ebx, 0 3 | 4 | loop0: 5 | push 0 6 | inc ebx 7 | cmp ebx, eax 8 | prn ebx 9 | jne loop0 10 | 11 | loop1: 12 | pop ecx 13 | dec ebx 14 | cmp ebx, 0 15 | prn ebx 16 | jne loop1 17 | -------------------------------------------------------------------------------- /cmd/examples/euler1.vm: -------------------------------------------------------------------------------- 1 | start: 2 | mov esi, 1000000 3 | mov eax, 0 4 | 5 | L0: 6 | mov ebx, eax 7 | mod ebx, 3 8 | rem ebx 9 | cmp ebx, 0 10 | jne L1 11 | 12 | add edx, eax 13 | je check 14 | 15 | L1: 16 | mov ebx, eax 17 | mod ebx, 5 18 | rem ebx 19 | cmp ebx, 0 20 | jne check 21 | 22 | add edx, eax 23 | 24 | check: 25 | inc eax 26 | cmp eax, esi 27 | jl L0 28 | 29 | prn edx 30 | -------------------------------------------------------------------------------- /vm/prof_test.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | const file = "/Users/martin/go/src/github.com/mna/specter/cmd/examples/loop.vm" 9 | 10 | // Benchmark function used for profiling (see ../bench/Makefile : run-prof) 11 | func BenchmarkForProfiling(b *testing.B) { 12 | for i := 0; i < b.N; i++ { 13 | vm := New() 14 | if f, err := os.Open(file); err != nil { 15 | b.Fatal(err) 16 | } else { 17 | vm.Run(f) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cmd/examples/primes.vm: -------------------------------------------------------------------------------- 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 17 | 18 | primeFound: prn eax 19 | 20 | nextPrime: inc eax 21 | cmp eax, 50 22 | jl checkPrime 23 | 24 | -------------------------------------------------------------------------------- /cmd/examples/euler2.vm: -------------------------------------------------------------------------------- 1 | 2 | 3 | start: 4 | mov eax, 1 5 | mov ebx, 0 6 | 7 | mov ecx, 0 # ECX is our sum 8 | 9 | loop: add eax, ebx 10 | add ebx, eax 11 | 12 | mov edx, eax 13 | and edx, 1 14 | cmp edx, 0 15 | jne L0 16 | add ecx, eax 17 | 18 | L0: 19 | mov edx, ebx 20 | and edx, 1 21 | cmp edx, 0 22 | jne L1 23 | add ecx, ebx 24 | 25 | L1: 26 | cmp eax, 4000000 27 | jg end 28 | 29 | cmp ebx, 4000000 30 | jl loop 31 | 32 | end: 33 | prn ecx 34 | -------------------------------------------------------------------------------- /bench/xtime.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copied from the blog post Profiling Go programs 4 | # http://blog.golang.org/2011/06/profiling-go-programs.html 5 | # 6 | # On Mac OSX you have to install gnu-time (via Macports or Homebrew). 7 | # 8 | os=$(uname) 9 | if [[ "$os" == 'Darwin' ]]; then 10 | echo user-time sys-time real-time max-mem cmd... 11 | gtime -f '%Uu %Ss %er %MkB %C' "$@" 12 | else 13 | echo user-time sys-time real-time max-mem cmd... 14 | /usr/bin/time -f '%Uu %Ss %er %MkB %C' "$@" 15 | fi 16 | -------------------------------------------------------------------------------- /cmd/examples/euler7.vm: -------------------------------------------------------------------------------- 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 17 | 18 | primeFound: inc edx 19 | cmp edx, 10001 20 | je printResult 21 | 22 | nextPrime: inc eax 23 | jmp checkPrime 24 | 25 | printResult: prn eax 26 | -------------------------------------------------------------------------------- /cmd/examples/fact.vm: -------------------------------------------------------------------------------- 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 0 { 24 | if len(cpuprof) > 0 { 25 | f, err := os.Create(cpuprof) 26 | if err != nil { 27 | panic(err) 28 | } 29 | pprof.StartCPUProfile(f) 30 | defer pprof.StopCPUProfile() 31 | } 32 | vm := vm.New() 33 | if f, err := os.Open(flag.Arg(0)); err != nil { 34 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 35 | os.Exit(1) 36 | } else { 37 | vm.Run(f) 38 | } 39 | } else { 40 | fmt.Println("A file name must be specified.") 41 | flag.Usage() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /vm/macro_test.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func getAllExampleFiles() []string { 14 | const dir string = "../cmd/examples" 15 | 16 | var ret []string 17 | 18 | files, err := ioutil.ReadDir(dir) 19 | if err != nil { 20 | panic(err) 21 | } 22 | for _, fi := range files { 23 | if filepath.Ext(fi.Name()) == ".vm" { 24 | ret = append(ret, filepath.Join(dir, fi.Name())) 25 | } 26 | } 27 | return ret 28 | } 29 | 30 | // Run all examples (*.vm files in ../cmd/examples/) 31 | func TestExamples(t *testing.T) { 32 | files := getAllExampleFiles() 33 | for _, fi := range files { 34 | if f, err := os.Open(fi); err != nil { 35 | t.Error(err) 36 | } else { 37 | runExample(t, f) 38 | } 39 | } 40 | } 41 | 42 | func runExample(t *testing.T, f *os.File) { 43 | defer func() { 44 | e := recover() 45 | // Error is NOT expected for files not beginning with "err_" 46 | if e != nil && !strings.HasPrefix(filepath.Base(f.Name()), "err_") { 47 | t.Error(e) 48 | } 49 | // Error is expected for files beginning with "err_" 50 | if e == nil && strings.HasPrefix(filepath.Base(f.Name()), "err_") { 51 | t.Error(e) 52 | } 53 | }() 54 | 55 | var b bytes.Buffer 56 | fmt.Printf("Running example %s\n", filepath.Base(f.Name())) 57 | vm := NewWithWriter(&b) 58 | // The file execution panics if there is an error 59 | vm.Run(f) 60 | } 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Martin Angers 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /bench/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | This folder contains commands to run benchmarks comparing the Go implementation (specter) to the original C implementation (TinyVM), and to run the profiler on the Go code. 4 | 5 | Make sure you adjust the paths in the Makefile and the shell scripts to point to your installation of each program. 6 | 7 | On Mac OSX, the gnu-time (gtime) command is required (`brew install gnu-time`). 8 | 9 | ## Running 10 | 11 | You can run the scripts with this command: 12 | 13 | make bench FILE=fib LOOPS=1000 14 | 15 | By default (if neither FILE nor LOOPS are specified), it runs with `fib` and `1000`. The FILE argument is the base example file name to run, without the `.vm` extension. The LOOPS argument is the number of iteration. It uses `[g]time` to capture the execution time. 16 | 17 | You can also run all benchmarks (that is, run the benchmark for all .vm files in the ./cmd/examples/ directory) with `make all`. To compare results (make sure that the output of specter is the same as the output of TinyVM), run `make cmp`. 18 | 19 | ## Performance 20 | 21 | All tests are run on a 2012 MacBook Pro Retina (Core i7 2.3GHz, 8Gb RAM). 22 | 23 | See the `./results/` directory for the raw numbers. I keep a spreadsheet with the changes and the results [here][drive]. 24 | 25 | You can follow the discussion on the golang-nuts mailing list [here][golang]. 26 | 27 | [drive]: https://docs.google.com/spreadsheet/ccc?key=0Atx1KnJmATDcdEcweWdGOHdld2lVajlaN0VRbXN6MUE 28 | [golang]: https://groups.google.com/forum/#!topic/golang-nuts/XhK5tGUsZnQ -------------------------------------------------------------------------------- /bench/results/raw: -------------------------------------------------------------------------------- 1 | 0.87u 1.58s 2.88r 4063232kB ./loop_tvm.sh fact 1000 2 | 3.07u 1.33s 6.56r 8093696kB ./loop_specter.sh fact 1000 3 | 0.79u 0.72s 1.92r 4063232kB ./loop_tvm.sh fib 1000 4 | 3.02u 1.39s 6.55r 8060928kB ./loop_specter.sh fib 1000 5 | 0.79u 1.55s 2.77r 4063232kB ./loop_tvm.sh jsr 1000 6 | 3.01u 1.28s 6.50r 8077312kB ./loop_specter.sh jsr 1000 7 | 8.58u 0.72s 9.72r 4063232kB ./loop_tvm.sh loop 1000 8 | 12.39u 1.52s 15.92r 8044544kB ./loop_specter.sh loop 1000 9 | 0.79u 0.71s 1.90r 4063232kB ./loop_tvm.sh loop2 1000 10 | 3.03u 1.33s 6.52r 8060928kB ./loop_specter.sh loop2 1000 11 | 21.09u 0.09s 21.20r 4014080kB ./loop_tvm.sh loop3 10 12 | 25.57u 0.37s 25.97r 8028160kB ./loop_specter.sh loop3 10 13 | 21.34u 0.77s 22.53r 4063232kB ./loop_tvm.sh nop 1000 14 | 37.58u 1.35s 41.03r 8044544kB ./loop_specter.sh nop 1000 15 | 0.82u 0.71s 1.95r 4063232kB ./loop_tvm.sh primes 1000 16 | 3.05u 1.30s 6.52r 8060928kB ./loop_specter.sh primes 1000 17 | 0.80u 1.52s 2.75r 4063232kB ./loop_tvm.sh stack_bench 1000 18 | 3.03u 1.32s 6.56r 8077312kB ./loop_specter.sh stack_bench 1000 19 | 109.39u 0.77s 110.61r 4063232kB ./loop_tvm.sh euler1 1000 20 | 119.14u 1.41s 122.64r 8077312kB ./loop_specter.sh euler1 1000 21 | 21.50u 0.76s 22.67r 4063232kB ./loop_tvm.sh euler1_nodiv 1000 22 | 19.78u 1.35s 23.20r 8060928kB ./loop_specter.sh euler1_nodiv 1000 23 | 0.81u 0.71s 1.93r 4063232kB ./loop_tvm.sh euler2 1000 24 | 3.03u 1.26s 6.50r 8060928kB ./loop_specter.sh euler2 1000 25 | 130.02u 0.02s 130.04r 4014080kB ./loop_tvm.sh euler7 4 26 | 139.60u 0.02s 139.63r 7995392kB ./loop_specter.sh euler7 4 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Specter 2 | 3 | Specter is an implementation of [GenTiradentes' TinyVM][tvm] in Go (the original is written in C). 4 | 5 | This is very much to learn about virtual machines (this is my first attempt at a VM, so huge thanks to GenTiradentes for making a minimal one, easy to grasp). 6 | 7 | The whole implementation takes about 500 lines of Go code. 8 | 9 | ## Performance 10 | 11 | It runs all examples available in TinyVM's repository, at roughly 30% slower than C, and at 7% slower with bounds-checking disabled. See more about the benchmarks in the [bench subfolder][bench]. 12 | 13 | ## Memory 14 | 15 | Specter runs at a higher (but stable) memory footprint than its C counterpart, however note the following (copied [from the mailing list][nuts], thanks to Carlos Castillo, edited): 16 | 17 | > BTW: The memory use [...] hovers at a high-ish number (70Mb in my case) because the garbage collection code doesn't run until memory use exceeds a given threshold. When it does, all the generated intermediate strings/slices are found but the memory is not returned to the OS, it is instead re-used for any following allocations, so the memory usage stat according to the OS stays at that threshold. If you set the environment variable GOGCTRACE=1 before running your code you can see when the garbage collector runs, and what happened during the run. 18 | 19 | ## Thanks 20 | 21 | The great Go community on the [mailing list][nuts]. 22 | 23 | ## License 24 | 25 | The [BSD 3-Clause license][bsd]. 26 | 27 | [bsd]: http://opensource.org/licenses/BSD-3-Clause 28 | [tvm]: https://github.com/GenTiradentes/tinyvm 29 | [bench]: https://github.com/mna/specter/tree/master/bench 30 | [nuts]: https://groups.google.com/forum/#!topic/golang-nuts/XhK5tGUsZnQ 31 | -------------------------------------------------------------------------------- /vm/memory.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | const ( 4 | // Since the stack can grow, start smaller than TinyVM (256 bytes) 5 | _STACK_CAP = 256 / 4 6 | 7 | // The heap is 64Mb in TinyVM, but the stack uses 2Mb of it. Since it is implemented 8 | // here as an array of int32, divide by 4 bytes, so that this is the number of elements 9 | // in the heap array. 10 | _HEAP_CAP = (62 * 1024 * 1024) / 4 11 | ) 12 | 13 | // Register: changes vs TinyVM 14 | // 15 | // i32_ptr is not needed, TinyVM uses it for registers that hold stack limits, not 16 | // needed given how the stack is implemented in specter (not a pointer into the 17 | // memory used for the heap). 18 | // i16 (high and low) is unused in TinyVM, dropped from this implementation. 19 | // So register is basically an int32! 20 | 21 | // The memory struct holds the memory representation of the VM (the registers, 22 | // the stack and the heap). 23 | type memory struct { 24 | // Special-use "registers" 25 | // FLAGS is similar to x86 register: 26 | // 0x1 = equal 27 | // 0x2 = greater 28 | FLAGS int32 29 | remainder int32 30 | 31 | // A fixed array for the regular registers 32 | registers []int32 33 | 34 | // Different approach than TinyVM for the stack, since it can only hold 35 | // integers, use a slice. 36 | stack []int32 37 | stackPos int32 38 | heap []int32 39 | } 40 | 41 | // Create a memory struct 42 | func newMemory() *memory { 43 | var m memory 44 | 45 | // Create the registers 46 | m.registers = make([]int32, rg_count) 47 | // Create the stack with initial capacity 48 | m.stack = make([]int32, 0, _STACK_CAP) 49 | // The heap is a fixed amount of memory, set the length so that it is completely indexable 50 | m.heap = make([]int32, _HEAP_CAP) 51 | return &m 52 | } 53 | 54 | // Push value on the stack. 55 | func (m *memory) pushStack(i int32) { 56 | m.stack = append(m.stack, i) 57 | m.stackPos++ 58 | } 59 | 60 | // Pop value from the stack. 61 | func (m *memory) popStack(i *int32) { 62 | m.stackPos-- 63 | *i = m.stack[m.stackPos] 64 | m.stack = m.stack[:m.stackPos] 65 | } 66 | -------------------------------------------------------------------------------- /bench/Makefile: -------------------------------------------------------------------------------- 1 | FILE=fib 2 | LOOPS=1000 3 | PROFFILE=cpu.prof 4 | PROF=$(GOPATH)/src/github.com/mna/specter/bench/$(PROFFILE) 5 | SPECTER=$(GOPATH)/bin/cmd 6 | TVMI=$(SUBWRKSPC)/tinyvm/bin/tvmi 7 | RESULTS=$(GOPATH)/src/github.com/mna/specter/bench/results 8 | EXDIR=$(GOPATH)/src/github.com/mna/specter/cmd/examples 9 | 10 | .SILENT: 11 | # Do not print Make commands 12 | 13 | .NOTPARALLEL: 14 | # Make sure there is no parallelism 15 | 16 | bench: 17 | # Times the execution of the specified file for both implementations, printing only 18 | # the timing information. 19 | # Not needed anymore, C time will not change... 20 | "./xtime.sh" "./loop_tvm.sh" $(FILE) $(LOOPS) | tail -n 0 21 | "./xtime.sh" "./loop_specter.sh" $(FILE) $(LOOPS) | tail -n 0 22 | 23 | all: 24 | # Run the benchmark (timing the execution) for all .vm files 25 | echo This will take a few minutes... 26 | echo 27 | $(MAKE) bench FILE=fact 28 | $(MAKE) bench FILE=fib 29 | $(MAKE) bench FILE=jsr 30 | $(MAKE) bench FILE=loop 31 | $(MAKE) bench FILE=loop2 32 | $(MAKE) bench FILE=loop3 LOOPS=10 # takes a lot of time 33 | $(MAKE) bench FILE=nop 34 | $(MAKE) bench FILE=primes 35 | $(MAKE) bench FILE=stack_bench 36 | $(MAKE) bench FILE=euler1 37 | $(MAKE) bench FILE=euler1_nodiv 38 | $(MAKE) bench FILE=euler2 39 | $(MAKE) bench FILE=euler7 LOOPS=4 # euler7 takes over 30secs, limit to 4 loops only! 40 | 41 | install: 42 | # Build and install the packages (VM and Main) 43 | go install github.com/mna/specter/... 44 | 45 | prof: 46 | # Run in profiling mode 47 | $(SPECTER) -cpu=$(PROF) $(EXDIR)/$(FILE).vm 48 | go tool pprof $(SPECTER) $(PROF) 49 | 50 | run: 51 | # Run a single iteration, not timed, of the specified file for each implementation 52 | $(SPECTER) $(GOPATH)/src/github.com/mna/specter/cmd/examples/$(FILE).vm 53 | $(TVMI) $(GOPATH)/src/github.com/mna/specter/cmd/examples/$(FILE).vm 54 | 55 | gdb: 56 | gdb $(SPECTER) -d $(GOROOT) 57 | 58 | clear: 59 | rm $(SPECTER) 60 | # rm $(GOPATH)/pkg/linux_amd64/github.com/mna/specter/vm.a 61 | rm $(GOPATH)/pkg/darwin_amd64/github.com/mna/specter/vm.a 62 | 63 | cmp: 64 | # Run and save the output for the specified file for each implementation, and compare the results. 65 | # This is to make sure both have the same output. 66 | echo No output means all is ok. 67 | $(SPECTER) $(GOPATH)/src/github.com/mna/specter/cmd/examples/$(FILE).vm > $(RESULTS)/tmp_specter 68 | $(TVMI) $(GOPATH)/src/github.com/mna/specter/cmd/examples/$(FILE).vm > $(RESULTS)/tmp_tvmi 69 | diff $(RESULTS)/tmp_specter $(RESULTS)/tmp_tvmi 70 | 71 | .PHONY: bench all prof run install cmp gdb 72 | -------------------------------------------------------------------------------- /vm/vm.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | "strconv" 8 | ) 9 | 10 | const _PBUF_CAP = 10 11 | 12 | // The VM, with its program and memory abstractions. 13 | type VM struct { 14 | p *program 15 | m *memory 16 | b *bufio.Writer 17 | pbuf []byte 18 | } 19 | 20 | // Create a new VM. 21 | func New() *VM { 22 | return NewWithWriter(os.Stdout) 23 | } 24 | 25 | // Create a new VM with the specified output stream. 26 | func NewWithWriter(w io.Writer) *VM { 27 | return &VM{newProgram(), newMemory(), bufio.NewWriter(w), make([]byte, 0, _PBUF_CAP)} 28 | } 29 | 30 | // Run executes the vm bytecode read by the reader. 31 | func (vm *VM) Run(r io.Reader) { 32 | var i int32 33 | 34 | // Parse the content to execute. 35 | vm.parse(r) 36 | 37 | // Execution loop. 38 | defer vm.b.Flush() 39 | for i = vm.p.start; vm.p.instrs[i] != _OP_END; i++ { 40 | vm.runInstruction(&i) 41 | } 42 | } 43 | 44 | // Run a single instruction. 45 | func (vm *VM) runInstruction(instrIndex *int32) { 46 | a0, a1 := vm.p.args[((*instrIndex)*2)], vm.p.args[((*instrIndex)*2)+1] 47 | 48 | switch vm.p.instrs[*instrIndex] { 49 | case _OP_NOP: 50 | // Nothing 51 | case _OP_INT: 52 | // Not implemented 53 | case _OP_MOV: 54 | *a0 = *a1 55 | case _OP_PUSH: 56 | vm.m.pushStack(*a0) 57 | case _OP_POP: 58 | vm.m.popStack(a0) 59 | case _OP_PUSHF: 60 | vm.m.pushStack(vm.m.FLAGS) 61 | case _OP_POPF: 62 | vm.m.popStack(a0) 63 | case _OP_INC: 64 | (*a0)++ 65 | case _OP_DEC: 66 | (*a0)-- 67 | case _OP_ADD: 68 | *a0 += *a1 69 | case _OP_SUB: 70 | *a0 -= *a1 71 | case _OP_MUL: 72 | *a0 *= *a1 73 | case _OP_DIV: 74 | *a0 /= *a1 75 | case _OP_MOD: 76 | vm.m.remainder = *a0 % *a1 77 | case _OP_REM: 78 | *a0 = vm.m.remainder 79 | case _OP_NOT: 80 | *a0 = ^(*a0) 81 | case _OP_XOR: 82 | *a0 ^= *a1 83 | case _OP_OR: 84 | *a0 |= *a1 85 | case _OP_AND: 86 | *a0 &= *a1 87 | case _OP_SHL: 88 | // cannot shift on signed int32 89 | if *a1 > 0 { 90 | *a0 <<= uint(*a1) 91 | } 92 | case _OP_SHR: 93 | // cannot shift on signed int32 94 | if *a1 > 0 { 95 | *a0 >>= uint(*a1) 96 | } 97 | case _OP_CMP: 98 | if *a0 == *a1 { 99 | vm.m.FLAGS = 0x1 100 | } else if *a0 > *a1 { 101 | vm.m.FLAGS = 0x2 102 | } else { 103 | vm.m.FLAGS = 0x0 104 | } 105 | case _OP_CALL: 106 | vm.m.pushStack(*instrIndex) 107 | fallthrough 108 | case _OP_JMP: 109 | *instrIndex = *a0 - 1 110 | case _OP_RET: 111 | vm.m.popStack(instrIndex) 112 | case _OP_JE: 113 | if vm.m.FLAGS&0x1 != 0 { 114 | *instrIndex = *a0 - 1 115 | } 116 | case _OP_JNE: 117 | if vm.m.FLAGS&0x1 == 0 { 118 | *instrIndex = *a0 - 1 119 | } 120 | case _OP_JG: 121 | if vm.m.FLAGS&0x2 != 0 { 122 | *instrIndex = *a0 - 1 123 | } 124 | case _OP_JGE: 125 | if vm.m.FLAGS&0x3 != 0 { 126 | *instrIndex = *a0 - 1 127 | } 128 | case _OP_JL: 129 | if vm.m.FLAGS&0x3 == 0 { 130 | *instrIndex = *a0 - 1 131 | } 132 | case _OP_JLE: 133 | if vm.m.FLAGS&0x2 == 0 { 134 | *instrIndex = *a0 - 1 135 | } 136 | case _OP_PRN: 137 | vm.pbuf = vm.pbuf[:0] // Clear buffer 138 | vm.pbuf = strconv.AppendInt(vm.pbuf, int64(*a0), 10) 139 | vm.pbuf = append(vm.pbuf, '\n') 140 | vm.b.Write(vm.pbuf) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /bench/results/all_20130112: -------------------------------------------------------------------------------- 1 | Running benchmark for fact.vm 1000 times 2 | 3 | TinyVM 4 | user-time sys-time real-time max-mem cmd... 5 | 0.88u 1.44s 2.77r 4063232kB ./loop_tvm.sh fact 1000 6 | 7 | Specter 8 | user-time sys-time real-time max-mem cmd... 9 | 2.03u 0.84s 3.56r 5505024kB ./loop_specter.sh fact 1000 10 | 11 | Running benchmark for fib.vm 1000 times 12 | 13 | TinyVM 14 | user-time sys-time real-time max-mem cmd... 15 | 0.80u 0.71s 1.93r 4063232kB ./loop_tvm.sh fib 1000 16 | 17 | Specter 18 | user-time sys-time real-time max-mem cmd... 19 | 1.98u 0.83s 3.51r 5652480kB ./loop_specter.sh fib 1000 20 | 21 | Running benchmark for jsr.vm 1000 times 22 | 23 | TinyVM 24 | user-time sys-time real-time max-mem cmd... 25 | 0.80u 1.46s 2.72r 4063232kB ./loop_tvm.sh jsr 1000 26 | 27 | Specter 28 | user-time sys-time real-time max-mem cmd... 29 | 1.95u 0.82s 3.46r 5668864kB ./loop_specter.sh jsr 1000 30 | 31 | Running benchmark for loop.vm 1000 times 32 | 33 | TinyVM 34 | user-time sys-time real-time max-mem cmd... 35 | 1.27u 0.71s 2.39r 4063232kB ./loop_tvm.sh loop 1000 36 | 37 | Specter 38 | user-time sys-time real-time max-mem cmd... 39 | 4.61u 2.46s 7.77r 5652480kB ./loop_specter.sh loop 1000 40 | 41 | Running benchmark for nop.vm 1000 times 42 | 43 | TinyVM 44 | user-time sys-time real-time max-mem cmd... 45 | 21.38u 0.78s 22.59r 4063232kB ./loop_tvm.sh nop 1000 46 | 47 | Specter 48 | user-time sys-time real-time max-mem cmd... 49 | 38.51u 0.84s 40.03r 5308416kB ./loop_specter.sh nop 1000 50 | 51 | Running benchmark for primes.vm 1000 times 52 | 53 | TinyVM 54 | user-time sys-time real-time max-mem cmd... 55 | 0.84u 0.71s 1.97r 4063232kB ./loop_tvm.sh primes 1000 56 | 57 | Specter 58 | user-time sys-time real-time max-mem cmd... 59 | 2.01u 0.82s 3.54r 5668864kB ./loop_specter.sh primes 1000 60 | 61 | Running benchmark for stack_bench.vm 1000 times 62 | 63 | TinyVM 64 | user-time sys-time real-time max-mem cmd... 65 | 0.82u 1.56s 2.84r 4063232kB ./loop_tvm.sh stack_bench 1000 66 | 67 | Specter 68 | user-time sys-time real-time max-mem cmd... 69 | 1.97u 0.82s 3.51r 5668864kB ./loop_specter.sh stack_bench 1000 70 | 71 | Running benchmark for euler1.vm 1000 times 72 | 73 | TinyVM 74 | user-time sys-time real-time max-mem cmd... 75 | 108.98u 0.82s 110.24r 4063232kB ./loop_tvm.sh euler1 1000 76 | 77 | Specter 78 | user-time sys-time real-time max-mem cmd... 79 | 124.19u 0.94s 125.82r 5668864kB ./loop_specter.sh euler1 1000 80 | 81 | Running benchmark for euler1_nodiv.vm 1000 times 82 | 83 | TinyVM 84 | user-time sys-time real-time max-mem cmd... 85 | 21.38u 0.79s 22.59r 4063232kB ./loop_tvm.sh euler1_nodiv 1000 86 | 87 | Specter 88 | user-time sys-time real-time max-mem cmd... 89 | 20.02u 0.85s 21.55r 5685248kB ./loop_specter.sh euler1_nodiv 1000 90 | 91 | Running benchmark for euler2.vm 1000 times 92 | 93 | TinyVM 94 | user-time sys-time real-time max-mem cmd... 95 | 0.84u 0.73s 1.99r 4063232kB ./loop_tvm.sh euler2 1000 96 | 97 | Specter 98 | user-time sys-time real-time max-mem cmd... 99 | 1.98u 0.81s 3.50r 5685248kB ./loop_specter.sh euler2 1000 100 | 101 | Running benchmark for euler7.vm 4 times 102 | 103 | TinyVM 104 | user-time sys-time real-time max-mem cmd... 105 | 129.50u 0.02s 129.53r 4030464kB ./loop_tvm.sh euler7 4 106 | 107 | Specter 108 | user-time sys-time real-time max-mem cmd... 109 | 154.87u 0.03s 154.90r 5341184kB ./loop_specter.sh euler7 4 110 | -------------------------------------------------------------------------------- /vm/codes.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | // The operation code type. 4 | type opcode int32 5 | 6 | // List of available opcodes. 7 | const ( 8 | _OP_END opcode = iota - 1 9 | _OP_NOP 10 | _OP_INT 11 | _OP_MOV 12 | _OP_PUSH 13 | _OP_POP 14 | _OP_PUSHF 15 | _OP_POPF 16 | _OP_INC 17 | _OP_DEC 18 | _OP_ADD 19 | _OP_SUB 20 | _OP_MUL 21 | _OP_DIV 22 | _OP_MOD 23 | _OP_REM 24 | _OP_NOT 25 | _OP_XOR 26 | _OP_OR 27 | _OP_AND 28 | _OP_SHL 29 | _OP_SHR 30 | _OP_CMP 31 | _OP_CALL 32 | _OP_JMP 33 | _OP_RET 34 | _OP_JE 35 | _OP_JNE 36 | _OP_JG 37 | _OP_JGE 38 | _OP_JL 39 | _OP_JLE 40 | _OP_PRN 41 | ) 42 | 43 | var ( 44 | // Reverse lookup of opcodes (opcode index = opcode string name) 45 | opsRev = []string{ 46 | _OP_NOP: "nop", 47 | _OP_INT: "int", 48 | _OP_MOV: "mov", 49 | _OP_PUSH: "push", 50 | _OP_POP: "pop", 51 | _OP_PUSHF: "pushf", 52 | _OP_POPF: "popf", 53 | _OP_INC: "inc", 54 | _OP_DEC: "dec", 55 | _OP_ADD: "add", 56 | _OP_SUB: "sub", 57 | _OP_MUL: "mul", 58 | _OP_DIV: "div", 59 | _OP_MOD: "mod", 60 | _OP_REM: "rem", 61 | _OP_NOT: "not", 62 | _OP_XOR: "xor", 63 | _OP_OR: "or", 64 | _OP_AND: "and", 65 | _OP_SHL: "shl", 66 | _OP_SHR: "shr", 67 | _OP_CMP: "cmp", 68 | _OP_CALL: "call", 69 | _OP_JMP: "jmp", 70 | _OP_RET: "ret", 71 | _OP_JE: "je", 72 | _OP_JNE: "jne", 73 | _OP_JG: "jg", 74 | _OP_JGE: "jge", 75 | _OP_JL: "jl", 76 | _OP_JLE: "jle", 77 | _OP_PRN: "prn", 78 | } 79 | 80 | // Lookup of opcodes (opcode string name key = opcode integer value) 81 | opsMap = map[string]opcode{ 82 | "nop": _OP_NOP, 83 | "int": _OP_INT, 84 | "mov": _OP_MOV, 85 | "push": _OP_PUSH, 86 | "pop": _OP_POP, 87 | "pushf": _OP_PUSHF, 88 | "popf": _OP_POPF, 89 | "inc": _OP_INC, 90 | "dec": _OP_DEC, 91 | "add": _OP_ADD, 92 | "sub": _OP_SUB, 93 | "mul": _OP_MUL, 94 | "div": _OP_DIV, 95 | "mod": _OP_MOD, 96 | "rem": _OP_REM, 97 | "not": _OP_NOT, 98 | "xor": _OP_XOR, 99 | "or": _OP_OR, 100 | "and": _OP_AND, 101 | "shl": _OP_SHL, 102 | "shr": _OP_SHR, 103 | "cmp": _OP_CMP, 104 | "call": _OP_CALL, 105 | "jmp": _OP_JMP, 106 | "ret": _OP_RET, 107 | "je": _OP_JE, 108 | "jne": _OP_JNE, 109 | "jg": _OP_JG, 110 | "jge": _OP_JGE, 111 | "jl": _OP_JL, 112 | "jle": _OP_JLE, 113 | "prn": _OP_PRN, 114 | } 115 | ) 116 | 117 | // Stringer implementation for debugging purpose. 118 | func (o opcode) String() string { 119 | return opsRev[o] 120 | } 121 | 122 | // Register code type. 123 | type regcode int32 124 | 125 | // List of available register codes. 126 | const ( 127 | _RG_EAX regcode = iota 128 | _RG_EBX 129 | _RG_ECX 130 | _RG_EDX 131 | _RG_ESI 132 | _RG_EDI 133 | _RG_ESP 134 | _RG_EBP 135 | _RG_EIP 136 | _RG_R08 137 | _RG_R09 138 | _RG_R10 139 | _RG_R11 140 | _RG_R12 141 | _RG_R13 142 | _RG_R14 143 | _RG_R15 144 | rg_count 145 | ) 146 | 147 | // Lookup map of registers (register string name key = register integer code) 148 | var rgsMap = map[string]regcode{ 149 | "eax": _RG_EAX, 150 | "ebx": _RG_EBX, 151 | "ecx": _RG_ECX, 152 | "edx": _RG_EDX, 153 | "esi": _RG_ESI, 154 | "edi": _RG_EDI, 155 | "esp": _RG_ESP, 156 | "ebp": _RG_EBP, 157 | "eip": _RG_EIP, 158 | "r08": _RG_R08, 159 | "r09": _RG_R09, 160 | "r10": _RG_R10, 161 | "r11": _RG_R11, 162 | "r12": _RG_R12, 163 | "r13": _RG_R13, 164 | "r14": _RG_R14, 165 | "r15": _RG_R15, 166 | } 167 | -------------------------------------------------------------------------------- /SYNTAX: -------------------------------------------------------------------------------- 1 | This virtual machine loosely follows traditional Intel x86 assembly syntax. 2 | 3 | ////////////////////////////////////////////////// 4 | // Table of Contents ///////////////////////////// 5 | ////////////////////////////////////////////////// 6 | 0. VALUES 7 | 1. REGISTERS 8 | 2. MEMORY 9 | 3. LABELS 10 | 4. INSTRUCTION LISTING 11 | I. Memory 12 | II. Stack 13 | III. Calling Conventions 14 | IV. Arithmetic Operators 15 | V. Binary Operators 16 | VI. Comparison 17 | VII. Control Flow Manipulation 18 | VIII. Input / Output 19 | 20 | ////////////////////////////////////////////////// 21 | // 0. VALUES ///////////////////////////////////// 22 | ////////////////////////////////////////////////// 23 | 24 | Values can be specified in decimal, octal, hexadecimal, or binary. The only difference between Intel syntax assembly and this 25 | syntax is the delimiter between the value and the base specifier. By default, values without a base specifier are assumed 26 | to be in decimal. Any value prepended with "0x" is assumed to be in hexadecimal. 27 | 28 | Values can also be specified using base identifiers. To specify the value "32," I can use 0x20 for hexadecimal, 20|h for hexadecimal 29 | using a base identifier, 40|o for octal using a base identifier, or 100000|b for binary using a base identifer. 30 | 31 | ////////////////////////////////////////////////// 32 | // 1. REGISTERS ////////////////////////////////// 33 | ////////////////////////////////////////////////// 34 | 35 | TVM has 17 registers, modeled after x86 registers. 36 | Register names are written lower-case. 37 | Because of the implementation of the stack in Specter, all registers are general purpose and 38 | can be freely used (the instruction pointer is not available via a special register, jumps and call 39 | must be used to alter the flow). 40 | 41 | EAX 42 | EBX 43 | ECX 44 | EDX 45 | 46 | ESI 47 | EDI 48 | 49 | ESP 50 | EBP 51 | 52 | EIP 53 | 54 | R08 - R15 55 | 56 | ////////////////////////////////////////////////// 57 | // 2. MEMORY ///////////////////////////////////// 58 | ////////////////////////////////////////////////// 59 | 60 | Memory addresses are specified using brackets, in units of four bytes. Programs running within the virtual machine have their own 61 | address space, so no positive address within the address space is off limits. 62 | 63 | Unlike TinyVM, Specter does not use a part of the heap memory for the stack, they are separate containers. So there are no areas of memory that are off limits. 64 | 65 | To specify the 256th word in the address space, you can use [256], [100|h], [0x100], or [100000000|b]. Any syntax that's 66 | valid when specifying a value is valid when specifying an address. 67 | 68 | ////////////////////////////////////////////////// 69 | // 3. LABELS ///////////////////////////////////// 70 | ////////////////////////////////////////////////// 71 | 72 | Labels are specified by appending a colon to an identifier. Local labels are not yet supported. 73 | 74 | Labels must be specified at the beginning of a line or on their own line. 75 | 76 | ////////////////////////////////////////////////// 77 | // 4. INSTRUCTION LISTING //////////////////////// 78 | ////////////////////////////////////////////////// 79 | 80 | Instructions listed are displayed in complete usage form, with example arguments, enclosed in square brackets. 81 | The square brackets are not to be used in actual TVM programs. 82 | 83 | // I. Memory // 84 | 85 | [mov arg0, arg1] 86 | Moves value specified from arg1 to arg0 87 | 88 | // II. Stack // 89 | 90 | [push arg] 91 | Pushes arg onto the stack 92 | 93 | [pop arg] 94 | Pops a value from the stack, storing it in arg 95 | 96 | [pushf] 97 | Pushes the FLAGS register to the stack 98 | 99 | [popf arg] 100 | Pops the flag register to arg 101 | 102 | // III. Calling Conventions // 103 | 104 | [call address] 105 | Push the current address to the stack and jump to the subroutine specified 106 | 107 | [ret] 108 | Pop the previous address from the stack to the instruction pointer to return control to the caller 109 | 110 | // IV. Arithmetic Operators // 111 | 112 | [inc arg] 113 | Increments arg 114 | 115 | [dec arg] 116 | Decrements arg 117 | 118 | [add arg0, arg1] 119 | Adds arg1 to arg0, storing the result in arg0 120 | 121 | [sub arg0, arg1] 122 | Subtracts arg1 from arg0, storing the result in arg0 123 | 124 | [mul arg0, arg1] 125 | Multiplies arg1 and arg0, storing the result in arg0 126 | 127 | [div arg0, arg1] 128 | Divides arg0 by arg1, storing the quotient in arg0 129 | 130 | [mod arg0, arg1] 131 | Same as the '%' (modulus) operator in C. Calculates arg0 mod arg1 and stores the result in the remainder register. 132 | 133 | [rem arg] 134 | Retrieves the value stored in the remainder register, storing it in arg 135 | 136 | // V. Binary Operators // 137 | 138 | [not arg] 139 | Calculates the binary NOT of arg, storing it in arg 140 | 141 | [xor arg0, arg1] 142 | Calculates the binary XOR of arg0 and arg1, storing the result in arg0 143 | 144 | [or arg0, arg1] 145 | Calculates the binary OR of arg0 and arg1, storing the result in arg0 146 | 147 | [and arg0, arg1] 148 | Calculates the binary AND of arg0 and arg1, storing the result in arg0 149 | 150 | [shl arg0, arg1] 151 | Shift arg0 left by arg1 places 152 | 153 | [shr arg0, arg1] 154 | Shifts arg0 right by arg1 places 155 | 156 | // VI. Comparison // 157 | 158 | [cmp arg0, arg1] 159 | Compares arg0 and arg1, storing the result in the FLAGS register 160 | 161 | // VII. Control Flow Manipulation // 162 | 163 | [jmp address] 164 | Jumps to an address or label 165 | 166 | [je address] 167 | Jump if equal 168 | 169 | [jne address] 170 | Jump if not equal 171 | 172 | [jg address] 173 | Jump if greater 174 | 175 | [jge address] 176 | Jump if equal or greater 177 | 178 | [jl address] 179 | Jump if lesser 180 | 181 | [jle address] 182 | Jump if lesser or equal 183 | 184 | // VIII. Input / Output // 185 | 186 | [prn arg] 187 | Print an integer 188 | -------------------------------------------------------------------------------- /vm/parser_test.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | var ( 10 | assertOps = map[string][]opcode{ 11 | "euler1.vm": []opcode{ 12 | _OP_MOV, 13 | _OP_MOV, 14 | _OP_MOV, 15 | _OP_MOD, 16 | _OP_REM, 17 | _OP_CMP, 18 | _OP_JNE, 19 | _OP_ADD, 20 | _OP_JE, 21 | _OP_MOV, 22 | _OP_MOD, 23 | _OP_REM, 24 | _OP_CMP, 25 | _OP_JNE, 26 | _OP_ADD, 27 | _OP_INC, 28 | _OP_CMP, 29 | _OP_JL, 30 | _OP_PRN, 31 | _OP_END, 32 | }, 33 | "euler1_nodiv.vm": []opcode{ 34 | _OP_MOV, 35 | _OP_XOR, 36 | _OP_MOV, 37 | _OP_ADD, 38 | _OP_ADD, 39 | _OP_CMP, 40 | _OP_JL, 41 | _OP_MOV, 42 | _OP_ADD, 43 | _OP_ADD, 44 | _OP_CMP, 45 | _OP_JL, 46 | _OP_MOV, 47 | _OP_ADD, 48 | _OP_ADD, 49 | _OP_CMP, 50 | _OP_JL, 51 | _OP_PRN, 52 | _OP_END, 53 | }, 54 | "euler2.vm": []opcode{ 55 | _OP_MOV, 56 | _OP_MOV, 57 | _OP_MOV, 58 | _OP_ADD, 59 | _OP_ADD, 60 | _OP_MOV, 61 | _OP_AND, 62 | _OP_CMP, 63 | _OP_JNE, 64 | _OP_ADD, 65 | _OP_MOV, 66 | _OP_AND, 67 | _OP_CMP, 68 | _OP_JNE, 69 | _OP_ADD, 70 | _OP_CMP, 71 | _OP_JG, 72 | _OP_CMP, 73 | _OP_JL, 74 | _OP_PRN, 75 | _OP_END, 76 | }, 77 | "euler7": []opcode{ 78 | _OP_MOV, 79 | _OP_MOV, 80 | _OP_CMP, 81 | _OP_JE, 82 | _OP_MOD, 83 | _OP_REM, 84 | _OP_CMP, 85 | _OP_JE, 86 | _OP_INC, 87 | _OP_JMP, 88 | _OP_INC, 89 | _OP_CMP, 90 | _OP_JE, 91 | _OP_INC, 92 | _OP_JMP, 93 | _OP_PRN, 94 | _OP_END, 95 | }, 96 | "fact.vm": []opcode{ 97 | _OP_PUSH, 98 | _OP_PUSH, 99 | _OP_PUSH, 100 | _OP_MOV, 101 | _OP_MOV, 102 | _OP_CMP, 103 | _OP_JLE, 104 | _OP_MOV, 105 | _OP_DEC, 106 | _OP_CALL, 107 | _OP_MUL, 108 | _OP_POP, 109 | _OP_POP, 110 | _OP_POP, 111 | _OP_RET, 112 | _OP_MOV, 113 | _OP_INC, 114 | _OP_CALL, 115 | _OP_PRN, 116 | _OP_CMP, 117 | _OP_JL, 118 | _OP_END, 119 | }, 120 | "fib.vm": []opcode{ 121 | _OP_MOV, 122 | _OP_MOV, 123 | _OP_ADD, 124 | _OP_ADD, 125 | _OP_PRN, 126 | _OP_PRN, 127 | _OP_CMP, 128 | _OP_JL, 129 | _OP_CMP, 130 | _OP_JG, 131 | _OP_END, 132 | }, 133 | "jsr.vm": []opcode{ 134 | _OP_PUSH, 135 | _OP_MOV, 136 | _OP_PRN, 137 | _OP_POP, 138 | _OP_RET, 139 | _OP_MOV, 140 | _OP_CALL, 141 | _OP_MOV, 142 | _OP_CALL, 143 | _OP_END, 144 | }, 145 | // No need to test loop2 and loop3, exact same files as loop.vm, only the iterations are changed. 146 | "loop.vm": []opcode{ 147 | _OP_INC, 148 | _OP_CMP, 149 | _OP_PRN, 150 | _OP_JL, 151 | _OP_END, 152 | }, 153 | "nop.vm": []opcode{ 154 | _OP_MOV, 155 | _OP_NOP, 156 | _OP_INC, 157 | _OP_CMP, 158 | _OP_JL, 159 | _OP_END, 160 | }, 161 | "primes.vm": []opcode{ 162 | _OP_MOV, 163 | _OP_MOV, 164 | _OP_CMP, 165 | _OP_JE, 166 | _OP_MOD, 167 | _OP_REM, 168 | _OP_CMP, 169 | _OP_JE, 170 | _OP_INC, 171 | _OP_JMP, 172 | _OP_PRN, 173 | _OP_INC, 174 | _OP_CMP, 175 | _OP_JL, 176 | _OP_END, 177 | }, 178 | "stack_bench.vm": []opcode{ 179 | _OP_MOV, 180 | _OP_MOV, 181 | _OP_PUSH, 182 | _OP_INC, 183 | _OP_CMP, 184 | _OP_PRN, 185 | _OP_JNE, 186 | _OP_POP, 187 | _OP_DEC, 188 | _OP_CMP, 189 | _OP_PRN, 190 | _OP_JNE, 191 | _OP_END, 192 | }, 193 | } 194 | 195 | assertLabels = map[string]map[string]int32{ 196 | "euler1.vm": map[string]int32{ 197 | "start": 0, 198 | "L0": 2, 199 | "L1": 9, 200 | "check": 15, 201 | }, 202 | "euler1_nodiv.vm": map[string]int32{ 203 | "start": 0, 204 | "loop0": 3, 205 | "loop1": 8, 206 | "loop2": 13, 207 | }, 208 | "euler2.vm": map[string]int32{ 209 | "start": 0, 210 | "loop": 3, 211 | "L0": 10, 212 | "L1": 15, 213 | "end": 19, 214 | }, 215 | "euler7.vm": map[string]int32{ 216 | "start": 0, 217 | "checkPrime": 1, 218 | "checkFactor": 2, 219 | "primeFound": 10, 220 | "nextPrime": 13, 221 | "printResult": 15, 222 | }, 223 | "fact.vm": map[string]int32{ 224 | "fact": 0, 225 | "end_fact": 11, 226 | "start": 15, 227 | "loop": 16, 228 | }, 229 | "fib.vm": map[string]int32{ 230 | "start": 0, 231 | "loop": 2, 232 | "end": 10, 233 | }, 234 | "jsr.vm": map[string]int32{ 235 | "print_eax": 0, 236 | "start": 5, 237 | }, 238 | "loop.vm": map[string]int32{ 239 | "loop": 0, 240 | }, 241 | "nop.vm": map[string]int32{ 242 | "loop": 1, 243 | }, 244 | "primes.vm": map[string]int32{ 245 | "start": 0, 246 | "checkPrime": 1, 247 | "checkFactor": 2, 248 | "primeFound": 10, 249 | "nextPrime": 11, 250 | }, 251 | "stack_bench.vm": map[string]int32{ 252 | "start": 0, 253 | "loop0": 2, 254 | "loop1": 7, 255 | }, 256 | } 257 | ) 258 | 259 | // Test all the .vm files that don't fail. 260 | func TestParse(t *testing.T) { 261 | fi := getAllExampleFiles() 262 | for _, path := range fi { 263 | base := filepath.Base(path) 264 | // Ignore err_ files (they would panic anyway) 265 | if expCodes, ok := assertOps[base]; ok { 266 | testFile(path, expCodes, assertLabels[base], t) 267 | } 268 | } 269 | } 270 | 271 | // Validate the start position, the instructions, and the labels (the arguments 272 | // will be validated by the behaviour tests, not the parser). 273 | func testFile(path string, expCodes []opcode, expLabels map[string]int32, t *testing.T) { 274 | base := filepath.Base(path) 275 | f, err := os.Open(path) 276 | if err != nil { 277 | t.Error(err) 278 | } else { 279 | vm := New() 280 | vm.parse(f) 281 | 282 | // Assert the start position 283 | if strt, ok := expLabels["start"]; ok { 284 | if vm.p.start != strt { 285 | t.Errorf("file %s: expected start instruction to be %d, got %d", base, strt, vm.p.start) 286 | } 287 | } 288 | 289 | // Instructions 290 | if len(vm.p.instrs) != len(expCodes) { 291 | t.Errorf("file %s: expected %d instructions, got %d", base, len(expCodes), len(vm.p.instrs)) 292 | } else { 293 | for i, code := range vm.p.instrs { 294 | if code != expCodes[i] { 295 | t.Errorf("file %s: expected opcode %s at index %d, got %s", base, expCodes[i], i, code) 296 | } 297 | } 298 | } 299 | 300 | // Labels 301 | if len(vm.p.labels) != len(expLabels) { 302 | t.Errorf("file %s: expected %d labels, got %d", base, len(expLabels), len(vm.p.labels)) 303 | } else { 304 | for k, v := range vm.p.labels { 305 | exp, ok := expLabels[k] 306 | if !ok { 307 | t.Errorf("file %s: unexpected label %s", base, k) 308 | } else if v != exp { 309 | t.Errorf("file %s: expected label %s to jump to %d, got %d", base, k, exp, v) 310 | } 311 | } 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /vm/parser.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | // Initial lines slice capacity 13 | _LINES_CAP = 1000 14 | 15 | // Maximum number of arguments for an opcode 16 | _MAX_ARGS = 2 17 | ) 18 | 19 | // Parse the content from the reader, load it into the program so that it can 20 | // be executed. The parser makes minimal checks, because it is assumed that a 21 | // compiler/code generator produced its input, and it is up to this program 22 | // to give relevant errors. The VM is not there to give helpful errors to correct 23 | // its code. 24 | func (vm *VM) parse(r io.Reader) { 25 | bio := bufio.NewReader(r) 26 | lines := make([][]string, 0, _LINES_CAP) 27 | lIdx := 0 28 | 29 | // First pass is to load label definitions and instructions only, since it has to be there 30 | // before parsing the arguments (jump-to-label). 31 | for l, err := bio.ReadString('\n'); true; l, err = bio.ReadString('\n') { 32 | // Split the line in tokens (ReadString returns the delimiter) 33 | lines = append(lines, strings.FieldsFunc(l, func(r rune) bool { 34 | // Split on either a space, a comma or a tab (or newline) 35 | switch r { 36 | case ' ', ',', '\t', '\n': 37 | return true 38 | } 39 | return false 40 | })) 41 | 42 | hasInstr := false 43 | // Loop through the tokens, store labels and instructions 44 | for _, tok := range lines[lIdx] { 45 | if strings.HasPrefix(tok, "#") { 46 | // This is a comment, ignore all other tokens on this line 47 | break 48 | } 49 | // Ignore empty tokens 50 | if tok == "" { 51 | continue 52 | } 53 | 54 | // Is it a label definition? 55 | if vm.parseLabelDef(tok) { 56 | if hasInstr { 57 | panic(fmt.Sprintf("cannot define label '%s' after an instruction on the same line", tok)) 58 | } 59 | continue 60 | } 61 | 62 | // Is it an instruction (opcode)? 63 | if vm.parseInstr(tok) { 64 | hasInstr = true 65 | continue 66 | } 67 | } 68 | 69 | // If EOF or error, return 70 | if err != nil { 71 | if err != io.EOF { 72 | panic(err) 73 | } else { 74 | break 75 | } 76 | } 77 | // Increment line index 78 | lIdx++ 79 | } 80 | 81 | // Here we know exactly the number of instructions, so allocate the right size 82 | // for the arguments slice 83 | vm.p.args = make([]*int32, len(vm.p.instrs)*2) 84 | 85 | // Next, parse instruction arguments one line at a time, a single line can contain at most one instruction, 86 | // possibly zero if it is only a label (a line may also contain a label AND an 87 | // instruction). 88 | instrIdx := -1 89 | for _, toks := range lines { 90 | // Loop through the tokens, store arguments 91 | hasInstr := false 92 | argIdx := 0 93 | 94 | for _, tok := range toks { 95 | if strings.HasPrefix(tok, "#") { 96 | // This is a comment, ignore all other tokens on this line 97 | break 98 | } 99 | // Ignore empty tokens 100 | if tok == "" { 101 | continue 102 | } 103 | 104 | // Is it a label definition? 105 | if strings.HasSuffix(tok, ":") { 106 | continue 107 | } 108 | 109 | // Is it an instruction (opcode)? 110 | if _, ok := opsMap[tok]; ok { 111 | instrIdx++ 112 | hasInstr = true 113 | continue 114 | } 115 | 116 | // It is not a comment, nor a label definition, nor an instruction, so this is 117 | // an argument. Make sure an instruction has been found. 118 | if !hasInstr { 119 | panic(fmt.Sprintf("found argument token '%s' without an instruction", tok)) 120 | } else if argIdx >= _MAX_ARGS { 121 | panic(fmt.Sprintf("found excessive argument token '%s' after %d arguments", tok, _MAX_ARGS)) 122 | } 123 | if vm.parseRegister(tok, instrIdx, argIdx) { 124 | argIdx++ 125 | continue 126 | } 127 | if vm.parseLabelVal(tok, instrIdx, argIdx) { 128 | argIdx++ 129 | continue 130 | } 131 | if vm.parseAddress(tok, instrIdx, argIdx) { 132 | argIdx++ 133 | continue 134 | } 135 | // Parse value panics if the value is invalid, so must be last, and no need 136 | // to add a panic after the call (or a continue) 137 | if vm.parseValue(tok, instrIdx, argIdx) { 138 | argIdx++ 139 | } 140 | } 141 | } 142 | // Insert a program-ending instruction, useful in execution loop 143 | vm.p.instrs = append(vm.p.instrs, _OP_END) 144 | } 145 | 146 | // Parse a literal value (with an optional base code) 147 | func (vm *VM) parseValue(tok string, instrIdx int, argIdx int) bool { 148 | // In Go, it is totally legal to grab the address of a stack variable, so 149 | // we can avoid the p.values slice altogether. 150 | i32 := toValue(tok) 151 | vm.p.args[(instrIdx*2)+argIdx] = &i32 152 | return true 153 | } 154 | 155 | func toValue(tok string) int32 { 156 | sepIdx := strings.IndexRune(tok, '|') 157 | base := 0 158 | val := tok 159 | 160 | if sepIdx > 0 && sepIdx < (len(tok)-1) { 161 | val = tok[:sepIdx] 162 | switch tok[sepIdx+1:] { 163 | case "h": 164 | base = 16 165 | case "d": 166 | base = 10 167 | case "o": 168 | base = 8 169 | case "b": 170 | base = 2 171 | default: 172 | panic(fmt.Sprintf("invalid base notation for value token '%s'", tok)) 173 | } 174 | } 175 | // ParseInt natively supports decimals and hexadecimals (if value starts with 0x). 176 | // Other bases must use the | notation. 177 | if i, err := strconv.ParseInt(val, base, 32); err != nil { 178 | panic(err) 179 | } else { 180 | return int32(i) 181 | } 182 | panic("unreachable") 183 | } 184 | 185 | // Parse an address (heap) pointer, format: [123] 186 | func (vm *VM) parseAddress(tok string, instrIdx int, argIdx int) bool { 187 | if strings.HasPrefix(tok, "[") { 188 | i := toValue(tok[1 : len(tok)-1]) 189 | vm.p.args[(instrIdx*2)+argIdx] = &vm.m.heap[i] 190 | return true 191 | } 192 | 193 | return false 194 | } 195 | 196 | // Parse a register name. 197 | func (vm *VM) parseRegister(tok string, instrIdx int, argIdx int) bool { 198 | if reg, ok := rgsMap[tok]; ok { 199 | vm.p.args[(instrIdx*2)+argIdx] = &vm.m.registers[reg] 200 | return true 201 | } 202 | 203 | return false 204 | } 205 | 206 | // Parse an instruction code (opcode). 207 | func (vm *VM) parseInstr(tok string) bool { 208 | if op, ok := opsMap[tok]; ok { 209 | // This is an instruction token 210 | vm.p.instrs = append(vm.p.instrs, op) 211 | return true 212 | } 213 | 214 | return false 215 | } 216 | 217 | // Parse a label value (label used as argument, i.e. to a jump). 218 | func (vm *VM) parseLabelVal(tok string, instrIdx int, argIdx int) bool { 219 | if instr, ok := vm.p.labels[tok]; ok { 220 | // In Go, it is totally legal to grab the address of a stack variable, so 221 | // we can avoid the p.values slice altogether. 222 | var i32 int32 = int32(instr) 223 | vm.p.args[(instrIdx*2)+argIdx] = &i32 224 | return true 225 | } 226 | 227 | return false 228 | } 229 | 230 | // Parse a label definition. 231 | func (vm *VM) parseLabelDef(tok string) bool { 232 | if strings.HasSuffix(tok, ":") { 233 | // This is a label 234 | lbl := tok[:len(tok)-1] 235 | 236 | // Check if this is a register name (invalid label) 237 | if _, ok := rgsMap[lbl]; ok { 238 | // This label uses a register name 239 | panic(fmt.Sprintf("the register name '%s' cannot be used as label", lbl)) 240 | } 241 | // Check if this is a duplicate 242 | if _, ok := vm.p.labels[lbl]; ok { 243 | // This label already exists 244 | panic(fmt.Sprintf("a label '%s' already exists", lbl)) 245 | } 246 | // Store it with a pointer to the next instruction 247 | vm.p.labels[lbl] = int32(len(vm.p.instrs)) 248 | // If this is the special-case "start" label, store the start instruction 249 | if lbl == "start" { 250 | vm.p.start = int32(len(vm.p.instrs)) 251 | } 252 | 253 | return true 254 | } 255 | 256 | return false 257 | } 258 | -------------------------------------------------------------------------------- /vm/vm_test.go: -------------------------------------------------------------------------------- 1 | package vm 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | type testInfo struct { 9 | op opcode 10 | name string 11 | code string 12 | exp string 13 | } 14 | 15 | var tests = [...]testInfo{ 16 | testInfo{ 17 | _OP_MOV, 18 | "", 19 | ` 20 | mov eax, 17 21 | prn eax 22 | `, 23 | "17\n", 24 | }, 25 | testInfo{ 26 | _OP_PUSH, 27 | "", 28 | ` 29 | push 1 30 | push 2 31 | push 3 32 | pop eax 33 | prn eax 34 | pop eax 35 | prn eax 36 | pop eax 37 | prn eax 38 | `, 39 | "3\n2\n1\n", 40 | }, 41 | testInfo{ 42 | _OP_PUSH, 43 | "", 44 | ` 45 | push 10 46 | push 20 47 | push 30 48 | pop eax 49 | pop ebx 50 | pop ecx 51 | prn eax 52 | prn ebx 53 | prn ecx 54 | `, 55 | "30\n20\n10\n", 56 | }, 57 | testInfo{ 58 | _OP_PUSHF, 59 | "", 60 | ` 61 | mov eax, 10 62 | cmp eax, 3 63 | pushf 64 | pop ecx 65 | prn ecx 66 | `, 67 | "2\n", 68 | }, 69 | testInfo{ 70 | _OP_POPF, 71 | "", 72 | ` 73 | mov eax, 10 74 | cmp eax, 30 75 | pushf 76 | popf eax 77 | prn eax 78 | `, 79 | "0\n", 80 | }, 81 | testInfo{ 82 | _OP_INC, 83 | "", 84 | ` 85 | mov eax, 10 86 | inc eax 87 | prn eax 88 | `, 89 | "11\n", 90 | }, 91 | testInfo{ 92 | _OP_DEC, 93 | "", 94 | ` 95 | mov edx, 10 96 | dec edx 97 | prn edx 98 | `, 99 | "9\n", 100 | }, 101 | testInfo{ 102 | _OP_ADD, 103 | "", 104 | ` 105 | mov eax, 10 106 | mov ebx, 7 107 | add eax, ebx 108 | prn eax 109 | `, 110 | "17\n", 111 | }, 112 | testInfo{ 113 | _OP_SUB, 114 | "", 115 | ` 116 | mov eax, 10 117 | mov ebx, 7 118 | sub eax, ebx 119 | prn eax 120 | `, 121 | "3\n", 122 | }, 123 | testInfo{ 124 | _OP_MUL, 125 | "", 126 | ` 127 | mov eax, 10 128 | mov ebx, 7 129 | mul eax, ebx 130 | prn eax 131 | `, 132 | "70\n", 133 | }, 134 | testInfo{ 135 | _OP_DIV, 136 | "", 137 | ` 138 | mov eax, 30 139 | mov ebx, 7 140 | div eax, ebx 141 | prn eax 142 | `, 143 | "4\n", 144 | }, 145 | testInfo{ 146 | _OP_MOD, 147 | "", 148 | ` 149 | mov eax, 30 150 | mov ebx, 7 151 | mod eax, ebx 152 | rem edx 153 | prn edx 154 | `, 155 | "2\n", 156 | }, 157 | testInfo{ 158 | _OP_NOT, 159 | "", 160 | ` 161 | mov eax, 1 162 | not eax 163 | prn eax 164 | `, 165 | "-2\n", 166 | }, 167 | testInfo{ 168 | _OP_XOR, 169 | "", 170 | ` 171 | mov eax, 137 172 | mov ebx, 145 173 | xor eax, ebx 174 | prn eax 175 | `, 176 | "24\n", 177 | }, 178 | testInfo{ 179 | _OP_OR, 180 | "", 181 | ` 182 | mov eax, 195 183 | mov ebx, 224 184 | or eax, ebx 185 | prn eax 186 | `, 187 | "227\n", 188 | }, 189 | testInfo{ 190 | _OP_AND, 191 | "", 192 | ` 193 | mov eax, 232 194 | mov ebx, 42 195 | and eax, ebx 196 | prn eax 197 | `, 198 | "40\n", 199 | }, 200 | testInfo{ 201 | _OP_SHL, 202 | "", 203 | ` 204 | mov eax, 1045 205 | mov ebx, 3 206 | shl eax, ebx 207 | prn eax 208 | `, 209 | "8360\n", 210 | }, 211 | testInfo{ 212 | _OP_SHR, 213 | "", 214 | ` 215 | mov eax, 1045 216 | mov ebx, 3 217 | shr eax, ebx 218 | prn eax 219 | `, 220 | "130\n", 221 | }, 222 | testInfo{ 223 | _OP_CMP, 224 | "", 225 | ` 226 | mov eax, 12 227 | mov ebx, 9 228 | cmp eax, ebx 229 | pushf 230 | popf ecx 231 | prn ecx 232 | `, 233 | "2\n", 234 | }, 235 | testInfo{ 236 | _OP_CALL, // Tests RET too 237 | "", 238 | ` 239 | call 3 240 | prn 1 241 | jmp 5 242 | prn 3 243 | ret 244 | nop 245 | `, 246 | "3\n1\n", 247 | }, 248 | testInfo{ 249 | _OP_JMP, 250 | "", 251 | ` 252 | jmp 2 253 | prn 1 254 | prn 3 255 | `, 256 | "3\n", 257 | }, 258 | testInfo{ 259 | _OP_JE, 260 | "", 261 | ` 262 | mov eax 4 263 | mov ebx 4 264 | cmp eax, ebx 265 | je 5 266 | prn 1 267 | prn 2 268 | `, 269 | "2\n", 270 | }, 271 | testInfo{ 272 | _OP_JE, 273 | "false", 274 | ` 275 | mov eax 5 276 | mov ebx 4 277 | cmp eax, ebx 278 | je 5 279 | prn 1 280 | prn 2 281 | `, 282 | "1\n2\n", 283 | }, 284 | testInfo{ 285 | _OP_JNE, 286 | "", 287 | ` 288 | mov eax 12 289 | mov ebx 4 290 | cmp eax, ebx 291 | jne 5 292 | prn 1 293 | prn 2 294 | `, 295 | "2\n", 296 | }, 297 | testInfo{ 298 | _OP_JNE, 299 | "false", 300 | ` 301 | mov eax 4 302 | mov ebx 4 303 | cmp eax, ebx 304 | jne 5 305 | prn 1 306 | prn 2 307 | `, 308 | "1\n2\n", 309 | }, 310 | testInfo{ 311 | _OP_JG, 312 | "", 313 | ` 314 | mov eax 6 315 | mov ebx 4 316 | cmp eax, ebx 317 | jg 5 318 | prn 1 319 | prn 2 320 | `, 321 | "2\n", 322 | }, 323 | testInfo{ 324 | _OP_JG, 325 | "false, equal", 326 | ` 327 | mov eax 4 328 | mov ebx 4 329 | cmp eax, ebx 330 | jg 5 331 | prn 1 332 | prn 2 333 | `, 334 | "1\n2\n", 335 | }, 336 | testInfo{ 337 | _OP_JG, 338 | "false, lower", 339 | ` 340 | mov eax 2 341 | mov ebx 4 342 | cmp eax, ebx 343 | jg 5 344 | prn 1 345 | prn 2 346 | `, 347 | "1\n2\n", 348 | }, 349 | testInfo{ 350 | _OP_JGE, 351 | "", 352 | ` 353 | mov eax 6 354 | mov ebx 4 355 | cmp eax, ebx 356 | jge 5 357 | prn 1 358 | prn 2 359 | `, 360 | "2\n", 361 | }, 362 | testInfo{ 363 | _OP_JGE, 364 | "true, equal", 365 | ` 366 | mov eax 4 367 | mov ebx 4 368 | cmp eax, ebx 369 | jge 5 370 | prn 1 371 | prn 2 372 | `, 373 | "2\n", 374 | }, 375 | testInfo{ 376 | _OP_JGE, 377 | "false", 378 | ` 379 | mov eax 2 380 | mov ebx 4 381 | cmp eax, ebx 382 | jge 5 383 | prn 1 384 | prn 2 385 | `, 386 | "1\n2\n", 387 | }, 388 | testInfo{ 389 | _OP_JL, 390 | "", 391 | ` 392 | mov eax 2 393 | mov ebx 4 394 | cmp eax, ebx 395 | jl 5 396 | prn 1 397 | prn 2 398 | `, 399 | "2\n", 400 | }, 401 | testInfo{ 402 | _OP_JL, 403 | "false, equal", 404 | ` 405 | mov eax 4 406 | mov ebx 4 407 | cmp eax, ebx 408 | jl 5 409 | prn 1 410 | prn 2 411 | `, 412 | "1\n2\n", 413 | }, 414 | testInfo{ 415 | _OP_JL, 416 | "false, greater", 417 | ` 418 | mov eax 6 419 | mov ebx 4 420 | cmp eax, ebx 421 | jl 5 422 | prn 1 423 | prn 2 424 | `, 425 | "1\n2\n", 426 | }, 427 | testInfo{ 428 | _OP_JLE, 429 | "", 430 | ` 431 | mov eax 2 432 | mov ebx 4 433 | cmp eax, ebx 434 | jle 5 435 | prn 1 436 | prn 2 437 | `, 438 | "2\n", 439 | }, 440 | testInfo{ 441 | _OP_JLE, 442 | "true, equal", 443 | ` 444 | mov eax 4 445 | mov ebx 4 446 | cmp eax, ebx 447 | jle 5 448 | prn 1 449 | prn 2 450 | `, 451 | "2\n", 452 | }, 453 | testInfo{ 454 | _OP_JLE, 455 | "false", 456 | ` 457 | mov eax 6 458 | mov ebx 4 459 | cmp eax, ebx 460 | jle 5 461 | prn 1 462 | prn 2 463 | `, 464 | "1\n2\n", 465 | }, 466 | testInfo{ 467 | _OP_PRN, 468 | "hexa", 469 | ` 470 | mov eax 0x1A4F 471 | prn eax 472 | `, 473 | "6735\n", 474 | }, 475 | testInfo{ 476 | _OP_PRN, 477 | "hexa2", 478 | ` 479 | mov eax 1A4F|h 480 | prn eax 481 | `, 482 | "6735\n", 483 | }, 484 | testInfo{ 485 | _OP_PRN, 486 | "decimal", 487 | ` 488 | mov eax 8374|d 489 | prn eax 490 | `, 491 | "8374\n", 492 | }, 493 | testInfo{ 494 | _OP_PRN, 495 | "octal", 496 | ` 497 | mov eax 12675|o 498 | prn eax 499 | `, 500 | "5565\n", 501 | }, 502 | testInfo{ 503 | _OP_PRN, 504 | "binary", 505 | ` 506 | mov eax 10010110|b 507 | prn eax 508 | `, 509 | "150\n", 510 | }, 511 | testInfo{ 512 | _OP_PRN, 513 | "address", 514 | ` 515 | mov [17] 25 516 | mov [124] 12 517 | add [17] [124] 518 | prn [17] 519 | `, 520 | "37\n", 521 | }, 522 | testInfo{ 523 | _OP_PRN, 524 | "address hex", 525 | ` 526 | mov [0x1] 5 527 | mov eax 12 528 | add eax [0x1] 529 | prn eax 530 | `, 531 | "17\n", 532 | }, 533 | testInfo{ 534 | _OP_PRN, 535 | "address post-hex", 536 | ` 537 | mov [12a|h] 62|o 538 | mov eax 13 539 | add eax [298|d] 540 | prn eax 541 | `, 542 | "63\n", 543 | }, 544 | testInfo{ 545 | _OP_PRN, 546 | "address post-octal", 547 | ` 548 | mov [62|o] 2 549 | mov eax 3 550 | add eax [0x32] 551 | prn eax 552 | `, 553 | "5\n", 554 | }, 555 | testInfo{ 556 | _OP_PRN, 557 | "address post-binary", 558 | ` 559 | mov [11|b] 2 560 | mov eax 3 561 | add eax [3|d] 562 | prn eax 563 | `, 564 | "5\n", 565 | }, 566 | } 567 | 568 | func TestCodes(t *testing.T) { 569 | for _, ti := range tests { 570 | testCode(ti, t) 571 | } 572 | } 573 | 574 | func testCode(ti testInfo, t *testing.T) { 575 | // Arrange 576 | b := new(bytes.Buffer) 577 | r := bytes.NewBufferString(ti.code) 578 | v := NewWithWriter(b) 579 | 580 | // Act 581 | v.Run(r) 582 | 583 | // Assert 584 | res := b.String() 585 | if res != ti.exp { 586 | if len(ti.name) > 0 { 587 | t.Errorf("test %s (%s): expected %s, got %s", ti.op, ti.name, ti.exp, res) 588 | } else { 589 | t.Errorf("test %s: expected %s, got %s", ti.op, ti.exp, res) 590 | } 591 | } 592 | } 593 | --------------------------------------------------------------------------------