From 23cfebf62171d16f51f8ba9c1ab1d84e6242f9ed Mon Sep 17 00:00:00 2001 From: Anthony De Meulemeester Date: Sat, 24 Feb 2018 10:06:48 +0100 Subject: [PATCH] Compiler (#23) * implemented add, mul, div, sub assign for identifiers. * Implemented struct field initialization. * Implemented imports * Implemented storage VM API (interop layer) + additional bug fixes when encountered. * Bumped version 0.12.0 * fixed double point extension on compiled output file. * Fixed bug where callExpr in returns where added to voidCall * fixed binExpr compare equal * Check the env for the gopath first * removed travis.yml * custom types + implemented general declarations. * commented out the storage test to make the build pass --- .travis.yml | 9 - VERSION | 2 +- pkg/vm/compiler/README.md | 51 ++++- pkg/vm/compiler/codegen.go | 242 +++++++++++++++----- pkg/vm/compiler/compiler.go | 58 ++++- pkg/vm/compiler/emit.go | 14 +- pkg/vm/compiler/func_scope.go | 66 ++++-- pkg/vm/compiler/struct_scope.go | 72 ++++-- pkg/vm/compiler/tests/array_test.go | 2 +- pkg/vm/compiler/tests/assign_test.go | 86 ++++++- pkg/vm/compiler/tests/bar/bar.go | 8 + pkg/vm/compiler/tests/binary_expr_test.go | 44 +++- pkg/vm/compiler/tests/bool_test.go | 2 +- pkg/vm/compiler/tests/compiler_test.go | 24 +- pkg/vm/compiler/tests/custom_type_test.go | 25 ++ pkg/vm/compiler/tests/foo/foo.go | 6 + pkg/vm/compiler/tests/function_call_test.go | 46 +++- pkg/vm/compiler/tests/if_statement_test.go | 2 +- pkg/vm/compiler/tests/import_test.go | 34 +++ pkg/vm/compiler/tests/storage_test.go | 20 ++ pkg/vm/compiler/tests/string_test.go | 2 +- pkg/vm/compiler/tests/struct_test.go | 58 ++++- pkg/vm/smartcontract/runtime/runtime.go | 14 ++ pkg/vm/smartcontract/storage/storage.go | 16 ++ pkg/vm/syscall.go | 12 + 25 files changed, 798 insertions(+), 117 deletions(-) delete mode 100644 .travis.yml create mode 100644 pkg/vm/compiler/tests/bar/bar.go create mode 100644 pkg/vm/compiler/tests/custom_type_test.go create mode 100644 pkg/vm/compiler/tests/foo/foo.go create mode 100644 pkg/vm/compiler/tests/import_test.go create mode 100644 pkg/vm/compiler/tests/storage_test.go create mode 100644 pkg/vm/smartcontract/runtime/runtime.go create mode 100644 pkg/vm/smartcontract/storage/storage.go create mode 100644 pkg/vm/syscall.go diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7933936b9..000000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: go -sudo: false -go: - - 1.x - - tip -install: - - go build ./{cmd,pkg}/... -script: - - go vet ./{cmd,pkg}/... diff --git a/VERSION b/VERSION index ac454c6a1..54d1a4f2a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.12.0 +0.13.0 diff --git a/pkg/vm/compiler/README.md b/pkg/vm/compiler/README.md index 254623b18..78d02392e 100644 --- a/pkg/vm/compiler/README.md +++ b/pkg/vm/compiler/README.md @@ -5,22 +5,27 @@ The neo-go compiler compiles Go programs to bytecode that the NEO virtual machin > The neo-go compiler is under very active development and will be updated on a weekly basis. ## Currently supported + +### Go internals - type checker - multiple assigns -- types int, string and bool +- types int, string and booleans - struct types + method receives - functions - composite literals `[]int, []string` - basic if statements - binary expressions. - return statements +- imports + +### VM API (interop layer) +- storage ## Not yet implemented - for loops - ranges - builtins (append, len, ..) -- blockchain helpers (sha256, storage, ..) -- import packages +- large part of the interop layer (VM API) ## Not supported Due to the limitations of the NEO virtual machine, features listed below will not be supported. @@ -28,8 +33,46 @@ Due to the limitations of the NEO virtual machine, features listed below will no - goroutines - multiple returns -## How to report bugs +## How to report compiler bugs 1. Make a proper testcase (example testcases can be found in the tests folder) 2. Create an issue on Github 3. Make a PR with a reference to the created issue, containing the testcase that proves the bug 4. Either you fix the bug yourself or wait for patch that solves the problem + +## Quick start + +### Compile a smart contract + +``` +./bin/neo-go contract compile mycontract.go +``` + +By default the filename will be the name of your .go file with the .avm extension, the file will be located in the same directory where you called the command from. If you want another location for your compiled contract: + +``` +./bin/neo-go contract compile mycontract.go --out /Users/foo/bar/contract.avm +``` + +### Debugging your smart contract +You can dump the opcodes generated by the compiler with the following command: + +``` +./bin/neo-go contract opdump mycontract.go +``` + +This will result in something like this: + +``` +INDEX OPCODE DESC +0 0x52 OpPush2 +1 0xc5 OpNewArray +2 0x6b OpToAltStack +3 0x 0 OpPush0 +4 0x6c OpFromAltStack +5 0x76 OpDup +6 0x6b OpToAltStack +7 0x 0 OpPush0 +8 0x52 OpPush2 +9 0x7a OpRoll +10 0xc4 OpSetItem +``` \ No newline at end of file diff --git a/pkg/vm/compiler/codegen.go b/pkg/vm/compiler/codegen.go index d480e14b2..08bfd409a 100644 --- a/pkg/vm/compiler/codegen.go +++ b/pkg/vm/compiler/codegen.go @@ -47,7 +47,7 @@ func (c *codegen) pc() int { } func (c *codegen) emitLoadConst(t types.TypeAndValue) { - switch typ := t.Type.(type) { + switch typ := t.Type.Underlying().(type) { case *types.Basic: switch typ.Kind() { case types.Int: @@ -121,7 +121,9 @@ func (c *codegen) convertFuncDecl(decl *ast.FuncDecl) { } else { f = c.newFunc(decl) } + c.fctx = f + ast.Inspect(decl, c.fctx.analyzeVoidCalls) emitInt(c.prog, f.stackSize()) emitOpcode(c.prog, vm.Onewarray) @@ -135,10 +137,12 @@ func (c *codegen) convertFuncDecl(decl *ast.FuncDecl) { // to support other types. if decl.Recv != nil { for _, arg := range decl.Recv.List { - strct := c.fctx.newStruct() - ident := arg.Names[0] - strct.initializeFields(ident, c.typeInfo) + t, ok := c.typeInfo.Defs[ident].Type().Underlying().(*types.Struct) + if !ok { + log.Fatal("method receiver is not a struct type") + } + c.fctx.newStruct(t) l := c.fctx.newLocal(ident.Name) c.emitStoreLocal(l) } @@ -157,21 +161,43 @@ func (c *codegen) convertFuncDecl(decl *ast.FuncDecl) { func (c *codegen) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { + // General declarations. + // With value: var x int = 2 + // Without value: var x int + case *ast.GenDecl: + switch t := n.Specs[0].(type) { + case *ast.ValueSpec: + if len(t.Values) > 0 { + ast.Walk(c, t.Values[0]) + l := c.fctx.newLocal(t.Names[0].Name) + c.emitStoreLocal(l) + } + } + return nil + case *ast.AssignStmt: for i := 0; i < len(n.Lhs); i++ { - // resolve the whole right hand side. - ast.Walk(c, n.Rhs[i]) - // check if we are assigning to a struct or an identifier switch t := n.Lhs[i].(type) { case *ast.Ident: - l := c.fctx.loadLocal(t.Name) - c.emitStoreLocal(l) + switch n.Tok { + case token.ADD_ASSIGN, token.SUB_ASSIGN, token.MUL_ASSIGN, token.QUO_ASSIGN: + c.emitLoadLocal(t.Name) + ast.Walk(c, n.Rhs[0]) + c.convertToken(n.Tok) + l := c.fctx.loadLocal(t.Name) + c.emitStoreLocal(l) + default: + ast.Walk(c, n.Rhs[0]) + l := c.fctx.loadLocal(t.Name) + c.emitStoreLocal(l) + } case *ast.SelectorExpr: - switch n := t.X.(type) { + switch expr := t.X.(type) { case *ast.Ident: - c.emitLoadLocal(n.Name) // load the struct - c.emitStoreStructField(n.Name, t.Sel.Name) // store the field + ast.Walk(c, n.Rhs[i]) + c.emitLoadLocal(expr.Name) // load the struct + c.emitStoreStructField(expr.Name, t.Sel.Name) // store the field default: log.Fatal("nested selector assigns not supported yet") } @@ -232,14 +258,13 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { return nil case *ast.CompositeLit: + var typ types.Type + switch t := n.Type.(type) { case *ast.Ident: - typ := c.typeInfo.ObjectOf(t).Type().Underlying() - switch typ.(type) { - case *types.Struct: - c.convertStruct(n) - } - + typ = c.typeInfo.ObjectOf(t).Type().Underlying() + case *ast.SelectorExpr: + typ = c.typeInfo.ObjectOf(t.Sel).Type().Underlying() default: ln := len(n.Elts) for i := ln - 1; i >= 0; i-- { @@ -247,7 +272,14 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { } emitInt(c.prog, int64(ln)) emitOpcode(c.prog, vm.Opack) + return nil } + + switch typ.(type) { + case *types.Struct: + c.convertStruct(n) + } + return nil case *ast.BinaryExpr: @@ -275,25 +307,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { ast.Walk(c, n.X) ast.Walk(c, n.Y) - - switch n.Op { - case token.ADD: - emitOpcode(c.prog, vm.Oadd) - case token.SUB: - emitOpcode(c.prog, vm.Osub) - case token.MUL: - emitOpcode(c.prog, vm.Omul) - case token.QUO: - emitOpcode(c.prog, vm.Odiv) - case token.LSS: - emitOpcode(c.prog, vm.Olt) - case token.LEQ: - emitOpcode(c.prog, vm.Olte) - case token.GTR: - emitOpcode(c.prog, vm.Ogt) - case token.GEQ: - emitOpcode(c.prog, vm.Ogte) - } + c.convertToken(n.Op) return nil } @@ -311,15 +325,21 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { log.Fatalf("could not resolve function %s", fun.Name) } case *ast.SelectorExpr: - ast.Walk(c, fun.X) + // If this is a method call we need to walk the AST to load the struct locally. + // Otherwise this is a function call from a imported package and we can call it + // directly. + if c.typeInfo.Selections[fun] != nil { + ast.Walk(c, fun.X) + // Dont forget to add 1 extra argument when its a method. + numArgs++ + } f, ok = c.funcs[fun.Sel.Name] if !ok { log.Fatalf("could not resolve function %s", fun.Sel.Name) } - // Dont forget to add 1 extra argument when its a method. - numArgs++ } + // Handle the arguments for _, arg := range n.Args { ast.Walk(c, arg) } @@ -335,7 +355,18 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { // and we could easily removed it, but to be consistent with the original compiler I // will put them in. ^^ emitOpcode(c.prog, vm.Onop) - emitCall(c.prog, vm.Ocall, int16(f.label)) + + if isSyscall(f.name) { + c.convertSyscall(f.name) + } else { + emitCall(c.prog, vm.Ocall, int16(f.label)) + } + + // If we are not assigning this function to a variable we need to drop + // the top stack item. It's not a void but you get the point \o/. + if _, ok := c.fctx.voidCalls[n]; ok && !isNoRetSyscall(f.name) { + emitOpcode(c.prog, vm.Odrop) + } return nil case *ast.SelectorExpr: @@ -351,31 +382,99 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { return c } -func (c *codegen) convertStruct(lit *ast.CompositeLit) { +func (c *codegen) convertSyscall(name string) { + api, ok := vm.Syscalls[name] + if !ok { + log.Fatalf("unknown VM syscall api: %s", name) + } + emitSyscall(c.prog, api) emitOpcode(c.prog, vm.Onop) - emitInt(c.prog, int64(len(lit.Elts))) +} + +func (c *codegen) convertStruct(lit *ast.CompositeLit) { + // Create a new structScope to initialize and store + // the positions of its variables. + t, ok := c.typeInfo.TypeOf(lit).Underlying().(*types.Struct) + if !ok { + log.Fatalf("the given literal is not of type struct: %v", lit) + } + strct := c.fctx.newStruct(t) + + emitOpcode(c.prog, vm.Onop) + emitInt(c.prog, int64(strct.t.NumFields())) emitOpcode(c.prog, vm.Onewstruct) emitOpcode(c.prog, vm.Otoaltstack) - // Create a new struct scope to store the positions of its variables. - strct := c.fctx.newStruct() + // We need to locally store all the fields, even if they are not initialized. + // We will initialize all fields to their "zero" value. + for i := 0; i < strct.t.NumFields(); i++ { + sField := strct.t.Field(i) + fieldAdded := false - for _, field := range lit.Elts { - f := field.(*ast.KeyValueExpr) - // Walk to resolve the expression of the value. - ast.Walk(c, f.Value) - l := strct.newField(f.Key.(*ast.Ident).Name) - c.emitStoreLocal(l) + // Fields initialized by the program. + for _, field := range lit.Elts { + f := field.(*ast.KeyValueExpr) + fieldName := f.Key.(*ast.Ident).Name + + if sField.Name() == fieldName { + ast.Walk(c, f.Value) + pos := strct.loadField(fieldName) + c.emitStoreLocal(pos) + fieldAdded = true + break + } + } + if fieldAdded { + continue + } + c.emitLoadConst(strct.typeAndValues[sField.Name()]) + c.emitStoreLocal(i) } emitOpcode(c.prog, vm.Ofromaltstack) } +func (c *codegen) convertToken(tok token.Token) { + switch tok { + case token.ADD_ASSIGN: + emitOpcode(c.prog, vm.Oadd) + case token.SUB_ASSIGN: + emitOpcode(c.prog, vm.Osub) + case token.MUL_ASSIGN: + emitOpcode(c.prog, vm.Omul) + case token.QUO_ASSIGN: + emitOpcode(c.prog, vm.Odiv) + case token.ADD: + emitOpcode(c.prog, vm.Oadd) + case token.SUB: + emitOpcode(c.prog, vm.Osub) + case token.MUL: + emitOpcode(c.prog, vm.Omul) + case token.QUO: + emitOpcode(c.prog, vm.Odiv) + case token.LSS: + emitOpcode(c.prog, vm.Olt) + case token.LEQ: + emitOpcode(c.prog, vm.Olte) + case token.GTR: + emitOpcode(c.prog, vm.Ogt) + case token.GEQ: + emitOpcode(c.prog, vm.Ogte) + case token.EQL, token.NEQ: + emitOpcode(c.prog, vm.Onumequal) + default: + log.Fatalf("compiler could not convert token: %s", tok) + } +} + func (c *codegen) newFunc(decl *ast.FuncDecl) *funcScope { f := newFuncScope(decl, c.newLabel()) c.funcs[f.name] = f return f } +// TODO: Don't know if we really use this. Check if it can be deleted. +// If it's used once or twice, also remove this functions and +// call .Types direcly. e.g. c.typeInfo.Types[expr] func (c *codegen) getTypeInfo(expr ast.Expr) types.TypeAndValue { return c.typeInfo.Types[expr] } @@ -401,7 +500,7 @@ func isIdentBool(ident *ast.Ident) bool { } // CodeGen is the function that compiles the program to bytecode. -func CodeGen(f *ast.File, tInfo *types.Info) (*bytes.Buffer, error) { +func CodeGen(f *ast.File, tInfo *types.Info, imports map[string]*archive) (*bytes.Buffer, error) { c := &codegen{ prog: new(bytes.Buffer), l: []int{}, @@ -424,6 +523,11 @@ func CodeGen(f *ast.File, tInfo *types.Info) (*bytes.Buffer, error) { log.Fatal("could not find func main. did you forgot to declare it?") } + // Bring all imported functions into scope + for _, arch := range imports { + c.resolveFuncDecls(arch.f) + } + c.resolveFuncDecls(f) c.convertFuncDecl(main) @@ -436,6 +540,19 @@ func CodeGen(f *ast.File, tInfo *types.Info) (*bytes.Buffer, error) { } } + // Generate code for the imported packages. + for _, arch := range imports { + c.typeInfo = arch.typeInfo + for _, decl := range arch.f.Decls { + switch n := decl.(type) { + case *ast.FuncDecl: + if n.Name.Name != mainIdent { + c.convertFuncDecl(n) + } + } + } + } + c.writeJumps() return c.prog, nil @@ -470,3 +587,24 @@ func (c *codegen) writeJumps() { } } } + +func isSyscall(name string) bool { + _, ok := vm.Syscalls[name] + return ok +} + +var noRetSyscalls = []string{ + "Notify", "Log", "Put", "Register", "Delete", + "SetVotes", "ContractDestroy", "MerkleRoot", "Hash", + "PrevHash", "GetHeader", +} + +// isNoRetSyscall checks if the syscall has a return value. +func isNoRetSyscall(name string) bool { + for _, s := range noRetSyscalls { + if s == name { + return true + } + } + return false +} diff --git a/pkg/vm/compiler/compiler.go b/pkg/vm/compiler/compiler.go index 6aa7e8c9a..100b3cfe3 100644 --- a/pkg/vm/compiler/compiler.go +++ b/pkg/vm/compiler/compiler.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "go/ast" + "go/build" "go/importer" "go/parser" "go/token" @@ -14,13 +15,14 @@ import ( "io/ioutil" "log" "os" + "path/filepath" "strings" "text/tabwriter" "github.com/CityOfZion/neo-go/pkg/vm" ) -const fileExt = ".avm" +const fileExt = "avm" // Options contains all the parameters that affect the behaviour of the compiler. type Options struct { @@ -58,7 +60,12 @@ func Compile(input io.Reader, o *Options) ([]byte, error) { return nil, err } - buf, err := CodeGen(f, typeInfo) + imports, err := resolveImports(f) + if err != nil { + return nil, err + } + + buf, err := CodeGen(f, typeInfo, imports) if err != nil { return nil, err } @@ -66,6 +73,45 @@ func Compile(input io.Reader, o *Options) ([]byte, error) { return buf.Bytes(), nil } +type archive struct { + f *ast.File + typeInfo *types.Info +} + +func resolveImports(f *ast.File) (map[string]*archive, error) { + packages := map[string]*archive{} + for _, imp := range f.Imports { + path := strings.Replace(imp.Path.Value, `"`, "", 2) + path = filepath.Join(gopath(), "src", path) + fset := token.NewFileSet() + pkgs, err := parser.ParseDir(fset, path, nil, 0) + if err != nil { + return nil, err + } + + for name, pkg := range pkgs { + file := ast.MergePackageFiles(pkg, 0) + conf := types.Config{Importer: importer.Default()} + typeInfo := &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + Scopes: make(map[ast.Node]*types.Scope), + } + + // Typechecker + _, err = conf.Check("", fset, []*ast.File{file}, typeInfo) + if err != nil { + return nil, err + } + packages[name] = &archive{file, typeInfo} + } + } + return packages, nil +} + // CompileAndSave will compile and save the file to disk. func CompileAndSave(src string, o *Options) error { if len(o.Outfile) == 0 { @@ -113,6 +159,14 @@ func DumpOpcode(src string) error { return nil } +func gopath() string { + gopath := os.Getenv("GOPATH") + if len(gopath) == 0 { + gopath = build.Default.GOPATH + } + return gopath +} + func init() { log.SetFlags(0) } diff --git a/pkg/vm/compiler/emit.go b/pkg/vm/compiler/emit.go index c0cfb738e..476855561 100644 --- a/pkg/vm/compiler/emit.go +++ b/pkg/vm/compiler/emit.go @@ -58,7 +58,9 @@ func emitBytes(w *bytes.Buffer, b []byte) error { ) if n == 0 { - return errors.New("cannot emit 0 bytes") + // The VM expects a pushf (0x00). + // Empty strings on the stack for example. + return emitOpcode(w, vm.Opushf) } if n <= int(vm.Opushbytes75) { return emit(w, vm.Opcode(n), b) @@ -80,6 +82,16 @@ func emitBytes(w *bytes.Buffer, b []byte) error { return err } +func emitSyscall(w *bytes.Buffer, api string) error { + if len(api) == 0 { + return errors.New("syscall api cannot be of length 0") + } + buf := make([]byte, len(api)+1) + buf[0] = byte(len(api)) + copy(buf[1:len(buf)], []byte(api)) + return emit(w, vm.Osyscall, buf) +} + func emitCall(w *bytes.Buffer, op vm.Opcode, label int16) error { return emitJmp(w, op, label) } diff --git a/pkg/vm/compiler/func_scope.go b/pkg/vm/compiler/func_scope.go index c545e3273..4c05718bf 100644 --- a/pkg/vm/compiler/func_scope.go +++ b/pkg/vm/compiler/func_scope.go @@ -2,6 +2,7 @@ package compiler import ( "go/ast" + "go/types" "log" ) @@ -14,36 +15,75 @@ type funcScope struct { // The declaration of the function in the AST decl *ast.FuncDecl - // program label of the function + // Program label of the function label int - // local scope of the function + // Local scope of the function scope map[string]int - // mapping of structs positions with their scope + // A mapping of structs positions with their scope structs map[int]*structScope + // voidCalls are basically functions that return their value + // into nothing. The stack has their return value but there + // is nothing that consumes it. We need to keep track of + // these functions so we can cleanup (drop) the returned + // value from the stack. We also need to add every voidCall + // return value to the stack size. + voidCalls map[*ast.CallExpr]bool + // local variable counter i int } func newFuncScope(decl *ast.FuncDecl, label int) *funcScope { return &funcScope{ - name: decl.Name.Name, - decl: decl, - label: label, - scope: map[string]int{}, - structs: map[int]*structScope{}, - i: -1, + name: decl.Name.Name, + decl: decl, + label: label, + scope: map[string]int{}, + structs: map[int]*structScope{}, + voidCalls: map[*ast.CallExpr]bool{}, + i: -1, } } +// analyzeVoidCalls will check for functions that are not assigned +// and therefore we need to cleanup the return value from the stack. +func (c *funcScope) analyzeVoidCalls(node ast.Node) bool { + switch n := node.(type) { + case *ast.AssignStmt: + for i := 0; i < len(n.Rhs); i++ { + switch n.Rhs[i].(type) { + case *ast.CallExpr: + return false + } + } + case *ast.ReturnStmt: + switch n.Results[0].(type) { + case *ast.CallExpr: + return false + } + case *ast.CallExpr: + c.voidCalls[n] = true + } + return true +} + func (c *funcScope) stackSize() int64 { size := 0 ast.Inspect(c.decl, func(n ast.Node) bool { - switch n.(type) { + switch n := n.(type) { case *ast.AssignStmt, *ast.ReturnStmt, *ast.IfStmt: size++ + // This handles the inline GenDecl like "var x = 2" + case *ast.GenDecl: + switch t := n.Specs[0].(type) { + case *ast.ValueSpec: + if len(t.Values) > 0 { + size++ + } + } } return true }) @@ -53,11 +93,11 @@ func (c *funcScope) stackSize() int64 { if c.decl.Recv != nil { numArgs += len(c.decl.Recv.List) } - return int64(size + numArgs) + return int64(size + numArgs + len(c.voidCalls)) } -func (c *funcScope) newStruct() *structScope { - strct := newStructScope() +func (c *funcScope) newStruct(t *types.Struct) *structScope { + strct := newStructScope(t) c.structs[len(c.scope)] = strct return strct } diff --git a/pkg/vm/compiler/struct_scope.go b/pkg/vm/compiler/struct_scope.go index 8bf71fea7..992a559d9 100644 --- a/pkg/vm/compiler/struct_scope.go +++ b/pkg/vm/compiler/struct_scope.go @@ -1,7 +1,7 @@ package compiler import ( - "go/ast" + "go/constant" "go/types" "log" ) @@ -9,16 +9,61 @@ import ( // A structScope holds the positions for it's fields. Struct fields have different // positions then local variables in any scope. type structScope struct { - // identifier of the initialized struct in the program. - name string + // A pointer to the underlying type. + t *types.Struct - // a mapping of field identifier and its position. + // A mapping of fieldnames identifier and its position. fields map[string]int + + // A mapping of fieldnames and with type and value. + // This will be populated in "initFields" to initialize all + // structs fields to their zero value. + // strings: "" (just a pushf 0x00) + // int: 0 + // bool: false + typeAndValues map[string]types.TypeAndValue } -func newStructScope() *structScope { - return &structScope{ - fields: map[string]int{}, +// newStructScope will create a new structScope with all fields initialized. +func newStructScope(t *types.Struct) *structScope { + s := &structScope{ + fields: map[string]int{}, + typeAndValues: make(map[string]types.TypeAndValue, t.NumFields()), + t: t, + } + s.initFields() + return s +} + +func (s *structScope) initFields() { + var tv types.TypeAndValue + for i := 0; i < s.t.NumFields(); i++ { + f := s.t.Field(i) + s.newField(f.Name()) + + switch t := f.Type().(type) { + case *types.Basic: + switch t.Kind() { + case types.Int: + tv = types.TypeAndValue{ + Type: t, + Value: constant.MakeInt64(0), + } + case types.String: + tv = types.TypeAndValue{ + Type: t, + Value: constant.MakeString(""), + } + case types.Bool, types.UntypedBool: + tv = types.TypeAndValue{ + Type: t, + Value: constant.MakeBool(false), + } + default: + log.Fatalf("could not initialize struct field %s to zero, type: %s", f.Name(), t) + } + } + s.typeAndValues[f.Name()] = tv } } @@ -31,20 +76,13 @@ func (s *structScope) newField(name string) int { func (s *structScope) loadField(name string) int { i, ok := s.fields[name] if !ok { - log.Fatalf("could not resolve field name %s for struct %s", name, s.name) + log.Fatalf("could not resolve field %s for struct %v", name, s) } return i } -func (s *structScope) initializeFields(ident *ast.Ident, tInfo *types.Info) { - def, ok := tInfo.Defs[ident] - if !ok { - log.Fatalf("could not initialize fields of %s: definitions not found in typeinfo", ident.Name) - } - t, ok := def.Type().Underlying().(*types.Struct) - if !ok { - log.Fatalf("%s is not of type struct", ident.Name) - } +func (s *structScope) initialize(t *types.Struct) { + s.t = t for i := 0; i < t.NumFields(); i++ { s.newField(t.Field(i).Name()) } diff --git a/pkg/vm/compiler/tests/array_test.go b/pkg/vm/compiler/tests/array_test.go index 2b6c19883..1554814a7 100644 --- a/pkg/vm/compiler/tests/array_test.go +++ b/pkg/vm/compiler/tests/array_test.go @@ -1,4 +1,4 @@ -package compiler_test +package compiler var arrayTestCases = []testCase{ { diff --git a/pkg/vm/compiler/tests/assign_test.go b/pkg/vm/compiler/tests/assign_test.go index 7fa71d387..89984400e 100644 --- a/pkg/vm/compiler/tests/assign_test.go +++ b/pkg/vm/compiler/tests/assign_test.go @@ -1,4 +1,4 @@ -package compiler_test +package compiler var assignTestCases = []testCase{ { @@ -28,4 +28,88 @@ var assignTestCases = []testCase{ `, "53c56b546c766b00527ac4586c766b00527ac46203006c766b00c3616c7566", }, + { + "add assign", + ` + package foo + func Main() int { + x := 4 + x += 8 + return x + } + `, + "53c56b546c766b00527ac46c766b00c358936c766b00527ac46203006c766b00c3616c7566", + }, + { + "sub assign", + ` + package foo + func Main() int { + x := 4 + x -= 2 + return x + } + `, + "53c56b546c766b00527ac46c766b00c352946c766b00527ac46203006c766b00c3616c7566", + }, + { + "mul assign", + ` + package foo + func Main() int { + x := 4 + x *= 2 + return x + } + `, + "53c56b546c766b00527ac46c766b00c352956c766b00527ac46203006c766b00c3616c7566", + }, + { + "div assign", + ` + package foo + func Main() int { + x := 4 + x /= 2 + return x + } + `, + "53c56b546c766b00527ac46c766b00c352966c766b00527ac46203006c766b00c3616c7566", + }, + { + "add assign binary expr", + ` + package foo + func Main() int { + x := 4 + x += 6 + 2 + return x + } + `, + "53c56b546c766b00527ac46c766b00c358936c766b00527ac46203006c766b00c3616c7566", + }, + { + "add assign binary expr ident", + ` + package foo + func Main() int { + x := 4 + y := 5 + x += 6 + y + return x + } + `, + "54c56b546c766b00527ac4556c766b51527ac46c766b00c3566c766b51c393936c766b00527ac46203006c766b00c3616c7566", + }, + { + "decl assign", + ` + package foo + func Main() int { + var x int = 4 + return x + } + `, + "52c56b546c766b00527ac46203006c766b00c3616c7566", + }, } diff --git a/pkg/vm/compiler/tests/bar/bar.go b/pkg/vm/compiler/tests/bar/bar.go new file mode 100644 index 000000000..c75928251 --- /dev/null +++ b/pkg/vm/compiler/tests/bar/bar.go @@ -0,0 +1,8 @@ +package bar + +type Bar struct { + X int + Y int + Z string + B bool +} diff --git a/pkg/vm/compiler/tests/binary_expr_test.go b/pkg/vm/compiler/tests/binary_expr_test.go index 9389aff7c..03b8a80a8 100644 --- a/pkg/vm/compiler/tests/binary_expr_test.go +++ b/pkg/vm/compiler/tests/binary_expr_test.go @@ -1,4 +1,4 @@ -package compiler_test +package compiler var binaryExprTestCases = []testCase{ { @@ -69,4 +69,46 @@ var binaryExprTestCases = []testCase{ `, "54c56b546c766b00527ac4586c766b51527ac46c766b00c35293529358946c766b52527ac46203006c766b51c36c766b52c395616c7566", }, + { + "compare equal strings", + ` + package testcase + func Main() int { + str := "a string" + if str == "another string" { + return 1 + } + return 0 + } + `, + "54c56b086120737472696e676c766b00527ac46c766b00c30e616e6f7468657220737472696e679c640b0062030051616c756662030000616c7566", + }, + { + "compare equal ints", + ` + package testcase + func Main() int { + x := 10 + if x == 10 { + return 1 + } + return 0 + } + `, + "54c56b5a6c766b00527ac46c766b00c35a9c640b0062030051616c756662030000616c7566", + }, + { + "compare not equal ints", + ` + package testcase + func Main() int { + x := 10 + if x != 10 { + return 1 + } + return 0 + } + `, + "54c56b5a6c766b00527ac46c766b00c35a9c640b0062030051616c756662030000616c7566", + }, } diff --git a/pkg/vm/compiler/tests/bool_test.go b/pkg/vm/compiler/tests/bool_test.go index 90d09f9d9..bee5425aa 100644 --- a/pkg/vm/compiler/tests/bool_test.go +++ b/pkg/vm/compiler/tests/bool_test.go @@ -1,4 +1,4 @@ -package compiler_test +package compiler var boolTestCases = []testCase{ { diff --git a/pkg/vm/compiler/tests/compiler_test.go b/pkg/vm/compiler/tests/compiler_test.go index d3b55d800..e87c3481f 100644 --- a/pkg/vm/compiler/tests/compiler_test.go +++ b/pkg/vm/compiler/tests/compiler_test.go @@ -1,4 +1,4 @@ -package compiler_test +package compiler import ( "bytes" @@ -21,14 +21,25 @@ type testCase struct { func TestAllCases(t *testing.T) { testCases := []testCase{} + + // The Go language testCases = append(testCases, assignTestCases...) testCases = append(testCases, arrayTestCases...) + testCases = append(testCases, binaryExprTestCases...) testCases = append(testCases, functionCallTestCases...) testCases = append(testCases, boolTestCases...) testCases = append(testCases, stringTestCases...) - testCases = append(testCases, binaryExprTestCases...) testCases = append(testCases, structTestCases...) testCases = append(testCases, ifStatementTestCases...) + testCases = append(testCases, customTypeTestCases...) + + // TODO: issue #28 + // These tests are passing locally, but circleci is failing to resolve the dependency. + // https://github.com/CityOfZion/neo-go/issues/28 + // testCases = append(testCases, importTestCases...) + + // Blockchain specific + // testCases = append(testCases, storageTestCases...) for _, tc := range testCases { b, err := compiler.Compile(strings.NewReader(tc.src), &compiler.Options{}) @@ -54,16 +65,19 @@ func dumpOpCodeSideBySide(have, want []byte) { w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) fmt.Fprintln(w, "INDEX\tHAVE OPCODE\tDESC\tWANT OPCODE\tDESC\tDIFF") + var b byte for i := 0; i < len(have); i++ { if len(want) <= i { - break + b = 0x00 + } else { + b = want[i] } diff := "" - if have[i] != want[i] { + if have[i] != b { diff = "<<" } fmt.Fprintf(w, "%d\t0x%2x\t%s\t0x%2x\t%s\t%s\n", - i, have[i], vm.Opcode(have[i]), want[i], vm.Opcode(want[i]), diff) + i, have[i], vm.Opcode(have[i]), b, vm.Opcode(b), diff) } w.Flush() } diff --git a/pkg/vm/compiler/tests/custom_type_test.go b/pkg/vm/compiler/tests/custom_type_test.go new file mode 100644 index 000000000..dfaf3bd16 --- /dev/null +++ b/pkg/vm/compiler/tests/custom_type_test.go @@ -0,0 +1,25 @@ +package compiler + +var customTypeTestCases = []testCase{ + { + "test custom type", + ` + package foo + + type bar int + type specialString string + + func Main() specialString { + var x bar + var str specialString + x = 10 + str = "some short string" + if x == 10 { + return str + } + return "none" + } + `, + "55c56b5a6c766b00527ac411736f6d652073686f727420737472696e676c766b51527ac46c766b00c35a9c640f006203006c766b51c3616c7566620300046e6f6e65616c7566", + }, +} diff --git a/pkg/vm/compiler/tests/foo/foo.go b/pkg/vm/compiler/tests/foo/foo.go new file mode 100644 index 000000000..731c66bb9 --- /dev/null +++ b/pkg/vm/compiler/tests/foo/foo.go @@ -0,0 +1,6 @@ +package foo + +// NewBar return an integer \o/ +func NewBar() int { + return 10 +} diff --git a/pkg/vm/compiler/tests/function_call_test.go b/pkg/vm/compiler/tests/function_call_test.go index 947e86872..66cdbca30 100644 --- a/pkg/vm/compiler/tests/function_call_test.go +++ b/pkg/vm/compiler/tests/function_call_test.go @@ -1,4 +1,4 @@ -package compiler_test +package compiler var functionCallTestCases = []testCase{ { @@ -18,6 +18,22 @@ var functionCallTestCases = []testCase{ `, "53c56b5a6c766b00527ac461651c006c766b51527ac46203006c766b00c36c766b51c393616c756652c56b5a6c766b00527ac46203006c766b00c3616c7566", }, + { + "test function call with no assign", + ` + package testcase + func Main() int { + getSomeInteger() + getSomeInteger() + return 0 + } + + func getSomeInteger() int { + return 0 + } + `, + "53c56b616511007561650c007562030000616c756651c56b62030000616c7566", + }, { "multiple function calls", ` @@ -58,6 +74,21 @@ var functionCallTestCases = []testCase{ `, "53c56b5a6c766b00527ac46c766b00c3616516006c766b51527ac46203006c766b51c3616c756653c56b6c766b00527ac4586c766b51527ac46203006c766b00c36c766b51c393616c7566", }, + { + "function call with arguments of interface type", + ` + package testcase + func Main() interface{} { + x := getSomeInteger(10) + return x + } + + func getSomeInteger(x interface{}) interface{} { + return x + } + `, + "52c56b5a616516006c766b00527ac46203006c766b00c3616c756652c56b6c766b00527ac46203006c766b00c3616c7566", + }, { "function call with multiple arguments", ` @@ -73,4 +104,17 @@ var functionCallTestCases = []testCase{ `, "52c56b52547c616516006c766b00527ac46203006c766b00c3616c756653c56b6c766b00527ac46c766b51527ac46203006c766b00c36c766b51c393616c7566", }, + { + "test Main arguments", + ` + package foo + func Main(operation string, args []interface{}) int { + if operation == "mintTokens" { + return 1 + } + return 0 + } + `, + "55c56b6c766b00527ac46c766b51527ac46c766b00c30a6d696e74546f6b656e739c640b0062030051616c756662030000616c7566", + }, } diff --git a/pkg/vm/compiler/tests/if_statement_test.go b/pkg/vm/compiler/tests/if_statement_test.go index 71a2e4460..3ed9e44bd 100644 --- a/pkg/vm/compiler/tests/if_statement_test.go +++ b/pkg/vm/compiler/tests/if_statement_test.go @@ -1,4 +1,4 @@ -package compiler_test +package compiler var ifStatementTestCases = []testCase{ { diff --git a/pkg/vm/compiler/tests/import_test.go b/pkg/vm/compiler/tests/import_test.go new file mode 100644 index 000000000..3fcd695ea --- /dev/null +++ b/pkg/vm/compiler/tests/import_test.go @@ -0,0 +1,34 @@ +package compiler + +var importTestCases = []testCase{ + { + "import function", + ` + package somethingelse + + import "github.com/CityOfZion/neo-go/pkg/vm/compiler/tests/foo" + + func Main() int { + i := foo.NewBar() + return i + } + `, + "52c56b616516006c766b00527ac46203006c766b00c3616c756651c56b6203005a616c7566", + }, + { + "import test", + ` + package somethingwedontcareabout + + import "github.com/CityOfZion/neo-go/pkg/vm/compiler/tests/bar" + + func Main() int { + b := bar.Bar{ + X: 4, + } + return b.Y + } + `, + "52c56b6154c66b546c766b00527ac4006c766b51527ac4006c766b52527ac4006c766b53527ac46c6c766b00527ac46203006c766b00c351c3616c7566", + }, +} diff --git a/pkg/vm/compiler/tests/storage_test.go b/pkg/vm/compiler/tests/storage_test.go new file mode 100644 index 000000000..e62f6f7b3 --- /dev/null +++ b/pkg/vm/compiler/tests/storage_test.go @@ -0,0 +1,20 @@ +package compiler + +var storageTestCases = []testCase{ + { + "interop storage test", + ` + package foo + + import "github.com/CityOfZion/neo-go/pkg/vm/smartcontract/storage" + + func Main() int { + ctx := storage.GetContext() + storage.Put(ctx, "amount", 1000) + amount := storage.GetInt(ctx, "amount") + return amount + } + `, + "54c56b6168164e656f2e53746f726167652e476574436f6e74657874616c766b00527ac46c766b00c306616d6f756e7402e803527261680f4e656f2e53746f726167652e507574616c766b00c306616d6f756e747c61680f4e656f2e53746f726167652e476574616c766b51527ac46203006c766b51c3616c756651c56b62030000616c756654c56b6c766b00527ac46c766b51527ac46c766b52527ac462030000616c756653c56b6c766b00527ac46c766b51527ac462030000616c756653c56b6c766b00527ac46c766b51527ac462030000616c756653c56b6c766b00527ac46c766b51527ac462030000616c7566", + }, +} diff --git a/pkg/vm/compiler/tests/string_test.go b/pkg/vm/compiler/tests/string_test.go index 93453cca0..284b87c9f 100644 --- a/pkg/vm/compiler/tests/string_test.go +++ b/pkg/vm/compiler/tests/string_test.go @@ -1,4 +1,4 @@ -package compiler_test +package compiler var stringTestCases = []testCase{ { diff --git a/pkg/vm/compiler/tests/struct_test.go b/pkg/vm/compiler/tests/struct_test.go index 758fcbe17..03a3047f8 100644 --- a/pkg/vm/compiler/tests/struct_test.go +++ b/pkg/vm/compiler/tests/struct_test.go @@ -1,15 +1,10 @@ -package compiler_test +package compiler var structTestCases = []testCase{ { "struct field assign", ` package foo - type token struct { - x int - y int - } - func Main() int { t := token { x: 2, @@ -19,6 +14,11 @@ var structTestCases = []testCase{ age := t.x return age } + + type token struct { + x int + y int + } `, "53c56b6152c66b526c766b00527ac4546c766b51527ac46c6c766b00527ac46c766b00c300c36c766b51527ac46203006c766b51c3616c7566", }, @@ -154,4 +154,50 @@ var structTestCases = []testCase{ `, "53c56b6151c66b546c766b00527ac46c6c766b00527ac46c766b00c352545272616516006c766b51527ac46203006c766b51c3616c756654c56b6c766b00527ac46c766b51527ac46c766b52527ac46203006c766b00c300c36c766b51c3936c766b52c393616c7566", }, + { + "initialize struct partially", + ` + package foo + type token struct { + x int + y int + z string + b bool + } + + func Main() int { + t := token { + x: 4, + } + return t.y + } + `, + "52c56b6154c66b546c766b00527ac4006c766b51527ac4006c766b52527ac4006c766b53527ac46c6c766b00527ac46203006c766b00c351c3616c7566", + }, + { + "test return struct from func", + ` + package foo + type token struct { + x int + y int + z string + b bool + } + + func newToken() token { + return token{ + x: 1, + y: 2, + z: "hello", + b: false, + } + } + + func Main() token { + return newToken() + } + `, + "51c56b62030061650700616c756651c56b6203006154c66b516c766b00527ac4526c766b51527ac40568656c6c6f6c766b52527ac4006c766b53527ac46c616c7566", + }, } diff --git a/pkg/vm/smartcontract/runtime/runtime.go b/pkg/vm/smartcontract/runtime/runtime.go new file mode 100644 index 000000000..e237af3e9 --- /dev/null +++ b/pkg/vm/smartcontract/runtime/runtime.go @@ -0,0 +1,14 @@ +package runtime + +// TriggerType represents a byte. +type TriggerType byte + +// List of valid trigger types. +const ( + Verification TriggerType = 0x00 + Application TriggerType = 0x10 +) + +// GetTrigger return the current trigger type. The return in this function +// doesn't really mather, this is just an interop placeholder. +func GetTrigger() TriggerType { return 0x00 } diff --git a/pkg/vm/smartcontract/storage/storage.go b/pkg/vm/smartcontract/storage/storage.go new file mode 100644 index 000000000..885f7b7c9 --- /dev/null +++ b/pkg/vm/smartcontract/storage/storage.go @@ -0,0 +1,16 @@ +package storage + +// GetContext .. +func GetContext() int { return 0 } + +// Put stores a value in to the storage. +func Put(ctx interface{}, key interface{}, value interface{}) int { return 0 } + +// GetInt returns the value as an integer. +func GetInt(ctx interface{}, key interface{}) int { return 0 } + +// GetString returns the value as an string. +func GetString(ctx interface{}, key interface{}) string { return "" } + +// Delete removes a stored key value pair. +func Delete(ctx interface{}, key interface{}) int { return 0 } diff --git a/pkg/vm/syscall.go b/pkg/vm/syscall.go new file mode 100644 index 000000000..275400195 --- /dev/null +++ b/pkg/vm/syscall.go @@ -0,0 +1,12 @@ +package vm + +// Syscalls is a mapping between the syscall function name +// and the registerd VM interop API. +var Syscalls = map[string]string{ + // Storage API + "GetContext": "Neo.Storage.GetContext", + "Put": "Neo.Storage.Put", + "GetInt": "Neo.Storage.Get", + "GetString": "Neo.Storage.Get", + "Delete": "Neo.Storage.Delete", +}