Merge pull request #3041 from nspcc-dev/generic-decl

compiler: temporary disallow generics usages
This commit is contained in:
Roman Khimov 2023-08-18 21:24:56 +03:00 committed by GitHub
commit 227b0c5480
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 182 additions and 1 deletions

View file

@ -31,6 +31,8 @@ a dialect of Go rather than a complete port of the language:
* 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.
* type aliases including the built-in `any` alias are supported.
* generics are not supported, but eventually will be (at least, partially), ref. https://github.com/nspcc-dev/neo-go/issues/2376.
## VM API (interop layer)
Compiler translates interop function calls into Neo VM syscalls or (for custom

View file

@ -19,6 +19,8 @@ var (
ErrMissingExportedParamName = errors.New("exported method is not allowed to have unnamed parameter")
// ErrInvalidExportedRetCount is returned when exported contract method has invalid return values count.
ErrInvalidExportedRetCount = errors.New("exported method is not allowed to have more than one return value")
// ErrGenericsUnsuppored is returned when generics-related tokens are encountered.
ErrGenericsUnsuppored = errors.New("generics are currently unsupported, please, see the https://github.com/nspcc-dev/neo-go/issues/2376")
)
var (
@ -361,6 +363,13 @@ func (c *codegen) analyzeFuncAndGlobalVarUsage() funcUsage {
case *ast.FuncDecl:
name := c.getFuncNameFromDecl(pkgPath, n)
// filter out generic functions
err := c.checkGenericsFuncDecl(n, name)
if err != nil {
c.prog.Err = err
return false // Program is invalid.
}
// exported functions and methods are always assumed to be used
if isMain && n.Name.IsExported() || isInitFunc(n) || isDeployFunc(n) {
diff[name] = true
@ -388,6 +397,13 @@ func (c *codegen) analyzeFuncAndGlobalVarUsage() funcUsage {
nodeCache[name] = declPair{n, c.importMap, pkgPath}
return false // will be processed in the next stage
case *ast.GenDecl:
// Filter out generics usage.
err := c.checkGenericsGenDecl(n, pkgPath)
if err != nil {
c.prog.Err = err
return false // Program is invalid.
}
// After skipping all funcDecls, we are sure that each value spec
// is a globally declared variable or constant. We need to gather global
// vars from both main and imported packages.
@ -535,6 +551,54 @@ func (c *codegen) analyzeFuncAndGlobalVarUsage() funcUsage {
return usage
}
// checkGenericFuncDecl checks whether provided ast.FuncDecl has generic code.
func (c *codegen) checkGenericsFuncDecl(n *ast.FuncDecl, funcName string) error {
var errGenerics error
// Generic function receiver.
if n.Recv != nil {
switch t := n.Recv.List[0].Type.(type) {
case *ast.StarExpr:
switch t.X.(type) {
case *ast.IndexExpr:
// func (x *Pointer[T]) Load() *T
errGenerics = errors.New("generic pointer function receiver")
}
case *ast.IndexExpr:
// func (x Structure[T]) Load() *T
errGenerics = errors.New("generic function receiver")
}
}
// Generic function parameters type: func SumInts[V int64 | int32](vals []V) V
if n.Type.TypeParams != nil {
errGenerics = errors.New("function type parameters")
}
if errGenerics != nil {
return fmt.Errorf("%w: %s has %s", ErrGenericsUnsuppored, funcName, errGenerics.Error())
}
return nil
}
// checkGenericsGenDecl checks whether provided ast.GenDecl has generic code.
func (c *codegen) checkGenericsGenDecl(n *ast.GenDecl, pkgPath string) error {
// Generic type declaration:
// type List[T any] struct
// type List[T any] interface
if n.Tok == token.TYPE {
for _, s := range n.Specs {
typeSpec := s.(*ast.TypeSpec)
if typeSpec.TypeParams != nil {
return fmt.Errorf("%w: type %s is generic", ErrGenericsUnsuppored, c.getIdentName(pkgPath, typeSpec.Name.Name))
}
}
}
return nil
}
// nodeContext contains ast node with the corresponding import map, type info and package information
// required to retrieve fully qualified node name (if so).
type nodeContext struct {

View file

@ -566,6 +566,13 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
// x = 2
// )
case *ast.GenDecl:
// Filter out generics usage.
err := c.checkGenericsGenDecl(n, c.currPkg.PkgPath)
if err != nil {
c.prog.Err = err
return nil // Program is invalid.
}
if n.Tok == token.VAR || n.Tok == token.CONST {
c.saveSequencePoint(n)
}

View file

@ -1,6 +1,7 @@
package compiler
import (
"fmt"
"go/ast"
"go/types"
)
@ -85,7 +86,23 @@ func (c *codegen) getFuncNameFromDecl(pkgPath string, decl *ast.FuncDecl) string
case *ast.Ident:
name = t.Name + "." + name
case *ast.StarExpr:
name = t.X.(*ast.Ident).Name + "." + name
switch t.X.(type) {
case *ast.Ident:
name = t.X.(*ast.Ident).Name + "." + name
case *ast.IndexExpr:
// Generic func declaration receiver: func (x *Pointer[T]) Load() *T
name = t.X.(*ast.IndexExpr).X.(*ast.Ident).Name + "." + name
default:
panic(fmt.Errorf("unexpected function `%s` receiver type: %T", name, t.X))
}
case *ast.IndexExpr:
switch t.X.(type) {
case *ast.Ident:
// Generic func declaration receiver: func (x Pointer[T]) Load() *T
name = t.X.(*ast.Ident).Name + "." + name
default:
panic(fmt.Errorf("unexpected function `%s` receiver type: %T", name, t.X))
}
}
}
return c.getIdentName(pkgPath, name)

View file

@ -0,0 +1,91 @@
package compiler_test
import (
"strings"
"testing"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/stretchr/testify/require"
)
func TestGenericMethodReceiver(t *testing.T) {
t.Run("star expression", func(t *testing.T) {
src := `
package receiver
type Pointer[T any] struct {
value T
}
func (x *Pointer[T]) Load() *T {
return &x.value
}
`
_, _, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil)
require.ErrorIs(t, err, compiler.ErrGenericsUnsuppored)
})
t.Run("ident expression", func(t *testing.T) {
src := `
package receiver
type Pointer[T any] struct {
value T
}
func (x Pointer[T]) Load() *T {
return &x.value
}
`
_, _, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil)
require.ErrorIs(t, err, compiler.ErrGenericsUnsuppored)
})
}
func TestGenericFuncArgument(t *testing.T) {
src := `
package sum
func SumInts[V int64 | int32 | int16](vals []V) V { // doesn't make sense with NeoVM, but still it's a valid go code.
var s V
for i := range vals {
s += vals[i]
}
return s
}
`
_, _, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil)
require.ErrorIs(t, err, compiler.ErrGenericsUnsuppored)
}
func TestGenericTypeDecl(t *testing.T) {
t.Run("global scope", func(t *testing.T) {
src := `
package sum
type List[T any] struct {
next *List[T]
val T
}
func Main() any {
l := List[int]{}
return l
}
`
_, _, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil)
require.ErrorIs(t, err, compiler.ErrGenericsUnsuppored)
})
t.Run("local scope", func(t *testing.T) {
src := `
package sum
func Main() any {
type (
SomeGoodType int
List[T any] struct {
next *List[T]
val T
}
)
l := List[int]{}
return l
}
`
_, _, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil)
require.ErrorIs(t, err, compiler.ErrGenericsUnsuppored)
})
}