From 415d44792a6a6d5f3836ba70621dbcdb696cfb3a Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 17 Aug 2023 12:39:06 +0300 Subject: [PATCH 1/5] compiler: properly retrieve name of generic functions Fix panic described in #3040. Ref. #2376. Signed-off-by: Anna Shaleva --- pkg/compiler/func_scope.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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) From 1b1b454ce440eba8b61f3f179917182af819cfd4 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 17 Aug 2023 12:40:29 +0300 Subject: [PATCH 2/5] compiler: disallow generic method receiver Either non-pointer or pointer, both cases are disallowed to be generic. Need to be reverted and properly handled within the scope of #2376. Signed-off-by: Anna Shaleva --- pkg/compiler/analysis.go | 35 ++++++++++++++++++++++++++++++++ pkg/compiler/generics_test.go | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 pkg/compiler/generics_test.go diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index 0dfefd2e6..e4ac01a94 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 @@ -535,6 +544,32 @@ 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") + } + } + + if errGenerics != nil { + return fmt.Errorf("%w: %s has %s", ErrGenericsUnsuppored, funcName, errGenerics.Error()) + } + + 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/generics_test.go b/pkg/compiler/generics_test.go new file mode 100644 index 000000000..6a73d65c5 --- /dev/null +++ b/pkg/compiler/generics_test.go @@ -0,0 +1,38 @@ +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) + }) +} From 380de580a73d493a0fd92dd0a6af9257d1f98294 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 16 Aug 2023 18:25:16 +0300 Subject: [PATCH 3/5] compiler: disallow generic function parameter types Need to be reverted and properly handled within the scope of #2376. Signed-off-by: Anna Shaleva --- pkg/compiler/analysis.go | 5 +++++ pkg/compiler/generics_test.go | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index e4ac01a94..23ab6dede 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -563,6 +563,11 @@ func (c *codegen) checkGenericsFuncDecl(n *ast.FuncDecl, funcName string) error } } + // 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()) } diff --git a/pkg/compiler/generics_test.go b/pkg/compiler/generics_test.go index 6a73d65c5..28eb00fda 100644 --- a/pkg/compiler/generics_test.go +++ b/pkg/compiler/generics_test.go @@ -36,3 +36,18 @@ func TestGenericMethodReceiver(t *testing.T) { 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) +} From 35c3b65c8add42ffd6847afc5b981d8988d7b3da Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 16 Aug 2023 18:33:03 +0300 Subject: [PATCH 4/5] compiler: disallow generic type decl Need to be reverted and properly handled within the scope of #2376. Signed-off-by: Anna Shaleva --- pkg/compiler/analysis.go | 24 ++++++++++++++++++++++ pkg/compiler/codegen.go | 7 +++++++ pkg/compiler/generics_test.go | 38 +++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index 23ab6dede..b3ca6954a 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -397,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. @@ -575,6 +582,23 @@ func (c *codegen) checkGenericsFuncDecl(n *ast.FuncDecl, funcName string) 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/generics_test.go b/pkg/compiler/generics_test.go index 28eb00fda..0bf58aeec 100644 --- a/pkg/compiler/generics_test.go +++ b/pkg/compiler/generics_test.go @@ -51,3 +51,41 @@ func TestGenericFuncArgument(t *testing.T) { _, _, 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) + }) +} From 66a80017d379fce57d8633b0eb856b38a1f57fd1 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 18 Aug 2023 16:24:56 +0300 Subject: [PATCH 5/5] docs: add note about type aliases and generics support Part of #3023. Signed-off-by: Anna Shaleva --- docs/compiler.md | 2 ++ 1 file changed, 2 insertions(+) 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