diff --git a/docs/compiler.md b/docs/compiler.md index 118b94d92..18660e703 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -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 diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index 0dfefd2e6..b3ca6954a 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -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 { diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 4ee702626..1a609f8fc 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -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) } diff --git a/pkg/compiler/func_scope.go b/pkg/compiler/func_scope.go index c6b6e19b7..31fd9d381 100644 --- a/pkg/compiler/func_scope.go +++ b/pkg/compiler/func_scope.go @@ -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) diff --git a/pkg/compiler/generics_test.go b/pkg/compiler/generics_test.go new file mode 100644 index 000000000..0bf58aeec --- /dev/null +++ b/pkg/compiler/generics_test.go @@ -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) + }) +}