From 578bbabd1d8f417dac1f36a61a96de199f14b81e Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Tue, 2 Mar 2021 14:15:25 +0300 Subject: [PATCH 1/8] compiler: allow to emit opcodes directly --- pkg/compiler/analysis.go | 3 ++- pkg/compiler/codegen.go | 25 +++++++++++++++++-------- pkg/compiler/syscall_test.go | 26 ++++++++++++++++++++++++++ pkg/interop/neogointernal/opcode.go | 11 +++++++++++ 4 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 pkg/interop/neogointernal/opcode.go diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index 4efbcc44c..c90a49c0f 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -288,7 +288,8 @@ func isSyscall(fun *funcScope) bool { if fun.selector == nil || fun.pkg == nil || !isInteropPath(fun.pkg.Path()) { return false } - return fun.pkg.Name() == "neogointernal" && strings.HasPrefix(fun.name, "Syscall") + return fun.pkg.Name() == "neogointernal" && (strings.HasPrefix(fun.name, "Syscall") || + strings.HasPrefix(fun.name, "Opcode")) } const interopPrefix = "github.com/nspcc-dev/neo-go/pkg/interop" diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 062dc185f..1baf52704 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -939,7 +939,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { c.emittedEvents[name] = append(c.emittedEvents[name], params) } } - c.convertSyscall(n) + c.convertSyscall(f, n) default: emit.Call(c.prog.BinWriter, opcode.CALLL, f.label) } @@ -1530,18 +1530,27 @@ func (c *codegen) getByteArray(expr ast.Expr) []byte { } } -func (c *codegen) convertSyscall(expr *ast.CallExpr) { +func (c *codegen) convertSyscall(f *funcScope, expr *ast.CallExpr) { for _, arg := range expr.Args[1:] { ast.Walk(c, arg) } - c.emitReverse(len(expr.Args) - 1) tv := c.typeAndValueOf(expr.Args[0]) - syscall := constant.StringVal(tv.Value) - emit.Syscall(c.prog.BinWriter, syscall) + name := constant.StringVal(tv.Value) + if strings.HasPrefix(f.name, "Syscall") { + c.emitReverse(len(expr.Args) - 1) + emit.Syscall(c.prog.BinWriter, name) - // This NOP instruction is basically not needed, but if we do, we have a - // one to one matching avm file with neo-python which is very nice for debugging. - emit.Opcodes(c.prog.BinWriter, opcode.NOP) + // This NOP instruction is basically not needed, but if we do, we have a + // one to one matching avm file with neo-python which is very nice for debugging. + emit.Opcodes(c.prog.BinWriter, opcode.NOP) + } else { + op, err := opcode.FromString(name) + if err != nil { + c.prog.Err = fmt.Errorf("invalid opcode: %s", op) + return + } + emit.Opcodes(c.prog.BinWriter, op) + } } // emitSliceHelper emits 3 items on stack: slice, its first index, and its size. diff --git a/pkg/compiler/syscall_test.go b/pkg/compiler/syscall_test.go index f5dbd8e35..b6d735f0a 100644 --- a/pkg/compiler/syscall_test.go +++ b/pkg/compiler/syscall_test.go @@ -92,3 +92,29 @@ func TestSyscallInGlobalInit(t *testing.T) { require.NoError(t, v.Run()) require.Equal(t, []byte{1, 2}, v.Estack().Pop().Value()) } + +func TestOpcode(t *testing.T) { + t.Run("1 argument", func(t *testing.T) { + src := `package foo + import "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal" + func abs(a int) int { + return neogointernal.Opcode1("ABS", a).(int) + } + func Main() int { + return abs(-42) + }` + eval(t, src, big.NewInt(42)) + }) + t.Run("2 arguments", func(t *testing.T) { + src := `package foo + import "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal" + func add3(a, b, c int) int { + return neogointernal.Opcode2("SUB", a, + neogointernal.Opcode2("SUB", b, c).(int)).(int) + } + func Main() int { + return add3(53, 12, 1) + }` + eval(t, src, big.NewInt(42)) + }) +} diff --git a/pkg/interop/neogointernal/opcode.go b/pkg/interop/neogointernal/opcode.go new file mode 100644 index 000000000..3a7cb00a4 --- /dev/null +++ b/pkg/interop/neogointernal/opcode.go @@ -0,0 +1,11 @@ +package neogointernal + +// Opcode1 emits opcode with 1 argument. +func Opcode1(op string, arg interface{}) interface{} { + return nil +} + +// Opcode2 emits opcode with 2 arguments. +func Opcode2(op string, arg1, arg2 interface{}) interface{} { + return nil +} From 5f4385d3fadcc631a1416dcaed77997aecc15900 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Tue, 2 Mar 2021 14:19:13 +0300 Subject: [PATCH 2/8] compiler: implement syscalls for POW and SQRT opcodes --- pkg/compiler/syscall_test.go | 16 ++++++++++++++++ pkg/interop/math/math.go | 14 ++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 pkg/interop/math/math.go diff --git a/pkg/compiler/syscall_test.go b/pkg/compiler/syscall_test.go index b6d735f0a..5ce03d9e2 100644 --- a/pkg/compiler/syscall_test.go +++ b/pkg/compiler/syscall_test.go @@ -117,4 +117,20 @@ func TestOpcode(t *testing.T) { }` eval(t, src, big.NewInt(42)) }) + t.Run("POW", func(t *testing.T) { + src := `package foo + import "github.com/nspcc-dev/neo-go/pkg/interop/math" + func Main() int { + return math.Pow(2, math.Pow(3, 2)) + }` + eval(t, src, big.NewInt(512)) + }) + t.Run("SRQT", func(t *testing.T) { + src := `package foo + import "github.com/nspcc-dev/neo-go/pkg/interop/math" + func Main() int { + return math.Sqrt(math.Sqrt(101)) // == sqrt(10) == 3 + }` + eval(t, src, big.NewInt(3)) + }) } diff --git a/pkg/interop/math/math.go b/pkg/interop/math/math.go new file mode 100644 index 000000000..86cbd020d --- /dev/null +++ b/pkg/interop/math/math.go @@ -0,0 +1,14 @@ +package math + +import "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal" + +// Pow returns a^b using POW VM opcode. +// b must be >= 0 and <= 2^31-1. +func Pow(a, b int) int { + return neogointernal.Opcode2("POW", a, b).(int) +} + +// Sqrt returns positive square root of x rounded down. +func Sqrt(x int) int { + return neogointernal.Opcode1("SQRT", x).(int) +} From e754ca62dbe1826609a5a2623ec7d7df9dccac9b Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Tue, 2 Mar 2021 14:34:23 +0300 Subject: [PATCH 3/8] compiler: do not emit CONVERT for syscall results When we encounter type assertion CONVERT is emitted. This isn't needed for SYSCALL (or opcode) results because value already has needed type. Problems can arise when result is converted to invalid type but `neogointernal` package shouldn't be used directly anyway. --- pkg/compiler/codegen.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 1baf52704..730256e9d 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -1226,6 +1226,10 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { // not the assertion type. case *ast.TypeAssertExpr: ast.Walk(c, n.X) + if c.isCallExprSyscall(n.X) { + return nil + } + goTyp := c.typeOf(n.Type) if canConvert(goTyp.String()) { typ := toNeoType(goTyp) @@ -1246,6 +1250,20 @@ func (c *codegen) packVarArgs(n *ast.CallExpr, typ *types.Signature) int { return varSize } +func (c *codegen) isCallExprSyscall(e ast.Expr) bool { + ce, ok := e.(*ast.CallExpr) + if !ok { + return false + } + sel, ok := ce.Fun.(*ast.SelectorExpr) + if !ok { + return false + } + name, _ := c.getFuncNameFromSelector(sel) + f, ok := c.funcs[name] + return ok && isSyscall(f) +} + // processDefers emits code for `defer` statements. // TRY-related opcodes handle exception as follows: // 1. CATCH block is executed only if exception has occurred. From 56fe6574c94465f40c8ebbae8c50031cfc017f4f Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Tue, 2 Mar 2021 14:43:58 +0300 Subject: [PATCH 4/8] compiler: simplify `convert.To*` processing With inlining there is no need for special logic in compiler. --- pkg/compiler/analysis.go | 3 +-- pkg/compiler/codegen.go | 9 --------- pkg/interop/convert/convert.go | 6 +++--- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index c90a49c0f..2aacbbfff 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -327,6 +327,5 @@ func canInline(s string) bool { return false } return !strings.HasPrefix(s[len(interopPrefix):], "/neogointernal") && - !strings.HasPrefix(s[len(interopPrefix):], "/util") && - !strings.HasPrefix(s[len(interopPrefix):], "/convert") + !strings.HasPrefix(s[len(interopPrefix):], "/util") } diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 730256e9d..d5eea6b74 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -1708,15 +1708,6 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) { c.emitStoreByIndex(varGlobal, c.exceptionIndex) case "delete": emit.Opcodes(c.prog.BinWriter, opcode.REMOVE) - case "ToInteger", "ToByteArray", "ToBool": - typ := stackitem.IntegerT - switch name { - case "ToByteArray": - typ = stackitem.ByteArrayT - case "ToBool": - typ = stackitem.BooleanT - } - c.emitConvert(typ) case "Remove": if !isCompoundSlice(c.typeOf(expr.Args[0])) { c.prog.Err = errors.New("`Remove` supports only non-byte slices") diff --git a/pkg/interop/convert/convert.go b/pkg/interop/convert/convert.go index 9cba7ae0f..060f14fbb 100644 --- a/pkg/interop/convert/convert.go +++ b/pkg/interop/convert/convert.go @@ -3,15 +3,15 @@ package convert // ToInteger converts it's argument to an Integer. func ToInteger(v interface{}) int64 { - return 0 + return v.(int64) } // ToByteArray converts it's argument to a ByteArray. func ToByteArray(v interface{}) []byte { - return nil + return v.([]byte) } // ToBool converts it's argument to a Boolean. func ToBool(v interface{}) bool { - return false + return v.(bool) } From 0b54870bfe4c50bd04f987483c7c74fd0aa2e241 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Tue, 2 Mar 2021 14:56:32 +0300 Subject: [PATCH 5/8] compiler: add missing math routines Add interops for ABS, SIGN, MIN, MAX, WITHIN opcodes --- pkg/compiler/syscall_test.go | 60 +++++++++++++++++++++++++++++ pkg/interop/math/math.go | 29 ++++++++++++++ pkg/interop/neogointernal/opcode.go | 5 +++ 3 files changed, 94 insertions(+) diff --git a/pkg/compiler/syscall_test.go b/pkg/compiler/syscall_test.go index 5ce03d9e2..9630c1f8c 100644 --- a/pkg/compiler/syscall_test.go +++ b/pkg/compiler/syscall_test.go @@ -133,4 +133,64 @@ func TestOpcode(t *testing.T) { }` eval(t, src, big.NewInt(3)) }) + t.Run("SIGN", func(t *testing.T) { + src := `package foo + import "github.com/nspcc-dev/neo-go/pkg/interop/math" + func Main() []int { + signs := make([]int, 3) + signs[0] = math.Sign(-123) + signs[1] = math.Sign(0) + signs[2] = math.Sign(42) + return signs + }` + eval(t, src, []stackitem.Item{ + stackitem.Make(-1), + stackitem.Make(0), + stackitem.Make(1), + }) + }) + t.Run("ABS", func(t *testing.T) { + src := `package foo + import "github.com/nspcc-dev/neo-go/pkg/interop/math" + func Main() int { + return math.Abs(-3) + }` + eval(t, src, big.NewInt(3)) + }) + t.Run("MAX", func(t *testing.T) { + src := `package foo + import "github.com/nspcc-dev/neo-go/pkg/interop/math" + func Main() int { + return math.Max(1, 2) + math.Max(8, 3) + }` + eval(t, src, big.NewInt(10)) + }) + t.Run("MIN", func(t *testing.T) { + src := `package foo + import "github.com/nspcc-dev/neo-go/pkg/interop/math" + func Main() int { + return math.Min(1, 2) + math.Min(8, 3) + }` + eval(t, src, big.NewInt(4)) + }) + t.Run("WITHIN", func(t *testing.T) { + src := `package foo + import "github.com/nspcc-dev/neo-go/pkg/interop/math" + func Main() []bool { + r := make([]bool, 5) + r[0] = math.Within(2, 3, 5) + r[1] = math.Within(3, 3, 5) + r[2] = math.Within(4, 3, 5) + r[3] = math.Within(5, 3, 5) + r[4] = math.Within(6, 3, 5) + return r + }` + eval(t, src, []stackitem.Item{ + stackitem.Make(false), + stackitem.Make(true), + stackitem.Make(true), + stackitem.Make(false), + stackitem.Make(false), + }) + }) } diff --git a/pkg/interop/math/math.go b/pkg/interop/math/math.go index 86cbd020d..6097bae1c 100644 --- a/pkg/interop/math/math.go +++ b/pkg/interop/math/math.go @@ -12,3 +12,32 @@ func Pow(a, b int) int { func Sqrt(x int) int { return neogointernal.Opcode1("SQRT", x).(int) } + +// Sign returns: +// +// -1 if x < 0 +// 0 if x == 0 +// +1 if x > 0 +func Sign(a int) int { + return neogointernal.Opcode1("SIGN", a).(int) +} + +// Abs returns absolute value of a. +func Abs(a int) int { + return neogointernal.Opcode1("ABS", a).(int) +} + +// Max returns maximum of a, b. +func Max(a, b int) int { + return neogointernal.Opcode2("MAX", a, b).(int) +} + +// Min returns minimum of a, b. +func Min(a, b int) int { + return neogointernal.Opcode2("MIN", a, b).(int) +} + +// Within returns true if a <= x < b. +func Within(x, a, b int) bool { + return neogointernal.Opcode3("WITHIN", x, a, b).(bool) +} diff --git a/pkg/interop/neogointernal/opcode.go b/pkg/interop/neogointernal/opcode.go index 3a7cb00a4..2e18b936b 100644 --- a/pkg/interop/neogointernal/opcode.go +++ b/pkg/interop/neogointernal/opcode.go @@ -9,3 +9,8 @@ func Opcode1(op string, arg interface{}) interface{} { func Opcode2(op string, arg1, arg2 interface{}) interface{} { return nil } + +// Opcode3 emits opcode with 3 arguments. +func Opcode3(op string, arg1, arg2, arg3 interface{}) interface{} { + return nil +} From 1e2944f492b0c7b18e8ed0b0d1e76a21ff241652 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Thu, 4 Mar 2021 13:09:03 +0300 Subject: [PATCH 6/8] compiler/interop: return proper type from `contract.GetCallFlags` --- pkg/interop/contract/contract.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/interop/contract/contract.go b/pkg/interop/contract/contract.go index 5c5e92887..e37064ce1 100644 --- a/pkg/interop/contract/contract.go +++ b/pkg/interop/contract/contract.go @@ -45,8 +45,8 @@ func CreateStandardAccount(pub interop.PublicKey) []byte { // GetCallFlags returns calling flags which execution context was created with. // This function uses `System.Contract.GetCallFlags` syscall. -func GetCallFlags() int64 { - return neogointernal.Syscall0("System.Contract.GetFlags").(int64) +func GetCallFlags() CallFlag { + return neogointernal.Syscall0("System.Contract.GetFlags").(CallFlag) } // Call executes previously deployed blockchain contract with specified hash From 1138143a504a6c3b3039c907001c5a649e46cbe9 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Thu, 4 Mar 2021 13:13:12 +0300 Subject: [PATCH 7/8] compiler/interop: add flag type for `PutEx` --- pkg/interop/storage/storage.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/interop/storage/storage.go b/pkg/interop/storage/storage.go index 5526aa96f..c9775ebfa 100644 --- a/pkg/interop/storage/storage.go +++ b/pkg/interop/storage/storage.go @@ -38,6 +38,16 @@ const ( PickField1 FindFlags = 1 << 5 ) +// PutFlags represents flag of `PutEx` syscall. +type PutFlags byte + +const ( + // PutDefault is a storage flag for non-constant items. + PutDefault PutFlags = 0 + // PutConstant is a storage flag for constant items. + PutConstant PutFlags = 0x01 +) + // ConvertContextToReadOnly returns new context from the given one, but with // writing capability turned off, so that you could only invoke Get and Find // using this new Context. If Context is already read-only this function is a @@ -74,7 +84,7 @@ func Put(ctx Context, key interface{}, value interface{}) { // can either be odd for constant storage items or even for variable storage items. // Refer to Put function description for details on how to pass the remaining // arguments. This function uses `System.Storage.PutEx` syscall. -func PutEx(ctx Context, key interface{}, value interface{}, flag int64) { +func PutEx(ctx Context, key interface{}, value interface{}, flag PutFlags) { neogointernal.Syscall4NoReturn("System.Storage.PutEx", ctx, key, value, flag) } From d66ce4323987bf863a74c66e57c5128a28f1ada4 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Thu, 4 Mar 2021 13:14:24 +0300 Subject: [PATCH 8/8] compiler/interop: replace `int64` with `int` --- pkg/compiler/constant_test.go | 2 +- pkg/compiler/convert_test.go | 14 +++++++------- pkg/interop/convert/convert.go | 4 ++-- pkg/interop/runtime/runtime.go | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/compiler/constant_test.go b/pkg/compiler/constant_test.go index 42456659a..9b5c7976c 100644 --- a/pkg/compiler/constant_test.go +++ b/pkg/compiler/constant_test.go @@ -40,7 +40,7 @@ func TestByteConstant(t *testing.T) { src := `package foo import "github.com/nspcc-dev/neo-go/pkg/interop/convert" const a byte = 0xFF - func Main() int64 { + func Main() int { x := convert.ToInteger(a) return x+1 }` diff --git a/pkg/compiler/convert_test.go b/pkg/compiler/convert_test.go index 44e4a696b..c50e11d75 100644 --- a/pkg/compiler/convert_test.go +++ b/pkg/compiler/convert_test.go @@ -18,7 +18,7 @@ func getFunctionName(typ string) string { return "Bool" case "[]byte": return "ByteArray" - case "int64": + case "int": return "Integer" } panic("invalid type") @@ -40,12 +40,12 @@ func TestConvert(t *testing.T) { {"bool", "[]byte{0, 1, 0}", true}, {"bool", "[]byte{0}", true}, {"bool", `""`, false}, - {"int64", "true", big.NewInt(1)}, - {"int64", "false", big.NewInt(0)}, - {"int64", "12", big.NewInt(12)}, - {"int64", "0", big.NewInt(0)}, - {"int64", "[]byte{0, 1, 0}", big.NewInt(256)}, - {"int64", "[]byte{0}", big.NewInt(0)}, + {"int", "true", big.NewInt(1)}, + {"int", "false", big.NewInt(0)}, + {"int", "12", big.NewInt(12)}, + {"int", "0", big.NewInt(0)}, + {"int", "[]byte{0, 1, 0}", big.NewInt(256)}, + {"int", "[]byte{0}", big.NewInt(0)}, {"[]byte", "true", []byte{1}}, {"[]byte", "false", []byte{0}}, {"[]byte", "12", []byte{0x0C}}, diff --git a/pkg/interop/convert/convert.go b/pkg/interop/convert/convert.go index 060f14fbb..e4c606a81 100644 --- a/pkg/interop/convert/convert.go +++ b/pkg/interop/convert/convert.go @@ -2,8 +2,8 @@ package convert // ToInteger converts it's argument to an Integer. -func ToInteger(v interface{}) int64 { - return v.(int64) +func ToInteger(v interface{}) int { + return v.(int) } // ToByteArray converts it's argument to a ByteArray. diff --git a/pkg/interop/runtime/runtime.go b/pkg/interop/runtime/runtime.go index a8595317c..8a8b8cb16 100644 --- a/pkg/interop/runtime/runtime.go +++ b/pkg/interop/runtime/runtime.go @@ -62,8 +62,8 @@ func GetTrigger() byte { // GasLeft returns the amount of gas available for the current execution. // This function uses `System.Runtime.GasLeft` syscall. -func GasLeft() int64 { - return neogointernal.Syscall0("System.Runtime.GasLeft").(int64) +func GasLeft() int { + return neogointernal.Syscall0("System.Runtime.GasLeft").(int) } // GetNotifications returns notifications emitted by contract h.