compiler: disallow comparing slices with nil

NEO VM does not distinguish between empty and nil slices. Supporting
this is not easy and requires changing lots of other opcodes.
Pointers are not supported anyway and slices can be checked for
emptiness by inspecting their `len`.
This commit is contained in:
Evgenii Stratonikov 2020-06-08 10:57:14 +03:00
parent bf6aa02dcf
commit 9d6b0ee4a8
2 changed files with 47 additions and 12 deletions

View file

@ -617,25 +617,31 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
ast.Walk(c, n.X) ast.Walk(c, n.X)
ast.Walk(c, n.Y) ast.Walk(c, n.Y)
switch { switch n.Op {
case n.Op == token.ADD: case token.ADD:
// VM has separate opcodes for number and string concatenation // VM has separate opcodes for number and string concatenation
if isStringType(tinfo.Type) { if isStringType(tinfo.Type) {
emit.Opcode(c.prog.BinWriter, opcode.CAT) emit.Opcode(c.prog.BinWriter, opcode.CAT)
} else { } else {
emit.Opcode(c.prog.BinWriter, opcode.ADD) emit.Opcode(c.prog.BinWriter, opcode.ADD)
} }
case n.Op == token.EQL: case token.EQL, token.NEQ:
// VM has separate opcodes for number and string equality if isExprNil(n.X) || isExprNil(n.Y) {
op := c.getEqualityOpcode(n.X) c.prog.Err = errors.New("comparison with `nil` is not supported, use `len(..) == 0` instead")
emit.Opcode(c.prog.BinWriter, op) return nil
case n.Op == token.NEQ: }
// VM has separate opcodes for number and string equality if n.Op == token.EQL {
if isStringType(c.typeInfo.Types[n.X].Type) { // VM has separate opcodes for number and string equality
emit.Opcode(c.prog.BinWriter, opcode.EQUAL) op := c.getEqualityOpcode(n.X)
emit.Opcode(c.prog.BinWriter, opcode.NOT) emit.Opcode(c.prog.BinWriter, op)
} else { } else {
emit.Opcode(c.prog.BinWriter, opcode.NUMNOTEQUAL) // VM has separate opcodes for number and string equality
if isStringType(c.typeInfo.Types[n.X].Type) {
emit.Opcode(c.prog.BinWriter, opcode.EQUAL)
emit.Opcode(c.prog.BinWriter, opcode.NOT)
} else {
emit.Opcode(c.prog.BinWriter, opcode.NUMNOTEQUAL)
}
} }
default: default:
c.convertToken(n.Op) c.convertToken(n.Op)

View file

@ -1,10 +1,14 @@
package compiler_test package compiler_test
import ( import (
"fmt"
"math/big" "math/big"
"strings"
"testing" "testing"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/stretchr/testify/require"
) )
var sliceTestCases = []testCase{ var sliceTestCases = []testCase{
@ -175,6 +179,31 @@ func TestSliceOperations(t *testing.T) {
runTestCases(t, sliceTestCases) runTestCases(t, sliceTestCases)
} }
func TestSliceEmpty(t *testing.T) {
srcTmpl := `package foo
func Main() int {
var a []int
%s
if %s {
return 1
}
return 2
}`
t.Run("WithNil", func(t *testing.T) {
src := fmt.Sprintf(srcTmpl, "", "a == nil")
_, err := compiler.Compile(strings.NewReader(src))
require.Error(t, err)
})
t.Run("WithLen", func(t *testing.T) {
src := fmt.Sprintf(srcTmpl, "", "len(a) == 0")
eval(t, src, big.NewInt(1))
})
t.Run("NonEmpty", func(t *testing.T) {
src := fmt.Sprintf(srcTmpl, "a = []int{1}", "len(a) == 0")
eval(t, src, big.NewInt(2))
})
}
func TestJumps(t *testing.T) { func TestJumps(t *testing.T) {
src := ` src := `
package foo package foo