diff --git a/build/build_test.go b/build/build_test.go index 343e8b933..7bda7f54a 100644 --- a/build/build_test.go +++ b/build/build_test.go @@ -418,18 +418,17 @@ func TestOverlayAugmentation(t *testing.T) { test.want = test.src } - fsetSrc := token.NewFileSet() - fileSrc := srctesting.Parse(t, fsetSrc, pkgName+test.src) + f := srctesting.New(t) + fileSrc := f.Parse("test.go", pkgName+test.src) overrides := map[string]overrideInfo{} augmentOverlayFile(fileSrc, overrides) pruneImports(fileSrc) - got := srctesting.Format(t, fsetSrc, fileSrc) + got := srctesting.Format(t, f.FileSet, fileSrc) - fsetWant := token.NewFileSet() - fileWant := srctesting.Parse(t, fsetWant, pkgName+test.want) - want := srctesting.Format(t, fsetWant, fileWant) + fileWant := f.Parse("test.go", pkgName+test.want) + want := srctesting.Format(t, f.FileSet, fileWant) if got != want { t.Errorf("augmentOverlayFile and pruneImports got unexpected code:\n"+ @@ -720,18 +719,17 @@ func TestOriginalAugmentation(t *testing.T) { t.Run(test.desc, func(t *testing.T) { pkgName := "package testpackage\n\n" importPath := `math/rand` - fsetSrc := token.NewFileSet() - fileSrc := srctesting.Parse(t, fsetSrc, pkgName+test.src) + f := srctesting.New(t) + fileSrc := f.Parse("test.go", pkgName+test.src) augmentOriginalImports(importPath, fileSrc) augmentOriginalFile(fileSrc, test.info) pruneImports(fileSrc) - got := srctesting.Format(t, fsetSrc, fileSrc) + got := srctesting.Format(t, f.FileSet, fileSrc) - fsetWant := token.NewFileSet() - fileWant := srctesting.Parse(t, fsetWant, pkgName+test.want) - want := srctesting.Format(t, fsetWant, fileWant) + fileWant := f.Parse("test.go", pkgName+test.want) + want := srctesting.Format(t, f.FileSet, fileWant) if got != want { t.Errorf("augmentOriginalImports, augmentOriginalFile, and pruneImports got unexpected code:\n"+ diff --git a/build/cache/cache.go b/build/cache/cache.go index 14257ef9e..2c4e5703a 100644 --- a/build/cache/cache.go +++ b/build/cache/cache.go @@ -58,7 +58,7 @@ func Clear() error { // the cache. For example, any artifacts that were cached for a minified build // must not be reused for a non-minified build. GopherJS version change also // invalidates the cache. It is callers responsibility to ensure that artifacts -// passed the the StoreArchive function were generated with the same build +// passed the StoreArchive function were generated with the same build // parameters as the cache is configured. // // There is no upper limit for the total cache size. It can be cleared diff --git a/circle.yml b/circle.yml index 8c55a7d1b..2bf6d5a26 100644 --- a/circle.yml +++ b/circle.yml @@ -74,6 +74,8 @@ orbs: jobs: build: executor: gopherjs + environment: + GOPHERJS_EXPERIMENT: generics steps: - setup_and_install_gopherjs - run: @@ -129,6 +131,8 @@ jobs: gopherjs_tests: executor: gopherjs parallelism: 4 + environment: + GOPHERJS_EXPERIMENT: generics steps: - setup_and_install_gopherjs - run: @@ -155,6 +159,8 @@ jobs: gorepo_tests: executor: gopherjs + environment: + GOPHERJS_EXPERIMENT: generics parallelism: 4 steps: - setup_environment @@ -170,6 +176,8 @@ jobs: executor: name: win/default shell: powershell.exe + environment: + GOPHERJS_EXPERIMENT: generics steps: - checkout - run: @@ -204,6 +212,8 @@ jobs: darwin_smoke: macos: xcode: 13.4.1 # Mac OS 12.6.1, see https://circleci.com/docs/using-macos/ + environment: + GOPHERJS_EXPERIMENT: generics steps: - checkout - setup_environment diff --git a/compiler/analysis/info.go b/compiler/analysis/info.go index 304c8808a..f7c28d3c4 100644 --- a/compiler/analysis/info.go +++ b/compiler/analysis/info.go @@ -342,8 +342,8 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { return nil // No need to walk under this CallExpr, we already did it manually. default: if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { - // This is a type assertion, not a call. Type assertion itself is not - // blocking, but we will visit the expression itself. + // This is a type conversion, not a call. Type assertion itself is not + // blocking, but we will visit the input expression. } else { // The function is returned by a non-trivial expression. We have to be // conservative and assume that function might be blocking. @@ -357,6 +357,7 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { func (fi *FuncInfo) callToNamedFunc(callee types.Object) { switch o := callee.(type) { case *types.Func: + o = o.Origin() if recv := o.Type().(*types.Signature).Recv(); recv != nil { if _, ok := recv.Type().Underlying().(*types.Interface); ok { // Conservatively assume that an interface implementation may be blocking. diff --git a/compiler/analysis/info_test.go b/compiler/analysis/info_test.go index 723208255..588f09a6c 100644 --- a/compiler/analysis/info_test.go +++ b/compiler/analysis/info_test.go @@ -2,7 +2,6 @@ package analysis import ( "go/ast" - "go/token" "go/types" "testing" @@ -34,11 +33,11 @@ func notBlocking() { func() { println() } () } ` - fset := token.NewFileSet() - file := srctesting.Parse(t, fset, src) - typesInfo, typesPkg := srctesting.Check(t, fset, file) + f := srctesting.New(t) + file := f.Parse("test.go", src) + typesInfo, typesPkg := f.Check("pkg/test", file) - pkgInfo := AnalyzePkg([]*ast.File{file}, fset, typesInfo, typesPkg, func(f *types.Func) bool { + pkgInfo := AnalyzePkg([]*ast.File{file}, f.FileSet, typesInfo, typesPkg, func(f *types.Func) bool { panic("isBlocking() should be never called for imported functions in this test.") }) diff --git a/compiler/astutil/astutil.go b/compiler/astutil/astutil.go index 5cfe2dbd3..9ff88a48c 100644 --- a/compiler/astutil/astutil.go +++ b/compiler/astutil/astutil.go @@ -35,7 +35,15 @@ func NewIdent(name string, t types.Type, info *types.Info, pkg *types.Package) * return ident } +// IsTypeExpr returns true if expr denotes a type. This can be used to +// distinguish between calls and type conversions. func IsTypeExpr(expr ast.Expr, info *types.Info) bool { + // Note that we could've used info.Types[expr].IsType() instead of doing our + // own analysis. However, that creates a problem because we synthesize some + // *ast.CallExpr nodes and, more importantly, *ast.Ident nodes that denote a + // type. Unfortunately, because the flag that controls + // types.TypeAndValue.IsType() return value is unexported we wouldn't be able + // to set it correctly. Thus, we can't rely on IsType(). switch e := expr.(type) { case *ast.ArrayType, *ast.ChanType, *ast.FuncType, *ast.InterfaceType, *ast.MapType, *ast.StructType: return true @@ -47,6 +55,20 @@ func IsTypeExpr(expr ast.Expr, info *types.Info) bool { case *ast.SelectorExpr: _, ok := info.Uses[e.Sel].(*types.TypeName) return ok + case *ast.IndexExpr: + ident, ok := e.X.(*ast.Ident) + if !ok { + return false + } + _, ok = info.Uses[ident].(*types.TypeName) + return ok + case *ast.IndexListExpr: + ident, ok := e.X.(*ast.Ident) + if !ok { + return false + } + _, ok = info.Uses[ident].(*types.TypeName) + return ok case *ast.ParenExpr: return IsTypeExpr(e.X, info) default: diff --git a/compiler/astutil/astutil_test.go b/compiler/astutil/astutil_test.go index 28528a2b3..56dabc510 100644 --- a/compiler/astutil/astutil_test.go +++ b/compiler/astutil/astutil_test.go @@ -2,7 +2,6 @@ package astutil import ( "go/ast" - "go/token" "strconv" "testing" @@ -44,8 +43,7 @@ func TestImportsUnsafe(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { src := "package testpackage\n\n" + test.imports - fset := token.NewFileSet() - file := srctesting.Parse(t, fset, src) + file := srctesting.New(t).Parse("test.go", src) got := ImportsUnsafe(file) if got != test.want { t.Fatalf("ImportsUnsafe() returned %t, want %t", got, test.want) @@ -81,8 +79,7 @@ func TestImportName(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { src := "package testpackage\n\n" + test.src - fset := token.NewFileSet() - file := srctesting.Parse(t, fset, src) + file := srctesting.New(t).Parse("test.go", src) if len(file.Imports) != 1 { t.Fatal(`expected one and only one import`) } @@ -399,8 +396,7 @@ func TestHasDirectiveOnFile(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { const action = `do-stuff` - fset := token.NewFileSet() - file := srctesting.Parse(t, fset, test.src) + file := srctesting.New(t).Parse("test.go", test.src) if got := hasDirective(file, action); got != test.want { t.Errorf(`hasDirective(%T, %q) returned %t, want %t`, file, action, got, test.want) } diff --git a/compiler/compiler.go b/compiler/compiler.go index 0588a923c..91578e884 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -17,6 +17,7 @@ import ( "strings" "time" + "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/gopherjs/gopherjs/compiler/prelude" "golang.org/x/tools/go/gcexportdata" ) @@ -32,22 +33,6 @@ func init() { } } -type ErrorList []error - -func (err ErrorList) Error() string { - if len(err) == 0 { - return "" - } - return fmt.Sprintf("%s (and %d more errors)", err[0].Error(), len(err[1:])) -} - -func (err ErrorList) Normalize() error { - if len(err) == 0 { - return nil - } - return err -} - // Archive contains intermediate build outputs of a single package. // // This is a logical equivalent of an object file in traditional compilers. @@ -112,9 +97,13 @@ type Decl struct { // Go compiler/linker toolchain. Used by GopherJS to support go:linkname // directives. Must be set for decls that are supported by go:linkname // implementation. - LinkingName SymName + LinkingName symbol.Name // A list of package-level JavaScript variable names this symbol needs to declare. Vars []string + // A JS expression by which the object represented by this decl may be + // referenced within the package context. Empty if the decl represents no such + // object. + RefExpr string // NamedRecvType is method named recv declare. NamedRecvType string // JavaScript code that declares basic information about a symbol. For a type @@ -326,7 +315,7 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameS if recv, method, ok := d.LinkingName.IsMethod(); ok { code = fmt.Sprintf("\t$linknames[%q] = $unsafeMethodToFunction(%v,%q,%t);\n", d.LinkingName.String(), d.NamedRecvType, method, strings.HasPrefix(recv, "*")) } else { - code = fmt.Sprintf("\t$linknames[%q] = %s;\n", d.LinkingName.String(), d.Vars[0]) + code = fmt.Sprintf("\t$linknames[%q] = %s;\n", d.LinkingName.String(), d.RefExpr) } if _, err := w.Write(removeWhitespace([]byte(code), minify)); err != nil { return err @@ -357,7 +346,7 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameS if !found { continue // The symbol is not affected by a go:linkname directive. } - lines = append(lines, fmt.Sprintf("\t\t%s = $linknames[%q];\n", d.Vars[0], impl.String())) + lines = append(lines, fmt.Sprintf("\t\t%s = $linknames[%q];\n", d.RefExpr, impl.String())) } if len(lines) > 0 { code := fmt.Sprintf("\t$pkg.$initLinknames = function() {\n%s};\n", strings.Join(lines, "")) diff --git a/compiler/errors.go b/compiler/errors.go new file mode 100644 index 000000000..48aed48ec --- /dev/null +++ b/compiler/errors.go @@ -0,0 +1,68 @@ +package compiler + +import ( + "errors" + "fmt" +) + +// ErrTooManyErrors is added to the ErrorList by the Trim method. +var ErrTooManyErrors = errors.New("too many errors") + +// ErrorList wraps multiple errors as a single error. +type ErrorList []error + +func (errs ErrorList) Error() string { + if len(errs) == 0 { + return "" + } + return fmt.Sprintf("%s (and %d more errors)", errs[0].Error(), len(errs[1:])) +} + +// ErrOrNil returns nil if ErrorList is empty, or the error otherwise. +func (errs ErrorList) ErrOrNil() error { + if len(errs) == 0 { + return nil + } + return errs +} + +// Append an error to the list. +// +// If err is an instance of ErrorList, the lists are concatenated together, +// otherwise err is appended at the end of the list. If err is nil, the list is +// returned unmodified. +// +// err := DoStuff() +// errList := errList.Append(err) +func (errs ErrorList) Append(err error) ErrorList { + if err == nil { + return errs + } + if err, ok := err.(ErrorList); ok { + return append(errs, err...) + } + return append(errs, err) +} + +// AppendDistinct is similar to Append, but doesn't append the error if it has +// the same message as the last error on the list. +func (errs ErrorList) AppendDistinct(err error) ErrorList { + if l := len(errs); l > 0 { + if prev := errs[l-1]; prev != nil && err.Error() == prev.Error() { + return errs // The new error is the same as the last one, skip it. + } + } + + return errs.Append(err) +} + +// Trim the error list if it has more than limit errors. If the list is trimmed, +// all extraneous errors are replaced with a single ErrTooManyErrors, making the +// returned ErrorList length of limit+1. +func (errs ErrorList) Trim(limit int) ErrorList { + if len(errs) <= limit { + return errs + } + + return append(errs[:limit], ErrTooManyErrors) +} diff --git a/compiler/expressions.go b/compiler/expressions.go index 21971ab5f..2768e3d2a 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -13,6 +13,7 @@ import ( "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) @@ -33,7 +34,7 @@ func (e *expression) StringWithParens() string { } func (fc *funcContext) translateExpr(expr ast.Expr) *expression { - exprType := fc.pkgCtx.TypeOf(expr) + exprType := fc.typeOf(expr) if value := fc.pkgCtx.Types[expr].Value; value != nil { basic := exprType.Underlying().(*types.Basic) switch { @@ -76,19 +77,16 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } } - var obj types.Object + var inst typeparams.Instance switch e := expr.(type) { case *ast.SelectorExpr: - obj = fc.pkgCtx.Uses[e.Sel] + inst = fc.instanceOf(e.Sel) case *ast.Ident: - obj = fc.pkgCtx.Defs[e] - if obj == nil { - obj = fc.pkgCtx.Uses[e] - } + inst = fc.instanceOf(e) } - if obj != nil && typesutil.IsJsPackage(obj.Pkg()) { - switch obj.Name() { + if inst.Object != nil && typesutil.IsJsPackage(inst.Object.Pkg()) { + switch inst.Object.Name() { case "Global": return fc.formatExpr("$global") case "Module": @@ -203,11 +201,16 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } case *ast.FuncLit: - _, fun := translateFunction(e.Type, nil, e.Body, fc, exprType.(*types.Signature), fc.pkgCtx.FuncLitInfos[e], "") + _, fun := translateFunction(e.Type, nil, e.Body, fc, exprType.(*types.Signature), fc.pkgCtx.FuncLitInfos[e], "", typeparams.Instance{}) if len(fc.pkgCtx.escapingVars) != 0 { names := make([]string, 0, len(fc.pkgCtx.escapingVars)) for obj := range fc.pkgCtx.escapingVars { - names = append(names, fc.pkgCtx.objectNames[obj]) + name, ok := fc.assignedObjectName(obj) + if !ok { + // This should never happen. + panic(fmt.Errorf("escaping variable %s hasn't been assigned a JS name", obj)) + } + names = append(names, name) } sort.Strings(names) list := strings.Join(names, ", ") @@ -216,7 +219,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { return fc.formatExpr("(%s)", fun) case *ast.UnaryExpr: - t := fc.pkgCtx.TypeOf(e.X) + t := fc.typeOf(e.X) switch e.Op { case token.AND: if typesutil.IsJsObject(exprType) { @@ -236,26 +239,31 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { switch x := astutil.RemoveParens(e.X).(type) { case *ast.CompositeLit: - return fc.formatExpr("$newDataPointer(%e, %s)", x, fc.typeName(fc.pkgCtx.TypeOf(e))) + return fc.formatExpr("$newDataPointer(%e, %s)", x, fc.typeName(fc.typeOf(e))) case *ast.Ident: obj := fc.pkgCtx.Uses[x].(*types.Var) if fc.pkgCtx.escapingVars[obj] { - return fc.formatExpr("(%1s.$ptr || (%1s.$ptr = new %2s(function() { return this.$target[0]; }, function($v) { this.$target[0] = $v; }, %1s)))", fc.pkgCtx.objectNames[obj], fc.typeName(exprType)) + name, ok := fc.assignedObjectName(obj) + if !ok { + // This should never happen. + panic(fmt.Errorf("escaping variable %s hasn't been assigned a JS name", obj)) + } + return fc.formatExpr("(%1s.$ptr || (%1s.$ptr = new %2s(function() { return this.$target[0]; }, function($v) { this.$target[0] = $v; }, %1s)))", name, fc.typeName(exprType)) } return fc.formatExpr(`(%1s || (%1s = new %2s(function() { return %3s; }, function($v) { %4s })))`, fc.varPtrName(obj), fc.typeName(exprType), fc.objectName(obj), fc.translateAssign(x, fc.newIdent("$v", elemType), false)) case *ast.SelectorExpr: - sel, ok := fc.pkgCtx.SelectionOf(x) + sel, ok := fc.selectionOf(x) if !ok { // qualified identifier obj := fc.pkgCtx.Uses[x.Sel].(*types.Var) return fc.formatExpr(`(%1s || (%1s = new %2s(function() { return %3s; }, function($v) { %4s })))`, fc.varPtrName(obj), fc.typeName(exprType), fc.objectName(obj), fc.translateAssign(x, fc.newIdent("$v", elemType), false)) } - newSel := &ast.SelectorExpr{X: fc.newIdent("this.$target", fc.pkgCtx.TypeOf(x.X)), Sel: x.Sel} + newSel := &ast.SelectorExpr{X: fc.newIdent("this.$target", fc.typeOf(x.X)), Sel: x.Sel} fc.setType(newSel, exprType) fc.pkgCtx.additionalSelections[newSel] = sel return fc.formatExpr("(%1e.$ptr_%2s || (%1e.$ptr_%2s = new %3s(function() { return %4e; }, function($v) { %5s }, %1e)))", x.X, x.Sel.Name, fc.typeName(exprType), newSel, fc.translateAssign(newSel, fc.newIdent("$v", exprType), false)) case *ast.IndexExpr: - if _, ok := fc.pkgCtx.TypeOf(x.X).Underlying().(*types.Slice); ok { + if _, ok := fc.typeOf(x.X).Underlying().(*types.Slice); ok { return fc.formatExpr("$indexPtr(%1e.$array, %1e.$offset + %2e, %3s)", x.X, x.Index, fc.typeName(exprType)) } return fc.formatExpr("$indexPtr(%e, %e, %s)", x.X, x.Index, fc.typeName(exprType)) @@ -312,8 +320,8 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { })) } - t := fc.pkgCtx.TypeOf(e.X) - t2 := fc.pkgCtx.TypeOf(e.Y) + t := fc.typeOf(e.X) + t2 := fc.typeOf(e.Y) _, isInterface := t2.Underlying().(*types.Interface) if isInterface || types.Identical(t, types.Typ[types.UntypedNil]) { t = t2 @@ -390,14 +398,14 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if isUnsigned(basic) { shift = ">>>" } - return fc.formatExpr(`(%1s = %2e / %3e, (%1s === %1s && %1s !== 1/0 && %1s !== -1/0) ? %1s %4s 0 : $throwRuntimeError("integer divide by zero"))`, fc.newVariable("_q"), e.X, e.Y, shift) + return fc.formatExpr(`(%1s = %2e / %3e, (%1s === %1s && %1s !== 1/0 && %1s !== -1/0) ? %1s %4s 0 : $throwRuntimeError("integer divide by zero"))`, fc.newLocalVariable("_q"), e.X, e.Y, shift) } if basic.Kind() == types.Float32 { return fc.fixNumber(fc.formatExpr("%e / %e", e.X, e.Y), basic) } return fc.formatExpr("%e / %e", e.X, e.Y) case token.REM: - return fc.formatExpr(`(%1s = %2e %% %3e, %1s === %1s ? %1s : $throwRuntimeError("integer divide by zero"))`, fc.newVariable("_r"), e.X, e.Y) + return fc.formatExpr(`(%1s = %2e %% %3e, %1s === %1s ? %1s : $throwRuntimeError("integer divide by zero"))`, fc.newLocalVariable("_r"), e.X, e.Y) case token.SHL, token.SHR: op := e.Op.String() if e.Op == token.SHR && isUnsigned(basic) { @@ -413,7 +421,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if e.Op == token.SHR && !isUnsigned(basic) { return fc.fixNumber(fc.formatParenExpr("%e >> $min(%f, 31)", e.X, e.Y), basic) } - y := fc.newVariable("y") + y := fc.newLocalVariable("y") return fc.fixNumber(fc.formatExpr("(%s = %f, %s < 32 ? (%e %s %s) : 0)", y, e.Y, y, e.X, op, y), basic) case token.AND, token.OR: if isUnsigned(basic) { @@ -436,7 +444,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if fc.Blocking[e.Y] { skipCase := fc.caseCounter fc.caseCounter++ - resultVar := fc.newVariable("_v") + resultVar := fc.newLocalVariable("_v") fc.Printf("if (!(%s)) { %s = false; $s = %d; continue s; }", fc.translateExpr(e.X), resultVar, skipCase) fc.Printf("%s = %s; case %d:", resultVar, fc.translateExpr(e.Y), skipCase) return fc.formatExpr("%s", resultVar) @@ -446,7 +454,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if fc.Blocking[e.Y] { skipCase := fc.caseCounter fc.caseCounter++ - resultVar := fc.newVariable("_v") + resultVar := fc.newLocalVariable("_v") fc.Printf("if (%s) { %s = true; $s = %d; continue s; }", fc.translateExpr(e.X), resultVar, skipCase) fc.Printf("%s = %s; case %d:", resultVar, fc.translateExpr(e.Y), skipCase) return fc.formatExpr("%s", resultVar) @@ -477,7 +485,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { return fc.formatParenExpr("%e", e.X) case *ast.IndexExpr: - switch t := fc.pkgCtx.TypeOf(e.X).Underlying().(type) { + switch t := fc.typeOf(e.X).Underlying().(type) { case *types.Pointer: if _, ok := t.Elem().Underlying().(*types.Array); !ok { // Should never happen in type-checked code. @@ -498,14 +506,14 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case *types.Slice: return fc.formatExpr(rangeCheck("%1e.$array[%1e.$offset + %2f]", fc.pkgCtx.Types[e.Index].Value != nil, false), e.X, e.Index) case *types.Map: - if typesutil.IsJsObject(fc.pkgCtx.TypeOf(e.Index)) { + if typesutil.IsJsObject(fc.typeOf(e.Index)) { fc.pkgCtx.errList = append(fc.pkgCtx.errList, types.Error{Fset: fc.pkgCtx.fileSet, Pos: e.Index.Pos(), Msg: "cannot use js.Object as map key"}) } key := fmt.Sprintf("%s.keyFor(%s)", fc.typeName(t.Key()), fc.translateImplicitConversion(e.Index, t.Key())) if _, isTuple := exprType.(*types.Tuple); isTuple { return fc.formatExpr( `(%1s = $mapIndex(%2e,%3s), %1s !== undefined ? [%1s.v, true] : [%4e, false])`, - fc.newVariable("_entry"), + fc.newLocalVariable("_entry"), e.X, key, fc.zeroValue(t.Elem()), @@ -513,7 +521,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } return fc.formatExpr( `(%1s = $mapIndex(%2e,%3s), %1s !== undefined ? %1s.v : %4e)`, - fc.newVariable("_entry"), + fc.newLocalVariable("_entry"), e.X, key, fc.zeroValue(t.Elem()), @@ -521,14 +529,19 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case *types.Basic: return fc.formatExpr("%e.charCodeAt(%f)", e.X, e.Index) case *types.Signature: - err := bailout(fmt.Errorf(`unsupported type parameters used at %s`, fc.pkgCtx.fileSet.Position(e.Pos()))) - panic(err) + return fc.formatExpr("%s", fc.instName(fc.instanceOf(e.X.(*ast.Ident)))) default: panic(fmt.Errorf(`unhandled IndexExpr: %T`, t)) } - + case *ast.IndexListExpr: + switch t := fc.typeOf(e.X).Underlying().(type) { + case *types.Signature: + return fc.formatExpr("%s", fc.instName(fc.instanceOf(e.X.(*ast.Ident)))) + default: + panic(fmt.Errorf("unhandled IndexListExpr: %T", t)) + } case *ast.SliceExpr: - if b, isBasic := fc.pkgCtx.TypeOf(e.X).Underlying().(*types.Basic); isBasic && isString(b) { + if b, isBasic := fc.typeOf(e.X).Underlying().(*types.Basic); isBasic && isString(b) { switch { case e.Low == nil && e.High == nil: return fc.translateExpr(e.X) @@ -559,10 +572,10 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } case *ast.SelectorExpr: - sel, ok := fc.pkgCtx.SelectionOf(e) + sel, ok := fc.selectionOf(e) if !ok { // qualified identifier - return fc.formatExpr("%s", fc.objectName(obj)) + return fc.formatExpr("%s", fc.instName(inst)) } switch sel.Kind() { @@ -593,10 +606,10 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { plainFun := astutil.RemoveParens(e.Fun) if astutil.IsTypeExpr(plainFun, fc.pkgCtx.Info.Info) { - return fc.formatExpr("(%s)", fc.translateConversion(e.Args[0], fc.pkgCtx.TypeOf(plainFun))) + return fc.formatExpr("(%s)", fc.translateConversion(e.Args[0], fc.typeOf(plainFun))) } - sig := fc.pkgCtx.TypeOf(plainFun).Underlying().(*types.Signature) + sig := fc.typeOf(plainFun).Underlying().(*types.Signature) switch f := plainFun.(type) { case *ast.Ident: @@ -610,10 +623,13 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { return fc.translateCall(e, sig, fc.translateExpr(f)) case *ast.SelectorExpr: - sel, ok := fc.pkgCtx.SelectionOf(f) + sel, ok := fc.selectionOf(f) if !ok { // qualified identifier obj := fc.pkgCtx.Uses[f.Sel] + if o, ok := obj.(*types.Builtin); ok { + return fc.translateBuiltin(o.Name(), sig, e.Args, e.Ellipsis.IsValid()) + } if typesutil.IsJsPackage(obj.Pkg()) { switch obj.Name() { case "Debugger": @@ -626,7 +642,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } externalizeExpr := func(e ast.Expr) string { - t := fc.pkgCtx.TypeOf(e) + t := fc.typeOf(e) if types.Identical(t, types.Typ[types.UntypedNil]) { return "null" } @@ -673,13 +689,13 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case "Call": if id, ok := fc.identifierConstant(e.Args[0]); ok { if e.Ellipsis.IsValid() { - objVar := fc.newVariable("obj") + objVar := fc.newLocalVariable("obj") return fc.formatExpr("(%s = %s, %s.%s.apply(%s, %s))", objVar, recv, objVar, id, objVar, externalizeExpr(e.Args[1])) } return fc.formatExpr("%s(%s)", globalRef(id), externalizeArgs(e.Args[1:])) } if e.Ellipsis.IsValid() { - objVar := fc.newVariable("obj") + objVar := fc.newLocalVariable("obj") return fc.formatExpr("(%s = %s, %s[$externalize(%e, $String)].apply(%s, %s))", objVar, recv, objVar, e.Args[0], objVar, externalizeExpr(e.Args[1])) } return fc.formatExpr("%s[$externalize(%e, $String)](%s)", recv, e.Args[0], externalizeArgs(e.Args[1:])) @@ -746,11 +762,11 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } case *ast.StarExpr: - if typesutil.IsJsObject(fc.pkgCtx.TypeOf(e.X)) { + if typesutil.IsJsObject(fc.typeOf(e.X)) { return fc.formatExpr("new $jsObjectPtr(%e)", e.X) } if c1, isCall := e.X.(*ast.CallExpr); isCall && len(c1.Args) == 1 { - if c2, isCall := c1.Args[0].(*ast.CallExpr); isCall && len(c2.Args) == 1 && types.Identical(fc.pkgCtx.TypeOf(c2.Fun), types.Typ[types.UnsafePointer]) { + if c2, isCall := c1.Args[0].(*ast.CallExpr); isCall && len(c2.Args) == 1 && types.Identical(fc.typeOf(c2.Fun), types.Typ[types.UnsafePointer]) { if unary, isUnary := c2.Args[0].(*ast.UnaryExpr); isUnary && unary.Op == token.AND { return fc.translateExpr(unary.X) // unsafe conversion } @@ -766,7 +782,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if e.Type == nil { return fc.translateExpr(e.X) } - t := fc.pkgCtx.TypeOf(e.Type) + t := fc.typeOf(e.Type) if _, isTuple := exprType.(*types.Tuple); isTuple { return fc.formatExpr("$assertType(%e, %s, true)", e.X, fc.typeName(t)) } @@ -776,11 +792,11 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if e.Name == "_" { panic("Tried to translate underscore identifier.") } - switch o := obj.(type) { + switch o := inst.Object.(type) { case *types.Var, *types.Const: - return fc.formatExpr("%s", fc.objectName(o)) + return fc.formatExpr("%s", fc.instName(inst)) case *types.Func: - return fc.formatExpr("%s", fc.objectName(o)) + return fc.formatExpr("%s", fc.instName(inst)) case *types.TypeName: return fc.formatExpr("%s", fc.typeName(o.Type())) case *types.Nil: @@ -826,7 +842,7 @@ func (fc *funcContext) translateCall(e *ast.CallExpr, sig *types.Signature, fun fc.caseCounter++ returnVar := "$r" if sig.Results().Len() != 0 { - returnVar = fc.newVariable("_r") + returnVar = fc.newLocalVariable("_r") } fc.Printf("%[1]s = %[2]s(%[3]s); /* */ $s = %[4]d; case %[4]d: if($c) { $c = false; %[1]s = %[1]s.$blk(); } if (%[1]s && %[1]s.$blk !== undefined) { break s; }", returnVar, fun, strings.Join(args, ", "), resumeCase) if sig.Results().Len() != 0 { @@ -841,7 +857,7 @@ func (fc *funcContext) translateCall(e *ast.CallExpr, sig *types.Signature, fun // and its arguments to be invoked elsewhere. // // This function is necessary in conjunction with keywords such as `go` and `defer`, -// where we need to compute function and its arguments at the the keyword site, +// where we need to compute function and its arguments at the keyword site, // but the call itself will happen elsewhere (hence "delegated"). // // Built-in functions and cetrain `js.Object` methods don't translate into JS @@ -857,9 +873,8 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, case *ast.SelectorExpr: isJs = typesutil.IsJsPackage(fc.pkgCtx.Uses[fun.Sel].Pkg()) } - sig := fc.pkgCtx.TypeOf(expr.Fun).Underlying().(*types.Signature) - sigTypes := signatureTypes{Sig: sig} - args := fc.translateArgs(sig, expr.Args, expr.Ellipsis.IsValid()) + sig := typesutil.Signature{Sig: fc.typeOf(expr.Fun).Underlying().(*types.Signature)} + args := fc.translateArgs(sig.Sig, expr.Args, expr.Ellipsis.IsValid()) if !isBuiltin && !isJs { // Normal function calls don't require wrappers. @@ -876,12 +891,12 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, ellipsis := expr.Ellipsis for i := range expr.Args { - v := fc.newVariable("_arg") + v := fc.newLocalVariable("_arg") vars[i] = v // Subtle: the proxy lambda argument needs to be assigned with the type // that the original function expects, and not with the argument // expression result type, or we may do implicit type conversion twice. - callArgs[i] = fc.newIdent(v, sigTypes.Param(i, ellipsis.IsValid())) + callArgs[i] = fc.newIdent(v, sig.Param(i, ellipsis.IsValid())) } wrapper := &ast.CallExpr{ Fun: expr.Fun, @@ -894,7 +909,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, } func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression { - sel, _ := fc.pkgCtx.SelectionOf(e) + sel, _ := fc.selectionOf(e) if !sel.Obj().Exported() { fc.pkgCtx.dependencies[sel.Obj()] = true } @@ -911,12 +926,7 @@ func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression { } fakeSel := &ast.SelectorExpr{X: x, Sel: ast.NewIdent("o")} - fc.pkgCtx.additionalSelections[fakeSel] = &fakeSelection{ - kind: types.FieldVal, - recv: sel.Recv(), - index: sel.Index()[:len(sel.Index())-1], - typ: recvType, - } + fc.pkgCtx.additionalSelections[fakeSel] = typesutil.NewSelection(types.FieldVal, sel.Recv(), sel.Index()[:len(sel.Index())-1], nil, recvType) x = fc.setType(fakeSel, recvType) } @@ -953,9 +963,9 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args return fc.formatExpr("$newDataPointer(%e, %s)", fc.zeroValue(t.Elem()), fc.typeName(t)) } case "make": - switch argType := fc.pkgCtx.TypeOf(args[0]).Underlying().(type) { + switch argType := fc.typeOf(args[0]).Underlying().(type) { case *types.Slice: - t := fc.typeName(fc.pkgCtx.TypeOf(args[0])) + t := fc.typeName(fc.typeOf(args[0])) if len(args) == 3 { return fc.formatExpr("$makeSlice(%s, %f, %f)", t, args[1], args[2]) } @@ -970,12 +980,12 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args if len(args) == 2 { length = fc.formatExpr("%f", args[1]).String() } - return fc.formatExpr("new $Chan(%s, %s)", fc.typeName(fc.pkgCtx.TypeOf(args[0]).Underlying().(*types.Chan).Elem()), length) + return fc.formatExpr("new $Chan(%s, %s)", fc.typeName(fc.typeOf(args[0]).Underlying().(*types.Chan).Elem()), length) default: panic(fmt.Sprintf("Unhandled make type: %T\n", argType)) } case "len": - switch argType := fc.pkgCtx.TypeOf(args[0]).Underlying().(type) { + switch argType := fc.typeOf(args[0]).Underlying().(type) { case *types.Basic: return fc.formatExpr("%e.length", args[0]) case *types.Slice: @@ -991,7 +1001,7 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args panic(fmt.Sprintf("Unhandled len type: %T\n", argType)) } case "cap": - switch argType := fc.pkgCtx.TypeOf(args[0]).Underlying().(type) { + switch argType := fc.typeOf(args[0]).Underlying().(type) { case *types.Slice, *types.Chan: return fc.formatExpr("%e.$capacity", args[0]) case *types.Pointer: @@ -1011,7 +1021,7 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args return fc.formatExpr("$append(%e, %s)", args[0], strings.Join(fc.translateExprSlice(args[1:], sliceType.Elem()), ", ")) case "delete": args = fc.expandTupleArgs(args) - keyType := fc.pkgCtx.TypeOf(args[0]).Underlying().(*types.Map).Key() + keyType := fc.typeOf(args[0]).Underlying().(*types.Map).Key() return fc.formatExpr( `$mapDelete(%1e, %2s.keyFor(%3s))`, args[0], @@ -1020,7 +1030,7 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args ) case "copy": args = fc.expandTupleArgs(args) - if basic, isBasic := fc.pkgCtx.TypeOf(args[1]).Underlying().(*types.Basic); isBasic && isString(basic) { + if basic, isBasic := fc.typeOf(args[1]).Underlying().(*types.Basic); isBasic && isString(basic) { return fc.formatExpr("$copyString(%e, %e)", args[0], args[1]) } return fc.formatExpr("$copySlice(%e, %e)", args[0], args[1]) @@ -1041,6 +1051,13 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args return fc.formatExpr("$recover()") case "close": return fc.formatExpr(`$close(%e)`, args[0]) + case "Sizeof": + return fc.formatExpr("%d", sizes32.Sizeof(fc.typeOf(args[0]))) + case "Alignof": + return fc.formatExpr("%d", sizes32.Alignof(fc.typeOf(args[0]))) + case "Offsetof": + sel, _ := fc.selectionOf(astutil.RemoveParens(args[0]).(*ast.SelectorExpr)) + return fc.formatExpr("%d", typesutil.OffsetOf(sizes32, sel)) default: panic(fmt.Sprintf("Unhandled builtin: %s\n", name)) } @@ -1072,13 +1089,13 @@ func (fc *funcContext) translateExprSlice(exprs []ast.Expr, desiredType types.Ty } func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type) *expression { - exprType := fc.pkgCtx.TypeOf(expr) + exprType := fc.typeOf(expr) if types.Identical(exprType, desiredType) { return fc.translateExpr(expr) } if fc.pkgCtx.Pkg.Path() == "reflect" || fc.pkgCtx.Pkg.Path() == "internal/reflectlite" { - if call, isCall := expr.(*ast.CallExpr); isCall && types.Identical(fc.pkgCtx.TypeOf(call.Fun), types.Typ[types.UnsafePointer]) { + if call, isCall := expr.(*ast.CallExpr); isCall && types.Identical(fc.typeOf(call.Fun), types.Typ[types.UnsafePointer]) { if ptr, isPtr := desiredType.(*types.Pointer); isPtr { if named, isNamed := ptr.Elem().(*types.Named); isNamed { switch named.Obj().Name() { @@ -1153,10 +1170,10 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type return fc.formatExpr("new Uint8Array(0)") } } - if ptr, isPtr := fc.pkgCtx.TypeOf(expr).(*types.Pointer); fc.pkgCtx.Pkg.Path() == "syscall" && isPtr { + if ptr, isPtr := fc.typeOf(expr).(*types.Pointer); fc.pkgCtx.Pkg.Path() == "syscall" && isPtr { if s, isStruct := ptr.Elem().Underlying().(*types.Struct); isStruct { - array := fc.newVariable("_array") - target := fc.newVariable("_struct") + array := fc.newLocalVariable("_array") + target := fc.newLocalVariable("_struct") fc.Printf("%s = new Uint8Array(%d);", array, sizes32.Sizeof(s)) fc.Delayed(func() { fc.Printf("%s = %s, %s;", target, fc.translateExpr(expr), fc.loadStruct(array, target, s)) @@ -1166,7 +1183,7 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type } if call, ok := expr.(*ast.CallExpr); ok { if id, ok := call.Fun.(*ast.Ident); ok && id.Name == "new" { - return fc.formatExpr("new Uint8Array(%d)", int(sizes32.Sizeof(fc.pkgCtx.TypeOf(call.Args[0])))) + return fc.formatExpr("new Uint8Array(%d)", int(sizes32.Sizeof(fc.typeOf(call.Args[0])))) } } } @@ -1204,8 +1221,8 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type // struct pointer when handling syscalls. // TODO(nevkontakte): Add a runtime assertion that the unsafe.Pointer is // indeed pointing at a byte array. - array := fc.newVariable("_array") - target := fc.newVariable("_struct") + array := fc.newLocalVariable("_array") + target := fc.newLocalVariable("_struct") return fc.formatExpr("(%s = %e, %s = %e, %s, %s)", array, expr, target, fc.zeroValue(t.Elem()), fc.loadStruct(array, target, ptrElType), target) } // Convert between structs of different types but identical layouts, @@ -1227,7 +1244,7 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type // type iPtr *int; var c int = 42; println((iPtr)(&c)); // TODO(nevkontakte): Are there any other cases that fall into this case? exprTypeElem := exprType.Underlying().(*types.Pointer).Elem() - ptrVar := fc.newVariable("_ptr") + ptrVar := fc.newLocalVariable("_ptr") getterConv := fc.translateConversion(fc.setType(&ast.StarExpr{X: fc.newIdent(ptrVar, exprType)}, exprTypeElem), t.Elem()) setterConv := fc.translateConversion(fc.newIdent("$v", t.Elem()), exprTypeElem) return fc.formatExpr("(%1s = %2e, new %3s(function() { return %4s; }, function($v) { %1s.$set(%5s); }, %1s.$target))", ptrVar, expr, fc.typeName(desiredType), getterConv, setterConv) @@ -1255,7 +1272,7 @@ func (fc *funcContext) translateImplicitConversion(expr ast.Expr, desiredType ty return fc.translateExpr(expr) } - exprType := fc.pkgCtx.TypeOf(expr) + exprType := fc.typeOf(expr) if types.Identical(exprType, desiredType) { return fc.translateExpr(expr) } @@ -1286,7 +1303,7 @@ func (fc *funcContext) translateImplicitConversion(expr ast.Expr, desiredType ty } func (fc *funcContext) translateConversionToSlice(expr ast.Expr, desiredType types.Type) *expression { - switch fc.pkgCtx.TypeOf(expr).Underlying().(type) { + switch fc.typeOf(expr).Underlying().(type) { case *types.Array, *types.Pointer: return fc.formatExpr("new %s(%e)", fc.typeName(desiredType), expr) } @@ -1294,7 +1311,7 @@ func (fc *funcContext) translateConversionToSlice(expr ast.Expr, desiredType typ } func (fc *funcContext) loadStruct(array, target string, s *types.Struct) string { - view := fc.newVariable("_view") + view := fc.newLocalVariable("_view") code := fmt.Sprintf("%s = new DataView(%s.buffer, %s.byteOffset)", view, array, array) var fields []*types.Var var collectFields func(s *types.Struct, path string) @@ -1424,7 +1441,7 @@ func (fc *funcContext) formatExprInternal(format string, a []interface{}, parens out.WriteByte('(') parens = false } - v := fc.newVariable("x") + v := fc.newLocalVariable("x") out.WriteString(v + " = " + fc.translateExpr(e.(ast.Expr)).String() + ", ") vars[i] = v } @@ -1447,7 +1464,7 @@ func (fc *funcContext) formatExprInternal(format string, a []interface{}, parens } out.WriteString(a[n].(string)) case 'd': - out.WriteString(strconv.Itoa(a[n].(int))) + fmt.Fprintf(out, "%d", a[n]) case 't': out.WriteString(a[n].(token.Token).String()) case 'e': @@ -1464,7 +1481,7 @@ func (fc *funcContext) formatExprInternal(format string, a []interface{}, parens out.WriteString(strconv.FormatInt(d, 10)) return } - if is64Bit(fc.pkgCtx.TypeOf(e).Underlying().(*types.Basic)) { + if is64Bit(fc.typeOf(e).Underlying().(*types.Basic)) { out.WriteString("$flatten64(") writeExpr("") out.WriteString(")") @@ -1475,7 +1492,7 @@ func (fc *funcContext) formatExprInternal(format string, a []interface{}, parens e := a[n].(ast.Expr) if val := fc.pkgCtx.Types[e].Value; val != nil { d, _ := constant.Uint64Val(constant.ToInt(val)) - if fc.pkgCtx.TypeOf(e).Underlying().(*types.Basic).Kind() == types.Int64 { + if fc.typeOf(e).Underlying().(*types.Basic).Kind() == types.Int64 { out.WriteString(strconv.FormatInt(int64(d)>>32, 10)) return } diff --git a/compiler/internal/symbol/symbol.go b/compiler/internal/symbol/symbol.go new file mode 100644 index 000000000..851ca1ef6 --- /dev/null +++ b/compiler/internal/symbol/symbol.go @@ -0,0 +1,60 @@ +package symbol + +import ( + "go/types" + "strings" +) + +// Name uniquely identifies a named symbol within a program. +// +// This is a logical equivalent of a symbol name used by traditional linkers. +// The following properties should hold true: +// +// - Each named symbol within a program has a unique Name. +// - Similarly named methods of different types will have different symbol names. +// - The string representation is opaque and should not be attempted to reversed +// to a struct form. +type Name struct { + PkgPath string // Full package import path. + Name string // Symbol name. +} + +// New constructs SymName for a given named symbol. +func New(o types.Object) Name { + if fun, ok := o.(*types.Func); ok { + sig := fun.Type().(*types.Signature) + if recv := sig.Recv(); recv != nil { + // Special case: disambiguate names for different types' methods. + typ := recv.Type() + if ptr, ok := typ.(*types.Pointer); ok { + return Name{ + PkgPath: o.Pkg().Path(), + Name: "(*" + ptr.Elem().(*types.Named).Obj().Name() + ")." + o.Name(), + } + } + return Name{ + PkgPath: o.Pkg().Path(), + Name: typ.(*types.Named).Obj().Name() + "." + o.Name(), + } + } + } + return Name{ + PkgPath: o.Pkg().Path(), + Name: o.Name(), + } +} + +func (n Name) String() string { return n.PkgPath + "." + n.Name } + +func (n Name) IsMethod() (recv string, method string, ok bool) { + pos := strings.IndexByte(n.Name, '.') + if pos == -1 { + return + } + recv, method, ok = n.Name[:pos], n.Name[pos+1:], true + size := len(recv) + if size > 2 && recv[0] == '(' && recv[size-1] == ')' { + recv = recv[1 : size-1] + } + return +} diff --git a/compiler/internal/symbol/symbol_test.go b/compiler/internal/symbol/symbol_test.go new file mode 100644 index 000000000..778e3b1e0 --- /dev/null +++ b/compiler/internal/symbol/symbol_test.go @@ -0,0 +1,53 @@ +package symbol + +import ( + "go/types" + "testing" + + "github.com/gopherjs/gopherjs/internal/srctesting" +) + +func TestName(t *testing.T) { + const src = `package testcase + + func AFunction() {} + type AType struct {} + func (AType) AMethod() {} + func (*AType) APointerMethod() {} + var AVariable int32 + ` + + f := srctesting.New(t) + _, pkg := f.Check("pkg/test", f.Parse("test.go", src)) + + tests := []struct { + obj types.Object + want Name + }{ + { + obj: pkg.Scope().Lookup("AFunction"), + want: Name{PkgPath: "pkg/test", Name: "AFunction"}, + }, { + obj: pkg.Scope().Lookup("AType"), + want: Name{PkgPath: "pkg/test", Name: "AType"}, + }, { + obj: types.NewMethodSet(pkg.Scope().Lookup("AType").Type()).Lookup(pkg, "AMethod").Obj(), + want: Name{PkgPath: "pkg/test", Name: "AType.AMethod"}, + }, { + obj: types.NewMethodSet(types.NewPointer(pkg.Scope().Lookup("AType").Type())).Lookup(pkg, "APointerMethod").Obj(), + want: Name{PkgPath: "pkg/test", Name: "(*AType).APointerMethod"}, + }, { + obj: pkg.Scope().Lookup("AVariable"), + want: Name{PkgPath: "pkg/test", Name: "AVariable"}, + }, + } + + for _, test := range tests { + t.Run(test.obj.Name(), func(t *testing.T) { + got := New(test.obj) + if got != test.want { + t.Errorf("NewSymName(%q) returned %#v, want: %#v", test.obj.Name(), got, test.want) + } + }) + } +} diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go new file mode 100644 index 000000000..0a9ae75da --- /dev/null +++ b/compiler/internal/typeparams/collect.go @@ -0,0 +1,263 @@ +package typeparams + +import ( + "fmt" + "go/ast" + "go/types" + + "github.com/gopherjs/gopherjs/compiler/typesutil" + "github.com/gopherjs/gopherjs/internal/govendor/subst" + "golang.org/x/exp/typeparams" +) + +// Resolver translates types defined in terms of type parameters into concrete +// types, given a mapping from type params to type arguments. +type Resolver struct { + subster *subst.Subster + selMemo map[typesutil.Selection]typesutil.Selection +} + +// NewResolver creates a new Resolver with tParams entries mapping to tArgs +// entries with the same index. +func NewResolver(tc *types.Context, tParams []*types.TypeParam, tArgs []types.Type) *Resolver { + r := &Resolver{ + subster: subst.New(tc, tParams, tArgs), + selMemo: map[typesutil.Selection]typesutil.Selection{}, + } + return r +} + +// Substitute replaces references to type params in the provided type definition +// with the corresponding concrete types. +func (r *Resolver) Substitute(typ types.Type) types.Type { + if r == nil || r.subster == nil || typ == nil { + return typ // No substitutions to be made. + } + return r.subster.Type(typ) +} + +// SubstituteAll same as Substitute, but accepts a TypeList are returns +// substitution results as a slice in the same order. +func (r *Resolver) SubstituteAll(list *types.TypeList) []types.Type { + result := make([]types.Type, list.Len()) + for i := range result { + result[i] = r.Substitute(list.At(i)) + } + return result +} + +// SubstituteSelection replaces a method of field selection on a generic type +// defined in terms of type parameters with a method selection on a concrete +// instantiation of the type. +func (r *Resolver) SubstituteSelection(sel typesutil.Selection) typesutil.Selection { + if r == nil || r.subster == nil || sel == nil { + return sel // No substitutions to be made. + } + if concrete, ok := r.selMemo[sel]; ok { + return concrete + } + + switch sel.Kind() { + case types.MethodExpr, types.MethodVal, types.FieldVal: + recv := r.Substitute(sel.Recv()) + if types.Identical(recv, sel.Recv()) { + return sel // Non-generic receiver, no substitution necessary. + } + + // Look up the method on the instantiated receiver. + pkg := sel.Obj().Pkg() + obj, index, _ := types.LookupFieldOrMethod(recv, true, pkg, sel.Obj().Name()) + if obj == nil { + panic(fmt.Errorf("failed to lookup field %q in type %v", sel.Obj().Name(), recv)) + } + typ := obj.Type() + + if sel.Kind() == types.MethodExpr { + typ = typesutil.RecvAsFirstArg(typ.(*types.Signature)) + } + concrete := typesutil.NewSelection(sel.Kind(), recv, index, obj, typ) + r.selMemo[sel] = concrete + return concrete + default: + panic(fmt.Errorf("unexpected selection kind %v: %v", sel.Kind(), sel)) + } +} + +// ToSlice converts TypeParamList into a slice with the same order of entries. +func ToSlice(tpl *types.TypeParamList) []*types.TypeParam { + result := make([]*types.TypeParam, tpl.Len()) + for i := range result { + result[i] = tpl.At(i) + } + return result +} + +// visitor implements ast.Visitor and collects instances of generic types and +// functions into an InstanceSet. +// +// When traversing an AST subtree corresponding to a generic type, method or +// function, Resolver must be provided mapping the type parameters into concrete +// types. +type visitor struct { + instances *PackageInstanceSets + resolver *Resolver + info *types.Info +} + +var _ ast.Visitor = &visitor{} + +func (c *visitor) Visit(n ast.Node) (w ast.Visitor) { + w = c // Always traverse the full depth of the AST tree. + + ident, ok := n.(*ast.Ident) + if !ok { + return + } + + instance, ok := c.info.Instances[ident] + if !ok { + return + } + + obj := c.info.ObjectOf(ident) + + // For types embedded in structs, the object the identifier resolves to is a + // *types.Var representing the implicitly declared struct field. However, the + // instance relates to the *types.TypeName behind the field type, which we + // obtain here. + typ := obj.Type() + if ptr, ok := typ.(*types.Pointer); ok { + typ = ptr.Elem() + } + if t, ok := typ.(*types.Named); ok { + obj = t.Obj() + } + c.instances.Add(Instance{ + Object: obj, + TArgs: c.resolver.SubstituteAll(instance.TypeArgs), + }) + + if t, ok := obj.Type().(*types.Named); ok { + for i := 0; i < t.NumMethods(); i++ { + method := t.Method(i) + c.instances.Add(Instance{ + Object: typeparams.OriginMethod(method), // TODO(nevkontakte): Can be replaced with method.Origin() in Go 1.19. + TArgs: c.resolver.SubstituteAll(instance.TypeArgs), + }) + } + } + return +} + +// seedVisitor implements ast.Visitor that collects information necessary to +// kickstart generic instantiation discovery. +// +// It serves double duty: +// - Builds a map from types.Object instances representing generic types, +// methods and functions to AST nodes that define them. +// - Collects an initial set of generic instantiations in the non-generic code. +type seedVisitor struct { + visitor + objMap map[types.Object]ast.Node + mapOnly bool // Only build up objMap, ignore any instances. +} + +var _ ast.Visitor = &seedVisitor{} + +func (c *seedVisitor) Visit(n ast.Node) ast.Visitor { + // Generic functions, methods and types require type arguments to scan for + // generic instantiations, remember their node for later and do not descend + // further. + switch n := n.(type) { + case *ast.FuncDecl: + obj := c.info.Defs[n.Name] + sig := obj.Type().(*types.Signature) + if sig.TypeParams().Len() != 0 || sig.RecvTypeParams().Len() != 0 { + c.objMap[obj] = n + return &seedVisitor{ + visitor: c.visitor, + objMap: c.objMap, + mapOnly: true, + } + } + case *ast.TypeSpec: + obj := c.info.Defs[n.Name] + named, ok := obj.Type().(*types.Named) + if !ok { + break + } + if named.TypeParams().Len() != 0 && named.TypeArgs().Len() == 0 { + c.objMap[obj] = n + return nil + } + } + + if !c.mapOnly { + // Otherwise check for fully defined instantiations and descend further into + // the AST tree. + c.visitor.Visit(n) + } + return c +} + +// Collector scans type-checked AST tree and adds discovered generic type and +// function instances to the InstanceSet. +// +// Collector will scan non-generic code for any instantiations of generic types +// or functions and add them to the InstanceSet. Then it will scan generic types +// and function with discovered sets of type arguments for more instantiations, +// until no new ones are discovered. +// +// InstanceSet may contain unprocessed instances of generic types and functions, +// which will be also scanned, for example found in depending packages. +// +// Note that instances of generic type methods are automatically added to the +// set whenever their receiver type instance is encountered. +type Collector struct { + TContext *types.Context + Info *types.Info + Instances *PackageInstanceSets +} + +// Scan package files for generic instances. +func (c *Collector) Scan(pkg *types.Package, files ...*ast.File) { + if c.Info.Instances == nil || c.Info.Defs == nil { + panic(fmt.Errorf("types.Info must have Instances and Defs populated")) + } + objMap := map[types.Object]ast.Node{} + + // Collect instances of generic objects in non-generic code in the package and + // add then to the existing InstanceSet. + sc := seedVisitor{ + visitor: visitor{ + instances: c.Instances, + resolver: nil, + info: c.Info, + }, + objMap: objMap, + } + for _, file := range files { + ast.Walk(&sc, file) + } + + for iset := c.Instances.Pkg(pkg); !iset.exhausted(); { + inst, _ := iset.next() + switch typ := inst.Object.Type().(type) { + case *types.Signature: + v := visitor{ + instances: c.Instances, + resolver: NewResolver(c.TContext, ToSlice(SignatureTypeParams(typ)), inst.TArgs), + info: c.Info, + } + ast.Walk(&v, objMap[inst.Object]) + case *types.Named: + obj := typ.Obj() + v := visitor{ + instances: c.Instances, + resolver: NewResolver(c.TContext, ToSlice(typ.TypeParams()), inst.TArgs), + info: c.Info, + } + ast.Walk(&v, objMap[obj]) + } + } +} diff --git a/compiler/internal/typeparams/collect_test.go b/compiler/internal/typeparams/collect_test.go new file mode 100644 index 000000000..9bd5faee4 --- /dev/null +++ b/compiler/internal/typeparams/collect_test.go @@ -0,0 +1,512 @@ +package typeparams + +import ( + "go/ast" + "go/types" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/gopherjs/gopherjs/internal/srctesting" + "golang.org/x/tools/go/ast/astutil" +) + +func TestVisitor(t *testing.T) { + // This test verifies that instance collector is able to discover + // instantiations of generic types and functions in all possible contexts. + const src = `package testcase + + type A struct{} + type B struct{} + type C struct{} + type D struct{} + type E struct{} + type F struct{} + type G struct{} + + type typ[T any, V any] []T + func (t *typ[T, V]) method(x T) {} + func fun[U any, W any](x U, y W) {} + + func entry1(arg typ[int8, A]) (result typ[int16, A]) { + fun(1, A{}) + fun[int8, A](1, A{}) + println(fun[int16, A]) + + t := typ[int, A]{} + t.method(0) + (*typ[int32, A]).method(nil, 0) + type x struct{ T []typ[int64, A] } + + return + } + + func entry2[T any](arg typ[int8, T]) (result typ[int16, T]) { + var zeroT T + fun(1, zeroT) + fun[int8, T](1, zeroT) + println(fun[int16, T]) + + t := typ[int, T]{} + t.method(0) + (*typ[int32, T]).method(nil, 0) + type x struct{ T []typ[int64, T] } + + return + } + + type entry3[T any] struct{ + typ[int, T] + field1 struct { field2 typ[int8, T] } + } + func (e entry3[T]) method(arg typ[int8, T]) (result typ[int16, T]) { + var zeroT T + fun(1, zeroT) + fun[int8, T](1, zeroT) + println(fun[int16, T]) + + t := typ[int, T]{} + t.method(0) + (*typ[int32, T]).method(nil, 0) + type x struct{ T []typ[int64, T] } + + return + } + + type entry4 struct{ + typ[int, E] + field1 struct { field2 typ[int8, E] } + } + + type entry5 = typ[int, F] + ` + f := srctesting.New(t) + file := f.Parse("test.go", src) + info, pkg := f.Check("pkg/test", file) + + lookupObj := func(name string) types.Object { + return srctesting.LookupObj(pkg, name) + } + lookupType := func(name string) types.Type { return lookupObj(name).Type() } + lookupDecl := func(name string) ast.Node { + obj := lookupObj(name) + path, _ := astutil.PathEnclosingInterval(file, obj.Pos(), obj.Pos()) + for _, n := range path { + switch n.(type) { + case *ast.FuncDecl, *ast.TypeSpec: + return n + } + } + t.Fatalf("Could not find AST node representing %v", obj) + return nil + } + + // Generates a list of instances we expect to discover from functions and + // methods. Sentinel type is a type parameter we use uniquely within one + // context, which allows us to make sure that collection is not being tested + // against a wrong part of AST. + instancesInFunc := func(sentinel types.Type) []Instance { + return []Instance{ + { + // Called with type arguments inferred. + Object: lookupObj("fun"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + // Called with type arguments explicitly specified. + Object: lookupObj("fun"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, { + // Passed as an argument. + Object: lookupObj("fun"), + TArgs: []types.Type{types.Typ[types.Int16], sentinel}, + }, { + // Literal expression. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + // Function argument. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, { + // Function return type. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int16], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int16], sentinel}, + }, { + // Method expression. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int32], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int32], sentinel}, + }, { + // Type decl statement. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int64], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int64], sentinel}, + }, + } + } + + // Generates a list of instances we expect to discover from type declarations. + // Sentinel type is a type parameter we use uniquely within one context, which + // allows us to make sure that collection is not being tested against a wrong + // part of AST. + instancesInType := func(sentinel types.Type) []Instance { + return []Instance{ + { + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, + } + } + + tests := []struct { + descr string + resolver *Resolver + node ast.Node + want []Instance + }{ + { + descr: "non-generic function", + resolver: nil, + node: lookupDecl("entry1"), + want: instancesInFunc(lookupType("A")), + }, { + descr: "generic function", + resolver: NewResolver( + types.NewContext(), + ToSlice(lookupType("entry2").(*types.Signature).TypeParams()), + []types.Type{lookupType("B")}, + ), + node: lookupDecl("entry2"), + want: instancesInFunc(lookupType("B")), + }, { + descr: "generic method", + resolver: NewResolver( + types.NewContext(), + ToSlice(lookupType("entry3.method").(*types.Signature).RecvTypeParams()), + []types.Type{lookupType("C")}, + ), + node: lookupDecl("entry3.method"), + want: append( + instancesInFunc(lookupType("C")), + Instance{ + Object: lookupObj("entry3"), + TArgs: []types.Type{lookupType("C")}, + }, + Instance{ + Object: lookupObj("entry3.method"), + TArgs: []types.Type{lookupType("C")}, + }, + ), + }, { + descr: "generic type declaration", + resolver: NewResolver( + types.NewContext(), + ToSlice(lookupType("entry3").(*types.Named).TypeParams()), + []types.Type{lookupType("D")}, + ), + node: lookupDecl("entry3"), + want: instancesInType(lookupType("D")), + }, { + descr: "non-generic type declaration", + resolver: nil, + node: lookupDecl("entry4"), + want: instancesInType(lookupType("E")), + }, { + descr: "non-generic type alias", + resolver: nil, + node: lookupDecl("entry5"), + want: []Instance{ + { + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int], lookupType("F")}, + }, + { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int], lookupType("F")}, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + v := visitor{ + instances: &PackageInstanceSets{}, + resolver: test.resolver, + info: info, + } + ast.Walk(&v, test.node) + got := v.instances.Pkg(pkg).Values() + if diff := cmp.Diff(test.want, got, instanceOpts()); diff != "" { + t.Errorf("Discovered instance diff (-want,+got):\n%s", diff) + } + }) + } +} + +func TestSeedVisitor(t *testing.T) { + src := `package test + type typ[T any] int + func (t typ[T]) method(arg T) { var x typ[string]; _ = x } + func fun[T any](arg T) { var y typ[string]; _ = y } + + const a typ[int] = 1 + var b typ[int] + type c struct { field typ[int8] } + func (_ c) method() { var _ typ[int16] } + type d = typ[int32] + func e() { var _ typ[int64] } + ` + + f := srctesting.New(t) + file := f.Parse("test.go", src) + info, pkg := f.Check("pkg/test", file) + + sv := seedVisitor{ + visitor: visitor{ + instances: &PackageInstanceSets{}, + resolver: nil, + info: info, + }, + objMap: map[types.Object]ast.Node{}, + } + ast.Walk(&sv, file) + + tInst := func(tArg types.Type) Instance { + return Instance{ + Object: pkg.Scope().Lookup("typ"), + TArgs: []types.Type{tArg}, + } + } + mInst := func(tArg types.Type) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, "typ.method"), + TArgs: []types.Type{tArg}, + } + } + want := []Instance{ + tInst(types.Typ[types.Int]), + mInst(types.Typ[types.Int]), + tInst(types.Typ[types.Int8]), + mInst(types.Typ[types.Int8]), + tInst(types.Typ[types.Int16]), + mInst(types.Typ[types.Int16]), + tInst(types.Typ[types.Int32]), + mInst(types.Typ[types.Int32]), + tInst(types.Typ[types.Int64]), + mInst(types.Typ[types.Int64]), + } + got := sv.instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != "" { + t.Errorf("Instances from initialSeeder contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector(t *testing.T) { + src := `package test + type typ[T any] int + func (t typ[T]) method(arg T) { var _ typ[int]; fun[int8](0) } + func fun[T any](arg T) { + var _ typ[int16] + + type nested[U any] struct{} + _ = nested[T]{} + } + + type ignore = int + + func a() { + var _ typ[int32] + fun[int64](0) + } + ` + + f := srctesting.New(t) + file := f.Parse("test.go", src) + info, pkg := f.Check("pkg/test", file) + + c := Collector{ + TContext: types.NewContext(), + Info: info, + Instances: &PackageInstanceSets{}, + } + c.Scan(pkg, file) + + inst := func(name string, tArg types.Type) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TArgs: []types.Type{tArg}, + } + } + want := []Instance{ + inst("typ", types.Typ[types.Int]), + inst("typ.method", types.Typ[types.Int]), + inst("fun", types.Typ[types.Int8]), + inst("fun.nested", types.Typ[types.Int8]), + inst("typ", types.Typ[types.Int16]), + inst("typ.method", types.Typ[types.Int16]), + inst("typ", types.Typ[types.Int32]), + inst("typ.method", types.Typ[types.Int32]), + inst("fun", types.Typ[types.Int64]), + inst("fun.nested", types.Typ[types.Int64]), + } + got := c.Instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != "" { + t.Errorf("Instances from initialSeeder contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_CrossPackage(t *testing.T) { + f := srctesting.New(t) + const src = `package foo + type X[T any] struct {Value T} + + func F[G any](g G) { + x := X[G]{} + println(x) + } + + func DoFoo() { + F(int8(8)) + } + ` + fooFile := f.Parse("foo.go", src) + _, fooPkg := f.Check("pkg/foo", fooFile) + + const src2 = `package bar + import "pkg/foo" + func FProxy[T any](t T) { + foo.F[T](t) + } + func DoBar() { + FProxy(int16(16)) + } + ` + barFile := f.Parse("bar.go", src2) + _, barPkg := f.Check("pkg/bar", barFile) + + c := Collector{ + TContext: types.NewContext(), + Info: f.Info, + Instances: &PackageInstanceSets{}, + } + c.Scan(barPkg, barFile) + c.Scan(fooPkg, fooFile) + + inst := func(pkg *types.Package, name string, tArg types.BasicKind) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TArgs: []types.Type{types.Typ[tArg]}, + } + } + + wantFooInstances := []Instance{ + inst(fooPkg, "F", types.Int16), // Found in "pkg/foo". + inst(fooPkg, "F", types.Int8), + inst(fooPkg, "X", types.Int16), // Found due to F[int16] found in "pkg/foo". + inst(fooPkg, "X", types.Int8), + } + gotFooInstances := c.Instances.Pkg(fooPkg).Values() + if diff := cmp.Diff(wantFooInstances, gotFooInstances, instanceOpts()); diff != "" { + t.Errorf("Instances from pkg/foo contain diff (-want,+got):\n%s", diff) + } + + wantBarInstances := []Instance{ + inst(barPkg, "FProxy", types.Int16), + } + gotBarInstances := c.Instances.Pkg(barPkg).Values() + if diff := cmp.Diff(wantBarInstances, gotBarInstances, instanceOpts()); diff != "" { + t.Errorf("Instances from pkg/foo contain diff (-want,+got):\n%s", diff) + } +} + +func TestResolver_SubstituteSelection(t *testing.T) { + tests := []struct { + descr string + src string + wantObj string + wantSig string + }{{ + descr: "type parameter method", + src: `package test + type stringer interface{ String() string } + + type x struct{} + func (_ x) String() string { return "" } + + type g[T stringer] struct{} + func (_ g[T]) Method(t T) string { + return t.String() + }`, + wantObj: "func (pkg/test.x).String() string", + wantSig: "func() string", + }, { + descr: "generic receiver type with type parameter", + src: `package test + type x struct{} + + type g[T any] struct{} + func (_ g[T]) Method(t T) string { + return g[T]{}.Method(t) + }`, + wantObj: "func (pkg/test.g[pkg/test.x]).Method(t pkg/test.x) string", + wantSig: "func(t pkg/test.x) string", + }, { + descr: "method expression", + src: `package test + type x struct{} + + type g[T any] struct{} + func (recv g[T]) Method(t T) string { + return g[T].Method(recv, t) + }`, + wantObj: "func (pkg/test.g[pkg/test.x]).Method(t pkg/test.x) string", + wantSig: "func(recv pkg/test.g[pkg/test.x], t pkg/test.x) string", + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + f := srctesting.New(t) + file := f.Parse("test.go", test.src) + info, pkg := f.Check("pkg/test", file) + + method := srctesting.LookupObj(pkg, "g.Method").(*types.Func).Type().(*types.Signature) + resolver := NewResolver(nil, ToSlice(method.RecvTypeParams()), []types.Type{srctesting.LookupObj(pkg, "x").Type()}) + + if l := len(info.Selections); l != 1 { + t.Fatalf("Got: %d selections. Want: 1", l) + } + for _, sel := range info.Selections { + gotObj := types.ObjectString(resolver.SubstituteSelection(sel).Obj(), nil) + if gotObj != test.wantObj { + t.Fatalf("Got: resolver.SubstituteSelection().Obj() = %q. Want: %q.", gotObj, test.wantObj) + } + gotSig := types.TypeString(resolver.SubstituteSelection(sel).Type(), nil) + if gotSig != test.wantSig { + t.Fatalf("Got: resolver.SubstituteSelection().Type() = %q. Want: %q.", gotSig, test.wantSig) + } + } + }) + } +} diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go new file mode 100644 index 000000000..87240c077 --- /dev/null +++ b/compiler/internal/typeparams/instance.go @@ -0,0 +1,169 @@ +package typeparams + +import ( + "fmt" + "go/types" + + "github.com/gopherjs/gopherjs/compiler/internal/symbol" + "github.com/gopherjs/gopherjs/compiler/typesutil" +) + +// Instance of a generic type or function. +// +// Non-generic objects can be represented as an Instance with zero type params, +// they are instances of themselves. +type Instance struct { + Object types.Object // Object to be instantiated. + TArgs typesutil.TypeList // Type params to instantiate with. +} + +// String returns a string representation of the Instance. +// +// Two semantically different instances may have the same string representation +// if the instantiated object or its type arguments shadow other types. +func (i *Instance) String() string { + sym := symbol.New(i.Object).String() + if len(i.TArgs) == 0 { + return sym + } + + return fmt.Sprintf("%s<%s>", sym, i.TArgs) +} + +// TypeString returns a Go type string representing the instance (suitable for %T verb). +func (i *Instance) TypeString() string { + tArgs := "" + if len(i.TArgs) > 0 { + tArgs = "[" + i.TArgs.String() + "]" + } + return fmt.Sprintf("%s.%s%s", i.Object.Pkg().Name(), i.Object.Name(), tArgs) +} + +// IsTrivial returns true if this is an instance of a non-generic object. +func (i *Instance) IsTrivial() bool { + return len(i.TArgs) == 0 +} + +// Recv returns an instance of the receiver type of a method. +// +// Returns zero value if not a method. +func (i *Instance) Recv() Instance { + sig, ok := i.Object.Type().(*types.Signature) + if !ok { + return Instance{} + } + recv := typesutil.RecvType(sig) + if recv == nil { + return Instance{} + } + return Instance{ + Object: recv.Obj(), + TArgs: i.TArgs, + } +} + +// InstanceSet allows collecting and processing unique Instances. +// +// Each Instance may be added to the set any number of times, but it will be +// returned for processing exactly once. Processing order is not specified. +type InstanceSet struct { + values []Instance + unprocessed int // Index in values for the next unprocessed element. + seen InstanceMap[int] // Maps instance to a unique numeric id. +} + +// Add instances to the set. Instances that have been previously added to the +// set won't be requeued for processing regardless of whether they have been +// processed already. +func (iset *InstanceSet) Add(instances ...Instance) *InstanceSet { + for _, inst := range instances { + if iset.seen.Has(inst) { + continue + } + iset.seen.Set(inst, iset.seen.Len()) + iset.values = append(iset.values, inst) + } + return iset +} + +// ID returns a unique numeric identifier assigned to an instance in the set. +// The ID is guaranteed to be unique among all instances of the same object +// within a given program. The ID will be consistent, as long as instances are +// added to the set in the same order. +// +// In order to have an ID assigned, the instance must have been previously added +// to the set. +// +// Note: these ids are used in the generated code as keys to the specific +// type/function instantiation in the type/function object. Using this has two +// advantages: +// +// - More compact generated code compared to string keys derived from type args. +// +// - Collision avoidance in case of two different types having the same name due +// to shadowing. +// +// Here's an example where it's very difficult to assign non-colliding +// name-based keys to the two different types T: +// +// func foo() { +// type T int +// { type T string } // Code block creates a new nested scope allowing for shadowing. +// } +func (iset *InstanceSet) ID(inst Instance) int { + id, ok := iset.seen.get(inst) + if !ok { + panic(fmt.Errorf("requesting ID of instance %v that hasn't been added to the set", inst)) + } + return id +} + +// next returns the next Instance to be processed. +// +// If there are no unprocessed instances, the second returned value will be false. +func (iset *InstanceSet) next() (Instance, bool) { + if iset.exhausted() { + return Instance{}, false + } + next := iset.values[iset.unprocessed] + iset.unprocessed++ + return next, true +} + +// exhausted returns true if there are no unprocessed instances in the set. +func (iset *InstanceSet) exhausted() bool { return len(iset.values) <= iset.unprocessed } + +// Values returns instances that are currently in the set. Order is not specified. +func (iset *InstanceSet) Values() []Instance { + return iset.values +} + +// PackageInstanceSets stores an InstanceSet for each package in a program, keyed +// by import path. +type PackageInstanceSets map[string]*InstanceSet + +// Pkg returns InstanceSet for objects defined in the given package. +func (i PackageInstanceSets) Pkg(pkg *types.Package) *InstanceSet { + path := pkg.Path() + iset, ok := i[path] + if !ok { + iset = &InstanceSet{} + i[path] = iset + } + return iset +} + +// Add instances to the appropriate package's set. Automatically initialized +// new per-package sets upon a first encounter. +func (i PackageInstanceSets) Add(instances ...Instance) { + for _, inst := range instances { + i.Pkg(inst.Object.Pkg()).Add(inst) + } +} + +// ID returns a unique numeric identifier assigned to an instance in the set. +// +// See: InstanceSet.ID(). +func (i PackageInstanceSets) ID(inst Instance) int { + return i.Pkg(inst.Object.Pkg()).ID(inst) +} diff --git a/compiler/internal/typeparams/instance_test.go b/compiler/internal/typeparams/instance_test.go new file mode 100644 index 000000000..154e95b82 --- /dev/null +++ b/compiler/internal/typeparams/instance_test.go @@ -0,0 +1,266 @@ +package typeparams + +import ( + "go/types" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/gopherjs/gopherjs/internal/srctesting" + "github.com/gopherjs/gopherjs/internal/testingx" +) + +func instanceOpts() cmp.Options { + return cmp.Options{ + // Instances are represented by their IDs for diffing purposes. + cmp.Transformer("Instance", func(i Instance) string { + return i.String() + }), + // Order of instances in a slice doesn't matter, sort them by ID. + cmpopts.SortSlices(func(a, b Instance) bool { + return a.String() < b.String() + }), + } +} + +func TestInstanceString(t *testing.T) { + const src = `package testcase + + type Ints []int + + type Typ[T any, V any] []T + func (t Typ[T, V]) Method(x T) {} + + type typ[T any, V any] []T + func (t typ[T, V]) method(x T) {} + + func Fun[U any, W any](x, y U) {} + func fun[U any, W any](x, y U) {} + ` + f := srctesting.New(t) + _, pkg := f.Check("pkg/test", f.Parse("test.go", src)) + mustType := testingx.Must[types.Type](t) + + tests := []struct { + descr string + instance Instance + wantStr string + wantTypeString string + }{{ + descr: "exported type", + instance: Instance{ + Object: pkg.Scope().Lookup("Typ"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + wantStr: "pkg/test.Typ", + wantTypeString: "testcase.Typ[int, string]", + }, { + descr: "exported method", + instance: Instance{ + Object: pkg.Scope().Lookup("Typ").Type().(*types.Named).Method(0), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + wantStr: "pkg/test.Typ.Method", + }, { + descr: "exported function", + instance: Instance{ + Object: pkg.Scope().Lookup("Fun"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + wantStr: "pkg/test.Fun", + }, { + descr: "unexported type", + instance: Instance{ + Object: pkg.Scope().Lookup("typ"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + wantStr: "pkg/test.typ", + wantTypeString: "testcase.typ[int, string]", + }, { + descr: "unexported method", + instance: Instance{ + Object: pkg.Scope().Lookup("typ").Type().(*types.Named).Method(0), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + wantStr: "pkg/test.typ.method", + }, { + descr: "unexported function", + instance: Instance{ + Object: pkg.Scope().Lookup("fun"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + wantStr: "pkg/test.fun", + }, { + descr: "no type params", + instance: Instance{ + Object: pkg.Scope().Lookup("Ints"), + }, + wantStr: "pkg/test.Ints", + wantTypeString: "testcase.Ints", + }, { + descr: "complex parameter type", + instance: Instance{ + Object: pkg.Scope().Lookup("fun"), + TArgs: []types.Type{ + types.NewSlice(types.Typ[types.Int]), + mustType(types.Instantiate(nil, pkg.Scope().Lookup("typ").Type(), []types.Type{ + types.Typ[types.Int], + types.Typ[types.String], + }, true)), + }, + }, + wantStr: "pkg/test.fun<[]int, pkg/test.typ[int, string]>", + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + got := test.instance.String() + if got != test.wantStr { + t.Errorf("Got: instance string %q. Want: %q.", got, test.wantStr) + } + if test.wantTypeString != "" { + got = test.instance.TypeString() + if got != test.wantTypeString { + t.Errorf("Got: instance type string %q. Want: %q.", got, test.wantTypeString) + } + } + }) + } +} + +func TestInstanceQueue(t *testing.T) { + const src = `package test + type Typ[T any, V any] []T + func Fun[U any, W any](x, y U) {} + ` + f := srctesting.New(t) + _, pkg := f.Check("pkg/test", f.Parse("test.go", src)) + + i1 := Instance{ + Object: pkg.Scope().Lookup("Typ"), + TArgs: []types.Type{types.Typ[types.String], types.Typ[types.String]}, + } + i2 := Instance{ + Object: pkg.Scope().Lookup("Typ"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.Int]}, + } + i3 := Instance{ + Object: pkg.Scope().Lookup("Fun"), + TArgs: []types.Type{types.Typ[types.String], types.Typ[types.String]}, + } + + set := InstanceSet{} + set.Add(i1, i2) + + if ex := set.exhausted(); ex { + t.Errorf("Got: set.exhausted() = true. Want: false") + } + + gotValues := set.Values() + wantValues := []Instance{i1, i2} + if diff := cmp.Diff(wantValues, gotValues, instanceOpts()); diff != "" { + t.Errorf("set.Values() returned diff (-want,+got):\n%s", diff) + } + + p1, ok := set.next() + if !ok { + t.Errorf("Got: _, ok := set.next(); ok == false. Want: true.") + } + p2, ok := set.next() + if !ok { + t.Errorf("Got: _, ok := set.next(); ok == false. Want: true.") + } + if ex := set.exhausted(); !ex { + t.Errorf("Got: set.exhausted() = false. Want: true") + } + + _, ok = set.next() + if ok { + t.Errorf("Got: _, ok := set.next(); ok == true. Want: false.") + } + + set.Add(i1) // Has been enqueued before. + if ex := set.exhausted(); !ex { + t.Errorf("Got: set.exhausted() = false. Want: true") + } + + set.Add(i3) + p3, ok := set.next() + if !ok { + t.Errorf("Got: _, ok := set.next(); ok == false. Want: true.") + } + + added := []Instance{i1, i2, i3} + processed := []Instance{p1, p2, p3} + + diff := cmp.Diff(added, processed, instanceOpts()) + if diff != "" { + t.Errorf("Processed instances differ from added (-want,+got):\n%s", diff) + } + + gotValues = set.Values() + wantValues = []Instance{i1, i2, i3} + if diff := cmp.Diff(wantValues, gotValues, instanceOpts()); diff != "" { + t.Errorf("set.Values() returned diff (-want,+got):\n%s", diff) + } +} + +func TestInstancesByPackage(t *testing.T) { + f := srctesting.New(t) + + const src1 = `package foo + type Typ[T any, V any] []T + ` + _, foo := f.Check("pkg/foo", f.Parse("foo.go", src1)) + + const src2 = `package bar + func Fun[U any, W any](x, y U) {} + ` + _, bar := f.Check("pkg/bar", f.Parse("bar.go", src2)) + + i1 := Instance{ + Object: foo.Scope().Lookup("Typ"), + TArgs: []types.Type{types.Typ[types.String], types.Typ[types.String]}, + } + i2 := Instance{ + Object: foo.Scope().Lookup("Typ"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.Int]}, + } + i3 := Instance{ + Object: bar.Scope().Lookup("Fun"), + TArgs: []types.Type{types.Typ[types.String], types.Typ[types.String]}, + } + + t.Run("Add", func(t *testing.T) { + instByPkg := PackageInstanceSets{} + instByPkg.Add(i1, i2, i3) + + gotFooInstances := instByPkg.Pkg(foo).Values() + wantFooInstances := []Instance{i1, i2} + if diff := cmp.Diff(wantFooInstances, gotFooInstances, instanceOpts()); diff != "" { + t.Errorf("instByPkg.Pkg(foo).Values() returned diff (-want,+got):\n%s", diff) + } + + gotValues := instByPkg.Pkg(bar).Values() + wantValues := []Instance{i3} + if diff := cmp.Diff(wantValues, gotValues, instanceOpts()); diff != "" { + t.Errorf("instByPkg.Pkg(bar).Values() returned diff (-want,+got):\n%s", diff) + } + }) + + t.Run("ID", func(t *testing.T) { + instByPkg := PackageInstanceSets{} + instByPkg.Add(i1, i2, i3) + + got := []int{ + instByPkg.ID(i1), + instByPkg.ID(i2), + instByPkg.ID(i3), + } + want := []int{0, 1, 0} + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("unexpected instance IDs assigned (-want,+got):\n%s", diff) + } + }) +} diff --git a/compiler/internal/typeparams/map.go b/compiler/internal/typeparams/map.go new file mode 100644 index 000000000..aa16130e2 --- /dev/null +++ b/compiler/internal/typeparams/map.go @@ -0,0 +1,135 @@ +package typeparams + +import ( + "go/types" + "sync" + + "golang.org/x/tools/go/types/typeutil" +) + +type ( + mapEntry[V any] struct { + key Instance + value V + } + mapBucket[V any] []*mapEntry[V] + mapBuckets[V any] map[uint32]mapBucket[V] +) + +// InstanceMap implements a map-like data structure keyed by instances. +// +// Zero value is an equivalent of an empty map. Methods are not thread-safe. +// +// Since Instance contains a slice and is not comparable, it can not be used as +// a regular map key, but we can compare its fields manually. When comparing +// instance equality, objects are compared by pointer equality, and type +// arguments with types.Identical(). To reduce access complexity, we bucket +// entries by a combined hash of type args. This type is generally inspired by +// typeutil.Map. +type InstanceMap[V any] struct { + bootstrap sync.Once + data map[types.Object]mapBuckets[V] + len int + hasher typeutil.Hasher + zero V +} + +func (im *InstanceMap[V]) init() { + im.bootstrap.Do(func() { + im.data = map[types.Object]mapBuckets[V]{} + im.hasher = typeutil.MakeHasher() + }) +} + +func (im *InstanceMap[V]) get(key Instance) (V, bool) { + im.init() + + buckets, ok := im.data[key.Object] + if !ok { + return im.zero, false + } + bucket := buckets[typeHash(im.hasher, key.TArgs...)] + if len(bucket) == 0 { + return im.zero, false + } + + for _, candidate := range bucket { + if typeArgsEq(candidate.key.TArgs, key.TArgs) { + return candidate.value, true + } + } + return im.zero, false +} + +// Get returns the stored value for the provided key. If the key is missing from +// the map, zero value is returned. +func (im *InstanceMap[V]) Get(key Instance) V { + val, _ := im.get(key) + return val +} + +// Has returns true if the given key is present in the map. +func (im *InstanceMap[V]) Has(key Instance) bool { + _, ok := im.get(key) + return ok +} + +// Set new value for the key in the map. Returns the previous value that was +// stored in the map, or zero value if the key wasn't present before. +func (im *InstanceMap[V]) Set(key Instance, value V) (old V) { + im.init() + + if _, ok := im.data[key.Object]; !ok { + im.data[key.Object] = mapBuckets[V]{} + } + bucketID := typeHash(im.hasher, key.TArgs...) + + // If there is already an identical key in the map, override the entry value. + for _, candidate := range im.data[key.Object][bucketID] { + if typeArgsEq(candidate.key.TArgs, key.TArgs) { + old = candidate.value + candidate.value = value + return old + } + } + + // Otherwise append a new entry. + im.data[key.Object][bucketID] = append(im.data[key.Object][bucketID], &mapEntry[V]{ + key: key, + value: value, + }) + im.len++ + return im.zero +} + +// Len returns the number of elements in the map. +func (im *InstanceMap[V]) Len() int { + return im.len +} + +// typeHash returns a combined hash of several types. +// +// Provided hasher is used to compute hashes of individual types, which are +// xor'ed together. Xor preserves bit distribution property, so the combined +// hash should be as good for bucketing, as the original. +func typeHash(hasher typeutil.Hasher, types ...types.Type) uint32 { + var hash uint32 + for _, typ := range types { + hash ^= hasher.Hash(typ) + } + return hash +} + +// typeArgsEq returns if both lists of type arguments are identical. +func typeArgsEq(a, b []types.Type) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !types.Identical(a[i], b[i]) { + return false + } + } + + return true +} diff --git a/compiler/internal/typeparams/map_test.go b/compiler/internal/typeparams/map_test.go new file mode 100644 index 000000000..5018ab0d8 --- /dev/null +++ b/compiler/internal/typeparams/map_test.go @@ -0,0 +1,111 @@ +package typeparams + +import ( + "go/token" + "go/types" + "testing" +) + +func TestInstanceMap(t *testing.T) { + i1 := Instance{ + Object: types.NewTypeName(token.NoPos, nil, "i1", nil), + TArgs: []types.Type{ + types.Typ[types.Int], + types.Typ[types.Int8], + }, + } + i1clone := Instance{ + Object: i1.Object, + TArgs: []types.Type{ + types.Typ[types.Int], + types.Typ[types.Int8], + }, + } + + i2 := Instance{ + Object: types.NewTypeName(token.NoPos, nil, "i2", nil), // Different pointer. + TArgs: []types.Type{ + types.Typ[types.Int], + types.Typ[types.Int8], + }, + } + i3 := Instance{ + Object: i1.Object, + TArgs: []types.Type{types.Typ[types.String]}, // Different type args. + } + + _ = i1 + _ = i1clone + _ = i3 + _ = i2 + + m := InstanceMap[string]{} + + // Check operations on a missing key. + t.Run("empty", func(t *testing.T) { + if got := m.Has(i1); got { + t.Errorf("Got: empty map contains %s. Want: empty map contains nothing.", i1) + } + if got := m.Get(i1); got != "" { + t.Errorf("Got: getting missing key returned %q. Want: zero value.", got) + } + if got := m.Len(); got != 0 { + t.Errorf("Got: empty map length %d. Want: 0.", got) + } + if got := m.Set(i1, "abc"); got != "" { + t.Errorf("Got: setting a new key returned old value %q. Want: zero value", got) + } + if got := m.Len(); got != 1 { + t.Errorf("Got: map length %d. Want: 1.", got) + } + }) + + // Check operations on the existing key. + t.Run("first key", func(t *testing.T) { + if got := m.Set(i1, "def"); got != "abc" { + t.Errorf(`Got: setting an existing key returned old value %q. Want: "abc".`, got) + } + if got := m.Len(); got != 1 { + t.Errorf("Got: map length %d. Want: 1.", got) + } + if got := m.Has(i1); !got { + t.Errorf("Got: set map key is reported as missing. Want: key present.") + } + if got := m.Get(i1); got != "def" { + t.Errorf(`Got: getting set key returned %q. Want: "def"`, got) + } + if got := m.Get(i1clone); got != "def" { + t.Errorf(`Got: getting set key returned %q. Want: "def"`, got) + } + }) + + // Check for key collisions. + t.Run("different object", func(t *testing.T) { + if got := m.Has(i2); got { + t.Errorf("Got: a new key %q is reported as present. Want: not present.", i2) + } + if got := m.Set(i2, "123"); got != "" { + t.Errorf("Got: a new key %q overrode an old value %q. Want: zero value.", i2, got) + } + if got := m.Get(i2); got != "123" { + t.Errorf(`Got: getting set key %q returned: %q. Want: "123"`, i2, got) + } + if got := m.Len(); got != 2 { + t.Errorf("Got: map length %d. Want: 2.", got) + } + }) + t.Run("different tArgs", func(t *testing.T) { + if got := m.Has(i3); got { + t.Errorf("Got: a new key %q is reported as present. Want: not present.", i3) + } + if got := m.Set(i3, "456"); got != "" { + t.Errorf("Got: a new key %q overrode an old value %q. Want: zero value.", i3, got) + } + if got := m.Get(i3); got != "456" { + t.Errorf(`Got: getting set key %q returned: %q. Want: "456"`, i3, got) + } + if got := m.Len(); got != 3 { + t.Errorf("Got: map length %d. Want: 3.", got) + } + }) +} diff --git a/compiler/internal/typeparams/utils.go b/compiler/internal/typeparams/utils.go new file mode 100644 index 000000000..d473f9b5c --- /dev/null +++ b/compiler/internal/typeparams/utils.go @@ -0,0 +1,48 @@ +package typeparams + +import ( + "errors" + "fmt" + "go/types" +) + +// SignatureTypeParams returns receiver type params for methods, or function +// type params for standalone functions, or nil for non-generic functions and +// methods. +func SignatureTypeParams(sig *types.Signature) *types.TypeParamList { + if tp := sig.RecvTypeParams(); tp != nil { + return tp + } else if tp := sig.TypeParams(); tp != nil { + return tp + } else { + return nil + } +} + +var ( + errInstantiatesGenerics = errors.New("instantiates generic type or function") + errDefinesGenerics = errors.New("defines generic type or function") +) + +// RequiresGenericsSupport returns an error if the type-checked code depends on +// generics support. +func RequiresGenericsSupport(info *types.Info) error { + type withTypeParams interface{ TypeParams() *types.TypeParamList } + + for ident := range info.Instances { + // Any instantiation means dependency on generics. + return fmt.Errorf("%w: %v", errInstantiatesGenerics, info.ObjectOf(ident)) + } + + for _, obj := range info.Defs { + if obj == nil { + continue + } + typ, ok := obj.Type().(withTypeParams) + if ok && typ.TypeParams().Len() > 0 { + return fmt.Errorf("%w: %v", errDefinesGenerics, obj) + } + } + + return nil +} diff --git a/compiler/internal/typeparams/utils_test.go b/compiler/internal/typeparams/utils_test.go new file mode 100644 index 000000000..2ef82e52f --- /dev/null +++ b/compiler/internal/typeparams/utils_test.go @@ -0,0 +1,65 @@ +package typeparams + +import ( + "errors" + "testing" + + "github.com/gopherjs/gopherjs/internal/srctesting" +) + +func TestRequiresGenericsSupport(t *testing.T) { + t.Run("generic func", func(t *testing.T) { + f := srctesting.New(t) + src := `package foo + func foo[T any](t T) {}` + info, _ := f.Check("pkg/foo", f.Parse("foo.go", src)) + + err := RequiresGenericsSupport(info) + if !errors.Is(err, errDefinesGenerics) { + t.Errorf("Got: RequiresGenericsSupport() = %v. Want: %v", err, errDefinesGenerics) + } + }) + + t.Run("generic type", func(t *testing.T) { + f := srctesting.New(t) + src := `package foo + type Foo[T any] struct{t T}` + info, _ := f.Check("pkg/foo", f.Parse("foo.go", src)) + + err := RequiresGenericsSupport(info) + if !errors.Is(err, errDefinesGenerics) { + t.Errorf("Got: RequiresGenericsSupport() = %v. Want: %v", err, errDefinesGenerics) + } + }) + + t.Run("imported generic instance", func(t *testing.T) { + f := srctesting.New(t) + f.Info = nil // Do not combine type checking info from different packages. + src1 := `package foo + type Foo[T any] struct{t T}` + f.Check("pkg/foo", f.Parse("foo.go", src1)) + + src2 := `package bar + import "pkg/foo" + func bar() { _ = foo.Foo[int]{} }` + info, _ := f.Check("pkg/bar", f.Parse("bar.go", src2)) + + err := RequiresGenericsSupport(info) + if !errors.Is(err, errInstantiatesGenerics) { + t.Errorf("Got: RequiresGenericsSupport() = %v. Want: %v", err, errInstantiatesGenerics) + } + }) + + t.Run("no generic usage", func(t *testing.T) { + f := srctesting.New(t) + src := `package foo + type Foo struct{} + func foo() { _ = Foo{} }` + info, _ := f.Check("pkg/foo", f.Parse("foo.go", src)) + + err := RequiresGenericsSupport(info) + if err != nil { + t.Errorf("Got: RequiresGenericsSupport() = %v. Want: nil", err) + } + }) +} diff --git a/compiler/linkname.go b/compiler/linkname.go index 29f91fbd0..6dd93a709 100644 --- a/compiler/linkname.go +++ b/compiler/linkname.go @@ -4,10 +4,10 @@ import ( "fmt" "go/ast" "go/token" - "go/types" "strings" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/symbol" ) // GoLinkname describes a go:linkname compiler directive found in the source code. @@ -17,62 +17,8 @@ import ( // symbols referencing it. This is subtly different from the upstream Go // implementation, which simply overrides symbol name the linker will use. type GoLinkname struct { - Implementation SymName - Reference SymName -} - -// SymName uniquely identifies a named submol within a program. -// -// This is a logical equivalent of a symbol name used by traditional linkers. -// The following properties should hold true: -// -// - Each named symbol within a program has a unique SymName. -// - Similarly named methods of different types will have different symbol names. -// - The string representation is opaque and should not be attempted to reversed -// to a struct form. -type SymName struct { - PkgPath string // Full package import path. - Name string // Symbol name. -} - -// newSymName constructs SymName for a given named symbol. -func newSymName(o types.Object) SymName { - if fun, ok := o.(*types.Func); ok { - sig := fun.Type().(*types.Signature) - if recv := sig.Recv(); recv != nil { - // Special case: disambiguate names for different types' methods. - typ := recv.Type() - if ptr, ok := typ.(*types.Pointer); ok { - return SymName{ - PkgPath: o.Pkg().Path(), - Name: "(*" + ptr.Elem().(*types.Named).Obj().Name() + ")." + o.Name(), - } - } - return SymName{ - PkgPath: o.Pkg().Path(), - Name: typ.(*types.Named).Obj().Name() + "." + o.Name(), - } - } - } - return SymName{ - PkgPath: o.Pkg().Path(), - Name: o.Name(), - } -} - -func (n SymName) String() string { return n.PkgPath + "." + n.Name } - -func (n SymName) IsMethod() (recv string, method string, ok bool) { - pos := strings.IndexByte(n.Name, '.') - if pos == -1 { - return - } - recv, method, ok = n.Name[:pos], n.Name[pos+1:], true - size := len(recv) - if size > 2 && recv[0] == '(' && recv[size-1] == ')' { - recv = recv[1 : size-1] - } - return + Implementation symbol.Name + Reference symbol.Name } // readLinknameFromComment reads the given comment to determine if it's a go:linkname @@ -119,15 +65,15 @@ func readLinknameFromComment(pkgPath string, comment *ast.Comment) (*GoLinkname, } return &GoLinkname{ - Reference: SymName{PkgPath: localPkg, Name: localName}, - Implementation: SymName{PkgPath: extPkg, Name: extName}, + Reference: symbol.Name{PkgPath: localPkg, Name: localName}, + Implementation: symbol.Name{PkgPath: extPkg, Name: extName}, }, nil } // isMitigatedVarLinkname checks if the given go:linkname directive on // a variable, which GopherJS doesn't support, is known about. // We silently ignore such directives, since it doesn't seem to cause any problems. -func isMitigatedVarLinkname(sym SymName) bool { +func isMitigatedVarLinkname(sym symbol.Name) bool { mitigatedLinks := map[string]bool{ `reflect.zeroVal`: true, `math/bits.overflowError`: true, // Defaults in bits_errors_bootstrap.go @@ -140,7 +86,7 @@ func isMitigatedVarLinkname(sym SymName) bool { // on a function, where the function has a body, is known about. // These are unsupported "insert"-style go:linkname directives, // that we ignore as a link and handle case-by-case in native overrides. -func isMitigatedInsertLinkname(sym SymName) bool { +func isMitigatedInsertLinkname(sym symbol.Name) bool { mitigatedPkg := map[string]bool{ `runtime`: true, // Lots of "insert"-style links `internal/fuzz`: true, // Defaults to no-op stubs @@ -216,23 +162,23 @@ func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]Go } } - return directives, errs.Normalize() + return directives, errs.ErrOrNil() } // goLinknameSet is a utility that enables quick lookup of whether a decl is // affected by any go:linkname directive in the program. type goLinknameSet struct { - byImplementation map[SymName][]GoLinkname - byReference map[SymName]GoLinkname + byImplementation map[symbol.Name][]GoLinkname + byReference map[symbol.Name]GoLinkname } // Add more GoLinkname directives into the set. func (gls *goLinknameSet) Add(entries []GoLinkname) error { if gls.byImplementation == nil { - gls.byImplementation = map[SymName][]GoLinkname{} + gls.byImplementation = map[symbol.Name][]GoLinkname{} } if gls.byReference == nil { - gls.byReference = map[SymName]GoLinkname{} + gls.byReference = map[symbol.Name]GoLinkname{} } for _, e := range entries { gls.byImplementation[e.Implementation] = append(gls.byImplementation[e.Implementation], e) @@ -247,7 +193,7 @@ func (gls *goLinknameSet) Add(entries []GoLinkname) error { // IsImplementation returns true if there is a directive referencing this symbol // as an implementation. -func (gls *goLinknameSet) IsImplementation(sym SymName) bool { +func (gls *goLinknameSet) IsImplementation(sym symbol.Name) bool { _, found := gls.byImplementation[sym] return found } @@ -255,7 +201,7 @@ func (gls *goLinknameSet) IsImplementation(sym SymName) bool { // FindImplementation returns a symbol name, which provides the implementation // for the given symbol. The second value indicates whether the implementation // was found. -func (gls *goLinknameSet) FindImplementation(sym SymName) (SymName, bool) { +func (gls *goLinknameSet) FindImplementation(sym symbol.Name) (symbol.Name, bool) { directive, found := gls.byReference[sym] return directive.Implementation, found } diff --git a/compiler/linkname_test.go b/compiler/linkname_test.go index a792ee2bc..614c1ff5b 100644 --- a/compiler/linkname_test.go +++ b/compiler/linkname_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/gopherjs/gopherjs/compiler/internal/symbol" ) func parseSource(t *testing.T, src string) (*ast.File, *token.FileSet) { @@ -41,49 +42,6 @@ func makePackage(t *testing.T, src string) *types.Package { return pkg } -func TestSymName(t *testing.T) { - pkg := makePackage(t, - `package testcase - - func AFunction() {} - type AType struct {} - func (AType) AMethod() {} - func (*AType) APointerMethod() {} - var AVariable int32 - `) - - tests := []struct { - obj types.Object - want SymName - }{ - { - obj: pkg.Scope().Lookup("AFunction"), - want: SymName{PkgPath: "testcase", Name: "AFunction"}, - }, { - obj: pkg.Scope().Lookup("AType"), - want: SymName{PkgPath: "testcase", Name: "AType"}, - }, { - obj: types.NewMethodSet(pkg.Scope().Lookup("AType").Type()).Lookup(pkg, "AMethod").Obj(), - want: SymName{PkgPath: "testcase", Name: "AType.AMethod"}, - }, { - obj: types.NewMethodSet(types.NewPointer(pkg.Scope().Lookup("AType").Type())).Lookup(pkg, "APointerMethod").Obj(), - want: SymName{PkgPath: "testcase", Name: "(*AType).APointerMethod"}, - }, { - obj: pkg.Scope().Lookup("AVariable"), - want: SymName{PkgPath: "testcase", Name: "AVariable"}, - }, - } - - for _, test := range tests { - t.Run(test.obj.Name(), func(t *testing.T) { - got := newSymName(test.obj) - if got != test.want { - t.Errorf("NewSymName(%q) returned %#v, want: %#v", test.obj.Name(), got, test.want) - } - }) - } -} - func TestParseGoLinknames(t *testing.T) { tests := []struct { desc string @@ -115,8 +73,8 @@ func TestParseGoLinknames(t *testing.T) { `, wantDirectives: []GoLinkname{ { - Reference: SymName{PkgPath: "testcase", Name: "a"}, - Implementation: SymName{PkgPath: "other/package", Name: "testcase_a"}, + Reference: symbol.Name{PkgPath: "testcase", Name: "a"}, + Implementation: symbol.Name{PkgPath: "other/package", Name: "testcase_a"}, }, }, }, { @@ -133,11 +91,11 @@ func TestParseGoLinknames(t *testing.T) { `, wantDirectives: []GoLinkname{ { - Reference: SymName{PkgPath: "testcase", Name: "a"}, - Implementation: SymName{PkgPath: "other/package", Name: "a"}, + Reference: symbol.Name{PkgPath: "testcase", Name: "a"}, + Implementation: symbol.Name{PkgPath: "other/package", Name: "a"}, }, { - Reference: SymName{PkgPath: "testcase", Name: "b"}, - Implementation: SymName{PkgPath: "other/package", Name: "b"}, + Reference: symbol.Name{PkgPath: "testcase", Name: "b"}, + Implementation: symbol.Name{PkgPath: "other/package", Name: "b"}, }, }, }, { diff --git a/compiler/natives/src/os/file.go b/compiler/natives/src/os/file.go index 37d4275f3..a3683b8b0 100644 --- a/compiler/natives/src/os/file.go +++ b/compiler/natives/src/os/file.go @@ -3,7 +3,7 @@ package os -// WriteString copied from Go 1.16, before it was made more peformant, and unsafe. +// WriteString copied from Go 1.16, before it was made more performant, and unsafe. func (f *File) WriteString(s string) (n int, err error) { return f.Write([]byte(s)) } diff --git a/compiler/natives/src/runtime/runtime.go b/compiler/natives/src/runtime/runtime.go index 41c60876c..9f8425af8 100644 --- a/compiler/natives/src/runtime/runtime.go +++ b/compiler/natives/src/runtime/runtime.go @@ -468,7 +468,7 @@ func StartTrace() error { return nil } func StopTrace() {} func ReadTrace() []byte -// We fake a cgo environment to catch errors. Therefor we have to implement this and always return 0 +// We fake a cgo environment to catch errors. Therefore we have to implement this and always return 0 func NumCgoCall() int64 { return 0 } diff --git a/compiler/natives/src/strings/strings.go b/compiler/natives/src/strings/strings.go index ebb1db1ef..2867872f6 100644 --- a/compiler/natives/src/strings/strings.go +++ b/compiler/natives/src/strings/strings.go @@ -68,7 +68,7 @@ func (b *Builder) copyCheck() { } func Clone(s string) string { - // Since in the JavaScript runtime we don't have access the the string's + // Since in the JavaScript runtime we don't have access the string's // baking memory, we let the engine's garbage collector deal with substring // memory overheads and simply return the string as-is. return s diff --git a/compiler/package.go b/compiler/package.go index ad918ba3e..f82cd798a 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -6,7 +6,6 @@ import ( "fmt" "go/ast" "go/constant" - "go/scanner" "go/token" "go/types" "sort" @@ -15,7 +14,10 @@ import ( "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" - "github.com/neelance/astrewrite" + "github.com/gopherjs/gopherjs/compiler/internal/symbol" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/typesutil" + "github.com/gopherjs/gopherjs/internal/experiments" "golang.org/x/tools/go/gcexportdata" "golang.org/x/tools/go/types/typeutil" ) @@ -23,11 +25,11 @@ import ( // pkgContext maintains compiler context for a specific package. type pkgContext struct { *analysis.Info - additionalSelections map[*ast.SelectorExpr]selection + additionalSelections map[*ast.SelectorExpr]typesutil.Selection - typeNames []*types.TypeName + typesCtx *types.Context + typeNames typesutil.TypeNames pkgVars map[string]string - objectNames map[types.Object]string varPtrNames map[*types.Var]string anonTypes []*types.TypeName anonTypeMap typeutil.Map @@ -37,56 +39,66 @@ type pkgContext struct { minify bool fileSet *token.FileSet errList ErrorList + instanceSet *typeparams.PackageInstanceSets } -func (p *pkgContext) SelectionOf(e *ast.SelectorExpr) (selection, bool) { - if sel, ok := p.Selections[e]; ok { - return sel, true - } - if sel, ok := p.additionalSelections[e]; ok { - return sel, true - } - return nil, false -} - -type selection interface { - Kind() types.SelectionKind - Recv() types.Type - Index() []int - Obj() types.Object - Type() types.Type -} - -type fakeSelection struct { - kind types.SelectionKind - recv types.Type - index []int - obj types.Object - typ types.Type -} - -func (sel *fakeSelection) Kind() types.SelectionKind { return sel.kind } -func (sel *fakeSelection) Recv() types.Type { return sel.recv } -func (sel *fakeSelection) Index() []int { return sel.index } -func (sel *fakeSelection) Obj() types.Object { return sel.obj } -func (sel *fakeSelection) Type() types.Type { return sel.typ } - -// funcContext maintains compiler context for a specific function (lexical scope?). +// funcContext maintains compiler context for a specific function. +// +// An instance of this type roughly corresponds to a lexical scope for generated +// JavaScript code (as defined for `var` declarations). type funcContext struct { *analysis.FuncInfo - pkgCtx *pkgContext - parent *funcContext - sig *types.Signature - allVars map[string]int - localVars []string - resultNames []ast.Expr - flowDatas map[*types.Label]*flowData - caseCounter int - labelCases map[*types.Label]int - output []byte + // Surrounding package context. + pkgCtx *pkgContext + // Function context, surrounding this function definition. For package-level + // functions or methods it is the package-level function context (even though + // it technically doesn't correspond to a function). nil for the package-level + // function context. + parent *funcContext + // Signature of the function this context corresponds to or nil for the + // package-level function context. For generic functions it is the original + // generic signature to make sure result variable identity in the signature + // matches the variable objects referenced in the function body. + sig *typesutil.Signature + // All variable names available in the current function scope. The key is a Go + // variable name and the value is the number of synonymous variable names + // visible from this scope (e.g. due to shadowing). This number is used to + // avoid conflicts when assigning JS variable names for Go variables. + allVars map[string]int + // Local JS variable names defined within this function context. This list + // contains JS variable names assigned to Go variables, as well as other + // auxiliary variables the compiler needs. It is used to generate `var` + // declaration at the top of the function, as well as context save/restore. + localVars []string + // AST expressions representing function's named return values. nil if the + // function has no return values or they are not named. + resultNames []ast.Expr + // Function's internal control flow graph used for generation of a "flattened" + // version of the function when the function is blocking or uses goto. + // TODO(nevkontakte): Describe the exact semantics of this map. + flowDatas map[*types.Label]*flowData + // Number of control flow blocks in a "flattened" function. + caseCounter int + // A mapping from Go labels statements (e.g. labelled loop) to the flow block + // id corresponding to it. + labelCases map[*types.Label]int + // Generated code buffer for the current function. + output []byte + // Generated code that should be emitted at the end of the JS statement. delayedOutput []byte - posAvailable bool - pos token.Pos + // Set to true if source position is available and should be emitted for the + // source map. + posAvailable bool + // Current position in the Go source code. + pos token.Pos + // For each instantiation of a generic function or method, contains the + // current mapping between type parameters and corresponding type arguments. + // The mapping is used to determine concrete types for expressions within the + // instance's context. Can be nil outside of the generic context, in which + // case calling its methods is safe and simply does no substitution. + typeResolver *typeparams.Resolver + // Mapping from function-level objects to JS variable names they have been assigned. + objectNames map[types.Object]string } type flowData struct { @@ -95,34 +107,20 @@ type flowData struct { endCase int } +// ImportContext provides access to information about imported packages. type ImportContext struct { + // Mapping for an absolute import path to the package type information. Packages map[string]*types.Package - Import func(string) (*Archive, error) -} - -// packageImporter implements go/types.Importer interface. -type packageImporter struct { - importContext *ImportContext - importError *error // A pointer to importError in Compile. -} - -func (pi packageImporter) Import(path string) (*types.Package, error) { - if path == "unsafe" { - return types.Unsafe, nil - } - - a, err := pi.importContext.Import(path) - if err != nil { - if *pi.importError == nil { - // If import failed, show first error of import only (https://github.com/gopherjs/gopherjs/issues/119). - *pi.importError = err - } - return nil, err - } - - return pi.importContext.Packages[a.ImportPath], nil + // Import returns a previously compiled Archive for a dependency package. If + // the Import() call was successful, the corresponding entry must be added to + // the Packages map. + Import func(importPath string) (*Archive, error) } +// Compile the provided Go sources as a single package. +// +// Import path must be the absolute import path for a package. Provided sources +// are always sorted by name to ensure reproducible JavaScript output. func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, importContext *ImportContext, minify bool) (_ *Archive, err error) { defer func() { e := recover() @@ -139,84 +137,29 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor err = bailout(fmt.Errorf("unexpected compiler panic while building package %q: %v", importPath, e)) }() - // Files must be in the same order to get reproducible JS - sort.Slice(files, func(i, j int) bool { - return fileSet.File(files[i].Pos()).Name() > fileSet.File(files[j].Pos()).Name() - }) - - typesInfo := &types.Info{ - Types: make(map[ast.Expr]types.TypeAndValue), - Defs: make(map[*ast.Ident]types.Object), - Uses: make(map[*ast.Ident]types.Object), - Implicits: make(map[ast.Node]types.Object), - Selections: make(map[*ast.SelectorExpr]*types.Selection), - Scopes: make(map[ast.Node]*types.Scope), - } - - var errList ErrorList - - // Extract all go:linkname compiler directives from the package source. - goLinknames := []GoLinkname{} - for _, file := range files { - found, err := parseGoLinknames(fileSet, importPath, file) - if err != nil { - if errs, ok := err.(ErrorList); ok { - errList = append(errList, errs...) - } else { - errList = append(errList, err) - } - } - goLinknames = append(goLinknames, found...) - } + srcs := sources{ + ImportPath: importPath, + Files: files, + FileSet: fileSet, + }.Sort() - var importError error - var previousErr error - config := &types.Config{ - Importer: packageImporter{ - importContext: importContext, - importError: &importError, - }, - Sizes: sizes32, - Error: func(err error) { - if previousErr != nil && previousErr.Error() == err.Error() { - return - } - errList = append(errList, err) - previousErr = err - }, - } - typesPkg, err := config.Check(importPath, fileSet, files, typesInfo) - if importError != nil { - return nil, importError - } - if errList != nil { - if len(errList) > 10 { - pos := token.NoPos - if last, ok := errList[9].(types.Error); ok { - pos = last.Pos - } - errList = append(errList[:10], types.Error{Fset: fileSet, Pos: pos, Msg: "too many errors"}) - } - return nil, errList - } + tContext := types.NewContext() + typesInfo, typesPkg, err := srcs.TypeCheck(importContext, tContext) if err != nil { return nil, err } - importContext.Packages[importPath] = typesPkg - - exportData := new(bytes.Buffer) - if err := gcexportdata.Write(exportData, nil, typesPkg); err != nil { - return nil, fmt.Errorf("failed to write export data: %v", err) + if genErr := typeparams.RequiresGenericsSupport(typesInfo); genErr != nil && !experiments.Env.Generics { + return nil, fmt.Errorf("package %s requires generics support (https://github.com/gopherjs/gopherjs/issues/1013): %w", importPath, genErr) } - encodedFileSet := new(bytes.Buffer) - if err := fileSet.Write(json.NewEncoder(encodedFileSet).Encode); err != nil { + importContext.Packages[srcs.ImportPath] = typesPkg + + // Extract all go:linkname compiler directives from the package source. + goLinknames, err := srcs.ParseGoLinknames() + if err != nil { return nil, err } - simplifiedFiles := make([]*ast.File, len(files)) - for i, file := range files { - simplifiedFiles[i] = astrewrite.Simplify(file, typesInfo, false) - } + srcs = srcs.Simplified(typesInfo) isBlocking := func(f *types.Func) bool { archive, err := importContext.Import(f.Pkg().Path()) @@ -231,26 +174,40 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor } panic(fullName) } - pkgInfo := analysis.AnalyzePkg(simplifiedFiles, fileSet, typesInfo, typesPkg, isBlocking) + + tc := typeparams.Collector{ + TContext: tContext, + Info: typesInfo, + Instances: &typeparams.PackageInstanceSets{}, + } + tc.Scan(typesPkg, srcs.Files...) + instancesByObj := map[types.Object][]typeparams.Instance{} + for _, inst := range tc.Instances.Pkg(typesPkg).Values() { + instancesByObj[inst.Object] = append(instancesByObj[inst.Object], inst) + } + + pkgInfo := analysis.AnalyzePkg(srcs.Files, fileSet, typesInfo, typesPkg, isBlocking) funcCtx := &funcContext{ FuncInfo: pkgInfo.InitFuncInfo, pkgCtx: &pkgContext{ Info: pkgInfo, - additionalSelections: make(map[*ast.SelectorExpr]selection), + additionalSelections: make(map[*ast.SelectorExpr]typesutil.Selection), + typesCtx: tContext, pkgVars: make(map[string]string), - objectNames: make(map[types.Object]string), varPtrNames: make(map[*types.Var]string), escapingVars: make(map[*types.Var]bool), indentation: 1, dependencies: make(map[types.Object]bool), minify: minify, - fileSet: fileSet, + fileSet: srcs.FileSet, + instanceSet: tc.Instances, }, allVars: make(map[string]int), flowDatas: map[*types.Label]*flowData{nil: {}}, caseCounter: 1, labelCases: make(map[*types.Label]int), + objectNames: map[types.Object]string{}, } for name := range reservedKeywords { funcCtx.allVars[name] = 1 @@ -265,7 +222,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor // but now we do it here to maintain previous behavior. continue } - funcCtx.pkgCtx.pkgVars[importedPkg.Path()] = funcCtx.newVariableWithLevel(importedPkg.Name(), true) + funcCtx.pkgCtx.pkgVars[importedPkg.Path()] = funcCtx.newVariable(importedPkg.Name(), true) importedPaths = append(importedPaths, importedPkg.Path()) } sort.Strings(importedPaths) @@ -283,7 +240,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor var functions []*ast.FuncDecl var vars []*types.Var - for _, file := range simplifiedFiles { + for _, file := range srcs.Files { for _, decl := range file.Decls { switch d := decl.(type) { case *ast.FuncDecl: @@ -299,7 +256,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor case token.TYPE: for _, spec := range d.Specs { o := funcCtx.pkgCtx.Defs[spec.(*ast.TypeSpec).Name].(*types.TypeName) - funcCtx.pkgCtx.typeNames = append(funcCtx.pkgCtx.typeNames, o) + funcCtx.pkgCtx.typeNames.Add(o) funcCtx.objectName(o) // register toplevel name } case token.VAR: @@ -393,69 +350,81 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor var mainFunc *types.Func for _, fun := range functions { o := funcCtx.pkgCtx.Defs[fun.Name].(*types.Func) + sig := o.Type().(*types.Signature) - if fun.Type.TypeParams.NumFields() > 0 { - return nil, scanner.Error{ - Pos: fileSet.Position(fun.Type.TypeParams.Pos()), - Msg: fmt.Sprintf("function %s: type parameters are not supported by GopherJS: https://github.com/gopherjs/gopherjs/issues/1013", o.Name()), - } - } - funcInfo := funcCtx.pkgCtx.FuncDeclInfos[o] - d := Decl{ - FullName: o.FullName(), - Blocking: len(funcInfo.Blocking) != 0, + var instances []typeparams.Instance + if typeparams.SignatureTypeParams(sig) != nil { + instances = instancesByObj[o] + } else { + instances = []typeparams.Instance{{Object: o}} } - d.LinkingName = newSymName(o) + if fun.Recv == nil { - d.Vars = []string{funcCtx.objectName(o)} - d.DceObjectFilter = o.Name() - switch o.Name() { - case "main": - mainFunc = o - d.DceObjectFilter = "" - case "init": - d.InitCode = funcCtx.CatchOutput(1, func() { - id := funcCtx.newIdent("", types.NewSignatureType(nil, nil, nil, nil, nil, false)) - funcCtx.pkgCtx.Uses[id] = o - call := &ast.CallExpr{Fun: id} - if len(funcCtx.pkgCtx.FuncDeclInfos[o].Blocking) != 0 { - funcCtx.Blocking[call] = true - } - funcCtx.translateStmt(&ast.ExprStmt{X: call}, nil) + // Auxiliary decl shared by all instances of the function that defines + // package-level variable by which they all are referenced. + // TODO(nevkontakte): Set DCE attributes such that it is eliminated if all + // instances are dead. + varDecl := Decl{} + varDecl.Vars = []string{funcCtx.objectName(o)} + if o.Type().(*types.Signature).TypeParams().Len() != 0 { + varDecl.DeclCode = funcCtx.CatchOutput(0, func() { + funcCtx.Printf("%s = {};", funcCtx.objectName(o)) }) - d.DceObjectFilter = "" } - } else { - recvType := o.Type().(*types.Signature).Recv().Type() - ptr, isPointer := recvType.(*types.Pointer) - namedRecvType, _ := recvType.(*types.Named) - if isPointer { - namedRecvType = ptr.Elem().(*types.Named) + funcDecls = append(funcDecls, &varDecl) + } + + for _, inst := range instances { + funcInfo := funcCtx.pkgCtx.FuncDeclInfos[o] + d := Decl{ + FullName: o.FullName(), + Blocking: len(funcInfo.Blocking) != 0, } - if namedRecvType.TypeParams() != nil { - return nil, scanner.Error{ - Pos: fileSet.Position(o.Pos()), - Msg: fmt.Sprintf("type %s: type parameters are not supported by GopherJS: https://github.com/gopherjs/gopherjs/issues/1013", o.FullName()), + d.LinkingName = symbol.New(o) + if fun.Recv == nil { + d.RefExpr = funcCtx.instName(inst) + d.DceObjectFilter = o.Name() + switch o.Name() { + case "main": + mainFunc = o + d.DceObjectFilter = "" + case "init": + d.InitCode = funcCtx.CatchOutput(1, func() { + id := funcCtx.newIdent("", types.NewSignatureType( /*recv=*/ nil /*rectTypeParams=*/, nil /*typeParams=*/, nil /*params=*/, nil /*results=*/, nil /*variadic=*/, false)) + funcCtx.pkgCtx.Uses[id] = o + call := &ast.CallExpr{Fun: id} + if len(funcCtx.pkgCtx.FuncDeclInfos[o].Blocking) != 0 { + funcCtx.Blocking[call] = true + } + funcCtx.translateStmt(&ast.ExprStmt{X: call}, nil) + }) + d.DceObjectFilter = "" + } + } else { + recvType := o.Type().(*types.Signature).Recv().Type() + ptr, isPointer := recvType.(*types.Pointer) + namedRecvType, _ := recvType.(*types.Named) + if isPointer { + namedRecvType = ptr.Elem().(*types.Named) + } + d.NamedRecvType = funcCtx.objectName(namedRecvType.Obj()) + d.DceObjectFilter = namedRecvType.Obj().Name() + if !fun.Name.IsExported() { + d.DceMethodFilter = o.Name() + "~" } } - name := funcCtx.objectName(namedRecvType.Obj()) - d.NamedRecvType = name - d.DceObjectFilter = namedRecvType.Obj().Name() - if !fun.Name.IsExported() { - d.DceMethodFilter = o.Name() + "~" - } - } - d.DceDeps = collectDependencies(func() { - d.DeclCode = funcCtx.translateToplevelFunction(fun, funcInfo) - }) - funcDecls = append(funcDecls, &d) + d.DceDeps = collectDependencies(func() { + d.DeclCode = funcCtx.translateToplevelFunction(fun, funcInfo, inst) + }) + funcDecls = append(funcDecls, &d) + } } if typesPkg.Name() == "main" { if mainFunc == nil { return nil, fmt.Errorf("missing main function") } - id := funcCtx.newIdent("", types.NewSignatureType(nil, nil, nil, nil, nil, false)) + id := funcCtx.newIdent("", types.NewSignatureType( /*recv=*/ nil /*rectTypeParams=*/, nil /*typeParams=*/, nil /*params=*/, nil /*results=*/, nil /*variadic=*/, false)) funcCtx.pkgCtx.Uses[id] = mainFunc call := &ast.CallExpr{Fun: id} ifStmt := &ast.IfStmt{ @@ -484,99 +453,120 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor // named types var typeDecls []*Decl - for _, o := range funcCtx.pkgCtx.typeNames { + for _, o := range funcCtx.pkgCtx.typeNames.Slice() { if o.IsAlias() { continue } + typ := o.Type().(*types.Named) + var instances []typeparams.Instance + if typ.TypeParams() != nil { + instances = instancesByObj[o] + } else { + instances = []typeparams.Instance{{Object: o}} + } + typeName := funcCtx.objectName(o) - if named, ok := o.Type().(*types.Named); ok && named.TypeParams().Len() > 0 { - return nil, scanner.Error{ - Pos: fileSet.Position(o.Pos()), - Msg: fmt.Sprintf("type %s: type parameters are not supported by GopherJS: https://github.com/gopherjs/gopherjs/issues/1013", o.Name()), - } + varDecl := Decl{Vars: []string{typeName}} + if typ.TypeParams() != nil { + varDecl.DeclCode = funcCtx.CatchOutput(0, func() { + funcCtx.Printf("%s = {};", funcCtx.objectName(o)) + }) } - - d := Decl{ - Vars: []string{typeName}, - DceObjectFilter: o.Name(), + if isPkgLevel(o) { + varDecl.TypeInitCode = funcCtx.CatchOutput(0, func() { + funcCtx.Printf("$pkg.%s = %s;", encodeIdent(o.Name()), funcCtx.objectName(o)) + }) } - d.DceDeps = collectDependencies(func() { - d.DeclCode = funcCtx.CatchOutput(0, func() { - typeName := funcCtx.objectName(o) - lhs := typeName - if isPkgLevel(o) { - lhs += " = $pkg." + encodeIdent(o.Name()) + typeDecls = append(typeDecls, &varDecl) + + for _, inst := range instances { + funcCtx.typeResolver = typeparams.NewResolver(funcCtx.pkgCtx.typesCtx, typeparams.ToSlice(typ.TypeParams()), inst.TArgs) + + named := typ + if !inst.IsTrivial() { + instantiated, err := types.Instantiate(funcCtx.pkgCtx.typesCtx, typ, inst.TArgs, true) + if err != nil { + return nil, fmt.Errorf("failed to instantiate type %v with args %v: %w", typ, inst.TArgs, err) } - size := int64(0) - constructor := "null" - switch t := o.Type().Underlying().(type) { - case *types.Struct: - params := make([]string, t.NumFields()) - for i := 0; i < t.NumFields(); i++ { - params[i] = fieldName(t, i) + "_" - } - constructor = fmt.Sprintf("function(%s) {\n\t\tthis.$val = this;\n\t\tif (arguments.length === 0) {\n", strings.Join(params, ", ")) - for i := 0; i < t.NumFields(); i++ { - constructor += fmt.Sprintf("\t\t\tthis.%s = %s;\n", fieldName(t, i), funcCtx.translateExpr(funcCtx.zeroValue(t.Field(i).Type())).String()) + named = instantiated.(*types.Named) + } + underlying := named.Underlying() + d := Decl{ + DceObjectFilter: o.Name(), + } + d.DceDeps = collectDependencies(func() { + d.DeclCode = funcCtx.CatchOutput(0, func() { + size := int64(0) + constructor := "null" + switch t := underlying.(type) { + case *types.Struct: + params := make([]string, t.NumFields()) + for i := 0; i < t.NumFields(); i++ { + params[i] = fieldName(t, i) + "_" + } + constructor = fmt.Sprintf("function(%s) {\n\t\tthis.$val = this;\n\t\tif (arguments.length === 0) {\n", strings.Join(params, ", ")) + for i := 0; i < t.NumFields(); i++ { + constructor += fmt.Sprintf("\t\t\tthis.%s = %s;\n", fieldName(t, i), funcCtx.translateExpr(funcCtx.zeroValue(t.Field(i).Type())).String()) + } + constructor += "\t\t\treturn;\n\t\t}\n" + for i := 0; i < t.NumFields(); i++ { + constructor += fmt.Sprintf("\t\tthis.%[1]s = %[1]s_;\n", fieldName(t, i)) + } + constructor += "\t}" + case *types.Basic, *types.Array, *types.Slice, *types.Chan, *types.Signature, *types.Interface, *types.Pointer, *types.Map: + size = sizes32.Sizeof(t) } - constructor += "\t\t\treturn;\n\t\t}\n" - for i := 0; i < t.NumFields(); i++ { - constructor += fmt.Sprintf("\t\tthis.%[1]s = %[1]s_;\n", fieldName(t, i)) + if tPointer, ok := underlying.(*types.Pointer); ok { + if _, ok := tPointer.Elem().Underlying().(*types.Array); ok { + // Array pointers have non-default constructors to support wrapping + // of the native objects. + constructor = "$arrayPtrCtor()" + } } - constructor += "\t}" - case *types.Basic, *types.Array, *types.Slice, *types.Chan, *types.Signature, *types.Interface, *types.Pointer, *types.Map: - size = sizes32.Sizeof(t) - } - if tPointer, ok := o.Type().Underlying().(*types.Pointer); ok { - if _, ok := tPointer.Elem().Underlying().(*types.Array); ok { - // Array pointers have non-default constructors to support wrapping - // of the native objects. - constructor = "$arrayPtrCtor()" + funcCtx.Printf(`%s = $newType(%d, %s, %q, %t, "%s", %t, %s);`, funcCtx.instName(inst), size, typeKind(typ), inst.TypeString(), o.Name() != "", o.Pkg().Path(), o.Exported(), constructor) + }) + d.MethodListCode = funcCtx.CatchOutput(0, func() { + if _, ok := underlying.(*types.Interface); ok { + return } - } - funcCtx.Printf(`%s = $newType(%d, %s, "%s.%s", %t, "%s", %t, %s);`, lhs, size, typeKind(o.Type()), o.Pkg().Name(), o.Name(), o.Name() != "", o.Pkg().Path(), o.Exported(), constructor) - }) - d.MethodListCode = funcCtx.CatchOutput(0, func() { - named := o.Type().(*types.Named) - if _, ok := named.Underlying().(*types.Interface); ok { - return - } - var methods []string - var ptrMethods []string - for i := 0; i < named.NumMethods(); i++ { - method := named.Method(i) - name := method.Name() - if reservedKeywords[name] { - name += "$" + var methods []string + var ptrMethods []string + for i := 0; i < named.NumMethods(); i++ { + method := named.Method(i) + name := method.Name() + if reservedKeywords[name] { + name += "$" + } + pkgPath := "" + if !method.Exported() { + pkgPath = method.Pkg().Path() + } + t := method.Type().(*types.Signature) + entry := fmt.Sprintf(`{prop: "%s", name: %s, pkg: "%s", typ: $funcType(%s)}`, name, encodeString(method.Name()), pkgPath, funcCtx.initArgs(t)) + if _, isPtr := t.Recv().Type().(*types.Pointer); isPtr { + ptrMethods = append(ptrMethods, entry) + continue + } + methods = append(methods, entry) } - pkgPath := "" - if !method.Exported() { - pkgPath = method.Pkg().Path() + if len(methods) > 0 { + funcCtx.Printf("%s.methods = [%s];", funcCtx.instName(inst), strings.Join(methods, ", ")) } - t := method.Type().(*types.Signature) - entry := fmt.Sprintf(`{prop: "%s", name: %s, pkg: "%s", typ: $funcType(%s)}`, name, encodeString(method.Name()), pkgPath, funcCtx.initArgs(t)) - if _, isPtr := t.Recv().Type().(*types.Pointer); isPtr { - ptrMethods = append(ptrMethods, entry) - continue + if len(ptrMethods) > 0 { + funcCtx.Printf("%s.methods = [%s];", funcCtx.typeName(types.NewPointer(named)), strings.Join(ptrMethods, ", ")) } - methods = append(methods, entry) - } - if len(methods) > 0 { - funcCtx.Printf("%s.methods = [%s];", funcCtx.typeName(named), strings.Join(methods, ", ")) - } - if len(ptrMethods) > 0 { - funcCtx.Printf("%s.methods = [%s];", funcCtx.typeName(types.NewPointer(named)), strings.Join(ptrMethods, ", ")) + }) + switch t := underlying.(type) { + case *types.Array, *types.Chan, *types.Interface, *types.Map, *types.Pointer, *types.Slice, *types.Signature, *types.Struct: + d.TypeInitCode = funcCtx.CatchOutput(0, func() { + funcCtx.Printf("%s.init(%s);", funcCtx.instName(inst), funcCtx.initArgs(t)) + }) } }) - switch t := o.Type().Underlying().(type) { - case *types.Array, *types.Chan, *types.Interface, *types.Map, *types.Pointer, *types.Slice, *types.Signature, *types.Struct: - d.TypeInitCode = funcCtx.CatchOutput(0, func() { - funcCtx.Printf("%s.init(%s);", funcCtx.objectName(o), funcCtx.initArgs(t)) - }) - } - }) - typeDecls = append(typeDecls, &d) + typeDecls = append(typeDecls, &d) + } + funcCtx.typeResolver = nil } // anonymous types @@ -604,8 +594,17 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor return nil, funcCtx.pkgCtx.errList } + exportData := new(bytes.Buffer) + if err := gcexportdata.Write(exportData, nil, typesPkg); err != nil { + return nil, fmt.Errorf("failed to write export data: %w", err) + } + encodedFileSet := new(bytes.Buffer) + if err := srcs.FileSet.Write(json.NewEncoder(encodedFileSet).Encode); err != nil { + return nil, err + } + return &Archive{ - ImportPath: importPath, + ImportPath: srcs.ImportPath, Name: typesPkg.Name(), Imports: importedPaths, ExportData: exportData.Bytes(), @@ -670,8 +669,8 @@ func (fc *funcContext) initArgs(ty types.Type) string { } } -func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analysis.FuncInfo) []byte { - o := fc.pkgCtx.Defs[fun.Name].(*types.Func) +func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analysis.FuncInfo, inst typeparams.Instance) []byte { + o := inst.Object.(*types.Func) sig := o.Type().(*types.Signature) var recv *ast.Ident if fun.Recv != nil && fun.Recv.List[0].Names != nil { @@ -684,7 +683,7 @@ func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analys return []byte(fmt.Sprintf("\t%s = function() {\n\t\t$throwRuntimeError(\"native function not implemented: %s\");\n\t};\n", funcRef, o.FullName())) } - params, fun := translateFunction(fun.Type, recv, fun.Body, fc, sig, info, funcRef) + params, fun := translateFunction(fun.Type, recv, fun.Body, fc, sig, info, funcRef, inst) joinedParams = strings.Join(params, ", ") return []byte(fmt.Sprintf("\t%s = %s;\n", funcRef, fun)) } @@ -692,7 +691,7 @@ func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analys code := bytes.NewBuffer(nil) if fun.Recv == nil { - funcRef := fc.objectName(o) + funcRef := fc.instName(inst) code.Write(primaryFunction(funcRef)) if fun.Name.IsExported() { fmt.Fprintf(code, "\t$pkg.%s = %s;\n", encodeIdent(fun.Name.Name), funcRef) @@ -700,72 +699,79 @@ func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analys return code.Bytes() } - recvType := sig.Recv().Type() - ptr, isPointer := recvType.(*types.Pointer) - namedRecvType, _ := recvType.(*types.Named) - if isPointer { - namedRecvType = ptr.Elem().(*types.Named) - } - typeName := fc.objectName(namedRecvType.Obj()) + recvInst := inst.Recv() + recvInstName := fc.instName(recvInst) + recvType := recvInst.Object.Type().(*types.Named) funName := fun.Name.Name if reservedKeywords[funName] { funName += "$" } - if _, isStruct := namedRecvType.Underlying().(*types.Struct); isStruct { - code.Write(primaryFunction(typeName + ".ptr.prototype." + funName)) - fmt.Fprintf(code, "\t%s.prototype.%s = function(%s) { return this.$val.%s(%s); };\n", typeName, funName, joinedParams, funName, joinedParams) + if _, isStruct := recvType.Underlying().(*types.Struct); isStruct { + code.Write(primaryFunction(recvInstName + ".ptr.prototype." + funName)) + fmt.Fprintf(code, "\t%s.prototype.%s = function(%s) { return this.$val.%s(%s); };\n", recvInstName, funName, joinedParams, funName, joinedParams) return code.Bytes() } - if isPointer { + if ptr, isPointer := sig.Recv().Type().(*types.Pointer); isPointer { if _, isArray := ptr.Elem().Underlying().(*types.Array); isArray { - code.Write(primaryFunction(typeName + ".prototype." + funName)) - fmt.Fprintf(code, "\t$ptrType(%s).prototype.%s = function(%s) { return (new %s(this.$get())).%s(%s); };\n", typeName, funName, joinedParams, typeName, funName, joinedParams) + code.Write(primaryFunction(recvInstName + ".prototype." + funName)) + fmt.Fprintf(code, "\t$ptrType(%s).prototype.%s = function(%s) { return (new %s(this.$get())).%s(%s); };\n", recvInstName, funName, joinedParams, recvInstName, funName, joinedParams) return code.Bytes() } - return primaryFunction(fmt.Sprintf("$ptrType(%s).prototype.%s", typeName, funName)) + return primaryFunction(fmt.Sprintf("$ptrType(%s).prototype.%s", recvInstName, funName)) } value := "this.$get()" if isWrapped(recvType) { - value = fmt.Sprintf("new %s(%s)", typeName, value) + value = fmt.Sprintf("new %s(%s)", recvInstName, value) } - code.Write(primaryFunction(typeName + ".prototype." + funName)) - fmt.Fprintf(code, "\t$ptrType(%s).prototype.%s = function(%s) { return %s.%s(%s); };\n", typeName, funName, joinedParams, value, funName, joinedParams) + code.Write(primaryFunction(recvInstName + ".prototype." + funName)) + fmt.Fprintf(code, "\t$ptrType(%s).prototype.%s = function(%s) { return %s.%s(%s); };\n", recvInstName, funName, joinedParams, value, funName, joinedParams) return code.Bytes() } -func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, outerContext *funcContext, sig *types.Signature, info *analysis.FuncInfo, funcRef string) ([]string, string) { +func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, outerContext *funcContext, sig *types.Signature, info *analysis.FuncInfo, funcRef string, inst typeparams.Instance) ([]string, string) { if info == nil { panic("nil info") } c := &funcContext{ - FuncInfo: info, - pkgCtx: outerContext.pkgCtx, - parent: outerContext, - sig: sig, - allVars: make(map[string]int, len(outerContext.allVars)), - localVars: []string{}, - flowDatas: map[*types.Label]*flowData{nil: {}}, - caseCounter: 1, - labelCases: make(map[*types.Label]int), + FuncInfo: info, + pkgCtx: outerContext.pkgCtx, + parent: outerContext, + allVars: make(map[string]int, len(outerContext.allVars)), + localVars: []string{}, + flowDatas: map[*types.Label]*flowData{nil: {}}, + caseCounter: 1, + labelCases: make(map[*types.Label]int), + typeResolver: outerContext.typeResolver, + objectNames: map[types.Object]string{}, + sig: &typesutil.Signature{Sig: sig}, } for k, v := range outerContext.allVars { c.allVars[k] = v } prevEV := c.pkgCtx.escapingVars + if sig.TypeParams().Len() > 0 { + c.typeResolver = typeparams.NewResolver(c.pkgCtx.typesCtx, typeparams.ToSlice(sig.TypeParams()), inst.TArgs) + } else if sig.RecvTypeParams().Len() > 0 { + c.typeResolver = typeparams.NewResolver(c.pkgCtx.typesCtx, typeparams.ToSlice(sig.RecvTypeParams()), inst.TArgs) + } + if c.objectNames == nil { + c.objectNames = map[types.Object]string{} + } + var params []string for _, param := range typ.Params.List { if len(param.Names) == 0 { - params = append(params, c.newVariable("param")) + params = append(params, c.newLocalVariable("param")) continue } for _, ident := range param.Names { if isBlank(ident) { - params = append(params, c.newVariable("param")) + params = append(params, c.newLocalVariable("param")) continue } params = append(params, c.objectName(c.pkgCtx.Defs[ident])) @@ -778,20 +784,21 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, c.handleEscapingVars(body) } - if c.sig != nil && c.sig.Results().Len() != 0 && c.sig.Results().At(0).Name() != "" { - c.resultNames = make([]ast.Expr, c.sig.Results().Len()) - for i := 0; i < c.sig.Results().Len(); i++ { - result := c.sig.Results().At(i) - c.Printf("%s = %s;", c.objectName(result), c.translateExpr(c.zeroValue(result.Type())).String()) + if c.sig != nil && c.sig.HasNamedResults() { + c.resultNames = make([]ast.Expr, c.sig.Sig.Results().Len()) + for i := 0; i < c.sig.Sig.Results().Len(); i++ { + result := c.sig.Sig.Results().At(i) + typ := c.typeResolver.Substitute(result.Type()) + c.Printf("%s = %s;", c.objectName(result), c.translateExpr(c.zeroValue(typ)).String()) id := ast.NewIdent("") c.pkgCtx.Uses[id] = result - c.resultNames[i] = c.setType(id, result.Type()) + c.resultNames[i] = c.setType(id, typ) } } if recv != nil && !isBlank(recv) { this := "this" - if isWrapped(c.pkgCtx.TypeOf(recv)) { + if isWrapped(c.typeOf(recv)) { this = "this.$val" // Unwrap receiver value. } c.Printf("%s = %s;", c.translateExpr(recv), this) @@ -851,7 +858,7 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, if len(c.Blocking) != 0 { deferSuffix += " $s = -1;" } - if c.resultNames == nil && c.sig.Results().Len() > 0 { + if c.resultNames == nil && c.sig.HasResults() { deferSuffix += fmt.Sprintf(" return%s;", c.translateResults(nil)) } deferSuffix += " } finally { $callDeferred($deferred, $err);" @@ -874,16 +881,16 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, } if prefix != "" { - bodyOutput = strings.Repeat("\t", c.pkgCtx.indentation+1) + "/* */" + prefix + "\n" + bodyOutput + bodyOutput = c.Indentation(1) + "/* */" + prefix + "\n" + bodyOutput } if suffix != "" { - bodyOutput = bodyOutput + strings.Repeat("\t", c.pkgCtx.indentation+1) + "/* */" + suffix + "\n" + bodyOutput = bodyOutput + c.Indentation(1) + "/* */" + suffix + "\n" } if localVarDefs != "" { - bodyOutput = strings.Repeat("\t", c.pkgCtx.indentation+1) + localVarDefs + bodyOutput + bodyOutput = c.Indentation(1) + localVarDefs + bodyOutput } c.pkgCtx.escapingVars = prevEV - return params, fmt.Sprintf("function%s(%s) {\n%s%s}", functionName, strings.Join(params, ", "), bodyOutput, strings.Repeat("\t", c.pkgCtx.indentation)) + return params, fmt.Sprintf("function%s(%s) {\n%s%s}", functionName, strings.Join(params, ", "), bodyOutput, c.Indentation(0)) } diff --git a/compiler/prelude/types.js b/compiler/prelude/types.js index 61475454e..9570b2fed 100644 --- a/compiler/prelude/types.js +++ b/compiler/prelude/types.js @@ -59,7 +59,7 @@ var $idKey = x => { }; // Creates constructor functions for array pointer types. Returns a new function -// instace each time to make sure each type is independent of the other. +// instance each time to make sure each type is independent of the other. var $arrayPtrCtor = () => { return function (array) { this.$get = () => { return array; }; diff --git a/compiler/sources.go b/compiler/sources.go new file mode 100644 index 000000000..e6c3710f4 --- /dev/null +++ b/compiler/sources.go @@ -0,0 +1,123 @@ +package compiler + +import ( + "go/ast" + "go/token" + "go/types" + "sort" + + "github.com/neelance/astrewrite" +) + +// sources is a slice of parsed Go sources. +// +// Note that the sources would normally belong to a single logical Go package, +// but they don't have to be a real Go package (i.e. found on the file system) +// or represent a complete package (i.e. it could be only a few source files +// compiled by `gopherjs build foo.go bar.go`). +type sources struct { + // ImportPath representing the sources, if exists. May be empty for "virtual" + // packages like testmain or playground-generated package. + ImportPath string + Files []*ast.File + FileSet *token.FileSet +} + +// Sort the Files slice by the original source name to ensure consistent order +// of processing. This is required for reproducible JavaScript output. +// +// Note this function mutates the original slice. +func (s sources) Sort() sources { + sort.Slice(s.Files, func(i, j int) bool { + return s.FileSet.File(s.Files[i].Pos()).Name() > s.FileSet.File(s.Files[j].Pos()).Name() + }) + return s +} + +// Simplified returns a new sources instance with each Files entry processed by +// astrewrite.Simplify. +func (s sources) Simplified(typesInfo *types.Info) sources { + simplified := sources{ + ImportPath: s.ImportPath, + FileSet: s.FileSet, + } + for _, file := range s.Files { + simplified.Files = append(simplified.Files, astrewrite.Simplify(file, typesInfo, false)) + } + return simplified +} + +// TypeCheck the sources. Returns information about declared package types and +// type information for the supplied AST. +func (s sources) TypeCheck(importContext *ImportContext, tContext *types.Context) (*types.Info, *types.Package, error) { + const errLimit = 10 // Max number of type checking errors to return. + + typesInfo := &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + Scopes: make(map[ast.Node]*types.Scope), + Instances: make(map[*ast.Ident]types.Instance), + } + + var typeErrs ErrorList + + importer := packageImporter{ImportContext: importContext} + + config := &types.Config{ + Context: tContext, + Importer: &importer, + Sizes: sizes32, + Error: func(err error) { typeErrs = typeErrs.AppendDistinct(err) }, + } + typesPkg, err := config.Check(s.ImportPath, s.FileSet, s.Files, typesInfo) + // If we encountered any import errors, it is likely that the other type errors + // are not meaningful and would be resolved by fixing imports. Return them + // separately, if any. https://github.com/gopherjs/gopherjs/issues/119. + if importer.Errors.ErrOrNil() != nil { + return nil, nil, importer.Errors.Trim(errLimit).ErrOrNil() + } + // Return any other type errors. + if typeErrs.ErrOrNil() != nil { + return nil, nil, typeErrs.Trim(errLimit).ErrOrNil() + } + // Any general errors that may have occurred during type checking. + if err != nil { + return nil, nil, err + } + return typesInfo, typesPkg, nil +} + +// ParseGoLinknames extracts all //go:linkname compiler directive from the sources. +func (s sources) ParseGoLinknames() ([]GoLinkname, error) { + goLinknames := []GoLinkname{} + var errs ErrorList + for _, file := range s.Files { + found, err := parseGoLinknames(s.FileSet, s.ImportPath, file) + errs = errs.Append(err) + goLinknames = append(goLinknames, found...) + } + return goLinknames, errs.ErrOrNil() +} + +// packageImporter implements go/types.Importer interface. +type packageImporter struct { + ImportContext *ImportContext + Errors ErrorList +} + +func (pi *packageImporter) Import(path string) (*types.Package, error) { + if path == "unsafe" { + return types.Unsafe, nil + } + + a, err := pi.ImportContext.Import(path) + if err != nil { + pi.Errors = pi.Errors.AppendDistinct(err) + return nil, err + } + + return pi.ImportContext.Packages[a.ImportPath], nil +} diff --git a/compiler/statements.go b/compiler/statements.go index 8518f9b71..3f228963e 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -100,7 +100,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { data.endCase = fc.caseCounter fc.caseCounter++ - fc.Indent(func() { + fc.Indented(func() { fc.translateStmtList(clause.Body) }) fc.Printf("case %d:", data.endCase) @@ -112,7 +112,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { fc.Printf("%s:", label.Name()) } fc.Printf("switch (0) { default:") - fc.Indent(func() { + fc.Indented(func() { fc.translateStmtList(clause.Body) }) fc.Printf("}") @@ -125,7 +125,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { if s.Init != nil { fc.translateStmt(s.Init, nil) } - refVar := fc.newVariable("_ref") + refVar := fc.newLocalVariable("_ref") var expr ast.Expr switch a := s.Assign.(type) { case *ast.AssignStmt: @@ -135,10 +135,10 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { } fc.Printf("%s = %s;", refVar, fc.translateExpr(expr)) translateCond := func(cond ast.Expr) *expression { - if types.Identical(fc.pkgCtx.TypeOf(cond), types.Typ[types.UntypedNil]) { + if types.Identical(fc.typeOf(cond), types.Typ[types.UntypedNil]) { return fc.formatExpr("%s === $ifaceNil", refVar) } - return fc.formatExpr("$assertType(%s, %s, true)[1]", refVar, fc.typeName(fc.pkgCtx.TypeOf(cond))) + return fc.formatExpr("$assertType(%s, %s, true)[1]", refVar, fc.typeName(fc.typeOf(cond))) } var caseClauses []*ast.CaseClause var defaultClause *ast.CaseClause @@ -146,16 +146,17 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { clause := cc.(*ast.CaseClause) var bodyPrefix []ast.Stmt if implicit := fc.pkgCtx.Implicits[clause]; implicit != nil { + typ := fc.typeResolver.Substitute(implicit.Type()) value := refVar - if typesutil.IsJsObject(implicit.Type().Underlying()) { + if typesutil.IsJsObject(typ.Underlying()) { value += ".$val.object" - } else if _, ok := implicit.Type().Underlying().(*types.Interface); !ok { + } else if _, ok := typ.Underlying().(*types.Interface); !ok { value += ".$val" } bodyPrefix = []ast.Stmt{&ast.AssignStmt{ - Lhs: []ast.Expr{fc.newIdent(fc.objectName(implicit), implicit.Type())}, + Lhs: []ast.Expr{fc.newIdent(fc.objectName(implicit), typ)}, Tok: token.DEFINE, - Rhs: []ast.Expr{fc.newIdent(value, implicit.Type())}, + Rhs: []ast.Expr{fc.newIdent(value, typ)}, }} } c := &ast.CaseClause{ @@ -187,14 +188,14 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { }, label, fc.Flattened[s]) case *ast.RangeStmt: - refVar := fc.newVariable("_ref") + refVar := fc.newLocalVariable("_ref") fc.Printf("%s = %s;", refVar, fc.translateExpr(s.X)) - switch t := fc.pkgCtx.TypeOf(s.X).Underlying().(type) { + switch t := fc.typeOf(s.X).Underlying().(type) { case *types.Basic: - iVar := fc.newVariable("_i") + iVar := fc.newLocalVariable("_i") fc.Printf("%s = 0;", iVar) - runeVar := fc.newVariable("_rune") + runeVar := fc.newLocalVariable("_rune") fc.translateLoopingStmt(func() string { return iVar + " < " + refVar + ".length" }, s.Body, func() { fc.Printf("%s = $decodeRune(%s, %s);", runeVar, refVar, iVar) if !isBlank(s.Key) { @@ -208,16 +209,16 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { }, label, fc.Flattened[s]) case *types.Map: - iVar := fc.newVariable("_i") + iVar := fc.newLocalVariable("_i") fc.Printf("%s = 0;", iVar) - keysVar := fc.newVariable("_keys") + keysVar := fc.newLocalVariable("_keys") fc.Printf("%s = %s ? %s.keys() : undefined;", keysVar, refVar, refVar) - sizeVar := fc.newVariable("_size") + sizeVar := fc.newLocalVariable("_size") fc.Printf("%s = %s ? %s.size : 0;", sizeVar, refVar, refVar) fc.translateLoopingStmt(func() string { return iVar + " < " + sizeVar }, s.Body, func() { - keyVar := fc.newVariable("_key") - entryVar := fc.newVariable("_entry") + keyVar := fc.newLocalVariable("_key") + entryVar := fc.newLocalVariable("_entry") fc.Printf("%s = %s.next().value;", keyVar, keysVar) fc.Printf("%s = %s.get(%s);", entryVar, refVar, keyVar) fc.translateStmt(&ast.IfStmt{ @@ -248,7 +249,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { length = refVar + ".$length" elemType = t2.Elem() } - iVar := fc.newVariable("_i") + iVar := fc.newLocalVariable("_i") fc.Printf("%s = 0;", iVar) fc.translateLoopingStmt(func() string { return iVar + " < " + length }, s.Body, func() { if !isBlank(s.Key) { @@ -265,7 +266,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { }, label, fc.Flattened[s]) case *types.Chan: - okVar := fc.newIdent(fc.newVariable("_ok"), types.Typ[types.Bool]) + okVar := fc.newIdent(fc.newLocalVariable("_ok"), types.Typ[types.Bool]) key := s.Key tok := s.Tok if key == nil { @@ -354,7 +355,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { if rVal != "" { // If returned expression is non empty, evaluate and store it in a // variable to avoid double-execution in case a deferred function blocks. - rVar := fc.newVariable("$r") + rVar := fc.newLocalVariable("$r") fc.Printf("%s =%s;", rVar, rVal) rVal = " " + rVar } @@ -380,15 +381,15 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { case len(s.Lhs) == 1 && len(s.Rhs) == 1: lhs := astutil.RemoveParens(s.Lhs[0]) if isBlank(lhs) { - fc.Printf("$unused(%s);", fc.translateImplicitConversion(s.Rhs[0], fc.pkgCtx.TypeOf(s.Lhs[0]))) + fc.Printf("$unused(%s);", fc.translateImplicitConversion(s.Rhs[0], fc.typeOf(s.Lhs[0]))) return } fc.Printf("%s", fc.translateAssign(lhs, s.Rhs[0], s.Tok == token.DEFINE)) case len(s.Lhs) > 1 && len(s.Rhs) == 1: - tupleVar := fc.newVariable("_tuple") + tupleVar := fc.newLocalVariable("_tuple") fc.Printf("%s = %s;", tupleVar, fc.translateExpr(s.Rhs[0])) - tuple := fc.pkgCtx.TypeOf(s.Rhs[0]).(*types.Tuple) + tuple := fc.typeOf(s.Rhs[0]).(*types.Tuple) for i, lhs := range s.Lhs { lhs = astutil.RemoveParens(lhs) if !isBlank(lhs) { @@ -398,17 +399,17 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { case len(s.Lhs) == len(s.Rhs): tmpVars := make([]string, len(s.Rhs)) for i, rhs := range s.Rhs { - tmpVars[i] = fc.newVariable("_tmp") + tmpVars[i] = fc.newLocalVariable("_tmp") if isBlank(astutil.RemoveParens(s.Lhs[i])) { fc.Printf("$unused(%s);", fc.translateExpr(rhs)) continue } - fc.Printf("%s", fc.translateAssign(fc.newIdent(tmpVars[i], fc.pkgCtx.TypeOf(s.Lhs[i])), rhs, true)) + fc.Printf("%s", fc.translateAssign(fc.newIdent(tmpVars[i], fc.typeOf(s.Lhs[i])), rhs, true)) } for i, lhs := range s.Lhs { lhs = astutil.RemoveParens(lhs) if !isBlank(lhs) { - fc.Printf("%s", fc.translateAssign(lhs, fc.newIdent(tmpVars[i], fc.pkgCtx.TypeOf(lhs)), s.Tok == token.DEFINE)) + fc.Printf("%s", fc.translateAssign(lhs, fc.newIdent(tmpVars[i], fc.typeOf(lhs)), s.Tok == token.DEFINE)) } } @@ -431,7 +432,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { if len(rhs) == 0 { rhs = make([]ast.Expr, len(lhs)) for i, e := range lhs { - rhs[i] = fc.zeroValue(fc.pkgCtx.TypeOf(e)) + rhs[i] = fc.zeroValue(fc.typeOf(e)) } } fc.translateStmt(&ast.AssignStmt{ @@ -443,8 +444,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { case token.TYPE: for _, spec := range decl.Specs { o := fc.pkgCtx.Defs[spec.(*ast.TypeSpec).Name].(*types.TypeName) - fc.pkgCtx.typeNames = append(fc.pkgCtx.typeNames, o) - fc.pkgCtx.objectNames[o] = fc.newVariableWithLevel(o.Name(), true) + fc.pkgCtx.typeNames.Add(o) fc.pkgCtx.dependencies[o] = true } case token.CONST: @@ -469,7 +469,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { fc.Printf("$go(%s, %s);", callable, arglist) case *ast.SendStmt: - chanType := fc.pkgCtx.TypeOf(s.Chan).Underlying().(*types.Chan) + chanType := fc.typeOf(s.Chan).Underlying().(*types.Chan) call := &ast.CallExpr{ Fun: fc.newIdent("$send", types.NewSignatureType(nil, nil, nil, types.NewTuple(types.NewVar(0, nil, "", chanType), types.NewVar(0, nil, "", chanType.Elem())), nil, false)), Args: []ast.Expr{s.Chan, fc.newIdent(fc.translateImplicitConversionWithCloning(s.Value, chanType.Elem()).String(), chanType.Elem())}, @@ -478,7 +478,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { fc.translateStmt(&ast.ExprStmt{X: call}, label) case *ast.SelectStmt: - selectionVar := fc.newVariable("_selection") + selectionVar := fc.newLocalVariable("_selection") var channels []string var caseClauses []*ast.CaseClause flattened := false @@ -494,7 +494,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { case *ast.AssignStmt: channels = append(channels, fc.formatExpr("[%e]", astutil.RemoveParens(comm.Rhs[0]).(*ast.UnaryExpr).X).String()) case *ast.SendStmt: - chanType := fc.pkgCtx.TypeOf(comm.Chan).Underlying().(*types.Chan) + chanType := fc.typeOf(comm.Chan).Underlying().(*types.Chan) channels = append(channels, fc.formatExpr("[%e, %s]", comm.Chan, fc.translateImplicitConversionWithCloning(comm.Value, chanType.Elem())).String()) default: panic(fmt.Sprintf("unhandled: %T", comm)) @@ -505,7 +505,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { var bodyPrefix []ast.Stmt if assign, ok := clause.Comm.(*ast.AssignStmt); ok { - switch rhsType := fc.pkgCtx.TypeOf(assign.Rhs[0]).(type) { + switch rhsType := fc.typeOf(assign.Rhs[0]).(type) { case *types.Tuple: bodyPrefix = []ast.Stmt{&ast.AssignStmt{Lhs: assign.Lhs, Rhs: []ast.Expr{fc.newIdent(selectionVar+"[1]", rhsType)}, Tok: assign.Tok}} default: @@ -614,7 +614,7 @@ func (fc *funcContext) translateBranchingStmt(caseClauses []*ast.CaseClause, def for i, clause := range caseClauses { fc.SetPos(clause.Pos()) fc.PrintCond(!flatten, fmt.Sprintf("%sif (%s) {", prefix, condStrs[i]), fmt.Sprintf("case %d:", caseOffset+i)) - fc.Indent(func() { + fc.Indented(func() { fc.translateStmtList(clause.Body) if flatten && (i < len(caseClauses)-1 || defaultClause != nil) && !astutil.EndsWithReturn(clause.Body) { fc.Printf("$s = %d; continue;", endCase) @@ -625,7 +625,7 @@ func (fc *funcContext) translateBranchingStmt(caseClauses []*ast.CaseClause, def if defaultClause != nil { fc.PrintCond(!flatten, prefix+"{", fmt.Sprintf("case %d:", caseOffset+len(caseClauses))) - fc.Indent(func() { + fc.Indented(func() { fc.translateStmtList(defaultClause.Body) }) } @@ -655,7 +655,7 @@ func (fc *funcContext) translateLoopingStmt(cond func() string, body *ast.BlockS } isTerminated := false fc.PrintCond(!flatten, "while (true) {", fmt.Sprintf("case %d:", data.beginCase)) - fc.Indent(func() { + fc.Indented(func() { condStr := cond() if condStr != "true" { fc.PrintCond(!flatten, fmt.Sprintf("if (!(%s)) { break; }", condStr), fmt.Sprintf("if(!(%s)) { $s = %d; continue; }", condStr, data.endCase)) @@ -700,11 +700,11 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { } if l, ok := lhs.(*ast.IndexExpr); ok { - if t, ok := fc.pkgCtx.TypeOf(l.X).Underlying().(*types.Map); ok { - if typesutil.IsJsObject(fc.pkgCtx.TypeOf(l.Index)) { + if t, ok := fc.typeOf(l.X).Underlying().(*types.Map); ok { + if typesutil.IsJsObject(fc.typeOf(l.Index)) { fc.pkgCtx.errList = append(fc.pkgCtx.errList, types.Error{Fset: fc.pkgCtx.fileSet, Pos: l.Index.Pos(), Msg: "cannot use js.Object as map key"}) } - keyVar := fc.newVariable("_key") + keyVar := fc.newLocalVariable("_key") return fmt.Sprintf( `%s = %s; (%s || $throwRuntimeError("assignment to entry in nil map")).set(%s.keyFor(%s), { k: %s, v: %s });`, keyVar, @@ -718,7 +718,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { } } - lhsType := fc.pkgCtx.TypeOf(lhs) + lhsType := fc.typeOf(lhs) rhsExpr := fc.translateConversion(rhs, lhsType) if _, ok := rhs.(*ast.CompositeLit); ok && define { return fmt.Sprintf("%s = %s;", fc.translateExpr(lhs), rhsExpr) // skip $copy @@ -742,7 +742,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { case *ast.Ident: return fmt.Sprintf("%s = %s;", fc.objectName(fc.pkgCtx.ObjectOf(l)), rhsExpr) case *ast.SelectorExpr: - sel, ok := fc.pkgCtx.SelectionOf(l) + sel, ok := fc.selectionOf(l) if !ok { // qualified identifier return fmt.Sprintf("%s = %s;", fc.objectName(fc.pkgCtx.Uses[l.Sel]), rhsExpr) @@ -755,7 +755,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { case *ast.StarExpr: return fmt.Sprintf("%s.$set(%s);", fc.translateExpr(l.X), rhsExpr) case *ast.IndexExpr: - switch t := fc.pkgCtx.TypeOf(l.X).Underlying().(type) { + switch t := fc.typeOf(l.X).Underlying().(type) { case *types.Array, *types.Pointer: pattern := rangeCheck("%1e[%2f] = %3s", fc.pkgCtx.Types[l.Index].Value != nil, true) if _, ok := t.(*types.Pointer); ok { // check pointer for nil (attribute getter causes a panic) @@ -773,7 +773,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { } func (fc *funcContext) translateResults(results []ast.Expr) string { - tuple := fc.sig.Results() + tuple := fc.typeResolver.Substitute(fc.sig.Sig.Results()).(*types.Tuple) switch tuple.Len() { case 0: return "" @@ -787,7 +787,7 @@ func (fc *funcContext) translateResults(results []ast.Expr) string { return " " + v.String() default: if len(results) == 1 { - resultTuple := fc.pkgCtx.TypeOf(results[0]).(*types.Tuple) + resultTuple := fc.typeOf(results[0]).(*types.Tuple) if resultTuple.Len() != tuple.Len() { panic("invalid tuple return assignment") @@ -799,7 +799,7 @@ func (fc *funcContext) translateResults(results []ast.Expr) string { return " " + resultExpr } - tmpVar := fc.newVariable("_returncast") + tmpVar := fc.newLocalVariable("_returncast") fc.Printf("%s = %s;", tmpVar, resultExpr) // Not all the return types matched, map everything out for implicit casting diff --git a/compiler/typesutil/map.go b/compiler/typesutil/map.go new file mode 100644 index 000000000..146f09765 --- /dev/null +++ b/compiler/typesutil/map.go @@ -0,0 +1,34 @@ +package typesutil + +import ( + "go/types" + + "golang.org/x/tools/go/types/typeutil" +) + +// Map is a type-safe wrapper around golang.org/x/tools/go/types/typeutil.Map. +type Map[Val any] struct{ impl typeutil.Map } + +func (m *Map[Val]) At(key types.Type) Val { + val := m.impl.At(key) + if val != nil { + return val.(Val) + } + var zero Val + return zero +} + +func (m *Map[Val]) Set(key types.Type, value Val) (prev Val) { + old := m.impl.Set(key, value) + if old != nil { + return old.(Val) + } + var zero Val + return zero +} + +func (m *Map[Val]) Delete(key types.Type) bool { return m.impl.Delete(key) } + +func (m *Map[Val]) Len() int { return m.impl.Len() } + +func (m *Map[Val]) String() string { return m.impl.String() } diff --git a/compiler/typesutil/signature.go b/compiler/typesutil/signature.go new file mode 100644 index 000000000..0a79432cb --- /dev/null +++ b/compiler/typesutil/signature.go @@ -0,0 +1,67 @@ +package typesutil + +import ( + "fmt" + "go/types" +) + +// Signature is a helper that provides convenient access to function +// signature type information. +type Signature struct { + Sig *types.Signature +} + +// RequiredParams returns the number of required parameters in the function signature. +func (st Signature) RequiredParams() int { + l := st.Sig.Params().Len() + if st.Sig.Variadic() { + return l - 1 // Last parameter is a slice of variadic params. + } + return l +} + +// VariadicType returns the slice-type corresponding to the signature's variadic +// parameter, or nil of the signature is not variadic. With the exception of +// the special-case `append([]byte{}, "string"...)`, the returned type is +// `*types.Slice` and `.Elem()` method can be used to get the type of individual +// arguments. +func (st Signature) VariadicType() types.Type { + if !st.Sig.Variadic() { + return nil + } + return st.Sig.Params().At(st.Sig.Params().Len() - 1).Type() +} + +// Param returns the expected argument type for the i'th argument position. +// +// This function is able to return correct expected types for variadic calls +// both when ellipsis syntax (e.g. myFunc(requiredArg, optionalArgSlice...)) +// is used and when optional args are passed individually. +// +// The returned types may differ from the actual argument expression types if +// there is an implicit type conversion involved (e.g. passing a struct into a +// function that expects an interface). +func (st Signature) Param(i int, ellipsis bool) types.Type { + if i < st.RequiredParams() { + return st.Sig.Params().At(i).Type() + } + if !st.Sig.Variadic() { + // This should never happen if the code was type-checked successfully. + panic(fmt.Errorf("tried to access parameter %d of a non-variadic signature %s", i, st.Sig)) + } + if ellipsis { + return st.VariadicType() + } + return st.VariadicType().(*types.Slice).Elem() +} + +// HasResults returns true if the function signature returns something. +func (st Signature) HasResults() bool { + return st.Sig.Results().Len() > 0 +} + +// HasNamedResults returns true if the function signature returns something and +// returned results are names (e.g. `func () (val int, err error)`). +func (st Signature) HasNamedResults() bool { + return st.HasResults() && st.Sig.Results().At(0).Name() != "" +} diff --git a/compiler/typesutil/signature_test.go b/compiler/typesutil/signature_test.go new file mode 100644 index 000000000..a6d159687 --- /dev/null +++ b/compiler/typesutil/signature_test.go @@ -0,0 +1,166 @@ +package typesutil + +import ( + "go/token" + "go/types" + "testing" +) + +func TestSignature_RequiredParams(t *testing.T) { + tests := []struct { + descr string + sig *types.Signature + want int + }{{ + descr: "regular signature", + sig: types.NewSignatureType(nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "a", types.Typ[types.Int]), + types.NewVar(token.NoPos, nil, "b", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "c", types.NewSlice(types.Typ[types.String])), + ), nil, false), + want: 3, + }, { + descr: "variadic signature", + sig: types.NewSignatureType(nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "a", types.Typ[types.Int]), + types.NewVar(token.NoPos, nil, "b", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "c", types.NewSlice(types.Typ[types.String])), + ), nil, true /*variadic*/), + want: 2, + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + sig := Signature{Sig: test.sig} + got := sig.RequiredParams() + if got != test.want { + t.Errorf("Got: {%s}.RequiredParams() = %d. Want: %d.", test.sig, got, test.want) + } + }) + } +} + +func TestSignature_VariadicType(t *testing.T) { + tests := []struct { + descr string + sig *types.Signature + want types.Type + }{{ + descr: "regular signature", + sig: types.NewSignatureType(nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "a", types.Typ[types.Int]), + types.NewVar(token.NoPos, nil, "b", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "c", types.NewSlice(types.Typ[types.String])), + ), nil, false), + want: nil, + }, { + descr: "variadic signature", + sig: types.NewSignatureType(nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "a", types.Typ[types.Int]), + types.NewVar(token.NoPos, nil, "b", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "c", types.NewSlice(types.Typ[types.String])), + ), nil, true /*variadic*/), + want: types.NewSlice(types.Typ[types.String]), + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + sig := Signature{Sig: test.sig} + got := sig.VariadicType() + if !types.Identical(got, test.want) { + t.Errorf("Got: {%s}.VariadicType() = %v. Want: %v.", test.sig, got, test.want) + } + }) + } +} + +func TestSignature_Param(t *testing.T) { + sig := types.NewSignatureType(nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "a", types.Typ[types.Int]), + types.NewVar(token.NoPos, nil, "b", types.Typ[types.Byte]), + types.NewVar(token.NoPos, nil, "c", types.NewSlice(types.Typ[types.String])), + ), nil, true /*variadic*/) + + tests := []struct { + descr string + param int + ellipsis bool + want types.Type + }{{ + descr: "required param", + param: 1, + want: types.Typ[types.Byte], + }, { + descr: "variadic param", + param: 2, + want: types.Typ[types.String], + }, { + descr: "variadic param repeated", + param: 3, + want: types.Typ[types.String], + }, { + descr: "variadic param with ellipsis", + param: 2, + ellipsis: true, + want: types.NewSlice(types.Typ[types.String]), + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + sig := Signature{Sig: sig} + got := sig.Param(test.param, test.ellipsis) + if !types.Identical(got, test.want) { + t.Errorf("Got: {%s}.Param(%v, %v) = %v. Want: %v.", sig, test.param, test.ellipsis, got, test.want) + } + }) + } +} + +func TestSignature_HasXResults(t *testing.T) { + tests := []struct { + descr string + sig *types.Signature + hasResults bool + hasNamedResults bool + }{{ + descr: "no results", + sig: types.NewSignatureType(nil, nil, nil, nil, types.NewTuple(), false), + hasResults: false, + hasNamedResults: false, + }, { + descr: "anonymous result", + sig: types.NewSignatureType(nil, nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "", types.Typ[types.String]), + ), false), + hasResults: true, + hasNamedResults: false, + }, { + descr: "named result", + sig: types.NewSignatureType(nil, nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "s", types.Typ[types.String]), + ), false), + hasResults: true, + hasNamedResults: true, + }, { + descr: "underscore named result", + sig: types.NewSignatureType(nil, nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "_", types.Typ[types.String]), + ), false), + hasResults: true, + hasNamedResults: true, + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + sig := Signature{Sig: test.sig} + gotHasResults := sig.HasResults() + if gotHasResults != test.hasResults { + t.Errorf("Got: {%s}.HasResults() = %v. Want: %v.", test.sig, gotHasResults, test.hasResults) + } + gotHasNamedResults := sig.HasNamedResults() + if gotHasNamedResults != test.hasNamedResults { + t.Errorf("Got: {%s}.HasResults() = %v. Want: %v.", test.sig, gotHasNamedResults, test.hasNamedResults) + } + }) + } +} diff --git a/compiler/typesutil/typelist.go b/compiler/typesutil/typelist.go new file mode 100644 index 000000000..04d0d6869 --- /dev/null +++ b/compiler/typesutil/typelist.go @@ -0,0 +1,20 @@ +package typesutil + +import ( + "go/types" + "strings" +) + +// TypeList an ordered list of types. +type TypeList []types.Type + +func (tl TypeList) String() string { + buf := strings.Builder{} + for i, typ := range tl { + if i != 0 { + buf.WriteString(", ") + } + buf.WriteString(types.TypeString(typ, nil)) + } + return buf.String() +} diff --git a/compiler/typesutil/typenames.go b/compiler/typesutil/typenames.go new file mode 100644 index 000000000..2f5ac6186 --- /dev/null +++ b/compiler/typesutil/typenames.go @@ -0,0 +1,30 @@ +package typesutil + +import "go/types" + +// TypeNames implements an ordered set of *types.TypeName pointers. +// +// The set is ordered to ensure deterministic behavior across compiler runs. +type TypeNames struct { + known map[*types.TypeName]struct{} + order []*types.TypeName +} + +// Add a type name to the set. If the type name has been previously added, +// this operation is a no-op. Two type names are considered equal iff they have +// the same memory address. +func (tn *TypeNames) Add(name *types.TypeName) { + if _, ok := tn.known[name]; ok { + return + } + if tn.known == nil { + tn.known = map[*types.TypeName]struct{}{} + } + tn.order = append(tn.order, name) + tn.known[name] = struct{}{} +} + +// Slice returns set elements in the order they were first added to the set. +func (tn *TypeNames) Slice() []*types.TypeName { + return tn.order +} diff --git a/compiler/typesutil/typenames_test.go b/compiler/typesutil/typenames_test.go new file mode 100644 index 000000000..1e8a4b994 --- /dev/null +++ b/compiler/typesutil/typenames_test.go @@ -0,0 +1,45 @@ +package typesutil + +import ( + "go/types" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/gopherjs/gopherjs/internal/srctesting" +) + +func typeNameOpts() cmp.Options { + return cmp.Options{ + cmp.Transformer("TypeName", func(name *types.TypeName) string { + return types.ObjectString(name, nil) + }), + } +} + +func TestTypeNames(t *testing.T) { + src := `package test + + type A int + type B int + type C int + ` + f := srctesting.New(t) + _, pkg := f.Check("pkg/test", f.Parse("test.go", src)) + A := srctesting.LookupObj(pkg, "A").(*types.TypeName) + B := srctesting.LookupObj(pkg, "B").(*types.TypeName) + C := srctesting.LookupObj(pkg, "C").(*types.TypeName) + + tn := TypeNames{} + tn.Add(A) + tn.Add(B) + tn.Add(A) + tn.Add(C) + tn.Add(B) + + got := tn.Slice() + want := []*types.TypeName{A, B, C} + + if diff := cmp.Diff(want, got, typeNameOpts()); diff != "" { + t.Errorf("tn.Slice() returned diff (-want,+got):\n%s", diff) + } +} diff --git a/compiler/typesutil/typesutil.go b/compiler/typesutil/typesutil.go index 600925b81..1434c23c5 100644 --- a/compiler/typesutil/typesutil.go +++ b/compiler/typesutil/typesutil.go @@ -1,6 +1,9 @@ package typesutil -import "go/types" +import ( + "fmt" + "go/types" +) func IsJsPackage(pkg *types.Package) bool { return pkg != nil && pkg.Path() == "github.com/gopherjs/gopherjs/js" @@ -14,3 +17,93 @@ func IsJsObject(t types.Type) bool { named, isNamed := ptr.Elem().(*types.Named) return isNamed && IsJsPackage(named.Obj().Pkg()) && named.Obj().Name() == "Object" } + +// RecvType returns a named type of a method receiver, or nil if it's not a method. +// +// For methods on a pointer receiver, the underlying named type is returned. +func RecvType(sig *types.Signature) *types.Named { + recv := sig.Recv() + if recv == nil { + return nil + } + + typ := recv.Type() + if ptrType, ok := typ.(*types.Pointer); ok { + typ = ptrType.Elem() + } + + return typ.(*types.Named) +} + +// RecvAsFirstArg takes a method signature and returns a function +// signature with receiver as the first parameter. +func RecvAsFirstArg(sig *types.Signature) *types.Signature { + params := make([]*types.Var, 0, 1+sig.Params().Len()) + params = append(params, sig.Recv()) + for i := 0; i < sig.Params().Len(); i++ { + params = append(params, sig.Params().At(i)) + } + return types.NewSignatureType(nil, nil, nil, types.NewTuple(params...), sig.Results(), sig.Variadic()) +} + +// Selection is a common interface for go/types.Selection and our custom-constructed +// method and field selections. +type Selection interface { + Kind() types.SelectionKind + Recv() types.Type + Index() []int + Obj() types.Object + Type() types.Type +} + +// NewSelection creates a new selection. +func NewSelection(kind types.SelectionKind, recv types.Type, index []int, obj types.Object, typ types.Type) Selection { + return &selectionImpl{ + kind: kind, + recv: recv, + index: index, + obj: obj, + typ: typ, + } +} + +type selectionImpl struct { + kind types.SelectionKind + recv types.Type + index []int + obj types.Object + typ types.Type +} + +func (sel *selectionImpl) Kind() types.SelectionKind { return sel.kind } +func (sel *selectionImpl) Recv() types.Type { return sel.recv } +func (sel *selectionImpl) Index() []int { return sel.index } +func (sel *selectionImpl) Obj() types.Object { return sel.obj } +func (sel *selectionImpl) Type() types.Type { return sel.typ } + +func fieldsOf(s *types.Struct) []*types.Var { + fields := make([]*types.Var, s.NumFields()) + for i := 0; i < s.NumFields(); i++ { + fields[i] = s.Field(i) + } + return fields +} + +// OffsetOf returns byte offset of a struct field specified by the provided +// selection. +// +// Adapted from go/types.Config.offsetof(). +func OffsetOf(sizes types.Sizes, sel Selection) int64 { + if sel.Kind() != types.FieldVal { + panic(fmt.Errorf("byte offsets are only defined for struct fields")) + } + typ := sel.Recv() + var o int64 + for _, idx := range sel.Index() { + s := typ.Underlying().(*types.Struct) + o += sizes.Offsetsof(fieldsOf(s))[idx] + typ = s.Field(idx).Type() + } + + return o +} diff --git a/compiler/utils.go b/compiler/utils.go index 058437f67..c1c5941f5 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -19,9 +19,18 @@ import ( "unicode" "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) +// root returns the topmost function context corresponding to the package scope. +func (fc *funcContext) root() *funcContext { + if fc.parent == nil { + return fc + } + return fc.parent.root() +} + func (fc *funcContext) Write(b []byte) (int, error) { fc.writePos() fc.output = append(fc.output, b...) @@ -29,7 +38,7 @@ func (fc *funcContext) Write(b []byte) (int, error) { } func (fc *funcContext) Printf(format string, values ...interface{}) { - fc.Write([]byte(strings.Repeat("\t", fc.pkgCtx.indentation))) + fc.Write([]byte(fc.Indentation(0))) fmt.Fprintf(fc, format, values...) fc.Write([]byte{'\n'}) fc.Write(fc.delayedOutput) @@ -57,12 +66,21 @@ func (fc *funcContext) writePos() { } } -func (fc *funcContext) Indent(f func()) { +// Indented increases generated code indentation level by 1 for the code emitted +// from the callback f. +func (fc *funcContext) Indented(f func()) { fc.pkgCtx.indentation++ f() fc.pkgCtx.indentation-- } +// Indentation returns a sequence of "\t" characters appropriate to the current +// generated code indentation level. The `extra` parameter provides relative +// indentation adjustment. +func (fc *funcContext) Indentation(extra int) string { + return strings.Repeat("\t", fc.pkgCtx.indentation+extra) +} + func (fc *funcContext) CatchOutput(indent int, f func()) []byte { origoutput := fc.output fc.output = nil @@ -101,12 +119,12 @@ func (fc *funcContext) expandTupleArgs(argExprs []ast.Expr) []ast.Expr { return argExprs } - tuple, isTuple := fc.pkgCtx.TypeOf(argExprs[0]).(*types.Tuple) + tuple, isTuple := fc.typeOf(argExprs[0]).(*types.Tuple) if !isTuple { return argExprs } - tupleVar := fc.newVariable("_tuple") + tupleVar := fc.newLocalVariable("_tuple") fc.Printf("%s = %s;", tupleVar, fc.translateExpr(argExprs[0])) argExprs = make([]ast.Expr, tuple.Len()) for i := range argExprs { @@ -118,7 +136,7 @@ func (fc *funcContext) expandTupleArgs(argExprs []ast.Expr) []ast.Expr { func (fc *funcContext) translateArgs(sig *types.Signature, argExprs []ast.Expr, ellipsis bool) []string { argExprs = fc.expandTupleArgs(argExprs) - sigTypes := signatureTypes{Sig: sig} + sigTypes := typesutil.Signature{Sig: sig} if sig.Variadic() && len(argExprs) == 0 { return []string{fmt.Sprintf("%s.nil", fc.typeName(sigTypes.VariadicType()))} @@ -134,7 +152,7 @@ func (fc *funcContext) translateArgs(sig *types.Signature, argExprs []ast.Expr, arg := fc.translateImplicitConversionWithCloning(argExpr, sigTypes.Param(i, ellipsis)).String() if preserveOrder && fc.pkgCtx.Types[argExpr].Value == nil { - argVar := fc.newVariable("_arg") + argVar := fc.newLocalVariable("_arg") fc.Printf("%s = %s;", argVar, arg) arg = argVar } @@ -158,7 +176,7 @@ func (fc *funcContext) translateArgs(sig *types.Signature, argExprs []ast.Expr, return args } -func (fc *funcContext) translateSelection(sel selection, pos token.Pos) ([]string, string) { +func (fc *funcContext) translateSelection(sel typesutil.Selection, pos token.Pos) ([]string, string) { var fields []string t := sel.Recv() for _, index := range sel.Index() { @@ -229,11 +247,26 @@ func (fc *funcContext) newConst(t types.Type, value constant.Value) ast.Expr { return id } -func (fc *funcContext) newVariable(name string) string { - return fc.newVariableWithLevel(name, false) +// newLocalVariable assigns a new JavaScript variable name for the given Go +// local variable name. In this context "local" means "in scope of the current" +// functionContext. +func (fc *funcContext) newLocalVariable(name string) string { + return fc.newVariable(name, false) } -func (fc *funcContext) newVariableWithLevel(name string, pkgLevel bool) string { +// newVariable assigns a new JavaScript variable name for the given Go variable +// or type. +// +// If there is already a variable with the same name visible in the current +// function context (e.g. due to shadowing), the returned name will be suffixed +// with a number to prevent conflict. This is necessary because Go name +// resolution scopes differ from var declarations in JS. +// +// If pkgLevel is true, the variable is declared at the package level and added +// to this functionContext, as well as all parents, but not to the list of local +// variables. If false, it is added to this context only, as well as the list of +// local vars. +func (fc *funcContext) newVariable(name string, pkgLevel bool) string { if name == "" { panic("newVariable: empty name") } @@ -283,7 +316,7 @@ func (fc *funcContext) newIdent(name string, t types.Type) *ast.Ident { fc.setType(ident, t) obj := types.NewVar(0, fc.pkgCtx.Pkg, name, t) fc.pkgCtx.Uses[ident] = obj - fc.pkgCtx.objectNames[obj] = name + fc.objectNames[obj] = name return ident } @@ -319,9 +352,30 @@ func isVarOrConst(o types.Object) bool { } func isPkgLevel(o types.Object) bool { - return o.Parent() != nil && o.Parent().Parent() == types.Universe + // Note: named types are always assigned a variable at package level to be + // initialized with the rest of the package types, even the types declared + // in a statement inside a function. + _, isType := o.(*types.TypeName) + return (o.Parent() != nil && o.Parent().Parent() == types.Universe) || isType +} + +// assignedObjectName checks if the object has been previously assigned a name +// in this or one of the parent contexts. If not, found will be false. +func (fc *funcContext) assignedObjectName(o types.Object) (name string, found bool) { + if fc == nil { + return "", false + } + if name, found := fc.parent.assignedObjectName(o); found { + return name, true + } + + name, found = fc.objectNames[o] + return name, found } +// objectName returns a JS expression that refers to the given object. If the +// object hasn't been previously assigned a JS variable name, it will be +// allocated as needed. func (fc *funcContext) objectName(o types.Object) string { if isPkgLevel(o) { fc.pkgCtx.dependencies[o] = true @@ -331,10 +385,15 @@ func (fc *funcContext) objectName(o types.Object) string { } } - name, ok := fc.pkgCtx.objectNames[o] + name, ok := fc.assignedObjectName(o) if !ok { - name = fc.newVariableWithLevel(o.Name(), isPkgLevel(o)) - fc.pkgCtx.objectNames[o] = name + pkgLevel := isPkgLevel(o) + name = fc.newVariable(o.Name(), pkgLevel) + if pkgLevel { + fc.root().objectNames[o] = name + } else { + fc.objectNames[o] = name + } } if v, ok := o.(*types.Var); ok && fc.pkgCtx.escapingVars[v] { @@ -343,6 +402,17 @@ func (fc *funcContext) objectName(o types.Object) string { return name } +// instName returns a JS expression that refers to the provided instance of a +// function or type. Non-generic objects may be represented as an instance with +// zero type arguments. +func (fc *funcContext) instName(inst typeparams.Instance) string { + objName := fc.objectName(inst.Object) + if inst.IsTrivial() { + return objName + } + return fmt.Sprintf("%s[%d /* %v */]", objName, fc.pkgCtx.instanceSet.ID(inst), inst.TArgs) +} + func (fc *funcContext) varPtrName(o *types.Var) string { if isPkgLevel(o) && o.Exported() { return fc.pkgVar(o.Pkg()) + "." + o.Name() + "$ptr" @@ -350,12 +420,17 @@ func (fc *funcContext) varPtrName(o *types.Var) string { name, ok := fc.pkgCtx.varPtrNames[o] if !ok { - name = fc.newVariableWithLevel(o.Name()+"$ptr", isPkgLevel(o)) + name = fc.newVariable(o.Name()+"$ptr", isPkgLevel(o)) fc.pkgCtx.varPtrNames[o] = name } return name } +// typeName returns a JS identifier name for the given Go type. +// +// For the built-in types it returns identifiers declared in the prelude. For +// all user-defined or composite types it creates a unique JS identifier and +// will return it on all subsequent calls for the type. func (fc *funcContext) typeName(ty types.Type) string { switch t := ty.(type) { case *types.Basic: @@ -364,17 +439,25 @@ func (fc *funcContext) typeName(ty types.Type) string { if t.Obj().Name() == "error" { return "$error" } - return fc.objectName(t.Obj()) + inst := typeparams.Instance{Object: t.Obj()} + for i := 0; i < t.TypeArgs().Len(); i++ { + inst.TArgs = append(inst.TArgs, t.TypeArgs().At(i)) + } + return fc.instName(inst) case *types.Interface: if t.Empty() { return "$emptyInterface" } } + // For anonymous composite types, generate a synthetic package-level type + // declaration, which will be reused for all instances of this time. This + // improves performance, since runtime won't have to synthesize the same type + // repeatedly. anonType, ok := fc.pkgCtx.anonTypeMap.At(ty).(*types.TypeName) if !ok { fc.initArgs(ty) // cause all embedded types to be registered - varName := fc.newVariableWithLevel(strings.ToLower(typeKind(ty)[5:])+"Type", true) + varName := fc.newVariable(strings.ToLower(typeKind(ty)[5:])+"Type", true) anonType = types.NewTypeName(token.NoPos, fc.pkgCtx.Pkg, varName, ty) // fake types.TypeName fc.pkgCtx.anonTypes = append(fc.pkgCtx.anonTypes, anonType) fc.pkgCtx.anonTypeMap.Set(ty, anonType) @@ -383,6 +466,42 @@ func (fc *funcContext) typeName(ty types.Type) string { return anonType.Name() } +// instanceOf constructs an instance description of the object the ident is +// referring to. For non-generic objects, it will return a trivial instance with +// no type arguments. +func (fc *funcContext) instanceOf(ident *ast.Ident) typeparams.Instance { + inst := typeparams.Instance{Object: fc.pkgCtx.ObjectOf(ident)} + if i, ok := fc.pkgCtx.Instances[ident]; ok { + inst.TArgs = fc.typeResolver.SubstituteAll(i.TypeArgs) + } + return inst +} + +// typeOf returns a type associated with the given AST expression. For types +// defined in terms of type parameters, it will substitute type parameters with +// concrete types from the current set of type arguments. +func (fc *funcContext) typeOf(expr ast.Expr) types.Type { + typ := fc.pkgCtx.TypeOf(expr) + // If the expression is referring to an instance of a generic type or function, + // we want the instantiated type. + if ident, ok := expr.(*ast.Ident); ok { + if inst, ok := fc.pkgCtx.Instances[ident]; ok { + typ = inst.Type + } + } + return fc.typeResolver.Substitute(typ) +} + +func (fc *funcContext) selectionOf(e *ast.SelectorExpr) (typesutil.Selection, bool) { + if sel, ok := fc.pkgCtx.Selections[e]; ok { + return fc.typeResolver.SubstituteSelection(sel), true + } + if sel, ok := fc.pkgCtx.additionalSelections[e]; ok { + return sel, true + } + return nil, false +} + func (fc *funcContext) externalize(s string, t types.Type) string { if typesutil.IsJsObject(t) { return s @@ -723,56 +842,6 @@ func formatJSStructTagVal(jsTag string) string { return "." + jsTag } -// signatureTypes is a helper that provides convenient access to function -// signature type information. -type signatureTypes struct { - Sig *types.Signature -} - -// RequiredParams returns the number of required parameters in the function signature. -func (st signatureTypes) RequiredParams() int { - l := st.Sig.Params().Len() - if st.Sig.Variadic() { - return l - 1 // Last parameter is a slice of variadic params. - } - return l -} - -// VariadicType returns the slice-type corresponding to the signature's variadic -// parameter, or nil of the signature is not variadic. With the exception of -// the special-case `append([]byte{}, "string"...)`, the returned type is -// `*types.Slice` and `.Elem()` method can be used to get the type of individual -// arguments. -func (st signatureTypes) VariadicType() types.Type { - if !st.Sig.Variadic() { - return nil - } - return st.Sig.Params().At(st.Sig.Params().Len() - 1).Type() -} - -// Returns the expected argument type for the i'th argument position. -// -// This function is able to return correct expected types for variadic calls -// both when ellipsis syntax (e.g. myFunc(requiredArg, optionalArgSlice...)) -// is used and when optional args are passed individually. -// -// The returned types may differ from the actual argument expression types if -// there is an implicit type conversion involved (e.g. passing a struct into a -// function that expects an interface). -func (st signatureTypes) Param(i int, ellipsis bool) types.Type { - if i < st.RequiredParams() { - return st.Sig.Params().At(i).Type() - } - if !st.Sig.Variadic() { - // This should never happen if the code was type-checked successfully. - panic(fmt.Errorf("tried to access parameter %d of a non-variadic signature %s", i, st.Sig)) - } - if ellipsis { - return st.VariadicType() - } - return st.VariadicType().(*types.Slice).Elem() -} - // ErrorAt annotates an error with a position in the source code. func ErrorAt(err error, fset *token.FileSet, pos token.Pos) error { return fmt.Errorf("%s: %w", fset.Position(pos), err) diff --git a/go.mod b/go.mod index e4810e9db..4ace51ddf 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/evanw/esbuild v0.18.0 github.com/fsnotify/fsnotify v1.5.1 - github.com/google/go-cmp v0.5.7 + github.com/google/go-cmp v0.5.8 github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 @@ -13,13 +13,11 @@ require ( github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 github.com/visualfc/goembed v0.3.3 - golang.org/x/sync v0.3.0 + golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a + golang.org/x/sync v0.5.0 golang.org/x/sys v0.10.0 golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 - golang.org/x/tools v0.11.0 + golang.org/x/tools v0.16.0 ) -require ( - github.com/inconshreveable/mousetrap v1.0.0 // indirect - golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect -) +require github.com/inconshreveable/mousetrap v1.0.0 // indirect diff --git a/go.sum b/go.sum index 349d599ba..8e69980d0 100644 --- a/go.sum +++ b/go.sum @@ -120,8 +120,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -270,6 +270,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= +golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -295,7 +297,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -355,8 +357,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -467,14 +469,12 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= -golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= diff --git a/internal/experiments/experiments.go b/internal/experiments/experiments.go new file mode 100644 index 000000000..85abce562 --- /dev/null +++ b/internal/experiments/experiments.go @@ -0,0 +1,122 @@ +// Package experiments managed the list of experimental feature flags supported +// by GopherJS. +// +// GOPHERJS_EXPERIMENT environment variable can be used to control which features +// are enabled. +package experiments + +import ( + "errors" + "fmt" + "os" + "reflect" + "strconv" + "strings" +) + +var ( + // ErrInvalidDest is a kind of error returned by parseFlags() when the dest + // argument does not meet the requirements. + ErrInvalidDest = errors.New("invalid flag struct") + // ErrInvalidFormat is a kind of error returned by parseFlags() when the raw + // flag string format is not valid. + ErrInvalidFormat = errors.New("invalid flag string format") +) + +// Env contains experiment flag values from the GOPHERJS_EXPERIMENT +// environment variable. +var Env Flags + +func init() { + if err := parseFlags(os.Getenv("GOPHERJS_EXPERIMENT"), &Env); err != nil { + panic(fmt.Errorf("failed to parse GOPHERJS_EXPERIMENT flags: %w", err)) + } +} + +// Flags contains flags for currently supported experiments. +type Flags struct { + Generics bool `flag:"generics"` +} + +// parseFlags parses the `raw` flags string and populates flag values in the +// `dest`. +// +// `raw` is a comma-separated experiment flag list: `,,...`. Each +// flag may be either `` or `=`. Omitting value is equivalent +// to " = true". Spaces around name and value are trimmed during +// parsing. Flag name can't be empty. If the same flag is specified multiple +// times, the last instance takes effect. +// +// `dest` must be a pointer to a struct, which fields will be populated with +// flag values. Mapping between flag names and fields is established with the +// `flag` field tag. Fields without a flag tag will be left unpopulated. +// If multiple fields are associated with the same flag result is unspecified. +// +// Flags that don't have a corresponding field are silently ignored. This is +// done to avoid fatal errors when an experiment flag is removed from code, but +// remains specified in user's environment. +// +// Currently only boolean flag values are supported, as defined by +// `strconv.ParseBool()`. +func parseFlags(raw string, dest any) error { + ptr := reflect.ValueOf(dest) + if ptr.Type().Kind() != reflect.Pointer || ptr.Type().Elem().Kind() != reflect.Struct { + return fmt.Errorf("%w: must be a pointer to a struct", ErrInvalidDest) + } + if ptr.IsNil() { + return fmt.Errorf("%w: must not be nil", ErrInvalidDest) + } + fields := fieldMap(ptr.Elem()) + + if raw == "" { + return nil + } + entries := strings.Split(raw, ",") + + for _, entry := range entries { + entry = strings.TrimSpace(entry) + var key, val string + if idx := strings.IndexRune(entry, '='); idx != -1 { + key = strings.TrimSpace(entry[0:idx]) + val = strings.TrimSpace(entry[idx+1:]) + } else { + key = entry + val = "true" + } + + if key == "" { + return fmt.Errorf("%w: empty flag name", ErrInvalidFormat) + } + + field, ok := fields[key] + if !ok { + // Unknown field value, possibly an obsolete experiment, ignore it. + continue + } + if field.Type().Kind() != reflect.Bool { + return fmt.Errorf("%w: only boolean flags are supported", ErrInvalidDest) + } + b, err := strconv.ParseBool(val) + if err != nil { + return fmt.Errorf("%w: can't parse %q as boolean for flag %q", ErrInvalidFormat, val, key) + } + field.SetBool(b) + } + + return nil +} + +// fieldMap returns a map of struct fieldMap keyed by the value of the "flag" tag. +// +// `s` must be a struct. Fields without a "flag" tag are ignored. If multiple +// fieldMap have the same flag, the last field wins. +func fieldMap(s reflect.Value) map[string]reflect.Value { + typ := s.Type() + result := map[string]reflect.Value{} + for i := 0; i < typ.NumField(); i++ { + if val, ok := typ.Field(i).Tag.Lookup("flag"); ok { + result[val] = s.Field(i) + } + } + return result +} diff --git a/internal/experiments/experiments_test.go b/internal/experiments/experiments_test.go new file mode 100644 index 000000000..e1c3e6b38 --- /dev/null +++ b/internal/experiments/experiments_test.go @@ -0,0 +1,132 @@ +package experiments + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestParseFlags(t *testing.T) { + type testFlags struct { + Exp1 bool `flag:"exp1"` + Exp2 bool `flag:"exp2"` + Untagged bool + } + + tests := []struct { + descr string + raw string + want testFlags + wantErr error + }{{ + descr: "default values", + raw: "", + want: testFlags{ + Exp1: false, + Exp2: false, + }, + }, { + descr: "true flag", + raw: "exp1=true", + want: testFlags{ + Exp1: true, + Exp2: false, + }, + }, { + descr: "false flag", + raw: "exp1=false", + want: testFlags{ + Exp1: false, + Exp2: false, + }, + }, { + descr: "implicit value", + raw: "exp1", + want: testFlags{ + Exp1: true, + Exp2: false, + }, + }, { + descr: "multiple flags", + raw: "exp1=true,exp2=true", + want: testFlags{ + Exp1: true, + Exp2: true, + }, + }, { + descr: "repeated flag", + raw: "exp1=false,exp1=true", + want: testFlags{ + Exp1: true, + Exp2: false, + }, + }, { + descr: "spaces", + raw: " exp1 = true, exp2=true ", + want: testFlags{ + Exp1: true, + Exp2: true, + }, + }, { + descr: "unknown flags", + raw: "Exp1=true,Untagged,Foo=true", + want: testFlags{ + Exp1: false, + Exp2: false, + Untagged: false, + }, + }, { + descr: "empty flag name", + raw: "=true", + wantErr: ErrInvalidFormat, + }, { + descr: "invalid flag value", + raw: "exp1=foo", + wantErr: ErrInvalidFormat, + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + got := testFlags{} + err := parseFlags(test.raw, &got) + if test.wantErr != nil { + if !errors.Is(err, test.wantErr) { + t.Errorf("Got: parseFlags(%q) returned error: %v. Want: %v.", test.raw, err, test.wantErr) + } + } else { + if err != nil { + t.Fatalf("Got: parseFlags(%q) returned error: %v. Want: no error.", test.raw, err) + } + if diff := cmp.Diff(test.want, got); diff != "" { + t.Fatalf("parseFlags(%q) returned diff (-want,+got):\n%s", test.raw, diff) + } + } + }) + } + + t.Run("invalid dest type", func(t *testing.T) { + var dest string + err := parseFlags("", &dest) + if !errors.Is(err, ErrInvalidDest) { + t.Fatalf("Got: parseFlags() returned error: %v. Want: %v.", err, ErrInvalidDest) + } + }) + + t.Run("nil dest", func(t *testing.T) { + err := parseFlags("", (*struct{})(nil)) + if !errors.Is(err, ErrInvalidDest) { + t.Fatalf("Got: parseFlags() returned error: %v. Want: %v.", err, ErrInvalidDest) + } + }) + + t.Run("unsupported flag type", func(t *testing.T) { + var dest struct { + Foo string `flag:"foo"` + } + err := parseFlags("foo", &dest) + if !errors.Is(err, ErrInvalidDest) { + t.Fatalf("Got: parseFlags() returned error: %v. Want: %v.", err, ErrInvalidDest) + } + }) +} diff --git a/internal/govendor/subst/export.go b/internal/govendor/subst/export.go new file mode 100644 index 000000000..38e394bda --- /dev/null +++ b/internal/govendor/subst/export.go @@ -0,0 +1,49 @@ +// Package subst is an excerpt from x/tools/go/ssa responsible for performing +// type substitution in types defined in terms of type parameters with provided +// type arguments. +package subst + +import ( + "go/types" +) + +// To simplify future updates of the borrowed code, we minimize modifications +// to it as much as possible. This file implements an exported interface to the +// original code for us to use. + +// Subster performs type parameter substitution. +type Subster struct { + impl *subster +} + +// New creates a new Subster with a given list of type parameters and matching args. +func New(tc *types.Context, tParams []*types.TypeParam, tArgs []types.Type) *Subster { + assert(len(tParams) == len(tArgs), "New() argument count must match") + + if len(tParams) == 0 { + return nil + } + + subst := &subster{ + replacements: make(map[*types.TypeParam]types.Type, len(tParams)), + cache: make(map[types.Type]types.Type), + ctxt: tc, + scope: nil, + debug: false, + } + for i := 0; i < len(tParams); i++ { + subst.replacements[tParams[i]] = tArgs[i] + } + return &Subster{ + impl: subst, + } +} + +// Type returns a version of typ with all references to type parameters replaced +// with the corresponding type arguments. +func (s *Subster) Type(typ types.Type) types.Type { + if s == nil { + return typ + } + return s.impl.typ(typ) +} diff --git a/internal/govendor/subst/subst.go b/internal/govendor/subst/subst.go new file mode 100644 index 000000000..9020e94f9 --- /dev/null +++ b/internal/govendor/subst/subst.go @@ -0,0 +1,475 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package subst + +import ( + "go/types" +) + +// Type substituter for a fixed set of replacement types. +// +// A nil *subster is an valid, empty substitution map. It always acts as +// the identity function. This allows for treating parameterized and +// non-parameterized functions identically while compiling to ssa. +// +// Not concurrency-safe. +type subster struct { + replacements map[*types.TypeParam]types.Type // values should contain no type params + cache map[types.Type]types.Type // cache of subst results + ctxt *types.Context // cache for instantiation + scope *types.Scope // *types.Named declared within this scope can be substituted (optional) + debug bool // perform extra debugging checks + // TODO(taking): consider adding Pos + // TODO(zpavlinovic): replacements can contain type params + // when generating instances inside of a generic function body. +} + +// Returns a subster that replaces tparams[i] with targs[i]. Uses ctxt as a cache. +// targs should not contain any types in tparams. +// scope is the (optional) lexical block of the generic function for which we are substituting. +func makeSubster(ctxt *types.Context, scope *types.Scope, tparams *types.TypeParamList, targs []types.Type, debug bool) *subster { + assert(tparams.Len() == len(targs), "makeSubster argument count must match") + + subst := &subster{ + replacements: make(map[*types.TypeParam]types.Type, tparams.Len()), + cache: make(map[types.Type]types.Type), + ctxt: ctxt, + scope: scope, + debug: debug, + } + for i := 0; i < tparams.Len(); i++ { + subst.replacements[tparams.At(i)] = targs[i] + } + if subst.debug { + subst.wellFormed() + } + return subst +} + +// wellFormed asserts that subst was properly initialized. +func (subst *subster) wellFormed() { + if subst == nil { + return + } + // Check that all of the type params do not appear in the arguments. + s := make(map[types.Type]bool, len(subst.replacements)) + for tparam := range subst.replacements { + s[tparam] = true + } + for _, r := range subst.replacements { + if reaches(r, s) { + panic(subst) + } + } +} + +// typ returns the type of t with the type parameter tparams[i] substituted +// for the type targs[i] where subst was created using tparams and targs. +func (subst *subster) typ(t types.Type) (res types.Type) { + if subst == nil { + return t // A nil subst is type preserving. + } + if r, ok := subst.cache[t]; ok { + return r + } + defer func() { + subst.cache[t] = res + }() + + // fall through if result r will be identical to t, types.Identical(r, t). + switch t := t.(type) { + case *types.TypeParam: + r := subst.replacements[t] + assert(r != nil, "type param without replacement encountered") + return r + + case *types.Basic: + return t + + case *types.Array: + if r := subst.typ(t.Elem()); r != t.Elem() { + return types.NewArray(r, t.Len()) + } + return t + + case *types.Slice: + if r := subst.typ(t.Elem()); r != t.Elem() { + return types.NewSlice(r) + } + return t + + case *types.Pointer: + if r := subst.typ(t.Elem()); r != t.Elem() { + return types.NewPointer(r) + } + return t + + case *types.Tuple: + return subst.tuple(t) + + case *types.Struct: + return subst.struct_(t) + + case *types.Map: + key := subst.typ(t.Key()) + elem := subst.typ(t.Elem()) + if key != t.Key() || elem != t.Elem() { + return types.NewMap(key, elem) + } + return t + + case *types.Chan: + if elem := subst.typ(t.Elem()); elem != t.Elem() { + return types.NewChan(t.Dir(), elem) + } + return t + + case *types.Signature: + return subst.signature(t) + + case *types.Union: + return subst.union(t) + + case *types.Interface: + return subst.interface_(t) + + case *types.Named: + return subst.named(t) + + default: + panic("unreachable") + } +} + +// types returns the result of {subst.typ(ts[i])}. +func (subst *subster) types(ts []types.Type) []types.Type { + res := make([]types.Type, len(ts)) + for i := range ts { + res[i] = subst.typ(ts[i]) + } + return res +} + +func (subst *subster) tuple(t *types.Tuple) *types.Tuple { + if t != nil { + if vars := subst.varlist(t); vars != nil { + return types.NewTuple(vars...) + } + } + return t +} + +type varlist interface { + At(i int) *types.Var + Len() int +} + +// fieldlist is an adapter for structs for the varlist interface. +type fieldlist struct { + str *types.Struct +} + +func (fl fieldlist) At(i int) *types.Var { return fl.str.Field(i) } +func (fl fieldlist) Len() int { return fl.str.NumFields() } + +func (subst *subster) struct_(t *types.Struct) *types.Struct { + if t != nil { + if fields := subst.varlist(fieldlist{t}); fields != nil { + tags := make([]string, t.NumFields()) + for i, n := 0, t.NumFields(); i < n; i++ { + tags[i] = t.Tag(i) + } + return types.NewStruct(fields, tags) + } + } + return t +} + +// varlist reutrns subst(in[i]) or return nils if subst(v[i]) == v[i] for all i. +func (subst *subster) varlist(in varlist) []*types.Var { + var out []*types.Var // nil => no updates + for i, n := 0, in.Len(); i < n; i++ { + v := in.At(i) + w := subst.var_(v) + if v != w && out == nil { + out = make([]*types.Var, n) + for j := 0; j < i; j++ { + out[j] = in.At(j) + } + } + if out != nil { + out[i] = w + } + } + return out +} + +func (subst *subster) var_(v *types.Var) *types.Var { + if v != nil { + if typ := subst.typ(v.Type()); typ != v.Type() { + if v.IsField() { + return types.NewField(v.Pos(), v.Pkg(), v.Name(), typ, v.Embedded()) + } + return types.NewVar(v.Pos(), v.Pkg(), v.Name(), typ) + } + } + return v +} + +func (subst *subster) union(u *types.Union) *types.Union { + var out []*types.Term // nil => no updates + + for i, n := 0, u.Len(); i < n; i++ { + t := u.Term(i) + r := subst.typ(t.Type()) + if r != t.Type() && out == nil { + out = make([]*types.Term, n) + for j := 0; j < i; j++ { + out[j] = u.Term(j) + } + } + if out != nil { + out[i] = types.NewTerm(t.Tilde(), r) + } + } + + if out != nil { + return types.NewUnion(out) + } + return u +} + +func (subst *subster) interface_(iface *types.Interface) *types.Interface { + if iface == nil { + return nil + } + + // methods for the interface. Initially nil if there is no known change needed. + // Signatures for the method where recv is nil. NewInterfaceType fills in the receivers. + var methods []*types.Func + initMethods := func(n int) { // copy first n explicit methods + methods = make([]*types.Func, iface.NumExplicitMethods()) + for i := 0; i < n; i++ { + f := iface.ExplicitMethod(i) + norecv := changeRecv(f.Type().(*types.Signature), nil) + methods[i] = types.NewFunc(f.Pos(), f.Pkg(), f.Name(), norecv) + } + } + for i := 0; i < iface.NumExplicitMethods(); i++ { + f := iface.ExplicitMethod(i) + // On interfaces, we need to cycle break on anonymous interface types + // being in a cycle with their signatures being in cycles with their receivers + // that do not go through a Named. + norecv := changeRecv(f.Type().(*types.Signature), nil) + sig := subst.typ(norecv) + if sig != norecv && methods == nil { + initMethods(i) + } + if methods != nil { + methods[i] = types.NewFunc(f.Pos(), f.Pkg(), f.Name(), sig.(*types.Signature)) + } + } + + var embeds []types.Type + initEmbeds := func(n int) { // copy first n embedded types + embeds = make([]types.Type, iface.NumEmbeddeds()) + for i := 0; i < n; i++ { + embeds[i] = iface.EmbeddedType(i) + } + } + for i := 0; i < iface.NumEmbeddeds(); i++ { + e := iface.EmbeddedType(i) + r := subst.typ(e) + if e != r && embeds == nil { + initEmbeds(i) + } + if embeds != nil { + embeds[i] = r + } + } + + if methods == nil && embeds == nil { + return iface + } + if methods == nil { + initMethods(iface.NumExplicitMethods()) + } + if embeds == nil { + initEmbeds(iface.NumEmbeddeds()) + } + return types.NewInterfaceType(methods, embeds).Complete() +} + +func (subst *subster) named(t *types.Named) types.Type { + // A named type may be: + // (1) ordinary named type (non-local scope, no type parameters, no type arguments), + // (2) locally scoped type, + // (3) generic (type parameters but no type arguments), or + // (4) instantiated (type parameters and type arguments). + tparams := t.TypeParams() + if tparams.Len() == 0 { + if subst.scope != nil && !subst.scope.Contains(t.Obj().Pos()) { + // Outside the current function scope? + return t // case (1) ordinary + } + + // case (2) locally scoped type. + // Create a new named type to represent this instantiation. + // We assume that local types of distinct instantiations of a + // generic function are distinct, even if they don't refer to + // type parameters, but the spec is unclear; see golang/go#58573. + // + // Subtle: We short circuit substitution and use a newly created type in + // subst, i.e. cache[t]=n, to pre-emptively replace t with n in recursive + // types during traversal. This both breaks infinite cycles and allows for + // constructing types with the replacement applied in subst.typ(under). + // + // Example: + // func foo[T any]() { + // type linkedlist struct { + // next *linkedlist + // val T + // } + // } + // + // When the field `next *linkedlist` is visited during subst.typ(under), + // we want the substituted type for the field `next` to be `*n`. + n := types.NewNamed(t.Obj(), nil, nil) + subst.cache[t] = n + subst.cache[n] = n + n.SetUnderlying(subst.typ(t.Underlying())) + return n + } + targs := t.TypeArgs() + + // insts are arguments to instantiate using. + insts := make([]types.Type, tparams.Len()) + + // case (3) generic ==> targs.Len() == 0 + // Instantiating a generic with no type arguments should be unreachable. + // Please report a bug if you encounter this. + assert(targs.Len() != 0, "substition into a generic Named type is currently unsupported") + + // case (4) instantiated. + // Substitute into the type arguments and instantiate the replacements/ + // Example: + // type N[A any] func() A + // func Foo[T](g N[T]) {} + // To instantiate Foo[string], one goes through {T->string}. To get the type of g + // one subsitutes T with string in {N with typeargs == {T} and typeparams == {A} } + // to get {N with TypeArgs == {string} and typeparams == {A} }. + assert(targs.Len() == tparams.Len(), "typeargs.Len() must match typeparams.Len() if present") + for i, n := 0, targs.Len(); i < n; i++ { + inst := subst.typ(targs.At(i)) // TODO(generic): Check with rfindley for mutual recursion + insts[i] = inst + } + r, err := types.Instantiate(subst.ctxt, t.Origin(), insts, false) + assert(err == nil, "failed to Instantiate Named type") + return r +} + +func (subst *subster) signature(t *types.Signature) types.Type { + tparams := t.TypeParams() + + // We are choosing not to support tparams.Len() > 0 until a need has been observed in practice. + // + // There are some known usages for types.Types coming from types.{Eval,CheckExpr}. + // To support tparams.Len() > 0, we just need to do the following [psuedocode]: + // targs := {subst.replacements[tparams[i]]]}; Instantiate(ctxt, t, targs, false) + + assert(tparams.Len() == 0, "Substituting types.Signatures with generic functions are currently unsupported.") + + // Either: + // (1)non-generic function. + // no type params to substitute + // (2)generic method and recv needs to be substituted. + + // Receivers can be either: + // named + // pointer to named + // interface + // nil + // interface is the problematic case. We need to cycle break there! + recv := subst.var_(t.Recv()) + params := subst.tuple(t.Params()) + results := subst.tuple(t.Results()) + if recv != t.Recv() || params != t.Params() || results != t.Results() { + return types.NewSignatureType(recv, nil, nil, params, results, t.Variadic()) + } + return t +} + +// reaches returns true if a type t reaches any type t' s.t. c[t'] == true. +// It updates c to cache results. +// +// reaches is currently only part of the wellFormed debug logic, and +// in practice c is initially only type parameters. It is not currently +// relied on in production. +func reaches(t types.Type, c map[types.Type]bool) (res bool) { + if c, ok := c[t]; ok { + return c + } + + // c is populated with temporary false entries as types are visited. + // This avoids repeat visits and break cycles. + c[t] = false + defer func() { + c[t] = res + }() + + switch t := t.(type) { + case *types.TypeParam, *types.Basic: + return false + case *types.Array: + return reaches(t.Elem(), c) + case *types.Slice: + return reaches(t.Elem(), c) + case *types.Pointer: + return reaches(t.Elem(), c) + case *types.Tuple: + for i := 0; i < t.Len(); i++ { + if reaches(t.At(i).Type(), c) { + return true + } + } + case *types.Struct: + for i := 0; i < t.NumFields(); i++ { + if reaches(t.Field(i).Type(), c) { + return true + } + } + case *types.Map: + return reaches(t.Key(), c) || reaches(t.Elem(), c) + case *types.Chan: + return reaches(t.Elem(), c) + case *types.Signature: + if t.Recv() != nil && reaches(t.Recv().Type(), c) { + return true + } + return reaches(t.Params(), c) || reaches(t.Results(), c) + case *types.Union: + for i := 0; i < t.Len(); i++ { + if reaches(t.Term(i).Type(), c) { + return true + } + } + case *types.Interface: + for i := 0; i < t.NumEmbeddeds(); i++ { + if reaches(t.Embedded(i), c) { + return true + } + } + for i := 0; i < t.NumExplicitMethods(); i++ { + if reaches(t.ExplicitMethod(i).Type(), c) { + return true + } + } + case *types.Named: + return reaches(t.Underlying(), c) + default: + panic("unreachable") + } + return false +} diff --git a/internal/govendor/subst/subst_test.go b/internal/govendor/subst/subst_test.go new file mode 100644 index 000000000..53fadbcf0 --- /dev/null +++ b/internal/govendor/subst/subst_test.go @@ -0,0 +1,103 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package subst + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" +) + +func TestSubst(t *testing.T) { + const source = ` +package P + +type t0 int +func (t0) f() +type t1 interface{ f() } +type t2 interface{ g() } +type t3 interface{ ~int } + +func Fn0[T t1](x T) T { + x.f() + return x +} + +type A[T any] [4]T +type B[T any] []T +type C[T, S any] []struct{s S; t T} +type D[T, S any] *struct{s S; t *T} +type E[T, S any] interface{ F() (T, S) } +type F[K comparable, V any] map[K]V +type G[T any] chan *T +type H[T any] func() T +type I[T any] struct{x, y, z int; t T} +type J[T any] interface{ t1 } +type K[T any] interface{ t1; F() T } +type L[T any] interface{ F() T; J[T] } + +var _ L[int] = Fn0[L[int]](nil) +` + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "hello.go", source, 0) + if err != nil { + t.Fatal(err) + } + + var conf types.Config + pkg, err := conf.Check("P", fset, []*ast.File{f}, nil) + if err != nil { + t.Fatal(err) + } + + for _, test := range []struct { + expr string // type expression of Named parameterized type + args []string // type expressions of args for named + want string // expected underlying value after substitution + }{ + {"A", []string{"string"}, "[4]string"}, + {"A", []string{"int"}, "[4]int"}, + {"B", []string{"int"}, "[]int"}, + {"B", []string{"int8"}, "[]int8"}, + {"C", []string{"int8", "string"}, "[]struct{s string; t int8}"}, + {"C", []string{"string", "int8"}, "[]struct{s int8; t string}"}, + {"D", []string{"int16", "string"}, "*struct{s string; t *int16}"}, + {"E", []string{"int32", "string"}, "interface{F() (int32, string)}"}, + {"F", []string{"int64", "string"}, "map[int64]string"}, + {"G", []string{"uint64"}, "chan *uint64"}, + {"H", []string{"uintptr"}, "func() uintptr"}, + {"I", []string{"t0"}, "struct{x int; y int; z int; t P.t0}"}, + {"J", []string{"t0"}, "interface{P.t1}"}, + {"K", []string{"t0"}, "interface{F() P.t0; P.t1}"}, + {"L", []string{"t0"}, "interface{F() P.t0; P.J[P.t0]}"}, + {"L", []string{"L[t0]"}, "interface{F() P.L[P.t0]; P.J[P.L[P.t0]]}"}, + } { + // Eval() expr for its type. + tv, err := types.Eval(fset, pkg, 0, test.expr) + if err != nil { + t.Fatalf("Eval(%s) failed: %v", test.expr, err) + } + // Eval() test.args[i] to get the i'th type arg. + var targs []types.Type + for _, astr := range test.args { + tv, err := types.Eval(fset, pkg, 0, astr) + if err != nil { + t.Fatalf("Eval(%s) failed: %v", astr, err) + } + targs = append(targs, tv.Type) + } + + T := tv.Type.(*types.Named) + + subst := makeSubster(types.NewContext(), nil, T.TypeParams(), targs, true) + sub := subst.typ(T.Underlying()) + if got := sub.String(); got != test.want { + t.Errorf("subst{%v->%v}.typ(%s) = %v, want %v", test.expr, test.args, T.Underlying(), got, test.want) + } + } +} diff --git a/internal/govendor/subst/util.go b/internal/govendor/subst/util.go new file mode 100644 index 000000000..22072e39f --- /dev/null +++ b/internal/govendor/subst/util.go @@ -0,0 +1,23 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package subst + +import "go/types" + +// This file defines a number of miscellaneous utility functions. + +//// Sanity checking utilities + +// assert panics with the mesage msg if p is false. +// Avoid combining with expensive string formatting. +func assert(p bool, msg string) { + if !p { + panic(msg) + } +} + +func changeRecv(s *types.Signature, recv *types.Var) *types.Signature { + return types.NewSignatureType(recv, nil, nil, s.Params(), s.Results(), s.Variadic()) +} diff --git a/internal/srctesting/srctesting.go b/internal/srctesting/srctesting.go index 4e374845e..a74d31958 100644 --- a/internal/srctesting/srctesting.go +++ b/internal/srctesting/srctesting.go @@ -4,48 +4,86 @@ package srctesting import ( "bytes" + "fmt" "go/ast" "go/format" "go/parser" "go/token" "go/types" + "strings" "testing" ) -// Parse source from the string and return complete AST. -// -// Assumes source file name `test.go`. Fails the test on parsing error. -func Parse(t *testing.T, fset *token.FileSet, src string) *ast.File { - t.Helper() - f, err := parser.ParseFile(fset, "test.go", src, parser.ParseComments) - if err != nil { - t.Fatalf("Failed to parse test source: %s", err) - } - return f +// Fixture provides utilities for parsing and type checking Go code in tests. +type Fixture struct { + T *testing.T + FileSet *token.FileSet + Info *types.Info + Packages map[string]*types.Package } -// Check type correctness of the provided AST. -// -// Assumes "test" package import path. Fails the test if type checking fails. -// Provided AST is expected not to have any imports. -func Check(t *testing.T, fset *token.FileSet, files ...*ast.File) (*types.Info, *types.Package) { - t.Helper() - typesInfo := &types.Info{ +func newInfo() *types.Info { + return &types.Info{ Types: make(map[ast.Expr]types.TypeAndValue), Defs: make(map[*ast.Ident]types.Object), Uses: make(map[*ast.Ident]types.Object), Implicits: make(map[ast.Node]types.Object), Selections: make(map[*ast.SelectorExpr]*types.Selection), Scopes: make(map[ast.Node]*types.Scope), + Instances: make(map[*ast.Ident]types.Instance), + } +} + +// New creates a fresh Fixture. +func New(t *testing.T) *Fixture { + return &Fixture{ + T: t, + FileSet: token.NewFileSet(), + Info: newInfo(), + Packages: map[string]*types.Package{}, + } +} + +// Parse source from the string and return complete AST. +func (f *Fixture) Parse(name, src string) *ast.File { + f.T.Helper() + file, err := parser.ParseFile(f.FileSet, name, src, parser.ParseComments) + if err != nil { + f.T.Fatalf("Failed to parse test source: %s", err) } + return file +} + +// Check type correctness of the provided AST. +// +// Fails the test if type checking fails. Provided AST is expected not to have +// any imports. If f.Info is nil, it will create a new types.Info instance +// to store type checking results and return it, otherwise f.Info is used. +func (f *Fixture) Check(importPath string, files ...*ast.File) (*types.Info, *types.Package) { + f.T.Helper() config := &types.Config{ - Sizes: &types.StdSizes{WordSize: 4, MaxAlign: 8}, + Sizes: &types.StdSizes{WordSize: 4, MaxAlign: 8}, + Importer: f, } - typesPkg, err := config.Check("test", fset, files, typesInfo) + info := f.Info + if info == nil { + info = newInfo() + } + pkg, err := config.Check(importPath, f.FileSet, files, info) if err != nil { - t.Fatalf("Filed to type check test source: %s", err) + f.T.Fatalf("Filed to type check test source: %s", err) + } + f.Packages[importPath] = pkg + return info, pkg +} + +// Import implements types.Importer. +func (f *Fixture) Import(path string) (*types.Package, error) { + pkg, ok := f.Packages[path] + if !ok { + return nil, fmt.Errorf("missing type info for package %q", path) } - return typesInfo, typesPkg + return pkg, nil } // ParseFuncDecl parses source with a single function defined and returns the @@ -68,8 +106,7 @@ func ParseFuncDecl(t *testing.T, src string) *ast.FuncDecl { // Fails the test if there isn't exactly one declaration in the source. func ParseDecl(t *testing.T, src string) ast.Decl { t.Helper() - fset := token.NewFileSet() - file := Parse(t, fset, src) + file := New(t).Parse("test.go", src) if l := len(file.Decls); l != 1 { t.Fatalf(`Got %d decls in the sources, expected exactly 1`, l) } @@ -107,3 +144,30 @@ func Format(t *testing.T, fset *token.FileSet, node any) string { } return buf.String() } + +// LookupObj returns a top-level object with the given name. +// +// Methods can be referred to as RecvTypeName.MethodName. +func LookupObj(pkg *types.Package, name string) types.Object { + path := strings.Split(name, ".") + scope := pkg.Scope() + var obj types.Object + + for len(path) > 0 { + obj = scope.Lookup(path[0]) + path = path[1:] + + if fun, ok := obj.(*types.Func); ok { + scope = fun.Scope() + continue + } + + // If we are here, the latest object is a named type. If there are more path + // elements left, they must refer to field or method. + if len(path) > 0 { + obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), path[0]) + path = path[1:] + } + } + return obj +} diff --git a/internal/srctesting/srctesting_test.go b/internal/srctesting/srctesting_test.go new file mode 100644 index 000000000..44fa51ead --- /dev/null +++ b/internal/srctesting/srctesting_test.go @@ -0,0 +1,28 @@ +package srctesting + +import "testing" + +func TestFixture(t *testing.T) { + f := New(t) + + const src1 = `package foo + type X int + ` + _, foo := f.Check("pkg/foo", f.Parse("foo.go", src1)) + + if !foo.Complete() { + t.Fatalf("Got: incomplete package pkg/foo: %s. Want: complete package.", foo) + } + + const src2 = `package bar + import "pkg/foo" + func Fun() foo.X { return 0 } + ` + + // Should type check successfully with dependency on pkg/foo. + _, bar := f.Check("pkg/bar", f.Parse("bar.go", src2)) + + if !bar.Complete() { + t.Fatalf("Got: incomplete package pkg/bar: %s. Want: complete package.", foo) + } +} diff --git a/internal/testingx/must.go b/internal/testingx/must.go new file mode 100644 index 000000000..62d27dce8 --- /dev/null +++ b/internal/testingx/must.go @@ -0,0 +1,24 @@ +// Package testingx provides helpers for use with the testing package. +package testingx + +import "testing" + +// Must provides a concise way to handle returned error in cases that +// "should never happen"©. +// +// This function can be used in test case setup that can be presumed to be +// correct, but technically may return an error. This function MUST NOT be used +// to check for test case conditions themselves because it generates a generic, +// nondescript test error message. +// +// func startServer(addr string) (*server, err) +// mustServer := testingx.Must[*server](t) +// mustServer(startServer(":8080")) +func Must[T any](t *testing.T) func(v T, err error) T { + return func(v T, err error) T { + if err != nil { + t.Fatalf("Got: unexpected error: %s. Want: no error.", err) + } + return v + } +} diff --git a/node-syscall/package-lock.json b/node-syscall/package-lock.json index a92b06df6..3282cd229 100644 --- a/node-syscall/package-lock.json +++ b/node-syscall/package-lock.json @@ -295,9 +295,9 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -631,16 +631,23 @@ } }, "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + } } }, "unique-filename": { diff --git a/package-lock.json b/package-lock.json index 21b8f5864..8efdaca3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -107,20 +107,29 @@ "link": true }, "node_modules/tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "optional": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "optional": true, + "engines": { + "node": ">=8" } }, "node_modules/uglify-es": { @@ -1101,7 +1110,6 @@ }, "syscall": { "version": "file:node-syscall", - "optional": true, "requires": { "node-gyp": "^8.1.0" }, @@ -1824,17 +1832,25 @@ } }, "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "optional": true, "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "optional": true + } } }, "uglify-es": { diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index d58968ada..6720f50d7 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -22,6 +22,7 @@ import ( "errors" "flag" "fmt" + "go/build/constraint" "hash/fnv" "io" "log" @@ -109,9 +110,6 @@ var knownFails = map[string]failReason{ "fixedbugs/issue23188.go": {desc: "incorrect order of evaluation of index operations"}, "fixedbugs/issue24547.go": {desc: "incorrect computing method sets with shadowed methods"}, - // These are new tests in Go 1.11.5 - "fixedbugs/issue28688.go": {category: notApplicable, desc: "testing runtime optimisations"}, - // These are new tests in Go 1.12. "fixedbugs/issue23837.go": {desc: "missing panic on nil pointer-to-empty-struct dereference"}, "fixedbugs/issue27201.go": {desc: "incorrect stack trace for nil dereference in inlined function"}, @@ -121,7 +119,6 @@ var knownFails = map[string]failReason{ // These are new tests in Go 1.12.9. "fixedbugs/issue30977.go": {category: neverTerminates, desc: "does for { runtime.GC() }"}, "fixedbugs/issue32477.go": {category: notApplicable, desc: "uses runtime.SetFinalizer and runtime.GC"}, - "fixedbugs/issue32680.go": {category: notApplicable, desc: "uses -gcflags=-d=ssa/check/on flag"}, // These are new tests in Go 1.13-1.16. "fixedbugs/issue19113.go": {category: lowLevelRuntimeDifference, desc: "JavaScript bit shifts by negative amount don't cause an exception"}, @@ -134,7 +131,6 @@ var knownFails = map[string]failReason{ "fixedbugs/issue30116u.go": {desc: "GopherJS doesn't specify the array/slice index selector in the out-of-bounds message"}, "fixedbugs/issue34395.go": {category: neverTerminates, desc: "https://github.com/gopherjs/gopherjs/issues/1007"}, "fixedbugs/issue35027.go": {category: usesUnsupportedPackage, desc: "uses unsupported conversion to reflect.SliceHeader and -gcflags=-d=checkptr"}, - "fixedbugs/issue35073.go": {category: usesUnsupportedPackage, desc: "uses unsupported flag -gcflags=-d=checkptr"}, "fixedbugs/issue35576.go": {category: lowLevelRuntimeDifference, desc: "GopherJS print/println format for floats differs from Go's"}, "fixedbugs/issue40917.go": {category: notApplicable, desc: "uses pointer arithmetic and unsupported flag -gcflags=-d=checkptr"}, @@ -149,17 +145,22 @@ var knownFails = map[string]failReason{ "fixedbugs/issue50854.go": {category: lowLevelRuntimeDifference, desc: "negative int32 overflow behaves differently in JS"}, // These are new tests in Go 1.18 - "fixedbugs/issue46938.go": {category: notApplicable, desc: "tests -d=checkptr compiler mode, which GopherJS doesn't support"}, - "fixedbugs/issue47928.go": {category: notApplicable, desc: "//go:nointerface is a part of GOEXPERIMENT=fieldtrack and is not supported by GopherJS"}, - "fixedbugs/issue48898.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1128"}, - "fixedbugs/issue48536.go": {category: usesUnsupportedPackage, desc: "https://github.com/gopherjs/gopherjs/issues/1130"}, - "fixedbugs/issue53600.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, + "fixedbugs/issue47928.go": {category: notApplicable, desc: "//go:nointerface is a part of GOEXPERIMENT=fieldtrack and is not supported by GopherJS"}, + "fixedbugs/issue48536.go": {category: usesUnsupportedPackage, desc: "https://github.com/gopherjs/gopherjs/issues/1130"}, + "fixedbugs/issue48898.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1128"}, + "fixedbugs/issue53600.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, + "typeparam/chans.go": {category: neverTerminates, desc: "uses runtime.SetFinalizer() and runtime.GC()."}, + "typeparam/issue51733.go": {category: usesUnsupportedPackage, desc: "unsafe: uintptr to struct pointer conversion is unsupported"}, + "typeparam/typeswitch5.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, + + // Failures related to the lack of generics support. Ideally, this section + // should be emptied once https://github.com/gopherjs/gopherjs/issues/1013 is + // fixed. + "typeparam/nested.go": {category: usesUnsupportedGenerics, desc: "incomplete support for generic types inside generic functions"}, // These are new tests in Go 1.19 - "fixedbugs/issue50672.go": {category: usesUnsupportedGenerics, desc: "Checking function nesting with one function having a type parameter."}, - "fixedbugs/issue53137.go": {category: usesUnsupportedGenerics, desc: "Checking setting type parameter of struct in parameter of a generic function."}, - "fixedbugs/issue53309.go": {category: usesUnsupportedGenerics, desc: "Checking unused type parameter in method call to interface"}, - "fixedbugs/issue53635.go": {category: usesUnsupportedGenerics, desc: "Checking switch type against nil type with unsupported type parameters"}, + "typeparam/issue51521.go": {category: lowLevelRuntimeDifference, desc: "different panic message when calling a method on nil interface"}, + "fixedbugs/issue50672.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1271"}, "fixedbugs/issue53653.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format of int64 is different from Go's"}, } @@ -202,7 +203,7 @@ var ( // dirs are the directories to look for *.go files in. // TODO(bradfitz): just use all directories? - dirs = []string{".", "ken", "chan", "interface", "syntax", "dwarf", "fixedbugs"} + dirs = []string{".", "ken", "chan", "interface", "syntax", "dwarf", "fixedbugs", "typeparam"} // ratec controls the max number of tests running at a time. ratec chan bool @@ -366,6 +367,7 @@ func goFiles(dir string) []string { f, err := os.Open(dir) check(err) dirnames, err := f.Readdirnames(-1) + f.Close() check(err) names := []string{} for _, name := range dirnames { @@ -520,36 +522,19 @@ func shouldTest(src string, goos, goarch string) (ok bool, whyNot string) { } for _, line := range strings.Split(src, "\n") { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "//") { - line = line[2:] - } else { - continue - } - line = strings.TrimSpace(line) - if len(line) == 0 || line[0] != '+' { - continue + if strings.HasPrefix(line, "package ") { + break } - ctxt := &context{ - GOOS: goos, - GOARCH: goarch, - } - words := strings.Fields(line) - if words[0] == "+build" { - ok := false - for _, word := range words[1:] { - if ctxt.match(word) { - ok = true - break - } + if expr, err := constraint.Parse(line); err == nil { + ctxt := &context{ + GOOS: goos, + GOARCH: goarch, } - if !ok { - // no matching tag found. + if !expr.Eval(ctxt.match) { return false, line } } } - // no build tags return true, "" } @@ -557,16 +542,6 @@ func (ctxt *context) match(name string) bool { if name == "" { return false } - if i := strings.Index(name, ","); i >= 0 { - // comma-separated list - return ctxt.match(name[:i]) && ctxt.match(name[i+1:]) - } - if strings.HasPrefix(name, "!!") { // bad syntax, reject always - return false - } - if strings.HasPrefix(name, "!") { // negation - return len(name) > 1 && !ctxt.match(name[1:]) - } // Tags must be letters, digits, underscores or dots. // Unlike in Go identifiers, all digits are fine (e.g., "386"). @@ -576,10 +551,18 @@ func (ctxt *context) match(name string) bool { } } + // GOPHERJS: Ignore "goexperiment." for now + // GOPHERJS: Don't match "cgo" since not supported + // GOPHERJS: Don't match "gc" if name == ctxt.GOOS || name == ctxt.GOARCH { return true } + // GOPHERJS: Don't match "gcflags_noopt" + if name == "test_run" { + return true + } + return false } @@ -611,26 +594,23 @@ func (t *test) run() { } // Execution recipe stops at first blank line. - pos := strings.Index(t.src, "\n\n") - if pos == -1 { - t.err = errors.New("double newline not found") + action, _, ok := strings.Cut(t.src, "\n\n") + if !ok { + t.err = fmt.Errorf("double newline ending execution recipe not found in %s", t.goFileName()) return } - action := t.src[:pos] - if nl := strings.Index(action, "\n"); nl >= 0 && strings.Contains(action[:nl], "+build") { + if firstLine, rest, ok := strings.Cut(action, "\n"); ok && strings.Contains(firstLine, "+build") { // skip first line - action = action[nl+1:] - } - if strings.HasPrefix(action, "//") { - action = action[2:] + action = rest } + action = strings.TrimPrefix(action, "//") // Check for build constraints only up to the actual code. - pkgPos := strings.Index(t.src, "\npackage") - if pkgPos == -1 { - pkgPos = pos // some files are intentionally malformed + header, _, ok := strings.Cut(t.src, "\npackage") + if !ok { + header = action // some files are intentionally malformed } - if ok, why := shouldTest(t.src[:pkgPos], goos, goarch); !ok { + if ok, why := shouldTest(header, goos, goarch); !ok { t.action = "skip" if *showSkips { fmt.Printf("%-20s %-20s: %s\n", t.action, t.goFileName(), why) @@ -640,16 +620,20 @@ func (t *test) run() { var args, flags []string wantError := false - f := strings.Fields(action) + f, err := splitQuoted(action) + if err != nil { + t.err = fmt.Errorf("invalid test recipe: %v", err) + return + } if len(f) > 0 { action = f[0] args = f[1:] } - // GOPHERJS: For now, only run with "run", "cmpout" actions, in "fixedbugs" dir. Skip all others. + // GOPHERJS: For now, only run with "run", "cmpout" actions, in "fixedbugs" and "typeparam" dirs. Skip all others. switch action { case "run", "cmpout": - if filepath.Clean(t.dir) != "fixedbugs" { + if d := filepath.Clean(t.dir); d != "fixedbugs" && d != "typeparam" { action = "skip" } default: @@ -699,6 +683,19 @@ func (t *test) run() { os.Setenv("GOARCH", goarch) } + { + // GopherJS: we don't support any of -gcflags, but for the most part they + // are not too relevant to the outcome of the test. + supportedArgs := []string{} + for _, a := range args { + if strings.HasPrefix(a, "-gcflags") { + continue + } + supportedArgs = append(supportedArgs, a) + } + args = supportedArgs + } + useTmp := true runcmd := func(args ...string) ([]byte, error) { cmd := exec.Command(args[0], args[1:]...) @@ -1263,3 +1260,65 @@ func getenv(key, def string) string { } return def } + +// splitQuoted splits the string s around each instance of one or more consecutive +// white space characters while taking into account quotes and escaping, and +// returns an array of substrings of s or an empty list if s contains only white space. +// Single quotes and double quotes are recognized to prevent splitting within the +// quoted region, and are removed from the resulting substrings. If a quote in s +// isn't closed err will be set and r will have the unclosed argument as the +// last element. The backslash is used for escaping. +// +// For example, the following string: +// +// a b:"c d" 'e''f' "g\"" +// +// Would be parsed as: +// +// []string{"a", "b:c d", "ef", `g"`} +// +// [copied from src/go/build/build.go] +func splitQuoted(s string) (r []string, err error) { + var args []string + arg := make([]rune, len(s)) + escaped := false + quoted := false + quote := '\x00' + i := 0 + for _, rune := range s { + switch { + case escaped: + escaped = false + case rune == '\\': + escaped = true + continue + case quote != '\x00': + if rune == quote { + quote = '\x00' + continue + } + case rune == '"' || rune == '\'': + quoted = true + quote = rune + continue + case unicode.IsSpace(rune): + if quoted || i > 0 { + quoted = false + args = append(args, string(arg[:i])) + i = 0 + } + continue + } + arg[i] = rune + i++ + } + if quoted || i > 0 { + args = append(args, string(arg[:i])) + } + if quote != 0 { + err = errors.New("unclosed quote") + } else if escaped { + err = errors.New("unfinished escaping") + } + return args, err +} diff --git a/tests/map_js_test.go b/tests/map_js_test.go index 64cc8e6f0..c815661ab 100644 --- a/tests/map_js_test.go +++ b/tests/map_js_test.go @@ -69,7 +69,7 @@ func Test_MapStructObjectWrapper(t *testing.T) { stringMap := map[string]string{"key": "value"} - // You cannot wrap a map directly, so put it in a stuct. + // You cannot wrap a map directly, so put it in a struct. type StructWithMap struct { Map map[string]string } diff --git a/tests/misc_test.go b/tests/misc_test.go index a38d91c81..8dc3be924 100644 --- a/tests/misc_test.go +++ b/tests/misc_test.go @@ -900,7 +900,7 @@ func TestReflectSetForEmbed(t *testing.T) { f0 := e.Field(0) e.Set(in) if e.Field(0) != f0 { - t.Fatalf("relfect.Set got %v, want %v", f0, e.Field(0)) + t.Fatalf("reflect.Set got %v, want %v", f0, e.Field(0)) } }