diff --git a/docs/compiler.md b/docs/compiler.md index 7d5810177..c96793d22 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -28,6 +28,9 @@ a dialect of Go rather than a complete port of the language: * converting value to interface type doesn't change the underlying type, original value will always be used, therefore it never panics and always "succeeds"; it's up to the programmer whether it's a correct use of a value + * type assertion with two return values is not supported; single return value (of the desired type) + is supported; type assertion panics if value can't be asserted to the desired type, therefore + it's up to the programmer whether assert can be performed successfully. ## VM API (interop layer) Compiler translates interop function calls into NEO VM syscalls or (for custom diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 1d56a2e2c..f8944a253 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -161,6 +161,9 @@ const ( varArgument ) +// ErrUnsupportedTypeAssertion is returned when type assertion statement is not supported by the compiler. +var ErrUnsupportedTypeAssertion = errors.New("type assertion with two return values is not supported") + // newLabel creates a new label to jump to. func (c *codegen) newLabel() (l uint16) { li := len(c.l) @@ -584,6 +587,14 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { for _, spec := range n.Specs { switch t := spec.(type) { case *ast.ValueSpec: + // Filter out type assertion with two return values: var i, ok = v.(int) + if len(t.Names) == 2 && len(t.Values) == 1 && n.Tok == token.VAR { + err := checkTypeAssertWithOK(t.Values[0]) + if err != nil { + c.prog.Err = err + return nil + } + } multiRet := n.Tok == token.VAR && len(t.Values) != 0 && len(t.Names) != len(t.Values) for _, id := range t.Names { if id.Name != "_" { @@ -630,6 +641,14 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { return nil case *ast.AssignStmt: + // Filter out type assertion with two return values: i, ok = v.(int) + if len(n.Lhs) == 2 && len(n.Rhs) == 1 && (n.Tok == token.DEFINE || n.Tok == token.ASSIGN) { + err := checkTypeAssertWithOK(n.Rhs[0]) + if err != nil { + c.prog.Err = err + return nil + } + } multiRet := len(n.Rhs) != len(n.Lhs) c.saveSequencePoint(n) // Assign operations are grouped https://github.com/golang/go/blob/master/src/go/types/stmt.go#L160 @@ -1346,6 +1365,14 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { return c } +func checkTypeAssertWithOK(n ast.Node) error { + if t, ok := n.(*ast.TypeAssertExpr); ok && + t.Type != nil { // not a type switch + return ErrUnsupportedTypeAssertion + } + return nil +} + // packVarArgs packs variadic arguments into an array // and returns the amount of arguments packed. func (c *codegen) packVarArgs(n *ast.CallExpr, typ *types.Signature) int { diff --git a/pkg/compiler/convert_test.go b/pkg/compiler/convert_test.go index 2793bfef8..073b153de 100644 --- a/pkg/compiler/convert_test.go +++ b/pkg/compiler/convert_test.go @@ -81,14 +81,93 @@ func TestConvert(t *testing.T) { } func TestTypeAssertion(t *testing.T) { - src := `package foo - func Main() int { - a := []byte{1} - var u interface{} - u = a - return u.(int) - }` - eval(t, src, big.NewInt(1)) + t.Run("inside return statement", func(t *testing.T) { + src := `package foo + func Main() int { + a := []byte{1} + var u interface{} + u = a + return u.(int) + }` + eval(t, src, big.NewInt(1)) + }) + t.Run("inside general declaration", func(t *testing.T) { + src := `package foo + func Main() int { + a := []byte{1} + var u interface{} + u = a + var ret = u.(int) + return ret + }` + eval(t, src, big.NewInt(1)) + }) + t.Run("inside assignment statement", func(t *testing.T) { + src := `package foo + func Main() int { + a := []byte{1} + var u interface{} + u = a + var ret int + ret = u.(int) + return ret + }` + eval(t, src, big.NewInt(1)) + }) + t.Run("inside definition statement", func(t *testing.T) { + src := `package foo + func Main() int { + a := []byte{1} + var u interface{} + u = a + ret := u.(int) + return ret + }` + eval(t, src, big.NewInt(1)) + }) +} + +func TestTypeAssertionWithOK(t *testing.T) { + t.Run("inside general declaration", func(t *testing.T) { + src := `package foo + func Main() bool { + a := 1 + var u interface{} + u = a + var _, ok = u.(int) // *ast.GenDecl + return ok + }` + _, _, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil) + require.Error(t, err) + require.ErrorIs(t, err, compiler.ErrUnsupportedTypeAssertion) + }) + t.Run("inside assignment statement", func(t *testing.T) { + src := `package foo + func Main() bool { + a := 1 + var u interface{} + u = a + var ok bool + _, ok = u.(int) // *ast.AssignStmt + return ok + }` + _, _, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil) + require.Error(t, err) + require.ErrorIs(t, err, compiler.ErrUnsupportedTypeAssertion) + }) + t.Run("inside definition statement", func(t *testing.T) { + src := `package foo + func Main() bool { + a := 1 + var u interface{} + u = a + _, ok := u.(int) // *ast.AssignStmt + return ok + }` + _, _, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil) + require.Error(t, err) + require.ErrorIs(t, err, compiler.ErrUnsupportedTypeAssertion) + }) } func TestTypeConversion(t *testing.T) {