Merge pull request #1352 from nspcc-dev/compiler/make

Fix MEMCPY, support `copy` and `make` in compiler
This commit is contained in:
Roman Khimov 2020-08-25 09:33:16 +03:00 committed by GitHub
commit a1fbe51bca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 187 additions and 7 deletions

View file

@ -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 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 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: 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 * `new()` is not supported, most of the time you can substitute structs with composite literals
them 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 * pointers are supported only for struct literals, one can't take an address
of an arbitrary variable of an arbitrary variable
* there is no real distinction between different integer types, all of them * there is no real distinction between different integer types, all of them

View file

@ -14,7 +14,7 @@ import (
var ( var (
// Go language builtin functions. // Go language builtin functions.
goBuiltins = []string{"len", "append", "panic"} goBuiltins = []string{"len", "append", "panic", "make", "copy"}
// Custom builtin utility functions. // Custom builtin utility functions.
customBuiltins = []string{ customBuiltins = []string{
"FromAddress", "Equals", "FromAddress", "Equals",

View file

@ -1313,6 +1313,44 @@ func (c *codegen) convertSyscall(expr *ast.CallExpr, api, name string) {
emit.Opcode(c.prog.BinWriter, opcode.NOP) 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) { func (c *codegen) convertBuiltin(expr *ast.CallExpr) {
var name string var name string
switch t := expr.Fun.(type) { switch t := expr.Fun.(type) {
@ -1323,6 +1361,32 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) {
} }
switch name { 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": case "len":
emit.Opcode(c.prog.BinWriter, opcode.DUP) emit.Opcode(c.prog.BinWriter, opcode.DUP)
emit.Opcode(c.prog.BinWriter, opcode.ISNULL) emit.Opcode(c.prog.BinWriter, opcode.ISNULL)
@ -1412,8 +1476,11 @@ func transformArgs(fun ast.Expr, args []ast.Expr) []ast.Expr {
return args[1:] return args[1:]
} }
case *ast.Ident: case *ast.Ident:
if f.Name == "panic" { switch f.Name {
case "panic":
return args[1:] return args[1:]
case "make", "copy":
return nil
} }
} }

View file

@ -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/pkg2"
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/pkg3" import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/pkg3"
func Main() int { return pkg3.A }` func Main() int { return pkg3.A }`
v := vmAndCompile(t, src)
v.PrintOps()
eval(t, src, big.NewInt(2)) eval(t, src, big.NewInt(2))
}) })
t.Run("2,1", func(t *testing.T) { t.Run("2,1", func(t *testing.T) {

View file

@ -2,9 +2,12 @@ package compiler_test
import ( import (
"math/big" "math/big"
"strings"
"testing" "testing"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
) )
var sliceTestCases = []testCase{ var sliceTestCases = []testCase{
@ -323,3 +326,105 @@ func TestJumps(t *testing.T) {
` `
eval(t, src, []byte{0x62, 0x01, 0x00}) 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})
})
}

View file

@ -27,6 +27,11 @@ func isBasicTypeOfKind(typ types.Type, ks ...types.BasicKind) bool {
return false return false
} }
func isMap(typ types.Type) bool {
_, ok := typ.Underlying().(*types.Map)
return ok
}
func isByte(typ types.Type) bool { func isByte(typ types.Type) bool {
return isBasicTypeOfKind(typ, types.Uint8, types.Int8) return isBasicTypeOfKind(typ, types.Uint8, types.Int8)
} }

View file

@ -659,7 +659,7 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
panic("invalid destination index") panic("invalid destination index")
} }
dst := v.estack.Pop().value.(*stackitem.Buffer).Value().([]byte) 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") panic("size is too big")
} }
copy(dst[di:], src[si:si+n]) copy(dst[di:], src[si:si+n])

View file

@ -1344,6 +1344,10 @@ func TestMEMCPY(t *testing.T) {
buf := stackitem.NewBuffer([]byte{0, 1, 2, 3}) 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) 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("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("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)) t.Run("NegativeDstIndex", getTestFuncForVM(prog, nil, stackitem.NewBuffer([]byte{0, 1}), -1, []byte{2}, 0, 1))