forked from TrueCloudLab/neoneo-go
Merge pull request #3041 from nspcc-dev/generic-decl
compiler: temporary disallow generics usages
This commit is contained in:
commit
227b0c5480
5 changed files with 182 additions and 1 deletions
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
91
pkg/compiler/generics_test.go
Normal file
91
pkg/compiler/generics_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in a new issue