From b257a06f3ef6d1a965a291c16bc7ecd786d721c6 Mon Sep 17 00:00:00 2001 From: Anthony De Meulemeester Date: Thu, 15 Feb 2018 16:35:49 +0100 Subject: [PATCH] Compiler update (#21) * added seperate folders for cmd packages. * Fix netmodes in test + reverse bigint bytes * glide get deps * add, sub, mul, div * booleans * strings * binary expressions * if statements * function calls * composite literals (slice, array) * Added lots of test cases and update readme. --- README.md | 47 +- VERSION | 2 +- cli/smartcontract/smart_contract.go | 45 +- pkg/util/uint256.go | 6 +- pkg/util/uint256_test.go | 24 + pkg/vm/compiler.go | 194 -------- pkg/vm/compiler/compiler.go | 503 ++++++++++++++++++++ pkg/vm/compiler/func_context.go | 96 ++++ pkg/vm/compiler/script_builder.go | 152 ++++++ pkg/vm/compiler/script_builder_test.go | 104 ++++ pkg/vm/compiler/tests/binary_expr_test.go | 72 +++ pkg/vm/compiler/tests/compiler_test.go | 65 +++ pkg/vm/compiler/tests/function_call_test.go | 61 +++ pkg/vm/compiler/tests/if_statement_test.go | 74 +++ pkg/vm/compiler/tests/string_test.go | 15 + pkg/vm/compiler_test.go | 86 ---- pkg/vm/script_builder.go | 127 ----- pkg/vm/script_builder_test.go | 74 --- 18 files changed, 1253 insertions(+), 494 deletions(-) delete mode 100644 pkg/vm/compiler.go create mode 100644 pkg/vm/compiler/compiler.go create mode 100644 pkg/vm/compiler/func_context.go create mode 100644 pkg/vm/compiler/script_builder.go create mode 100644 pkg/vm/compiler/script_builder_test.go create mode 100644 pkg/vm/compiler/tests/binary_expr_test.go create mode 100644 pkg/vm/compiler/tests/compiler_test.go create mode 100644 pkg/vm/compiler/tests/function_call_test.go create mode 100644 pkg/vm/compiler/tests/if_statement_test.go create mode 100644 pkg/vm/compiler/tests/string_test.go delete mode 100644 pkg/vm/compiler_test.go delete mode 100644 pkg/vm/script_builder.go delete mode 100644 pkg/vm/script_builder_test.go diff --git a/README.md b/README.md index e25fd85ed..14a4af689 100644 --- a/README.md +++ b/README.md @@ -74,23 +74,23 @@ Currently, there is a minimal subset of the NEO protocol implemented. To start experimenting make sure you a have a private net running on your machine. If you dont, take a look at [docker-privnet-with-gas](https://hub.docker.com/r/metachris/neo-privnet-with-gas/). -Start the server: +Start a NEO node: ``` -./bin/neo-go -seed 127.0.0.1:20333 +./bin/neo-go node -seed 127.0.0.1:20333 ``` You can add multiple seeds if you want: ``` -./bin/neo-go -seed 127.0.0.1:20333,127.0.01:20334 +./bin/neo-go node -seed 127.0.0.1:20333,127.0.01:20334 ``` By default the server will currently run on port 3000, for testing purposes. You can change that by setting the tcp flag: ``` -./bin/neo-go -seed 127.0.0.1:20333 -tcp 1337 +./bin/neo-go node -seed 127.0.0.1:20333 -tcp 1337 ``` ## RPC @@ -98,10 +98,10 @@ You can change that by setting the tcp flag: If you want your node to also serve JSON-RPC, you can do that by setting the following flag: ``` -./bin/neo-go -rpc 4000 +./bin/neo-go node -rpc 4000 ``` -In this case server will accept and respond JSON-RPC on port 4000. +In this case the server will accept and respond JSON-RPC on port 4000. Keep in mind that currently there is only a small subset of the JSON-RPC implemented. Feel free to make a PR with more functionality. @@ -112,9 +112,42 @@ TODO ``` ## Smart Contracts +> NOTE: At this moment there is only a small subset of the Go language implemented. + +### Compile a smart contract ``` -TODO +./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 ``` # Contributing diff --git a/VERSION b/VERSION index ac39a106c..78bc1abd1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.0 +0.10.0 diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 2f2b00b9f..e2cc2084d 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -1,9 +1,14 @@ package smartcontract import ( + "encoding/hex" + "errors" "fmt" + "io" + "os" + "strings" - "github.com/CityOfZion/neo-go/pkg/vm" + "github.com/CityOfZion/neo-go/pkg/vm/compiler" "github.com/urfave/cli" ) @@ -17,6 +22,12 @@ func NewCommand() cli.Command { Name: "compile", Usage: "compile a smart contract to a .avm file", Action: contractCompile, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "out, o", + Usage: "Output of the compiled contract", + }, + }, }, { Name: "opdump", @@ -28,14 +39,40 @@ func NewCommand() cli.Command { } func contractCompile(ctx *cli.Context) error { - fmt.Println("compile") - return nil + if len(ctx.Args()) == 0 { + return errors.New("not enough arguments") + } + + src := ctx.Args()[0] + c := compiler.New() + if err := c.CompileSource(src); err != nil { + return err + } + + filename := strings.Split(src, ".")[0] + filename = filename + ".avm" + + out := ctx.String("out") + if len(out) > 0 { + filename = out + } + + f, err := os.Create(out) + if err != nil { + return err + } + + hx := hex.EncodeToString(c.Buffer().Bytes()) + fmt.Println(hx) + + _, err = io.Copy(f, c.Buffer()) + return err } func contractDumpOpcode(ctx *cli.Context) error { src := ctx.Args()[0] - c := vm.NewCompiler() + c := compiler.New() if err := c.CompileSource(src); err != nil { return err } diff --git a/pkg/util/uint256.go b/pkg/util/uint256.go index ee2a6ec54..0f8c03439 100644 --- a/pkg/util/uint256.go +++ b/pkg/util/uint256.go @@ -45,8 +45,12 @@ func Uint256DecodeFromBytes(b []byte) (Uint256, error) { // ToArrayReverse return a reversed version of the given byte slice. func ToArrayReverse(b []byte) []byte { - dest := make([]byte, len(b)) + // Protect from big.Ints that have 1 len bytes. + if len(b) < 2 { + return b + } + dest := make([]byte, len(b)) for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 { dest[i], dest[j] = b[j], b[i] } diff --git a/pkg/util/uint256_test.go b/pkg/util/uint256_test.go index c7d868219..5229c138d 100644 --- a/pkg/util/uint256_test.go +++ b/pkg/util/uint256_test.go @@ -1 +1,25 @@ package util + +import ( + "bytes" + "testing" +) + +func TestToArrayReverse(t *testing.T) { + arr := []byte{0x01, 0x02, 0x03, 0x04} + have := ToArrayReverse(arr) + want := []byte{0x04, 0x03, 0x02, 0x01} + if bytes.Compare(have, want) != 0 { + t.Fatalf("expected %v got %v", want, have) + } +} + +// This tests a bug that occured with arrays of size 1 +func TestToArrayReverseLen2(t *testing.T) { + arr := []byte{0x01} + have := ToArrayReverse(arr) + want := []byte{0x01} + if bytes.Compare(have, want) != 0 { + t.Fatalf("expected %v got %v", want, have) + } +} diff --git a/pkg/vm/compiler.go b/pkg/vm/compiler.go deleted file mode 100644 index 9aeb7282d..000000000 --- a/pkg/vm/compiler.go +++ /dev/null @@ -1,194 +0,0 @@ -package vm - -import ( - "bytes" - "fmt" - "go/ast" - "go/parser" - "go/token" - "io" - "os" - "reflect" - "strconv" - "strings" -) - -const ( - outputExt = ".avm" -) - -// Syscalls that have no return value. -var nonReturnSysCalls = []string{ - "Notify", "print", "Log", "Put", "Register", - "append", "Delete", "SetVotes", "ContractDestroy", - "MerkleRoot", "Hash", "PrevHash", "GetHeader"} - -// Compiler holds the output buffer of the compiled source. -type Compiler struct { - // Output extension of the file. Default .avm. - OutputExt string - sb *ScriptBuilder - curLineNum int - - i int - varList []Variable - vars map[string]Variable -} - -// NewCompiler returns a new compiler ready to compile smartcontracts. -func NewCompiler() *Compiler { - return &Compiler{ - OutputExt: outputExt, - sb: &ScriptBuilder{new(bytes.Buffer)}, - vars: map[string]Variable{}, - varList: []Variable{}, - } -} - -// CompileSource will compile the source file into an avm format. -func (c *Compiler) CompileSource(src string) error { - file, err := os.Open(src) - if err != nil { - return err - } - return c.Compile(file) -} - -// Visit implements the ast.Visitor interface. -func (c *Compiler) Visit(node ast.Node) ast.Visitor { - switch t := node.(type) { - case *ast.AssignStmt: - c.processAssignStmt(t) - case *ast.FuncDecl: - case *ast.ReturnStmt: - } - return c -} - -func (c *Compiler) newVariable(k token.Token, i, val string) Variable { - v := Variable{ - kind: k, - ident: i, - value: val, - pos: c.i, - } - c.vars[v.ident] = v - c.i++ - return v -} - -func (c *Compiler) initialize(n OpCode) { - // Get the (n) localVars which is basicly the number of args passed in Main - // and the number of local Vars in the function body. - c.sb.emitPush(n) - c.sb.emitPush(OpNewArray) - c.sb.emitPush(OpToAltStack) -} - -func (c *Compiler) teardown() { - c.sb.emitPush(OpNOP) - c.sb.emitPush(OpFromAltStack) - c.sb.emitPush(OpDrop) - c.sb.emitPush(OpRET) -} - -// Push a variable on to the stack. -func (c *Compiler) storeLocal(v Variable) { - if v.kind == token.INT { - val, _ := strconv.Atoi(v.value) - c.sb.emitPushInt(int64(val)) - } - if v.kind == token.STRING { - val := strings.Replace(v.value, `"`, "", 2) - c.sb.emitPushString(val) - } - - c.sb.emitPush(OpFromAltStack) - c.sb.emitPush(OpDup) - c.sb.emitPush(OpToAltStack) - - pos := int64(v.pos) - - c.sb.emitPushInt(pos) - c.sb.emitPushInt(2) - c.sb.emitPush(OpRoll) - c.sb.emitPush(OpSetItem) -} - -func (c *Compiler) loadLocal(ident string) { - val, ok := c.vars[ident] - if !ok { - c.reportError(fmt.Sprintf("local variable %s not found", ident)) - } - - pos := int64(val.pos) - - c.sb.emitPush(OpFromAltStack) - c.sb.emitPush(OpDup) - c.sb.emitPush(OpToAltStack) - - // push it's index on the stack - c.sb.emitPushInt(pos) - c.sb.emitPush(OpPickItem) -} - -// TODO: instead of passing the stmt in to this, put the lhs and rhs in. -// so we can reuse this. -func (c *Compiler) processAssignStmt(stmt *ast.AssignStmt) { - lhs := stmt.Lhs[0].(*ast.Ident) - switch t := stmt.Rhs[0].(type) { - case *ast.BasicLit: - c.storeLocal(c.newVariable(t.Kind, lhs.Name, t.Value)) - case *ast.CompositeLit: - switch t.Type.(type) { - case *ast.StructType: - c.reportError("assigning struct literals not yet implemented") - case *ast.ArrayType: - // for _, expr := range t.Elts { - // v := expr.(*ast.BasicLit) - // c.storeLocal(c.newVariable(v.Kind, lhs.Name, v.Value)) - // } - } - case *ast.Ident: - c.loadLocal(t.Name) - case *ast.FuncLit: - c.reportError("assigning function literals not yet implemented") - default: - fmt.Println(reflect.TypeOf(t)) - } -} - -// Compile will compile from r into an avm format. -func (c *Compiler) Compile(r io.Reader) error { - fset := token.NewFileSet() - f, err := parser.ParseFile(fset, "", r, 0) - if err != nil { - return err - } - - c.initialize(OpPush2) // initialize the compiler with n local stack vars. - ast.Walk(c, f) // walk through and process the AST - c.teardown() // done compiling - - return nil -} - -// TODO: More detailed report (lineno, ...) -func (c *Compiler) reportError(msg string) { - fmt.Printf("COMPILER ERROR :: %s\n", msg) - os.Exit(1) -} - -// DumpOpcode dumps the current buffer, formatted with index, hex and opcode. -// Usefull for debugging smartcontracts. -func (c *Compiler) DumpOpcode() { - c.sb.dumpOpcode() -} - -// A Variable can represent any variable in the program. -type Variable struct { - ident string - kind token.Token - value string - pos int -} diff --git a/pkg/vm/compiler/compiler.go b/pkg/vm/compiler/compiler.go new file mode 100644 index 000000000..eecd355a2 --- /dev/null +++ b/pkg/vm/compiler/compiler.go @@ -0,0 +1,503 @@ +package compiler + +import ( + "bytes" + "go/ast" + "go/constant" + "go/importer" + "go/parser" + "go/token" + "go/types" + "io" + "log" + "os" + "reflect" + + "github.com/CityOfZion/neo-go/pkg/vm" +) + +const ( + outputExt = ".avm" + // Identifier off the entry point function. + mainIdent = "Main" +) + +// CallContext represents more details on function calls in the program. +// It stores the position off where the call happend along with the +// function it called. The compiler will store all function calls, so +// it can update them later. +type CallContext struct { + pos int + funcName string +} + +// A VarContext holds the info about the context of a variable in the program. +type VarContext struct { + name string + tinfo types.TypeAndValue + pos int +} + +func newVarContext(name string, tinfo types.TypeAndValue) *VarContext { + return &VarContext{ + name: name, + pos: -1, + tinfo: tinfo, + } +} + +// Compiler holds the output buffer of the compiled source. +type Compiler struct { + // Output extension of the file. Default .avm. + OutputExt string + + // scriptBuilder is responsible for all opcode writes. + sb *ScriptBuilder + + // map with all function contexts across the program. + funcs map[string]*FuncContext + + // list of function calls + funcCalls []CallContext + + // struct with info about decls, types, uses, .. + typeInfo *types.Info +} + +// New returns a new compiler ready to compile smartcontracts. +func New() *Compiler { + return &Compiler{ + OutputExt: outputExt, + sb: &ScriptBuilder{buf: new(bytes.Buffer)}, + funcs: map[string]*FuncContext{}, + funcCalls: []CallContext{}, + } +} + +// CompileSource will compile the source file into an avm format. +func (c *Compiler) CompileSource(src string) error { + file, err := os.Open(src) + if err != nil { + return err + } + return c.Compile(file) +} + +// LoadConst load a constant, if storeLocal is true it will store it on the position +// of the VarContext. +func (c *Compiler) loadConst(ctx *VarContext, storeLocal bool) { + switch t := ctx.tinfo.Type.(type) { + case *types.Basic: + switch t.Kind() { + case types.Int: + val, _ := constant.Int64Val(ctx.tinfo.Value) + c.sb.emitPushInt(val) + case types.String: + val := constant.StringVal(ctx.tinfo.Value) + c.sb.emitPushString(val) + case types.Bool, types.UntypedBool: + val := constant.BoolVal(ctx.tinfo.Value) + c.sb.emitPushBool(val) + } + default: + log.Fatalf("compiler don't know how to handle this => %v", ctx) + } + + if storeLocal { + c.storeLocal(ctx) + } +} + +// Load a local variable. The position of the VarContext is used to retrieve from +// that position. +func (c *Compiler) loadLocal(ctx *VarContext) { + pos := int64(ctx.pos) + if pos < 0 { + log.Fatalf("want to load local %v but got invalid position => %d <=", ctx, pos) + } + + c.sb.emitPush(vm.OpFromAltStack) + c.sb.emitPush(vm.OpDup) + c.sb.emitPush(vm.OpToAltStack) + + // push it's index on the stack + c.sb.emitPushInt(pos) + c.sb.emitPush(vm.OpPickItem) +} + +// Store a local variable on the stack. The position of the VarContext is used +// to store at that position. +func (c *Compiler) storeLocal(vctx *VarContext) { + c.sb.emitPush(vm.OpFromAltStack) + c.sb.emitPush(vm.OpDup) + c.sb.emitPush(vm.OpToAltStack) + + pos := int64(vctx.pos) + if pos < 0 { + log.Fatalf("want to store local %v but got invalid positionl => %d", vctx, pos) + } + + c.sb.emitPushInt(pos) + c.sb.emitPushInt(2) + c.sb.emitPush(vm.OpRoll) + c.sb.emitPush(vm.OpSetItem) +} + +// Compile will compile from r into an avm format. +func (c *Compiler) Compile(r io.Reader) error { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "", r, 0) + if err != nil { + return err + } + + 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), + } + + c.typeInfo = typeInfo + + // Typechecker + _, err = conf.Check("", fset, []*ast.File{f}, typeInfo) + if err != nil { + log.Fatal(err) + } + + var main *ast.FuncDecl + ast.Inspect(f, func(n ast.Node) bool { + switch t := n.(type) { + case *ast.FuncDecl: + if t.Name.Name == mainIdent { + main = t + return false + } + } + return true + }) + if main == nil { + log.Fatal("could not find func main. did you forgot to declare it?") + } + + c.resolveFuncDecls(f) + c.convertFuncDecl(main) + + // Start building all declarations + for _, decl := range f.Decls { + switch t := decl.(type) { + case *ast.GenDecl: + case *ast.FuncDecl: + if t.Name.Name != mainIdent { + c.convertFuncDecl(t) + } + } + } + + // update all local function calls. + c.updateFuncCalls() + + return nil +} + +// updateFuncCalls will update all local function calls that occured the program. +func (c *Compiler) updateFuncCalls() { + for _, ctx := range c.funcCalls { + fun, ok := c.funcs[ctx.funcName] + if !ok { + log.Fatalf("could not resolve function %s", ctx.funcName) + } + // pos is the position of the call op, we need to add 1 to get the + // start of the label. + // for calculating the correct offset we need to subtract the target label + // with the position the call occured. + offset := fun.label - int16(ctx.pos) + c.sb.updatePushCall(ctx.pos+1, offset) + } +} + +func (c *Compiler) resolveFuncDecls(f *ast.File) { + for _, decl := range f.Decls { + switch t := decl.(type) { + case *ast.GenDecl: + case *ast.FuncDecl: + if t.Name.Name != mainIdent { + c.funcs[t.Name.Name] = newFuncContext(t, 0) + } + } + } +} + +func (c *Compiler) convertFuncDecl(decl *ast.FuncDecl) { + ctx := newFuncContext(decl, c.currentPos()) + c.funcs[ctx.name] = ctx + + // We need to write the the total stack size of the function first. + // That size is the number of arguments + body operations that will be + // pushed on the stack + c.sb.emitPushInt(ctx.numStackOps()) + c.sb.emitPush(vm.OpNewArray) + c.sb.emitPush(vm.OpToAltStack) + + // Load the arguments into scope. + for _, arg := range decl.Type.Params.List { + name := arg.Names[0].Name + ctx.args[name] = true + vctx := ctx.newConst(name, c.getTypeInfo(arg.Type), true) + c.storeLocal(vctx) + } + + for _, stmt := range decl.Body.List { + c.convertStmt(ctx, stmt) + } +} + +func (c *Compiler) convertStmt(fctx *FuncContext, stmt ast.Stmt) { + switch t := stmt.(type) { + case *ast.AssignStmt: + for i := 0; i < len(t.Lhs); i++ { + lhs := t.Lhs[i].(*ast.Ident) + + switch rhs := t.Rhs[i].(type) { + case *ast.BasicLit: + vctx := fctx.newConst(lhs.Name, c.getTypeInfo(t.Rhs[i]), true) + c.loadConst(vctx, true) + + case *ast.CompositeLit: + // Write constants in reverse order on the stack. + n := len(rhs.Elts) + for i := n - 1; i >= 0; i-- { + vctx := fctx.newConst("", c.getTypeInfo(rhs.Elts[i]), false) + c.loadConst(vctx, false) + } + + c.sb.emitPushInt(int64(n)) + c.sb.emitPush(vm.OpPack) + + vctx := fctx.newConst(lhs.Name, c.getTypeInfo(rhs), true) + c.storeLocal(vctx) + + case *ast.Ident: + if isIdentBool(rhs) { + vctx := fctx.newConst(lhs.Name, makeBoolFromIdent(rhs, c.typeInfo), true) + c.loadConst(vctx, true) + continue + } + + knownCtx := fctx.getContext(rhs.Name) + c.loadLocal(knownCtx) + newCtx := fctx.newConst(lhs.Name, c.getTypeInfo(rhs), true) + c.storeLocal(newCtx) + + default: + c.convertExpr(fctx, t.Rhs[i]) + vctx := fctx.newConst(lhs.Name, c.getTypeInfo(t.Rhs[i]), true) + c.storeLocal(vctx) + } + } + + //Due to the design of the orginal VM, multiple return are not supported. + case *ast.ReturnStmt: + if len(t.Results) > 1 { + log.Fatal("multiple returns not supported.") + } + + c.sb.emitPush(vm.OpJMP) + c.sb.emitPush(vm.OpCode(0x03)) + c.sb.emitPush(vm.OpPush0) + + c.convertExpr(fctx, t.Results[0]) + + c.sb.emitPush(vm.OpNOP) + c.sb.emitPush(vm.OpFromAltStack) + c.sb.emitPush(vm.OpDrop) + c.sb.emitPush(vm.OpRET) + + // TODO: this needs a rewrite ASAP. + case *ast.IfStmt: + c.convertExpr(fctx, t.Cond) + + binExpr, ok := t.Cond.(*ast.BinaryExpr) + if ok && binExpr.Op != token.LAND && binExpr.Op != token.LOR { + // use a placeholder for the label. + c.sb.emitJump(vm.OpJMPIFNOT, int16(0)) + // track our offset to update later subtract sizeOf int16. + offset := int(c.currentPos()) - 2 + + defer func(offset int) { + jumpTo := c.currentPos() + 1 - int16(offset) + c.sb.updateJmpLabel(jumpTo, offset) + }(offset) + } + + labelBeforeBlock := c.currentPos() + // Process the block. + for _, stmt := range t.Body.List { + c.convertStmt(fctx, stmt) + } + + // if there are any labels we need to update. + if len(fctx.jumpLabels) > 0 { + for _, label := range fctx.jumpLabels { + var pos int16 + if label.op == vm.OpJMPIF { + pos = labelBeforeBlock + 1 + } else { + pos = c.currentPos() + 1 + } + jumpTo := pos - int16(label.offset) + c.sb.updateJmpLabel(jumpTo, label.offset) + } + fctx.jumpLabels = []jumpLabel{} + } + + default: + log.Fatalf("compiler has not implemented this statement => %v", reflect.TypeOf(t)) + } +} + +func (c *Compiler) convertExpr(fctx *FuncContext, expr ast.Expr) { + switch t := expr.(type) { + case *ast.BasicLit: + vctx := fctx.newConst("", c.getTypeInfo(t), false) + c.loadConst(vctx, false) + + case *ast.Ident: + if isIdentBool(t) { + vctx := fctx.newConst(t.Name, makeBoolFromIdent(t, c.typeInfo), false) + c.loadConst(vctx, false) + return + } + if fctx.isArgument(t.Name) { + vctx := fctx.getContext(t.Name) + c.loadLocal(vctx) + return + } + vctx := fctx.getContext(t.Name) + c.loadLocal(vctx) + + case *ast.CallExpr: + fun := t.Fun.(*ast.Ident) + fctx, ok := c.funcs[fun.Name] + if !ok { + log.Fatalf("could not resolve func %s", fun.Name) + } + + // handle the passed arguments. + for _, arg := range t.Args { + vctx := fctx.newConst("", c.getTypeInfo(arg), false) + c.loadLocal(vctx) + } + + // c# compiler adds a NOP (0x61) before every function call. Dont think its relevant + // and we could easily removed it, but to be consistent with the original compiler I + // will put them in. ^^ + c.sb.emitPush(vm.OpNOP) + + c.funcCalls = append(c.funcCalls, CallContext{int(c.currentPos()), fun.Name}) + c.sb.emitPushCall(0) // placeholder, update later. + + case *ast.BinaryExpr: + if t.Op == token.LAND || t.Op == token.LOR { + c.convertExpr(fctx, t.X) + + opJMP := vm.OpJMPIFNOT + if t.Op == token.LOR { + opJMP = vm.OpJMPIF + } + + if e, ok := t.X.(*ast.BinaryExpr); ok && e.Op != token.LAND && e.Op != token.LOR { + c.sb.emitJump(opJMP, int16(0)) + fctx.addJump(opJMP, int(c.currentPos())-2) + } + + c.convertExpr(fctx, t.Y) + c.sb.emitJump(vm.OpJMPIFNOT, int16(0)) + fctx.addJump(vm.OpJMPIFNOT, int(c.currentPos())-2) + c.convertToken(t.Op) + return + } + + // The AST package resolves all basic literals for us. If the typeinfo.Value is not nil + // we know that the bin expr is resolved and needs no further action. + // e.g. x := 2 + 2 + 2 will be resolved to 6. + if tinfo := c.getTypeInfo(t); tinfo.Value != nil { + vctx := fctx.newConst("", tinfo, false) + c.loadConst(vctx, false) + return + } + + c.convertExpr(fctx, t.X) + c.convertExpr(fctx, t.Y) + c.convertToken(t.Op) + + default: + log.Fatalf("compiler has not implemented this expr => %v", reflect.TypeOf(t)) + } +} + +func (c *Compiler) convertToken(tok token.Token) { + switch tok { + case token.ADD: + c.sb.emitPush(vm.OpAdd) + case token.SUB: + c.sb.emitPush(vm.OpSub) + case token.MUL: + c.sb.emitPush(vm.OpMul) + case token.QUO: + c.sb.emitPush(vm.OpDiv) + case token.LSS: + c.sb.emitPush(vm.OpLT) + case token.LEQ: + c.sb.emitPush(vm.OpLTE) + case token.GTR: + c.sb.emitPush(vm.OpGT) + case token.GEQ: + c.sb.emitPush(vm.OpGTE) + } +} + +// getTypeInfo return TypeAndValue for the given expression. If it could not resolve +// the type value and type will be NIL. +func (c *Compiler) getTypeInfo(expr ast.Expr) types.TypeAndValue { + return c.typeInfo.Types[expr] +} + +// currentPos return the current position (address) of the latest opcode. +func (c *Compiler) currentPos() int16 { + return int16(c.sb.buf.Len()) +} + +// Buffer returns the buffer of the builder as a io.Reader. +func (c *Compiler) Buffer() *bytes.Buffer { + return c.sb.buf +} + +// DumpOpcode dumps the current buffer, formatted with index, hex and opcode. +func (c *Compiler) DumpOpcode() { + c.sb.dumpOpcode() +} + +func makeBoolFromIdent(ident *ast.Ident, tinfo *types.Info) types.TypeAndValue { + var b bool + if ident.Name == "true" { + b = true + } else if ident.Name == "false" { + b = false + } else { + log.Fatalf("givent identifier cannot be converted to a boolean => %s", ident.Name) + } + + return types.TypeAndValue{ + Type: tinfo.ObjectOf(ident).Type(), + Value: constant.MakeBool(b), + } +} + +func isIdentBool(ident *ast.Ident) bool { + return ident.Name == "true" || ident.Name == "false" +} diff --git a/pkg/vm/compiler/func_context.go b/pkg/vm/compiler/func_context.go new file mode 100644 index 000000000..e00e6dcc4 --- /dev/null +++ b/pkg/vm/compiler/func_context.go @@ -0,0 +1,96 @@ +package compiler + +import ( + "go/ast" + "go/types" + "log" + + "github.com/CityOfZion/neo-go/pkg/vm" +) + +type jumpLabel struct { + offset int + op vm.OpCode +} + +// A FuncContext represents details about a function in the program along withs its variables. +type FuncContext struct { + // The declaration tree of this function. + decl *ast.FuncDecl + // Identifier (name of the function in the program). + name string + // The scope of the function. + scope map[string]*VarContext + // Arguments of the function. + args map[string]bool + // Address (label) where the compiler can find this function when someone calls it. + label int16 + // Counter for stored local variables. + i int + // This needs refactor along with the (if stmt) + jumpLabels []jumpLabel +} + +func (f *FuncContext) addJump(op vm.OpCode, offset int) { + f.jumpLabels = append(f.jumpLabels, jumpLabel{offset, op}) +} + +func newFuncContext(decl *ast.FuncDecl, label int16) *FuncContext { + return &FuncContext{ + decl: decl, + label: int16(label), + name: decl.Name.Name, + scope: map[string]*VarContext{}, + args: map[string]bool{}, + jumpLabels: []jumpLabel{}, + } +} + +func (f *FuncContext) newConst(name string, t types.TypeAndValue, needStore bool) *VarContext { + ctx := &VarContext{ + name: name, + tinfo: t, + } + if needStore { + f.storeContext(ctx) + } + return ctx +} + +func (f *FuncContext) numStackOps() int64 { + ops := 0 + ast.Inspect(f.decl, func(n ast.Node) bool { + switch n.(type) { + case *ast.AssignStmt, *ast.ReturnStmt, *ast.IfStmt: + ops++ + } + return true + }) + + numArgs := len(f.decl.Type.Params.List) + return int64(ops + numArgs) +} + +func (f *FuncContext) storeContext(ctx *VarContext) { + ctx.pos = f.i + f.scope[ctx.name] = ctx + f.i++ +} + +func (f *FuncContext) getContext(name string) *VarContext { + ctx, ok := f.scope[name] + if !ok { + log.Fatalf("could not resolve variable %s", name) + } + return ctx +} + +func (f *FuncContext) isRegistered(ctx *VarContext) bool { + _, ok := f.scope[ctx.name] + return ok +} + +func (f *FuncContext) isArgument(name string) bool { + _, ok := f.args[name] + return ok +} diff --git a/pkg/vm/compiler/script_builder.go b/pkg/vm/compiler/script_builder.go new file mode 100644 index 000000000..317e2582a --- /dev/null +++ b/pkg/vm/compiler/script_builder.go @@ -0,0 +1,152 @@ +package compiler + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math/big" + "os" + "text/tabwriter" + + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" +) + +// ScriptBuilder generates bytecode and will write all +// generated bytecode into its internal buffer. +type ScriptBuilder struct { + buf *bytes.Buffer +} + +func (sb *ScriptBuilder) emit(op vm.OpCode, b []byte) error { + if err := sb.buf.WriteByte(byte(op)); err != nil { + return err + } + _, err := sb.buf.Write(b) + return err +} + +func (sb *ScriptBuilder) emitPush(op vm.OpCode) error { + return sb.buf.WriteByte(byte(op)) +} + +func (sb *ScriptBuilder) emitPushBool(b bool) error { + if b { + return sb.emitPush(vm.OpPushT) + } + return sb.emitPush(vm.OpPushF) +} + +func (sb *ScriptBuilder) emitPushInt(i int64) error { + if i == -1 { + return sb.emitPush(vm.OpPushM1) + } + if i == 0 { + return sb.emitPush(vm.OpPushF) + } + if i > 0 && i < 16 { + val := vm.OpCode((int(vm.OpPush1) - 1 + int(i))) + return sb.emitPush(val) + } + + bInt := big.NewInt(i) + val := util.ToArrayReverse(bInt.Bytes()) + return sb.emitPushArray(val) +} + +func (sb *ScriptBuilder) emitPushArray(b []byte) error { + var ( + err error + n = len(b) + ) + + if n == 0 { + return errors.New("0 bytes given in pushArray") + } + if n <= int(vm.OpPushBytes75) { + return sb.emit(vm.OpCode(n), b) + } else if n < 0x100 { + err = sb.emit(vm.OpPushData1, []byte{byte(n)}) + } else if n < 0x10000 { + buf := make([]byte, 2) + binary.LittleEndian.PutUint16(buf, uint16(n)) + err = sb.emit(vm.OpPushData2, buf) + } else { + buf := make([]byte, 4) + binary.LittleEndian.PutUint32(buf, uint32(n)) + err = sb.emit(vm.OpPushData4, buf) + } + if err != nil { + return err + } + _, err = sb.buf.Write(b) + return err +} + +func (sb *ScriptBuilder) emitPushString(str string) error { + return sb.emitPushArray([]byte(str)) +} + +func (sb *ScriptBuilder) emitSysCall(api string) error { + lenAPI := len(api) + if lenAPI == 0 { + return errors.New("syscall argument cant be 0") + } + if lenAPI > 252 { + return fmt.Errorf("invalid syscall argument: %s", api) + } + + bapi := []byte(api) + args := make([]byte, lenAPI+1) + args[0] = byte(lenAPI) + copy(args, bapi[1:]) + return sb.emit(vm.OpSysCall, args) +} + +func (sb *ScriptBuilder) emitPushCall(offset int16) error { + buf := new(bytes.Buffer) + binary.Write(buf, binary.LittleEndian, offset) + return sb.emit(vm.OpCall, buf.Bytes()) +} + +func (sb *ScriptBuilder) emitJump(op vm.OpCode, offset int16) error { + if op != vm.OpJMP && op != vm.OpJMPIF && op != vm.OpJMPIFNOT && op != vm.OpCall { + return fmt.Errorf("invalid jump opcode: %v", op) + } + buf := make([]byte, 2) + binary.LittleEndian.PutUint16(buf, uint16(offset)) + return sb.emit(op, buf) +} + +func (sb *ScriptBuilder) updateJmpLabel(label int16, offset int) error { + sizeOfInt16 := 2 + if sizeOfInt16+offset >= sb.buf.Len() { + return fmt.Errorf("cannot update label at offset %d", offset) + } + + b := make([]byte, sizeOfInt16) + binary.LittleEndian.PutUint16(b, uint16(label)) + buf := sb.buf.Bytes() + copy(buf[offset:offset+sizeOfInt16], b) + return nil +} + +func (sb *ScriptBuilder) updatePushCall(offset int, label int16) { + b := new(bytes.Buffer) + binary.Write(b, binary.LittleEndian, label) + + buf := sb.buf.Bytes() + copy(buf[offset:offset+2], b.Bytes()) +} + +func (sb *ScriptBuilder) dumpOpcode() { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) + buf := sb.buf.Bytes() + + fmt.Fprintln(w, "INDEX\tOPCODE\tDESC") + for i := 0; i < len(buf); i++ { + fmt.Fprintf(w, "%d\t0x%2x\t%s\n", i, buf[i], vm.OpCode(buf[i])) + } + w.Flush() +} diff --git a/pkg/vm/compiler/script_builder_test.go b/pkg/vm/compiler/script_builder_test.go new file mode 100644 index 000000000..9e22cbd04 --- /dev/null +++ b/pkg/vm/compiler/script_builder_test.go @@ -0,0 +1,104 @@ +package compiler + +import ( + "bytes" + "math/big" + "testing" + + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" +) + +func TestEmitPush(t *testing.T) { + sb := &ScriptBuilder{buf: new(bytes.Buffer)} + + if err := sb.emitPush(vm.OpPush1); err != nil { + t.Fatal(err) + } + if sb.buf.Len() != 1 { + t.Fatalf("expect buffer len of 1 got %d", sb.buf.Len()) + } +} +func TestEmitPushIntNeg(t *testing.T) { + sb := &ScriptBuilder{buf: new(bytes.Buffer)} + val := -1 + if err := sb.emitPushInt(int64(val)); err != nil { + t.Fatal(err) + } + if want, have := vm.OpPushM1, vm.OpCode(sb.buf.Bytes()[0]); want != have { + t.Fatalf("expected %v got %v", want, have) + } +} + +func TestEmitPushInt0(t *testing.T) { + sb := &ScriptBuilder{buf: new(bytes.Buffer)} + val := 0 + if err := sb.emitPushInt(int64(val)); err != nil { + t.Fatal(err) + } + if want, have := vm.OpPushF, vm.OpCode(sb.buf.Bytes()[0]); want != have { + t.Fatalf("expected %v got %v", want, have) + } +} + +func TestEmitPushInt1(t *testing.T) { + sb := &ScriptBuilder{buf: new(bytes.Buffer)} + val := 1 + if err := sb.emitPushInt(int64(val)); err != nil { + t.Fatal(err) + } + if want, have := vm.OpPush1, vm.OpCode(sb.buf.Bytes()[0]); want != have { + t.Fatalf("expected %v got %v", want, have) + } +} + +func TestEmitPushInt100(t *testing.T) { + x := 100 + bigx := big.NewInt(int64(x)) + t.Log(bigx.Bytes()) + + sb := &ScriptBuilder{buf: new(bytes.Buffer)} + val := 100 + if err := sb.emitPushInt(int64(val)); err != nil { + t.Fatal(err) + } + // len = 1 + if want, have := byte(0x01), byte(sb.buf.Bytes()[0]); want != have { + t.Fatalf("expected %v got %v", want, have) + } + if want, have := byte(0x64), byte(sb.buf.Bytes()[1]); want != have { + t.Fatalf("expected %v got %v", want, have) + } +} + +func TestEmitPush1000(t *testing.T) { + sb := &ScriptBuilder{buf: new(bytes.Buffer)} + val := 1000 + if err := sb.emitPushInt(int64(val)); err != nil { + t.Fatal(err) + } + bInt := big.NewInt(int64(val)) + if want, have := byte(len(bInt.Bytes())), byte(sb.buf.Bytes()[0]); want != have { + t.Fatalf("expected %v got %v", want, have) + } + want := util.ToArrayReverse(bInt.Bytes()) // reverse + have := sb.buf.Bytes()[1:] + if bytes.Compare(want, have) != 0 { + t.Fatalf("expected %v got %v", want, have) + } +} + +func TestEmitPushString(t *testing.T) { + sb := &ScriptBuilder{buf: new(bytes.Buffer)} + str := "anthdm" + if err := sb.emitPushString(str); err != nil { + t.Fatal(err) + } + if want, have := byte(len(str)), sb.buf.Bytes()[0]; want != have { + t.Fatalf("expected %v got %v", want, have) + } + want, have := []byte(str), sb.buf.Bytes()[1:] + if bytes.Compare(want, have) != 0 { + t.Fatalf("expected %v got %v", want, have) + } +} diff --git a/pkg/vm/compiler/tests/binary_expr_test.go b/pkg/vm/compiler/tests/binary_expr_test.go new file mode 100644 index 000000000..9389aff7c --- /dev/null +++ b/pkg/vm/compiler/tests/binary_expr_test.go @@ -0,0 +1,72 @@ +package compiler_test + +var binaryExprTestCases = []testCase{ + { + "simple add", + ` + package testcase + func Main() int { + x := 2 + 2 + return x + } + `, + "52c56b546c766b00527ac46203006c766b00c3616c7566", + }, + { + "simple sub", + ` + package testcase + func Main() int { + x := 2 - 2 + return x + } + `, + "52c56b006c766b00527ac46203006c766b00c3616c7566", + }, + { + "simple div", + ` + package testcase + func Main() int { + x := 2 / 2 + return x + } + `, + "52c56b516c766b00527ac46203006c766b00c3616c7566", + }, + { + "simple mul", + ` + package testcase + func Main() int { + x := 4 * 2 + return x + } + `, + "52c56b586c766b00527ac46203006c766b00c3616c7566", + }, + { + "simple binary expr in return", + ` + package testcase + func Main() int { + x := 2 + return 2 + x + } + `, + "52c56b526c766b00527ac4620300526c766b00c393616c7566", + }, + { + "complex binary expr", + ` + package testcase + func Main() int { + x := 4 + y := 8 + z := x + 2 + 2 - 8 + return y * z + } + `, + "54c56b546c766b00527ac4586c766b51527ac46c766b00c35293529358946c766b52527ac46203006c766b51c36c766b52c395616c7566", + }, +} diff --git a/pkg/vm/compiler/tests/compiler_test.go b/pkg/vm/compiler/tests/compiler_test.go new file mode 100644 index 000000000..4f600b5ad --- /dev/null +++ b/pkg/vm/compiler/tests/compiler_test.go @@ -0,0 +1,65 @@ +package compiler_test + +import ( + "bytes" + "encoding/hex" + "fmt" + "os" + "strings" + "testing" + "text/tabwriter" + + "github.com/CityOfZion/neo-go/pkg/vm" + "github.com/CityOfZion/neo-go/pkg/vm/compiler" +) + +type testCase struct { + name string + src string + result string +} + +func TestAllCases(t *testing.T) { + testCases := []testCase{} + testCases = append(testCases, stringTestCases...) + testCases = append(testCases, binaryExprTestCases...) + testCases = append(testCases, ifStatementTestCases...) + testCases = append(testCases, functionCallTestCases...) + + for _, tc := range testCases { + c := compiler.New() + if err := c.Compile(strings.NewReader(tc.src)); err != nil { + t.Fatal(err) + } + + expectedResult, err := hex.DecodeString(tc.result) + if err != nil { + t.Fatal(err) + } + + if bytes.Compare(c.Buffer().Bytes(), expectedResult) != 0 { + t.Log(hex.EncodeToString(c.Buffer().Bytes())) + want, _ := hex.DecodeString(tc.result) + dumpOpCodeSideBySide(c.Buffer().Bytes(), want) + t.Fatalf("compiling %s failed", tc.name) + } + } +} + +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") + + for i := 0; i < len(have); i++ { + if len(want) <= i { + break + } + diff := "" + if have[i] != want[i] { + 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) + } + w.Flush() +} diff --git a/pkg/vm/compiler/tests/function_call_test.go b/pkg/vm/compiler/tests/function_call_test.go new file mode 100644 index 000000000..4b41621be --- /dev/null +++ b/pkg/vm/compiler/tests/function_call_test.go @@ -0,0 +1,61 @@ +package compiler_test + +var functionCallTestCases = []testCase{ + { + "simple function call", + ` + package testcase + func Main() int { + x := 10 + y := getSomeInteger() + return x + y + } + + func getSomeInteger() int { + x := 10 + return x + } + `, + "53c56b5a6c766b00527ac461651c006c766b51527ac46203006c766b00c36c766b51c393616c756652c56b5a6c766b00527ac46203006c766b00c3616c7566", + }, + { + "multiple function calls", + ` + package testcase + func Main() int { + x := 10 + y := getSomeInteger() + return x + y + } + + func getSomeInteger() int { + x := 10 + y := getSomeOtherInt() + return x + y + } + + func getSomeOtherInt() int { + x := 8 + return x + } + `, + "53c56b5a6c766b00527ac461651c006c766b51527ac46203006c766b00c36c766b51c393616c756653c56b5a6c766b00527ac461651c006c766b51527ac46203006c766b00c36c766b51c393616c756652c56b586c766b00527ac46203006c766b00c3616c7566", + }, + { + "function call with arguments", + ` + package testcase + func Main() int { + x := 10 + y := getSomeInteger(x) + return y + } + + func getSomeInteger(x int) int { + y := 8 + return x + y + } + `, + "53c56b5a6c766b00527ac46c766b00c3616516006c766b51527ac46203006c766b51c3616c756653c56b6c766b00527ac4586c766b51527ac46203006c766b00c36c766b51c393616c7566", + }, +} diff --git a/pkg/vm/compiler/tests/if_statement_test.go b/pkg/vm/compiler/tests/if_statement_test.go new file mode 100644 index 000000000..71a2e4460 --- /dev/null +++ b/pkg/vm/compiler/tests/if_statement_test.go @@ -0,0 +1,74 @@ +package compiler_test + +var ifStatementTestCases = []testCase{ + { + "if statement LT", + ` + package testcase + func Main() int { + x := 10 + if x < 100 { + return 1 + } + return 0 + } + `, + "54c56b5a6c766b00527ac46c766b00c301649f640b0062030051616c756662030000616c7566", + }, + { + "if statement GT", + ` + package testcase + func Main() int { + x := 10 + if x > 100 { + return 1 + } + return 0 + } + `, + "54c56b5a6c766b00527ac46c766b00c30164a0640b0062030051616c756662030000616c7566", + }, + { + "if statement GTE", + ` + package testcase + func Main() int { + x := 10 + if x >= 100 { + return 1 + } + return 0 + } + `, + "54c56b5a6c766b00527ac46c766b00c30164a2640b0062030051616c756662030000616c7566", + }, + { + "complex if statement with LAND", + ` + package testcase + func Main() int { + x := 10 + if x >= 10 && x <= 20 { + return 1 + } + return 0 + } + `, + "54c56b5a6c766b00527ac46c766b00c35aa26416006c766b00c30114a1640b0062030051616c756662030000616c7566", + }, + { + "complex if statement with LOR", + ` + package testcase + func Main() int { + x := 10 + if x >= 10 || x <= 20 { + return 1 + } + return 0 + } + `, + "54c56b5a6c766b00527ac46c766b00c35aa2630e006c766b00c30114a1640b0062030051616c756662030000616c7566", + }, +} diff --git a/pkg/vm/compiler/tests/string_test.go b/pkg/vm/compiler/tests/string_test.go new file mode 100644 index 000000000..93453cca0 --- /dev/null +++ b/pkg/vm/compiler/tests/string_test.go @@ -0,0 +1,15 @@ +package compiler_test + +var stringTestCases = []testCase{ + { + "simple string", + ` + package testcase + func Main() string { + x := "NEO" + return x + } + `, + "52c56b034e454f6c766b00527ac46203006c766b00c3616c7566", + }, +} diff --git a/pkg/vm/compiler_test.go b/pkg/vm/compiler_test.go deleted file mode 100644 index 52017aaf2..000000000 --- a/pkg/vm/compiler_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package vm - -import ( - "strings" - "testing" -) - -// opener -// -// 0x53 push len arguments -// 0xc5 open new array -// 0x6B to alt stack -// -// 0x5A the number 10 is pushed on the stack -// 0x6C put the input onto the main stack remove from alt -// 0x76 dup the item on top of the stack -// 0x6B put the item on the alt stack -// 0x00 put empty array on the stack -// 0x52 put the number 2 on the stack -// 0x7A put the item n back on top of the stack -// 0xC4 set item -// 0x59 put the number 9 on the stack -// 0x6C put the input onto the main stack remove from alt stack -// 0x76 dup the item on top of the stackj -// 0x6B put the item on the alt stack -// 0x51 push the number 1 on the stack -// 0x52 push the number 2 on the stack -// 0x7A put the item n back on top of the stack -// 0xC4 set the item -// 0x62 JMP -// 0x03 the next 3 bytes is dat pushed on the stack -// 0x6C put the input ont the main stack remove from alt stack -// 0x00 put empty array onto the stack -// 0x02 the next 2 bytes is data pushed on the stack -// 0xE8 1000 uint16 -// 0x03 1000 uint16 -// 0x6C put the input onto the main stack remove from alt -// 0x76 dup the item on top of the stack -// 0x6B put the item on the alt stack -// 0x52 push the number 2 on the stack -// 0x52 push the number 2 on the stack -// 0x7A put the item n back on top of the stack -// 0xC4 set the item -// 0x00 empty array is pushed on the stack -// 0x61 nop -// 0x6C put the input onto the main stack remove from alt -// 0x75 removes the top stack item -// 0x66 return - -// - -func TestSimpleAssign(t *testing.T) { - src := ` - package NEP5 - - func Main() { - x := 10 - y := 8 - } - ` - - c := NewCompiler() - if err := c.Compile(strings.NewReader(src)); err != nil { - t.Fatal(err) - } - - // c.DumpOpcode() -} - -func TestAssignLoadLocal(t *testing.T) { - src := ` - package NEP5 - - func Main() { - x := 1 - y := x - } - ` - - c := NewCompiler() - if err := c.Compile(strings.NewReader(src)); err != nil { - t.Fatal(err) - } - - // c.DumpOpcode() -} diff --git a/pkg/vm/script_builder.go b/pkg/vm/script_builder.go deleted file mode 100644 index 19644f6f6..000000000 --- a/pkg/vm/script_builder.go +++ /dev/null @@ -1,127 +0,0 @@ -package vm - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "math/big" - - "github.com/CityOfZion/neo-go/pkg/util" -) - -// ScriptBuilder generates bytecode and will write all -// generated bytecode into its internal buffer. -type ScriptBuilder struct { - buf *bytes.Buffer -} - -func (sb *ScriptBuilder) emit(op OpCode, b []byte) error { - if err := sb.buf.WriteByte(byte(op)); err != nil { - return err - } - _, err := sb.buf.Write(b) - return err -} - -func (sb *ScriptBuilder) emitPush(op OpCode) error { - return sb.buf.WriteByte(byte(op)) -} - -func (sb *ScriptBuilder) emitPushBool(b bool) error { - if b { - return sb.emitPush(OpPushT) - } - return sb.emitPush(OpPushF) -} - -func (sb *ScriptBuilder) emitPushInt(i int64) error { - if i == -1 { - return sb.emitPush(OpPushM1) - } - if i == 0 { - return sb.emitPush(OpPushF) - } - if i > 0 && i < 16 { - val := OpCode((int(OpPush1) - 1 + int(i))) - return sb.emitPush(val) - } - - bInt := big.NewInt(i) - val := util.ToArrayReverse(bInt.Bytes()) - return sb.emitPushArray(val) -} - -func (sb *ScriptBuilder) emitPushArray(b []byte) error { - var ( - err error - n = len(b) - ) - - if n == 0 { - return errors.New("0 bytes given in pushArray") - } - if n <= int(OpPushBytes75) { - return sb.emit(OpCode(n), b) - } else if n < 0x100 { - err = sb.emit(OpPushData1, []byte{byte(n)}) - } else if n < 0x10000 { - buf := make([]byte, 2) - binary.LittleEndian.PutUint16(buf, uint16(n)) - err = sb.emit(OpPushData2, buf) - } else { - buf := make([]byte, 4) - binary.LittleEndian.PutUint32(buf, uint32(n)) - err = sb.emit(OpPushData4, buf) - } - if err != nil { - return err - } - _, err = sb.buf.Write(b) - return err -} - -func (sb *ScriptBuilder) emitPushString(str string) error { - return sb.emitPushArray([]byte(str)) -} - -func (sb *ScriptBuilder) emitSysCall(api string) error { - lenAPI := len(api) - if lenAPI == 0 { - return errors.New("syscall argument cant be 0") - } - if lenAPI > 252 { - return fmt.Errorf("invalid syscall argument: %s", api) - } - - bapi := []byte(api) - args := make([]byte, lenAPI+1) - args[0] = byte(lenAPI) - copy(args, bapi[1:]) - return sb.emit(OpSysCall, args) -} - -func (sb *ScriptBuilder) emitPushCall(scriptHash []byte, tailCall bool) error { - if len(scriptHash) != 20 { - return errors.New("expected a 20 byte long scriptHash (uint160) for pushCall") - } - op := OpAppCall - if tailCall { - op = OpTailCall - } - return sb.emit(op, scriptHash) -} - -func (sb *ScriptBuilder) emitJump(op OpCode, offset int16) error { - if op != OpJMP && op != OpJMPIF && op != OpJMPIFNOT && op != OpCall { - return fmt.Errorf("invalid jump opcode: %v", op) - } - return sb.emit(op, []byte{}) // convert to bits? -} - -func (sb *ScriptBuilder) dumpOpcode() { - buf := sb.buf.Bytes() - for i := 0; i < len(buf); i++ { - fmt.Printf("OPCODE AT INDEX \t %d \t 0x%2x \t %s \n", i, buf[i], OpCode(buf[i])) - } -} diff --git a/pkg/vm/script_builder_test.go b/pkg/vm/script_builder_test.go deleted file mode 100644 index 5e29aff05..000000000 --- a/pkg/vm/script_builder_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package vm - -import ( - "bytes" - "math/big" - "testing" - - "github.com/CityOfZion/neo-go/pkg/util" -) - -func TestEmitPush(t *testing.T) { - sb := &ScriptBuilder{new(bytes.Buffer)} - - if err := sb.emitPush(OpPush1); err != nil { - t.Fatal(err) - } - if sb.buf.Len() != 1 { - t.Fatalf("expect buffer len of 1 got %d", sb.buf.Len()) - } -} - -func TestEmitPushInt(t *testing.T) { - sb := &ScriptBuilder{new(bytes.Buffer)} - - val := -1 - if err := sb.emitPushInt(int64(val)); err != nil { - t.Fatal(err) - } - if want, have := OpPushM1, OpCode(sb.buf.Bytes()[0]); want != have { - t.Fatalf("expected %v got %v", want, have) - } - val = 0 - if err := sb.emitPushInt(int64(val)); err != nil { - t.Fatal(err) - } - if want, have := OpPushF, OpCode(sb.buf.Bytes()[1]); want != have { - t.Fatalf("expected %v got %v", want, have) - } - val = 1 - if err := sb.emitPushInt(int64(val)); err != nil { - t.Fatal(err) - } - if want, have := OpPush1, OpCode(sb.buf.Bytes()[2]); want != have { - t.Fatalf("expected %v got %v", want, have) - } - val = 1000 - if err := sb.emitPushInt(int64(val)); err != nil { - t.Fatal(err) - } - bInt := big.NewInt(int64(val)) - if want, have := byte(len(bInt.Bytes())), byte(sb.buf.Bytes()[3]); want != have { - t.Fatalf("expected %v got %v", want, have) - } - want := util.ToArrayReverse(bInt.Bytes()) // reverse - have := sb.buf.Bytes()[4:] - if bytes.Compare(want, have) != 0 { - t.Fatalf("expected %v got %v", want, have) - } -} - -func TestEmitPushString(t *testing.T) { - sb := &ScriptBuilder{new(bytes.Buffer)} - str := "anthdm" - if err := sb.emitPushString(str); err != nil { - t.Fatal(err) - } - if want, have := byte(len(str)), sb.buf.Bytes()[0]; want != have { - t.Fatalf("expected %v got %v", want, have) - } - want, have := []byte(str), sb.buf.Bytes()[1:] - if bytes.Compare(want, have) != 0 { - t.Fatalf("expected %v got %v", want, have) - } -}