From 0f1111604077c28d7237f595fae2a2f24da5cd6f Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 24 Aug 2020 11:58:29 +0300 Subject: [PATCH 1/4] compiler: support `make()` --- docs/compiler.md | 4 ++-- pkg/compiler/analysis.go | 2 +- pkg/compiler/codegen.go | 21 +++++++++++++++++++ pkg/compiler/slice_test.go | 41 ++++++++++++++++++++++++++++++++++++++ pkg/compiler/types.go | 5 +++++ 5 files changed, 70 insertions(+), 3 deletions(-) diff --git a/docs/compiler.md b/docs/compiler.md index 8b2613478..7249a085b 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -7,8 +7,8 @@ The neo-go compiler compiles Go programs to bytecode that the NEO virtual machin The compiler is mostly compatible with regular Go language specification, but there are some important deviations that you need to be aware of that make it a dialect of Go rather than a complete port of the language: - * `make()` ane `new()` are not supported, most of the time you can substitute - them with composite literals + * `new()` is not supported, most of the time you can substitute structs with composite literals + * `make()` is supported for maps and slices with elements of basic types * pointers are supported only for struct literals, one can't take an address of an arbitrary variable * there is no real distinction between different integer types, all of them diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index 71870e98c..e83873d31 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -14,7 +14,7 @@ import ( var ( // Go language builtin functions. - goBuiltins = []string{"len", "append", "panic"} + goBuiltins = []string{"len", "append", "panic", "make"} // Custom builtin utility functions. customBuiltins = []string{ "FromAddress", "Equals", diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index a9cac98d8..42a9881c8 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -1305,6 +1305,24 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) { } switch name { + case "make": + typ := c.typeOf(expr.Args[0]) + switch { + case isMap(typ): + emit.Opcode(c.prog.BinWriter, opcode.NEWMAP) + default: + if len(expr.Args) == 3 { + c.prog.Err = fmt.Errorf("`make()` with a capacity argument is not supported") + return + } + ast.Walk(c, expr.Args[1]) + if isByteSlice(typ) { + emit.Opcode(c.prog.BinWriter, opcode.NEWBUFFER) + } else { + neoT := toNeoType(typ.(*types.Slice).Elem()) + emit.Instruction(c.prog.BinWriter, opcode.NEWARRAYT, []byte{byte(neoT)}) + } + } case "len": emit.Opcode(c.prog.BinWriter, opcode.DUP) emit.Opcode(c.prog.BinWriter, opcode.ISNULL) @@ -1397,6 +1415,9 @@ func transformArgs(fun ast.Expr, args []ast.Expr) []ast.Expr { if f.Name == "panic" { return args[1:] } + if f.Name == "make" { + return nil + } } return args diff --git a/pkg/compiler/slice_test.go b/pkg/compiler/slice_test.go index 20223fb04..ca1d62fe7 100644 --- a/pkg/compiler/slice_test.go +++ b/pkg/compiler/slice_test.go @@ -2,9 +2,12 @@ package compiler_test import ( "math/big" + "strings" "testing" + "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" ) var sliceTestCases = []testCase{ @@ -323,3 +326,41 @@ func TestJumps(t *testing.T) { ` eval(t, src, []byte{0x62, 0x01, 0x00}) } + +func TestMake(t *testing.T) { + t.Run("Map", func(t *testing.T) { + src := `package foo + func Main() int { + a := make(map[int]int) + a[1] = 10 + a[2] = 20 + return a[1] + }` + eval(t, src, big.NewInt(10)) + }) + t.Run("IntSlice", func(t *testing.T) { + src := `package foo + func Main() int { + a := make([]int, 10) + return len(a) + a[0] + }` + eval(t, src, big.NewInt(10)) + }) + t.Run("ByteSlice", func(t *testing.T) { + src := `package foo + func Main() int { + a := make([]byte, 10) + return len(a) + int(a[0]) + }` + eval(t, src, big.NewInt(10)) + }) + t.Run("CapacityError", func(t *testing.T) { + src := `package foo + func Main() int { + a := make([]int, 1, 2) + return a[0] + }` + _, err := compiler.Compile("foo.go", strings.NewReader(src)) + require.Error(t, err) + }) +} diff --git a/pkg/compiler/types.go b/pkg/compiler/types.go index 9b17de395..649c1f146 100644 --- a/pkg/compiler/types.go +++ b/pkg/compiler/types.go @@ -27,6 +27,11 @@ func isBasicTypeOfKind(typ types.Type, ks ...types.BasicKind) bool { return false } +func isMap(typ types.Type) bool { + _, ok := typ.Underlying().(*types.Map) + return ok +} + func isByte(typ types.Type) bool { return isBasicTypeOfKind(typ, types.Uint8, types.Int8) } From 931dc6c64f3c9bc9c961843662f85a94522d262e Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 24 Aug 2020 11:59:07 +0300 Subject: [PATCH 2/4] compiler: remove debug PrintOps() from tests --- pkg/compiler/init_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/compiler/init_test.go b/pkg/compiler/init_test.go index dd74639ad..696e119f2 100644 --- a/pkg/compiler/init_test.go +++ b/pkg/compiler/init_test.go @@ -70,8 +70,6 @@ func TestImportOrder(t *testing.T) { import _ "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/pkg2" import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/pkg3" func Main() int { return pkg3.A }` - v := vmAndCompile(t, src) - v.PrintOps() eval(t, src, big.NewInt(2)) }) t.Run("2,1", func(t *testing.T) { From ab4cd8a990767a20d11b78b3ebd7db1602f3aa90 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 24 Aug 2020 12:52:02 +0300 Subject: [PATCH 3/4] vm: fix typo in MEMCPY handling --- pkg/vm/vm.go | 2 +- pkg/vm/vm_test.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 6b838752f..ca68a2716 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -659,7 +659,7 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro panic("invalid destination index") } dst := v.estack.Pop().value.(*stackitem.Buffer).Value().([]byte) - if sum := si + n; sum < 0 || sum > len(dst) { + if sum := di + n; sum < 0 || sum > len(dst) { panic("size is too big") } copy(dst[di:], src[si:si+n]) diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index 24e0a3150..65c51e2d4 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -1344,6 +1344,10 @@ func TestMEMCPY(t *testing.T) { buf := stackitem.NewBuffer([]byte{0, 1, 2, 3}) runWithArgs(t, prog, stackitem.NewBuffer([]byte{0, 6, 7, 3}), buf, buf, 1, []byte{4, 5, 6, 7}, 2, 2) }) + t.Run("NonZeroDstIndex", func(t *testing.T) { + buf := stackitem.NewBuffer([]byte{0, 1, 2}) + runWithArgs(t, prog, stackitem.NewBuffer([]byte{0, 6, 7}), buf, buf, 1, []byte{4, 5, 6, 7}, 2, 2) + }) t.Run("NegativeSize", getTestFuncForVM(prog, nil, stackitem.NewBuffer([]byte{0, 1}), 0, []byte{2}, 0, -1)) t.Run("NegativeSrcIndex", getTestFuncForVM(prog, nil, stackitem.NewBuffer([]byte{0, 1}), 0, []byte{2}, -1, 1)) t.Run("NegativeDstIndex", getTestFuncForVM(prog, nil, stackitem.NewBuffer([]byte{0, 1}), -1, []byte{2}, 0, 1)) From 69989e1227ddd90f1922aec36392ff31d5b6f174 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 24 Aug 2020 12:54:01 +0300 Subject: [PATCH 4/4] compiler: support `copy()` --- docs/compiler.md | 1 + pkg/compiler/analysis.go | 2 +- pkg/compiler/codegen.go | 52 +++++++++++++++++++++++++++++-- pkg/compiler/slice_test.go | 64 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/docs/compiler.md b/docs/compiler.md index 7249a085b..56b95a90d 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -9,6 +9,7 @@ there are some important deviations that you need to be aware of that make it a dialect of Go rather than a complete port of the language: * `new()` is not supported, most of the time you can substitute structs with composite literals * `make()` is supported for maps and slices with elements of basic types + * `copy()` is supported only for byte slices, because of underlying `MEMCPY` opcode * pointers are supported only for struct literals, one can't take an address of an arbitrary variable * there is no real distinction between different integer types, all of them diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index e83873d31..62ca08ce6 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -14,7 +14,7 @@ import ( var ( // Go language builtin functions. - goBuiltins = []string{"len", "append", "panic", "make"} + goBuiltins = []string{"len", "append", "panic", "make", "copy"} // Custom builtin utility functions. customBuiltins = []string{ "FromAddress", "Equals", diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 42a9881c8..693b3f109 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -1295,6 +1295,44 @@ func (c *codegen) convertSyscall(expr *ast.CallExpr, api, name string) { emit.Opcode(c.prog.BinWriter, opcode.NOP) } +// emitSliceHelper emits 3 items on stack: slice, its first index, and its size. +func (c *codegen) emitSliceHelper(e ast.Expr) { + if !isByteSlice(c.typeOf(e)) { + c.prog.Err = fmt.Errorf("copy is supported only for byte-slices") + return + } + var hasLowIndex bool + switch src := e.(type) { + case *ast.SliceExpr: + ast.Walk(c, src.X) + if src.High != nil { + ast.Walk(c, src.High) + } else { + emit.Opcode(c.prog.BinWriter, opcode.DUP) + emit.Opcode(c.prog.BinWriter, opcode.SIZE) + } + if src.Low != nil { + ast.Walk(c, src.Low) + hasLowIndex = true + } else { + emit.Int(c.prog.BinWriter, 0) + } + default: + ast.Walk(c, src) + emit.Opcode(c.prog.BinWriter, opcode.DUP) + emit.Opcode(c.prog.BinWriter, opcode.SIZE) + emit.Int(c.prog.BinWriter, 0) + } + if !hasLowIndex { + emit.Opcode(c.prog.BinWriter, opcode.SWAP) + } else { + emit.Opcode(c.prog.BinWriter, opcode.DUP) + emit.Opcode(c.prog.BinWriter, opcode.ROT) + emit.Opcode(c.prog.BinWriter, opcode.SWAP) + emit.Opcode(c.prog.BinWriter, opcode.SUB) + } +} + func (c *codegen) convertBuiltin(expr *ast.CallExpr) { var name string switch t := expr.Fun.(type) { @@ -1305,6 +1343,14 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) { } switch name { + case "copy": + // stack for MEMCPY is: dst, dst_index, src, src_index, count + c.emitSliceHelper(expr.Args[0]) + c.emitSliceHelper(expr.Args[1]) + emit.Int(c.prog.BinWriter, 3) + emit.Opcode(c.prog.BinWriter, opcode.ROLL) + emit.Opcode(c.prog.BinWriter, opcode.MIN) + emit.Opcode(c.prog.BinWriter, opcode.MEMCPY) case "make": typ := c.typeOf(expr.Args[0]) switch { @@ -1412,10 +1458,10 @@ func transformArgs(fun ast.Expr, args []ast.Expr) []ast.Expr { return args[1:] } case *ast.Ident: - if f.Name == "panic" { + switch f.Name { + case "panic": return args[1:] - } - if f.Name == "make" { + case "make", "copy": return nil } } diff --git a/pkg/compiler/slice_test.go b/pkg/compiler/slice_test.go index ca1d62fe7..6ceb967dc 100644 --- a/pkg/compiler/slice_test.go +++ b/pkg/compiler/slice_test.go @@ -364,3 +364,67 @@ func TestMake(t *testing.T) { require.Error(t, err) }) } + +func TestCopy(t *testing.T) { + t.Run("Invalid", func(t *testing.T) { + src := `package foo + func Main() []int { + src := []int{3, 2, 1} + dst := make([]int, 2) + copy(dst, src) + return dst + }` + _, err := compiler.Compile("foo.go", strings.NewReader(src)) + require.Error(t, err) + }) + t.Run("Simple", func(t *testing.T) { + src := `package foo + func Main() []byte { + src := []byte{3, 2, 1} + dst := make([]byte, 2) + copy(dst, src) + return dst + }` + eval(t, src, []byte{3, 2}) + }) + t.Run("LowSrcIndex", func(t *testing.T) { + src := `package foo + func Main() []byte { + src := []byte{3, 2, 1} + dst := make([]byte, 2) + copy(dst, src[1:]) + return dst + }` + eval(t, src, []byte{2, 1}) + }) + t.Run("LowDstIndex", func(t *testing.T) { + src := `package foo + func Main() []byte { + src := []byte{3, 2, 1} + dst := make([]byte, 2) + copy(dst[1:], src[1:]) + return dst + }` + eval(t, src, []byte{0, 2}) + }) + t.Run("BothIndices", func(t *testing.T) { + src := `package foo + func Main() []byte { + src := []byte{4, 3, 2, 1} + dst := make([]byte, 4) + copy(dst[1:], src[1:3]) + return dst + }` + eval(t, src, []byte{0, 3, 2, 0}) + }) + t.Run("EmptySliceExpr", func(t *testing.T) { + src := `package foo + func Main() []byte { + src := []byte{3, 2, 1} + dst := make([]byte, 2) + copy(dst[1:], src[:]) + return dst + }` + eval(t, src, []byte{0, 3}) + }) +}