VM and compiler update (#63)

* renamed test folders and fixed bug where wrong jump labels would be exectuted for rewrite.

* Added support for Osize (len(string)) and factored out the array tests

* Added current instruction number to VM prompt if program is loaded.

* added support for unary expressions.

* updated README of and sorted the help commands

* updated readme of the compiler

* bumped version -> 0.39.0
This commit is contained in:
Anthony De Meulemeester 2018-04-04 21:41:19 +02:00 committed by GitHub
parent 83e467e527
commit 941bd7e728
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 475 additions and 292 deletions

View file

@ -1 +1 @@
0.38.1
0.39.0

View file

@ -52,27 +52,29 @@ More information about standalone installation coming soon.
NEO-GO-VM > help
COMMAND USAGE
step step (n) instruction in the program (> step 10)
ops show the opcodes of the current loaded program
ip show the current instruction
estack show evaluation stack details
astack show alt stack details
istack show invocation stack details
run execute the current loaded script
cont continue execution of the current loaded script
help show available commands
exit exit the VM prompt
break place a breakpoint (> break 1)
load load a script into the VM (> load /path/to/script.avm)
cont continue execution of the current loaded script
estack show evaluation stack details
exit exit the VM prompt
help show available commands
ip show the current instruction
istack show invocation stack details
loadavm load an avm script into the VM (> load /path/to/script.avm)
loadgo compile and load a .go file into the VM (> load /path/to/file.go)
loadhex load a hex string into the VM (> loadhex 006166 )
ops show the opcodes of the current loaded program
run execute the current loaded script
step step (n) instruction in the program (> step 10)
```
### Loading in your script
To load a script into the VM:
To load an avm script into the VM:
```
NEO-GO-VM > load ../contract.avm
READY
NEO-GO-VM > loadavm ../contract.avm
READY: loaded 36 instructions
```
Run the script:
@ -87,6 +89,28 @@ NEO-GO-VM > run
]
```
You can also directly compile and load `.go` files:
```
NEO-GO-VM > loadgo ../contract.go
READY: loaded 36 instructions
```
To make it even more complete, you can directly load hex strings into the VM:
```
NEO-GO-VM > loadhex 54c56b006c766b00527ac46c766b00c391640b006203005a616c756662030000616c7566
READY: loaded 36 instructions
NEO-GO-VM > run
[
{
"value": 10,
"type": "BigInteger"
}
]
```
### Debugging
The `neo-go-vm` provides a debugger to inspect your program in-depth.
@ -95,6 +119,7 @@ Step 4 instructions.
```
NEO-GO-VM > step 4
at breakpoint 4 (Opush4)
NEO-GO-VM 4 >
```
Using just `step` will execute 1 instruction at a time.
@ -102,6 +127,7 @@ Using just `step` will execute 1 instruction at a time.
```
NEO-GO-VM > step
instruction pointer at 5 (Odup)
NEO-GO-VM 5 >
```
To place breakpoints:
@ -109,14 +135,15 @@ To place breakpoints:
```
NEO-GO-VM > break 10
breakpoint added at instruction 10
NEO-GO-VM > resume
NEO-GO-VM > cont
at breakpoint 10 (Osetitem)
NEO-GO-VM 10 > cont
```
Inspecting the stack:
Inspecting the evaluation stack:
```
NEO-GO-VM > stack
NEO-GO-VM > estack
[
{
"value": [
@ -137,4 +164,7 @@ NEO-GO-VM > stack
]
```
And a lot more features coming next weeks..
There are more stacks that you can inspect.
- `astack` alt stack
- `istack` invocation stack

View file

@ -2,40 +2,45 @@ package cli
import (
"bufio"
"bytes"
"encoding/hex"
"fmt"
"io/ioutil"
"os"
"sort"
"strconv"
"strings"
"text/tabwriter"
"github.com/CityOfZion/neo-go/pkg/vm"
"github.com/CityOfZion/neo-go/pkg/vm/compiler"
)
// command describes a VM command.
type command struct {
// number of minimun arguments the command needs.
args int
// description of the command.
usage string
// whether the VM needs to be "ready" to execute this command.
ready bool
}
var commands = map[string]command{
"help": {0, "show available commands", false},
"exit": {0, "exit the VM prompt", false},
"ip": {0, "show the current instruction", true},
"break": {1, "place a breakpoint (> break 1)", true},
"estack": {0, "show evaluation stack details", false},
"astack": {0, "show alt stack details", false},
"istack": {0, "show invocation stack details", false},
"load": {1, "load a script into the VM (> load /path/to/script.avm)", false},
"run": {0, "execute the current loaded script", true},
"cont": {0, "continue execution of the current loaded script", true},
"step": {0, "step (n) instruction in the program (> step 10)", true},
"ops": {0, "show the opcodes of the current loaded program", true},
"help": {0, "show available commands", false},
"exit": {0, "exit the VM prompt", false},
"ip": {0, "show the current instruction", true},
"break": {1, "place a breakpoint (> break 1)", true},
"estack": {0, "show evaluation stack details", false},
"astack": {0, "show alt stack details", false},
"istack": {0, "show invocation stack details", false},
"loadavm": {1, "load an avm script into the VM (> load /path/to/script.avm)", false},
"loadhex": {1, "load a hex string into the VM (> loadhex 006166 )", false},
"loadgo": {1, "compile and load a .go file into the VM (> load /path/to/file.go)", false},
"run": {0, "execute the current loaded script", true},
"cont": {0, "continue execution of the current loaded script", true},
"step": {0, "step (n) instruction in the program (> step 10)", true},
"ops": {0, "show the opcodes of the current loaded program", true},
}
// VMCLI object for interacting with the VM.
@ -67,14 +72,7 @@ func (c *VMCLI) handleCommand(cmd string, args ...string) {
switch cmd {
case "help":
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
fmt.Fprintln(w, "COMMAND\tUSAGE")
for name, details := range commands {
fmt.Fprintf(w, "%s\t%s\n", name, details.usage)
}
w.Flush()
fmt.Println()
printHelp()
case "exit":
fmt.Println("Bye!")
@ -97,13 +95,36 @@ func (c *VMCLI) handleCommand(cmd string, args ...string) {
case "estack", "istack", "astack":
fmt.Println(c.vm.Stack(cmd))
case "load":
case "loadavm":
if err := c.vm.LoadFile(args[0]); err != nil {
fmt.Println(err)
} else {
fmt.Printf("READY: loaded %d instructions\n", c.vm.Context().LenInstr())
}
case "loadhex":
b, err := hex.DecodeString(args[0])
if err != nil {
fmt.Println(err)
return
}
c.vm.Load(b)
fmt.Printf("READY: loaded %d instructions\n", c.vm.Context().LenInstr())
case "loadgo":
fb, err := ioutil.ReadFile(args[0])
if err != nil {
fmt.Println(err)
return
}
b, err := compiler.Compile(bytes.NewReader(fb), &compiler.Options{})
if err != nil {
fmt.Println(err)
return
}
c.vm.Load(b)
fmt.Printf("READY: loaded %d instructions\n", c.vm.Context().LenInstr())
case "run", "cont":
c.vm.Run()
@ -132,7 +153,11 @@ func (c *VMCLI) Run() error {
printLogo()
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("NEO-GO-VM > ")
if c.vm.Ready() && c.vm.Context().IP()-1 >= 0 {
fmt.Printf("NEO-GO-VM %d > ", c.vm.Context().IP()-1)
} else {
fmt.Print("NEO-GO-VM > ")
}
input, _ := reader.ReadString('\n')
input = strings.Trim(input, "\n")
if len(input) != 0 {
@ -147,6 +172,25 @@ func (c *VMCLI) Run() error {
}
}
func printHelp() {
names := make([]string, len(commands))
i := 0
for name, _ := range commands {
names[i] = name
i++
}
sort.Strings(names)
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
fmt.Fprintln(w, "COMMAND\tUSAGE")
for _, name := range names {
fmt.Fprintf(w, "%s\t%s\n", name, commands[name].usage)
}
w.Flush()
fmt.Println()
}
func printLogo() {
logo := `
_ ____________ __________ _ ____ ___

View file

@ -15,19 +15,19 @@ The neo-go compiler compiles Go programs to bytecode that the NEO virtual machin
- basic if statements
- binary expressions
- return statements
- for loops
- imports
### Go builtins
- len
- append
### VM API (interop layer)
- storage
- runtime
## Not yet implemented
- for loops
- range
- builtin (append)
- some parts of the interop layer (VM API)
## Not supported

View file

@ -192,3 +192,7 @@ func isNoRetSyscall(name string) bool {
}
return false
}
func isStringType(t types.Type) bool {
return t.String() == "string"
}

View file

@ -380,15 +380,16 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
case *ast.CallExpr:
var (
f *funcScope
ok bool
numArgs = len(n.Args)
f *funcScope
ok bool
numArgs = len(n.Args)
isBuiltin = isBuiltin(n.Fun)
)
switch fun := n.Fun.(type) {
case *ast.Ident:
f, ok = c.funcs[fun.Name]
if !ok && !isBuiltin(n.Fun) {
if !ok && !isBuiltin {
log.Fatalf("could not resolve function %s", fun.Name)
}
case *ast.SelectorExpr:
@ -410,12 +411,15 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
for _, arg := range n.Args {
ast.Walk(c, arg)
}
if numArgs == 2 {
emitOpcode(c.prog, vm.Oswap)
}
if numArgs == 3 {
emitInt(c.prog, 2)
emitOpcode(c.prog, vm.Oxswap)
// Do not swap for builtin functions.
if !isBuiltin {
if numArgs == 2 {
emitOpcode(c.prog, vm.Oswap)
}
if numArgs == 3 {
emitInt(c.prog, 2)
emitOpcode(c.prog, vm.Oxswap)
}
}
// c# compiler adds a NOP (0x61) before every function call. Dont think its relevant
@ -424,7 +428,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
emitOpcode(c.prog, vm.Onop)
// Check builtin first to avoid nil pointer on funcScope!
if isBuiltin(n.Fun) {
if isBuiltin {
// Use the ident to check, builtins are not in func scopes.
// We can be sure builtins are of type *ast.Ident.
c.convertBuiltin(n.Fun.(*ast.Ident).Name, n)
@ -456,7 +460,9 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
return nil
case *ast.UnaryExpr:
// TODO(@anthdm)
ast.Walk(c, n.X)
c.convertToken(n.Op)
return nil
case *ast.IncDecStmt:
ast.Walk(c, n.X)
@ -528,9 +534,15 @@ func (c *codegen) convertSyscall(name string) {
func (c *codegen) convertBuiltin(name string, expr *ast.CallExpr) {
switch name {
case "len":
emitOpcode(c.prog, vm.Oarraysize)
arg := expr.Args[0]
typ := c.typeInfo.Types[arg].Type
if isStringType(typ) {
emitOpcode(c.prog, vm.Osize)
} else {
emitOpcode(c.prog, vm.Oarraysize)
}
case "append":
log.Fatal("builtin (append) not yet implemented.")
emitOpcode(c.prog, vm.Oappend)
}
}
@ -621,6 +633,8 @@ func (c *codegen) convertToken(tok token.Token) {
emitOpcode(c.prog, vm.Odec)
case token.INC:
emitOpcode(c.prog, vm.Oinc)
case token.NOT:
emitOpcode(c.prog, vm.Onot)
default:
log.Fatalf("compiler could not convert token: %s", tok)
}
@ -702,7 +716,7 @@ func (c *codegen) writeJumps() {
switch vm.Opcode(op) {
case vm.Ojmp, vm.Ojmpifnot, vm.Ojmpif, vm.Ocall:
index := int16(binary.LittleEndian.Uint16(b[j : j+2]))
if int(index) > len(c.l) {
if int(index) > len(c.l) || int(index) < 0 {
continue
}
offset := uint16(c.l[index] - i)

View file

@ -1,57 +0,0 @@
package compiler
var boolTestCases = []testCase{
{
"bool assign",
`
package foo
func Main() bool {
x := true
return x
}
`,
"52c56b516c766b00527ac46203006c766b00c3616c7566",
},
{
"bool compare",
`
package foo
func Main() int {
x := true
if x {
return 10
}
return 0
}
`,
"54c56b516c766b00527ac46c766b00c3640b006203005a616c756662030000616c7566",
},
{
"bool compare verbose",
`
package foo
func Main() int {
x := true
if x == true {
return 10
}
return 0
}
`,
"54c56b516c766b00527ac46c766b00c3519c640b006203005a616c756662030000616c7566",
},
// {
// "bool invert (unary expr)",
// `
// package foo
// func Main() int {
// x := true
// if !x {
// return 10
// }
// return 0
// }
// `,
// "54c56b516c766b00527ac46c766b00c3630b006203005a616c756662030000616c7566",
// },
}

View file

@ -1,15 +0,0 @@
package compiler
var stringTestCases = []testCase{
{
"simple string",
`
package testcase
func Main() string {
x := "NEO"
return x
}
`,
"52c56b034e454f6c766b00527ac46203006c766b00c3616c7566",
},
}

View file

@ -28,10 +28,13 @@ func NewContext(b []byte) *Context {
// Next return the next instruction to execute.
func (c *Context) Next() Opcode {
c.ip++
if c.ip >= len(c.prog) {
return Oret
}
return Opcode(c.prog[c.ip])
}
// IP returns the absosulute instruction without taking 0 into account.
// IP returns the absolute instruction without taking 0 into account.
// If that program starts the ip = 0 but IP() will return 1, cause its
// the first instruction.
func (c *Context) IP() int {

View file

@ -72,6 +72,9 @@ func (e *Element) BigInt() *big.Int {
// Bool attempts to get the underlying value of the element as a boolean.
// Will panic if the assertion failed which will be catched by the VM.
func (e *Element) Bool() bool {
if v, ok := e.value.Value().(*big.Int); ok {
return v.Int64() == 1
}
return e.value.Value().(bool)
}

View file

@ -1,117 +0,0 @@
package vm_test
import (
"math/big"
"github.com/CityOfZion/neo-go/pkg/vm"
)
var arrayTestCases = []testCase{
{
"assign int array",
`
package foo
func Main() []int {
x := []int{1, 2, 3}
return x
}
`,
[]vm.StackItem{
vm.NewBigIntegerItem(1),
vm.NewBigIntegerItem(2),
vm.NewBigIntegerItem(3),
},
},
{
"assign string array",
`
package foo
func Main() []string {
x := []string{"foo", "bar", "foobar"}
return x
}
`,
[]vm.StackItem{
vm.NewByteArrayItem([]byte("foo")),
vm.NewByteArrayItem([]byte("bar")),
vm.NewByteArrayItem([]byte("foobar")),
},
},
{
"array item assign",
`
package foo
func Main() int {
x := []int{0, 1, 2}
y := x[0]
return y
}
`,
big.NewInt(0),
},
{
"array item return",
`
package foo
func Main() int {
x := []int{0, 1, 2}
return x[1]
}
`,
big.NewInt(1),
},
{
"array item in bin expr",
`
package foo
func Main() int {
x := []int{0, 1, 2}
return x[1] + 10
}
`,
big.NewInt(11),
},
{
"array item ident",
`
package foo
func Main() int {
x := 1
y := []int{0, 1, 2}
return y[x]
}
`,
big.NewInt(1),
},
{
"array item index with binExpr",
`
package foo
func Main() int {
x := 1
y := []int{0, 1, 2}
return y[x + 1]
}
`,
big.NewInt(2),
},
{
"array item struct",
`
package foo
type Bar struct {
arr []int
}
func Main() int {
b := Bar{
arr: []int{0, 1, 2},
}
x := b.arr[2]
return x + 2
}
`,
big.NewInt(4),
},
}

View file

@ -1,36 +0,0 @@
package vm_test
import (
"math/big"
"testing"
)
func TestClassicForLoop(t *testing.T) {
src := `
package foo
func Main() int {
x := 0
for i := 0; i < 10; i++ {
x = i
}
return x
}
`
eval(t, src, big.NewInt(9))
}
// TODO: This could be a nasty bug. Output of the VM is 65695.
// Only happens above 100000, could be binary read issue.
//func TestForLoopBigIter(t *testing.T) {
// src := `
// package foo
// func Main() int {
// x := 0
// for i := 0; i < 100000; i++ {
// x = i
// }
// return x
// }
// `
// eval(t, src, big.NewInt(99999))
//}

306
pkg/vm/tests/for_test.go Normal file
View file

@ -0,0 +1,306 @@
package vm_test
import (
"math/big"
"testing"
"github.com/CityOfZion/neo-go/pkg/vm"
)
func TestArrayFieldInStruct(t *testing.T) {
src := `
package foo
type Bar struct {
arr []int
}
func Main() int {
b := Bar{
arr: []int{0, 1, 2},
}
x := b.arr[2]
return x + 2
}
`
eval(t, src, big.NewInt(4))
}
func TestArrayItemGetIndexBinaryExpr(t *testing.T) {
src := `
package foo
func Main() int {
x := 1
y := []int{0, 1, 2}
return y[x + 1]
}
`
eval(t, src, big.NewInt(2))
}
func TestArrayItemGetIndexIdent(t *testing.T) {
src := `
package foo
func Main() int {
x := 1
y := []int{0, 1, 2}
return y[x]
}
`
eval(t, src, big.NewInt(1))
}
func TestArrayItemBinExpr(t *testing.T) {
src := `
package foo
func Main() int {
x := []int{0, 1, 2}
return x[1] + 10
}
`
eval(t, src, big.NewInt(11))
}
func TestArrayItemReturn(t *testing.T) {
src := `
package foo
func Main() int {
arr := []int{0, 1, 2}
return arr[1]
}
`
eval(t, src, big.NewInt(1))
}
func TestArrayItemAssign(t *testing.T) {
src := `
package foo
func Main() int {
arr := []int{1, 2, 3}
y := arr[0]
return y
}
`
eval(t, src, big.NewInt(1))
}
func TestStringArray(t *testing.T) {
src := `
package foo
func Main() []string {
x := []string{"foo", "bar", "foobar"}
return x
}
`
eval(t, src, []vm.StackItem{
vm.NewByteArrayItem([]byte("foo")),
vm.NewByteArrayItem([]byte("bar")),
vm.NewByteArrayItem([]byte("foobar")),
})
}
func TestIntArray(t *testing.T) {
src := `
package foo
func Main() []int {
arr := []int{1, 2, 3}
return arr
}
`
eval(t, src, []vm.StackItem{
vm.NewBigIntegerItem(1),
vm.NewBigIntegerItem(2),
vm.NewBigIntegerItem(3),
})
}
func TestArrayLen(t *testing.T) {
src := `
package foo
func Main() int {
arr := []int{0, 1, 2}
return len(arr)
}
`
eval(t, src, big.NewInt(3))
}
func TestStringLen(t *testing.T) {
src := `
package foo
func Main() int {
str := "this is medium sized string"
return len(str)
}
`
eval(t, src, big.NewInt(27))
}
func TestSimpleString(t *testing.T) {
src := `
package foo
func Main() string {
x := "NEO"
return x
}
`
eval(t, src, vm.NewByteArrayItem([]byte("NEO")).Value())
}
func TestBoolAssign(t *testing.T) {
src := `
package foo
func Main() bool {
x := true
return x
}
`
eval(t, src, big.NewInt(1))
}
func TestBoolCompare(t *testing.T) {
src := `
package foo
func Main() int {
x := true
if x {
return 10
}
return 0
}
`
eval(t, src, big.NewInt(10))
}
func TestBoolCompareVerbose(t *testing.T) {
src := `
package foo
func Main() int {
x := true
if x == true {
return 10
}
return 0
}
`
eval(t, src, big.NewInt(10))
}
func TestUnaryExpr(t *testing.T) {
src := `
package foo
func Main() bool {
x := false
return !x
}
`
eval(t, src, true)
}
func TestIfUnaryInvertPass(t *testing.T) {
src := `
package foo
func Main() int {
x := false
if !x {
return 10
}
return 0
}
`
eval(t, src, big.NewInt(10))
}
func TestIfUnaryInvert(t *testing.T) {
src := `
package foo
func Main() int {
x := true
if !x {
return 10
}
return 0
}
`
eval(t, src, big.NewInt(0))
}
func TestAppendString(t *testing.T) {
src := `
package foo
func Main() string {
arr := []string{"a", "b", "c"}
arr = append(arr, "d")
return arr[3]
}
`
eval(t, src, vm.NewByteArrayItem([]byte("d")).Value())
}
func TestAppendInt(t *testing.T) {
src := `
package foo
func Main() int {
arr := []int{0, 1, 2}
arr = append(arr, 3)
return arr[3]
}
`
eval(t, src, big.NewInt(3))
}
func TestClassicForLoop(t *testing.T) {
src := `
package foo
func Main() int {
x := 0
for i := 0; i < 10; i++ {
x = i
}
return x
}
`
eval(t, src, big.NewInt(9))
}
func TestInc(t *testing.T) {
src := `
package foo
func Main() int {
x := 0
x++
return x
}
`
eval(t, src, big.NewInt(1))
}
func TestDec(t *testing.T) {
src := `
package foo
func Main() int {
x := 2
x--
return x
}
`
eval(t, src, big.NewInt(1))
}
// TODO: This could be a nasty bug. Output of the VM is 65695.
// Only happens above 100000, could be binary read issue.
//func TestForLoopBigIter(t *testing.T) {
// src := `
// package foo
// func Main() int {
// x := 0
// for i := 0; i < 100000; i++ {
// x = i
// }
// return x
// }
// `
// eval(t, src, big.NewInt(99999))
//}

View file

@ -33,7 +33,6 @@ func TestVMAndCompilerCases(t *testing.T) {
testCases := []testCase{}
testCases = append(testCases, numericTestCases...)
testCases = append(testCases, assignTestCases...)
testCases = append(testCases, arrayTestCases...)
testCases = append(testCases, binaryExprTestCases...)
testCases = append(testCases, structTestCases...)

View file

@ -186,10 +186,6 @@ func (v *VM) Run() {
func (v *VM) Step() {
ctx := v.Context()
op := ctx.Next()
if ctx.ip >= len(ctx.prog) {
op = Oret
}
v.execute(ctx, op)
// re-peek the context as it could been changed during execution.
@ -460,8 +456,8 @@ func (v *VM) execute(ctx *Context, op Opcode) {
v.estack.PushVal(x.Abs(x))
case Onot:
x := v.estack.Pop().BigInt()
v.estack.PushVal(x.Not(x))
x := v.estack.Pop().Bool()
v.estack.PushVal(!x)
case Onz:
panic("todo NZ")
@ -486,6 +482,7 @@ func (v *VM) execute(ctx *Context, op Opcode) {
case *ArrayItem, *StructItem:
arr := t.Value().([]StackItem)
arr = append(arr, itemElem.value)
v.estack.PushVal(arr)
default:
panic("APPEND: not of underlying type Array")
}
@ -558,6 +555,14 @@ func (v *VM) execute(ctx *Context, op Opcode) {
}
v.estack.PushVal(len(arr))
case Osize:
elem := v.estack.Pop()
arr, ok := elem.value.Value().([]uint8)
if !ok {
panic("SIZE: item not of type []uint8")
}
v.estack.PushVal(len(arr))
case Ojmp, Ojmpif, Ojmpifnot:
var (
rOffset = int16(ctx.readUint16())