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) * 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 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. 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) ## VM API (interop layer)
Compiler translates interop function calls into Neo VM syscalls or (for custom 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") 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 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") 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 ( var (
@ -361,6 +363,13 @@ func (c *codegen) analyzeFuncAndGlobalVarUsage() funcUsage {
case *ast.FuncDecl: case *ast.FuncDecl:
name := c.getFuncNameFromDecl(pkgPath, n) 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 // exported functions and methods are always assumed to be used
if isMain && n.Name.IsExported() || isInitFunc(n) || isDeployFunc(n) { if isMain && n.Name.IsExported() || isInitFunc(n) || isDeployFunc(n) {
diff[name] = true diff[name] = true
@ -388,6 +397,13 @@ func (c *codegen) analyzeFuncAndGlobalVarUsage() funcUsage {
nodeCache[name] = declPair{n, c.importMap, pkgPath} nodeCache[name] = declPair{n, c.importMap, pkgPath}
return false // will be processed in the next stage return false // will be processed in the next stage
case *ast.GenDecl: 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 // After skipping all funcDecls, we are sure that each value spec
// is a globally declared variable or constant. We need to gather global // is a globally declared variable or constant. We need to gather global
// vars from both main and imported packages. // vars from both main and imported packages.
@ -535,6 +551,54 @@ func (c *codegen) analyzeFuncAndGlobalVarUsage() funcUsage {
return usage 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 // nodeContext contains ast node with the corresponding import map, type info and package information
// required to retrieve fully qualified node name (if so). // required to retrieve fully qualified node name (if so).
type nodeContext struct { type nodeContext struct {

View file

@ -566,6 +566,13 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
// x = 2 // x = 2
// ) // )
case *ast.GenDecl: 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 { if n.Tok == token.VAR || n.Tok == token.CONST {
c.saveSequencePoint(n) c.saveSequencePoint(n)
} }

View file

@ -1,6 +1,7 @@
package compiler package compiler
import ( import (
"fmt"
"go/ast" "go/ast"
"go/types" "go/types"
) )
@ -85,7 +86,23 @@ func (c *codegen) getFuncNameFromDecl(pkgPath string, decl *ast.FuncDecl) string
case *ast.Ident: case *ast.Ident:
name = t.Name + "." + name name = t.Name + "." + name
case *ast.StarExpr: 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) 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)
})
}