diff --git a/docs/compiler.md b/docs/compiler.md index 8b2613478..56b95a90d 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -7,8 +7,9 @@ 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 + * `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 71870e98c..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"} + 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 0d4445b3f..1bb4e163d 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -1313,6 +1313,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) { @@ -1323,6 +1361,32 @@ 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 { + 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) @@ -1412,8 +1476,11 @@ 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:] + case "make", "copy": + return nil } } 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) { diff --git a/pkg/compiler/slice_test.go b/pkg/compiler/slice_test.go index 20223fb04..6ceb967dc 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,105 @@ 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) + }) +} + +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}) + }) +} 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) } 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))