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
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

View file

@ -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",

View file

@ -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
}
}

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/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) {

View file

@ -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})
})
}

View file

@ -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)
}

View file

@ -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])

View file

@ -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))