diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5c4677658..305891173 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,6 +8,7 @@ on: permissions: contents: read + pull-requests: read concurrency: group: ci-${{ github.ref }} @@ -17,7 +18,6 @@ env: GO_VERSION: 1.20.14 NODE_VERSION: 18 GOLANGCI_VERSION: v1.53.3 - GOPHERJS_EXPERIMENT: generics SOURCE_MAP_SUPPORT: true GOPATH: ${{ github.workspace }}/go GOPHERJS_PATH: ${{ github.workspace }}/go/src/github.com/${{ github.repository }} @@ -114,8 +114,9 @@ jobs: run: go mod tidy && git diff --exit-code - name: Check natives build tags working-directory: ${{ env.GOPHERJS_PATH }} - # All those packages should have // +build js. - run: diff -u <(echo -n) <(go list ./compiler/natives/src/...) + run: | + echo "Any following packages have at least one file that is missing //go:build js" + diff -u <(echo -n) <(go list ./compiler/natives/src/...) go_tests: name: Go Tests diff --git a/README.md b/README.md index d17409736..eac9d4405 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,7 @@ _Note: GopherJS will try to write compiled object files of the core packages to #### gopherjs run, gopherjs test -If you want to use `gopherjs run` or `gopherjs test` to run the generated code locally, install Node.js 10.0.0 (or newer), and the `source-map-support` module: - -``` -npm install --global source-map-support -``` +If you want to use `gopherjs run` or `gopherjs test` to run the generated code locally, install Node.js 18 (or newer). On supported `GOOS` platforms, it's possible to make system calls (file system access, etc.) available. See [doc/syscalls.md](https://github.com/gopherjs/gopherjs/blob/master/doc/syscalls.md) for instructions on how to do so. diff --git a/build/build.go b/build/build.go index 42b979846..6ff365aa2 100644 --- a/build/build.go +++ b/build/build.go @@ -21,17 +21,20 @@ import ( "sort" "strconv" "strings" + "sync" "time" "github.com/fsnotify/fsnotify" "github.com/gopherjs/gopherjs/compiler" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/jsFile" + "github.com/gopherjs/gopherjs/compiler/sources" + "github.com/gopherjs/gopherjs/internal/errorList" + "github.com/gopherjs/gopherjs/internal/testmain" log "github.com/sirupsen/logrus" "github.com/neelance/sourcemap" "golang.org/x/tools/go/buildutil" - - "github.com/gopherjs/gopherjs/build/cache" ) // DefaultGOROOT is the default GOROOT value for builds. @@ -163,7 +166,7 @@ type overrideInfo struct { // - Otherwise for identifiers that exist in the original and the overrides, // the original is removed. // - New identifiers that don't exist in original package get added. -func parseAndAugment(xctx XContext, pkg *PackageData, isTest bool, fileSet *token.FileSet) ([]*ast.File, []JSFile, error) { +func parseAndAugment(xctx XContext, pkg *PackageData, isTest bool, fileSet *token.FileSet) ([]*ast.File, []jsFile.JSFile, error) { jsFiles, overlayFiles := parseOverlayFiles(xctx, pkg, isTest, fileSet) originalFiles, err := parserOriginalFiles(pkg, fileSet) @@ -192,7 +195,7 @@ func parseAndAugment(xctx XContext, pkg *PackageData, isTest bool, fileSet *toke // parseOverlayFiles loads and parses overlay files // to augment the original files with. -func parseOverlayFiles(xctx XContext, pkg *PackageData, isTest bool, fileSet *token.FileSet) ([]JSFile, []*ast.File) { +func parseOverlayFiles(xctx XContext, pkg *PackageData, isTest bool, fileSet *token.FileSet) ([]jsFile.JSFile, []*ast.File) { isXTest := strings.HasSuffix(pkg.ImportPath, "_test") importPath := pkg.ImportPath if isXTest { @@ -238,7 +241,7 @@ func parseOverlayFiles(xctx XContext, pkg *PackageData, isTest bool, fileSet *to // parserOriginalFiles loads and parses the original files to augment. func parserOriginalFiles(pkg *PackageData, fileSet *token.FileSet) ([]*ast.File, error) { var files []*ast.File - var errList compiler.ErrorList + var errList errorList.ErrorList for _, name := range pkg.GoFiles { if !filepath.IsAbs(name) { // name might be absolute if specified directly. E.g., `gopherjs build /abs/file.go`. name = filepath.Join(pkg.Dir, name) @@ -622,18 +625,11 @@ func (o *Options) PrintSuccess(format string, a ...interface{}) { fmt.Fprintf(os.Stderr, format, a...) } -// JSFile represents a *.inc.js file metadata and content. -type JSFile struct { - Path string // Full file path for the build context the file came from. - ModTime time.Time - Content []byte -} - // PackageData is an extension of go/build.Package with additional metadata // GopherJS requires. type PackageData struct { *build.Package - JSFiles []JSFile + JSFiles []jsFile.JSFile // IsTest is true if the package is being built for running tests. IsTest bool SrcModTime time.Time @@ -762,15 +758,28 @@ func (p *PackageData) InstallPath() (string, error) { // This is the main interface to GopherJS build system. Session lifetime is // roughly equivalent to a single GopherJS tool invocation. type Session struct { - options *Options - xctx XContext - buildCache cache.BuildCache + options *Options + xctx XContext + + // importPaths is a map of the resolved import paths given the + // source directory (first key) and the unresolved import path (second key). + // This is used to cache the resolved import returned from XContext.Import. + // XContent.Import can be slow, so we cache the resolved path that is used + // as the map key by parsedPackages and UpToDateArchives. + // This makes subsequent lookups faster during compilation when all we have + // is the unresolved import path and source directory. + importPaths map[string]map[string]string + + // sources is a map of parsed packages that have been built and augmented. + // This is keyed using resolved import paths. This is used to avoid + // rebuilding and augmenting packages that are imported by several packages. + // The files in these sources haven't been sorted nor simplified yet. + sources map[string]*sources.Sources // Binary archives produced during the current session and assumed to be // up to date with input sources and dependencies. In the -w ("watch") mode // must be cleared upon entering watching. UpToDateArchives map[string]*compiler.Archive - Types map[string]*types.Package Watcher *fsnotify.Watcher } @@ -780,6 +789,8 @@ func NewSession(options *Options) (*Session, error) { s := &Session{ options: options, + importPaths: make(map[string]map[string]string), + sources: make(map[string]*sources.Sources), UpToDateArchives: make(map[string]*compiler.Archive), } s.xctx = NewBuildContext(s.InstallSuffix(), s.options.BuildTags) @@ -790,16 +801,6 @@ func NewSession(options *Options) (*Session, error) { return nil, err } - s.buildCache = cache.BuildCache{ - GOOS: env.GOOS, - GOARCH: env.GOARCH, - GOROOT: env.GOROOT, - GOPATH: env.GOPATH, - BuildTags: append([]string{}, env.BuildTags...), - Minify: options.Minify, - TestedPackage: options.TestedPackage, - } - s.Types = make(map[string]*types.Package) if options.Watch { if out, err := exec.Command("ulimit", "-n").Output(); err == nil { if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil && n < 1024 { @@ -906,36 +907,107 @@ func (s *Session) BuildFiles(filenames []string, pkgObj string, cwd string) erro if err != nil { return fmt.Errorf("failed to stat %s: %w", file, err) } - pkg.JSFiles = append(pkg.JSFiles, JSFile{ + pkg.JSFiles = append(pkg.JSFiles, jsFile.JSFile{ Path: filepath.Join(pkg.Dir, filepath.Base(file)), ModTime: info.ModTime(), Content: content, }) } - archive, err := s.BuildPackage(pkg) + archive, err := s.BuildProject(pkg) if err != nil { return err } - if s.Types["main"].Name() != "main" { + if s.sources["main"].Package.Name() != "main" { return fmt.Errorf("cannot build/run non-main package") } return s.WriteCommandPackage(archive, pkgObj) } -// BuildImportPath loads and compiles package with the given import path. -// -// Relative paths are interpreted relative to the current working dir. -func (s *Session) BuildImportPath(path string) (*compiler.Archive, error) { - _, archive, err := s.buildImportPathWithSrcDir(path, "") - return archive, err +// BuildProject builds a command project (one with a main method) or +// builds a test project (one with a synthesized test main package). +func (s *Session) BuildProject(pkg *PackageData) (*compiler.Archive, error) { + // ensure that runtime for gopherjs is imported + pkg.Imports = append(pkg.Imports, `runtime`) + + // Load the project to get the sources for the parsed packages. + var rootSrcs *sources.Sources + var err error + if pkg.IsTest { + rootSrcs, err = s.loadTestPackage(pkg) + } else { + rootSrcs, err = s.loadPackages(pkg) + } + if err != nil { + return nil, err + } + + // TODO(grantnelson-wf): We could investigate caching the results of + // the sources prior to preparing them to avoid re-parsing the same + // sources and augmenting them when the files on disk haven't changed. + // This would require a way to determine if the sources are up-to-date + // which could be done with the left over srcModTime from when the archives + // were being cached. + + // Compile the project into Archives containing the generated JS. + return s.prepareAndCompilePackages(rootSrcs) } -// buildImportPathWithSrcDir builds the package specified by the import path. +// getSortedSources returns the sources sorted by import path. +// The files in the sources may still not be sorted yet. +func (s *Session) getSortedSources() []*sources.Sources { + allSources := make([]*sources.Sources, 0, len(s.sources)) + for _, srcs := range s.sources { + allSources = append(allSources, srcs) + } + sources.SortedSourcesSlice(allSources) + return allSources +} + +func (s *Session) loadTestPackage(pkg *PackageData) (*sources.Sources, error) { + _, err := s.loadPackages(pkg.TestPackage()) + if err != nil { + return nil, err + } + _, err = s.loadPackages(pkg.XTestPackage()) + if err != nil { + return nil, err + } + + // Generate a synthetic testmain package. + fset := token.NewFileSet() + tests := testmain.TestMain{Package: pkg.Package, Context: pkg.bctx} + tests.Scan(fset) + mainPkg, mainFile, err := tests.Synthesize(fset) + if err != nil { + return nil, fmt.Errorf("failed to generate testmain package for %s: %w", pkg.ImportPath, err) + } + + // Create the sources for parsed package for the testmain package. + srcs := &sources.Sources{ + ImportPath: mainPkg.ImportPath, + Dir: mainPkg.Dir, + Files: []*ast.File{mainFile}, + FileSet: fset, + } + s.sources[srcs.ImportPath] = srcs + + // Import dependencies for the testmain package. + for _, importedPkgPath := range srcs.UnresolvedImports() { + _, _, err := s.loadImportPathWithSrcDir(importedPkgPath, pkg.Dir) + if err != nil { + return nil, err + } + } + + return srcs, nil +} + +// loadImportPathWithSrcDir gets the parsed package specified by the import path. // -// Relative import paths are interpreted relative to the passed srcDir. If -// srcDir is empty, current working directory is assumed. -func (s *Session) buildImportPathWithSrcDir(path string, srcDir string) (*PackageData, *compiler.Archive, error) { +// Relative import paths are interpreted relative to the passed srcDir. +// If srcDir is empty, current working directory is assumed. +func (s *Session) loadImportPathWithSrcDir(path, srcDir string) (*PackageData, *sources.Sources, error) { pkg, err := s.xctx.Import(path, srcDir, 0) if s.Watcher != nil && pkg != nil { // add watch even on error s.Watcher.Add(pkg.Dir) @@ -944,65 +1016,85 @@ func (s *Session) buildImportPathWithSrcDir(path string, srcDir string) (*Packag return nil, nil, err } - archive, err := s.BuildPackage(pkg) + srcs, err := s.loadPackages(pkg) if err != nil { return nil, nil, err } - return pkg, archive, nil + s.cacheImportPath(path, srcDir, pkg.ImportPath) + return pkg, srcs, nil } -// BuildPackage compiles an already loaded package. -func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) { - if archive, ok := s.UpToDateArchives[pkg.ImportPath]; ok { - return archive, nil +// cacheImportPath stores the resolved import path for the build package +// so we can look it up later without getting the whole build package. +// The given path and source directly are the ones passed into +// XContext.Import to the get the build package originally. +func (s *Session) cacheImportPath(path, srcDir, importPath string) { + if paths, ok := s.importPaths[srcDir]; ok { + paths[path] = importPath + } else { + s.importPaths[srcDir] = map[string]string{path: importPath} } +} - var fileInfo os.FileInfo - gopherjsBinary, err := os.Executable() - if err == nil { - fileInfo, err = os.Stat(gopherjsBinary) - if err == nil && fileInfo.ModTime().After(pkg.SrcModTime) { - pkg.SrcModTime = fileInfo.ModTime() +// getExeModTime will determine the mod time of the GopherJS binary +// the first time this is called and cache the result for subsequent calls. +var getExeModTime = func() func() time.Time { + var ( + once sync.Once + result time.Time + ) + getTime := func() { + gopherjsBinary, err := os.Executable() + if err == nil { + var fileInfo os.FileInfo + fileInfo, err = os.Stat(gopherjsBinary) + if err == nil { + result = fileInfo.ModTime() + return + } } - } - if err != nil { os.Stderr.WriteString("Could not get GopherJS binary's modification timestamp. Please report issue.\n") - pkg.SrcModTime = time.Now() + result = time.Now() + } + return func() time.Time { + once.Do(getTime) + return result + } +}() + +// loadPackages will recursively load and parse the given package and +// its dependencies. This will return the sources for the given package. +// The returned source and sources for the dependencies will be added +// to the session's sources map. +func (s *Session) loadPackages(pkg *PackageData) (*sources.Sources, error) { + if srcs, ok := s.sources[pkg.ImportPath]; ok { + return srcs, nil + } + + if exeModTime := getExeModTime(); exeModTime.After(pkg.SrcModTime) { + pkg.SrcModTime = exeModTime } for _, importedPkgPath := range pkg.Imports { if importedPkgPath == "unsafe" { continue } - importedPkg, _, err := s.buildImportPathWithSrcDir(importedPkgPath, pkg.Dir) + importedPkg, _, err := s.loadImportPathWithSrcDir(importedPkgPath, pkg.Dir) if err != nil { return nil, err } - impModTime := importedPkg.SrcModTime - if impModTime.After(pkg.SrcModTime) { + if impModTime := importedPkg.SrcModTime; impModTime.After(pkg.SrcModTime) { pkg.SrcModTime = impModTime } } - if pkg.FileModTime().After(pkg.SrcModTime) { - pkg.SrcModTime = pkg.FileModTime() + if fileModTime := pkg.FileModTime(); fileModTime.After(pkg.SrcModTime) { + pkg.SrcModTime = fileModTime } - if !s.options.NoCache { - archive := s.buildCache.LoadArchive(pkg.ImportPath) - if archive != nil && !pkg.SrcModTime.After(archive.BuildTime) { - if err := archive.RegisterTypes(s.Types); err != nil { - panic(fmt.Errorf("failed to load type information from %v: %w", archive, err)) - } - s.UpToDateArchives[pkg.ImportPath] = archive - // Existing archive is up to date, no need to build it from scratch. - return archive, nil - } - } - - // Existing archive is out of date or doesn't exist, let's build the package. + // Build the package by parsing and augmenting the original files with overlay files. fileSet := token.NewFileSet() files, overlayJsFiles, err := parseAndAugment(s.xctx, pkg, pkg.IsTest, fileSet) if err != nil { @@ -1016,40 +1108,125 @@ func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) { files = append(files, embed) } - importContext := &compiler.ImportContext{ - Packages: s.Types, - Import: s.ImportResolverFor(pkg), + srcs := &sources.Sources{ + ImportPath: pkg.ImportPath, + Dir: pkg.Dir, + Files: files, + FileSet: fileSet, + JSFiles: append(pkg.JSFiles, overlayJsFiles...), + } + s.sources[pkg.ImportPath] = srcs + + // Import dependencies from the augmented files, + // whilst skipping any that have been already imported. + for _, importedPkgPath := range srcs.UnresolvedImports(pkg.Imports...) { + _, _, err := s.loadImportPathWithSrcDir(importedPkgPath, pkg.Dir) + if err != nil { + return nil, err + } + } + + return srcs, nil +} + +func (s *Session) prepareAndCompilePackages(rootSrcs *sources.Sources) (*compiler.Archive, error) { + tContext := types.NewContext() + allSources := s.getSortedSources() + + // Prepare and analyze the source code. + // This will be performed recursively for all dependencies. + if err := compiler.PrepareAllSources(allSources, s.SourcesForImport, tContext); err != nil { + return nil, err + } + + // Compile all the sources into archives. + for _, srcs := range allSources { + if _, err := s.compilePackage(srcs, tContext); err != nil { + return nil, err + } + } + + rootArchive, ok := s.UpToDateArchives[rootSrcs.ImportPath] + if !ok { + // This is confirmation that the root package is in the sources map and got compiled. + return nil, fmt.Errorf(`root package %q was not found in archives`, rootSrcs.ImportPath) } - archive, err := compiler.Compile(pkg.ImportPath, files, fileSet, importContext, s.options.Minify) + return rootArchive, nil +} + +func (s *Session) compilePackage(srcs *sources.Sources, tContext *types.Context) (*compiler.Archive, error) { + if archive, ok := s.UpToDateArchives[srcs.ImportPath]; ok { + return archive, nil + } + + archive, err := compiler.Compile(srcs, tContext, s.options.Minify) if err != nil { return nil, err } - for _, jsFile := range append(pkg.JSFiles, overlayJsFiles...) { + for _, jsFile := range srcs.JSFiles { archive.IncJSCode = append(archive.IncJSCode, []byte("\t(function() {\n")...) archive.IncJSCode = append(archive.IncJSCode, jsFile.Content...) archive.IncJSCode = append(archive.IncJSCode, []byte("\n\t}).call($global);\n")...) } if s.options.Verbose { - fmt.Println(pkg.ImportPath) + fmt.Println(srcs.ImportPath) } - s.buildCache.StoreArchive(archive) - s.UpToDateArchives[pkg.ImportPath] = archive + s.UpToDateArchives[srcs.ImportPath] = archive return archive, nil } +func (s *Session) getImportPath(path, srcDir string) (string, error) { + // If path is for an xtest package, just return it. + if strings.HasSuffix(path, "_test") { + return path, nil + } + + // Check if the import path is already cached. + if importPath, ok := s.importPaths[srcDir][path]; ok { + return importPath, nil + } + + // Fall back to the slow import of the build package. + pkg, err := s.xctx.Import(path, srcDir, 0) + if err != nil { + return ``, err + } + s.cacheImportPath(path, srcDir, pkg.ImportPath) + return pkg.ImportPath, nil +} + +func (s *Session) SourcesForImport(path, srcDir string) (*sources.Sources, error) { + importPath, err := s.getImportPath(path, srcDir) + if err != nil { + return nil, err + } + + srcs, ok := s.sources[importPath] + if !ok { + return nil, fmt.Errorf(`sources for %q not found`, path) + } + + return srcs, nil +} + // ImportResolverFor returns a function which returns a compiled package archive // given an import path. -func (s *Session) ImportResolverFor(pkg *PackageData) func(string) (*compiler.Archive, error) { +func (s *Session) ImportResolverFor(srcDir string) func(string) (*compiler.Archive, error) { return func(path string) (*compiler.Archive, error) { - if archive, ok := s.UpToDateArchives[path]; ok { + importPath, err := s.getImportPath(path, srcDir) + if err != nil { + return nil, err + } + + if archive, ok := s.UpToDateArchives[importPath]; ok { return archive, nil } - _, archive, err := s.buildImportPathWithSrcDir(path, pkg.Dir) - return archive, err + + return nil, fmt.Errorf(`archive for %q not found`, importPath) } } @@ -1087,13 +1264,7 @@ func (s *Session) WriteCommandPackage(archive *compiler.Archive, pkgObj string) sourceMapFilter.MappingCallback = s.SourceMappingCallback(m) } - deps, err := compiler.ImportDependencies(archive, func(path string) (*compiler.Archive, error) { - if archive, ok := s.UpToDateArchives[path]; ok { - return archive, nil - } - _, archive, err := s.buildImportPathWithSrcDir(path, "") - return archive, err - }) + deps, err := compiler.ImportDependencies(archive, s.ImportResolverFor("")) if err != nil { return err } @@ -1143,8 +1314,9 @@ func hasGopathPrefix(file, gopath string) (hasGopathPrefix bool, prefixLen int) func (s *Session) WaitForChange() { // Will need to re-validate up-to-dateness of all archives, so flush them from // memory. + s.importPaths = map[string]map[string]string{} + s.sources = map[string]*sources.Sources{} s.UpToDateArchives = map[string]*compiler.Archive{} - s.Types = map[string]*types.Package{} s.options.PrintSuccess("watching for changes...\n") for { diff --git a/build/cache/cache.go b/build/cache/cache.go index 2c4e5703a..fc0949d67 100644 --- a/build/cache/cache.go +++ b/build/cache/cache.go @@ -6,9 +6,11 @@ import ( "crypto/sha256" "fmt" "go/build" + "go/types" "os" "path" "path/filepath" + "time" "github.com/gopherjs/gopherjs/compiler" log "github.com/sirupsen/logrus" @@ -90,7 +92,10 @@ func (bc BuildCache) String() string { // StoreArchive compiled archive in the cache. Any error inside this method // will cause the cache not to be persisted. -func (bc *BuildCache) StoreArchive(a *compiler.Archive) { +// +// The passed in buildTime is used to determine if the archive is out-of-date when reloaded. +// Typically it should be set to the srcModTime or time.Now(). +func (bc *BuildCache) StoreArchive(a *compiler.Archive, buildTime time.Time) { if bc == nil { return // Caching is disabled. } @@ -106,7 +111,7 @@ func (bc *BuildCache) StoreArchive(a *compiler.Archive) { return } defer f.Close() - if err := compiler.WriteArchive(a, f); err != nil { + if err := compiler.WriteArchive(a, buildTime, f); err != nil { log.Warningf("Failed to write build cache archive %q: %v", a, err) // Make sure we don't leave a half-written archive behind. os.Remove(f.Name()) @@ -125,7 +130,10 @@ func (bc *BuildCache) StoreArchive(a *compiler.Archive) { // // The returned archive would have been built with the same configuration as // the build cache was. -func (bc *BuildCache) LoadArchive(importPath string) *compiler.Archive { +// +// The imports map is used to resolve package dependencies and may modify the +// map to include the package from the read archive. See [gcexportdata.Read]. +func (bc *BuildCache) LoadArchive(importPath string, srcModTime time.Time, imports map[string]*types.Package) *compiler.Archive { if bc == nil { return nil // Caching is disabled. } @@ -140,12 +148,16 @@ func (bc *BuildCache) LoadArchive(importPath string) *compiler.Archive { return nil // Cache miss. } defer f.Close() - a, err := compiler.ReadArchive(importPath, f) + a, buildTime, err := compiler.ReadArchive(importPath, f, srcModTime, imports) if err != nil { log.Warningf("Failed to read cached package archive for %q: %v", importPath, err) return nil // Invalid/corrupted archive, cache miss. } - log.Infof("Found cached package archive for %q, built at %v.", importPath, a.BuildTime) + if a == nil { + log.Infof("Found out-of-date package archive for %q, built at %v.", importPath, buildTime) + return nil // Archive is out-of-date, cache miss. + } + log.Infof("Found cached package archive for %q, built at %v.", importPath, buildTime) return a } diff --git a/build/cache/cache_test.go b/build/cache/cache_test.go index fd89ec187..0a0541f64 100644 --- a/build/cache/cache_test.go +++ b/build/cache/cache_test.go @@ -1,7 +1,9 @@ package cache import ( + "go/types" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/gopherjs/gopherjs/compiler" @@ -15,21 +17,24 @@ func TestStore(t *testing.T) { Imports: []string{"fake/dep"}, } + srcModTime := newTime(0.0) + buildTime := newTime(5.0) + imports := map[string]*types.Package{} bc := BuildCache{} - if got := bc.LoadArchive(want.ImportPath); got != nil { + if got := bc.LoadArchive(want.ImportPath, srcModTime, imports); got != nil { t.Errorf("Got: %s was found in the cache. Want: empty cache.", got.ImportPath) } - bc.StoreArchive(want) - got := bc.LoadArchive(want.ImportPath) + bc.StoreArchive(want, buildTime) + got := bc.LoadArchive(want.ImportPath, srcModTime, imports) if got == nil { - t.Errorf("Got: %s wan not found in the cache. Want: archive is can be loaded after store.", want.ImportPath) + t.Errorf("Got: %s was not found in the cache. Want: archive is can be loaded after store.", want.ImportPath) } if diff := cmp.Diff(want, got); diff != "" { t.Errorf("Loaded archive is different from stored (-want,+got):\n%s", diff) } // Make sure the package names are a part of the cache key. - if got := bc.LoadArchive("fake/other"); got != nil { + if got := bc.LoadArchive("fake/other", srcModTime, imports); got != nil { t.Errorf("Got: fake/other was found in cache: %#v. Want: nil for packages that weren't cached.", got) } } @@ -59,20 +64,54 @@ func TestInvalidation(t *testing.T) { }, } + srcModTime := newTime(0.0) + buildTime := newTime(5.0) + imports := map[string]*types.Package{} for _, test := range tests { a := &compiler.Archive{ImportPath: "package/fake"} - test.cache1.StoreArchive(a) + test.cache1.StoreArchive(a, buildTime) - if got := test.cache2.LoadArchive(a.ImportPath); got != nil { + if got := test.cache2.LoadArchive(a.ImportPath, srcModTime, imports); got != nil { t.Logf("-cache1,+cache2:\n%s", cmp.Diff(test.cache1, test.cache2)) t.Errorf("Got: %v loaded from cache. Want: build parameter change invalidates cache.", got) } } } +func TestOldArchive(t *testing.T) { + cacheForTest(t) + + want := &compiler.Archive{ + ImportPath: "fake/package", + Imports: []string{"fake/dep"}, + } + + buildTime := newTime(5.0) + imports := map[string]*types.Package{} + bc := BuildCache{} + bc.StoreArchive(want, buildTime) + + oldSrcModTime := newTime(2.0) // older than archive build time, so archive is up-to-date + got := bc.LoadArchive(want.ImportPath, oldSrcModTime, imports) + if got == nil { + t.Errorf("Got: %s was nil. Want: up-to-date archive to be loaded.", want.ImportPath) + } + + newerSrcModTime := newTime(7.0) // newer than archive build time, so archive is stale + got = bc.LoadArchive(want.ImportPath, newerSrcModTime, imports) + if got != nil { + t.Errorf("Got: %s was not nil. Want: stale archive to not be loaded.", want.ImportPath) + } +} + func cacheForTest(t *testing.T) { t.Helper() originalRoot := cacheRoot t.Cleanup(func() { cacheRoot = originalRoot }) cacheRoot = t.TempDir() } + +func newTime(seconds float64) time.Time { + return time.Date(1969, 7, 20, 20, 17, 0, 0, time.UTC). + Add(time.Duration(seconds * float64(time.Second))) +} diff --git a/build/context.go b/build/context.go index 316bfb2bb..657300839 100644 --- a/build/context.go +++ b/build/context.go @@ -4,7 +4,6 @@ import ( "fmt" "go/build" "go/token" - "io" "net/http" "os" "os/exec" @@ -16,6 +15,7 @@ import ( _ "github.com/gopherjs/gopherjs/build/versionhack" // go/build release tags hack. "github.com/gopherjs/gopherjs/compiler" "github.com/gopherjs/gopherjs/compiler/gopherjspkg" + "github.com/gopherjs/gopherjs/compiler/jsFile" "github.com/gopherjs/gopherjs/compiler/natives" "golang.org/x/tools/go/buildutil" ) @@ -91,7 +91,7 @@ func (sc simpleCtx) Import(importPath string, srcDir string, mode build.ImportMo if err != nil { return nil, err } - jsFiles, err := jsFilesFromDir(&sc.bctx, pkg.Dir) + jsFiles, err := jsFile.JSFilesFromDir(&sc.bctx, pkg.Dir) if err != nil { return nil, fmt.Errorf("failed to enumerate .inc.js files in %s: %w", pkg.Dir, err) } @@ -440,40 +440,3 @@ func updateImports(sources []string, importPos map[string][]token.Position) (new sort.Strings(newImports) return newImports, newImportPos } - -// jsFilesFromDir finds and loads any *.inc.js packages in the build context -// directory. -func jsFilesFromDir(bctx *build.Context, dir string) ([]JSFile, error) { - files, err := buildutil.ReadDir(bctx, dir) - if err != nil { - return nil, err - } - var jsFiles []JSFile - for _, file := range files { - if !strings.HasSuffix(file.Name(), ".inc.js") || file.IsDir() { - continue - } - if file.Name()[0] == '_' || file.Name()[0] == '.' { - continue // Skip "hidden" files that are typically ignored by the Go build system. - } - - path := buildutil.JoinPath(bctx, dir, file.Name()) - f, err := buildutil.OpenFile(bctx, path) - if err != nil { - return nil, fmt.Errorf("failed to open %s from %v: %w", path, bctx, err) - } - defer f.Close() - - content, err := io.ReadAll(f) - if err != nil { - return nil, fmt.Errorf("failed to read %s from %v: %w", path, bctx, err) - } - - jsFiles = append(jsFiles, JSFile{ - Path: path, - ModTime: file.ModTime(), - Content: content, - }) - } - return jsFiles, nil -} diff --git a/build/embed.go b/build/embed.go index c749eeb50..a68fb9494 100644 --- a/build/embed.go +++ b/build/embed.go @@ -8,7 +8,7 @@ import ( "go/token" "strconv" - "github.com/visualfc/goembed" + "github.com/msvitok77/goembed" ) func buildIdent(name string) string { diff --git a/compiler/analysis/info.go b/compiler/analysis/info.go deleted file mode 100644 index 44ea6c165..000000000 --- a/compiler/analysis/info.go +++ /dev/null @@ -1,407 +0,0 @@ -package analysis - -import ( - "fmt" - "go/ast" - "go/token" - "go/types" - "strings" - - "github.com/gopherjs/gopherjs/compiler/astutil" - "github.com/gopherjs/gopherjs/compiler/typesutil" -) - -type continueStmt struct { - forStmt *ast.ForStmt - analyzeStack astPath -} - -func newContinueStmt(forStmt *ast.ForStmt, stack astPath) continueStmt { - cs := continueStmt{ - forStmt: forStmt, - analyzeStack: stack.copy(), - } - return cs -} - -// astPath is a list of AST nodes where each previous node is a parent of the -// next node. -type astPath []ast.Node - -func (src astPath) copy() astPath { - dst := make(astPath, len(src)) - copy(dst, src) - return dst -} - -func (ap astPath) String() string { - s := &strings.Builder{} - s.WriteString("[") - for i, n := range ap { - if i > 0 { - s.WriteString(", ") - } - fmt.Fprintf(s, "%T(%p)", n, n) - } - s.WriteString("]") - return s.String() -} - -type Info struct { - *types.Info - Pkg *types.Package - HasPointer map[*types.Var]bool - FuncDeclInfos map[*types.Func]*FuncInfo - FuncLitInfos map[*ast.FuncLit]*FuncInfo - InitFuncInfo *FuncInfo // Context for package variable initialization. - - isImportedBlocking func(*types.Func) bool // For functions from other packages. - allInfos []*FuncInfo -} - -func (info *Info) newFuncInfo(n ast.Node) *FuncInfo { - funcInfo := &FuncInfo{ - pkgInfo: info, - Flattened: make(map[ast.Node]bool), - Blocking: make(map[ast.Node]bool), - GotoLabel: make(map[*types.Label]bool), - localNamedCallees: make(map[*types.Func][]astPath), - literalFuncCallees: make(map[*ast.FuncLit][]astPath), - } - - // Register the function in the appropriate map. - switch n := n.(type) { - case *ast.FuncDecl: - if n.Body == nil { - // Function body comes from elsewhere (for example, from a go:linkname - // directive), conservatively assume that it may be blocking. - // TODO(nevkontakte): It is possible to improve accuracy of this detection. - // Since GopherJS supports inly "import-style" go:linkname, at this stage - // the compiler already determined whether the implementation function is - // blocking, and we could check that. - funcInfo.Blocking[n] = true - } - info.FuncDeclInfos[info.Defs[n.Name].(*types.Func)] = funcInfo - case *ast.FuncLit: - info.FuncLitInfos[n] = funcInfo - } - - // And add it to the list of all functions. - info.allInfos = append(info.allInfos, funcInfo) - - return funcInfo -} - -// IsBlocking returns true if the function may contain blocking calls or operations. -func (info *Info) IsBlocking(fun *types.Func) bool { - if funInfo := info.FuncDeclInfos[fun]; funInfo != nil { - return len(funInfo.Blocking) > 0 - } - panic(fmt.Errorf(`info did not have function declaration for %s`, fun.FullName())) -} - -// VarsWithInitializers returns a set of package-level variables that have -// explicit initializers. -func (info *Info) VarsWithInitializers() map[*types.Var]bool { - result := map[*types.Var]bool{} - for _, init := range info.InitOrder { - for _, o := range init.Lhs { - result[o] = true - } - } - return result -} - -func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(*types.Func) bool) *Info { - info := &Info{ - Info: typesInfo, - Pkg: typesPkg, - HasPointer: make(map[*types.Var]bool), - isImportedBlocking: isBlocking, - FuncDeclInfos: make(map[*types.Func]*FuncInfo), - FuncLitInfos: make(map[*ast.FuncLit]*FuncInfo), - } - info.InitFuncInfo = info.newFuncInfo(nil) - - // Traverse the full AST of the package and collect information about existing - // functions. - for _, file := range files { - ast.Walk(info.InitFuncInfo, file) - } - - for _, funcInfo := range info.allInfos { - if !funcInfo.HasDefer { - continue - } - // Conservatively assume that if a function has a deferred call, it might be - // blocking, and therefore all return statements need to be treated as - // blocking. - // TODO(nevkontakte): This could be improved by detecting whether a deferred - // call is actually blocking. Doing so might reduce generated code size a - // bit. - for _, returnStmt := range funcInfo.returnStmts { - funcInfo.markBlocking(returnStmt) - } - } - - // Propagate information about blocking calls to the caller functions. - // For each function we check all other functions it may call and if any of - // them are blocking, we mark the caller blocking as well. The process is - // repeated until no new blocking functions is detected. - for { - done := true - for _, caller := range info.allInfos { - // Check calls to named functions and function-typed variables. - for callee, callSites := range caller.localNamedCallees { - if info.IsBlocking(callee) { - for _, callSite := range callSites { - caller.markBlocking(callSite) - } - delete(caller.localNamedCallees, callee) - done = false - } - } - - // Check direct calls to function literals. - for callee, callSites := range caller.literalFuncCallees { - if len(info.FuncLitInfos[callee].Blocking) > 0 { - for _, callSite := range callSites { - caller.markBlocking(callSite) - } - delete(caller.literalFuncCallees, callee) - done = false - } - } - } - if done { - break - } - } - - // After all function blocking information was propagated, mark flow control - // statements as blocking whenever they may lead to a blocking function call. - for _, funcInfo := range info.allInfos { - for _, continueStmt := range funcInfo.continueStmts { - if funcInfo.Blocking[continueStmt.forStmt.Post] { - // If a for-loop post-expression is blocking, the continue statement - // that leads to it must be treated as blocking. - funcInfo.markBlocking(continueStmt.analyzeStack) - } - } - } - - return info -} - -type FuncInfo struct { - HasDefer bool - // Nodes are "flattened" into a switch-case statement when we need to be able - // to jump into an arbitrary position in the code with a GOTO statement, or - // resume a goroutine after a blocking call unblocks. - Flattened map[ast.Node]bool - // Blocking indicates that either the AST node itself or its descendant may - // block goroutine execution (for example, a channel operation). - Blocking map[ast.Node]bool - // GotoLavel indicates a label referenced by a goto statement, rather than a - // named loop. - GotoLabel map[*types.Label]bool - // List of continue statements in the function. - continueStmts []continueStmt - // List of return statements in the function. - returnStmts []astPath - // List of other named functions from the current package this function calls. - // If any of them are blocking, this function will become blocking too. - localNamedCallees map[*types.Func][]astPath - // List of function literals directly called from this function (for example: - // `func() { /* do stuff */ }()`). This is distinct from function literals - // assigned to named variables (for example: `doStuff := func() {}; - // doStuff()`), which are handled by localNamedCallees. If any of them are - // identified as blocking, this function will become blocking too. - literalFuncCallees map[*ast.FuncLit][]astPath - - pkgInfo *Info // Function's parent package. - visitorStack astPath -} - -func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { - if node == nil { - if len(fi.visitorStack) != 0 { - fi.visitorStack = fi.visitorStack[:len(fi.visitorStack)-1] - } - return nil - } - fi.visitorStack = append(fi.visitorStack, node) - - switch n := node.(type) { - case *ast.FuncDecl, *ast.FuncLit: - // Analyze the function in its own context. - return fi.pkgInfo.newFuncInfo(n) - case *ast.BranchStmt: - switch n.Tok { - case token.GOTO: - // Emulating GOTO in JavaScript requires the code to be flattened into a - // switch-statement. - fi.markFlattened(fi.visitorStack) - fi.GotoLabel[fi.pkgInfo.Uses[n.Label].(*types.Label)] = true - case token.CONTINUE: - loopStmt := astutil.FindLoopStmt(fi.visitorStack, n, fi.pkgInfo.Info) - if forStmt, ok := (loopStmt).(*ast.ForStmt); ok { - // In `for x; y; z { ... }` loops `z` may be potentially blocking - // and therefore continue expression that triggers it would have to - // be treated as blocking. - fi.continueStmts = append(fi.continueStmts, newContinueStmt(forStmt, fi.visitorStack)) - } - } - return fi - case *ast.CallExpr: - return fi.visitCallExpr(n) - case *ast.SendStmt: - // Sending into a channel is blocking. - fi.markBlocking(fi.visitorStack) - return fi - case *ast.UnaryExpr: - switch n.Op { - case token.AND: - if id, ok := astutil.RemoveParens(n.X).(*ast.Ident); ok { - fi.pkgInfo.HasPointer[fi.pkgInfo.Uses[id].(*types.Var)] = true - } - case token.ARROW: - // Receiving from a channel is blocking. - fi.markBlocking(fi.visitorStack) - } - return fi - case *ast.RangeStmt: - if _, ok := fi.pkgInfo.TypeOf(n.X).Underlying().(*types.Chan); ok { - // for-range loop over a channel is blocking. - fi.markBlocking(fi.visitorStack) - } - return fi - case *ast.SelectStmt: - for _, s := range n.Body.List { - if s.(*ast.CommClause).Comm == nil { // default clause - return fi - } - } - // Select statements without a default case are blocking. - fi.markBlocking(fi.visitorStack) - return fi - case *ast.CommClause: - // FIXME(nevkontakte): Does this need to be manually spelled out? Presumably - // ast.Walk would visit all those nodes anyway, and we are not creating any - // new contexts here. - // https://github.com/gopherjs/gopherjs/issues/230 seems to be relevant? - switch comm := n.Comm.(type) { - case *ast.SendStmt: - ast.Walk(fi, comm.Chan) - ast.Walk(fi, comm.Value) - case *ast.ExprStmt: - ast.Walk(fi, comm.X.(*ast.UnaryExpr).X) - case *ast.AssignStmt: - ast.Walk(fi, comm.Rhs[0].(*ast.UnaryExpr).X) - } - for _, s := range n.Body { - ast.Walk(fi, s) - } - return nil // The subtree was manually checked, no need to visit it again. - case *ast.GoStmt: - // Unlike a regular call, the function in a go statement doesn't block the - // caller goroutine, but the expression that determines the function and its - // arguments still need to be checked. - ast.Walk(fi, n.Call.Fun) - for _, arg := range n.Call.Args { - ast.Walk(fi, arg) - } - return nil // The subtree was manually checked, no need to visit it again. - case *ast.DeferStmt: - fi.HasDefer = true - if funcLit, ok := n.Call.Fun.(*ast.FuncLit); ok { - ast.Walk(fi, funcLit.Body) - } - return fi - case *ast.ReturnStmt: - // Capture all return statements in the function. They could become blocking - // if the function has a blocking deferred call. - fi.returnStmts = append(fi.returnStmts, fi.visitorStack.copy()) - return fi - default: - return fi - } - // Deliberately no return here to make sure that each of the cases above is - // self-sufficient and explicitly decides in which context the its AST subtree - // needs to be analyzed. -} - -func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { - switch f := astutil.RemoveParens(n.Fun).(type) { - case *ast.Ident: - fi.callToNamedFunc(fi.pkgInfo.Uses[f]) - case *ast.SelectorExpr: - if sel := fi.pkgInfo.Selections[f]; sel != nil && typesutil.IsJsObject(sel.Recv()) { - // js.Object methods are known to be non-blocking, but we still must - // check its arguments. - } else { - fi.callToNamedFunc(fi.pkgInfo.Uses[f.Sel]) - } - case *ast.FuncLit: - // Collect info about the function literal itself. - ast.Walk(fi, n.Fun) - - // Check all argument expressions. - for _, arg := range n.Args { - ast.Walk(fi, arg) - } - // Register literal function call site in case it is identified as blocking. - fi.literalFuncCallees[f] = append(fi.literalFuncCallees[f], fi.visitorStack.copy()) - 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 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. - fi.markBlocking(fi.visitorStack) - } - } - - return fi -} - -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. - fi.markBlocking(fi.visitorStack) - return - } - } - if o.Pkg() != fi.pkgInfo.Pkg { - if fi.pkgInfo.isImportedBlocking(o) { - fi.markBlocking(fi.visitorStack) - } - return - } - // We probably don't know yet whether the callee function is blocking. - // Record the calls site for the later stage. - fi.localNamedCallees[o] = append(fi.localNamedCallees[o], fi.visitorStack.copy()) - case *types.Var: - // Conservatively assume that a function in a variable might be blocking. - fi.markBlocking(fi.visitorStack) - } -} - -func (fi *FuncInfo) markBlocking(stack astPath) { - for _, n := range stack { - fi.Blocking[n] = true - fi.Flattened[n] = true - } -} - -func (fi *FuncInfo) markFlattened(stack astPath) { - for _, n := range stack { - fi.Flattened[n] = true - } -} diff --git a/compiler/analysis/info_test.go b/compiler/analysis/info_test.go deleted file mode 100644 index 588f09a6c..000000000 --- a/compiler/analysis/info_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package analysis - -import ( - "go/ast" - "go/types" - "testing" - - "github.com/gopherjs/gopherjs/internal/srctesting" -) - -// See: https://github.com/gopherjs/gopherjs/issues/955. -func TestBlockingFunctionLiteral(t *testing.T) { - src := ` -package test - -func blocking() { - c := make(chan bool) - <-c -} - -func indirectlyBlocking() { - func() { blocking() }() -} - -func directlyBlocking() { - func() { - c := make(chan bool) - <-c - }() -} - -func notBlocking() { - func() { println() } () -} -` - f := srctesting.New(t) - file := f.Parse("test.go", src) - typesInfo, typesPkg := f.Check("pkg/test", file) - - 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.") - }) - - assertBlocking(t, file, pkgInfo, "blocking") - assertBlocking(t, file, pkgInfo, "indirectlyBlocking") - assertBlocking(t, file, pkgInfo, "directlyBlocking") - assertNotBlocking(t, file, pkgInfo, "notBlocking") -} - -func assertBlocking(t *testing.T, file *ast.File, pkgInfo *Info, funcName string) { - typesFunc := getTypesFunc(t, file, pkgInfo, funcName) - if !pkgInfo.IsBlocking(typesFunc) { - t.Errorf("Got: %q is not blocking. Want: %q is blocking.", typesFunc, typesFunc) - } -} - -func assertNotBlocking(t *testing.T, file *ast.File, pkgInfo *Info, funcName string) { - typesFunc := getTypesFunc(t, file, pkgInfo, funcName) - if pkgInfo.IsBlocking(typesFunc) { - t.Errorf("Got: %q is blocking. Want: %q is not blocking.", typesFunc, typesFunc) - } -} - -func getTypesFunc(t *testing.T, file *ast.File, pkgInfo *Info, funcName string) *types.Func { - obj := file.Scope.Lookup(funcName) - if obj == nil { - t.Fatalf("Declaration of %q is not found in the AST.", funcName) - } - decl, ok := obj.Decl.(*ast.FuncDecl) - if !ok { - t.Fatalf("Got: %q is %v. Want: a function declaration.", funcName, obj.Kind) - } - blockingType, ok := pkgInfo.Defs[decl.Name] - if !ok { - t.Fatalf("No type information is found for %v.", decl.Name) - } - return blockingType.(*types.Func) -} diff --git a/compiler/astutil/astutil.go b/compiler/astutil/astutil.go index 9ff88a48c..5f259bfff 100644 --- a/compiler/astutil/astutil.go +++ b/compiler/astutil/astutil.go @@ -53,22 +53,11 @@ func IsTypeExpr(expr ast.Expr, info *types.Info) bool { _, ok := info.Uses[e].(*types.TypeName) return ok case *ast.SelectorExpr: - _, ok := info.Uses[e.Sel].(*types.TypeName) - return ok + return IsTypeExpr(e.Sel, info) case *ast.IndexExpr: - ident, ok := e.X.(*ast.Ident) - if !ok { - return false - } - _, ok = info.Uses[ident].(*types.TypeName) - return ok + return IsTypeExpr(e.X, info) case *ast.IndexListExpr: - ident, ok := e.X.(*ast.Ident) - if !ok { - return false - } - _, ok = info.Uses[ident].(*types.TypeName) - return ok + return IsTypeExpr(e.X, info) case *ast.ParenExpr: return IsTypeExpr(e.X, info) default: diff --git a/compiler/compiler.go b/compiler/compiler.go index b8f6a49bc..82a51d9cf 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -17,6 +17,8 @@ import ( "strings" "time" + "github.com/gopherjs/gopherjs/compiler/internal/dce" + "github.com/gopherjs/gopherjs/compiler/linkname" "github.com/gopherjs/gopherjs/compiler/prelude" "golang.org/x/tools/go/gcexportdata" ) @@ -45,45 +47,26 @@ type Archive struct { // A list of full package import paths that the current package imports across // all source files. See go/types.Package.Imports(). Imports []string - // Serialized contents of go/types.Package in a binary format. This information - // is used by the compiler to type-check packages that import this one. See - // gcexportdata.Write(). - // - // TODO(nevkontakte): It would be more convenient to store go/types.Package - // itself and only serialize it when writing the archive onto disk. - ExportData []byte + // The package information is used by the compiler to type-check packages + // that import this one. See [gcexportdata.Write]. + Package *types.Package // Compiled package-level symbols. Declarations []*Decl // Concatenated contents of all raw .inc.js of the package. IncJSCode []byte - // JSON-serialized contents of go/token.FileSet. This is used to obtain source - // code locations for various symbols (e.g. for sourcemap generation). See - // token.FileSet.Write(). - // - // TODO(nevkontakte): This is also more convenient to store as the original - // object and only serialize before writing onto disk. - FileSet []byte + // The file set containing the source code locations for various symbols + // (e.g. for sourcemap generation). See [token.FileSet.Write]. + FileSet *token.FileSet // Whether or not the package was compiled with minification enabled. Minified bool // A list of go:linkname directives encountered in the package. - GoLinknames []GoLinkname - // Time when this archive was built. - BuildTime time.Time + GoLinknames []linkname.GoLinkname } func (a Archive) String() string { return fmt.Sprintf("compiler.Archive{%s}", a.ImportPath) } -// RegisterTypes adds package type information from the archive into the provided map. -func (a *Archive) RegisterTypes(packages map[string]*types.Package) error { - var err error - // TODO(nevkontakte): Should this be shared throughout the build? - fset := token.NewFileSet() - packages[a.ImportPath], err = gcexportdata.Read(bytes.NewReader(a.ExportData), fset, packages, a.ImportPath) - return err -} - type Dependency struct { Pkg string Type string @@ -125,77 +108,31 @@ func ImportDependencies(archive *Archive, importPkg func(string) (*Archive, erro return deps, nil } -type dceInfo struct { - decl *Decl - objectFilter string - methodFilter string -} - func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) error { mainPkg := pkgs[len(pkgs)-1] minify := mainPkg.Minified // Aggregate all go:linkname directives in the program together. - gls := goLinknameSet{} + gls := linkname.GoLinknameSet{} for _, pkg := range pkgs { gls.Add(pkg.GoLinknames) } - byFilter := make(map[string][]*dceInfo) - var pendingDecls []*Decl // A queue of live decls to find other live decls. + sel := &dce.Selector[*Decl]{} for _, pkg := range pkgs { for _, d := range pkg.Declarations { - if d.DceObjectFilter == "" && d.DceMethodFilter == "" { - // This is an entry point (like main() or init() functions) or a variable - // initializer which has a side effect, consider it live. - pendingDecls = append(pendingDecls, d) - continue - } + implementsLink := false if gls.IsImplementation(d.LinkingName) { // If a decl is referenced by a go:linkname directive, we just assume // it's not dead. // TODO(nevkontakte): This is a safe, but imprecise assumption. We should // try and trace whether the referencing functions are actually live. - pendingDecls = append(pendingDecls, d) - } - info := &dceInfo{decl: d} - if d.DceObjectFilter != "" { - info.objectFilter = pkg.ImportPath + "." + d.DceObjectFilter - byFilter[info.objectFilter] = append(byFilter[info.objectFilter], info) - } - if d.DceMethodFilter != "" { - info.methodFilter = pkg.ImportPath + "." + d.DceMethodFilter - byFilter[info.methodFilter] = append(byFilter[info.methodFilter], info) - } - } - } - - dceSelection := make(map[*Decl]struct{}) // Known live decls. - for len(pendingDecls) != 0 { - d := pendingDecls[len(pendingDecls)-1] - pendingDecls = pendingDecls[:len(pendingDecls)-1] - - dceSelection[d] = struct{}{} // Mark the decl as live. - - // Consider all decls the current one is known to depend on and possible add - // them to the live queue. - for _, dep := range d.DceDeps { - if infos, ok := byFilter[dep]; ok { - delete(byFilter, dep) - for _, info := range infos { - if info.objectFilter == dep { - info.objectFilter = "" - } - if info.methodFilter == dep { - info.methodFilter = "" - } - if info.objectFilter == "" && info.methodFilter == "" { - pendingDecls = append(pendingDecls, info.decl) - } - } + implementsLink = true } + sel.Include(d, implementsLink) } } + dceSelection := sel.AliveDecls() if _, err := w.Write([]byte("\"use strict\";\n(function() {\n\n")); err != nil { return err @@ -222,18 +159,15 @@ func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) err } } - if _, err := w.Write([]byte("$synthesizeMethods();\n$initAllLinknames();\nvar $mainPkg = $packages[\"" + string(mainPkg.ImportPath) + "\"];\n$packages[\"runtime\"].$init();\n$go($mainPkg.$init, []);\n$flushConsole();\n\n}).call(this);\n")); err != nil { + if _, err := w.Write([]byte("$callForAllPackages(\"$finishSetup\");\n$synthesizeMethods();\n$callForAllPackages(\"$initLinknames\");\nvar $mainPkg = $packages[\"" + string(mainPkg.ImportPath) + "\"];\n$packages[\"runtime\"].$init();\n$go($mainPkg.$init, []);\n$flushConsole();\n\n}).call(this);\n")); err != nil { return err } return nil } -func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameSet, minify bool, w *SourceMapFilter) error { +func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls linkname.GoLinknameSet, minify bool, w *SourceMapFilter) error { if w.MappingCallback != nil && pkg.FileSet != nil { - w.fileSet = token.NewFileSet() - if err := w.fileSet.Read(json.NewDecoder(bytes.NewReader(pkg.FileSet)).Decode); err != nil { - panic(err) - } + w.fileSet = pkg.FileSet } if _, err := w.Write(pkg.IncJSCode); err != nil { return err @@ -249,13 +183,68 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameS filteredDecls = append(filteredDecls, d) } } + // Write variable names if _, err := w.Write(removeWhitespace([]byte(fmt.Sprintf("\tvar %s;\n", strings.Join(vars, ", "))), minify)); err != nil { return err } + // Write imports + for _, d := range filteredDecls { + if _, err := w.Write(d.ImportCode); err != nil { + return err + } + } + // Write named type declarations for _, d := range filteredDecls { - if _, err := w.Write(d.DeclCode); err != nil { + if _, err := w.Write(d.TypeDeclCode); err != nil { return err } + } + // Write exports for named type declarations + for _, d := range filteredDecls { + if _, err := w.Write(d.ExportTypeCode); err != nil { + return err + } + } + + // The following parts have to be run after all packages have been added + // to handle generics that use named types defined in a package that + // is defined after this package has been defined. + if _, err := w.Write(removeWhitespace([]byte("\t$pkg.$finishSetup = function() {\n"), minify)); err != nil { + return err + } + + // Write anonymous type declarations + for _, d := range filteredDecls { + if _, err := w.Write(d.AnonTypeDeclCode); err != nil { + return err + } + } + // Write function declarations + for _, d := range filteredDecls { + if _, err := w.Write(d.FuncDeclCode); err != nil { + return err + } + } + // Write exports for function declarations + for _, d := range filteredDecls { + if _, err := w.Write(d.ExportFuncCode); err != nil { + return err + } + } + // Write reflection metadata for types' methods + for _, d := range filteredDecls { + if _, err := w.Write(d.MethodListCode); err != nil { + return err + } + } + // Write the calls to finish initialization of types + for _, d := range filteredDecls { + if _, err := w.Write(d.TypeInitCode); err != nil { + return err + } + } + + for _, d := range filteredDecls { if gls.IsImplementation(d.LinkingName) { // This decl is referenced by a go:linkname directive, expose it to external // callers via $linkname object (declared in prelude). We are not using @@ -271,16 +260,6 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameS } } } - for _, d := range filteredDecls { - if _, err := w.Write(d.MethodListCode); err != nil { - return err - } - } - for _, d := range filteredDecls { - if _, err := w.Write(d.TypeInitCode); err != nil { - return err - } - } { // Set up all functions which package declares, but which implementation @@ -295,16 +274,23 @@ 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.RefExpr, impl.String())) + lines = append(lines, fmt.Sprintf("\t\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, "")) + code := fmt.Sprintf("\t\t$pkg.$initLinknames = function() {\n%s};\n", strings.Join(lines, "")) if _, err := w.Write(removeWhitespace([]byte(code), minify)); err != nil { return err } } } + // Write the end of the `$finishSetup` function. + if _, err := w.Write(removeWhitespace([]byte("\t};\n"), minify)); err != nil { + return err + } + + // Write the initialization function that will initialize this package + // (e.g. initialize package-level variable value). if _, err := w.Write(removeWhitespace([]byte("\t$init = function() {\n\t\t$pkg.$init = function() {};\n\t\t/* */ var $f, $c = false, $s = 0, $r; if (this !== undefined && this.$blk !== undefined) { $f = this; $c = true; $s = $f.$s; $r = $f.$r; } s: while (true) { switch ($s) { case 0:\n"), minify)); err != nil { return err } @@ -322,19 +308,98 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameS return nil } +type serializableArchive struct { + ImportPath string + Name string + Imports []string + ExportData []byte + Declarations []*Decl + IncJSCode []byte + FileSet []byte + Minified bool + GoLinknames []linkname.GoLinkname + BuildTime time.Time +} + // ReadArchive reads serialized compiled archive of the importPath package. -func ReadArchive(path string, r io.Reader) (*Archive, error) { +// +// The given srcModTime is used to determine if the archive is out-of-date. +// If the archive is out-of-date, the returned archive is nil. +// If there was not an error, the returned time is when the archive was built. +// +// The imports map is used to resolve package dependencies and may modify the +// map to include the package from the read archive. See [gcexportdata.Read]. +func ReadArchive(importPath string, r io.Reader, srcModTime time.Time, imports map[string]*types.Package) (*Archive, time.Time, error) { + var sa serializableArchive + if err := gob.NewDecoder(r).Decode(&sa); err != nil { + return nil, time.Time{}, err + } + + if srcModTime.After(sa.BuildTime) { + // Archive is out-of-date. + return nil, sa.BuildTime, nil + } + var a Archive - if err := gob.NewDecoder(r).Decode(&a); err != nil { - return nil, err + fset := token.NewFileSet() + if len(sa.ExportData) > 0 { + pkg, err := gcexportdata.Read(bytes.NewReader(sa.ExportData), fset, imports, importPath) + if err != nil { + return nil, sa.BuildTime, err + } + a.Package = pkg + } + + if len(sa.FileSet) > 0 { + a.FileSet = token.NewFileSet() + if err := a.FileSet.Read(json.NewDecoder(bytes.NewReader(sa.FileSet)).Decode); err != nil { + return nil, sa.BuildTime, err + } } - return &a, nil + a.ImportPath = sa.ImportPath + a.Name = sa.Name + a.Imports = sa.Imports + a.Declarations = sa.Declarations + a.IncJSCode = sa.IncJSCode + a.Minified = sa.Minified + a.GoLinknames = sa.GoLinknames + return &a, sa.BuildTime, nil } // WriteArchive writes compiled package archive on disk for later reuse. -func WriteArchive(a *Archive, w io.Writer) error { - return gob.NewEncoder(w).Encode(a) +// +// The passed in buildTime is used to determine if the archive is out-of-date. +// Typically it should be set to the srcModTime or time.Now() but it is exposed for testing purposes. +func WriteArchive(a *Archive, buildTime time.Time, w io.Writer) error { + exportData := new(bytes.Buffer) + if a.Package != nil { + if err := gcexportdata.Write(exportData, nil, a.Package); err != nil { + return fmt.Errorf("failed to write export data: %w", err) + } + } + + encodedFileSet := new(bytes.Buffer) + if a.FileSet != nil { + if err := a.FileSet.Write(json.NewEncoder(encodedFileSet).Encode); err != nil { + return err + } + } + + sa := serializableArchive{ + ImportPath: a.ImportPath, + Name: a.Name, + Imports: a.Imports, + ExportData: exportData.Bytes(), + Declarations: a.Declarations, + IncJSCode: a.IncJSCode, + FileSet: encodedFileSet.Bytes(), + Minified: a.Minified, + GoLinknames: a.GoLinknames, + BuildTime: buildTime, + } + + return gob.NewEncoder(w).Encode(sa) } type SourceMapFilter struct { diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 377f09d94..f06a03c2f 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -2,145 +2,1427 @@ package compiler import ( "bytes" - "go/ast" - "go/build" - "go/parser" - "go/token" "go/types" + "regexp" + "sort" + "strings" "testing" + "time" "github.com/google/go-cmp/cmp" - "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/packages" + + "github.com/gopherjs/gopherjs/compiler/internal/dce" + "github.com/gopherjs/gopherjs/compiler/linkname" + "github.com/gopherjs/gopherjs/compiler/sources" + "github.com/gopherjs/gopherjs/internal/srctesting" ) func TestOrder(t *testing.T) { fileA := ` -package foo + package foo -var Avar = "a" + var Avar = "a" -type Atype struct{} + type Atype struct{} -func Afunc() int { - var varA = 1 - var varB = 2 - return varA+varB -} -` + func Afunc() int { + var varA = 1 + var varB = 2 + return varA+varB + }` fileB := ` -package foo + package foo + + var Bvar = "b" -var Bvar = "b" + type Btype struct{} -type Btype struct{} + func Bfunc() int { + var varA = 1 + var varB = 2 + return varA+varB + }` -func Bfunc() int { - var varA = 1 - var varB = 2 - return varA+varB + files := []srctesting.Source{ + {Name: "fileA.go", Contents: []byte(fileA)}, + {Name: "fileB.go", Contents: []byte(fileB)}, + } + + compareOrder(t, files, false) + compareOrder(t, files, true) } -` - files := []source{{"fileA.go", []byte(fileA)}, {"fileB.go", []byte(fileB)}} - compare(t, "foo", files, false) - compare(t, "foo", files, true) +func TestDeclSelection_KeepUnusedExportedMethods(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) Bar() { + println("bar") + } + func (f Foo) Baz() { // unused + println("baz") + } + func main() { + Foo{}.Bar() + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) + sel.IsAlive(`func:command-line-arguments.Foo.Baz`) } -func compare(t *testing.T, path string, sourceFiles []source, minify bool) { - outputNormal, err := compile(path, sourceFiles, minify) - if err != nil { - t.Fatal(err) +func TestDeclSelection_RemoveUnusedUnexportedMethods(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) Bar() { + println("bar") + } + func (f Foo) baz() { // unused + println("baz") + } + func main() { + Foo{}.Bar() + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) + + sel.IsDead(`func:command-line-arguments.Foo.baz`) +} + +func TestDeclSelection_KeepUnusedUnexportedMethodForInterface(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) Bar() { + println("foo") + } + func (f Foo) baz() {} // unused + + type Foo2 struct {} + func (f Foo2) Bar() { + println("foo2") + } + + type IFoo interface { + Bar() + baz() + } + func main() { + fs := []any{ Foo{}, Foo2{} } + for _, f := range fs { + if i, ok := f.(IFoo); ok { + i.Bar() + } + } + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) + + // `baz` signature metadata is used to check a type assertion against IFoo, + // but the method itself is never called, so it can be removed. + // The method is kept in Foo's MethodList for type checking. + sel.IsDead(`func:command-line-arguments.Foo.baz`) +} + +func TestDeclSelection_KeepUnexportedMethodUsedViaInterfaceLit(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) Bar() { + println("foo") + } + func (f Foo) baz() { + println("baz") + } + func main() { + var f interface { + Bar() + baz() + } = Foo{} + f.baz() + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) + sel.IsAlive(`func:command-line-arguments.Foo.baz`) +} + +func TestDeclSelection_KeepAliveUnexportedMethodsUsedInMethodExpressions(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) baz() { + println("baz") + } + func main() { + fb := Foo.baz + fb(Foo{}) + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.baz`) +} + +func TestDeclSelection_RemoveUnusedFuncInstance(t *testing.T) { + src := ` + package main + func Sum[T int | float64](values ...T) T { + var sum T + for _, v := range values { + sum += v + } + return sum + } + func Foo() { // unused + println(Sum(1, 2, 3)) + } + func main() { + println(Sum(1.1, 2.2, 3.3)) + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.IsAlive(`func:command-line-arguments.Sum`) + sel.IsAlive(`anonType:command-line-arguments.sliceType$1`) // []float64 + + sel.IsDead(`func:command-line-arguments.Foo`) + sel.IsDead(`anonType:command-line-arguments.sliceType`) // []int + sel.IsDead(`func:command-line-arguments.Sum`) +} + +func TestDeclSelection_RemoveUnusedStructTypeInstances(t *testing.T) { + src := ` + package main + type Foo[T any] struct { v T } + func (f Foo[T]) Bar() { + println(f.v) + } + + var _ = Foo[float64]{v: 3.14} // unused + + func main() { + Foo[int]{v: 7}.Bar() + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) + + sel.IsDead(`type:command-line-arguments.Foo`) + sel.IsDead(`func:command-line-arguments.Foo.Bar`) +} + +func TestDeclSelection_RemoveUnusedInterfaceTypeInstances(t *testing.T) { + src := ` + package main + type Foo[T any] interface { Bar(v T) } + + type Baz int + func (b Baz) Bar(v int) { + println(v + int(b)) + } + + var F64 = FooBar[float64] // unused + + func FooBar[T any](f Foo[T], v T) { + f.Bar(v) + } + + func main() { + FooBar[int](Baz(42), 12) // Baz implements Foo[int] + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.IsAlive(`type:command-line-arguments.Baz`) + sel.IsAlive(`func:command-line-arguments.Baz.Bar`) + sel.IsDead(`var:command-line-arguments.F64`) + + sel.IsAlive(`func:command-line-arguments.FooBar`) + // The Foo[int] instance is defined as a parameter in FooBar[int] that is alive. + // However, Foo[int] isn't used directly in the code so it can be removed. + // JS will simply duck-type the Baz object to Foo[int] without Foo[int] specifically defined. + sel.IsDead(`type:command-line-arguments.Foo`) + + sel.IsDead(`func:command-line-arguments.FooBar`) + sel.IsDead(`type:command-line-arguments.Foo`) +} + +func TestDeclSelection_RemoveUnusedMethodWithDifferentSignature(t *testing.T) { + src := ` + package main + type Foo struct{} + func (f Foo) Bar() { println("Foo") } + func (f Foo) baz(x int) { println(x) } // unused + + type Foo2 struct{} + func (f Foo2) Bar() { println("Foo2") } + func (f Foo2) baz(x string) { println(x) } + + func main() { + f1 := Foo{} + f1.Bar() + + f2 := Foo2{} + f2.Bar() + f2.baz("foo") + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) + sel.IsDead(`func:command-line-arguments.Foo.baz`) + + sel.IsAlive(`type:command-line-arguments.Foo2`) + sel.IsAlive(`func:command-line-arguments.Foo2.Bar`) + sel.IsAlive(`func:command-line-arguments.Foo2.baz`) +} + +func TestDeclSelection_RemoveUnusedUnexportedMethodInstance(t *testing.T) { + src := ` + package main + type Foo[T any] struct{} + func (f Foo[T]) Bar() { println("Foo") } + func (f Foo[T]) baz(x T) { Baz[T]{v: x}.Bar() } + + type Baz[T any] struct{ v T } + func (b Baz[T]) Bar() { println("Baz", b.v) } + + func main() { + f1 := Foo[int]{} + f1.Bar() + f1.baz(7) + + f2 := Foo[uint]{} // Foo[uint].baz is unused + f2.Bar() + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) + sel.IsAlive(`func:command-line-arguments.Foo.baz`) + sel.IsAlive(`type:command-line-arguments.Baz`) + sel.IsAlive(`func:command-line-arguments.Baz.Bar`) + + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) + + // All three below are dead because Foo[uint].baz is unused. + sel.IsDead(`func:command-line-arguments.Foo.baz`) + sel.IsDead(`type:command-line-arguments.Baz`) + sel.IsDead(`func:command-line-arguments.Baz.Bar`) +} + +func TestDeclSelection_RemoveUnusedTypeConstraint(t *testing.T) { + src := ` + package main + type Foo interface{ int | string } + + type Bar[T Foo] struct{ v T } + func (b Bar[T]) Baz() { println(b.v) } + + var ghost = Bar[int]{v: 7} // unused + + func main() { + println("do nothing") + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.IsDead(`type:command-line-arguments.Foo`) + sel.IsDead(`type:command-line-arguments.Bar`) + sel.IsDead(`func:command-line-arguments.Bar.Baz`) + sel.IsDead(`var:command-line-arguments.ghost`) +} + +func TestDeclSelection_RemoveUnusedNestedTypesInFunction(t *testing.T) { + src := ` + package main + func Foo[T any](u T) any { + type Bar struct { v T } + return Bar{v: u} + } + func deadCode() { + println(Foo[int](42)) + } + func main() { + println(Foo[string]("cat")) + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + sel.IsAlive(`func:command-line-arguments.main`) + + sel.IsAlive(`funcVar:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo`) + sel.IsDead(`func:command-line-arguments.Foo`) + + sel.IsAlive(`typeVar:command-line-arguments.Bar`) + sel.IsAlive(`type:command-line-arguments.Bar`) + sel.IsDead(`type:command-line-arguments.Bar`) + + sel.IsDead(`funcVar:command-line-arguments.deadCode`) + sel.IsDead(`func:command-line-arguments.deadCode`) +} + +func TestDeclSelection_RemoveUnusedNestedTypesInMethod(t *testing.T) { + src := ` + package main + type Baz[T any] struct{} + func (b *Baz[T]) Foo(u T) any { + type Bar struct { v T } + return Bar{v: u} + } + func deadCode() { + b := Baz[int]{} + println(b.Foo(42)) + } + func main() { + b := Baz[string]{} + println(b.Foo("cat")) + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + sel.IsAlive(`func:command-line-arguments.main`) + + sel.IsAlive(`typeVar:command-line-arguments.Baz`) + sel.IsDead(`type:command-line-arguments.Baz`) + sel.IsAlive(`type:command-line-arguments.Baz`) + + sel.IsDead(`func:command-line-arguments.(*Baz).Foo`) + sel.IsAlive(`func:command-line-arguments.(*Baz).Foo`) + + sel.IsAlive(`typeVar:command-line-arguments.Bar`) + sel.IsDead(`type:command-line-arguments.Bar`) + sel.IsAlive(`type:command-line-arguments.Bar`) + + sel.IsDead(`funcVar:command-line-arguments.deadCode`) + sel.IsDead(`func:command-line-arguments.deadCode`) +} + +func TestDeclSelection_RemoveAllUnusedNestedTypes(t *testing.T) { + src := ` + package main + func Foo[T any](u T) any { + type Bar struct { v T } + return Bar{v: u} + } + func deadCode() { + println(Foo[int](42)) + println(Foo[string]("cat")) + } + func main() {}` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + sel.IsAlive(`func:command-line-arguments.main`) + + sel.IsDead(`funcVar:command-line-arguments.Foo`) + sel.IsDead(`func:command-line-arguments.Foo`) + sel.IsDead(`func:command-line-arguments.Foo`) + + sel.IsDead(`typeVar:command-line-arguments.Bar`) + sel.IsDead(`type:command-line-arguments.Bar`) + sel.IsDead(`type:command-line-arguments.Bar`) + + sel.IsDead(`funcVar:command-line-arguments.deadCode`) + sel.IsDead(`func:command-line-arguments.deadCode`) +} + +func TestDeclSelection_CompletelyRemoveNestedType(t *testing.T) { + src := ` + package main + func Foo[T any](u T) any { + type Bar struct { v T } + return Bar{v: u} + } + func deadCode() { + println(Foo[int](42)) + } + func main() {}` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.IsAlive(`func:command-line-arguments.main`) + + sel.IsDead(`funcVar:command-line-arguments.Foo`) + sel.IsDead(`func:command-line-arguments.Foo`) + + sel.IsDead(`typeVar:command-line-arguments.Bar`) + sel.IsDead(`type:command-line-arguments.Bar`) + + sel.IsDead(`funcVar:command-line-arguments.deadCode`) + sel.IsDead(`func:command-line-arguments.deadCode`) +} + +func TestDeclSelection_RemoveAnonNestedTypes(t *testing.T) { + // Based on test/fixedbugs/issue53635.go + // This checks that if an anon type (e.g. []T) is used in a function + // that is not used, the type is removed, otherwise it is kept. + + src := ` + package main + func Foo[T any](u T) any { + return []T(nil) + } + func deadCode() { + println(Foo[string]("cat")) + } + func main() { + println(Foo[int](42)) + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + sel.IsDead(`anonType:command-line-arguments.sliceType`) // []string + sel.IsAlive(`anonType:command-line-arguments.sliceType$1`) // []int +} + +func TestDeclSelection_NoNestAppliedToFuncCallInMethod(t *testing.T) { + // Checks that a function call to a non-local function isn't + // being labeled as a nested function call. + src := ` + package main + func foo(a any) { + println(a) + } + type Bar[T any] struct { u T } + func (b *Bar[T]) Baz() { + foo(b.u) + } + func main() { + b := &Bar[int]{u: 42} + b.Baz() + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + sel.IsAlive(`init:main`) + + sel.IsAlive(`typeVar:command-line-arguments.Bar`) + sel.IsAlive(`type:command-line-arguments.Bar`) + sel.IsAlive(`func:command-line-arguments.(*Bar).Baz`) + + sel.IsAlive(`func:command-line-arguments.foo`) +} + +func TestLengthParenthesizingIssue841(t *testing.T) { + // See issue https://github.com/gopherjs/gopherjs/issues/841 + // + // Summary: Given `len(a+b)` where a and b are strings being concatenated + // together, the result was `a + b.length` instead of `(a+b).length`. + // + // The fix was to check if the expression in `len` is a binary + // expression or not. If it is, then the expression is parenthesized. + // This will work for concatenations any combination of variables and + // literals but won't pick up `len(Foo(a+b))` or `len(a[0:i+3])`. + + src := ` + package main + + func main() { + a := "a" + b := "b" + ab := a + b + if len(a+b) != len(ab) { + panic("unreachable") + } + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + archives := compileProject(t, root, false) + mainPkg := archives[root.PkgPath] + + badRegex := regexp.MustCompile(`a\s*\+\s*b\.length`) + goodRegex := regexp.MustCompile(`\(a\s*\+\s*b\)\.length`) + goodFound := false + for i, decl := range mainPkg.Declarations { + if badRegex.Match(decl.FuncDeclCode) { + t.Errorf("found length issue in decl #%d: %s", i, decl.FullName) + t.Logf("decl code:\n%s", string(decl.FuncDeclCode)) + } + if goodRegex.Match(decl.FuncDeclCode) { + goodFound = true + } } + if !goodFound { + t.Error("parenthesized length not found") + } +} + +func TestDeclNaming_Import(t *testing.T) { + src1 := ` + package main + + import ( + newt "github.com/gopherjs/gopherjs/compiler/jorden" + "github.com/gopherjs/gopherjs/compiler/burke" + "github.com/gopherjs/gopherjs/compiler/hudson" + ) + + func main() { + newt.Quote() + burke.Quote() + hudson.Quote() + }` + src2 := `package jorden + func Quote() { println("They mostly come at night... mostly") }` + src3 := `package burke + func Quote() { println("Busy little creatures, huh?") }` + src4 := `package hudson + func Quote() { println("Game over, man! Game over!") }` + + root := srctesting.ParseSources(t, + []srctesting.Source{ + {Name: `main.go`, Contents: []byte(src1)}, + }, + []srctesting.Source{ + {Name: `jorden/rebecca.go`, Contents: []byte(src2)}, + {Name: `burke/carter.go`, Contents: []byte(src3)}, + {Name: `hudson/william.go`, Contents: []byte(src4)}, + }) + + archives := compileProject(t, root, false) + checkForDeclFullNames(t, archives, + `import:github.com/gopherjs/gopherjs/compiler/burke`, + `import:github.com/gopherjs/gopherjs/compiler/hudson`, + `import:github.com/gopherjs/gopherjs/compiler/jorden`, + ) +} + +func TestDeclNaming_FuncAndFuncVar(t *testing.T) { + src := ` + package main + + func Avasarala(value int) { println("Chrisjen", value) } + + func Draper[T any](value T) { println("Bobbie", value) } + + type Nagata struct{ value int } + func (n Nagata) Print() { println("Naomi", n.value) } + + type Burton[T any] struct{ value T } + func (b Burton[T]) Print() { println("Amos", b.value) } + + func main() { + Avasarala(10) + Draper(11) + Draper("Babs") + Nagata{value: 12}.Print() + Burton[int]{value: 13}.Print() + Burton[string]{value: "Timothy"}.Print() + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + archives := compileProject(t, root, false) + checkForDeclFullNames(t, archives, + `funcVar:command-line-arguments.Avasarala`, + `func:command-line-arguments.Avasarala`, + + `funcVar:command-line-arguments.Draper`, + `func:command-line-arguments.Draper`, + `func:command-line-arguments.Draper`, + + `func:command-line-arguments.Nagata.Print`, + + `typeVar:command-line-arguments.Burton`, + `type:command-line-arguments.Burton`, + `type:command-line-arguments.Burton`, + `func:command-line-arguments.Burton.Print`, + `func:command-line-arguments.Burton.Print`, + + `funcVar:command-line-arguments.main`, + `func:command-line-arguments.main`, + `init:main`, + ) +} + +func TestDeclNaming_InitsAndVars(t *testing.T) { + src1 := ` + package main + + import ( + _ "github.com/gopherjs/gopherjs/compiler/spengler" + _ "github.com/gopherjs/gopherjs/compiler/barrett" + _ "github.com/gopherjs/gopherjs/compiler/tully" + ) + + var peck = "Walter" + func init() { println(peck) } + + func main() { + println("Janosz Poha") + }` + src2 := `package spengler + func init() { println("Egon") } + var egie = func() { println("Dirt Farmer") } + func init() { egie() }` + src3 := `package barrett + func init() { println("Dana") }` + src4 := `package barrett + func init() { println("Zuul") }` + src5 := `package barrett + func init() { println("Gatekeeper") }` + src6 := `package tully + func init() { println("Louis") }` + src7 := `package tully + var keymaster = "Vinz Clortho" + func init() { println(keymaster) }` + + root := srctesting.ParseSources(t, + []srctesting.Source{ + {Name: `main.go`, Contents: []byte(src1)}, + }, + []srctesting.Source{ + {Name: `spengler/a.go`, Contents: []byte(src2)}, + {Name: `barrett/a.go`, Contents: []byte(src3)}, + {Name: `barrett/b.go`, Contents: []byte(src4)}, + {Name: `barrett/c.go`, Contents: []byte(src5)}, + {Name: `tully/a.go`, Contents: []byte(src6)}, + {Name: `tully/b.go`, Contents: []byte(src7)}, + }) + + archives := compileProject(t, root, false) + checkForDeclFullNames(t, archives, + // tully + `var:github.com/gopherjs/gopherjs/compiler/tully.keymaster`, + `funcVar:github.com/gopherjs/gopherjs/compiler/tully.init`, + `funcVar:github.com/gopherjs/gopherjs/compiler/tully.init`, + `func:github.com/gopherjs/gopherjs/compiler/tully.init`, + `func:github.com/gopherjs/gopherjs/compiler/tully.init`, + + // spangler + `var:github.com/gopherjs/gopherjs/compiler/spengler.egie`, + `funcVar:github.com/gopherjs/gopherjs/compiler/spengler.init`, + `funcVar:github.com/gopherjs/gopherjs/compiler/spengler.init`, + `func:github.com/gopherjs/gopherjs/compiler/spengler.init`, + `func:github.com/gopherjs/gopherjs/compiler/spengler.init`, + + // barrett + `funcVar:github.com/gopherjs/gopherjs/compiler/barrett.init`, + `funcVar:github.com/gopherjs/gopherjs/compiler/barrett.init`, + `funcVar:github.com/gopherjs/gopherjs/compiler/barrett.init`, + `func:github.com/gopherjs/gopherjs/compiler/barrett.init`, + `func:github.com/gopherjs/gopherjs/compiler/barrett.init`, + `func:github.com/gopherjs/gopherjs/compiler/barrett.init`, + + // main + `var:command-line-arguments.peck`, + `funcVar:command-line-arguments.init`, + `func:command-line-arguments.init`, + `funcVar:command-line-arguments.main`, + `func:command-line-arguments.main`, + `init:main`, + ) +} + +func TestDeclNaming_VarsAndTypes(t *testing.T) { + src := ` + package main + + var _, shawn, _ = func() (int, string, float64) { + return 1, "Vizzini", 3.14 + }() + + var _ = func() string { + return "Inigo Montoya" + }() + + var fezzik = struct{ value int }{value: 7} + var inigo = struct{ value string }{value: "Montoya"} + + type westley struct{ value string } + + func main() {}` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + + archives := compileProject(t, root, false) + checkForDeclFullNames(t, archives, + `var:command-line-arguments.shawn`, + `var:blank`, + + `var:command-line-arguments.fezzik`, + `anonType:command-line-arguments.structType`, + + `var:command-line-arguments.inigo`, + `anonType:command-line-arguments.structType$1`, + + `typeVar:command-line-arguments.westley`, + `type:command-line-arguments.westley`, + ) +} + +func Test_CrossPackageAnalysis(t *testing.T) { + src1 := ` + package main + import "github.com/gopherjs/gopherjs/compiler/stable" + + func main() { + m := map[string]int{ + "one": 1, + "two": 2, + "three": 3, + } + stable.Print(m) + }` + src2 := ` + package collections + import "github.com/gopherjs/gopherjs/compiler/cmp" + + func Keys[K cmp.Ordered, V any, M ~map[K]V](m M) []K { + keys := make([]K, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + return keys + }` + src3 := ` + package collections + import "github.com/gopherjs/gopherjs/compiler/cmp" + + func Values[K cmp.Ordered, V any, M ~map[K]V](m M) []V { + values := make([]V, 0, len(m)) + for _, v := range m { + values = append(values, v) + } + return values + }` + src4 := ` + package sorts + import "github.com/gopherjs/gopherjs/compiler/cmp" + + func Pair[K cmp.Ordered, V any, SK ~[]K, SV ~[]V](k SK, v SV) { + Bubble(len(k), + func(i, j int) bool { return k[i] < k[j] }, + func(i, j int) { k[i], v[i], k[j], v[j] = k[j], v[j], k[i], v[i] }) + } + + func Bubble(length int, less func(i, j int) bool, swap func(i, j int)) { + for i := 0; i < length; i++ { + for j := i + 1; j < length; j++ { + if less(j, i) { + swap(i, j) + } + } + } + }` + src5 := ` + package stable + import ( + "github.com/gopherjs/gopherjs/compiler/collections" + "github.com/gopherjs/gopherjs/compiler/sorts" + "github.com/gopherjs/gopherjs/compiler/cmp" + ) + + func Print[K cmp.Ordered, V any, M ~map[K]V](m M) { + keys := collections.Keys(m) + values := collections.Values(m) + sorts.Pair(keys, values) + for i, k := range keys { + println(i, k, values[i]) + } + }` + src6 := ` + package cmp + type Ordered interface { ~int | ~uint | ~float64 | ~string }` + + root := srctesting.ParseSources(t, + []srctesting.Source{ + {Name: `main.go`, Contents: []byte(src1)}, + }, + []srctesting.Source{ + {Name: `collections/keys.go`, Contents: []byte(src2)}, + {Name: `collections/values.go`, Contents: []byte(src3)}, + {Name: `sorts/sorts.go`, Contents: []byte(src4)}, + {Name: `stable/print.go`, Contents: []byte(src5)}, + {Name: `cmp/ordered.go`, Contents: []byte(src6)}, + }) + + archives := compileProject(t, root, false) + checkForDeclFullNames(t, archives, + // collections + `funcVar:github.com/gopherjs/gopherjs/compiler/collections.Values`, + `func:github.com/gopherjs/gopherjs/compiler/collections.Values`, + `funcVar:github.com/gopherjs/gopherjs/compiler/collections.Keys`, + `func:github.com/gopherjs/gopherjs/compiler/collections.Keys`, + + // sorts + `funcVar:github.com/gopherjs/gopherjs/compiler/sorts.Pair`, + `func:github.com/gopherjs/gopherjs/compiler/sorts.Pair`, + `funcVar:github.com/gopherjs/gopherjs/compiler/sorts.Bubble`, + `func:github.com/gopherjs/gopherjs/compiler/sorts.Bubble`, + + // stable + `funcVar:github.com/gopherjs/gopherjs/compiler/stable.Print`, + `func:github.com/gopherjs/gopherjs/compiler/stable.Print`, + + // main + `init:main`, + ) +} + +func Test_IndexedSelectors(t *testing.T) { + src1 := ` + package main + import "github.com/gopherjs/gopherjs/compiler/other" + func main() { + // Instance IndexExpr with a package SelectorExpr for a function call. + other.PrintZero[int]() + other.PrintZero[string]() + + // Instance IndexListExpr with a package SelectorExpr for a function call. + other.PrintZeroZero[int, string]() + + // Index IndexExpr with a struct SelectorExpr for a function call. + f := other.Foo{Ops: []func() { + other.PrintZero[int], + other.PrintZero[string], + other.PrintZeroZero[int, string], + }} + f.Ops[0]() + f.Ops[1]() + + // Index IndexExpr with a package/var SelectorExpr for a function call. + other.Bar.Ops[0]() + other.Baz[0]() + + // IndexExpr with a SelectorExpr for a cast + _ = other.ZHandle[int](other.PrintZero[int]) + + // IndexListExpr with a SelectorExpr for a cast + _ = other.ZZHandle[int, string](other.PrintZeroZero[int, string]) + }` + src2 := ` + package other + func PrintZero[T any]() { + var zero T + println("Zero is ", zero) + } + + func PrintZeroZero[T any, U any]() { + PrintZero[T]() + PrintZero[U]() + } + + type ZHandle[T any] func() + type ZZHandle[T any, U any] func() + + type Foo struct { Ops []func() } + var Bar = Foo{Ops: []func() { + PrintZero[int], + PrintZero[string], + }} + var Baz = Bar.Ops` + + root := srctesting.ParseSources(t, + []srctesting.Source{ + {Name: `main.go`, Contents: []byte(src1)}, + }, + []srctesting.Source{ + {Name: `other/other.go`, Contents: []byte(src2)}, + }) + + archives := compileProject(t, root, false) + // We mostly are checking that the code was turned into decls correctly, + // since the issue was that indexed selectors were not being handled correctly, + // so if it didn't panic by this point, it should be fine. + checkForDeclFullNames(t, archives, + `func:command-line-arguments.main`, + `type:github.com/gopherjs/gopherjs/compiler/other.ZHandle`, + `type:github.com/gopherjs/gopherjs/compiler/other.ZZHandle`, + ) +} + +func TestArchiveSelectionAfterSerialization(t *testing.T) { + src := ` + package main + type Foo interface{ int | string } + + type Bar[T Foo] struct{ v T } + func (b Bar[T]) Baz() { println(b.v) } + + var ghost = Bar[int]{v: 7} // unused + + func main() { + println("do nothing") + }` + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + rootPath := root.PkgPath + origArchives := compileProject(t, root, false) + readArchives := reloadCompiledProject(t, origArchives, rootPath) + + origJS := renderPackage(t, origArchives[rootPath], false) + readJS := renderPackage(t, readArchives[rootPath], false) + + if diff := cmp.Diff(origJS, readJS); diff != "" { + t.Errorf("the reloaded files produce different JS:\n%s", diff) + } +} + +func TestNestedConcreteTypeInGenericFunc(t *testing.T) { + // This is a test of a type defined inside a generic function + // that uses the type parameter of the function as a field type. + // The `T` type is unique for each instance of `F`. + // The use of `A` as a field is do demonstrate the difference in the types + // however even if T had no fields, the type would still be different. + // + // Change `print(F[?]())` to `fmt.Printf("%T\n", F[?]())` for + // golang playground to print the type of T in the different F instances. + // (I just didn't want this test to depend on `fmt` when it doesn't need to.) + + src := ` + package main + func F[A any]() any { + type T struct{ + a A + } + return T{} + } + func main() { + type Int int + print(F[int]()) + print(F[Int]()) + } + ` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + archives := compileProject(t, root, false) + mainPkg := archives[root.PkgPath] + insts := collectDeclInstances(t, mainPkg) + + exp := []string{ + `F[int]`, + `F[main.Int]`, // Go prints `F[main.Int·2]` + `T[int;]`, // `T` from `F[int]` (Go prints `T[int]`) + `T[main.Int;]`, // `T` from `F[main.Int]` (Go prints `T[main.Int·2]`) + } + if diff := cmp.Diff(exp, insts); len(diff) > 0 { + t.Errorf("the instances of generics are different:\n%s", diff) + } +} + +func TestNestedGenericTypeInGenericFunc(t *testing.T) { + // This is a subset of the type param nested test from the go repo. + // See https://github.com/golang/go/blob/go1.19.13/test/typeparam/nested.go + // The test is failing because nested types aren't being typed differently. + // For example the type of `T[int]` below is different based on `F[X]` + // instance for different `X` type parameters, hence Go prints the type as + // `T[X;int]` instead of `T[int]`. + + src := ` + package main + func F[A any]() any { + type T[B any] struct{ + a A + b B + } + return T[int]{} + } + func main() { + type Int int + print(F[int]()) + print(F[Int]()) + } + ` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + archives := compileProject(t, root, false) + mainPkg := archives[root.PkgPath] + insts := collectDeclInstances(t, mainPkg) + + exp := []string{ + `F[int]`, + `F[main.Int]`, + `T[int; int]`, + `T[main.Int; int]`, + } + if diff := cmp.Diff(exp, insts); len(diff) > 0 { + t.Errorf("the instances of generics are different:\n%s", diff) + } +} + +func TestNestedGenericTypeInGenericFuncWithSharedTArgs(t *testing.T) { + src := ` + package main + func F[A any]() any { + type T[B any] struct { + b B + } + return T[A]{} + } + func main() { + type Int int + print(F[int]()) + print(F[Int]()) + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + archives := compileProject(t, root, false) + mainPkg := archives[root.PkgPath] + insts := collectDeclInstances(t, mainPkg) + + exp := []string{ + `F[int]`, + `F[main.Int]`, + `T[int; int]`, + `T[main.Int; main.Int]`, + // Make sure that T[int;main.Int] and T[main.Int;int] aren't created. + } + if diff := cmp.Diff(exp, insts); len(diff) > 0 { + t.Errorf("the instances of generics are different:\n%s", diff) + } +} + +func collectDeclInstances(t *testing.T, pkg *Archive) []string { + t.Helper() + + // Regex to match strings like `Foo[42 /* bar */] =` and capture + // the name (`Foo`), the index (`42`), and the instance type (`bar`). + rex := regexp.MustCompile(`^\s*(\w+)\s*\[\s*(\d+)\s*\/\*(.+)\*\/\s*\]\s*\=`) + + // Collect all instances of generics (e.g. `Foo[bar] @ 2`) written to the decl code. + insts := []string{} + checkCode := func(code []byte) { + if match := rex.FindAllStringSubmatch(string(code), 1); len(match) > 0 { + instance := match[0][1] + `[` + strings.TrimSpace(match[0][3]) + `]` + instance = strings.ReplaceAll(instance, `command-line-arguments`, pkg.Name) + insts = append(insts, instance) + } + } + for _, decl := range pkg.Declarations { + checkCode(decl.TypeDeclCode) + checkCode(decl.FuncDeclCode) + } + sort.Strings(insts) + return insts +} + +func compareOrder(t *testing.T, sourceFiles []srctesting.Source, minify bool) { + t.Helper() + outputNormal := compile(t, sourceFiles, minify) // reverse the array for i, j := 0, len(sourceFiles)-1; i < j; i, j = i+1, j-1 { sourceFiles[i], sourceFiles[j] = sourceFiles[j], sourceFiles[i] } - outputReversed, err := compile(path, sourceFiles, minify) - if err != nil { - t.Fatal(err) - } + outputReversed := compile(t, sourceFiles, minify) - if diff := cmp.Diff(string(outputNormal), string(outputReversed)); diff != "" { + if diff := cmp.Diff(outputNormal, outputReversed); diff != "" { t.Errorf("files in different order produce different JS:\n%s", diff) } } -type source struct { - name string - contents []byte +func compile(t *testing.T, sourceFiles []srctesting.Source, minify bool) string { + t.Helper() + rootPkg := srctesting.ParseSources(t, sourceFiles, nil) + archives := compileProject(t, rootPkg, minify) + + path := rootPkg.PkgPath + a, ok := archives[path] + if !ok { + t.Fatalf(`root package not found in archives: %s`, path) + } + + return renderPackage(t, a, minify) } -func compile(path string, sourceFiles []source, minify bool) ([]byte, error) { - conf := loader.Config{} - conf.Fset = token.NewFileSet() - conf.ParserMode = parser.ParseComments +// compileProject compiles the given root package and all packages imported by the root. +// This returns the compiled archives of all packages keyed by their import path. +func compileProject(t *testing.T, root *packages.Package, minify bool) map[string]*Archive { + t.Helper() + pkgMap := map[string]*packages.Package{} + packages.Visit([]*packages.Package{root}, nil, func(pkg *packages.Package) { + pkgMap[pkg.PkgPath] = pkg + }) - context := build.Default // make a copy of build.Default - conf.Build = &context - conf.Build.BuildTags = []string{"js"} + allSrcs := map[string]*sources.Sources{} + for _, pkg := range pkgMap { + srcs := &sources.Sources{ + ImportPath: pkg.PkgPath, + Dir: ``, + Files: pkg.Syntax, + FileSet: pkg.Fset, + } + allSrcs[pkg.PkgPath] = srcs + } - var astFiles []*ast.File - for _, sourceFile := range sourceFiles { - astFile, err := parser.ParseFile(conf.Fset, sourceFile.name, sourceFile.contents, parser.ParseComments) + importer := func(path, srcDir string) (*sources.Sources, error) { + srcs, ok := allSrcs[path] + if !ok { + t.Fatal(`package not found:`, path) + return nil, nil + } + return srcs, nil + } + + tContext := types.NewContext() + sortedSources := make([]*sources.Sources, 0, len(allSrcs)) + for _, srcs := range allSrcs { + sortedSources = append(sortedSources, srcs) + } + sources.SortedSourcesSlice(sortedSources) + PrepareAllSources(sortedSources, importer, tContext) + + archives := map[string]*Archive{} + for _, srcs := range allSrcs { + a, err := Compile(srcs, tContext, minify) if err != nil { - return nil, err + t.Fatal(`failed to compile:`, err) } - astFiles = append(astFiles, astFile) + archives[srcs.ImportPath] = a } - conf.CreateFromFiles(path, astFiles...) - prog, err := conf.Load() - if err != nil { - return nil, err + return archives +} + +// newTime creates an arbitrary time.Time offset by the given number of seconds. +// This is useful for quickly creating times that are before or after another. +func newTime(seconds float64) time.Time { + return time.Date(1969, 7, 20, 20, 17, 0, 0, time.UTC). + Add(time.Duration(seconds * float64(time.Second))) +} + +// reloadCompiledProject persists the given archives into memory then reloads +// them from memory to simulate a cache reload of a precompiled project. +func reloadCompiledProject(t *testing.T, archives map[string]*Archive, rootPkgPath string) map[string]*Archive { + t.Helper() + + // TODO(grantnelson-wf): The tests using this function are out-of-date + // since they are testing the old archive caching that has been disabled. + // At some point, these tests should be updated to test any new caching + // mechanism that is implemented or removed. As is this function is faking + // the old recursive archive loading that is no longer used since it + // doesn't allow cross package analysis for generings. + + buildTime := newTime(5.0) + serialized := map[string][]byte{} + for path, a := range archives { + buf := &bytes.Buffer{} + if err := WriteArchive(a, buildTime, buf); err != nil { + t.Fatalf(`failed to write archive for %s: %v`, path, err) + } + serialized[path] = buf.Bytes() } - archiveCache := map[string]*Archive{} + srcModTime := newTime(0.0) + reloadCache := map[string]*Archive{} + type ImportContext struct { + Packages map[string]*types.Package + ImportArchive func(path string) (*Archive, error) + } var importContext *ImportContext importContext = &ImportContext{ - Packages: make(map[string]*types.Package), - Import: func(path string) (*Archive, error) { + Packages: map[string]*types.Package{}, + ImportArchive: func(path string) (*Archive, error) { // find in local cache - if a, ok := archiveCache[path]; ok { + if a, ok := reloadCache[path]; ok { return a, nil } - pi := prog.Package(path) - importContext.Packages[path] = pi.Pkg - - // compile package - a, err := Compile(path, pi.Files, prog.Fset, importContext, minify) + // deserialize archive + buf, ok := serialized[path] + if !ok { + t.Fatalf(`archive not found for %s`, path) + } + a, _, err := ReadArchive(path, bytes.NewReader(buf), srcModTime, importContext.Packages) if err != nil { - return nil, err + t.Fatalf(`failed to read archive for %s: %v`, path, err) } - archiveCache[path] = a + reloadCache[path] = a return a, nil }, } - a, err := importContext.Import(path) + _, err := importContext.ImportArchive(rootPkgPath) if err != nil { - return nil, err + t.Fatal(`failed to reload archives:`, err) } - b, err := renderPackage(a) - if err != nil { - return nil, err - } - return b, nil + return reloadCache } -func renderPackage(archive *Archive) ([]byte, error) { - selection := make(map[*Decl]struct{}) +func renderPackage(t *testing.T, archive *Archive, minify bool) string { + t.Helper() + + sel := &dce.Selector[*Decl]{} for _, d := range archive.Declarations { - selection[d] = struct{}{} + sel.Include(d, false) } + selection := sel.AliveDecls() buf := &bytes.Buffer{} - if err := WritePkgCode(archive, selection, goLinknameSet{}, false, &SourceMapFilter{Writer: buf}); err != nil { - return nil, err + if err := WritePkgCode(archive, selection, linkname.GoLinknameSet{}, minify, &SourceMapFilter{Writer: buf}); err != nil { + t.Fatal(err) + } + + b := buf.String() + if len(b) == 0 { + t.Fatal(`render package had no output`) + } + return b +} + +type selectionTester struct { + t *testing.T + mainPkg *Archive + archives map[string]*Archive + packages []*Archive + dceSelection map[*Decl]struct{} +} + +func declSelection(t *testing.T, sourceFiles []srctesting.Source, auxFiles []srctesting.Source) *selectionTester { + t.Helper() + root := srctesting.ParseSources(t, sourceFiles, auxFiles) + archives := compileProject(t, root, false) + mainPkg := archives[root.PkgPath] + + paths := make([]string, 0, len(archives)) + for path := range archives { + paths = append(paths, path) + } + sort.Strings(paths) + packages := make([]*Archive, 0, len(archives)) + for _, path := range paths { + packages = append(packages, archives[path]) + } + + sel := &dce.Selector[*Decl]{} + for _, pkg := range packages { + for _, d := range pkg.Declarations { + sel.Include(d, false) + } } + dceSelection := sel.AliveDecls() - return buf.Bytes(), nil + return &selectionTester{ + t: t, + mainPkg: mainPkg, + archives: archives, + packages: packages, + dceSelection: dceSelection, + } +} + +func (st *selectionTester) PrintDeclStatus() { + st.t.Helper() + for _, pkg := range st.packages { + st.t.Logf(`Package %s`, pkg.ImportPath) + for _, decl := range pkg.Declarations { + if _, ok := st.dceSelection[decl]; ok { + st.t.Logf(` [Alive] %q`, decl.FullName) + } else { + st.t.Logf(` [Dead] %q`, decl.FullName) + } + } + } +} + +func (st *selectionTester) IsAlive(declFullName string) { + st.t.Helper() + decl := st.FindDecl(declFullName) + if _, ok := st.dceSelection[decl]; !ok { + st.t.Error(`expected the decl to be alive:`, declFullName) + } +} + +func (st *selectionTester) IsDead(declFullName string) { + st.t.Helper() + decl := st.FindDecl(declFullName) + if _, ok := st.dceSelection[decl]; ok { + st.t.Error(`expected the decl to be dead:`, declFullName) + } +} + +func (st *selectionTester) FindDecl(declFullName string) *Decl { + st.t.Helper() + var found *Decl + for _, pkg := range st.packages { + for _, d := range pkg.Declarations { + if d.FullName == declFullName { + if found != nil { + st.t.Fatal(`multiple decls found with the name`, declFullName) + } + found = d + } + } + } + if found == nil { + st.t.Fatal(`no decl found by the name`, declFullName) + } + return found +} + +func checkForDeclFullNames(t *testing.T, archives map[string]*Archive, expectedFullNames ...string) { + t.Helper() + + expected := map[string]int{} + counts := map[string]int{} + for _, name := range expectedFullNames { + expected[name]++ + counts[name]++ + } + for _, pkg := range archives { + for _, decl := range pkg.Declarations { + if found, has := expected[decl.FullName]; has { + if found <= 0 { + t.Errorf(`decl name existed more than %d time(s): %q`, counts[decl.FullName], decl.FullName) + } else { + expected[decl.FullName]-- + } + } + } + } + for imp, found := range expected { + if found > 0 { + t.Errorf(`missing %d decl name(s): %q`, found, imp) + } + } + if t.Failed() { + t.Log("Declarations:") + for pkgName, pkg := range archives { + t.Logf("\t%q", pkgName) + for i, decl := range pkg.Declarations { + t.Logf("\t\t%d:\t%q", i, decl.FullName) + } + } + } } diff --git a/compiler/declNames.go b/compiler/declNames.go new file mode 100644 index 000000000..57d6ac255 --- /dev/null +++ b/compiler/declNames.go @@ -0,0 +1,72 @@ +package compiler + +import ( + "go/types" + + "github.com/gopherjs/gopherjs/compiler/internal/symbol" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" +) + +// importDeclFullName returns a unique name for an import declaration. +// This import name may be duplicated in different packages if they both +// import the same package, they are only unique per package. +func importDeclFullName(importedPkg *types.Package) string { + return `import:` + importedPkg.Path() +} + +// varDeclFullName returns a name for a package-level variable declaration. +// This var name only references the first named variable in an assignment. +// If no variables are named, the name is `var:blank` and not unique. +func varDeclFullName(init *types.Initializer) string { + for _, lhs := range init.Lhs { + if lhs.Name() != `_` { + return `var:` + symbol.New(lhs).String() + } + } + return `var:blank` +} + +// funcVarDeclFullName returns a name for a package-level variable +// that is used for a function (without a receiver) declaration. +// The name is unique unless the function is an `init` function. +// If the function is generic, this declaration name is also for the list +// of instantiations of the function. +func funcVarDeclFullName(o *types.Func) string { + return `funcVar:` + symbol.New(o).String() +} + +// mainFuncFullName returns the name for the declaration used to invoke the +// main function of the program. There should only be one decl with this name. +func mainFuncDeclFullName() string { + return `init:main` +} + +// funcDeclFullName returns a name for a package-level function +// declaration for the given instance of a function. +// The name is unique unless the function is an `init` function. +func funcDeclFullName(inst typeparams.Instance) string { + return `func:` + inst.String() +} + +// typeVarDeclFullName returns a unique name for a package-level variable +// that is used for a named type declaration or a named nested type declaration. +// If the type is generic, this declaration name is also for the list +// of instantiations of the type. +func typeVarDeclFullName(o *types.TypeName) string { + return `typeVar:` + symbol.New(o).String() +} + +// typeDeclFullName returns a unique name for a package-level type declaration +// for the given instance of a type. Names are only unique per scope package +// unless the type is a nested type then the name is only unique per the +// function or method it is declared in. +func typeDeclFullName(inst typeparams.Instance) string { + return `type:` + inst.String() +} + +// anonTypeDeclFullName returns a unique name for a package-level type +// declaration for an anonymous type. Names are only unique per package. +// These names are generated for types that are not named in the source code. +func anonTypeDeclFullName(o types.Object) string { + return `anonType:` + symbol.New(o).String() +} diff --git a/compiler/decls.go b/compiler/decls.go index 36f97d3ff..0979a77ef 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -12,9 +12,11 @@ import ( "sort" "strings" - "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/sources" "github.com/gopherjs/gopherjs/compiler/typesutil" ) @@ -22,6 +24,9 @@ import ( // // It contains code generated by the compiler for this specific symbol, which is // grouped by the execution stage it belongs to in the JavaScript runtime. +// +// When adding new fields to this struct, make sure the field is exported +// so that it Gob serializes correctly for the archive cache. type Decl struct { // The package- or receiver-type-qualified name of function or method obj. // See go/types.Func.FullName(). @@ -39,28 +44,36 @@ type Decl struct { RefExpr string // NamedRecvType is method named recv declare. NamedRecvType string - // JavaScript code that declares basic information about a symbol. For a type - // it configures basic information about the type and its identity. For a function - // or method it contains its compiled body. - DeclCode []byte - // JavaScript code that initializes reflection metadata about type's method list. + // JavaScript code that declares a local variable for an imported package. + ImportCode []byte + // JavaScript code that declares basic information about a named type symbol. + // It configures basic information about the type and its identity. + TypeDeclCode []byte + // JavaScript code that assigns exposed named types to the package. + ExportTypeCode []byte + // JavaScript code that declares basic information about an anonymous type. + // It configures basic information about the type. + // This is added to the finish setup phase to have access to all packages. + AnonTypeDeclCode []byte + // JavaScript code that declares basic information about a function or + // method symbol. This contains the function's or method's compiled body. + // This is added to the finish setup phase to have access to all packages. + FuncDeclCode []byte + // JavaScript code that assigns exposed functions to the package. + // This is added to the finish setup phase to have access to all packages. + ExportFuncCode []byte + // JavaScript code that initializes reflection metadata about a type's method list. + // This is added to the finish setup phase to have access to all packages. MethodListCode []byte // JavaScript code that initializes the rest of reflection metadata about a type // (e.g. struct fields, array type sizes, element types, etc.). + // This is added to the finish setup phase to have access to all packages. TypeInitCode []byte // JavaScript code that needs to be executed during the package init phase to // set the symbol up (e.g. initialize package-level variable value). InitCode []byte - // Symbol's identifier used by the dead-code elimination logic, not including - // package path. If empty, the symbol is assumed to be alive and will not be - // eliminated. For methods it is the same as its receiver type identifier. - DceObjectFilter string - // The second part of the identified used by dead-code elimination for methods. - // Empty for other types of symbols. - DceMethodFilter string - // List of fully qualified (including package path) DCE symbol identifiers the - // symbol depends on for dead code elimination purposes. - DceDeps []string + // DCEInfo stores the information for dead-code elimination. + DCEInfo dce.Info // Set to true if a function performs a blocking operation (I/O or // synchronization). The compiler will have to generate function code such // that it can be resumed after a blocking operation completes without @@ -71,16 +84,26 @@ type Decl struct { // minify returns a copy of Decl with unnecessary whitespace removed from the // JS code. func (d Decl) minify() Decl { - d.DeclCode = removeWhitespace(d.DeclCode, true) + d.ImportCode = removeWhitespace(d.ImportCode, true) + d.TypeDeclCode = removeWhitespace(d.TypeDeclCode, true) + d.ExportTypeCode = removeWhitespace(d.ExportTypeCode, true) + d.AnonTypeDeclCode = removeWhitespace(d.AnonTypeDeclCode, true) + d.FuncDeclCode = removeWhitespace(d.FuncDeclCode, true) + d.ExportFuncCode = removeWhitespace(d.ExportFuncCode, true) d.MethodListCode = removeWhitespace(d.MethodListCode, true) d.TypeInitCode = removeWhitespace(d.TypeInitCode, true) d.InitCode = removeWhitespace(d.InitCode, true) return d } +// Dce gets the information for dead-code elimination. +func (d *Decl) Dce() *dce.Info { + return &d.DCEInfo +} + // topLevelObjects extracts package-level variables, functions and named types // from the package AST. -func (fc *funcContext) topLevelObjects(srcs sources) (vars []*types.Var, functions []*ast.FuncDecl, typeNames typesutil.TypeNames) { +func (fc *funcContext) topLevelObjects(srcs *sources.Sources) (vars []*types.Var, functions []*ast.FuncDecl, typeNames typesutil.TypeNames) { if !fc.isRoot() { panic(bailout(fmt.Errorf("functionContext.discoverObjects() must be only called on the package-level context"))) } @@ -161,11 +184,14 @@ func (fc *funcContext) importDecls() (importedPaths []string, importDecls []*Dec // newImportDecl registers the imported package and returns a Decl instance for it. func (fc *funcContext) newImportDecl(importedPkg *types.Package) *Decl { pkgVar := fc.importedPkgVar(importedPkg) - return &Decl{ - Vars: []string{pkgVar}, - DeclCode: []byte(fmt.Sprintf("\t%s = $packages[\"%s\"];\n", pkgVar, importedPkg.Path())), - InitCode: fc.CatchOutput(1, func() { fc.translateStmt(fc.importInitializer(importedPkg.Path()), nil) }), + d := &Decl{ + FullName: importDeclFullName(importedPkg), + Vars: []string{pkgVar}, + ImportCode: []byte(fmt.Sprintf("\t%s = $packages[\"%s\"];\n", pkgVar, importedPkg.Path())), + InitCode: fc.CatchOutput(1, func() { fc.translateStmt(fc.importInitializer(importedPkg.Path()), nil) }), } + d.Dce().SetAsAlive() + return d } // importInitializer calls the imported package $init() function to ensure it is @@ -224,7 +250,9 @@ func (fc *funcContext) varDecls(vars []*types.Var) []*Decl { // newVarDecl creates a new Decl describing a variable, given an explicit // initializer. func (fc *funcContext) newVarDecl(init *types.Initializer) *Decl { - var d Decl + d := &Decl{ + FullName: varDeclFullName(init), + } assignLHS := []ast.Expr{} for _, o := range init.Lhs { @@ -241,7 +269,7 @@ func (fc *funcContext) newVarDecl(init *types.Initializer) *Decl { } } - d.DceDeps = fc.CollectDCEDeps(func() { + fc.pkgCtx.CollectDCEDeps(d, func() { fc.localVars = nil d.InitCode = fc.CatchOutput(1, func() { fc.translateStmt(&ast.AssignStmt{ @@ -257,12 +285,11 @@ func (fc *funcContext) newVarDecl(init *types.Initializer) *Decl { fc.localVars = nil // Clean up after ourselves. }) - if len(init.Lhs) == 1 { - if !analysis.HasSideEffect(init.Rhs, fc.pkgCtx.Info.Info) { - d.DceObjectFilter = init.Lhs[0].Name() - } + d.Dce().SetName(init.Lhs[0], nil, nil) + if len(init.Lhs) != 1 || analysis.HasSideEffect(init.Rhs, fc.pkgCtx.Info.Info) { + d.Dce().SetAsAlive() } - return &d + return d } // funcDecls translates all package-level function and methods. @@ -276,23 +303,36 @@ func (fc *funcContext) funcDecls(functions []*ast.FuncDecl) ([]*Decl, error) { var mainFunc *types.Func for _, fun := range functions { o := fc.pkgCtx.Defs[fun.Name].(*types.Func) + instances := fc.knownInstances(o) + if len(instances) == 0 { + // No instances of the function, skip it. + continue + } if fun.Recv == 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{fc.objectName(o)} - if o.Type().(*types.Signature).TypeParams().Len() != 0 { - varDecl.DeclCode = fc.CatchOutput(0, func() { - fc.Printf("%s = {};", fc.objectName(o)) + // package-level variable by which they all are referenced, + // e.g. init functions and instances of generic functions. + objName := fc.objectName(o) + generic := len(instances) > 1 || !instances[0].IsTrivial() + varName := objName + if generic { + varName += ` = []` + } + varDecl := &Decl{ + FullName: funcVarDeclFullName(o), + Vars: []string{varName}, + } + varDecl.Dce().SetName(o, nil, nil) + if generic && o.Exported() { + varDecl.ExportFuncCode = fc.CatchOutput(1, func() { + fc.Printf("$pkg.%s = %s;", encodeIdent(fun.Name.Name), objName) }) } - funcDecls = append(funcDecls, &varDecl) + funcDecls = append(funcDecls, varDecl) } - for _, inst := range fc.knownInstances(o) { + for _, inst := range instances { funcDecls = append(funcDecls, fc.newFuncDecl(fun, inst)) if o.Name() == "main" { @@ -308,6 +348,7 @@ func (fc *funcContext) funcDecls(functions []*ast.FuncDecl) ([]*Decl, error) { // been initialized. It must come after all other functions, especially all // init() functions, otherwise main() will be invoked too early. funcDecls = append(funcDecls, &Decl{ + FullName: mainFuncDeclFullName(), InitCode: fc.CatchOutput(1, func() { fc.translateStmt(fc.callMainFunc(mainFunc), nil) }), }) } @@ -318,34 +359,30 @@ func (fc *funcContext) funcDecls(functions []*ast.FuncDecl) ([]*Decl, error) { func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance) *Decl { o := fc.pkgCtx.Defs[fun.Name].(*types.Func) d := &Decl{ - FullName: o.FullName(), - Blocking: fc.pkgCtx.IsBlocking(o), + FullName: funcDeclFullName(inst), + Blocking: fc.pkgCtx.IsBlocking(inst), LinkingName: symbol.New(o), } + d.Dce().SetName(o, inst.TNest, inst.TArgs) if typesutil.IsMethod(o) { recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj() d.NamedRecvType = fc.objectName(recv) - d.DceObjectFilter = recv.Name() - if !fun.Name.IsExported() { - d.DceMethodFilter = o.Name() + "~" - } } else { d.RefExpr = fc.instName(inst) - d.DceObjectFilter = o.Name() switch o.Name() { case "main": if fc.pkgCtx.isMain() { // Found main() function of the program. - d.DceObjectFilter = "" // Always reachable. + d.Dce().SetAsAlive() // Always reachable. } case "init": d.InitCode = fc.CatchOutput(1, func() { fc.translateStmt(fc.callInitFunc(o), nil) }) - d.DceObjectFilter = "" // init() function is always reachable. + d.Dce().SetAsAlive() // init() function is always reachable. } } - d.DceDeps = fc.CollectDCEDeps(func() { - d.DeclCode = fc.translateTopLevelFunction(fun, inst) + fc.pkgCtx.CollectDCEDeps(d, func() { + d.FuncDeclCode = fc.namedFuncContext(inst).translateTopLevelFunction(fun) }) return d } @@ -355,7 +392,7 @@ func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance) func (fc *funcContext) callInitFunc(init *types.Func) ast.Stmt { id := fc.newIdentFor(init) call := &ast.CallExpr{Fun: id} - if fc.pkgCtx.IsBlocking(init) { + if fc.pkgCtx.IsBlocking(typeparams.Instance{Object: init}) { fc.Blocking[call] = true } return &ast.ExprStmt{X: call} @@ -379,7 +416,7 @@ func (fc *funcContext) callMainFunc(main *types.Func) ast.Stmt { }, }, } - if fc.pkgCtx.IsBlocking(main) { + if fc.pkgCtx.IsBlocking(typeparams.Instance{Object: main}) { fc.Blocking[call] = true fc.Flattened[ifStmt] = true } @@ -423,15 +460,20 @@ func (fc *funcContext) namedTypeDecls(typeNames typesutil.TypeNames) ([]*Decl, e // of the type, keyed by the type argument combination. Otherwise it contains // the type definition directly. func (fc *funcContext) newNamedTypeVarDecl(obj *types.TypeName) *Decl { - varDecl := &Decl{Vars: []string{fc.objectName(obj)}} - if typeparams.HasTypeParams(obj.Type()) { - varDecl.DeclCode = fc.CatchOutput(0, func() { - fc.Printf("%s = {};", fc.objectName(obj)) - }) + name := fc.objectName(obj) + generic := fc.pkgCtx.instanceSet.Pkg(obj.Pkg()).ObjHasInstances(obj) + varName := name + if generic { + varName += ` = []` } + varDecl := &Decl{ + FullName: typeVarDeclFullName(obj), + Vars: []string{varName}, + } + varDecl.Dce().SetName(obj, nil, nil) if isPkgLevel(obj) { - varDecl.TypeInitCode = fc.CatchOutput(0, func() { - fc.Printf("$pkg.%s = %s;", encodeIdent(obj.Name()), fc.objectName(obj)) + varDecl.ExportTypeCode = fc.CatchOutput(0, func() { + fc.Printf("$pkg.%s = %s;", encodeIdent(obj.Name()), name) }) } return varDecl @@ -442,25 +484,29 @@ func (fc *funcContext) newNamedTypeVarDecl(obj *types.TypeName) *Decl { func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, error) { originType := inst.Object.Type().(*types.Named) - fc.typeResolver = typeparams.NewResolver(fc.pkgCtx.typesCtx, typeparams.ToSlice(originType.TypeParams()), inst.TArgs) + fc.typeResolver = typeparams.NewResolver(fc.pkgCtx.typesCtx, inst) defer func() { fc.typeResolver = nil }() instanceType := originType if !inst.IsTrivial() { - instantiated, err := types.Instantiate(fc.pkgCtx.typesCtx, originType, inst.TArgs, true) - if err != nil { - return nil, fmt.Errorf("failed to instantiate type %v with args %v: %w", originType, inst.TArgs, err) + if len(inst.TArgs) > 0 { + instantiated, err := types.Instantiate(fc.pkgCtx.typesCtx, originType, inst.TArgs, true) + if err != nil { + return nil, fmt.Errorf("failed to instantiate type %v with args %v: %w", originType, inst.TArgs, err) + } + instanceType = instantiated.(*types.Named) } - instanceType = instantiated.(*types.Named) + instanceType = fc.typeResolver.Substitute(instanceType).(*types.Named) } underlying := instanceType.Underlying() d := &Decl{ - DceObjectFilter: inst.Object.Name(), + FullName: typeDeclFullName(inst), } - d.DceDeps = fc.CollectDCEDeps(func() { + d.Dce().SetName(inst.Object, inst.TNest, inst.TArgs) + fc.pkgCtx.CollectDCEDeps(d, func() { // Code that declares a JS type (i.e. prototype) for each Go type. - d.DeclCode = fc.CatchOutput(0, func() { + d.TypeDeclCode = fc.CatchOutput(0, func() { size := int64(0) constructor := "null" @@ -482,7 +528,7 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er }) // Reflection metadata about methods the type has. - d.MethodListCode = fc.CatchOutput(0, func() { + d.MethodListCode = fc.CatchOutput(1, func() { if _, ok := underlying.(*types.Interface); ok { return } @@ -508,7 +554,7 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er // initialize themselves. switch t := underlying.(type) { case *types.Array, *types.Chan, *types.Interface, *types.Map, *types.Pointer, *types.Slice, *types.Signature, *types.Struct: - d.TypeInitCode = fc.CatchOutput(0, func() { + d.TypeInitCode = fc.CatchOutput(1, func() { fc.Printf("%s.init(%s);", fc.instName(inst), fc.initArgs(t)) }) } @@ -518,6 +564,10 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er // structConstructor returns JS constructor function for a struct type. func (fc *funcContext) structConstructor(t *types.Struct) string { + if t.NumFields() == 0 { + return `function() { this.$val = this; }` + } + constructor := &strings.Builder{} ctrArgs := make([]string, t.NumFields()) @@ -531,7 +581,8 @@ func (fc *funcContext) structConstructor(t *types.Struct) string { // If no arguments were passed, zero-initialize all fields. fmt.Fprintf(constructor, "\t\tif (arguments.length === 0) {\n") for i := 0; i < t.NumFields(); i++ { - fmt.Fprintf(constructor, "\t\t\tthis.%s = %s;\n", fieldName(t, i), fc.translateExpr(fc.zeroValue(t.Field(i).Type())).String()) + zeroValue := fc.zeroValue(fc.fieldType(t, i)) + fmt.Fprintf(constructor, "\t\t\tthis.%s = %s;\n", fieldName(t, i), fc.translateExpr(zeroValue).String()) } fmt.Fprintf(constructor, "\t\t\treturn;\n") fmt.Fprintf(constructor, "\t\t}\n") @@ -577,14 +628,15 @@ func (fc *funcContext) anonTypeDecls(anonTypes []*types.TypeName) []*Decl { } decls := []*Decl{} for _, t := range anonTypes { - d := Decl{ - Vars: []string{t.Name()}, - DceObjectFilter: t.Name(), + d := &Decl{ + FullName: anonTypeDeclFullName(t), + Vars: []string{t.Name()}, } - d.DceDeps = fc.CollectDCEDeps(func() { - d.DeclCode = []byte(fmt.Sprintf("\t%s = $%sType(%s);\n", t.Name(), strings.ToLower(typeKind(t.Type())[5:]), fc.initArgs(t.Type()))) + d.Dce().SetName(t, nil, nil) + fc.pkgCtx.CollectDCEDeps(d, func() { + d.AnonTypeDeclCode = []byte(fmt.Sprintf("\t\t%s = $%sType(%s);\n", t.Name(), strings.ToLower(typeKind(t.Type())[5:]), fc.initArgs(t.Type()))) }) - decls = append(decls, &d) + decls = append(decls, d) } return decls } diff --git a/compiler/expressions.go b/compiler/expressions.go index 4b6653731..75a24ce20 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -11,8 +11,8 @@ import ( "strconv" "strings" - "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) @@ -178,18 +178,18 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } if !isKeyValue { for i, element := range e.Elts { - elements[i] = fc.translateImplicitConversionWithCloning(element, t.Field(i).Type()).String() + elements[i] = fc.translateImplicitConversionWithCloning(element, fc.fieldType(t, i)).String() } } if isKeyValue { for i := range elements { - elements[i] = fc.translateExpr(fc.zeroValue(t.Field(i).Type())).String() + elements[i] = fc.translateExpr(fc.zeroValue(fc.fieldType(t, i))).String() } for _, element := range e.Elts { kve := element.(*ast.KeyValueExpr) for j := range elements { if kve.Key.(*ast.Ident).Name == t.Field(j).Name() { - elements[j] = fc.translateImplicitConversionWithCloning(kve.Value, t.Field(j).Type()).String() + elements[j] = fc.translateImplicitConversionWithCloning(kve.Value, fc.fieldType(t, j)).String() break } } @@ -201,7 +201,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } case *ast.FuncLit: - fun := fc.nestedFunctionContext(fc.pkgCtx.FuncLitInfos[e], exprType.(*types.Signature), typeparams.Instance{}).translateFunctionBody(e.Type, nil, e.Body, "") + fun := fc.literalFuncContext(e).translateFunctionBody(e.Type, nil, e.Body) if len(fc.pkgCtx.escapingVars) != 0 { names := make([]string, 0, len(fc.pkgCtx.escapingVars)) for obj := range fc.pkgCtx.escapingVars { @@ -529,14 +529,28 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case *types.Basic: return fc.formatExpr("%e.charCodeAt(%f)", e.X, e.Index) case *types.Signature: - return fc.formatExpr("%s", fc.instName(fc.instanceOf(e.X.(*ast.Ident)))) + switch u := e.X.(type) { + case *ast.Ident: + return fc.formatExpr("%s", fc.instName(fc.instanceOf(u))) + case *ast.SelectorExpr: + return fc.formatExpr("%s", fc.instName(fc.instanceOf(u.Sel))) + default: + panic(fmt.Errorf("unhandled IndexExpr of a Signature: %T in %T", u, fc.typeOf(e.X))) + } default: - panic(fmt.Errorf(`unhandled IndexExpr: %T`, t)) + panic(fmt.Errorf(`unhandled IndexExpr: %T in %T`, t, fc.typeOf(e.X))) } 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)))) + switch u := e.X.(type) { + case *ast.Ident: + return fc.formatExpr("%s", fc.instName(fc.instanceOf(u))) + case *ast.SelectorExpr: + return fc.formatExpr("%s", fc.instName(fc.instanceOf(u.Sel))) + default: + panic(fmt.Errorf("unhandled IndexListExpr of a Signature: %T in %T", u, fc.typeOf(e.X))) + } default: panic(fmt.Errorf("unhandled IndexListExpr: %T", t)) } @@ -591,9 +605,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case types.MethodVal: return fc.formatExpr(`$methodVal(%s, "%s")`, fc.makeReceiver(e), sel.Obj().(*types.Func).Name()) case types.MethodExpr: - if !sel.Obj().Exported() { - fc.DeclareDCEDep(sel.Obj()) - } + fc.pkgCtx.DeclareDCEDep(sel.Obj(), inst.TNest, inst.TArgs) if _, ok := sel.Recv().Underlying().(*types.Interface); ok { return fc.formatExpr(`$ifaceMethodExpr("%s")`, sel.Obj().(*types.Func).Name()) } @@ -730,10 +742,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } } - methodName := sel.Obj().Name() - if reservedKeywords[methodName] { - methodName += "$" - } + methodName := fc.methodName(sel.Obj().(*types.Func)) return fc.translateCall(e, sig, fc.formatExpr("%s.%s", recv, methodName)) case types.FieldVal: @@ -806,7 +815,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { switch t := exprType.Underlying().(type) { case *types.Basic: if t.Kind() != types.UnsafePointer { - panic("unexpected basic type") + panic(fmt.Errorf(`unexpected basic type: %v in %v`, t, e.Name)) } return fc.formatExpr("0") case *types.Slice, *types.Pointer: @@ -911,7 +920,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression { sel, _ := fc.selectionOf(e) if !sel.Obj().Exported() { - fc.DeclareDCEDep(sel.Obj()) + fc.pkgCtx.DeclareDCEDep(sel.Obj(), nil, nil) } x := e.X @@ -922,7 +931,7 @@ func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression { recvType = ptr.Elem() } s := recvType.Underlying().(*types.Struct) - recvType = s.Field(index).Type() + recvType = fc.fieldType(s, index) } fakeSel := &ast.SelectorExpr{X: x, Sel: ast.NewIdent("o")} @@ -987,6 +996,10 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args case "len": switch argType := fc.typeOf(args[0]).Underlying().(type) { case *types.Basic: + // If the argument is a concatenation of strings, then add parentheses. + if _, ok := args[0].(*ast.BinaryExpr); ok { + return fc.formatExpr("(%e).length", args[0]) + } return fc.formatExpr("%e.length", args[0]) case *types.Slice: return fc.formatExpr("%e.$length", args[0]) @@ -1318,12 +1331,13 @@ func (fc *funcContext) loadStruct(array, target string, s *types.Struct) string var collectFields func(s *types.Struct, path string) collectFields = func(s *types.Struct, path string) { for i := 0; i < s.NumFields(); i++ { - field := s.Field(i) - if fs, isStruct := field.Type().Underlying().(*types.Struct); isStruct { - collectFields(fs, path+"."+fieldName(s, i)) + fieldName := path + "." + fieldName(s, i) + fieldType := fc.fieldType(s, i) + if fs, isStruct := fieldType.Underlying().(*types.Struct); isStruct { + collectFields(fs, fieldName) continue } - fields = append(fields, types.NewVar(0, nil, path+"."+fieldName(s, i), field.Type())) + fields = append(fields, types.NewVar(0, nil, fieldName, fieldType)) } } collectFields(s, target) diff --git a/compiler/functions.go b/compiler/functions.go index ed3062c60..ef5c57f7d 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -5,30 +5,34 @@ package compiler import ( "bytes" + "errors" "fmt" "go/ast" "go/types" "sort" "strings" - "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) -// newFunctionContext creates a new nested context for a function corresponding +// nestedFunctionContext creates a new nested context for a function corresponding // to the provided info and instance. -func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, sig *types.Signature, inst typeparams.Instance) *funcContext { +func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, inst typeparams.Instance) *funcContext { if info == nil { - panic(fmt.Errorf("missing *analysis.FuncInfo")) + panic(errors.New("missing *analysis.FuncInfo")) } - if sig == nil { - panic(fmt.Errorf("missing *types.Signature")) + if inst.Object == nil { + panic(errors.New("missing inst.Object")) } + o := inst.Object.(*types.Func) + sig := o.Type().(*types.Signature) c := &funcContext{ FuncInfo: info, + instance: inst, pkgCtx: fc.pkgCtx, parent: fc, allVars: make(map[string]int, len(fc.allVars)), @@ -44,62 +48,110 @@ func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, sig *types c.allVars[k] = v } - 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) + // Use the parent function's resolver unless the function has it's own type arguments. + if !inst.IsTrivial() { + c.typeResolver = typeparams.NewResolver(fc.pkgCtx.typesCtx, inst) } - if c.objectNames == nil { - c.objectNames = map[types.Object]string{} + + // Synthesize an identifier by which the function may reference itself. Since + // it appears in the stack trace, it's useful to include the receiver type in + // it. + funcRef := o.Name() + if recvType := typesutil.RecvType(sig); recvType != nil { + funcRef = recvType.Obj().Name() + midDot + funcRef } + c.funcRef = c.newVariable(funcRef, true /*pkgLevel*/) + + return c +} + +// namedFuncContext creates a new funcContext for a named Go function +// (standalone or method). +func (fc *funcContext) namedFuncContext(inst typeparams.Instance) *funcContext { + info := fc.pkgCtx.FuncInfo(inst) + c := fc.nestedFunctionContext(info, inst) + + return c +} + +// literalFuncContext creates a new funcContext for a function literal. Since +// go/types doesn't generate *types.Func objects for function literals, we +// generate a synthetic one for it. +func (fc *funcContext) literalFuncContext(fun *ast.FuncLit) *funcContext { + info := fc.pkgCtx.FuncLitInfo(fun, fc.TypeArgs()) + sig := fc.pkgCtx.TypeOf(fun).(*types.Signature) + o := types.NewFunc(fun.Pos(), fc.pkgCtx.Pkg, fc.newLitFuncName(), sig) + inst := typeparams.Instance{Object: o} + c := fc.nestedFunctionContext(info, inst) return c } // translateTopLevelFunction translates a top-level function declaration -// (standalone function or method) into a corresponding JS function. +// (standalone function or method) into a corresponding JS function. Must be +// called on the function context created for the function corresponding instance. // -// Returns a string with a JavaScript statements that define the function or +// Returns a string with JavaScript statements that define the function or // method. For methods it returns declarations for both value- and // pointer-receiver (if appropriate). -func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl, inst typeparams.Instance) []byte { +func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl) []byte { if fun.Recv == nil { - return fc.translateStandaloneFunction(fun, inst) + return fc.translateStandaloneFunction(fun) } - o := inst.Object.(*types.Func) - info := fc.pkgCtx.FuncDeclInfos[o] + return fc.translateMethod(fun) +} + +// translateStandaloneFunction translates a package-level function. +// +// It returns JS statements which define the corresponding function in a +// package context. Exported functions are also assigned to the `$pkg` object. +func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl) []byte { + o := fc.instance.Object.(*types.Func) + + if fun.Recv != nil { + panic(fmt.Errorf("expected standalone function, got method: %s", o)) + } + + lvalue := fc.instName(fc.instance) + + if fun.Body == nil { + return []byte(fmt.Sprintf("\t\t%s = %s;\n", lvalue, fc.unimplementedFunction(o))) + } + + body := fc.translateFunctionBody(fun.Type, nil, fun.Body) + code := bytes.NewBuffer(nil) + fmt.Fprintf(code, "\t\t%s = %s;\n", lvalue, body) + if fun.Name.IsExported() && fc.instance.IsTrivial() { + fmt.Fprintf(code, "\t\t$pkg.%s = %s;\n", encodeIdent(fun.Name.Name), lvalue) + } + return code.Bytes() +} + +// translateMethod translates a named type method. +// +// It returns one or more JS statements which define the method. Methods with +// non-pointer receiver are automatically defined for the pointer-receiver type. +func (fc *funcContext) translateMethod(fun *ast.FuncDecl) []byte { + o := fc.instance.Object.(*types.Func) + funName := fc.methodName(o) - sig := o.Type().(*types.Signature) // primaryFunction generates a JS function equivalent of the current Go function // and assigns it to the JS expression defined by lvalue. primaryFunction := func(lvalue string) []byte { if fun.Body == nil { - return []byte(fmt.Sprintf("\t%s = function() {\n\t\t$throwRuntimeError(\"native function not implemented: %s\");\n\t};\n", lvalue, o.FullName())) + return []byte(fmt.Sprintf("\t\t%s = %s;\n", lvalue, fc.unimplementedFunction(o))) } var recv *ast.Ident if fun.Recv != nil && fun.Recv.List[0].Names != nil { recv = fun.Recv.List[0].Names[0] } - fun := fc.nestedFunctionContext(info, sig, inst).translateFunctionBody(fun.Type, recv, fun.Body, lvalue) - return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fun)) - } - - funName := fun.Name.Name - if reservedKeywords[funName] { - funName += "$" + fun := fc.translateFunctionBody(fun.Type, recv, fun.Body) + return []byte(fmt.Sprintf("\t\t%s = %s;\n", lvalue, fun)) } - // proxyFunction generates a JS function that forwards the call to the actual - // method implementation for the alternate receiver (e.g. pointer vs - // non-pointer). - proxyFunction := func(lvalue, receiver string) []byte { - fun := fmt.Sprintf("function(...$args) { return %s.%s(...$args); }", receiver, funName) - return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fun)) - } - - recvInst := inst.Recv() + recvInst := fc.instance.Recv() recvInstName := fc.instName(recvInst) recvType := recvInst.Object.Type().(*types.Named) @@ -108,70 +160,51 @@ func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl, inst typepar prototypeVar := fmt.Sprintf("%s.prototype.%s", recvInstName, funName) ptrPrototypeVar := fmt.Sprintf("$ptrType(%s).prototype.%s", recvInstName, funName) - code := bytes.NewBuffer(nil) + // Methods with pointer-receiver are only attached to the pointer-receiver type. + if _, isPointer := fc.sig.Sig.Recv().Type().(*types.Pointer); isPointer { + return primaryFunction(ptrPrototypeVar) + } + // Methods with non-pointer receivers must be defined both for the pointer + // and non-pointer types. To minimize generated code size, we generate a + // complete implementation for only one receiver (non-pointer for most types) + // and define a proxy function on the other, which converts the receiver type + // and forwards the call to the primary implementation. + proxyFunction := func(lvalue, receiver string) []byte { + fun := fmt.Sprintf("function(...$args) { return %s.%s(...$args); }", receiver, funName) + return []byte(fmt.Sprintf("\t\t%s = %s;\n", lvalue, fun)) + } + + // Structs are a special case: they are represented by JS objects and their + // methods are the underlying object's methods. Due to reference semantics of + // the JS variables, the actual backing object is considered to represent the + // pointer-to-struct type, and methods are attacher to it first and foremost. if _, isStruct := recvType.Underlying().(*types.Struct); isStruct { - // Structs are a special case: they are represented by JS objects and their - // methods are the underlying object's methods. Due to reference semantics - // of the JS variables, the actual backing object is considered to represent - // the pointer-to-struct type, and methods are attacher to it first and - // foremost. + code := bytes.Buffer{} code.Write(primaryFunction(ptrPrototypeVar)) code.Write(proxyFunction(prototypeVar, "this.$val")) return code.Bytes() } - if ptr, isPointer := sig.Recv().Type().(*types.Pointer); isPointer { - if _, isArray := ptr.Elem().Underlying().(*types.Array); isArray { - // Pointer-to-array is another special case. - // TODO(nevkontakte) Find out and document why. - code.Write(primaryFunction(prototypeVar)) - code.Write(proxyFunction(ptrPrototypeVar, fmt.Sprintf("(new %s(this.$get()))", recvInstName))) - return code.Bytes() - } - - // Methods with pointer-receiver are only attached to the pointer-receiver - // type. - return primaryFunction(ptrPrototypeVar) - } - // Methods defined for non-pointer receiver are attached to both pointer- and // non-pointer-receiver types. - recvExpr := "this.$get()" + proxyRecvExpr := "this.$get()" if isWrapped(recvType) { - recvExpr = fmt.Sprintf("new %s(%s)", recvInstName, recvExpr) + proxyRecvExpr = fmt.Sprintf("new %s(%s)", recvInstName, proxyRecvExpr) } + code := bytes.Buffer{} code.Write(primaryFunction(prototypeVar)) - code.Write(proxyFunction(ptrPrototypeVar, recvExpr)) + code.Write(proxyFunction(ptrPrototypeVar, proxyRecvExpr)) return code.Bytes() } -// translateStandaloneFunction translates a package-level function. +// unimplementedFunction returns a JS function expression for a Go function +// without a body, which would throw an exception if called. // -// It returns a JS statements which define the corresponding function in a -// package context. Exported functions are also assigned to the `$pkg` object. -func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl, inst typeparams.Instance) []byte { - o := inst.Object.(*types.Func) - info := fc.pkgCtx.FuncDeclInfos[o] - sig := o.Type().(*types.Signature) - - if fun.Recv != nil { - panic(fmt.Errorf("expected standalone function, got method: %s", o)) - } - - lvalue := fc.instName(inst) - - if fun.Body == nil { - return []byte(fmt.Sprintf("\t%s = function() {\n\t\t$throwRuntimeError(\"native function not implemented: %s\");\n\t};\n", lvalue, o.FullName())) - } - - body := fc.nestedFunctionContext(info, sig, inst).translateFunctionBody(fun.Type, nil, fun.Body, lvalue) - code := bytes.NewBuffer(nil) - fmt.Fprintf(code, "\t%s = %s;\n", lvalue, body) - if fun.Name.IsExported() { - fmt.Fprintf(code, "\t$pkg.%s = %s;\n", encodeIdent(fun.Name.Name), lvalue) - } - return code.Bytes() +// In Go such functions are either used with a //go:linkname directive or with +// assembler intrinsics, only former of which is supported by GopherJS. +func (fc *funcContext) unimplementedFunction(o *types.Func) string { + return fmt.Sprintf("function() {\n\t\t$throwRuntimeError(\"native function not implemented: %s\");\n\t}", o.FullName()) } // translateFunctionBody translates body of a top-level or literal function. @@ -179,7 +212,7 @@ func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl, inst typep // It returns a JS function expression that represents the given Go function. // Function receiver must have been created with nestedFunctionContext() to have // required metadata set up. -func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, funcRef string) string { +func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt) string { prevEV := fc.pkgCtx.escapingVars // Generate a list of function argument variables. Since Go allows nameless @@ -199,8 +232,8 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, } } - bodyOutput := string(fc.CatchOutput(1, func() { - if len(fc.Blocking) != 0 { + bodyOutput := string(fc.CatchOutput(2, func() { + if fc.IsBlocking() { fc.pkgCtx.Scopes[body] = fc.pkgCtx.Scopes[typ] fc.handleEscapingVars(body) } @@ -233,7 +266,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, sort.Strings(fc.localVars) - var prefix, suffix, functionName string + var prefix, suffix string if len(fc.Flattened) != 0 { // $s contains an index of the switch case a blocking function reached @@ -246,29 +279,27 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.HasDefer { fc.localVars = append(fc.localVars, "$deferred") suffix = " }" + suffix - if len(fc.Blocking) != 0 { + if fc.IsBlocking() { suffix = " }" + suffix } } localVarDefs := "" // Function-local var declaration at the top. - if len(fc.Blocking) != 0 { - if funcRef == "" { - funcRef = "$b" - functionName = " $b" - } - + if fc.IsBlocking() { localVars := append([]string{}, fc.localVars...) // There are several special variables involved in handling blocking functions: // $r is sometimes used as a temporary variable to store blocking call result. // $c indicates that a function is being resumed after a blocking call when set to true. // $f is an object used to save and restore function context for blocking calls. localVars = append(localVars, "$r") + // funcRef identifies the function object itself, so it doesn't need to be saved + // or restored. + localVars = removeMatching(localVars, fc.funcRef) // If a blocking function is being resumed, initialize local variables from the saved context. localVarDefs = fmt.Sprintf("var {%s, $c} = $restore(this, {%s});\n", strings.Join(localVars, ", "), strings.Join(args, ", ")) // If the function gets blocked, save local variables for future. - saveContext := fmt.Sprintf("var $f = {$blk: "+funcRef+", $c: true, $r, %s};", strings.Join(fc.localVars, ", ")) + saveContext := fmt.Sprintf("var $f = {$blk: "+fc.funcRef+", $c: true, $r, %s};", strings.Join(fc.localVars, ", ")) suffix = " " + saveContext + "return $f;" + suffix } else if len(fc.localVars) > 0 { @@ -279,7 +310,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.HasDefer { prefix = prefix + " var $err = null; try {" deferSuffix := " } catch(err) { $err = err;" - if len(fc.Blocking) != 0 { + if fc.IsBlocking() { deferSuffix += " $s = -1;" } if fc.resultNames == nil && fc.sig.HasResults() { @@ -289,7 +320,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.resultNames != nil { deferSuffix += fmt.Sprintf(" if (!$curGoroutine.asleep) { return %s; }", fc.translateResults(fc.resultNames)) } - if len(fc.Blocking) != 0 { + if fc.IsBlocking() { deferSuffix += " if($curGoroutine.asleep) {" } suffix = deferSuffix + suffix @@ -305,16 +336,16 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, } if prefix != "" { - bodyOutput = fc.Indentation(1) + "/* */" + prefix + "\n" + bodyOutput + bodyOutput = fc.Indentation(2) + "/* */" + prefix + "\n" + bodyOutput } if suffix != "" { - bodyOutput = bodyOutput + fc.Indentation(1) + "/* */" + suffix + "\n" + bodyOutput = bodyOutput + fc.Indentation(2) + "/* */" + suffix + "\n" } if localVarDefs != "" { - bodyOutput = fc.Indentation(1) + localVarDefs + bodyOutput + bodyOutput = fc.Indentation(2) + localVarDefs + bodyOutput } fc.pkgCtx.escapingVars = prevEV - return fmt.Sprintf("function%s(%s) {\n%s%s}", functionName, strings.Join(args, ", "), bodyOutput, fc.Indentation(0)) + return fmt.Sprintf("function %s(%s) {\n%s%s}", fc.funcRef, strings.Join(args, ", "), bodyOutput, fc.Indentation(1)) } diff --git a/compiler/gopherjspkg/fs.go b/compiler/gopherjspkg/fs.go index 0ec155308..caa07e11c 100644 --- a/compiler/gopherjspkg/fs.go +++ b/compiler/gopherjspkg/fs.go @@ -1,8 +1,6 @@ package gopherjspkg -import ( - "net/http" -) +import "net/http" // FS is a virtual filesystem that contains core GopherJS packages. var FS http.FileSystem diff --git a/compiler/analysis/bool.go b/compiler/internal/analysis/bool.go similarity index 100% rename from compiler/analysis/bool.go rename to compiler/internal/analysis/bool.go diff --git a/compiler/analysis/break.go b/compiler/internal/analysis/break.go similarity index 100% rename from compiler/analysis/break.go rename to compiler/internal/analysis/break.go diff --git a/compiler/internal/analysis/defer.go b/compiler/internal/analysis/defer.go new file mode 100644 index 000000000..5d4f151a3 --- /dev/null +++ b/compiler/internal/analysis/defer.go @@ -0,0 +1,101 @@ +package analysis + +import ( + "go/ast" + "go/types" + + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/typesutil" +) + +// deferStmt represents a defer statement that is blocking or not. +// +// A blocking defer statement will cause a return statement to be blocking +// since the defer is called and potentially blocked while leaving the method. +// We try to determine which defers affect which returns so that we only +// mark returns as blocking if they are affected by a blocking defer. +// In general we know that a defer will affect all returns that have been +// declared after the defer statement. +// +// Since analysis doesn't create [CFG] basic blocks for full control +// flow analysis we can't easily determine several cases: +// +// - Terminating if-statements(i.e. does the body of the if-statement always +// return from the method) are difficult to determine. Any defer that is +// added whilst inside a terminating if-statement body can only affect the +// returns inside that if-statement body. +// Otherwise, the defer may affect returns after the if-statement block has +// rejoined the flow that it branched from. Since terminating if-statements +// are difficult to determine without [CFG] blocks, we treat all +// if-statements as if they are not terminating. +// That means there may be some false positives, since returns declared +// after a terminating branch will be marked as affected by a defer +// declared in that branch, when in reality they are not. +// +// - Same as above but for else blocks, switch cases, and any branching. +// +// - Loops (i.e. for-statements and for-range-statements) can cause return +// statements declared earlier in the loop to be affected by defers +// declared after it in the loop. We can't determine which branches in a +// loop may return to the start of the loop so we assume anywhere inside +// of a loop can return to the start of the loop. +// To handle this, all defers defined anywhere within a loop are assumed +// to affect any return also defined in that loop. +// We only need to track the top-level loop since nested loops will be +// superseded by the top-level loop. +// +// - Labels and goto's are similar to loops in [CFG] blocks but without those +// blocks it's harder to determine which defers will affect which returns. +// To be safe, for any function with any blocking defers, returns, and +// goto's, all the returns are defaulted to blocking. +// +// [CFG]: https://en.wikipedia.org/wiki/Control-flow_graph +type deferStmt struct { + obj types.Object + lit *ast.FuncLit + typeArgs typesutil.TypeList +} + +// newBlockingDefer creates a new defer statement that is blocking. +// +// If the defer is calling a js.Object method then the defer is non-blocking. +// If the defers calling an interface method or function pointer in a var +// then the defer is blocking. +func newBlockingDefer() *deferStmt { + return &deferStmt{} +} + +// newInstDefer creates a new defer statement for an instances of a method. +// The instance is used to look up the blocking information later. +func newInstDefer(inst typeparams.Instance) *deferStmt { + return &deferStmt{obj: inst.Object, typeArgs: inst.TArgs} +} + +// newLitDefer creates a new defer statement for a function literal. +// The literal is used to look up the blocking information later. +func newLitDefer(lit *ast.FuncLit, typeArgs typesutil.TypeList) *deferStmt { + return &deferStmt{lit: lit, typeArgs: typeArgs} +} + +// IsBlocking determines if the defer statement is blocking or not. +func (d *deferStmt) IsBlocking(info *Info) bool { + // If the object or the literal is set then we can look up the blocking, + // otherwise assume blocking because otherwise the defer wouldn't + // have been recorded. + if d.obj != nil { + return info.IsBlocking(typeparams.Instance{Object: d.obj, TArgs: d.typeArgs}) + } + if d.lit != nil { + return info.FuncLitInfo(d.lit, d.typeArgs).IsBlocking() + } + return true +} + +func isAnyDeferBlocking(deferStmts []*deferStmt, info *Info) bool { + for _, def := range deferStmts { + if def.IsBlocking(info) { + return true + } + } + return false +} diff --git a/compiler/analysis/escape.go b/compiler/internal/analysis/escape.go similarity index 100% rename from compiler/analysis/escape.go rename to compiler/internal/analysis/escape.go diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go new file mode 100644 index 000000000..3350834df --- /dev/null +++ b/compiler/internal/analysis/info.go @@ -0,0 +1,738 @@ +package analysis + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "strings" + + "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/typesutil" +) + +type continueStmt struct { + forStmt *ast.ForStmt + analyzeStack astPath +} + +func newContinueStmt(forStmt *ast.ForStmt, stack astPath) continueStmt { + cs := continueStmt{ + forStmt: forStmt, + analyzeStack: stack.copy(), + } + return cs +} + +// astPath is a list of AST nodes where each previous node is a parent of the +// next node. +type astPath []ast.Node + +func (src astPath) copy() astPath { + dst := make(astPath, len(src)) + copy(dst, src) + return dst +} + +func (ap astPath) String() string { + s := &strings.Builder{} + s.WriteString("[") + for i, n := range ap { + if i > 0 { + s.WriteString(", ") + } + fmt.Fprintf(s, "%T(%p)", n, n) + } + s.WriteString("]") + return s.String() +} + +type Info struct { + *types.Info + Pkg *types.Package + typeCtx *types.Context + InstanceSets *typeparams.PackageInstanceSets + HasPointer map[*types.Var]bool + funcInstInfos *typeparams.InstanceMap[*FuncInfo] + funcLitInfos map[*ast.FuncLit][]*FuncInfo + InitFuncInfo *FuncInfo // Context for package variable initialization. + + infoImporter InfoImporter // To get `Info` for other packages. + allInfos []*FuncInfo +} + +// InfoImporter is used to get the `Info` for another package. +// The path is the resolved import path of the package to get the `Info` for. +type InfoImporter func(path string) (*Info, error) + +func (info *Info) newFuncInfo(n ast.Node, obj types.Object, typeArgs typesutil.TypeList, resolver *typeparams.Resolver) *FuncInfo { + funcInfo := &FuncInfo{ + pkgInfo: info, + Flattened: make(map[ast.Node]bool), + Blocking: make(map[ast.Node]bool), + GotoLabel: make(map[*types.Label]bool), + loopReturnIndex: -1, + instCallees: new(typeparams.InstanceMap[[]astPath]), + literalFuncCallees: make(map[*ast.FuncLit]astPath), + typeArgs: typeArgs, + resolver: resolver, + } + + // Register the function in the appropriate map. + switch n := n.(type) { + case *ast.FuncDecl: + if n.Body == nil { + // Function body comes from elsewhere (for example, from a go:linkname + // directive), conservatively assume that it may be blocking. + // TODO(nevkontakte): It is possible to improve accuracy of this detection. + // Since GopherJS supports only "import-style" go:linkname, at this stage + // the compiler already determined whether the implementation function is + // blocking, and we could check that. + funcInfo.Blocking[n] = true + } + + if obj == nil { + obj = info.Defs[n.Name] + } + inst := typeparams.Instance{Object: obj, TArgs: typeArgs} + info.funcInstInfos.Set(inst, funcInfo) + + case *ast.FuncLit: + info.funcLitInfos[n] = append(info.funcLitInfos[n], funcInfo) + } + + // And add it to the list of all functions. + info.allInfos = append(info.allInfos, funcInfo) + + return funcInfo +} + +func (info *Info) newFuncInfoInstances(fd *ast.FuncDecl) []*FuncInfo { + obj := info.Defs[fd.Name] + instances := info.InstanceSets.Pkg(info.Pkg).ForObj(obj) + if len(instances) == 0 { + if typeparams.HasTypeParams(obj.Type()) { + // This is a generic function, but no instances were found, + // this is an unused function, so skip over it. + return []*FuncInfo{} + } + + // No instances found and this is a non-generic function. + return []*FuncInfo{info.newFuncInfo(fd, nil, nil, nil)} + } + + funcInfos := make([]*FuncInfo, 0, len(instances)) + for _, inst := range instances { + resolver := typeparams.NewResolver(info.typeCtx, inst) + fi := info.newFuncInfo(fd, inst.Object, inst.TArgs, resolver) + funcInfos = append(funcInfos, fi) + } + return funcInfos +} + +// IsBlocking returns true if the function may contain blocking calls or operations. +// If inst is from a different package, this will use the getImportInfo function +// to lookup the information from the other package. +func (info *Info) IsBlocking(inst typeparams.Instance) bool { + if inst.Object.Pkg() != info.Pkg { + path := inst.Object.Pkg().Path() + otherInfo, err := info.infoImporter(path) + if err != nil { + panic(fmt.Errorf(`failed to get info for package %q: %v`, path, err)) + } + return otherInfo.IsBlocking(inst) + } + if funInfo := info.FuncInfo(inst); funInfo != nil { + return funInfo.IsBlocking() + } + panic(fmt.Errorf(`info did not have function declaration instance for %q`, inst.TypeString())) +} + +// FuncInfo returns information about the given function declaration instance, or nil if not found. +func (info *Info) FuncInfo(inst typeparams.Instance) *FuncInfo { + return info.funcInstInfos.Get(inst) +} + +// FuncLitInfo returns information about the given function literal, or nil if not found. +// The given type arguments are used to identify the correct instance of the +// function literal in the case the literal was defined inside a generic function. +func (info *Info) FuncLitInfo(fun *ast.FuncLit, typeArgs typesutil.TypeList) *FuncInfo { + lits := info.funcLitInfos[fun] + for _, fi := range lits { + if fi.typeArgs.Equal(typeArgs) { + return fi + } + } + return nil +} + +// VarsWithInitializers returns a set of package-level variables that have +// explicit initializers. +func (info *Info) VarsWithInitializers() map[*types.Var]bool { + result := map[*types.Var]bool{} + for _, init := range info.InitOrder { + for _, o := range init.Lhs { + result[o] = true + } + } + return result +} + +// AnalyzePkg analyzes the given package for blocking calls, defers, etc. +// +// Note that at the end of this call the analysis information +// has NOT been propagated across packages yet. Once all the packages +// have been analyzed, call PropagateAnalysis to propagate the information. +func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info, typeCtx *types.Context, typesPkg *types.Package, instanceSets *typeparams.PackageInstanceSets, infoImporter InfoImporter) *Info { + info := &Info{ + Info: typesInfo, + Pkg: typesPkg, + typeCtx: typeCtx, + InstanceSets: instanceSets, + HasPointer: make(map[*types.Var]bool), + infoImporter: infoImporter, + funcInstInfos: new(typeparams.InstanceMap[*FuncInfo]), + funcLitInfos: make(map[*ast.FuncLit][]*FuncInfo), + } + info.InitFuncInfo = info.newFuncInfo(nil, nil, nil, nil) + + // Traverse the full AST of the package and collect information about existing + // functions. + for _, file := range files { + ast.Walk(info.InitFuncInfo, file) + } + + return info +} + +// PropagateAnalysis will propagate analysis information across package +// boundaries to finish the analysis of a whole project. +func PropagateAnalysis(allInfo []*Info) { + done := false + for !done { + done = true + for _, info := range allInfo { + if !info.propagateFunctionBlocking() { + done = false + } + } + } + + for _, info := range allInfo { + info.propagateControlStatementBlocking() + } +} + +// propagateFunctionBlocking propagates information about blocking calls +// to the caller functions. Returns true if done, false if more iterations +// are needed. +// +// For each function we check all other functions it may call and if any of +// them are blocking, we mark the caller blocking as well. The process is +// repeated until no new blocking functions is detected. +func (info *Info) propagateFunctionBlocking() bool { + done := true + for _, caller := range info.allInfos { + // Check calls to named functions and function-typed variables. + caller.instCallees.Iterate(func(callee typeparams.Instance, callSites []astPath) { + if info.IsBlocking(callee) { + for _, callSite := range callSites { + caller.markBlocking(callSite) + } + caller.instCallees.Delete(callee) + done = false + } + }) + + // Check direct calls to function literals. + for callee, callSite := range caller.literalFuncCallees { + if info.FuncLitInfo(callee, caller.typeArgs).IsBlocking() { + caller.markBlocking(callSite) + delete(caller.literalFuncCallees, callee) + done = false + } + } + } + return done +} + +// propagateControlStatementBlocking is called after all function blocking +// information was propagated, mark flow control statements as blocking +// whenever they may lead to a blocking function call. +func (info *Info) propagateControlStatementBlocking() { + for _, funcInfo := range info.allInfos { + funcInfo.propagateReturnBlocking() + funcInfo.propagateContinueBlocking() + } +} + +type FuncInfo struct { + // HasDefer indicates if any defer statement exists in the function. + HasDefer bool + // Nodes are "flattened" into a switch-case statement when we need to be able + // to jump into an arbitrary position in the code with a GOTO statement, or + // resume a goroutine after a blocking call unblocks. + Flattened map[ast.Node]bool + // Blocking indicates that either the AST node itself or its descendant may + // block goroutine execution (for example, a channel operation). + Blocking map[ast.Node]bool + // GotoLabel indicates a label referenced by a goto statement, rather than a + // named loop. + GotoLabel map[*types.Label]bool + // List of continue statements in the function. + continueStmts []continueStmt + // List of return statements in the function. + returnStmts []returnStmt + // List of deferred function calls which could be blocking. + // This is built up as the function is analyzed so that we can mark all + // return statements with the defers that each return would need to call. + deferStmts []*deferStmt + // The index of the return statement that was analyzed prior to a top-level + // loop starting. This is used to determine which return statements + // were added within the loop so that they can be updated to reflect all + // the defers that were added anywhere inside the loop. This is because + // returns defined before any defers in a loop may still be affected by + // those defers because of the loop. See comment on [deferStmt]. + loopReturnIndex int + // List of other named functions in the current package or another package + // that this function calls. + // If any of them are blocking, this function will become blocking too. + instCallees *typeparams.InstanceMap[[]astPath] + // List of function literals directly called from this function (for example: + // `func() { /* do stuff */ }()`). This is distinct from function literals + // assigned to named variables (for example: `doStuff := func() {}; + // doStuff()`), which are handled by localInstCallees. If any of them are + // identified as blocking, this function will become blocking too. + literalFuncCallees map[*ast.FuncLit]astPath + // typeArgs are the type arguments for the function instance. + typeArgs typesutil.TypeList + // resolver is used by this function instance to resolve any type arguments + // for internal function calls. + // This may be nil if not an instance of a generic function. + resolver *typeparams.Resolver + + pkgInfo *Info // Function's parent package. + visitorStack astPath +} + +// IsBlocking indicates if this function may block goroutine execution. +// +// For example, a channel operation in a function or a call to another +// possibly blocking function may block the function. +func (fi *FuncInfo) IsBlocking() bool { + return fi == nil || len(fi.Blocking) != 0 +} + +// TypeArgs gets the type arguments of this inside of a function instance +// or empty if not in a function instance. +func (fi *FuncInfo) TypeArgs() typesutil.TypeList { + return fi.typeArgs +} + +// propagateReturnBlocking updates the blocking on the return statements. +// See comment on [deferStmt]. +// +// This should only be called once when finishing analysis and only after +// all functions have been analyzed and all blocking information has been +// propagated across functions. +func (fi *FuncInfo) propagateReturnBlocking() { + if len(fi.GotoLabel) > 0 { + // If there are any goto statements in the function then + // all the return statements are marked the same. + // If any defer is blocking, then all return statements are blocking. + if isAnyDeferBlocking(fi.deferStmts, fi.pkgInfo) { + for _, returnStmt := range fi.returnStmts { + fi.markBlocking(returnStmt.analyzeStack) + } + } + return + } + + for _, returnStmt := range fi.returnStmts { + // Check all the defer statements that affect the return statement, + // if any are blocking then the return statement is blocking. + if returnStmt.IsBlocking(fi) { + fi.markBlocking(returnStmt.analyzeStack) + } + } +} + +// propagateContinueBlocking updates the blocking on the continue statements. +// +// This should only be called once when finishing analysis and only after +// all functions have been analyzed and all blocking information has been +// propagated across functions. +func (fi *FuncInfo) propagateContinueBlocking() { + for _, continueStmt := range fi.continueStmts { + if fi.Blocking[continueStmt.forStmt.Post] { + // If a for-loop post-expression is blocking, the continue statement + // that leads to it must be treated as blocking. + fi.markBlocking(continueStmt.analyzeStack) + } + } +} + +func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { + if node == nil { + if len(fi.visitorStack) != 0 { + fi.visitorStack = fi.visitorStack[:len(fi.visitorStack)-1] + } + return nil + } + fi.visitorStack = append(fi.visitorStack, node) + + switch n := node.(type) { + case *ast.FuncDecl: + // Analyze all the instances of the function declarations + // in their own context with their own type arguments. + fis := fi.pkgInfo.newFuncInfoInstances(n) + if n.Body != nil { + for _, fi := range fis { + ast.Walk(fi, n.Body) + } + } + return nil + case *ast.FuncLit: + // Analyze the function literal in its own context. + return fi.pkgInfo.newFuncInfo(n, nil, fi.typeArgs, fi.resolver) + case *ast.BranchStmt: + switch n.Tok { + case token.GOTO: + // Emulating GOTO in JavaScript requires the code to be flattened into a + // switch-statement. + fi.markFlattened(fi.visitorStack) + fi.GotoLabel[fi.pkgInfo.Uses[n.Label].(*types.Label)] = true + case token.CONTINUE: + loopStmt := astutil.FindLoopStmt(fi.visitorStack, n, fi.pkgInfo.Info) + if forStmt, ok := (loopStmt).(*ast.ForStmt); ok { + // In `for x; y; z { ... }` loops `z` may be potentially blocking + // and therefore continue expression that triggers it would have to + // be treated as blocking. + fi.continueStmts = append(fi.continueStmts, newContinueStmt(forStmt, fi.visitorStack)) + } + } + return fi + case *ast.CallExpr: + return fi.visitCallExpr(n, false) + case *ast.SendStmt: + // Sending into a channel is blocking. + fi.markBlocking(fi.visitorStack) + return fi + case *ast.UnaryExpr: + switch n.Op { + case token.AND: + if id, ok := astutil.RemoveParens(n.X).(*ast.Ident); ok { + fi.pkgInfo.HasPointer[fi.pkgInfo.Uses[id].(*types.Var)] = true + } + case token.ARROW: + // Receiving from a channel is blocking. + fi.markBlocking(fi.visitorStack) + } + return fi + case *ast.RangeStmt: + if _, ok := fi.pkgInfo.TypeOf(n.X).Underlying().(*types.Chan); ok { + // for-range loop over a channel is blocking. + fi.markBlocking(fi.visitorStack) + } + if fi.loopReturnIndex >= 0 { + // Already in a loop so just continue walking. + return fi + } + // Top-level for-loop, analyze it separately to be able to update + // returns with the defers that were added inside the loop. + // See comment on deferStmt. + fi.loopReturnIndex = len(fi.returnStmts) + // Analyze the for-loop's children. + ast.Walk(skipParentNode{then: fi}, n) + // After the for-loop is analyzed, update all return statements that + // were inside the loop with the resulting list of defer statements. + for i := fi.loopReturnIndex; i < len(fi.returnStmts); i++ { + fi.returnStmts[i].deferStmts = fi.deferStmts + } + fi.loopReturnIndex = -1 + return nil + case *ast.ForStmt: + if fi.loopReturnIndex >= 0 { + // Already in a loop so just continue walking. + return fi + } + // Top-level for-loop, analyze it separately to be able to update + // returns with the defers that were added inside the loop. + // See comment on deferStmt. + fi.loopReturnIndex = len(fi.returnStmts) + // Analyze the for-loop's children. + ast.Walk(skipParentNode{then: fi}, n) + // After the for-loop is analyzed, update all return statements that + // were inside the loop with the resulting list of defer statements. + for i := fi.loopReturnIndex; i < len(fi.returnStmts); i++ { + fi.returnStmts[i].deferStmts = fi.deferStmts + } + fi.loopReturnIndex = -1 + return nil + case *ast.SelectStmt: + for _, s := range n.Body.List { + if s.(*ast.CommClause).Comm == nil { // default clause + return fi + } + } + // Select statements without a default case are blocking. + fi.markBlocking(fi.visitorStack) + return fi + case *ast.CommClause: + // FIXME(nevkontakte): Does this need to be manually spelled out? Presumably + // ast.Walk would visit all those nodes anyway, and we are not creating any + // new contexts here. + // https://github.com/gopherjs/gopherjs/issues/230 seems to be relevant? + switch comm := n.Comm.(type) { + case *ast.SendStmt: + ast.Walk(fi, comm.Chan) + ast.Walk(fi, comm.Value) + case *ast.ExprStmt: + ast.Walk(fi, comm.X.(*ast.UnaryExpr).X) + case *ast.AssignStmt: + ast.Walk(fi, comm.Rhs[0].(*ast.UnaryExpr).X) + } + for _, s := range n.Body { + ast.Walk(fi, s) + } + return nil // The subtree was manually checked, no need to visit it again. + case *ast.GoStmt: + // Unlike a regular call, the function in a go statement doesn't block the + // caller goroutine, but the expression that determines the function and its + // arguments still need to be checked. + ast.Walk(fi, n.Call.Fun) + for _, arg := range n.Call.Args { + ast.Walk(fi, arg) + } + return nil // The subtree was manually checked, no need to visit it again. + case *ast.DeferStmt: + fi.HasDefer = true + return fi.visitCallExpr(n.Call, true) + case *ast.ReturnStmt: + // Capture all return statements in the function. They could become blocking + // if the function has a blocking deferred call. + rs := newReturnStmt(fi.visitorStack, fi.deferStmts) + fi.returnStmts = append(fi.returnStmts, rs) + return fi + default: + return fi + } + // Deliberately no return here to make sure that each of the cases above is + // self-sufficient and explicitly decides in which context the its AST subtree + // needs to be analyzed. +} + +func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr, deferredCall bool) ast.Visitor { + switch f := astutil.RemoveParens(n.Fun).(type) { + case *ast.Ident: + fi.callToNamedFunc(fi.instanceForIdent(f), deferredCall) + return fi + case *ast.SelectorExpr: + if sel := fi.pkgInfo.Selections[f]; sel != nil { + if typesutil.IsJsObject(sel.Recv()) { + // js.Object methods are known to be non-blocking, + // but we still must check its arguments. + // We don't need to add a deferStmt when `deferredCall` + // is true, since that defer will always be non-blocking. + return fi + } + // selection is a method call like `foo.Bar()`, where `foo` might + // be generic and needs to be substituted with the type argument. + fi.callToNamedFunc(fi.instanceForSelection(sel), deferredCall) + return fi + } + + fi.callToNamedFunc(fi.instanceForIdent(f.Sel), deferredCall) + return fi + case *ast.FuncLit: + // Collect info about the function literal itself. + ast.Walk(fi, n.Fun) + + // Check all argument expressions. + for _, arg := range n.Args { + ast.Walk(fi, arg) + } + // Register literal function call site in case it is identified as blocking. + fi.literalFuncCallees[f] = fi.visitorStack.copy() + if deferredCall { + fi.deferStmts = append(fi.deferStmts, newLitDefer(f, fi.typeArgs)) + } + return nil // No need to walk under this CallExpr, we already did it manually. + case *ast.IndexExpr: + // Collect info about the instantiated type or function, or index expression. + if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { + // This is a type conversion to an instance of a generic type, + // not a call. Type assertion itself is not blocking, but we will + // visit the input expression. + return fi + } + if astutil.IsTypeExpr(f.Index, fi.pkgInfo.Info) { + // This is a call of an instantiation of a generic function, + // e.g. `foo[int]` in `func foo[T any]() { ... }; func main() { foo[int]() }` + var inst typeparams.Instance + switch fxt := f.X.(type) { + case *ast.Ident: + inst = fi.instanceForIdent(fxt) + case *ast.SelectorExpr: + if sel := fi.pkgInfo.Selections[fxt]; sel != nil { + inst = fi.instanceForSelection(sel) + } else { + // For qualified identifiers like `pkg.Foo` + inst = fi.instanceForIdent(fxt.Sel) + } + default: + panic(fmt.Errorf(`unexpected type %T for index expression %s`, f.X, f.X)) + } + fi.callToNamedFunc(inst, deferredCall) + return fi + } + // The called function is gotten with an index or key from a map, array, or slice. + // e.g. `m := map[string]func(){}; m["key"]()`, `s := []func(); s[0]()`. + // Since we can't predict if the returned function will be blocking + // or not, we have to be conservative and assume that function might be blocking. + fi.markBlocking(fi.visitorStack) + if deferredCall { + fi.deferStmts = append(fi.deferStmts, newBlockingDefer()) + } + return fi + case *ast.IndexListExpr: + // Collect info about the instantiated type or function. + if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { + // This is a type conversion to an instance of a generic type, + // not a call. Type assertion itself is not blocking, but we will + // visit the input expression. + return fi + } + // This is a call of an instantiation of a generic function, + // e.g. `foo[int, bool]` in `func foo[T1, T2 any]() { ... }; func main() { foo[int, bool]() }` + var inst typeparams.Instance + switch fxt := f.X.(type) { + case *ast.Ident: + inst = fi.instanceForIdent(fxt) + case *ast.SelectorExpr: + if sel := fi.pkgInfo.Selections[fxt]; sel != nil { + inst = fi.instanceForSelection(sel) + } else { + // For qualified identifiers like `pkg.Foo` + inst = fi.instanceForIdent(fxt.Sel) + } + default: + panic(fmt.Errorf(`unexpected type %T for index list expression %s`, f.X, f.X)) + } + fi.callToNamedFunc(inst, deferredCall) + return fi + default: + if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { + // This is a type conversion, not a call. Type assertion itself is not + // blocking, but we will visit the input expression. + return fi + } + // The function is returned by a non-trivial expression. We have to be + // conservative and assume that function might be blocking. + fi.markBlocking(fi.visitorStack) + if deferredCall { + fi.deferStmts = append(fi.deferStmts, newBlockingDefer()) + } + return fi + } +} + +func (fi *FuncInfo) instanceForIdent(fnId *ast.Ident) typeparams.Instance { + tArgs := fi.pkgInfo.Info.Instances[fnId].TypeArgs + return typeparams.Instance{ + Object: fi.pkgInfo.Uses[fnId], + TArgs: fi.resolver.SubstituteAll(tArgs), + } +} + +func (fi *FuncInfo) instanceForSelection(sel *types.Selection) typeparams.Instance { + if _, ok := sel.Obj().Type().(*types.Signature); ok { + // Substitute the selection to ensure that the receiver has the correct + // type arguments propagated down from the caller. + resolved := fi.resolver.SubstituteSelection(sel) + sig := resolved.Obj().Type().(*types.Signature) + + // Using the substituted receiver type, find the instance of this call. + // This does require looking up the original method in the receiver type + // that may or may not have been the receiver prior to the substitution. + if recv := sig.Recv(); recv != nil { + typ := recv.Type() + if ptrType, ok := typ.(*types.Pointer); ok { + typ = ptrType.Elem() + } + + if rt, ok := typ.(*types.Named); ok { + origMethod, _, _ := types.LookupFieldOrMethod(rt.Origin(), true, rt.Obj().Pkg(), resolved.Obj().Name()) + if origMethod == nil { + panic(fmt.Errorf(`failed to lookup field %q in type %v`, resolved.Obj().Name(), rt.Origin())) + } + return typeparams.Instance{ + Object: origMethod, + TArgs: fi.resolver.SubstituteAll(rt.TypeArgs()), + } + } + } + } + return typeparams.Instance{Object: sel.Obj()} +} + +func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance, deferredCall bool) { + switch o := callee.Object.(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. + fi.markBlocking(fi.visitorStack) + if deferredCall { + fi.deferStmts = append(fi.deferStmts, newBlockingDefer()) + } + return + } + } + + // We probably don't know yet whether the callee function is blocking. + // Record the calls site for the later stage. + paths := fi.instCallees.Get(callee) + paths = append(paths, fi.visitorStack.copy()) + fi.instCallees.Set(callee, paths) + if deferredCall { + fi.deferStmts = append(fi.deferStmts, newInstDefer(callee)) + } + case *types.Var: + // Conservatively assume that a function in a variable might be blocking. + fi.markBlocking(fi.visitorStack) + if deferredCall { + fi.deferStmts = append(fi.deferStmts, newBlockingDefer()) + } + default: + // No need to add defers for other call types, such as *types.Builtin, + // since those are considered non-blocking. + return + } +} + +func (fi *FuncInfo) markBlocking(stack astPath) { + for _, n := range stack { + fi.Blocking[n] = true + fi.Flattened[n] = true + } +} + +func (fi *FuncInfo) markFlattened(stack astPath) { + for _, n := range stack { + fi.Flattened[n] = true + } +} + +// skipParentNode is a visitor that skips the next node in the AST +// but will continue visiting the rest of the tree including the +// children of the skipped node. +type skipParentNode struct { + then ast.Visitor +} + +func (v skipParentNode) Visit(node ast.Node) ast.Visitor { + return v.then +} diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go new file mode 100644 index 000000000..4c18d05d4 --- /dev/null +++ b/compiler/internal/analysis/info_test.go @@ -0,0 +1,1896 @@ +package analysis + +import ( + "fmt" + "go/ast" + "go/types" + "sort" + "testing" + + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/internal/srctesting" +) + +func TestBlocking_Simple(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func notBlocking() { + println("hi") + }`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_Recursive(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func notBlocking(i int) { + if i > 0 { + println(i) + notBlocking(i - 1) + } + }`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_AlternatingRecursive(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func near(i int) { + if i > 0 { + println(i) + far(i) + } + } + + func far(i int) { + near(i - 1) + }`) + bt.assertNotBlocking(`near`) + bt.assertNotBlocking(`far`) +} + +func TestBlocking_Channels(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func readFromChannel(c chan bool) { + <-c + } + + func readFromChannelAssign(c chan bool) { + v := <-c + println(v) + } + + func readFromChannelAsArg(c chan bool) { + println(<-c) + } + + func sendToChannel(c chan bool) { + c <- true + } + + func rangeOnChannel(c chan bool) { + for v := range c { + println(v) + } + } + + func rangeOnSlice(c []bool) { + for v := range c { + println(v) + } + }`) + bt.assertBlocking(`readFromChannel`) + bt.assertBlocking(`sendToChannel`) + bt.assertBlocking(`rangeOnChannel`) + bt.assertBlocking(`readFromChannelAssign`) + bt.assertBlocking(`readFromChannelAsArg`) + bt.assertNotBlocking(`rangeOnSlice`) +} + +func TestBlocking_Selects(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func selectReadWithoutDefault(a, b chan bool) { + select { + case <-a: + println("a") + case v := <-b: + println("b", v) + } + } + + func selectReadWithDefault(a, b chan bool) { + select { + case <-a: + println("a") + case v := <-b: + println("b", v) + default: + println("nothing") + } + } + + func selectSendWithoutDefault(a, b chan bool) { + select { + case a <- true: + println("a") + case b <- false: + println("b") + } + } + + func selectSendWithDefault(a, b chan bool) { + select { + case a <- true: + println("a") + case b <- false: + println("b") + default: + println("nothing") + } + }`) + bt.assertBlocking(`selectReadWithoutDefault`) + bt.assertBlocking(`selectSendWithoutDefault`) + bt.assertNotBlocking(`selectReadWithDefault`) + bt.assertNotBlocking(`selectSendWithDefault`) +} + +func TestBlocking_GoRoutines_WithFuncLiterals(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func notBlocking(c chan bool) { + go func(c chan bool) { // line 4 + println(<-c) + }(c) + } + + func blocking(c chan bool) { + go func(v bool) { // line 10 + println(v) + }(<-c) + }`) + bt.assertNotBlocking(`notBlocking`) + bt.assertBlockingLit(4, ``) + + bt.assertBlocking(`blocking`) + bt.assertNotBlockingLit(10, ``) +} + +func TestBlocking_GoRoutines_WithNamedFuncs(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingRoutine(c chan bool) { + println(<-c) + } + + func nonBlockingRoutine(v bool) { + println(v) + } + + func notBlocking(c chan bool) { + go blockingRoutine(c) + } + + func blocking(c chan bool) { + go nonBlockingRoutine(<-c) + }`) + bt.assertBlocking(`blockingRoutine`) + bt.assertNotBlocking(`nonBlockingRoutine`) + + bt.assertNotBlocking(`notBlocking`) + bt.assertBlocking(`blocking`) +} + +func TestBlocking_Defers_WithoutReturns_WithFuncLiterals(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingBody(c chan bool) { + defer func(c chan bool) { // line 4 + println(<-c) + }(c) + } + + func blockingArg(c chan bool) { + defer func(v bool) { // line 10 + println(v) + }(<-c) + } + + func notBlocking(c chan bool) { + defer func(v bool) { // line 16 + println(v) + }(true) + }`) + bt.assertBlocking(`blockingBody`) + bt.assertBlockingLit(4, ``) + + bt.assertBlocking(`blockingArg`) + bt.assertNotBlockingLit(10, ``) + + bt.assertNotBlocking(`notBlocking`) + bt.assertNotBlockingLit(16, ``) +} + +func TestBlocking_Defers_WithoutReturns_WithNamedFuncs(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingPrint(c chan bool) { + println(<-c) + } + + func nonBlockingPrint(v bool) { + println(v) + } + + func blockingBody(c chan bool) { + defer blockingPrint(c) + } + + func blockingArg(c chan bool) { + defer nonBlockingPrint(<-c) + } + + func notBlocking(c chan bool) { + defer nonBlockingPrint(true) + }`) + bt.assertFuncInstCount(5) + bt.assertFuncLitCount(0) + + bt.assertBlocking(`blockingPrint`) + bt.assertNotBlocking(`nonBlockingPrint`) + + bt.assertBlocking(`blockingBody`) + bt.assertBlocking(`blockingArg`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_Defers_WithReturns_WithFuncLiterals(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingBody(c chan bool) int { + defer func(c chan bool) { // line 4 + println(<-c) + }(c) + return 42 + } + + func blockingArg(c chan bool) int { + defer func(v bool) { // line 11 + println(v) + }(<-c) + return 42 + } + + func notBlocking(c chan bool) int { + defer func(v bool) { // line 18 + println(v) + }(true) + return 42 + }`) + bt.assertBlocking(`blockingBody`) + bt.assertBlockingLit(4, ``) + + bt.assertBlocking(`blockingArg`) + bt.assertNotBlockingLit(11, ``) + + bt.assertNotBlocking(`notBlocking`) + bt.assertNotBlockingLit(18, ``) +} + +func TestBlocking_Defers_WithReturns_WithNamedFuncs(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingPrint(c chan bool) { + println(<-c) + } + + func nonBlockingPrint(v bool) { + println(v) + } + + func blockingBody(c chan bool) int { + defer blockingPrint(c) + return 42 // line 13 + } + + func blockingArg(c chan bool) int { + defer nonBlockingPrint(<-c) + return 42 // line 18 + } + + func notBlocking(c chan bool) int { + defer nonBlockingPrint(true) + return 42 // line 23 + }`) + bt.assertBlocking(`blockingPrint`) + bt.assertNotBlocking(`nonBlockingPrint`) + + bt.assertBlocking(`blockingBody`) + bt.assertBlockingReturn(13, ``) + + bt.assertBlocking(`blockingArg`) + // The defer is non-blocking so the return is not blocking + // even though the function is blocking. + bt.assertNotBlockingReturn(18, ``) + + bt.assertNotBlocking(`notBlocking`) + bt.assertNotBlockingReturn(23, ``) +} + +func TestBlocking_Defers_WithMultipleReturns(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func foo(c chan int) bool { + defer func() { // line 4 + if r := recover(); r != nil { + println("Error", r) + } + }() + + if c == nil { + return false // line 11 + } + + defer func(v int) { // line 14 + println(v) + }(<-c) + + value := <-c + if value < 0 { + return false // line 20 + } + + if value > 0 { + defer func() { // line 24 + println(<-c) + }() + + return false // line 28 + } + + return true // line 31 + }`) + bt.assertBlocking(`foo`) + bt.assertNotBlockingLit(4, ``) + // Early escape from function without blocking defers is not blocking. + bt.assertNotBlockingReturn(11, ``) + bt.assertNotBlockingLit(14, ``) + // Function has had blocking by this point but no blocking defers yet. + bt.assertNotBlockingReturn(20, ``) + bt.assertBlockingLit(24, ``) + // The return is blocking because of a blocking defer. + bt.assertBlockingReturn(28, ``) + // Technically the return on line 31 is not blocking since the defer that + // is blocking can only exit through the return on line 28, but it would be + // difficult to determine which defers would only affect certain returns + // without doing full control flow analysis. + // + // TODO(grantnelson-wf): We could fix this at some point by keeping track + // of which flow control statements (e.g. if-statements) are terminating + // or not. Any defers added in a terminating control flow would not + // propagate to returns that are not in that block. + // See golang.org/x/tools/go/ssa for flow control analysis. + // + // For now we simply build up the list of defers as we go making + // the return on line 31 also blocking. + bt.assertBlockingReturn(31, ``) +} + +func TestBlocking_Defers_WithReturnsAndDefaultBlocking(t *testing.T) { + bt := newBlockingTest(t, + `package test + + type foo struct {} + func (f foo) Bar() { + println("foo") + } + + type stringer interface { + Bar() + } + + var fb = foo{}.Bar + + func deferInterfaceCall() bool { + var s stringer = foo{} + defer s.Bar() + return true // line 17 + } + + func deferVarCall() bool { + defer fb() + return true // line 22 + } + + func deferLocalVarCall() bool { + fp := foo{}.Bar + defer fp() + return true // line 28 + } + + func deferMethodExpressionCall() bool { + fp := foo.Bar + defer fp(foo{}) + return true // line 34 + } + + func deferSlicedFuncCall() bool { + s := []func() { fb, foo{}.Bar } + defer s[0]() + return true // line 40 + } + + func deferMappedFuncCall() bool { + m := map[string]func() { + "fb": fb, + "fNew": foo{}.Bar, + } + defer m["fb"]() + return true // line 49 + }`) + + bt.assertFuncInstCount(7) + bt.assertNotBlocking(`foo.Bar`) + + // None of these are actually blocking but we treat them like they are + // because the defers invoke functions via interfaces and function pointers. + bt.assertBlocking(`deferInterfaceCall`) + bt.assertBlocking(`deferVarCall`) + bt.assertBlocking(`deferLocalVarCall`) + bt.assertBlocking(`deferMethodExpressionCall`) + bt.assertBlocking(`deferSlicedFuncCall`) + bt.assertBlocking(`deferMappedFuncCall`) + + // All of these returns are blocking because they have blocking defers. + bt.assertBlockingReturn(17, ``) + bt.assertBlockingReturn(22, ``) + bt.assertBlockingReturn(28, ``) + bt.assertBlockingReturn(34, ``) + bt.assertBlockingReturn(40, ``) + bt.assertBlockingReturn(49, ``) +} + +func TestBlocking_Defers_WithReturnsAndDeferBuiltin(t *testing.T) { + bt := newBlockingTest(t, + `package test + + type strSet map[string]bool + + func deferBuiltinCall() strSet { + m := strSet{ + "foo": true, + } + defer delete(m, "foo") + return m // line 10 + }`) + + bt.assertFuncInstCount(1) + bt.assertNotBlocking(`deferBuiltinCall`) + bt.assertNotBlockingReturn(10, ``) +} + +func TestBlocking_Defers_WithReturnsInLoops(t *testing.T) { + // These are example of where a defer can affect the return that + // occurs prior to the defer in the function body. + bt := newBlockingTest(t, + `package test + + func blocking(c chan int) { + println(<-c) + } + + func deferInForLoop(c chan int) bool { + i := 1000 + for { + i-- + if i <= 0 { + return true // line 12 + } + defer blocking(c) + } + } + + func deferInForLoopReturnAfter(c chan int) bool { + for i := 1000; i > 0; i-- { + defer blocking(c) + } + return true // line 22 + } + + func deferInNamedForLoop(c chan int) bool { + i := 1000 + Start: + for { + i-- + if i <= 0 { + return true // line 31 + } + defer blocking(c) + continue Start + } + } + + func deferInNamedForLoopReturnAfter(c chan int) bool { + Start: + for i := 1000; i > 0; i-- { + defer blocking(c) + continue Start + } + return true // line 44 + } + + func deferInGotoLoop(c chan int) bool { + i := 1000 + Start: + i-- + if i <= 0 { + return true // line 52 + } + defer blocking(c) + goto Start + } + + func deferInGotoLoopReturnAfter(c chan int) bool { + i := 1000 + Start: + defer blocking(c) + i-- + if i > 0 { + goto Start + } + return true // line 66 + } + + func deferInRangeLoop(c chan int) bool { + s := []int{1, 2, 3} + for i := range s { + if i > 3 { + return true // line 73 + } + defer blocking(c) + } + return false // line 77 + }`) + + bt.assertFuncInstCount(8) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`deferInForLoop`) + bt.assertBlocking(`deferInForLoopReturnAfter`) + bt.assertBlocking(`deferInNamedForLoop`) + bt.assertBlocking(`deferInNamedForLoopReturnAfter`) + bt.assertBlocking(`deferInGotoLoop`) + bt.assertBlocking(`deferInGotoLoopReturnAfter`) + bt.assertBlocking(`deferInRangeLoop`) + // When the following 2 returns are defined there are no defers, however, + // because of the loop, the blocking defers defined after the return will + // block the returns. + bt.assertBlockingReturn(12, ``) + bt.assertBlockingReturn(22, ``) + bt.assertBlockingReturn(31, ``) + bt.assertBlockingReturn(44, ``) + bt.assertBlockingReturn(52, ``) + bt.assertBlockingReturn(66, ``) + bt.assertBlockingReturn(73, ``) + bt.assertBlockingReturn(77, ``) +} + +func TestBlocking_Defers_WithReturnsInLoopsInLoops(t *testing.T) { + // These are example of where a defer can affect the return that + // occurs prior to the defer in the function body. + bt := newBlockingTest(t, + `package test + + func blocking(c chan int) { + println(<-c) + } + + func forLoopTheLoop(c chan int) bool { + if c == nil { + return false // line 9 + } + for i := 0; i < 10; i++ { + if i > 3 { + return true // line 13 + } + for j := 0; j < 10; j++ { + if j > 3 { + return true // line 17 + } + defer blocking(c) + if j > 2 { + return false // line 21 + } + } + if i > 2 { + return false // line 25 + } + } + return false // line 28 + } + + func rangeLoopTheLoop(c chan int) bool { + data := []int{1, 2, 3} + for i := range data { + for j := range data { + if i + j > 3 { + return true // line 36 + } + } + defer blocking(c) + } + return false // line 41 + } + + func noopThenLoop(c chan int) bool { + data := []int{1, 2, 3} + for i := range data { + if i > 13 { + return true // line 48 + } + defer func() { println("hi") }() + } + for i := range data { + if i > 3 { + return true // line 54 + } + defer blocking(c) + } + return false // line 58 + }`) + + bt.assertFuncInstCount(4) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`forLoopTheLoop`) + bt.assertNotBlockingReturn(9, ``) + bt.assertBlockingReturn(13, ``) + bt.assertBlockingReturn(17, ``) + bt.assertBlockingReturn(21, ``) + bt.assertBlockingReturn(25, ``) + bt.assertBlockingReturn(28, ``) + bt.assertBlocking(`rangeLoopTheLoop`) + bt.assertBlockingReturn(36, ``) + bt.assertBlockingReturn(41, ``) + bt.assertBlocking(`noopThenLoop`) + bt.assertNotBlockingReturn(48, ``) + bt.assertBlockingReturn(54, ``) + bt.assertBlockingReturn(58, ``) +} + +func TestBlocking_Returns_WithoutDefers(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blocking(c chan bool) bool { + return <-c // line 4 + } + + func blockingBeforeReturn(c chan bool) bool { + v := <-c + return v // line 9 + } + + func indirectlyBlocking(c chan bool) bool { + return blocking(c) // line 13 + } + + func indirectlyBlockingBeforeReturn(c chan bool) bool { + v := blocking(c) + return v // line 18 + } + + func notBlocking(c chan bool) bool { + return true // line 22 + }`) + bt.assertBlocking(`blocking`) + bt.assertBlockingReturn(4, ``) + + bt.assertBlocking(`blockingBeforeReturn`) + bt.assertNotBlockingReturn(9, ``) + + bt.assertBlocking(`indirectlyBlocking`) + bt.assertBlockingReturn(13, ``) + + bt.assertBlocking(`indirectlyBlockingBeforeReturn`) + bt.assertNotBlockingReturn(18, ``) + + bt.assertNotBlocking(`notBlocking`) + bt.assertNotBlockingReturn(22, ``) +} + +func TestBlocking_Defers_WithReturnsInInstances(t *testing.T) { + // This is an example of a deferred function literal inside of + // an instance of a generic function affecting the return + // differently based on the type arguments of the instance. + bt := newBlockingTest(t, + `package test + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + type Foo interface { Baz() } + func FooBaz[T Foo]() bool { + defer func() { // line 17 + var foo T + foo.Baz() + }() + return true // line 21 + } + + func main() { + FooBaz[BazBlocker]() + FooBaz[BazNotBlocker]() + }`) + + bt.assertFuncInstCount(5) + bt.assertBlocking(`BazBlocker.Baz`) + bt.assertNotBlocking(`BazNotBlocker.Baz`) + bt.assertBlockingInst(`pkg/test.FooBaz`) + bt.assertNotBlockingInst(`pkg/test.FooBaz`) + bt.assertBlocking(`main`) + + bt.assertFuncLitCount(2) + bt.assertBlockingLit(17, `pkg/test.BazBlocker`) + bt.assertNotBlockingLit(17, `pkg/test.BazNotBlocker`) + + bt.assertBlockingReturn(21, `pkg/test.BazBlocker`) + bt.assertNotBlockingReturn(21, `pkg/test.BazNotBlocker`) +} + +func TestBlocking_Defers_WithReturnsAndOtherPackages(t *testing.T) { + otherSrc := `package other + + func Blocking() { + c := make(chan int) + println(<-c) + } + + func NotBlocking() { + println("Hello") + }` + + testSrc := `package test + + import "pkg/other" + + func deferOtherBlocking() bool { + defer other.Blocking() + return true // line 7 + } + + func deferOtherNotBlocking() bool { + defer other.NotBlocking() + return true // line 12 + }` + + bt := newBlockingTestWithOtherPackage(t, testSrc, otherSrc) + + bt.assertBlocking(`deferOtherBlocking`) + bt.assertBlockingReturn(7, ``) + + bt.assertNotBlocking(`deferOtherNotBlocking`) + bt.assertNotBlockingReturn(12, ``) +} + +func TestBlocking_FunctionLiteral(t *testing.T) { + // See: https://github.com/gopherjs/gopherjs/issues/955. + bt := newBlockingTest(t, + `package test + + func blocking() { + c := make(chan bool) + <-c + } + + func indirectlyBlocking() { + func() { blocking() }() // line 9 + } + + func directlyBlocking() { + func() { // line 13 + c := make(chan bool) + <-c + }() + } + + func notBlocking() { + func() { println() } () // line 20 + }`) + bt.assertBlocking(`blocking`) + + bt.assertBlocking(`indirectlyBlocking`) + bt.assertBlockingLit(9, ``) + + bt.assertBlocking(`directlyBlocking`) + bt.assertBlockingLit(13, ``) + + bt.assertNotBlocking(`notBlocking`) + bt.assertNotBlockingLit(20, ``) +} + +func TestBlocking_LinkedFunction(t *testing.T) { + bt := newBlockingTest(t, + `package test + + // linked to some other function + func blocking() + + func indirectlyBlocking() { + blocking() + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indirectlyBlocking`) +} + +func TestBlocking_Instances_WithSingleTypeArg(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blocking[T any]() { + c := make(chan T) + <-c + } + + func notBlocking[T any]() { + var v T + println(v) + } + + func bInt() { + blocking[int]() + } + + func nbUint() { + notBlocking[uint]() + }`) + bt.assertFuncInstCount(4) + // blocking and notBlocking as generics do not have FuncInfo, + // only non-generic and instances have FuncInfo. + + bt.assertBlockingInst(`pkg/test.blocking`) + bt.assertBlocking(`bInt`) + bt.assertNotBlockingInst(`pkg/test.notBlocking`) + bt.assertNotBlocking(`nbUint`) +} + +func TestBlocking_Instances_WithMultipleTypeArgs(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blocking[K comparable, V any, M ~map[K]V]() { + c := make(chan M) + <-c + } + + func notBlocking[K comparable, V any, M ~map[K]V]() { + var m M + println(m) + } + + func bInt() { + blocking[string, int, map[string]int]() + } + + func nbUint() { + notBlocking[string, uint, map[string]uint]() + }`) + bt.assertFuncInstCount(4) + // blocking and notBlocking as generics do not have FuncInfo, + // only non-generic and instances have FuncInfo. + + bt.assertBlockingInst(`pkg/test.blocking`) + bt.assertBlocking(`bInt`) + bt.assertNotBlockingInst(`pkg/test.notBlocking`) + bt.assertNotBlocking(`nbUint`) +} + +func TestBlocking_Indexed_FunctionSlice(t *testing.T) { + // This calls notBlocking but since the function pointers + // are in the slice they will both be considered as blocking. + bt := newBlockingTest(t, + `package test + + func blocking() { + c := make(chan int) + <-c + } + + func notBlocking() { + println() + } + + var funcs = []func() { blocking, notBlocking } + + func indexer(i int) { + funcs[i]() + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indexer`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_Indexed_FunctionMap(t *testing.T) { + // This calls notBlocking but since the function pointers + // are in the map they will both be considered as blocking. + bt := newBlockingTest(t, + `package test + + func blocking() { + c := make(chan int) + <-c + } + + func notBlocking() { + println() + } + + var funcs = map[string]func() { + "b": blocking, + "nb": notBlocking, + } + + func indexer(key string) { + funcs[key]() + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indexer`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_Indexed_FunctionArray(t *testing.T) { + // This calls notBlocking but since the function pointers + // are in the array they will both be considered as blocking. + bt := newBlockingTest(t, + `package test + + func blocking() { + c := make(chan int) + <-c + } + + func notBlocking() { + println() + } + + var funcs = [2]func() { blocking, notBlocking } + + func indexer(i int) { + funcs[i]() + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indexer`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_Casting_InterfaceInstanceWithSingleTypeParam(t *testing.T) { + // This checks that casting to an instance type with a single type parameter + // is treated as a cast and not accidentally treated as a function call. + bt := newBlockingTest(t, + `package test + + type Foo[T any] interface { + Baz() T + } + + type Bar struct { + name string + } + + func (b Bar) Baz() string { + return b.name + } + + func caster() Foo[string] { + b := Bar{name: "foo"} + return Foo[string](b) + }`) + bt.assertNotBlocking(`caster`) +} + +func TestBlocking_Casting_InterfaceInstanceWithMultipleTypeParams(t *testing.T) { + // This checks that casting to an instance type with multiple type parameters + // is treated as a cast and not accidentally treated as a function call. + bt := newBlockingTest(t, + `package test + + type Foo[K comparable, V any] interface { + Baz(K) V + } + + type Bar struct { + dat map[string]int + } + + func (b Bar) Baz(key string) int { + return b.dat[key] + } + + func caster() Foo[string, int] { + b := Bar{ dat: map[string]int{ "foo": 2 }} + return Foo[string, int](b) + }`) + bt.assertNotBlocking(`caster`) +} + +func TestBlocking_Casting_Interface(t *testing.T) { + // This checks that non-generic casting of type is treated as a + // cast and not accidentally treated as a function call. + bt := newBlockingTest(t, + `package test + + type Foo interface { + Baz() string + } + + type Bar struct { + name string + } + + func (b Bar) Baz() string { + return b.name + } + + func caster() Foo { + b := Bar{"foo"} + return Foo(b) + }`) + bt.assertNotBlocking(`caster`) +} + +func TestBlocking_ComplexCasting(t *testing.T) { + // This checks a complex casting to a type is treated as a + // cast and not accidentally treated as a function call. + bt := newBlockingTest(t, + `package test + + type Foo interface { + Bar() string + } + + func doNothing(f Foo) Foo { + return interface{ Bar() string }(f) + }`) + bt.assertNotBlocking(`doNothing`) +} + +func TestBlocking_ComplexCall(t *testing.T) { + // This checks a complex call of a function is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + type Foo func() string + + func bar(f any) string { + return f.(Foo)() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_CallWithNamedInterfaceReceiver(t *testing.T) { + // This checks that calling a named interface function is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + type Foo interface { + Baz() + } + + func bar(f Foo) { + f.Baz() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_CallWithUnnamedInterfaceReceiver(t *testing.T) { + // This checks that calling an unnamed interface function is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + func bar(f interface { Baz() }) { + f.Baz() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_VarFunctionCall(t *testing.T) { + // This checks that calling a function in a var is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + var foo = func() { // line 3 + println("hi") + } + + func bar() { + foo() + }`) + bt.assertNotBlockingLit(3, ``) + bt.assertBlocking(`bar`) +} + +func TestBlocking_FieldFunctionCallOnNamed(t *testing.T) { + // This checks that calling a function in a field is defaulted to blocking. + // This should be the same as the previous test but with a field since + // all function pointers are treated as blocking. + bt := newBlockingTest(t, + `package test + + type foo struct { + Baz func() + } + + func bar(f foo) { + f.Baz() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_FieldFunctionCallOnUnnamed(t *testing.T) { + // Same as previous test but with an unnamed struct. + bt := newBlockingTest(t, + `package test + + func bar(f struct { Baz func() }) { + f.Baz() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_ParamFunctionCall(t *testing.T) { + // Same as previous test but with an unnamed function parameter. + bt := newBlockingTest(t, + `package test + + func bar(baz func()) { + baz() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_FunctionUnwrapping(t *testing.T) { + // Test that calling a function that calls a function etc. + // is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + func bar(baz func()func()func()) { + baz()()() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_MethodCall_NonPointer(t *testing.T) { + // Test that calling a method on a non-pointer receiver. + bt := newBlockingTest(t, + `package test + + type Foo struct {} + + func (f Foo) blocking() { + ch := make(chan bool) + <-ch + } + + func (f Foo) notBlocking() { + println("hi") + } + + func blocking(f Foo) { + f.blocking() + } + + func notBlocking(f Foo) { + f.notBlocking() + }`) + bt.assertBlocking(`Foo.blocking`) + bt.assertNotBlocking(`Foo.notBlocking`) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_MethodCall_Pointer(t *testing.T) { + // Test that calling a method on a pointer receiver. + bt := newBlockingTest(t, + `package test + + type Foo struct {} + + func (f *Foo) blocking() { + ch := make(chan bool) + <-ch + } + + func (f *Foo) notBlocking() { + println("hi") + } + + func blocking(f *Foo) { + f.blocking() + } + + func notBlocking(f *Foo) { + f.notBlocking() + }`) + bt.assertBlocking(`Foo.blocking`) + bt.assertNotBlocking(`Foo.notBlocking`) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_InstantiationBlocking(t *testing.T) { + // This checks that the instantiation of a generic function is + // being used when checking for blocking not the type argument interface. + bt := newBlockingTest(t, + `package test + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + type Foo interface { Baz() } + func FooBaz[T Foo](foo T) { + foo.Baz() + } + + func blockingViaExplicit() { + FooBaz[BazBlocker](BazBlocker{c: make(chan bool)}) + } + + func notBlockingViaExplicit() { + FooBaz[BazNotBlocker](BazNotBlocker{}) + } + + func blockingViaImplicit() { + FooBaz(BazBlocker{c: make(chan bool)}) + } + + func notBlockingViaImplicit() { + FooBaz(BazNotBlocker{}) + }`) + bt.assertFuncInstCount(8) + // `FooBaz` as a generic function does not have FuncInfo for it, + // only non-generic or instantiations of a generic functions have FuncInfo. + + bt.assertBlocking(`BazBlocker.Baz`) + bt.assertBlocking(`blockingViaExplicit`) + bt.assertBlocking(`blockingViaImplicit`) + bt.assertBlockingInst(`pkg/test.FooBaz`) + + bt.assertNotBlocking(`BazNotBlocker.Baz`) + bt.assertNotBlocking(`notBlockingViaExplicit`) + bt.assertNotBlocking(`notBlockingViaImplicit`) + bt.assertNotBlockingInst(`pkg/test.FooBaz`) +} + +func TestBlocking_NestedInstantiations(t *testing.T) { + // Checking that the type parameters are being propagated down into calls. + bt := newBlockingTest(t, + `package test + + func Foo[T any](t T) { + println(t) + } + + func Bar[K comparable, V any, M ~map[K]V](m M) { + Foo(m) + } + + func Baz[T any, S ~[]T](s S) { + m:= map[int]T{} + for i, v := range s { + m[i] = v + } + Bar(m) + } + + func bazInt() { + Baz([]int{1, 2, 3}) + } + + func bazString() { + Baz([]string{"one", "two", "three"}) + }`) + bt.assertFuncInstCount(8) + bt.assertNotBlocking(`bazInt`) + bt.assertNotBlocking(`bazString`) + bt.assertNotBlockingInst(`pkg/test.Foo`) + bt.assertNotBlockingInst(`pkg/test.Foo`) + bt.assertNotBlockingInst(`pkg/test.Bar`) + bt.assertNotBlockingInst(`pkg/test.Bar`) + bt.assertNotBlockingInst(`pkg/test.Baz`) + bt.assertNotBlockingInst(`pkg/test.Baz`) +} + +func TestBlocking_UnusedGenericFunctions(t *testing.T) { + // Checking that the type parameters are being propagated down into callee. + // This is based off of go1.19.13/test/typeparam/orderedmap.go + bt := newBlockingTest(t, + `package test + + type node[K, V any] struct { + key K + val V + left, right *node[K, V] + } + + type Tree[K, V any] struct { + root *node[K, V] + eq func(K, K) bool + } + + func New[K, V any](eq func(K, K) bool) *Tree[K, V] { + return &Tree[K, V]{eq: eq} + } + + func NewStrKey[K ~string, V any]() *Tree[K, V] { // unused + return New[K, V](func(k1, k2 K) bool { + return string(k1) == string(k2) + }) + } + + func NewStrStr[V any]() *Tree[string, V] { // unused + return NewStrKey[string, V]() + } + + func main() { + t := New[int, string](func(k1, k2 int) bool { + return k1 == k2 + }) + println(t) + }`) + bt.assertFuncInstCount(2) + // Notice that `NewStrKey` and `NewStrStr` are not called so doesn't have + // any known instances and therefore they don't have any FuncInfos. + bt.assertNotBlockingInst(`pkg/test.New`) + bt.assertNotBlocking(`main`) +} + +func TestBlocking_LitInstanceCalls(t *testing.T) { + // Literals defined inside a generic function must inherit the + // type arguments (resolver) of the enclosing instance it is defined in + // so that things like calls to other generic functions create the + // call to the correct concrete instance. + bt := newBlockingTest(t, + `package test + + func foo[T any](x T) { + println(x) + } + + func bar[T any](x T) { + f := func(v T) { // line 8 + foo[T](v) + } + f(x) + } + + func main() { + bar[int](42) + bar[float64](3.14) + }`) + bt.assertFuncInstCount(5) + + bt.assertNotBlockingInst(`pkg/test.foo`) + bt.assertNotBlockingInst(`pkg/test.foo`) + bt.assertNotBlockingLit(8, `int`) + bt.assertNotBlockingLit(8, `float64`) + // The following are blocking because the function literal call. + bt.assertBlockingInst(`pkg/test.bar`) + bt.assertBlockingInst(`pkg/test.bar`) +} + +func TestBlocking_BlockingLitInstance(t *testing.T) { + bt := newBlockingTest(t, + `package test + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + type Foo interface { Baz() } + func FooBaz[T Foo](foo T) func() { + return func() { // line 17 + foo.Baz() + } + } + + func main() { + _ = FooBaz(BazBlocker{}) + _ = FooBaz(BazNotBlocker{}) + }`) + bt.assertFuncInstCount(5) + + bt.assertBlocking(`BazBlocker.Baz`) + // THe following is not blocking because the function literal is not called. + bt.assertNotBlockingInst(`pkg/test.FooBaz`) + bt.assertBlockingLit(17, `pkg/test.BazBlocker`) + + bt.assertNotBlocking(`BazNotBlocker.Baz`) + bt.assertNotBlockingInst(`pkg/test.FooBaz`) + bt.assertNotBlockingLit(17, `pkg/test.BazNotBlocker`) +} + +func TestBlocking_MethodSelection(t *testing.T) { + // This tests method selection using method expression (receiver as the first + // argument) selecting on type and method call selecting on a variable. + // This tests in both generic (FooBaz[T]) and non-generic contexts. + bt := newBlockingTest(t, + `package test + + type Foo interface { Baz() } + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + type FooBaz[T Foo] struct {} + func (fb FooBaz[T]) ByMethodExpression() { + var foo T + T.Baz(foo) + } + func (fb FooBaz[T]) ByInstance() { + var foo T + foo.Baz() + } + + func blocking() { + fb := FooBaz[BazBlocker]{} + + FooBaz[BazBlocker].ByMethodExpression(fb) + FooBaz[BazBlocker].ByInstance(fb) + + fb.ByMethodExpression() + fb.ByInstance() + } + + func notBlocking() { + fb := FooBaz[BazNotBlocker]{} + + FooBaz[BazNotBlocker].ByMethodExpression(fb) + FooBaz[BazNotBlocker].ByInstance(fb) + + fb.ByMethodExpression() + fb.ByInstance() + }`) + bt.assertFuncInstCount(8) + + bt.assertBlocking(`BazBlocker.Baz`) + bt.assertBlockingInst(`pkg/test.FooBaz.ByMethodExpression`) + bt.assertBlockingInst(`pkg/test.FooBaz.ByInstance`) + bt.assertBlocking(`blocking`) + + bt.assertNotBlocking(`BazNotBlocker.Baz`) + bt.assertNotBlockingInst(`pkg/test.FooBaz.ByMethodExpression`) + bt.assertNotBlockingInst(`pkg/test.FooBaz.ByInstance`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_IsImportBlocking_Simple(t *testing.T) { + otherSrc := `package other + + func Blocking() { + ch := make(chan bool) + <-ch + } + + func NotBlocking() { + println("hi") + }` + + testSrc := `package test + + import "pkg/other" + + func blocking() { + other.Blocking() + } + + func notBlocking() { + other.NotBlocking() + }` + + bt := newBlockingTestWithOtherPackage(t, testSrc, otherSrc) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_IsImportBlocking_ForwardInstances(t *testing.T) { + otherSrc := `package other + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + }` + + testSrc := `package test + + import "pkg/other" + + type Foo interface { Baz() } + func FooBaz[T Foo](f T) { + f.Baz() + } + + func blocking() { + FooBaz(other.BazBlocker{}) + } + + func notBlocking() { + FooBaz(other.BazNotBlocker{}) + }` + + bt := newBlockingTestWithOtherPackage(t, testSrc, otherSrc) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_IsImportBlocking_BackwardInstances(t *testing.T) { + // This tests propagation of information across package boundaries. + // `FooBaz` has no instances in it until it is referenced in the `test` package. + // That instance information needs to propagate back across the package + // boundary to the `other` package. The information for `BazBlocker` and + // `BazNotBlocker` is propagated back to `FooBaz[BazBlocker]` and + // `FooBaz[BazNotBlocker]`. That information is then propagated forward + // to the `blocking` and `notBlocking` functions in the `test` package. + + otherSrc := `package other + + type Foo interface { Baz() } + func FooBaz[T Foo](f T) { + f.Baz() + }` + + testSrc := `package test + + import "pkg/other" + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + func blocking() { + other.FooBaz(BazBlocker{}) + } + + func notBlocking() { + other.FooBaz(BazNotBlocker{}) + }` + + bt := newBlockingTestWithOtherPackage(t, testSrc, otherSrc) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +type blockingTest struct { + f *srctesting.Fixture + file *ast.File + pkgInfo *Info +} + +func newBlockingTest(t *testing.T, src string) *blockingTest { + f := srctesting.New(t) + tContext := types.NewContext() + tc := typeparams.Collector{ + TContext: tContext, + Instances: &typeparams.PackageInstanceSets{}, + } + + file := f.Parse(`test.go`, src) + testInfo, testPkg := f.Check(`pkg/test`, file) + tc.Scan(testInfo, testPkg, file) + tc.Finish() + + getImportInfo := func(path string) (*Info, error) { + return nil, fmt.Errorf(`getImportInfo should not be called in this test, called with %v`, path) + } + pkgInfo := AnalyzePkg([]*ast.File{file}, f.FileSet, testInfo, tContext, testPkg, tc.Instances, getImportInfo) + PropagateAnalysis([]*Info{pkgInfo}) + + return &blockingTest{ + f: f, + file: file, + pkgInfo: pkgInfo, + } +} + +func newBlockingTestWithOtherPackage(t *testing.T, testSrc string, otherSrc string) *blockingTest { + f := srctesting.New(t) + tContext := types.NewContext() + tc := typeparams.Collector{ + TContext: tContext, + Instances: &typeparams.PackageInstanceSets{}, + } + + pkgInfo := map[string]*Info{} + getImportInfo := func(path string) (*Info, error) { + if info, ok := pkgInfo[path]; ok { + return info, nil + } + return nil, fmt.Errorf(`unexpected package in getImportInfo for %v`, path) + } + + otherFile := f.Parse(`other.go`, otherSrc) + _, otherPkg := f.Check(`pkg/other`, otherFile) + tc.Scan(f.Info, otherPkg, otherFile) + + testFile := f.Parse(`test.go`, testSrc) + _, testPkg := f.Check(`pkg/test`, testFile) + tc.Scan(f.Info, testPkg, testFile) + tc.Finish() + + otherPkgInfo := AnalyzePkg([]*ast.File{otherFile}, f.FileSet, f.Info, tContext, otherPkg, tc.Instances, getImportInfo) + pkgInfo[otherPkg.Path()] = otherPkgInfo + + testPkgInfo := AnalyzePkg([]*ast.File{testFile}, f.FileSet, f.Info, tContext, testPkg, tc.Instances, getImportInfo) + pkgInfo[testPkg.Path()] = testPkgInfo + + PropagateAnalysis([]*Info{otherPkgInfo, testPkgInfo}) + + return &blockingTest{ + f: f, + file: testFile, + pkgInfo: testPkgInfo, + } +} + +func (bt *blockingTest) assertFuncInstCount(expCount int) { + bt.f.T.Helper() + if got := bt.pkgInfo.funcInstInfos.Len(); got != expCount { + bt.f.T.Errorf(`Got %d function instance infos but expected %d.`, got, expCount) + for i, inst := range bt.pkgInfo.funcInstInfos.Keys() { + bt.f.T.Logf(` %d. %q`, i+1, inst.String()) + } + } +} + +func (bt *blockingTest) assertFuncLitCount(expCount int) { + bt.f.T.Helper() + got := 0 + for _, fis := range bt.pkgInfo.funcLitInfos { + got += len(fis) + } + if got != expCount { + bt.f.T.Errorf(`Got %d function literal infos but expected %d.`, got, expCount) + + lits := make([]string, 0, len(bt.pkgInfo.funcLitInfos)) + for fl, fis := range bt.pkgInfo.funcLitInfos { + pos := bt.f.FileSet.Position(fl.Pos()).String() + for _, fi := range fis { + lits = append(lits, pos+`<`+fi.typeArgs.String()+`>`) + } + } + sort.Strings(lits) + for i := range lits { + bt.f.T.Logf(` %d. %q`, i+1, lits[i]) + } + } +} + +func (bt *blockingTest) assertBlocking(funcName string) { + bt.f.T.Helper() + if !bt.isTypesFuncBlocking(funcName) { + bt.f.T.Errorf(`Got %q as not blocking but expected it to be blocking.`, funcName) + } +} + +func (bt *blockingTest) assertNotBlocking(funcName string) { + bt.f.T.Helper() + if bt.isTypesFuncBlocking(funcName) { + bt.f.T.Errorf(`Got %q as blocking but expected it to be not blocking.`, funcName) + } +} + +func getFuncDeclName(fd *ast.FuncDecl) string { + name := fd.Name.Name + if fd.Recv != nil && len(fd.Recv.List) == 1 && fd.Recv.List[0].Type != nil { + typ := fd.Recv.List[0].Type + if p, ok := typ.(*ast.StarExpr); ok { + typ = p.X + } + if id, ok := typ.(*ast.Ident); ok { + name = id.Name + `.` + name + } + } + return name +} + +func (bt *blockingTest) isTypesFuncBlocking(funcName string) bool { + bt.f.T.Helper() + var decl *ast.FuncDecl + ast.Inspect(bt.file, func(n ast.Node) bool { + if f, ok := n.(*ast.FuncDecl); ok && getFuncDeclName(f) == funcName { + decl = f + return false + } + return decl == nil + }) + + if decl == nil { + bt.f.T.Fatalf(`Declaration of %q is not found in the AST.`, funcName) + } + + blockingType, ok := bt.pkgInfo.Defs[decl.Name] + if !ok { + bt.f.T.Fatalf(`No function declaration found for %q.`, decl.Name) + } + + inst := typeparams.Instance{Object: blockingType.(*types.Func)} + return bt.pkgInfo.IsBlocking(inst) +} + +func (bt *blockingTest) assertBlockingLit(lineNo int, typeArgsStr string) { + bt.f.T.Helper() + if !bt.isFuncLitBlocking(lineNo, typeArgsStr) { + bt.f.T.Errorf(`Got FuncLit at line %d with type args %q as not blocking but expected it to be blocking.`, lineNo, typeArgsStr) + } +} + +func (bt *blockingTest) assertNotBlockingLit(lineNo int, typeArgsStr string) { + bt.f.T.Helper() + if bt.isFuncLitBlocking(lineNo, typeArgsStr) { + bt.f.T.Errorf(`Got FuncLit at line %d with type args %q as blocking but expected it to be not blocking.`, lineNo, typeArgsStr) + } +} + +func (bt *blockingTest) isFuncLitBlocking(lineNo int, typeArgsStr string) bool { + bt.f.T.Helper() + fnLit := srctesting.GetNodeAtLineNo[*ast.FuncLit](bt.file, bt.f.FileSet, lineNo) + if fnLit == nil { + bt.f.T.Fatalf(`FuncLit on line %d not found in the AST.`, lineNo) + } + + fis, ok := bt.pkgInfo.funcLitInfos[fnLit] + if !ok { + bt.f.T.Fatalf(`No FuncInfo found for FuncLit at line %d.`, lineNo) + } + + for _, fi := range fis { + if fi.typeArgs.String() == typeArgsStr { + return fi.IsBlocking() + } + } + + bt.f.T.Logf("FuncList instances:") + for i, fi := range fis { + bt.f.T.Logf("\t%d. %q\n", i+1, fi.typeArgs.String()) + } + bt.f.T.Fatalf(`No FuncInfo found for FuncLit at line %d with type args %q.`, lineNo, typeArgsStr) + return false +} + +func (bt *blockingTest) assertBlockingInst(instanceStr string) { + bt.f.T.Helper() + if !bt.isFuncInstBlocking(instanceStr) { + bt.f.T.Errorf(`Got function instance of %q as not blocking but expected it to be blocking.`, instanceStr) + } +} + +func (bt *blockingTest) assertNotBlockingInst(instanceStr string) { + bt.f.T.Helper() + if bt.isFuncInstBlocking(instanceStr) { + bt.f.T.Errorf(`Got function instance of %q as blocking but expected it to be not blocking.`, instanceStr) + } +} + +func (bt *blockingTest) isFuncInstBlocking(instanceStr string) bool { + bt.f.T.Helper() + instances := bt.pkgInfo.funcInstInfos.Keys() + for _, inst := range instances { + if inst.String() == instanceStr { + return bt.pkgInfo.FuncInfo(inst).IsBlocking() + } + } + bt.f.T.Logf(`Function instances found in package info:`) + for i, inst := range instances { + bt.f.T.Logf("\t%d. %s", i+1, inst.String()) + } + bt.f.T.Fatalf(`No function instance found for %q in package info.`, instanceStr) + return false +} + +func (bt *blockingTest) assertBlockingReturn(lineNo int, typeArgsStr string) { + bt.f.T.Helper() + if !bt.isReturnBlocking(lineNo, typeArgsStr) { + bt.f.T.Errorf(`Got return at line %d (%q) as not blocking but expected it to be blocking.`, lineNo, typeArgsStr) + } +} + +func (bt *blockingTest) assertNotBlockingReturn(lineNo int, typeArgsStr string) { + bt.f.T.Helper() + if bt.isReturnBlocking(lineNo, typeArgsStr) { + bt.f.T.Errorf(`Got return at line %d (%q) as blocking but expected it to be not blocking.`, lineNo, typeArgsStr) + } +} + +func (bt *blockingTest) isReturnBlocking(lineNo int, typeArgsStr string) bool { + bt.f.T.Helper() + ret := srctesting.GetNodeAtLineNo[*ast.ReturnStmt](bt.file, bt.f.FileSet, lineNo) + if ret == nil { + bt.f.T.Fatalf(`ReturnStmt on line %d not found in the AST.`, lineNo) + } + + foundInfo := []*FuncInfo{} + for _, info := range bt.pkgInfo.allInfos { + for _, rs := range info.returnStmts { + if rs.analyzeStack[len(rs.analyzeStack)-1] == ret { + if info.typeArgs.String() == typeArgsStr { + // Found info that matches the type args and + // has the return statement so return the blocking value. + return info.Blocking[ret] + } + + // Wrong instance, record for error message in the case + // that the correct one instance is not found. + foundInfo = append(foundInfo, info) + break + } + } + } + + bt.f.T.Logf("FuncInfo instances with ReturnStmt at line %d:", lineNo) + for i, info := range foundInfo { + bt.f.T.Logf("\t%d. %q\n", i+1, info.typeArgs.String()) + } + bt.f.T.Fatalf(`No FuncInfo found for ReturnStmt at line %d with type args %q.`, lineNo, typeArgsStr) + return false +} diff --git a/compiler/internal/analysis/return.go b/compiler/internal/analysis/return.go new file mode 100644 index 000000000..3c83b3c1f --- /dev/null +++ b/compiler/internal/analysis/return.go @@ -0,0 +1,21 @@ +package analysis + +// returnStmt represents a return statement that is blocking or not. +type returnStmt struct { + analyzeStack astPath + deferStmts []*deferStmt +} + +func newReturnStmt(stack astPath, deferStmts []*deferStmt) returnStmt { + return returnStmt{ + analyzeStack: stack.copy(), + deferStmts: deferStmts, + } +} + +// IsBlocking determines if the return statement is blocking or not +// based on the defer statements that affect the return. +// The return may still be blocking if the function has labels and goto's. +func (r returnStmt) IsBlocking(info *FuncInfo) bool { + return isAnyDeferBlocking(r.deferStmts, info.pkgInfo) +} diff --git a/compiler/analysis/sideeffect.go b/compiler/internal/analysis/sideeffect.go similarity index 100% rename from compiler/analysis/sideeffect.go rename to compiler/internal/analysis/sideeffect.go diff --git a/compiler/internal/dce/README.md b/compiler/internal/dce/README.md new file mode 100644 index 000000000..209b51d68 --- /dev/null +++ b/compiler/internal/dce/README.md @@ -0,0 +1,640 @@ +# Dead-Code Elimination + +Dead-Code Eliminations (DCE) is used to remove code that isn't +reachable from a code entry point. Entry points are code like the main method, +init functions, and variable initializations with side effects. +These entry points are always considered alive. Any dependency of +something alive, is also considered alive. + +Once all dependencies are taken into consideration we have the set of alive +declarations. Anything not considered alive is considered dead and +may be safely eliminated, i.e. not outputted to JS. + +- [Idea](#idea) + - [Package](#package) + - [Named Types](#named-types) + - [Named Structs](#named-structs) + - [Interfaces](#interfaces) + - [Functions](#functions) + - [Variables](#variables) + - [Generics and Instances](#generics-and-instances) + - [Links](#links) +- [Design](#design) + - [Initially Alive](#initially-alive) + - [Naming](#naming) + - [Name Specifics](#name-specifics) + - [Dependencies](#dependencies) +- [Examples](#examples) + - [Dead Package](#dead-package) + - [Grandmas and Zombies](#grandmas-and-zombies) + - [Side Effects](#side-effects) + - [Instance Duck-typing](#instance-duck-typing) +- [Additional Notes](#additional-notes) + +## Idea + +The following is the logic behind the DCE mechanism. Not all of the following +is used since some conditions are difficult to determine even with a lot of +additional information, and because GopherJS stores some additional information +making some parts of DCE unnecessary. To ensure that the JS output is fully +functional, we bias the DCE towards things being alive. We'd rather keep +something we don't need than remove something that is needed. + +### Package + +Package declarations (e.g. `package foo`) might be able to be removed +when only used by dead-code. However, packages may be imported and not used +for various reasons including to invoke some initialization or to implement +a link. So it is difficult to determine. +See [Dead Package](#dead-package) example. + +Currently, we won't remove any packages, but someday the complexity +could be added to check for inits, side effects, links, etc then determine +if any of those are are alive or affect alive things. + +### Named Types + +Named type definitions (e.g. `type Foo int`) depend on +the underlying type for each definition. + +When a named type is alive, all of its exported methods +(e.g. `func (f Foo) Bar() { }`) are also alive, even any unused exported method. +Unused exported methods are still important when duck-typing. +See [Interfaces](#interfaces) for more information. +See [Grandmas and Zombies](#grandmas-and-zombies) for an example of what +can happen when removing an unused exported method. + +Also unused exported methods could be accessed by name via reflect +(e.g. `reflect.ValueOf(&Foo{}).MethodByName("Bar")`). Since the +string name may be provided from outside the code, such as the command line, +it is impossible to determine which exported methods could be accessed this way. +It would be very difficult to determine which types are ever accessed via +reflect so by default we simply assume any can be. + +Methods that are unexported may be considered dead when unused even when +the receiver type is alive. The exception is when an interface in the same +package has the same unexported method in it. +See [Interfaces](#interfaces) for more information. + +#### Named Structs + +A named struct is a named type that has a struct as its underlying type, +e.g. `type Foo struct { }`. A struct type depends on all of the types in +its fields and embedded fields. + +If the struct type is alive then all the types of the fields will also be alive. +Even unexported fields maybe accessed via reflections, so they all must be +alive. Also, the fields are needed for comparisons and serializations +(such as `encoding/binary`). + +### Interfaces + +All the types in the function signatures and embedded interfaces are the +dependents of the interface. + +Interfaces may contain exported and unexported function signatures. +If an interface is alive then all of the functions are alive. +Since there are many ways to wrap a type with an interface, any alive type that +duck-types to an interface must have all of the matching methods also alive. + +In theory the unexported functions are also alive however, for GopherJS there +is an exception because duck-typing is handled separately from the method +definitions. Those difference are discussed in [Dependencies](#dependencies) +but for this idea we discuss DCE more generally. + +Since the exported methods in an alive type will be alive, see +[Named Types](#named-types), the only ones here that need to be considered +are the unexported methods. An interface with unexported methods may only +duck-type to types within the package the interface is defined in. +Therefore, if an interface is alive with unexported methods, then all +alive types within the same package that duck-type to that interface, +will have the matching unexported methods be alive. + +Since doing a full `types.Implements` check between every named types and +interfaces in a package is difficult, we simplify this requirement to be +any unexported method in an alive named type that matches an unexported +method in an alive interface is alive even if the named type doesn't duck-type +to the interface. This means that in some rare cases, some unexported +methods on named structs that could have been eliminated will not be. +For example, given `type Foo struct{}; func(f Foo) X(); func (f Foo) y()` the +`Foo.y()` method may be alive if `types Bar interface { Z(); y() }` is alive +even though the `X()` and `Z()` means that `Foo` doesn't implement `Bar` +and therefore `Foo.y()` can not be called via a `Bar.y()`. + +We will try to reduce the false positives in alive unexported methods by using +the parameter and result types of the methods. Meaning that +`y()`, `y(int)`, `y() int`, etc won't match just because they are named `y`. +This also helps with a generic type's unexported methods that use +type parameters, e.g. `Foo.y(T)`. Since the generic type may be instantiated +with `int` and `string`, the different instances of the method are `Foo.y(int)` +and `Foo.y(string)`. By using the parameter and result types, it is possible +to remove the unused unexported method instantiations even when some +instantiations of the same method are used. + +### Functions + +Functions with or without a receiver are dependent on the types used by the +parameters, results, and type uses inside the body of the function. +They are also dependent on any function invoked or used, and +any package level variable that is used. + +Unused functions without a receiver, that are exported or not, may be +considered dead since they aren't used in duck-typing and cannot be accessed +by name via reflections. + +### Variables + +Variables (or constants) depend on their type and anything used during +initialization. + +The exported or unexported variables are dead unless they are used by something +else that is alive or if the initialization has side effects. + +If the initialization has side effects the variable will be alive even +if unused. The side effect may be simply setting another variable's value +that is also unused, however it would be difficult to determine if the +side effects are used or not. +See [Side Effects](#side-effects) example. + +### Generics and Instances + +For functions and types with generics, the definitions are split into +unique instances. For example, `type StringKeys[T any] map[string]T` +could be used in code as `StringKeys[int]` and `StringKeys[*Cat]`. +We don't need all possible instances, only the ones which are realized +in code. Each instance depends on the realized parameter types (type arguments). +In the example the type arguments are `int` and `*Cat`. + +The instance of the generic type also defines the code with the specific +type arguments (e.g. `map[string]int` and `map[string]*Cat`). When an +instance is depended on by alive code, only that instance is alive, not the +entire generic type. This means if `StringKey[*Cat]` is only used from dead +code then it is also dead and can be safely eliminated. + +The named generic types may have methods that are also copied for an instance +with the parameter types replaced by the type arguments. For example, +`func (sk StringKeys[T]) values() []T { ... }` becomes +`func (sk StringKeys[int]) values() []int { ... }` when the type argument +is `int`. This method in the instance now duck-types to +`interface { values() []int }` and therefore must follow the rules for +unexported methods. +See [Instance Duck-typing](#instance-duck-typing) example for more information. + +Functions and named types may be generic, but methods and unnamed types +may not be. This makes somethings simpler. A method with a receiver is used, +only the receiver's type arguments are needed. The generic type or function +may not be needed since only the instances are written out. + +This also means that inside of a generic function or named type there is only +one type parameter list being used. Even generic types used inside of the +generic function must be specified in terms of the type parameter for the +generic and doesn't contribute any type parameters of it's own. +For example, inside of `func Foo[K comparable, V any]() { ... }` every +usage of a generic type must specify a concrete type (`int`, `*Cat`, +`Bar[Bar[bool]]`) or use the parameter types `K` and `V`. This is simpler +than languages that allow a method of an object to have it's own type +parameters, e.g. `class X { void Y() { ... } ... }`. + +However, generics mean that the same method, receiver, type, etc names +will be used with different parameters types caused by different type +arguments. The type arguments are being passed into those parameter types +for a specific instance. +When an interface is alive, the signatures for unexported methods +need to be instantiated with type arguments so that we know which instances +the interface is duck-typing to. See [Interfaces](#interfaces) for more detail. + +### Links + +Links use compiler directives +([`//go:linkname`](https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives)) +to alias a `var` or `func` with another. +For example some code may have `func bar_foo()` as a function stub that is +linked with `foo() { ... }` as a function with a body, i.e. the target of the +link. The links are single directional but allow multiple stubs to link to the +same target. + +When a link is made, the dependencies for the linked code come from +the target. If the target is used by something alive then it is alive. +If a stub linked to a target is used by something alive then that stub and +the target are both alive. + +Since links cross package boundaries in ways that may violate encapsulation +and the dependency tree, it may be difficult to determine if a link is alive +or not. Therefore, currently all links are considered alive. + +## Design + +The design is created taking all the parts of the above idea together and +simplifying the justifications down to a simple set of rules. + +### Initially alive + +- The `main` method in the `main` package +- The `init` in every included file +- Any variable initialization that has a side effect +- Any linked function or variable +- Anything not given a DCE named, e.g. packages + +### Naming + +The following specifies what declarations should be named and how +the names should look. These names are later used to match (via string +comparisons) dependencies with declarations that should be set as alive. +Since the names are used to filter out alive code from all the code +these names may also be referred to as filters. + +Some names will have multiple name parts; an object name and method name. +This is kind of like a first name and last name when a first name alone isn't +specific enough. This helps with matching multiple dependency requirements +for a declaration, i.e. both name parts must be alive before the declaration +is considered alive. + +Currently, only unexported method declarations will have a method +name to support duck-typing with unexported signatures on interfaces. +If the unexported method is depended on, then both names will be in +the dependencies. If the receiver is alive and an alive interface has the +matching unexported signature, then both names will be depended on thus making +the unexported method alive. Since the unexported method is only visible in +the package in which it is defined, the package path is included in the +method name. + +To simplify the above for GopherJS, we don't look at the receiver for +an unexported method before indicating it is alive. Meaning if there is no +interface, only two named objects with identical unexported methods, the use +of either will indicate a use of both. This will cause slightly more unexported +methods to be alive while reducing the complication of type checking which object +or type of object is performing the call. + +| Declaration | exported | unexported | generic | object name | method name | +|:------------|:--------:|:----------:|:-------:|:------------|:------------| +| variables | █ | █ | | `.` | | +| functions | █ | █ | | `.` | | +| functions | █ | █ | █ | `.[]` | | +| named type | █ | █ | | `.` | | +| named type | █ | █ | █ | `.[]` | | +| method | █ | | | `.` | | +| method | █ | | █ | `.[]` | | +| method | | █ | | `.` | `.()()` | +| method | | █ | █ | `.[]` | `.()()` | + +For nested types, the nest (the function or method in which the type is +declared) needs to be taken into consideration to differentiate types with +the same name but different nests. + +| Nest type | generic nest | generic object | object name | +|:----------|:------------:|:--------------:|:------------| +| function | | | `.:` | +| function | █ | | `.:[;]` | +| function | | █ | `.:[]` | +| function | █ | █ | `.:[;]` | +| method | | | `.::` | +| method | █ | | `.::[;]` | +| method | | █ | `.::[]` | +| method | █ | █ | `.::[;]` | + +#### Name Specifics + +The following are specifics about the different types of names that show +up in the above table. This isn't the only way to represent this information. +These names can get long but don't have to. The goal is to make the names +as unique as possible whilst still ensuring that signatures in +interfaces will still match the correct methods. The less unique +the more false positives for alive will occur meaning more dead code is +kept alive. However, too unique could cause needed alive code to not match +and be eliminated causing the application to not run. + +`.`, `.`, `.` +and `.` all have the same form. They are +the package path followed by a `.`, if there is a package path, +and the object name or receiver name. +For example [`rand.Shuffle`](https://pkg.go.dev/math/rand@go1.23.1#Shuffle) +will be named `math/rand.Shuffle`. The builtin [`error`](https://pkg.go.dev/builtin@go1.23.1#error) +will be named `error` without a package path. + +`.[]`, `.[]`, +and `.[]` are the same as above +except with comma separated type parameters or type arguments in square brackets. +The type parameter names are not used, instead the constraint types are since +the names for type parameters may not match even if the constraints match. +For example `type Foo[T any] struct{}; type Bar[B any] { f Foo[B] }` +has `Foo[B]` used in `Bar` that is identical to `Foo[T]` even though +technically `Foo[B]` is an instance of `Foo[T]` with the `B` type parameter +as the type argument. + +Command compiles, i.e. compiles with a `main` entry point, and test builds +should not have any type parameters that aren't resolved to concrete types, +however to handle partial compiles of packages, there may still +be a type parameter, including unions of approximate constraints, +i.e. `~int|~string`. + +Therefore, type arguments need to be reduced to only types. This means +something like [`maps.Keys`](https://pkg.go.dev/maps@go1.23.1#Keys), i.e. +`func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K]`, +will be named `maps.Keys[~map[comparable]any, comparable, any]` as a generic. +If the instances for `Map` are `map[string]int` and `map[int][]*cats.Cat`, +then respectively the names would be `maps.Keys[map[string]int, string, int]` +and `maps.Keys[map[int][]*cats.Cat, int, []*cats.Cat]`. If this function is used +in `func Foo[T ~string|~int](data map[string]T) { ... maps.Keys(data) ... }` +then the instance of `maps.Keys` that `Foo` depends on would be named +`maps.Keys[map[string]~int|~string, string, ~int|~string]`. + +For the method name of unexposed methods, +`.()()`, the prefix, +`.`, is in the same format as `.`. +The rest contains the signature, `()()`. +The signature is defined with only the types since +`(v, u int)(ok bool, err error)` should match `(x, y int)(bool, error)`. +To match both, both will have to be `(int, int)(bool, error)`. +Also the parameter types should include the veridic indicator, +e.g. `sum(...int) int`, since that affects how the signature is matched. +If there are no results then the results part is left off. Otherwise, +the result types only need parenthesis if there are more than one result, +e.g. `(int, int)`, `(int, int) bool`, and `(int, int)(bool, error)`. + +In either the object name or method name, if there is a recursive +type parameter, e.g. `func Foo[T Bar[T]]()` the second usage of the +type parameter will have it's type parameters as `...` to prevent an +infinite loop whilst also indicating which object in the type parameter +is recursive, e.g. `Foo[Bar[Bar[...]]]`. + +### Dependencies + +The dependencies are specified in an expression. +For example a function that invokes another function will be dependent on +that invoked function. When a dependency is added it will be added as one +or more names to the declaration that depends on it. It follows the +[naming rules](#naming) so that the dependencies will match correctly. + +In theory, structural dependencies would be needed to be added +automatically while the declaration is being named. When an interface is named, +it would automatically add all unexported signatures as dependencies via +`.()()`. +However, we do not need to do that in GopherJS because we aren't using +the existence of realized methods in duck-typing. GopherJS stores full set +of method information when describing the type so that, even when things like +unexported methods in interfaces are removed, duck-typing will still work +correctly. This reduces the size of the code by not keeping a potentially +long method body when the signature is all that is needed. + +Currently we don't filter unused packages so there is no need to automatically +add dependencies on the packages themselves. This is also why the package +declarations aren't named and therefore are always alive. + +## Examples + +### Dead Package + +In this example, a point package defines a `Point` object. +The point package may be used by several repos as shared code so can not +have code manually removed from it to reduce its dependencies for specific +applications. + +For the current example, the `Distance` method is never used and therefore +dead. The `Distance` method is the only method dependent on the math package. +It might be safe to make the whole math package dead too and eliminate it in +this case, however, it is possible that some packages aren't used on purpose +and their reason for being included is to invoke the initialization functions +within the package. If a package has any inits or any variable definitions +with side effects, then the package can not be safely removed. + +```go +package point + +import "math" + +type Point struct { + X float64 + Y float64 +} + +func (p Point) Sub(other Point) Point { + p.X -= other.X + p.Y -= other.Y + return p +} + +func (p Point) ToQuadrant1() Point { + if p.X < 0.0 { + p.X = -p.X + } + if p.Y < 0.0 { + p.Y = -p.Y + } + return p +} + +func (p Point) Manhattan(other Point) float64 { + a := p.Sub(other).ToQuadrant1() + return a.X + a.Y +} + +func (p Point) Distance(other Point) float64 { + d := p.Sub(other) + return math.Sqrt(d.X*d.X + d.Y*d.Y) +} +``` + +```go +package main + +import "point" + +func main() { + a := point.Point{X: 10.2, Y: 45.3} + b := point.Point{X: -23.0, Y: 7.7} + println(`Manhattan a to b:`, a.Manhattan(b)) +} +``` + +### Grandmas and Zombies + +In this example, the following code sorts grandmas and zombies by if they are +`Dangerous`. The method `EatBrains` is never used. If we remove `EatBrains` +from `Zombie` then both the grandmas and zombies are moved to the safe +bunker. If we remove `EatBrains` from `Dangerous` then both grandmas and +zombies will be moved to the air lock because `Dangerous` will duck-type +to all `Person` instances. Unused exported methods and signatures must be +considered alive if the type is alive. + +```go +package main + +import "fmt" + +type Person interface { + MoveTo(loc string) +} + +type Dangerous interface { + Person + EatBrains() +} + +type Grandma struct{} + +func (g Grandma) MoveTo(loc string) { + fmt.Println(`grandma was moved to`, loc) +} + +type Zombie struct{} + +func (z Zombie) MoveTo(loc string) { + fmt.Println(`zombie was moved to`, loc) +} + +func (z Zombie) EatBrains() {} + +func main() { + people := []Person{Grandma{}, Zombie{}, Grandma{}, Zombie{}} + for _, person := range people { + if _, ok := person.(Dangerous); ok { + person.MoveTo(`air lock`) + } else { + person.MoveTo(`safe bunker`) + } + } +} +``` + +### Side Effects + +In this example unused variables are being initialized with expressions +that has side effects. The `max` value is 8 by the time `main` is called +because each initialization calls `count()` that increments `max`. +The expression doesn't have to have a function call and can be any combination +of operations. + +An initialization may have a side effect even if it doesn't set a value. For +example, simply printing a message to the console is a side effect that +can not be removed even if it is part of an unused initializer. + +```go +package main + +import "fmt" + +func count() int { + max++ + return max +} + +var ( + max = 0 + _ = count() // a + b, c = count(), count() + x = []int{count(), count(), count()}[0] + y, z = func() (int, int) { return count(), count() }() +) + +func main() { + fmt.Println(`max count`, max) // Outputs: max count 8 +} +``` + +### Instance Duck-typing + +In this example the type `StringKeys[T any]` is a map that stores +any kind of value with string keys. There is an interface `IntProvider` +that `StringKeys` will duck-type to iff the type argument is `int`, +i.e. `StringKeys[int]`. This exemplifies how the type arguments used +in the type arguments affect the overall signature such that in some +cases a generic object may match an interface and in others it may not. + +Also notice that the structure was typed with `T` as the parameter type's +name whereas the methods use `S`. This shows that the name of the type +doesn't matter in the instancing. Therefore, outputting a methods name +(assuming it is unexported) should use the type argument type, +not the type parameter name, e.g. `value() []int` or `value() []any` +instead of `value() []S` or `value() []T`. + +```go +package main + +import ( + "fmt" + "sort" +) + +type StringKeys[T any] map[string]T + +func (sk StringKeys[S]) Keys() []string { + keys := make([]string, 0, len(sk)) + for key := range sk { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func (sk StringKeys[S]) Values() []S { + values := make([]S, len(sk)) + for i, key := range sk.Keys() { + values[i] = sk[key] + } + return values +} + +type IntProvider interface { + Values() []int +} + +func Sum(data IntProvider) int { + sum := 0 + for _, value := range data.Values() { + sum += value + } + return sum +} + +func main() { + sInt := StringKeys[int]{ + `one`: 1, + `two`: 2, + `three`: 3, + `four`: 4, + } + fmt.Println(sInt.Keys()) // Outputs: [four one three two] + fmt.Println(sInt.Values()) // Outputs: [4 1 3 2] + fmt.Println(Sum(sInt)) // Outputs: 10 + + sFp := StringKeys[float64]{ + `one`: 1.1, + `two`: 2.2, + `three`: 3.3, + `four`: 4.4, + } + fmt.Println(sFp.Keys()) // Outputs: [four one three two] + fmt.Println(sFp.Values()) // [4.4 1.1 3.3 2.2] + //fmt.Println(Sum(sFp)) // Fails with "StringKeys[float64] does not implement IntProvider" +} +``` + +## Additional Notes + +This DCE is different from those found in +Muchnick, Steven S.. “Advanced Compiler Design and Implementation.” (1997), +Chapter 18 Control-Flow and Low-Level Optimization, +Section 10 Dead-Code Elimination. And different from related DCE designs +such as Knoop, Rüthing, and Steffen. "Partial dead code elimination." (1994), +SIGPLAN Not. 29, 6, 147–158. +See [DCE wiki](https://en.wikipedia.org/wiki/Dead-code_elimination) +for more information. + +Those discuss DCE at the block code level where the higher level +constructs such as functions and objects have been reduced to a graphs of +blocks with variables, procedures, and routines. Since we want to keep the +higher level constructs during transpilation, we simply are reducing +the higher level constructs not being used. + +Any variable internal to the body of a function or method that is unused or +only used for computing new values for itself, are left as is. +The Go compiler and linters have requirements that attempt to prevent this +kind of dead-code in a function body (unless an underscore is used to quite +usage warnings, e.g. `_ = unusedVar`) and prevent unreachable code. +Therefore, we aren't going to worry about trying to DCE inside of function +bodies or in variable initializers. + +GopherJS does not implicitly perform JS Tree Shaking Algorithms, as discussed in +[How Modern Javascript eliminate dead code](https://blog.stackademic.com/how-modern-javascript-eliminates-dead-code-tree-shaking-algorithm-d7861e48df40) +(2023) at this time and provides no guarantees about the effectiveness +of running such an algorithm on the resulting JS. diff --git a/compiler/internal/dce/collector.go b/compiler/internal/dce/collector.go new file mode 100644 index 000000000..3ddac5e8b --- /dev/null +++ b/compiler/internal/dce/collector.go @@ -0,0 +1,46 @@ +package dce + +import ( + "errors" + "go/types" +) + +// Decl is any code declaration that has dead-code elimination (DCE) +// information attached to it. +type Decl interface { + Dce() *Info +} + +// Collector is a tool to collect dependencies for a declaration +// that'll be used in dead-code elimination (DCE). +type Collector struct { + dce *Info +} + +// CollectDCEDeps captures a list of Go objects (types, functions, etc.) +// the code translated inside f() depends on. Then sets those objects +// as dependencies of the given dead-code elimination info. +// +// Only one CollectDCEDeps call can be active at a time. +func (c *Collector) CollectDCEDeps(decl Decl, f func()) { + if c.dce != nil { + panic(errors.New(`called CollectDCEDeps inside another CollectDCEDeps call`)) + } + + c.dce = decl.Dce() + defer func() { c.dce = nil }() + + f() +} + +// DeclareDCEDep records that the code that is currently being transpiled +// depends on a given Go object with optional type arguments. +// +// The given optional type arguments are used to when the object is a +// function with type parameters or anytime the object doesn't carry them. +// If not given, this attempts to get the type arguments from the object. +func (c *Collector) DeclareDCEDep(o types.Object, tNest, tArgs []types.Type) { + if c.dce != nil { + c.dce.addDep(o, tNest, tArgs) + } +} diff --git a/compiler/internal/dce/dce_test.go b/compiler/internal/dce/dce_test.go new file mode 100644 index 000000000..849596a37 --- /dev/null +++ b/compiler/internal/dce/dce_test.go @@ -0,0 +1,1323 @@ +package dce + +import ( + "fmt" + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "regexp" + "sort" + "testing" + + "github.com/gopherjs/gopherjs/compiler/typesutil" +) + +func Test_Collector_CalledOnce(t *testing.T) { + var c Collector + decl1 := &testDecl{} + decl2 := &testDecl{} + + err := capturePanic(t, func() { + c.CollectDCEDeps(decl1, func() { + c.CollectDCEDeps(decl2, func() { + t.Fatal(`the nested collect function was called`) + }) + }) + }) + errorMatches(t, err, `^called CollectDCEDeps inside another`) +} + +func Test_Collector_Collecting(t *testing.T) { + pkg := testPackage(`tristan`) + obj1 := quickVar(pkg, `Primus`) + obj2 := quickVar(pkg, `Secundus`) + obj3 := quickVar(pkg, `Tertius`) + obj4 := quickVar(pkg, `Quartus`) + obj5 := quickVar(pkg, `Quintus`) + obj6 := quickVar(pkg, `Sextus`) + obj7 := quickVar(pkg, `Una`) + + decl1 := quickTestDecl(obj1) + decl2 := quickTestDecl(obj2) + var c Collector + + c.DeclareDCEDep(obj1, nil, nil) // no effect since a collection isn't running. + depCount(t, decl1, 0) + depCount(t, decl2, 0) + + c.CollectDCEDeps(decl1, func() { + c.DeclareDCEDep(obj2, nil, nil) + c.DeclareDCEDep(obj3, nil, nil) + c.DeclareDCEDep(obj3, nil, nil) // already added so has no effect. + }) + depCount(t, decl1, 2) + depCount(t, decl2, 0) + + c.DeclareDCEDep(obj4, nil, nil) // no effect since a collection isn't running. + depCount(t, decl1, 2) + depCount(t, decl2, 0) + + c.CollectDCEDeps(decl2, func() { + c.DeclareDCEDep(obj5, nil, nil) + c.DeclareDCEDep(obj6, nil, nil) + c.DeclareDCEDep(obj7, nil, nil) + }) + depCount(t, decl1, 2) + depCount(t, decl2, 3) + + // The second collection adds to existing dependencies. + c.CollectDCEDeps(decl2, func() { + c.DeclareDCEDep(obj4, nil, nil) + c.DeclareDCEDep(obj5, nil, nil) + }) + depCount(t, decl1, 2) + depCount(t, decl2, 4) +} + +func Test_Info_SetNameAndDep(t *testing.T) { + tests := []struct { + name string + obj types.Object + want Info // expected Info after SetName + }{ + { + name: `package`, + obj: parseObject(t, `Sarah`, + `package jim + import Sarah "fmt"`), + want: Info{ + objectFilter: `jim.Sarah`, + }, + }, + { + name: `exported var`, + obj: parseObject(t, `Toby`, + `package jim + var Toby float64`), + want: Info{ + objectFilter: `jim.Toby`, + }, + }, + { + name: `exported const`, + obj: parseObject(t, `Ludo`, + `package jim + const Ludo int = 42`), + want: Info{ + objectFilter: `jim.Ludo`, + }, + }, + { + name: `label`, + obj: parseObject(t, `Gobo`, + `package jim + func main() { + i := 0 + Gobo: + i++ + if i < 10 { + goto Gobo + } + }`), + want: Info{ + objectFilter: `jim.Gobo`, + }, + }, + { + name: `exported specific type`, + obj: parseObject(t, `Jen`, + `package jim + type Jen struct{}`), + want: Info{ + objectFilter: `jim.Jen`, + }, + }, + { + name: `exported generic type`, + obj: parseObject(t, `Henson`, + `package jim + type Henson[T comparable] struct{}`), + want: Info{ + objectFilter: `jim.Henson[comparable]`, + }, + }, + { + name: `exported specific function`, + obj: parseObject(t, `Jareth`, + `package jim + func Jareth() {}`), + want: Info{ + objectFilter: `jim.Jareth`, + }, + }, + { + name: `exported generic function`, + obj: parseObject(t, `Didymus`, + `package jim + func Didymus[T comparable]() {}`), + want: Info{ + objectFilter: `jim.Didymus[comparable]`, + }, + }, + { + name: `exported specific method`, + obj: parseObject(t, `Kira`, + `package jim + type Fizzgig string + func (f Fizzgig) Kira() {}`), + want: Info{ + objectFilter: `jim.Fizzgig`, + }, + }, + { + name: `unexported specific method without parameters or results`, + obj: parseObject(t, `frank`, + `package jim + type Aughra int + func (a Aughra) frank() {}`), + want: Info{ + objectFilter: `jim.Aughra`, + methodFilter: `jim.frank()`, + }, + }, + { + name: `unexported specific method with parameters and results`, + obj: parseObject(t, `frank`, + `package jim + type Aughra int + func (a Aughra) frank(other Aughra) (bool, error) { + return a == other, nil + }`), + want: Info{ + objectFilter: `jim.Aughra`, + methodFilter: `jim.frank(jim.Aughra)(bool, error)`, + }, + }, + { + name: `unexported specific method with variadic parameter`, + obj: parseObject(t, `frank`, + `package jim + type Aughra int + func (a Aughra) frank(others ...Aughra) int { + return len(others) + 1 + }`), + want: Info{ + objectFilter: `jim.Aughra`, + methodFilter: `jim.frank(...jim.Aughra) int`, + }, + }, + { + name: `unexported generic method with type parameters and instance argument`, + obj: parseObject(t, `frank`, + `package jim + type Aughra[T ~float64] struct { + value T + } + func (a *Aughra[T]) frank(other *Aughra[float64]) bool { + return float64(a.value) == other.value + }`), + want: Info{ + objectFilter: `jim.Aughra[~float64]`, + methodFilter: `jim.frank(*jim.Aughra[float64]) bool`, + }, + }, + { + name: `unexported generic method with type parameters and generic argument`, + obj: parseObject(t, `frank`, + `package jim + type Aughra[T ~float64] struct { + value T + } + func (a *Aughra[T]) frank(other *Aughra[T]) bool { + return a.value == other.value + }`), + want: Info{ + objectFilter: `jim.Aughra[~float64]`, + methodFilter: `jim.frank(*jim.Aughra[~float64]) bool`, + }, + }, + { + name: `specific method on unexported type`, + obj: parseObject(t, `Red`, + `package jim + type wembley struct{} + func (w wembley) Red() {}`), + want: Info{ + objectFilter: `jim.wembley`, + }, + }, + { + name: `unexported method resulting in an interface with exported methods`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() interface{ + WakkaWakka(joke string)(landed bool) + Firth()(string, error) + }`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() interface{ Firth()(string, error); WakkaWakka(string) bool }`, + }, + }, + { + name: `unexported method resulting in an interface with unexported methods`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() interface{ + wakkaWakka(joke string)(landed bool) + firth()(string, error) + }`), + want: Info{ + objectFilter: `jim.Fozzie`, + // The package path, i.e. `jim.`, is used on unexported methods + // to ensure the filter will not match another package's method. + methodFilter: `jim.bear() interface{ jim.firth()(string, error); jim.wakkaWakka(string) bool }`, + }, + }, + { + name: `unexported method resulting in an empty interface `, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() interface{}`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() any`, + }, + }, + { + name: `unexported method resulting in a function`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() func(joke string)(landed bool)`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() func(string) bool`, + }, + }, + { + name: `unexported method resulting in a struct`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() struct{ + Joke string + WakkaWakka bool + }`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() struct{ Joke string; WakkaWakka bool }`, + }, + }, + { + name: `unexported method resulting in a struct with type parameter`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie[T ~string|~int] struct{} + func (f *Fozzie[T]) bear() struct{ + Joke T + wakkaWakka bool + }`), + want: Info{ + objectFilter: `jim.Fozzie[~int|~string]`, + // The `Joke ~int|~string` part will likely not match other methods + // such as methods with `Joke string` or `Joke int`, however the + // interface should be defined for the instantiations of this type + // and those should have the correct field type for `Joke`. + methodFilter: `jim.bear() struct{ Joke ~int|~string; jim.wakkaWakka bool }`, + }, + }, + { + name: `unexported method resulting in an empty struct`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() struct{}`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() struct{}`, + }, + }, + { + name: `unexported method resulting in a slice`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear()(jokes []string)`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() []string`, + }, + }, + { + name: `unexported method resulting in an array`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear()(jokes [2]string)`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() [2]string`, + }, + }, + { + name: `unexported method resulting in a map`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear()(jokes map[string]bool)`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() map[string]bool`, + }, + }, + { + name: `unexported method resulting in a channel`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() chan string`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() chan string`, + }, + }, + { + name: `unexported method resulting in a complex compound named type`, + obj: parseObject(t, `packRat`, + `package jim + type Gonzo[T any] struct{ + v T + } + func (g Gonzo[T]) Get() T { return g.v } + type Rizzo struct{} + func (r Rizzo) packRat(v int) Gonzo[Gonzo[Gonzo[int]]] { + return Gonzo[Gonzo[Gonzo[int]]]{v: Gonzo[Gonzo[int]]{v: Gonzo[int]{v: v}}} + } + var _ int = Rizzo{}.packRat(42).Get().Get().Get()`), + want: Info{ + objectFilter: `jim.Rizzo`, + methodFilter: `jim.packRat(int) jim.Gonzo[jim.Gonzo[jim.Gonzo[int]]]`, + }, + }, + { + name: `unexported method resulting in an instance with same type parameter`, + obj: parseObject(t, `sidekick`, + `package jim + type Beaker[T any] struct{} + type Honeydew[S any] struct{} + func (hd Honeydew[S]) sidekick() Beaker[S] { + return Beaker[S]{} + }`), + want: Info{ + objectFilter: `jim.Honeydew[any]`, + methodFilter: `jim.sidekick() jim.Beaker[any]`, + }, + }, + { + name: `struct with self referencing type parameter constraints`, + obj: parseObject(t, `Keys`, + `package jim + func Keys[K comparable, V any, M ~map[K]V](m M) []K { + keys := make([]K, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + return keys + }`), + want: Info{ + objectFilter: `jim.Keys[comparable, any, ~map[comparable]any]`, + }, + }, + { + name: `interface with self referencing type parameter constraints`, + obj: parseObject(t, `ElectricMayhem`, + `package jim + type ElectricMayhem[K comparable, V any, M ~map[K]V] interface { + keys() []K + values() []V + asMap() M + }`), + want: Info{ + objectFilter: `jim.ElectricMayhem[comparable, any, ~map[comparable]any]`, + }, + }, + { + name: `function with recursive referencing type parameter constraints`, + obj: parseObject(t, `doWork`, + `package jim + type Doozer[T any] interface { + comparable + Work() T + } + + func doWork[T Doozer[T]](a T) T { + return a.Work() + }`), + want: Info{ + objectFilter: `jim.doWork[jim.Doozer[jim.Doozer[...]]]`, + }, + }, + { + name: `function with recursive referencing multiple type parameter constraints`, + obj: parseObject(t, `doWork`, + `package jim + type Doozer[T, U any] interface { + Work() T + Play() U + } + + func doWork[T Doozer[T, U], U any](a T) T { + return a.Work() + }`), + want: Info{ + objectFilter: `jim.doWork[jim.Doozer[jim.Doozer[...], any], any]`, + }, + }, + { + name: `function with multiple recursive referencing multiple type parameter constraints`, + obj: parseObject(t, `doWork`, + `package jim + type Doozer[T, U any] interface { + Work() T + Play() U + } + + func doWork[T Doozer[T, U], U Doozer[T, U]](a T) T { + return a.Work() + }`), + want: Info{ + objectFilter: `jim.doWork[jim.Doozer[jim.Doozer[...], jim.Doozer[...]], jim.Doozer[jim.Doozer[...], jim.Doozer[...]]]`, + }, + }, + { + name: `function with multiple recursive referencing type parameter constraints`, + obj: parseObject(t, `doWork`, + `package jim + type Doozer[T any] interface { + Work() T + } + + type Fraggle[U any] interface { + Play() U + } + + func doWork[T Doozer[T], U Fraggle[U]](a T) T { + return a.Work() + }`), + want: Info{ + objectFilter: `jim.doWork[jim.Doozer[jim.Doozer[...]], jim.Fraggle[jim.Fraggle[...]]]`, + }, + }, + { + name: `function with osculating recursive referencing type parameter constraints`, + obj: parseObject(t, `doWork`, + `package jim + type Doozer[T any] interface { + Work() T + } + + type Fraggle[U any] interface { + Play() U + } + + func doWork[T Doozer[U], U Fraggle[T]]() {}`), + want: Info{ + objectFilter: `jim.doWork[jim.Doozer[jim.Fraggle[jim.Doozer[...]]], jim.Fraggle[jim.Doozer[jim.Fraggle[...]]]]`, + }, + }, + } + + t.Run(`SetName`, func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &testDecl{} + equal(t, d.Dce().unnamed(), true) + equal(t, d.Dce().String(), `[unnamed] -> []`) + t.Log(`object:`, types.ObjectString(tt.obj, nil)) + + d.Dce().SetName(tt.obj, nil, nil) + equal(t, d.Dce().unnamed(), tt.want.unnamed()) + equal(t, d.Dce().objectFilter, tt.want.objectFilter) + equal(t, d.Dce().methodFilter, tt.want.methodFilter) + equalSlices(t, d.Dce().getDeps(), tt.want.getDeps()) + equal(t, d.Dce().String(), tt.want.String()) + }) + } + }) + + t.Run(`addDep`, func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &testDecl{} + t.Log(`object:`, types.ObjectString(tt.obj, nil)) + + wantDeps := []string{} + if len(tt.want.objectFilter) > 0 { + wantDeps = append(wantDeps, tt.want.objectFilter) + } + if len(tt.want.methodFilter) > 0 { + wantDeps = append(wantDeps, tt.want.methodFilter) + } + sort.Strings(wantDeps) + + c := Collector{} + c.CollectDCEDeps(d, func() { + c.DeclareDCEDep(tt.obj, nil, nil) + }) + equalSlices(t, d.Dce().getDeps(), wantDeps) + }) + } + }) +} + +func Test_Info_SetNameOnlyOnce(t *testing.T) { + pkg := testPackage(`mogwai`) + obj1 := quickVar(pkg, `Gizmo`) + obj2 := quickVar(pkg, `Stripe`) + + decl := &testDecl{} + decl.Dce().SetName(obj1, nil, nil) + + err := capturePanic(t, func() { + decl.Dce().SetName(obj2, nil, nil) + }) + errorMatches(t, err, `^may only set the name once for path/to/mogwai\.Gizmo .*$`) +} + +func Test_Info_UsesDeps(t *testing.T) { + tests := []struct { + name string + id string // identifier to check for usage and instance + line int // line number to find the identifier on + src string + wantDeps []string + }{ + { + name: `usage of specific struct`, + id: `Sinclair`, + line: 5, + src: `package epsilon3 + type Sinclair struct{} + func (s Sinclair) command() { } + func main() { + Sinclair{}.command() //<-- line 5 + }`, + wantDeps: []string{`epsilon3.Sinclair`}, + }, + { + name: `usage of generic struct`, + id: `Sheridan`, + line: 5, + src: `package epsilon3 + type Sheridan[T comparable] struct{} + func (s Sheridan[T]) command() { } + func main() { + Sheridan[string]{}.command() //<-- line 5 + }`, + wantDeps: []string{`epsilon3.Sheridan[string]`}, + }, + { + name: `usage of unexported method of generic struct`, + id: `command`, + line: 5, + src: `package epsilon3 + type Sheridan[T comparable] struct{} + func (s Sheridan[T]) command() { } + func main() { + Sheridan[string]{}.command() //<-- line 5 + }`, + // unexported methods need the method filter for matching with + // unexported methods on interfaces. + wantDeps: []string{ + `epsilon3.Sheridan[string]`, + `epsilon3.command()`, + }, + }, + { + name: `usage of unexported method of generic struct pointer`, + id: `command`, + line: 5, + src: `package epsilon3 + type Sheridan[T comparable] struct{} + func (s *Sheridan[T]) command() { } + func main() { + (&Sheridan[string]{}).command() //<-- line 5 + }`, + // unexported methods need the method filter for matching with + // unexported methods on interfaces. + wantDeps: []string{ + `epsilon3.Sheridan[string]`, + `epsilon3.command()`, + }, + }, + { + name: `invocation of function with implicit type arguments`, + id: `Move`, + line: 5, + src: `package epsilon3 + type Ivanova[T any] struct{} + func Move[T ~string|~int](i Ivanova[T]) { } + func main() { + Move(Ivanova[string]{}) //<-- line 5 + }`, + wantDeps: []string{`epsilon3.Move[string]`}, + }, + { + name: `exported method on a complex generic type`, + id: `Get`, + line: 6, + src: `package epsilon3 + type Garibaldi[T any] struct{ v T } + func (g Garibaldi[T]) Get() T { return g.v } + func main() { + michael := Garibaldi[Garibaldi[Garibaldi[int]]]{v: Garibaldi[Garibaldi[int]]{v: Garibaldi[int]{v: 42}}} + _ = michael.Get() // <-- line 6 + }`, + wantDeps: []string{`epsilon3.Garibaldi[epsilon3.Garibaldi[epsilon3.Garibaldi[int]]]`}, + }, + { + name: `unexported method on a complex generic type`, + id: `get`, + line: 6, + src: `package epsilon3 + type Garibaldi[T any] struct{ v T } + func (g Garibaldi[T]) get() T { return g.v } + func main() { + michael := Garibaldi[Garibaldi[Garibaldi[int]]]{v: Garibaldi[Garibaldi[int]]{v: Garibaldi[int]{v: 42}}} + _ = michael.get() // <-- line 6 + }`, + wantDeps: []string{ + `epsilon3.Garibaldi[epsilon3.Garibaldi[epsilon3.Garibaldi[int]]]`, + `epsilon3.get() epsilon3.Garibaldi[epsilon3.Garibaldi[int]]`, + }, + }, + { + name: `invoke of method with an unnamed interface receiver`, + id: `heal`, + line: 8, + src: `package epsilon3 + type Franklin struct{} + func (g Franklin) heal() {} + func main() { + var stephen interface{ + heal() + } = Franklin{} + stephen.heal() // <-- line 8 + }`, + wantDeps: []string{ + `epsilon3.heal()`, + }, + }, + { + name: `invoke a method with a generic return type via instance`, + // Based on go/1.19.13/x64/test/dictionaryCapture-noinline.go + id: `lennier`, + line: 6, + src: `package epsilon3 + type delenn[T any] struct { a T } + func (d delenn[T]) lennier() T { return d.a } + func cocoon() int { + x := delenn[int]{a: 7} + f := delenn[int].lennier // <-- line 6 + return f(x) + }`, + wantDeps: []string{ + `epsilon3.delenn[int]`, + `epsilon3.lennier() int`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &testDecl{} + uses, inst := parseInstanceUse(t, tt.line, tt.id, tt.src) + tArgs := typeListToSlice(inst.TypeArgs) + t.Logf(`object: %s with [%s]`, types.ObjectString(uses, nil), (typesutil.TypeList)(tArgs).String()) + + c := Collector{} + c.CollectDCEDeps(d, func() { + c.DeclareDCEDep(uses, nil, tArgs) + }) + equalSlices(t, d.Dce().getDeps(), tt.wantDeps) + }) + } +} + +func Test_Info_SpecificCasesDeps(t *testing.T) { + tests := []struct { + name string + obj types.Object + nestTArgs []types.Type + tArgs []types.Type + wantDeps []string + }{ + { + name: `struct instantiation with generic object`, + obj: parseObject(t, `Mikey`, + `package astoria; + type Mikey[T comparable] struct{} + `), + tArgs: []types.Type{types.Typ[types.String]}, + wantDeps: []string{`astoria.Mikey[string]`}, + }, + { + name: `method instantiation with generic object`, + obj: parseObject(t, `brand`, + `package astoria; + type Mikey[T comparable] struct{ a T} + func (m Mikey[T]) brand() T { + return m.a + }`), + tArgs: []types.Type{types.Typ[types.String]}, + wantDeps: []string{ + `astoria.Mikey[string]`, + `astoria.brand() string`, + }, + }, + { + name: `method instantiation with generic object and multiple type parameters`, + obj: parseObject(t, `shuffle`, + `package astoria; + type Chunk[K comparable, V any] struct{ data map[K]V } + func (c Chunk[K, V]) shuffle(k K) V { + return c.data[k] + }`), + tArgs: []types.Type{types.Typ[types.String], types.Typ[types.Int]}, + wantDeps: []string{ + `astoria.Chunk[string, int]`, + `astoria.shuffle(string) int`, + }, + }, + { + name: `method instantiation with generic object renamed type parameters`, + obj: parseObject(t, `shuffle`, + `package astoria; + type Chunk[K comparable, V any] struct{ data map[K]V } + func (c Chunk[T, K]) shuffle(k T) K { + return c.data[k] + }`), + tArgs: []types.Type{types.Typ[types.String], types.Typ[types.Int]}, + wantDeps: []string{ + `astoria.Chunk[string, int]`, + `astoria.shuffle(string) int`, + }, + }, + { + name: `a concrete function with a nested concrete type instance`, + obj: parseObject(t, `davi`, + `package astoria + func jake(v int) any { + type davi struct { V int } + return davi{ V: v } + }`), + wantDeps: []string{`astoria.jake:davi`}, + }, + { + name: `a concrete function with a nested generic type instance`, + obj: parseObject(t, `pantoliano`, + `package astoria + func francis(v int) any { + type pantoliano[T any] struct { V int } + return pantoliano[int]{ V: v } + }`), + tArgs: []types.Type{types.Typ[types.Int]}, + wantDeps: []string{`astoria.francis:pantoliano[int]`}, + }, + { + name: `a generic function with a nested concrete type instance`, + obj: parseObject(t, `ramsey`, + `package astoria + func mama[T any](v T) any { + type ramsey struct { V T } + return ramsey{ V: v } + }`), + nestTArgs: []types.Type{types.Typ[types.Int]}, + wantDeps: []string{`astoria.mama:ramsey[int;]`}, + }, + { + name: `a generic function with a nested generic type instance`, + obj: parseObject(t, `matuszak`, + `package astoria + func sloth[T any]() any { + type matuszak[U any] struct { X T; Y U } + return matuszak[bool]{} + }`), + nestTArgs: []types.Type{types.Typ[types.String]}, + tArgs: []types.Type{types.Typ[types.Bool]}, + wantDeps: []string{`astoria.sloth:matuszak[string; bool]`}, + }, + { + name: `a concrete method with a nested concrete type instance`, + obj: parseObject(t, `davi`, + `package astoria + type fratelli struct { V int } + func (m *fratelli) jake() any { + type davi struct { V int } + return davi{ V: m.V } + }`), + wantDeps: []string{`astoria.fratelli:jake:davi`}, + }, + { + name: `a concrete method with a nested generic type instance`, + obj: parseObject(t, `pantoliano`, + `package astoria + type fratelli struct { V int } + func (f *fratelli) francis(v int) any { + type pantoliano[T any] struct { V int } + return pantoliano[int]{ V: v } + }`), + tArgs: []types.Type{types.Typ[types.Int]}, + wantDeps: []string{`astoria.fratelli:francis:pantoliano[int]`}, + }, + { + name: `a generic method with a nested concrete type instance`, + obj: parseObject(t, `ramsey`, + `package astoria + type fratelli[T any] struct { v T } + func (f *fratelli[T]) mama() any { + type ramsey struct { V T } + return ramsey{ V: f.v } + }`), + nestTArgs: []types.Type{types.Typ[types.Int]}, + wantDeps: []string{`astoria.fratelli:mama:ramsey[int;]`}, + }, + { + name: `a generic method with a nested generic type instance`, + obj: parseObject(t, `matuszak`, + `package astoria + type fratelli[T any] struct {} + func (f *fratelli[T]) sloth() any { + type matuszak[U any] struct { X T; Y U } + return matuszak[bool]{} + }`), + nestTArgs: []types.Type{types.Typ[types.String]}, + tArgs: []types.Type{types.Typ[types.Bool]}, + wantDeps: []string{`astoria.fratelli:sloth:matuszak[string; bool]`}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &testDecl{} + tail := `` + if len(tt.nestTArgs) > 0 { + tail += (typesutil.TypeList)(tt.nestTArgs).String() + `;` + } + tail += (typesutil.TypeList)(tt.tArgs).String() + t.Logf(`object: %s with [%s]`, types.ObjectString(tt.obj, nil), tail) + + c := Collector{} + c.CollectDCEDeps(d, func() { + c.DeclareDCEDep(tt.obj, tt.nestTArgs, tt.tArgs) + }) + equalSlices(t, d.Dce().getDeps(), tt.wantDeps) + }) + } +} + +func Test_Info_SetAsAlive(t *testing.T) { + pkg := testPackage(`fantasia`) + + t.Run(`set alive prior to naming`, func(t *testing.T) { + obj := quickVar(pkg, `Falkor`) + decl := &testDecl{} + equal(t, decl.Dce().isAlive(), true) // unnamed is automatically alive + equal(t, decl.Dce().String(), `[unnamed] -> []`) + + decl.Dce().SetAsAlive() + equal(t, decl.Dce().isAlive(), true) // still alive but now explicitly alive + equal(t, decl.Dce().String(), `[alive] [unnamed] -> []`) + + decl.Dce().SetName(obj, nil, nil) + equal(t, decl.Dce().isAlive(), true) // alive because SetAsAlive was called + equal(t, decl.Dce().String(), `[alive] path/to/fantasia.Falkor -> []`) + }) + + t.Run(`set alive after naming`, func(t *testing.T) { + obj := quickVar(pkg, `Artax`) + decl := &testDecl{} + equal(t, decl.Dce().isAlive(), true) // unnamed is automatically alive + equal(t, decl.Dce().String(), `[unnamed] -> []`) + + decl.Dce().SetName(obj, nil, nil) + equal(t, decl.Dce().isAlive(), false) // named so no longer automatically alive + equal(t, decl.Dce().String(), `path/to/fantasia.Artax -> []`) + + decl.Dce().SetAsAlive() + equal(t, decl.Dce().isAlive(), true) // alive because SetAsAlive was called + equal(t, decl.Dce().String(), `[alive] path/to/fantasia.Artax -> []`) + }) +} + +func Test_Selector_JustVars(t *testing.T) { + pkg := testPackage(`tolkien`) + frodo := quickTestDecl(quickVar(pkg, `Frodo`)) + samwise := quickTestDecl(quickVar(pkg, `Samwise`)) + meri := quickTestDecl(quickVar(pkg, `Meri`)) + pippin := quickTestDecl(quickVar(pkg, `Pippin`)) + aragorn := quickTestDecl(quickVar(pkg, `Aragorn`)) + boromir := quickTestDecl(quickVar(pkg, `Boromir`)) + gimli := quickTestDecl(quickVar(pkg, `Gimli`)) + legolas := quickTestDecl(quickVar(pkg, `Legolas`)) + gandalf := quickTestDecl(quickVar(pkg, `Gandalf`)) + fellowship := []*testDecl{ + frodo, samwise, meri, pippin, aragorn, + boromir, gimli, legolas, gandalf, + } + + c := Collector{} + c.CollectDCEDeps(frodo, func() { + c.DeclareDCEDep(samwise.obj, nil, nil) + c.DeclareDCEDep(meri.obj, nil, nil) + c.DeclareDCEDep(pippin.obj, nil, nil) + }) + c.CollectDCEDeps(pippin, func() { + c.DeclareDCEDep(meri.obj, nil, nil) + }) + c.CollectDCEDeps(aragorn, func() { + c.DeclareDCEDep(boromir.obj, nil, nil) + }) + c.CollectDCEDeps(gimli, func() { + c.DeclareDCEDep(legolas.obj, nil, nil) + }) + c.CollectDCEDeps(legolas, func() { + c.DeclareDCEDep(gimli.obj, nil, nil) + }) + c.CollectDCEDeps(gandalf, func() { + c.DeclareDCEDep(frodo.obj, nil, nil) + c.DeclareDCEDep(aragorn.obj, nil, nil) + c.DeclareDCEDep(gimli.obj, nil, nil) + c.DeclareDCEDep(legolas.obj, nil, nil) + }) + + for _, decl := range fellowship { + equal(t, decl.Dce().isAlive(), false) + } + + tests := []struct { + name string + init []*testDecl // which decls to set explicitly alive + want []*testDecl // which decls should be determined as alive + }{ + { + name: `all alive`, + init: fellowship, + want: fellowship, + }, + { + name: `all dead`, + init: []*testDecl{}, + want: []*testDecl{}, + }, + { + name: `Frodo`, + init: []*testDecl{frodo}, + want: []*testDecl{frodo, samwise, meri, pippin}, + }, + { + name: `Sam and Pippin`, + init: []*testDecl{samwise, pippin}, + want: []*testDecl{samwise, meri, pippin}, + }, + { + name: `Gandalf`, + init: []*testDecl{gandalf}, + want: fellowship, + }, + { + name: `Legolas`, + init: []*testDecl{legolas}, + want: []*testDecl{legolas, gimli}, + }, + { + name: `Gimli`, + init: []*testDecl{gimli}, + want: []*testDecl{legolas, gimli}, + }, + { + name: `Boromir`, + init: []*testDecl{boromir}, + want: []*testDecl{boromir}, + }, + { + name: `Aragorn`, + init: []*testDecl{aragorn}, + want: []*testDecl{aragorn, boromir}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, decl := range fellowship { + decl.Dce().alive = false + } + for _, decl := range tt.init { + decl.Dce().SetAsAlive() + } + + s := &Selector[*testDecl]{} + for _, decl := range fellowship { + s.Include(decl, false) + } + + selected := s.AliveDecls() + for _, decl := range tt.want { + if _, ok := selected[decl]; !ok { + t.Errorf(`expected %q to be alive`, decl.obj.String()) + } + delete(selected, decl) + } + for decl := range selected { + t.Errorf(`expected %q to be dead`, decl.obj.String()) + } + }) + } +} + +func Test_Selector_SpecificMethods(t *testing.T) { + objects := parseObjects(t, + `package pratchett + + type rincewind struct{} + func (r rincewind) Run() {} + func (r rincewind) hide() {} + + type Vimes struct{} + func (v Vimes) Run() {} + func (v Vimes) Read() {} + + func Vetinari() {}`) + + var ( + // Objects are in read order so pick the objects we want for this test + // while skipping over `r rincewind` and `v Vimes`. + rincewind = quickTestDecl(objects[0]) + rincewindRun = quickTestDecl(objects[2]) + rincewindHide = quickTestDecl(objects[4]) + vimes = quickTestDecl(objects[5]) + vimesRun = quickTestDecl(objects[7]) + vimesRead = quickTestDecl(objects[9]) + vetinari = quickTestDecl(objects[10]) + ) + allDecls := []*testDecl{rincewind, rincewindRun, rincewindHide, vimes, vimesRun, vimesRead, vetinari} + + c := Collector{} + c.CollectDCEDeps(rincewindRun, func() { + c.DeclareDCEDep(rincewind.obj, nil, nil) + }) + c.CollectDCEDeps(rincewindHide, func() { + c.DeclareDCEDep(rincewind.obj, nil, nil) + }) + c.CollectDCEDeps(vimesRun, func() { + c.DeclareDCEDep(vimes.obj, nil, nil) + }) + c.CollectDCEDeps(vimesRead, func() { + c.DeclareDCEDep(vimes.obj, nil, nil) + }) + vetinari.Dce().SetAsAlive() + + tests := []struct { + name string + deps []*testDecl // which decls are vetinari dependent on + want []*testDecl // which decls should be determined as alive + }{ + { + name: `no deps`, + deps: []*testDecl{}, + want: []*testDecl{vetinari}, + }, + { + name: `structs`, + deps: []*testDecl{rincewind, vimes}, + // rincewindHide is not included because it is not exported and not used. + want: []*testDecl{rincewind, rincewindRun, vimes, vimesRun, vimesRead, vetinari}, + }, + { + name: `exported method`, + deps: []*testDecl{rincewind, rincewindRun}, + want: []*testDecl{rincewind, rincewindRun, vetinari}, + }, + { + name: `unexported method`, + deps: []*testDecl{rincewind, rincewindHide}, + want: []*testDecl{rincewind, rincewindRun, rincewindHide, vetinari}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vetinari.Dce().deps = nil // reset deps + c.CollectDCEDeps(vetinari, func() { + for _, decl := range tt.deps { + c.DeclareDCEDep(decl.obj, nil, nil) + } + }) + + s := Selector[*testDecl]{} + for _, decl := range allDecls { + s.Include(decl, false) + } + selected := s.AliveDecls() + for _, decl := range tt.want { + if _, ok := selected[decl]; !ok { + t.Errorf(`expected %q to be alive`, decl.obj.String()) + } + delete(selected, decl) + } + for decl := range selected { + t.Errorf(`expected %q to be dead`, decl.obj.String()) + } + }) + } +} + +type testDecl struct { + obj types.Object // should match the object used in Dce.SetName when set + dce Info +} + +func (d *testDecl) Dce() *Info { + return &d.dce +} + +func testPackage(name string) *types.Package { + return types.NewPackage(`path/to/`+name, name) +} + +func quickTestDecl(o types.Object) *testDecl { + d := &testDecl{obj: o} + d.Dce().SetName(o, nil, nil) + return d +} + +func quickVar(pkg *types.Package, name string) *types.Var { + return types.NewVar(token.NoPos, pkg, name, types.Typ[types.Int]) +} + +func newTypeInfo() *types.Info { + return &types.Info{ + Defs: map[*ast.Ident]types.Object{}, + Uses: map[*ast.Ident]types.Object{}, + Instances: map[*ast.Ident]types.Instance{}, + } +} + +func parseObject(t *testing.T, name, source string) types.Object { + t.Helper() + objects := parseObjects(t, source) + for _, obj := range objects { + if obj.Name() == name { + return obj + } + } + t.Fatalf(`object %q not found`, name) + return nil +} + +func parseObjects(t *testing.T, source string) []types.Object { + t.Helper() + fset := token.NewFileSet() + info := newTypeInfo() + parsePackage(t, source, fset, info) + objects := make([]types.Object, 0, len(info.Defs)) + for _, obj := range info.Defs { + if obj != nil { + objects = append(objects, obj) + } + } + sort.Slice(objects, func(i, j int) bool { + return objects[i].Pos() < objects[j].Pos() + }) + return objects +} + +func parseInstanceUse(t *testing.T, lineNo int, idName, source string) (types.Object, types.Instance) { + t.Helper() + fset := token.NewFileSet() + info := newTypeInfo() + parsePackage(t, source, fset, info) + for id, obj := range info.Uses { + if id.Name == idName && fset.Position(id.Pos()).Line == lineNo { + return obj, info.Instances[id] + } + } + t.Fatalf(`failed to find %s on line %d`, idName, lineNo) + return nil, types.Instance{} +} + +func parsePackage(t *testing.T, source string, fset *token.FileSet, info *types.Info) *types.Package { + t.Helper() + f, err := parser.ParseFile(fset, `test.go`, source, 0) + if err != nil { + t.Fatal(`parsing source:`, err) + } + + conf := types.Config{ + Importer: importer.Default(), + DisableUnusedImportCheck: true, + } + pkg, err := conf.Check(f.Name.Name, fset, []*ast.File{f}, info) + if err != nil { + t.Fatal(`type checking:`, err) + } + return pkg +} + +func capturePanic(t *testing.T, f func()) (err error) { + t.Helper() + defer func() { + t.Helper() + if r := recover(); r != nil { + if err2, ok := r.(error); ok { + err = err2 + return + } + t.Errorf(`expected an error to be panicked but got (%[1]T) %[1]#v`, r) + return + } + t.Error(`expected a panic but got none`) + }() + + f() + return nil +} + +func errorMatches(t *testing.T, err error, wantPattern string) { + t.Helper() + re := regexp.MustCompile(wantPattern) + if got := fmt.Sprint(err); !re.MatchString(got) { + t.Errorf(`expected error %q to match %q`, got, re.String()) + } +} + +func depCount(t *testing.T, decl *testDecl, want int) { + t.Helper() + if got := len(decl.Dce().deps); got != want { + t.Errorf(`expected %d deps but got %d`, want, got) + } +} + +func equal[T comparable](t *testing.T, got, want T) { + t.Helper() + if got != want { + t.Errorf("Unexpected value was gotten:\n\texp: %#v\n\tgot: %#v", want, got) + } +} + +func equalSlices[T comparable](t *testing.T, got, want []T) { + t.Helper() + if len(got) != len(want) { + t.Errorf("expected %d but got %d\n\texp: %#v\n\tgot: %#v", len(want), len(got), want, got) + return + } + for i, wantElem := range want { + equal(t, got[i], wantElem) + } +} diff --git a/compiler/internal/dce/filters.go b/compiler/internal/dce/filters.go new file mode 100644 index 000000000..c074fe545 --- /dev/null +++ b/compiler/internal/dce/filters.go @@ -0,0 +1,451 @@ +package dce + +import ( + "go/types" + "sort" + "strconv" + "strings" + + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/typesutil" +) + +// getFilters determines the DCE filters for the given object. +// This will return an object filter and optionally return a method filter. +// +// Typically, the object filter will always be set and the method filter +// will be empty unless the object is an unexported method. +// However, when the object is a method invocation on an unnamed interface type +// the object filter will be empty and only the method filter will be set. +// The later shouldn't happen when naming a declaration but only when creating +// dependencies. +func getFilters(o types.Object, nestTArgs, tArgs []types.Type) (objectFilter, methodFilter string) { + if f, ok := o.(*types.Func); ok { + sig := f.Type().(*types.Signature) + if recv := sig.Recv(); recv != nil { + // The object is a method so the object filter is the receiver type + // if the receiver type is named, otherwise it's an unnamed interface. + typ := recv.Type() + if ptrType, ok := typ.(*types.Pointer); ok { + typ = ptrType.Elem() + } + if len(tArgs) == 0 { + tArgs = getTypeArgs(typ) + } + if named, ok := typ.(*types.Named); ok { + objectFilter = getObjectFilter(named.Obj(), nil, tArgs) + } + + // The method is not exported so we only need the method filter. + if !o.Exported() { + methodFilter = getMethodFilter(o, tArgs) + } + return + } + } + + // The object is not a method so we only need the object filter. + objectFilter = getObjectFilter(o, nestTArgs, tArgs) + return +} + +// getObjectFilter returns the object filter that functions as the primary +// name when determining if a declaration is alive or not. +// See [naming design] for more information. +// +// [naming design]: https://github.com/gopherjs/gopherjs/compiler/internal/dce/README.md#naming +func getObjectFilter(o types.Object, nestTArgs, tArgs []types.Type) string { + return (&filterGen{}).Object(o, nestTArgs, tArgs) +} + +// getMethodFilter returns the method filter that functions as the secondary +// name when determining if a declaration is alive or not. +// See [naming design] for more information. +// +// [naming design]: https://github.com/gopherjs/gopherjs/compiler/internal/dce/README.md#naming +func getMethodFilter(o types.Object, tArgs []types.Type) string { + if sig, ok := o.Type().(*types.Signature); ok { + gen := &filterGen{} + tParams := getTypeParams(o.Type()) + if len(tArgs) == 0 { + tArgs = getTypeArgs(sig) + } + if len(tArgs) > 0 { + gen.addReplacements(tParams, tArgs) + } + return objectName(o) + gen.Signature(sig) + } + return `` +} + +// objectName returns the name part of a filter name, +// including the package path and nest names, if available. +// +// This is different from `o.Id` since it always includes the package path +// when available and doesn't add "_." when not available. +func objectName(o types.Object) string { + prefix := `` + if o.Pkg() != nil { + prefix += o.Pkg().Path() + `.` + } + if nest := typeparams.FindNestingFunc(o); nest != nil && nest != o { + if recv := typesutil.RecvType(nest.Type().(*types.Signature)); recv != nil { + prefix += recv.Obj().Name() + `:` + } + prefix += nest.Name() + `:` + } + return prefix + o.Name() +} + +// getNestTypeParams gets the type parameters for the nesting function +// or nil if the object is not nested in a function or +// the given object is a function itself. +func getNestTypeParams(o types.Object) []types.Type { + fn := typeparams.FindNestingFunc(o) + if fn == nil || fn == o { + return nil + } + + tp := typeparams.SignatureTypeParams(fn.Type().(*types.Signature)) + nestTParams := make([]types.Type, tp.Len()) + for i := 0; i < tp.Len(); i++ { + nestTParams[i] = tp.At(i) + } + return nestTParams +} + +// getTypeArgs gets the type arguments for the given type +// or nil if the type does not have type arguments. +func getTypeArgs(typ types.Type) []types.Type { + switch t := typ.(type) { + case *types.Pointer: + return getTypeArgs(t.Elem()) + case *types.Named: + return typeListToSlice(t.TypeArgs()) + } + return nil +} + +// getTypeParams gets the type parameters for the given type +// or nil if the type does not have type parameters. +func getTypeParams(typ types.Type) []types.Type { + switch t := typ.(type) { + case *types.Pointer: + return getTypeParams(t.Elem()) + case *types.Named: + if typeParams := t.TypeParams(); typeParams != nil { + return typeParamListToSlice(typeParams) + } + case *types.Signature: + if typeParams := t.RecvTypeParams(); typeParams != nil { + return typeParamListToSlice(typeParams) + } + if typeParams := t.TypeParams(); typeParams != nil { + return typeParamListToSlice(typeParams) + } + } + return nil +} + +// typeListToSlice returns the list of type arguments for the type arguments. +func typeListToSlice(typeArgs *types.TypeList) []types.Type { + tArgs := make([]types.Type, typeArgs.Len()) + for i := range tArgs { + tArgs[i] = typeArgs.At(i) + } + return tArgs +} + +// typeParamListToSlice returns the list of type arguments for the type parameters. +func typeParamListToSlice(typeParams *types.TypeParamList) []types.Type { + tParams := make([]types.Type, typeParams.Len()) + for i := range tParams { + tParams[i] = typeParams.At(i) + } + return tParams +} + +type processingGroup struct { + o types.Object + nestTArgs []types.Type + tArgs []types.Type +} + +func (p processingGroup) is(o types.Object, nestTArgs, tArgs []types.Type) bool { + if len(p.nestTArgs) != len(nestTArgs) || len(p.tArgs) != len(tArgs) || p.o != o { + return false + } + for i, ta := range nestTArgs { + if p.nestTArgs[i] != ta { + return false + } + } + for i, ta := range tArgs { + if p.tArgs[i] != ta { + return false + } + } + return true +} + +type filterGen struct { + // replacement is used to use another type in place of a given type + // this is typically used for type parameters to type arguments. + replacement map[types.Type]types.Type + inProgress []processingGroup +} + +// addReplacements adds a mapping from one type to another. +// The slices should be the same length but will ignore any extra types. +// Any replacement where the key and value are the same will be ignored. +func (gen *filterGen) addReplacements(from []types.Type, to []types.Type) { + if len(from) == 0 || len(to) == 0 { + return + } + + if gen.replacement == nil { + gen.replacement = map[types.Type]types.Type{} + } + + count := len(from) + if count > len(to) { + count = len(to) + } + for i := 0; i < count; i++ { + if from[i] != to[i] { + gen.replacement[from[i]] = to[i] + } + } +} + +// pushGenerics prepares the filter generator for processing an object +// by setting any generic information and nesting information needed for it. +// It returns the type arguments for the object and a function to restore +// the previous state of the filter generator. +func (gen *filterGen) pushGenerics(o types.Object, nestTArgs, tArgs []types.Type) ([]types.Type, []types.Type, func()) { + // Create a new replacement map and copy the old one into it. + oldReplacement := gen.replacement + gen.replacement = map[types.Type]types.Type{} + for tp, ta := range oldReplacement { + gen.replacement[tp] = ta + } + + // Prepare the nested context for the object. + nestTParams := getNestTypeParams(o) + if len(nestTArgs) > 0 { + gen.addReplacements(nestTParams, nestTArgs) + } else { + nestTArgs = nestTParams + } + + // Prepare the type arguments for the object. + tParams := getTypeParams(o.Type()) + if len(tArgs) == 0 { + tArgs = getTypeArgs(o.Type()) + } + if len(tArgs) > 0 { + gen.addReplacements(tParams, tArgs) + } else { + tArgs = tParams + } + + // Return a function to restore the old replacement map. + return nestTArgs, tArgs, func() { + gen.replacement = oldReplacement + } +} + +func (gen *filterGen) startProcessing(o types.Object, nestTArgs, tArgs []types.Type) bool { + for _, p := range gen.inProgress { + if p.is(o, nestTArgs, tArgs) { + return false + } + } + gen.inProgress = append(gen.inProgress, processingGroup{o: o, nestTArgs: nestTArgs, tArgs: tArgs}) + return true +} + +func (gen *filterGen) stopProcessing() { + gen.inProgress = gen.inProgress[:len(gen.inProgress)-1] +} + +// Object returns an object filter or filter part for an object. +func (gen *filterGen) Object(o types.Object, nestTArgs, tArgs []types.Type) string { + filter := objectName(o) + + // Add additional type information for generics and instances. + nestTArgs, tArgs, popGenerics := gen.pushGenerics(o, nestTArgs, tArgs) + defer popGenerics() + + if len(tArgs) > 0 || len(nestTArgs) > 0 { + // Avoid infinite recursion in type arguments by + // tracking the current object and type arguments being processed + // and skipping if already in progress. + if gen.startProcessing(o, nestTArgs, tArgs) { + filter += gen.TypeArgs(nestTArgs, tArgs) + gen.stopProcessing() + } else { + filter += `[...]` + } + } + + return filter +} + +// Signature returns the filter part containing the signature +// parameters and results for a function or method, e.g. `(int)(bool,error)`. +func (gen *filterGen) Signature(sig *types.Signature) string { + filter := `(` + gen.Tuple(sig.Params(), sig.Variadic()) + `)` + switch sig.Results().Len() { + case 0: + break + case 1: + filter += ` ` + gen.Type(sig.Results().At(0).Type()) + default: + filter += `(` + gen.Tuple(sig.Results(), false) + `)` + } + return filter +} + +// TypeArgs returns the filter part containing the type +// arguments, e.g. `[bool;any,int|string]`. +func (gen *filterGen) TypeArgs(nestTArgs, tArgs []types.Type) string { + toStr := func(t []types.Type) string { + parts := make([]string, len(t)) + for i, ta := range t { + parts[i] = gen.Type(ta) + } + return strings.Join(parts, `, `) + } + + head := `[` + if len(nestTArgs) > 0 { + head += toStr(nestTArgs) + `;` + if len(tArgs) > 0 { + head += ` ` + } + } + return head + toStr(tArgs) + `]` +} + +// Tuple returns the filter part containing parameter or result +// types for a function, e.g. `(int,string)`, `(int,...string)`. +func (gen *filterGen) Tuple(t *types.Tuple, variadic bool) string { + count := t.Len() + parts := make([]string, count) + for i := range parts { + argType := t.At(i).Type() + if i == count-1 && variadic { + if slice, ok := argType.(*types.Slice); ok { + argType = slice.Elem() + } + parts[i] = `...` + gen.Type(argType) + } else { + parts[i] = gen.Type(argType) + } + } + return strings.Join(parts, `, `) +} + +// Type returns the filter part for a single type. +func (gen *filterGen) Type(typ types.Type) string { + switch t := typ.(type) { + case *types.Array: + return `[` + strconv.FormatInt(t.Len(), 10) + `]` + gen.Type(t.Elem()) + case *types.Chan: + return `chan ` + gen.Type(t.Elem()) + case *types.Interface: + return gen.Interface(t) + case *types.Map: + return `map[` + gen.Type(t.Key()) + `]` + gen.Type(t.Elem()) + case *types.Named: + // Get type args from named instance not generic object. + return gen.Object(t.Obj(), nil, typeListToSlice(t.TypeArgs())) + case *types.Pointer: + return `*` + gen.Type(t.Elem()) + case *types.Signature: + return `func` + gen.Signature(t) + case *types.Slice: + return `[]` + gen.Type(t.Elem()) + case *types.Struct: + return gen.Struct(t) + case *types.TypeParam: + return gen.TypeParam(t) + default: + // Anything else, like basics, just stringify normally. + return t.String() + } +} + +// Union returns the filter part for a union of types from an type parameter +// constraint, e.g. `~string|int|~float64`. +func (gen *filterGen) Union(u *types.Union) string { + parts := make([]string, u.Len()) + for i := range parts { + term := u.Term(i) + part := gen.Type(term.Type()) + if term.Tilde() { + part = "~" + part + } + parts[i] = part + } + // Sort the union so that "string|int" matches "int|string". + sort.Strings(parts) + return strings.Join(parts, `|`) +} + +// Interface returns the filter part for an interface type or +// an interface for a type parameter constraint. +func (gen *filterGen) Interface(inter *types.Interface) string { + // Collect all method constraints with method names and signatures. + parts := make([]string, inter.NumMethods()) + for i := range parts { + fn := inter.Method(i) + parts[i] = fn.Id() + gen.Signature(fn.Type().(*types.Signature)) + } + // Add any union constraints. + for i := 0; i < inter.NumEmbeddeds(); i++ { + if union, ok := inter.EmbeddedType(i).(*types.Union); ok { + parts = append(parts, gen.Union(union)) + } + } + // Sort the parts of the interface since the order doesn't matter. + // e.g. `interface { a(); b() }` is the same as `interface { b(); a() }`. + sort.Strings(parts) + + if len(parts) == 0 { + return `any` + } + if inter.NumMethods() == 0 && len(parts) == 1 { + return parts[0] // single constraint union, i.e. `bool|~int|string` + } + return `interface{ ` + strings.Join(parts, `; `) + ` }` +} + +// Struct returns the filter part for a struct type. +func (gen *filterGen) Struct(s *types.Struct) string { + if s.NumFields() == 0 { + return `struct{}` + } + parts := make([]string, s.NumFields()) + for i := range parts { + f := s.Field(i) + // The field name and order is required to be part of the filter since + // struct matching rely on field names too. Tags are not needed. + // See https://go.dev/ref/spec#Conversions + parts[i] = f.Id() + ` ` + gen.Type(f.Type()) + } + return `struct{ ` + strings.Join(parts, `; `) + ` }` +} + +// TypeParam returns the filter part for a type parameter. +// If there is an argument remap, it will use the remapped type. +func (gen *filterGen) TypeParam(t *types.TypeParam) string { + if inst, exists := gen.replacement[t]; exists { + return gen.Type(inst) + } + if t.Constraint() == nil { + return `any` + } + return gen.Type(t.Constraint()) +} diff --git a/compiler/internal/dce/info.go b/compiler/internal/dce/info.go new file mode 100644 index 000000000..ef310f047 --- /dev/null +++ b/compiler/internal/dce/info.go @@ -0,0 +1,157 @@ +package dce + +import ( + "bytes" + "encoding/gob" + "fmt" + "go/types" + "sort" + "strings" +) + +// Info contains information used by the dead-code elimination (DCE) logic to +// determine whether a declaration is alive or dead. +type Info struct { + // alive indicates if the declaration is marked as alive + // and will not be eliminated. + alive bool + + // objectFilter is the primary DCE name for a declaration. + // This will be the variable, function, or type identifier. + // For methods it is the receiver type identifier. + // If empty, the declaration is assumed to be alive. + objectFilter string + + // methodFilter is the secondary DCE name for a declaration. + // This will be empty if objectFilter is empty. + // This will be set to a qualified method name if the objectFilter + // can not determine if the declaration is alive on it's own. + // See ./README.md for more information. + methodFilter string + + // Set of fully qualified (including package path) DCE symbol + // and/or method names that this DCE declaration depends on. + deps map[string]struct{} +} + +// String gets a human-readable representation of the DCE info. +func (d *Info) String() string { + tags := `` + if d.alive { + tags += `[alive] ` + } + if d.unnamed() { + tags += `[unnamed] ` + } + names := []string{} + if len(d.objectFilter) > 0 { + names = append(names, d.objectFilter+` `) + } + if len(d.methodFilter) > 0 { + names = append(names, d.methodFilter+` `) + } + return tags + strings.Join(names, `& `) + `-> [` + strings.Join(d.getDeps(), `, `) + `]` +} + +// unnamed returns true if SetName has not been called for this declaration. +// This indicates that the DCE is not initialized. +func (d *Info) unnamed() bool { + return d.objectFilter == `` && d.methodFilter == `` +} + +// isAlive returns true if the declaration is marked as alive. +// +// Returns true if SetAsAlive was called on this declaration or +// if SetName was not called meaning the DCE is not initialized. +func (d *Info) isAlive() bool { + return d.alive || d.unnamed() +} + +// SetAsAlive marks the declaration as alive, meaning it will not be eliminated. +// +// This should be called by an entry point (like main() or init() functions) +// or a variable initializer which has a side effect, consider it live. +func (d *Info) SetAsAlive() { + d.alive = true +} + +// SetName sets the name used by DCE to represent the declaration +// this DCE info is attached to. +// +// The given optional type arguments are used to when the object is a +// function with type parameters or anytime the object doesn't carry them. +// If not given, this attempts to get the type arguments from the object. +func (d *Info) SetName(o types.Object, tNest, tArgs []types.Type) { + if !d.unnamed() { + panic(fmt.Errorf(`may only set the name once for %s`, d.String())) + } + + // Determine name(s) for DCE. + d.objectFilter, d.methodFilter = getFilters(o, tNest, tArgs) +} + +// addDep add a declaration dependencies used by DCE +// for the declaration this DCE info is attached to. +func (d *Info) addDep(o types.Object, tNest, tArgs []types.Type) { + objectFilter, methodFilter := getFilters(o, tNest, tArgs) + d.addDepName(objectFilter) + d.addDepName(methodFilter) +} + +// addDepName adds a declaration dependency by name. +func (d *Info) addDepName(depName string) { + if len(depName) > 0 { + if d.deps == nil { + d.deps = make(map[string]struct{}) + } + d.deps[depName] = struct{}{} + } +} + +// getDeps gets the dependencies for the declaration sorted by name. +func (id *Info) getDeps() []string { + deps := make([]string, len(id.deps)) + i := 0 + for dep := range id.deps { + deps[i] = dep + i++ + } + sort.Strings(deps) + return deps +} + +type serializableInfo struct { + Alive bool + ObjectFilter string + MethodFilter string + Deps []string +} + +func (id *Info) GobEncode() ([]byte, error) { + si := serializableInfo{ + Alive: id.alive, + ObjectFilter: id.objectFilter, + MethodFilter: id.methodFilter, + Deps: id.getDeps(), + } + + buf := &bytes.Buffer{} + err := gob.NewEncoder(buf).Encode(si) + return buf.Bytes(), err +} + +func (id *Info) GobDecode(data []byte) error { + var si serializableInfo + if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&si); err != nil { + return err + } + + id.alive = si.Alive + id.objectFilter = si.ObjectFilter + id.methodFilter = si.MethodFilter + id.deps = make(map[string]struct{}, len(si.Deps)) + for _, dep := range si.Deps { + id.deps[dep] = struct{}{} + } + return nil +} diff --git a/compiler/internal/dce/selector.go b/compiler/internal/dce/selector.go new file mode 100644 index 000000000..3dff49028 --- /dev/null +++ b/compiler/internal/dce/selector.go @@ -0,0 +1,93 @@ +package dce + +// DeclConstraint is type constraint for any code declaration that has +// dead-code elimination (DCE) information attached to it and will be +// used in a set. +type DeclConstraint interface { + Decl + comparable +} + +// Selector gathers all declarations that are still alive after dead-code elimination. +type Selector[D DeclConstraint] struct { + byFilter map[string][]*declInfo[D] + + // A queue of live decls to find other live decls. + pendingDecls []D +} + +type declInfo[D DeclConstraint] struct { + decl D + objectFilter string + methodFilter string +} + +// Include will add a new declaration to be checked as alive or not. +func (s *Selector[D]) Include(decl D, implementsLink bool) { + if s.byFilter == nil { + s.byFilter = make(map[string][]*declInfo[D]) + } + + dce := decl.Dce() + + if dce.isAlive() { + s.pendingDecls = append(s.pendingDecls, decl) + return + } + + if implementsLink { + s.pendingDecls = append(s.pendingDecls, decl) + } + + info := &declInfo[D]{decl: decl} + + if dce.objectFilter != `` { + info.objectFilter = dce.objectFilter + s.byFilter[info.objectFilter] = append(s.byFilter[info.objectFilter], info) + } + + if dce.methodFilter != `` { + info.methodFilter = dce.methodFilter + s.byFilter[info.methodFilter] = append(s.byFilter[info.methodFilter], info) + } +} + +func (s *Selector[D]) popPending() D { + max := len(s.pendingDecls) - 1 + d := s.pendingDecls[max] + s.pendingDecls = s.pendingDecls[:max] + return d +} + +// AliveDecls returns a set of declarations that are still alive +// after dead-code elimination. +// This should only be called once all declarations have been included. +func (s *Selector[D]) AliveDecls() map[D]struct{} { + dceSelection := make(map[D]struct{}) // Known live decls. + for len(s.pendingDecls) != 0 { + d := s.popPending() + dce := d.Dce() + + dceSelection[d] = struct{}{} // Mark the decl as live. + + // Consider all decls the current one is known to depend on and possible add + // them to the live queue. + for _, dep := range dce.getDeps() { + if infos, ok := s.byFilter[dep]; ok { + delete(s.byFilter, dep) + for _, info := range infos { + if info.objectFilter == dep { + info.objectFilter = `` + } + if info.methodFilter == dep { + info.methodFilter = `` + } + if info.objectFilter == `` && info.methodFilter == `` { + s.pendingDecls = append(s.pendingDecls, info.decl) + } + } + } + } + } + return dceSelection +} diff --git a/compiler/internal/symbol/symbol.go b/compiler/internal/symbol/symbol.go index 851ca1ef6..d460ea86d 100644 --- a/compiler/internal/symbol/symbol.go +++ b/compiler/internal/symbol/symbol.go @@ -21,6 +21,11 @@ type Name struct { // New constructs SymName for a given named symbol. func New(o types.Object) Name { + pkgPath := `_` + if pkg := o.Pkg(); pkg != nil { + pkgPath = pkg.Path() + } + if fun, ok := o.(*types.Func); ok { sig := fun.Type().(*types.Signature) if recv := sig.Recv(); recv != nil { @@ -28,18 +33,18 @@ func New(o types.Object) Name { typ := recv.Type() if ptr, ok := typ.(*types.Pointer); ok { return Name{ - PkgPath: o.Pkg().Path(), + PkgPath: pkgPath, Name: "(*" + ptr.Elem().(*types.Named).Obj().Name() + ")." + o.Name(), } } return Name{ - PkgPath: o.Pkg().Path(), + PkgPath: pkgPath, Name: typ.(*types.Named).Obj().Name() + "." + o.Name(), } } } return Name{ - PkgPath: o.Pkg().Path(), + PkgPath: pkgPath, Name: o.Name(), } } diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index 0a9ae75da..b42c1031d 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -4,94 +4,8 @@ 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. // @@ -102,24 +16,37 @@ type visitor struct { instances *PackageInstanceSets resolver *Resolver info *types.Info + + nestTParams *types.TypeParamList // The type parameters for a nested context. + nestTArgs []types.Type // The type arguments for a nested context. } 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. +func (c *visitor) Visit(n ast.Node) ast.Visitor { + if ident, ok := n.(*ast.Ident); ok { + c.visitIdent(ident) + } + return c +} - ident, ok := n.(*ast.Ident) - if !ok { - return +func (c *visitor) visitIdent(ident *ast.Ident) { + if inst, ok := c.info.Instances[ident]; ok { + // Found the use of a generic type or function. + c.visitInstance(ident, inst) } - instance, ok := c.info.Instances[ident] - if !ok { - return + if len(c.resolver.TypeArgs()) > 0 { + if obj, ok := c.info.Defs[ident]; ok && obj != nil { + // Found instance of a type defined inside a generic context. + c.visitNestedType(obj) + } } +} - obj := c.info.ObjectOf(ident) +func (c *visitor) visitInstance(ident *ast.Ident, inst types.Instance) { + obj := c.info.Uses[ident] + tArgs := inst.TypeArgs // For types embedded in structs, the object the identifier resolves to is a // *types.Var representing the implicitly declared struct field. However, the @@ -132,21 +59,67 @@ func (c *visitor) Visit(n ast.Node) (w ast.Visitor) { if t, ok := typ.(*types.Named); ok { obj = t.Obj() } + + // If the object is defined in the same scope as the instance, + // then we apply the current nested type arguments. + var nestTParams *types.TypeParamList + var nestTArgs []types.Type + if obj.Parent().Contains(ident.Pos()) { + nestTParams = c.nestTParams + nestTArgs = c.nestTArgs + } + + c.addInstance(obj, tArgs, nestTParams, nestTArgs) +} + +func (c *visitor) visitNestedType(obj types.Object) { + if _, ok := obj.(*types.TypeName); !ok { + // Found a variable or function, not a type, so skip it. + return + } + + typ := obj.Type() + if ptr, ok := typ.(*types.Pointer); ok { + typ = ptr.Elem() + } + + t, ok := typ.(*types.Named) + if !ok || t.TypeParams().Len() > 0 { + // Found a generic type or an unnamed type (e.g. type parameter). + // Don't add generic types yet because they + // will be added when we find an instance of them. + return + } + + c.addInstance(obj, nil, c.resolver.TypeParams(), c.resolver.TypeArgs()) +} + +func (c *visitor) addInstance(obj types.Object, tArgList *types.TypeList, nestTParams *types.TypeParamList, nestTArgs []types.Type) { + tArgs := c.resolver.SubstituteAll(tArgList) + if isGeneric(nestTParams, tArgs) { + // Skip any instances that still have type parameters in them after + // substitution. This occurs when a type is defined while nested + // in a generic context and is not fully instantiated yet. + // We need to wait until we find a full instantiation of the type. + return + } + c.instances.Add(Instance{ Object: obj, - TArgs: c.resolver.SubstituteAll(instance.TypeArgs), + TArgs: tArgs, + TNest: nestTArgs, }) 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), + Object: method.Origin(), + TArgs: tArgs, + TNest: nestTArgs, }) } } - return } // seedVisitor implements ast.Visitor that collects information necessary to @@ -215,16 +188,26 @@ func (c *seedVisitor) Visit(n ast.Node) ast.Visitor { // set whenever their receiver type instance is encountered. type Collector struct { TContext *types.Context - Info *types.Info Instances *PackageInstanceSets + + objMap map[types.Object]ast.Node + infoMap map[string]*types.Info } // 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 { +func (c *Collector) Scan(info *types.Info, pkg *types.Package, files ...*ast.File) { + if c.objMap == nil { + c.objMap = map[types.Object]ast.Node{} + } + if c.infoMap == nil { + c.infoMap = map[string]*types.Info{} + } + + // Check the info for this package then record it for later use. + if info.Instances == nil || info.Defs == nil { panic(fmt.Errorf("types.Info must have Instances and Defs populated")) } - objMap := map[types.Object]ast.Node{} + c.infoMap[pkg.Path()] = info // Collect instances of generic objects in non-generic code in the package and // add then to the existing InstanceSet. @@ -232,32 +215,77 @@ func (c *Collector) Scan(pkg *types.Package, files ...*ast.File) { visitor: visitor{ instances: c.Instances, resolver: nil, - info: c.Info, + info: info, }, - objMap: objMap, + objMap: c.objMap, } for _, file := range files { ast.Walk(&sc, file) } +} - for iset := c.Instances.Pkg(pkg); !iset.exhausted(); { +// Finish will finish the collecting instances by propagating instances of +// generic types and functions found in generic code. The generic code is +// rescanned with in an instances context to find internally defined instances. +// +// This should only be called after all the files are scanned. +func (c *Collector) Finish() { + for !c.Instances.allExhausted() { + for pkgPath, instances := range *c.Instances { + c.propagate(pkgPath, instances) + } + } +} + +func (c *Collector) propagate(pkgPath string, instances *InstanceSet) { + info := c.infoMap[pkgPath] + for iset := instances; !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]) + c.scanSignature(inst, typ, info) + 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]) + c.scanNamed(inst, typ, info) } } } + +func (c *Collector) scanSignature(inst Instance, typ *types.Signature, info *types.Info) { + v := visitor{ + instances: c.Instances, + resolver: NewResolver(c.TContext, inst), + info: info, + + nestTParams: SignatureTypeParams(typ), + nestTArgs: inst.TArgs, + } + ast.Walk(&v, c.objMap[inst.Object]) +} + +func (c *Collector) scanNamed(inst Instance, typ *types.Named, info *types.Info) { + obj := typ.Obj() + node := c.objMap[obj] + if node == nil { + // Types without an entry in objMap are concrete types + // that are defined in a generic context. Skip them. + return + } + + var nestTParams *types.TypeParamList + nest := FindNestingFunc(obj) + if nest != nil { + nestTParams = SignatureTypeParams(nest.Type().(*types.Signature)) + } + + v := visitor{ + instances: c.Instances, + resolver: NewResolver(c.TContext, inst), + info: info, + + nestTParams: nestTParams, + nestTArgs: inst.TNest, + } + ast.Walk(&v, node) +} diff --git a/compiler/internal/typeparams/collect_test.go b/compiler/internal/typeparams/collect_test.go index 9bd5faee4..1021e9dad 100644 --- a/compiler/internal/typeparams/collect_test.go +++ b/compiler/internal/typeparams/collect_test.go @@ -2,12 +2,15 @@ package typeparams import ( "go/ast" + "go/token" "go/types" + "strings" "testing" + "golang.org/x/tools/go/ast/astutil" + "github.com/google/go-cmp/cmp" "github.com/gopherjs/gopherjs/internal/srctesting" - "golang.org/x/tools/go/ast/astutil" ) func TestVisitor(t *testing.T) { @@ -35,7 +38,11 @@ func TestVisitor(t *testing.T) { t := typ[int, A]{} t.method(0) (*typ[int32, A]).method(nil, 0) + type x struct{ T []typ[int64, A] } + type y[X any] struct{ T []typ[A, X] } + _ = y[int8]{} + _ = y[A]{} return } @@ -49,7 +56,11 @@ func TestVisitor(t *testing.T) { t := typ[int, T]{} t.method(0) (*typ[int32, T]).method(nil, 0) + type x struct{ T []typ[int64, T] } + type y[X any] struct{ T []typ[T, X] } + _ = y[int8]{} + _ = y[T]{} return } @@ -67,7 +78,11 @@ func TestVisitor(t *testing.T) { t := typ[int, T]{} t.method(0) (*typ[int32, T]).method(nil, 0) + type x struct{ T []typ[int64, T] } + type y[X any] struct{ T []typ[T, X] } + _ = y[int8]{} + _ = y[T]{} return } @@ -189,23 +204,51 @@ func TestVisitor(t *testing.T) { descr: "non-generic function", resolver: nil, node: lookupDecl("entry1"), - want: instancesInFunc(lookupType("A")), + want: append( + instancesInFunc(lookupType("A")), + Instance{ + Object: lookupObj("entry1.y"), + TArgs: []types.Type{types.Typ[types.Int8]}, + }, + Instance{ + Object: lookupObj("entry1.y"), + TArgs: []types.Type{lookupType("A")}, + }, + ), }, { descr: "generic function", resolver: NewResolver( types.NewContext(), - ToSlice(lookupType("entry2").(*types.Signature).TypeParams()), - []types.Type{lookupType("B")}, - ), + Instance{ + Object: lookupObj("entry2"), + TArgs: []types.Type{lookupType("B")}, + }), node: lookupDecl("entry2"), - want: instancesInFunc(lookupType("B")), + want: append( + instancesInFunc(lookupType("B")), + Instance{ + Object: lookupObj("entry2.x"), + TNest: []types.Type{lookupType("B")}, + }, + Instance{ + Object: lookupObj("entry1.y"), + TNest: []types.Type{lookupType("B")}, + TArgs: []types.Type{types.Typ[types.Int8]}, + }, + Instance{ + Object: lookupObj("entry2.y"), + TNest: []types.Type{lookupType("B")}, + TArgs: []types.Type{lookupType("B")}, + }, + ), }, { descr: "generic method", resolver: NewResolver( types.NewContext(), - ToSlice(lookupType("entry3.method").(*types.Signature).RecvTypeParams()), - []types.Type{lookupType("C")}, - ), + Instance{ + Object: lookupObj("entry3.method"), + TArgs: []types.Type{lookupType("C")}, + }), node: lookupDecl("entry3.method"), want: append( instancesInFunc(lookupType("C")), @@ -217,14 +260,29 @@ func TestVisitor(t *testing.T) { Object: lookupObj("entry3.method"), TArgs: []types.Type{lookupType("C")}, }, + Instance{ + Object: lookupObj("entry3.method.x"), + TNest: []types.Type{lookupType("C")}, + }, + Instance{ + Object: lookupObj("entry3.method.y"), + TNest: []types.Type{lookupType("C")}, + TArgs: []types.Type{types.Typ[types.Int8]}, + }, + Instance{ + Object: lookupObj("entry3.method.y"), + TNest: []types.Type{lookupType("C")}, + TArgs: []types.Type{lookupType("C")}, + }, ), }, { descr: "generic type declaration", resolver: NewResolver( types.NewContext(), - ToSlice(lookupType("entry3").(*types.Named).TypeParams()), - []types.Type{lookupType("D")}, - ), + Instance{ + Object: lookupObj("entry3"), + TArgs: []types.Type{lookupType("D")}, + }), node: lookupDecl("entry3"), want: instancesInType(lookupType("D")), }, { @@ -256,6 +314,12 @@ func TestVisitor(t *testing.T) { resolver: test.resolver, info: info, } + if test.resolver != nil { + // Since we know all the tests are for functions and methods, + // set the nested type to the type parameter from the resolver. + v.nestTParams = test.resolver.tParams + v.nestTArgs = test.resolver.tArgs + } ast.Walk(&v, test.node) got := v.instances.Pkg(pkg).Values() if diff := cmp.Diff(test.want, got, instanceOpts()); diff != "" { @@ -319,7 +383,7 @@ func TestSeedVisitor(t *testing.T) { } 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) + t.Errorf("Instances from seedVisitor contain diff (-want,+got):\n%s", diff) } } @@ -348,33 +412,478 @@ func TestCollector(t *testing.T) { c := Collector{ TContext: types.NewContext(), - Info: info, Instances: &PackageInstanceSets{}, } - c.Scan(pkg, file) + c.Scan(info, pkg, file) + c.Finish() - inst := func(name string, tArg types.Type) Instance { + inst := func(name, tNest, tArg string) Instance { return Instance{ Object: srctesting.LookupObj(pkg, name), - TArgs: []types.Type{tArg}, + TNest: evalTypeArgs(t, f.FileSet, pkg, tNest), + TArgs: evalTypeArgs(t, f.FileSet, pkg, 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]), + inst(`typ`, ``, `int`), + inst(`typ.method`, ``, `int`), + inst(`fun`, ``, `int8`), + inst(`fun.nested`, `int8`, `int8`), + inst(`typ`, ``, `int16`), + inst(`typ.method`, ``, `int16`), + inst(`typ`, ``, `int32`), + inst(`typ.method`, ``, `int32`), + inst(`fun`, ``, `int64`), + inst(`fun.nested`, `int64`, `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) + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_MoreNesting(t *testing.T) { + src := `package test + + func fun[T any]() { + type nestedCon struct{ X T } + _ = nestedCon{} + + type nestedGen[U any] struct{ Y T; Z U } + _ = nestedGen[T]{} + _ = nestedGen[int8]{} + + type nestedCover[T any] struct{ W T } + _ = nestedCover[T]{} + _ = nestedCover[int16]{} + } + + func a() { + fun[int32]() + fun[int64]() + } + ` + + f := srctesting.New(t) + file := f.Parse(`test.go`, src) + info, pkg := f.Check(`pkg/test`, file) + + c := Collector{ + TContext: types.NewContext(), + Instances: &PackageInstanceSets{}, + } + c.Scan(info, pkg, file) + c.Finish() + + inst := func(name, tNest, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TNest: evalTypeArgs(t, f.FileSet, pkg, tNest), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } + want := []Instance{ + inst(`fun`, ``, `int32`), + inst(`fun`, ``, `int64`), + + inst(`fun.nestedCon`, `int32`, ``), + inst(`fun.nestedCon`, `int64`, ``), + + inst(`fun.nestedGen`, `int32`, `int32`), + inst(`fun.nestedGen`, `int32`, `int8`), + inst(`fun.nestedGen`, `int64`, `int64`), + inst(`fun.nestedGen`, `int64`, `int8`), + + inst(`fun.nestedCover`, `int32`, `int32`), + inst(`fun.nestedCover`, `int32`, `int16`), + inst(`fun.nestedCover`, `int64`, `int64`), + inst(`fun.nestedCover`, `int64`, `int16`), + } + got := c.Instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_NestingWithVars(t *testing.T) { + // This is loosely based off of go1.19.13/test/typeparam/issue47740b.go + // I was getting an error where `Q.print[int;]` was showing up when + // `Q.print` is not in a nesting context with `int` and this helped debug + // it. The problem was that `q` was being treated like a type not a var. + src := `package test + + type Q struct{ v any } + func (q Q) print() { + println(q.v) + } + + func newQ(v any) Q { + return Q{v} + } + + type S[T any] struct{ x T } + func (s S[T]) echo() { + q := newQ(s.x) + q.print() + } + + func a() { + s := S[int]{x: 0} + s.echo() + } + ` + + f := srctesting.New(t) + file := f.Parse(`test.go`, src) + info, pkg := f.Check(`pkg/test`, file) + inst := func(name, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } + + c := Collector{ + TContext: types.NewContext(), + Instances: &PackageInstanceSets{}, + } + c.Scan(info, pkg, file) + c.Finish() + + want := []Instance{ + inst(`S`, `int`), + inst(`S.echo`, `int`), + } + got := c.Instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_RecursiveTypeParams(t *testing.T) { + // This is based off of part of go1.19.13/test/typeparam/nested.go + src := `package test + func F[A any]() {} + func main() { + type U[_ any] int + type X[A any] U[X[A]] + F[X[int]]() + } + ` + + f := srctesting.New(t) + tc := types.NewContext() + file := f.Parse(`test.go`, src) + info, pkg := f.Check(`test`, file) + inst := func(name, tNest, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TNest: evalTypeArgs(t, f.FileSet, pkg, tNest), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } + + c := Collector{ + TContext: tc, + Instances: &PackageInstanceSets{}, + } + c.Scan(info, pkg, file) + c.Finish() + + xInst := inst(`main.X`, ``, `int`) + xInt := xInst.Resolve(tc) + want := []Instance{ + xInst, + { + Object: srctesting.LookupObj(pkg, `F`), + TArgs: []types.Type{xInt}, + }, + { + Object: srctesting.LookupObj(pkg, `main.U`), + TArgs: []types.Type{xInt}, + }, + } + got := c.Instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_NestedRecursiveTypeParams(t *testing.T) { + // This is based off of part of go1.19.13/test/typeparam/nested.go + src := `package test + func F[A any]() any { + type U[_ any] struct{ x A } + type X[B any] U[X[B]] + return X[int]{} + } + func main() { + print(F[string]()) + } + ` + + f := srctesting.New(t) + tc := types.NewContext() + file := f.Parse(`test.go`, src) + info, pkg := f.Check(`test`, file) + inst := func(name, tNest, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TNest: evalTypeArgs(t, f.FileSet, pkg, tNest), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } + + c := Collector{ + TContext: tc, + Instances: &PackageInstanceSets{}, + } + c.Scan(info, pkg, file) + c.Finish() + + xInst := inst(`F.X`, `string`, `int`) + xInt := xInst.Resolve(tc) + want := []Instance{ + inst(`F`, ``, `string`), + xInst, + { + Object: srctesting.LookupObj(pkg, `F.U`), + TNest: []types.Type{types.Typ[types.String]}, + TArgs: []types.Type{xInt}, + }, + } + got := c.Instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_LooselyRecursiveTypeParams(t *testing.T) { + // This is based off of part of go1.19.13/test/typeparam/nested.go + src := `package test + func main() { + type U[B any] struct{ y *B } + type X[C any] struct{ p U[X[C]] } + print(X[int]{}) + } + ` + + f := srctesting.New(t) + tc := types.NewContext() + file := f.Parse(`test.go`, src) + info, pkg := f.Check(`test`, file) + + c := Collector{ + TContext: tc, + Instances: &PackageInstanceSets{}, + } + c.Scan(info, pkg, file) + c.Finish() + + xInst := Instance{ + Object: srctesting.LookupObj(pkg, `main.X`), + TArgs: []types.Type{types.Typ[types.Int]}, + } + xInt := xInst.Resolve(tc) + want := []Instance{ + xInst, + { + Object: srctesting.LookupObj(pkg, `main.U`), + TArgs: []types.Type{xInt}, + }, + } + got := c.Instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_NestedTypeParams(t *testing.T) { + // This is based off of part of go1.19.13/test/typeparam/nested.go + src := `package test + func F[A any]() any { + type T[B any] struct{} + type U[_ any] struct{ X A } + return T[U[bool]]{} + } + func main() { + print(F[int]()) + } + ` + + f := srctesting.New(t) + tc := types.NewContext() + file := f.Parse(`test.go`, src) + info, pkg := f.Check(`test`, file) + inst := func(name, tNest, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TNest: evalTypeArgs(t, f.FileSet, pkg, tNest), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } + + c := Collector{ + TContext: tc, + Instances: &PackageInstanceSets{}, + } + c.Scan(info, pkg, file) + c.Finish() + + uInst := inst(`F.U`, `int`, `bool`) + uIntBool := uInst.Resolve(tc) + want := []Instance{ + inst(`F`, ``, `int`), + inst(`F.U`, `int`, `bool`), + { + Object: srctesting.LookupObj(pkg, `F.T`), + TNest: []types.Type{types.Typ[types.Int]}, + TArgs: []types.Type{uIntBool}, + }, + } + got := c.Instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_ImplicitTypeInstance(t *testing.T) { + f := srctesting.New(t) + tc := types.NewContext() + inst := func(pkg *types.Package, name, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } + + fooSrc := `package foo + type Foo[T any] struct{} + func NewFoo[T any]() *Foo[T] { + return &Foo[T]{} + }` + fooFile := f.Parse(`foo/foo.go`, fooSrc) + _, fooPkg := f.Check(`foo`, fooFile) + + mainSrc := `package test + import "foo" + func main() { + print(foo.NewFoo[int]()) + }` + mainFile := f.Parse(`main.go`, mainSrc) + _, mainPkg := f.Check(`test`, mainFile) + + c := Collector{ + TContext: tc, + Instances: &PackageInstanceSets{}, + } + // The issue which caused this test to be written only occurred when + // fooFile was scanned before mainFile, otherwise it would work fine. + // The problem was that `foo.Foo[int]` was not being collected in this + // order because mainFile adds an instance into the instance set for "foo" + // after "foo" was already propagated. This was fixed by performing + // propagation after all the packages are scanned via `Finish`. + c.Scan(f.Info, fooPkg, fooFile) + c.Scan(f.Info, mainPkg, mainFile) + c.Finish() + + want := []Instance{ + inst(fooPkg, `NewFoo`, `int`), + inst(fooPkg, `Foo`, `int`), + } + got := c.Instances.Pkg(fooPkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_MoreImplicitTypeInstance(t *testing.T) { + f := srctesting.New(t) + tc := types.NewContext() + inst := func(pkg *types.Package, name, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } + + fooSrc := `package foo + type Foo[T, U any] struct{} + func NewFoo[T, U any]() *Foo[T, U] { + return &Foo[T, U]{} + }` + fooFile := f.Parse(`foo/foo.go`, fooSrc) + _, fooPkg := f.Check(`foo`, fooFile) + + barSrc := `package bar + import "foo" + func Bar[T, U any](f *foo.Foo[T, U]) *foo.Foo[U, T] { + return foo.NewFoo[U, T]() + }` + barFile := f.Parse(`bar/bar.go`, barSrc) + _, barPkg := f.Check(`bar`, barFile) + + mainSrc := `package test + import "foo" + import "bar" + func main() { + f := foo.NewFoo[int, string]() + print(bar.Bar[int, string](f)) + }` + mainFile := f.Parse(`main.go`, mainSrc) + _, mainPkg := f.Check(`test`, mainFile) + + want := []Instance{ + inst(fooPkg, `NewFoo`, `int, string`), + inst(fooPkg, `NewFoo`, `string, int`), + inst(fooPkg, `Foo`, `int, string`), + inst(fooPkg, `Foo`, `string, int`), + } + trial := func(order ...int) { + c := Collector{ + TContext: tc, + Instances: &PackageInstanceSets{}, + } + for _, i := range order { + switch i { + case 1: + c.Scan(f.Info, fooPkg, fooFile) + case 2: + c.Scan(f.Info, barPkg, barFile) + case 3: + c.Scan(f.Info, mainPkg, mainFile) + } + } + c.Finish() + + got := c.Instances.Pkg(fooPkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector trial %v contain diff (-want,+got):\n%s", order, diff) + } + } + + trial(1, 2, 3) + trial(1, 3, 2) + trial(2, 1, 3) + trial(2, 3, 1) + trial(3, 1, 2) + trial(3, 2, 1) +} + +func evalTypeArgs(t *testing.T, fSet *token.FileSet, pkg *types.Package, expr string) []types.Type { + if len(expr) == 0 { + return nil + } + args := strings.Split(expr, ",") + targs := make([]types.Type, 0, len(args)) + for _, astr := range 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) } + return targs } func TestCollector_CrossPackage(t *testing.T) { @@ -408,11 +917,11 @@ func TestCollector_CrossPackage(t *testing.T) { c := Collector{ TContext: types.NewContext(), - Info: f.Info, Instances: &PackageInstanceSets{}, } - c.Scan(barPkg, barFile) - c.Scan(fooPkg, fooFile) + c.Scan(f.Info, barPkg, barFile) + c.Scan(f.Info, fooPkg, fooFile) + c.Finish() inst := func(pkg *types.Package, name string, tArg types.BasicKind) Instance { return Instance{ @@ -491,8 +1000,10 @@ func TestResolver_SubstituteSelection(t *testing.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()}) + resolver := NewResolver(nil, Instance{ + Object: srctesting.LookupObj(pkg, "g.Method"), + TArgs: []types.Type{srctesting.LookupObj(pkg, "x").Type()}, + }) if l := len(info.Selections); l != 1 { t.Fatalf("Got: %d selections. Want: 1", l) diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index f847a9825..438239707 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -3,6 +3,7 @@ package typeparams import ( "fmt" "go/types" + "strings" "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/gopherjs/gopherjs/compiler/typesutil" @@ -15,39 +16,82 @@ import ( type Instance struct { Object types.Object // Object to be instantiated. TArgs typesutil.TypeList // Type params to instantiate with. + + // TNest is the type params of the function this object was nested with-in. + // e.g. In `func A[X any]() { type B[Y any] struct {} }` the `X` + // from `A` is the context of `B[Y]` thus creating `B[X;Y]`. + TNest typesutil.TypeList } // 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 +func (i Instance) String() string { + return i.symbolicName() + i.TypeParamsString(`<`, `>`) +} + +// TypeString returns a Go type string representing the instance (suitable for %T verb). +func (i Instance) TypeString() string { + return i.qualifiedName() + i.TypeParamsString(`[`, `]`) +} + +// symbolicName returns a string representation of the instance's name +// including the package name and pointer indicators but +// excluding the type parameters. +func (i Instance) symbolicName() string { + if i.Object == nil { + return `` } + return symbol.New(i.Object).String() +} - return fmt.Sprintf("%s<%s>", sym, i.TArgs) +// qualifiedName returns a string representation of the instance's name +// including the package name but +// excluding the type parameters and pointer indicators. +func (i Instance) qualifiedName() string { + if i.Object == nil { + return `` + } + if i.Object.Pkg() == nil { + return i.Object.Name() + } + return fmt.Sprintf("%s.%s", i.Object.Pkg().Name(), i.Object.Name()) } -// 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() + "]" +// TypeParamsString returns part of a Go type string that represents the type +// parameters of the instance including the nesting type parameters, e.g. [X;Y,Z]. +func (i Instance) TypeParamsString(open, close string) string { + hasNest := len(i.TNest) > 0 + hasArgs := len(i.TArgs) > 0 + buf := strings.Builder{} + if hasNest || hasArgs { + buf.WriteString(open) + if hasNest { + buf.WriteString(i.TNest.String()) + buf.WriteRune(';') + if hasArgs { + buf.WriteRune(' ') + } + } + if hasArgs { + buf.WriteString(i.TArgs.String()) + } + buf.WriteString(close) } - return fmt.Sprintf("%s.%s%s", i.Object.Pkg().Name(), i.Object.Name(), tArgs) + return buf.String() } -// IsTrivial returns true if this is an instance of a non-generic object. -func (i *Instance) IsTrivial() bool { - return len(i.TArgs) == 0 +// IsTrivial returns true if this is an instance of a non-generic object +// and it is not nested in a generic function. +func (i Instance) IsTrivial() bool { + return len(i.TArgs) == 0 && len(i.TNest) == 0 } // Recv returns an instance of the receiver type of a method. // // Returns zero value if not a method. -func (i *Instance) Recv() Instance { +func (i Instance) Recv() Instance { sig, ok := i.Object.Type().(*types.Signature) if !ok { return Instance{} @@ -62,6 +106,33 @@ func (i *Instance) Recv() Instance { } } +// Resolve instantiates and performs a substitution of the instance +// to get the concrete type or function. +// This will panic if the instance is not valid, e.g. if there are a different +// number of type arguments than the type parameters. +// +// If `tc` is non-nil, it de-duplicates the instance against previous +// instances with the same identity. See types.Instantiate for more info. +// +// Instances of named types may be lazily substituted, meaning the underlying +// type may not be fully substituted with the type arguments when returned. +// +// This is useful for quickly resolving an instance for a test or for debugging +// but this uses a temporary Resolver that will not be reused. +// When resolving several instances in the same context, it is more efficient +// to use NewResolver to take advantage of caching. +func (i Instance) Resolve(tc *types.Context) types.Type { + instType := i.Object.Type() + if len(i.TArgs) > 0 { + var err error + instType, err = types.Instantiate(tc, instType, i.TArgs, true) + if err != nil { + panic(fmt.Errorf("failed to instantiate %v: %w", i, err)) + } + } + return NewResolver(tc, i).Substitute(instType) +} + // InstanceSet allows collecting and processing unique Instances. // // Each Instance may be added to the set any number of times, but it will be @@ -131,7 +202,9 @@ func (iset *InstanceSet) next() (Instance, bool) { } // exhausted returns true if there are no unprocessed instances in the set. -func (iset *InstanceSet) exhausted() bool { return len(iset.values) <= iset.unprocessed } +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 { @@ -147,10 +220,44 @@ func (iset *InstanceSet) ByObj() map[types.Object][]Instance { return result } +// ForObj returns the instances that belong to the given object type. +// Order is not specified. This returns the same values as `ByObj()[obj]`. +func (iset *InstanceSet) ForObj(obj types.Object) []Instance { + result := []Instance{} + for _, inst := range iset.values { + if inst.Object == obj { + result = append(result, inst) + } + } + return result +} + +// ObjHasInstances returns true if there are any instances (either trivial +// or non-trivial) that belong to the given object type, otherwise false. +func (iset *InstanceSet) ObjHasInstances(obj types.Object) bool { + for _, inst := range iset.values { + if inst.Object == obj { + return true + } + } + return false +} + // PackageInstanceSets stores an InstanceSet for each package in a program, keyed // by import path. type PackageInstanceSets map[string]*InstanceSet +// allExhausted returns true if there are no unprocessed instances in +// any of the instance sets. +func (i PackageInstanceSets) allExhausted() bool { + for _, iset := range i { + if !iset.exhausted() { + return false + } + } + return true +} + // Pkg returns InstanceSet for objects defined in the given package. func (i PackageInstanceSets) Pkg(pkg *types.Package) *InstanceSet { path := pkg.Path() diff --git a/compiler/internal/typeparams/map.go b/compiler/internal/typeparams/map.go index aa16130e2..7edbdc016 100644 --- a/compiler/internal/typeparams/map.go +++ b/compiler/internal/typeparams/map.go @@ -1,8 +1,10 @@ package typeparams import ( + "fmt" "go/types" - "sync" + "sort" + "strings" "golang.org/x/tools/go/types/typeutil" ) @@ -25,40 +27,35 @@ type ( // 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. +// [golang.org/x/tools/go/types/typeutil#Map] type InstanceMap[V any] struct { - bootstrap sync.Once - data map[types.Object]mapBuckets[V] - len int - hasher typeutil.Hasher - zero V + data map[types.Object]mapBuckets[V] + len int + hasher typeutil.Hasher } -func (im *InstanceMap[V]) init() { - im.bootstrap.Do(func() { - im.data = map[types.Object]mapBuckets[V]{} - im.hasher = typeutil.MakeHasher() - }) +// findIndex returns bucket and index of the entry with the given key. +// If the given key isn't found, an empty bucket and -1 are returned. +func (im *InstanceMap[V]) findIndex(key Instance) (mapBucket[V], int) { + if im != nil && im.data != nil { + bucket := im.data[key.Object][typeHash(im.hasher, key.TNest, key.TArgs)] + for i, candidate := range bucket { + if candidateArgsMatch(key, candidate) { + return bucket, i + } + } + } + return nil, -1 } +// get returns the stored value for the provided key and +// a bool indicating whether the key was present in the map or not. func (im *InstanceMap[V]) get(key Instance) (V, bool) { - im.init() - - buckets, ok := im.data[key.Object] - if !ok { - return im.zero, false + if bucket, i := im.findIndex(key); i >= 0 { + return bucket[i].value, true } - 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 + var zero V + return zero, false } // Get returns the stored value for the provided key. If the key is missing from @@ -76,35 +73,119 @@ func (im *InstanceMap[V]) Has(key Instance) bool { // 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() +func (im *InstanceMap[V]) Set(key Instance, value V) V { + if im.data == nil { + im.data = map[types.Object]mapBuckets[V]{} + im.hasher = typeutil.MakeHasher() + } if _, ok := im.data[key.Object]; !ok { im.data[key.Object] = mapBuckets[V]{} } - bucketID := typeHash(im.hasher, key.TArgs...) + bucketID := typeHash(im.hasher, key.TNest, 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 + hole := -1 + bucket := im.data[key.Object][bucketID] + for i, candidate := range bucket { + if candidate == nil { + hole = i + } else if candidateArgsMatch(key, candidate) { + 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, - }) + // If there is a hole in the bucket, reuse it. + if hole >= 0 { + im.data[key.Object][bucketID][hole] = &mapEntry[V]{ + key: key, + value: value, + } + } else { + // Otherwise append a new entry. + im.data[key.Object][bucketID] = append(bucket, &mapEntry[V]{ + key: key, + value: value, + }) + } im.len++ - return im.zero + var zero V + return zero } // Len returns the number of elements in the map. func (im *InstanceMap[V]) Len() int { - return im.len + if im != nil { + return im.len + } + return 0 +} + +// Delete removes the entry with the given key, if any. +// It returns true if the entry was found. +func (im *InstanceMap[V]) Delete(key Instance) bool { + if bucket, i := im.findIndex(key); i >= 0 { + // We can't compact the bucket as it + // would disturb iterators. + bucket[i] = nil + im.len-- + return true + } + return false +} + +// Iterate calls function f on each entry in the map in unspecified order. +// +// Return true from f to continue the iteration, or false to stop it. +// +// If f should mutate the map, Iterate provides the same guarantees as +// Go maps: if f deletes a map entry that Iterate has not yet reached, +// f will not be invoked for it, but if f inserts a map entry that +// Iterate has not yet reached, whether or not f will be invoked for +// it is unspecified. +func (im *InstanceMap[V]) Iterate(f func(key Instance, value V)) { + if im != nil && im.data != nil { + for _, mapBucket := range im.data { + for _, bucket := range mapBucket { + for _, e := range bucket { + if e != nil { + f(e.key, e.value) + } + } + } + } + } +} + +// Keys returns a new slice containing the set of map keys. +// The order is unspecified. +func (im *InstanceMap[V]) Keys() []Instance { + keys := make([]Instance, 0, im.Len()) + im.Iterate(func(key Instance, _ V) { + keys = append(keys, key) + }) + return keys +} + +// String returns a string representation of the map's entries. +// The entries are sorted by string representation of the entry. +func (im *InstanceMap[V]) String() string { + entries := make([]string, 0, im.Len()) + im.Iterate(func(key Instance, value V) { + entries = append(entries, fmt.Sprintf("%v:%v", key, value)) + }) + sort.Strings(entries) + return `{` + strings.Join(entries, `, `) + `}` +} + +// candidateArgsMatch checks if the candidate entry has the same type +// arguments as the given key. +func candidateArgsMatch[V any](key Instance, candidate *mapEntry[V]) bool { + return candidate != nil && + candidate.key.TNest.Equal(key.TNest) && + candidate.key.TArgs.Equal(key.TArgs) } // typeHash returns a combined hash of several types. @@ -112,24 +193,13 @@ func (im *InstanceMap[V]) Len() int { // 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 { +func typeHash(hasher typeutil.Hasher, nestTypes, types []types.Type) uint32 { var hash uint32 + for _, typ := range nestTypes { + hash ^= hasher.Hash(typ) + } 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 index 5018ab0d8..d67a1884d 100644 --- a/compiler/internal/typeparams/map_test.go +++ b/compiler/internal/typeparams/map_test.go @@ -7,8 +7,10 @@ import ( ) func TestInstanceMap(t *testing.T) { + pkg := types.NewPackage(`testPkg`, `testPkg`) + i1 := Instance{ - Object: types.NewTypeName(token.NoPos, nil, "i1", nil), + Object: types.NewTypeName(token.NoPos, pkg, "i1", nil), TArgs: []types.Type{ types.Typ[types.Int], types.Typ[types.Int8], @@ -23,7 +25,7 @@ func TestInstanceMap(t *testing.T) { } i2 := Instance{ - Object: types.NewTypeName(token.NoPos, nil, "i2", nil), // Different pointer. + Object: types.NewTypeName(token.NoPos, pkg, "i2", nil), // Different pointer. TArgs: []types.Type{ types.Typ[types.Int], types.Typ[types.Int8], @@ -31,18 +33,30 @@ func TestInstanceMap(t *testing.T) { } i3 := Instance{ Object: i1.Object, - TArgs: []types.Type{types.Typ[types.String]}, // Different type args. + TArgs: []types.Type{ // Different type args, same number. + types.Typ[types.Int], + types.Typ[types.Int], + }, + } + i4 := Instance{ + Object: i1.Object, + TArgs: []types.Type{ // This hash matches i3's hash. + types.Typ[types.String], + types.Typ[types.String], + }, + } + i5 := Instance{ + Object: i1.Object, + TArgs: []types.Type{}, // This hash matches i3's hash. } - - _ = i1 - _ = i1clone - _ = i3 - _ = i2 m := InstanceMap[string]{} // Check operations on a missing key. t.Run("empty", func(t *testing.T) { + if got, want := m.String(), `{}`; got != want { + t.Errorf("Got: empty map string %q. Want: map string %q.", got, want) + } if got := m.Has(i1); got { t.Errorf("Got: empty map contains %s. Want: empty map contains nothing.", i1) } @@ -58,6 +72,12 @@ func TestInstanceMap(t *testing.T) { if got := m.Len(); got != 1 { t.Errorf("Got: map length %d. Want: 1.", got) } + if got, want := m.String(), `{testPkg.i1:abc}`; got != want { + t.Errorf("Got: map string %q. Want: map string %q.", got, want) + } + if got, want := m.Keys(), []Instance{i1}; !keysMatch(got, want) { + t.Errorf("Got: map keys %v. Want: [i1].", got) + } }) // Check operations on the existing key. @@ -77,9 +97,15 @@ func TestInstanceMap(t *testing.T) { if got := m.Get(i1clone); got != "def" { t.Errorf(`Got: getting set key returned %q. Want: "def"`, got) } + if got, want := m.String(), `{testPkg.i1:def}`; got != want { + t.Errorf("Got: map string %q. Want: map string %q.", got, want) + } + if got, want := m.Keys(), []Instance{i1}; !keysMatch(got, want) { + t.Errorf("Got: map keys %v. Want: [i1].", got) + } }) - // Check for key collisions. + // Check for key collisions with different object pointer. 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) @@ -94,6 +120,8 @@ func TestInstanceMap(t *testing.T) { t.Errorf("Got: map length %d. Want: 2.", got) } }) + + // Check for collisions with different type arguments and different hash. 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) @@ -108,4 +136,192 @@ func TestInstanceMap(t *testing.T) { t.Errorf("Got: map length %d. Want: 3.", got) } }) + + // Check for collisions with different type arguments, same hash, count. + t.Run("different tArgs hash", func(t *testing.T) { + if got := m.Has(i4); got { + t.Errorf("Got: a new key %q is reported as present. Want: not present.", i3) + } + if got := m.Set(i4, "789"); got != "" { + t.Errorf("Got: a new key %q overrode an old value %q. Want: zero value.", i3, got) + } + if got := m.Get(i4); got != "789" { + t.Errorf(`Got: getting set key %q returned: %q. Want: "789"`, i3, got) + } + if got := m.Len(); got != 4 { + t.Errorf("Got: map length %d. Want: 4.", got) + } + }) + + // Check for collisions with different type arguments and same hash, but different count. + t.Run("different tArgs count", func(t *testing.T) { + if got := m.Has(i5); got { + t.Errorf("Got: a new key %q is reported as present. Want: not present.", i3) + } + if got := m.Set(i5, "ghi"); got != "" { + t.Errorf("Got: a new key %q overrode an old value %q. Want: zero value.", i3, got) + } + if got := m.Get(i5); got != "ghi" { + t.Errorf(`Got: getting set key %q returned: %q. Want: "ghi"`, i3, got) + } + if got := m.Len(); got != 5 { + t.Errorf("Got: map length %d. Want: 5.", got) + } + if got, want := m.String(), `{testPkg.i1:ghi, testPkg.i1:def, testPkg.i1:456, testPkg.i1:789, testPkg.i2:123}`; got != want { + t.Errorf("Got: map string %q. Want: map string %q.", got, want) + } + if got, want := m.Keys(), []Instance{i1, i2, i3, i4, i5}; !keysMatch(got, want) { + t.Errorf("Got: map keys %v. Want: [i1, i2, i3, i4, i5].", got) + } + }) + + // Check an existing entry can be deleted. + t.Run("delete existing", func(t *testing.T) { + if got := m.Delete(i3); !got { + t.Errorf("Got: deleting existing key %q returned not deleted. Want: found and deleted.", i3) + } + if got := m.Len(); got != 4 { + t.Errorf("Got: map length %d. Want: 4.", got) + } + if got := m.Has(i3); got { + t.Errorf("Got: a deleted key %q is reported as present. Want: not present.", i3) + } + if got, want := m.Keys(), []Instance{i1, i2, i4, i5}; !keysMatch(got, want) { + t.Errorf("Got: map keys %v. Want: [i1, i2, i4, i5].", got) + } + }) + + // Check deleting an existing entry has no effect. + t.Run("delete already deleted", func(t *testing.T) { + if got := m.Delete(i3); got { + t.Errorf("Got: deleting not present key %q returned as deleted. Want: not found.", i3) + } + if got := m.Len(); got != 4 { + t.Errorf("Got: map length %d. Want: 4.", got) + } + if got, want := m.Keys(), []Instance{i1, i2, i4, i5}; !keysMatch(got, want) { + t.Errorf("Got: map keys %v. Want: [i1, i2, i4, i5].", got) + } + }) + + // Check adding back a deleted value works (should fill hole in bucket). + t.Run("set deleted key", func(t *testing.T) { + if got := m.Set(i3, "jkl"); got != "" { + t.Errorf("Got: a new key %q overrode an old value %q. Want: zero value.", i3, got) + } + if got := m.Len(); got != 5 { + t.Errorf("Got: map length %d. Want: 5.", got) + } + if got, want := m.Keys(), []Instance{i1, i2, i3, i4, i5}; !keysMatch(got, want) { + t.Errorf("Got: map keys %v. Want: [i1, i2, i3, i4, i5].", got) + } + }) + + // Check deleting while iterating over the map. + t.Run("deleting while iterating", func(t *testing.T) { + notSeen := []Instance{i1, i2, i3, i4, i5} + seen := []Instance{} + kept := []Instance{} + var skipped Instance + m.Iterate(func(key Instance, value string) { + // update seen and not seen + seen = append(seen, key) + i := keyAt(notSeen, key) + if i < 0 { + t.Fatalf(`Got: failed to find current key %q in not seen. Want: it to be not seen yet.`, key) + } + notSeen = append(notSeen[:i], notSeen[i+1:]...) + + if len(seen) == 3 { + // delete the first seen key, the current key, and an unseen key + if got := m.Delete(seen[0]); !got { + t.Errorf("Got: deleting seen key %q returned not deleted. Want: found and deleted.", seen[0]) + } + if got := m.Delete(key); !got { + t.Errorf("Got: deleting current key %q returned not deleted. Want: found and deleted.", key) + } + skipped = notSeen[0] // skipped has not yet been seen so it should not be iterated over + if got := m.Delete(skipped); !got { + t.Errorf("Got: deleting not seen key %q returned not deleted. Want: found and deleted.", skipped) + } + kept = append(kept, seen[1], notSeen[1]) + } + }) + + if got := len(seen); got != 4 { + t.Errorf("Got: seen %d keys. Want: 4.", got) + } + if got := len(notSeen); got != 1 { + t.Errorf("Got: seen %d keys. Want: 1.", got) + } + if got := keyAt(notSeen, skipped); got != 0 { + t.Errorf("Got: a deleted unseen key %q was not the skipped key %q. Want: it to be skipped.", notSeen[0], skipped) + } + if got := m.Len(); got != 2 { + t.Errorf("Got: map length %d. Want: 2.", got) + } + if got := m.Keys(); !keysMatch(got, kept) { + t.Errorf("Got: map keys %v did not match kept keys. Want: %v.", got, kept) + } + }) +} + +func TestNilInstanceMap(t *testing.T) { + i1 := Instance{ + Object: types.NewTypeName(token.NoPos, nil, "i1", nil), + TArgs: []types.Type{ + types.Typ[types.Int], + types.Typ[types.Int8], + }, + } + + var m *InstanceMap[string] + if got, want := m.String(), `{}`; got != want { + t.Errorf("Got: nil map string %q. Want: map string %q.", got, want) + } + if got := m.Has(i1); got { + t.Errorf("Got: nil map contains %s. Want: nil map contains nothing.", i1) + } + if got := m.Get(i1); got != "" { + t.Errorf("Got: missing key returned %q. Want: zero value.", got) + } + if got := m.Len(); got != 0 { + t.Errorf("Got: nil map length %d. Want: 0.", got) + } + if got := m.Keys(); len(got) > 0 { + t.Errorf("Got: map keys %v did not match kept keys. Want: [].", got) + } + + // The only thing that a nil map can't safely handle is setting a key. + func() { + defer func() { + recover() + }() + m.Set(i1, "abc") + t.Errorf("Got: setting a new key on nil map did not panic, %s. Want: panic.", m.String()) + }() +} + +func keysMatch(a, b []Instance) bool { + if len(a) != len(b) { + return false + } + found := make([]bool, len(b)) + for _, v := range a { + i := keyAt(b, v) + if i < 0 || found[i] { + return false + } + found[i] = true + } + return true +} + +func keyAt(keys []Instance, target Instance) int { + for i, v := range keys { + if v.Object == target.Object && v.TArgs.Equal(target.TArgs) { + return i + } + } + return -1 } diff --git a/compiler/internal/typeparams/resolver.go b/compiler/internal/typeparams/resolver.go new file mode 100644 index 000000000..5718c364d --- /dev/null +++ b/compiler/internal/typeparams/resolver.go @@ -0,0 +1,215 @@ +package typeparams + +import ( + "fmt" + "go/types" + "sort" + "strings" + + "github.com/gopherjs/gopherjs/compiler/typesutil" + "github.com/gopherjs/gopherjs/internal/govendor/subst" +) + +// Resolver translates types defined in terms of type parameters into concrete +// types, given a root instance. The root instance provides context for mapping +// from type parameters to type arguments so that the resolver can substitute +// any type parameters used in types to the corresponding type arguments. +// +// In some cases, a generic type may not be able to be fully instantiated. +// Generic named types that have no type arguments applied will have the +// type parameters substituted, however the type arguments will not be +// applied to instantiate the named type. +// +// For example, given `func Foo[T any]() { type Bar[U *T] struct { x T; y U } }`, +// and if `Foo[int]` is used as the root for the resolver, then `Bar[U *T]` will +// be substituted to create the generic `Bar[U *int] struct { x int; y U }`. +// Alternatively, the instantiated but still generic because of the `T`, +// `Bar[bool] struct { x T; y bool}` will be substituted for `Foo[int]` to +// create the concrete `Bar[bool] struct { x int; y bool }`. +// +// Typically the instantiated type from `info.Instances` should be substituted +// to resolve the implicit nesting types and create a concrete type. +// See internal/govendor/subst/subst.go for more details. +type Resolver struct { + tParams *types.TypeParamList + tArgs []types.Type + nest *types.Func + nestTParams *types.TypeParamList + nestTArgs []types.Type + replacements map[*types.TypeParam]types.Type + root Instance + + // subster is the substitution helper that will perform the actual + // substitutions. This maybe nil when there are no substitutions but + // will still be usable when nil. + subster *subst.Subster + selMemo map[typesutil.Selection]typesutil.Selection +} + +// NewResolver creates a new Resolver that will substitute type parameters +// with the type arguments as defined in the provided Instance. +func NewResolver(tc *types.Context, root Instance) *Resolver { + var ( + nest *types.Func + nestTParams *types.TypeParamList + tParams *types.TypeParamList + replacements = map[*types.TypeParam]types.Type{} + ) + + switch typ := root.Object.Type().(type) { + case *types.Signature: + nest = root.Object.(*types.Func) + tParams = SignatureTypeParams(typ) + case *types.Named: + tParams = typ.TypeParams() + nest = FindNestingFunc(root.Object) + if nest != nil { + nestTParams = SignatureTypeParams(nest.Type().(*types.Signature)) + } + default: + panic(fmt.Errorf("unexpected type %T for object %s", typ, root.Object)) + } + + // Check the root's implicit nesting type parameters and arguments match, + // then add them to the replacements. + if nestTParams.Len() != len(root.TNest) { + panic(fmt.Errorf(`number of nesting type parameters and arguments must match: %d => %d`, nestTParams.Len(), len(root.TNest))) + } + for i := 0; i < nestTParams.Len(); i++ { + replacements[nestTParams.At(i)] = root.TNest[i] + } + + // Check the root's type parameters and arguments match, + // then add them to the replacements. + if tParams.Len() != len(root.TArgs) { + panic(fmt.Errorf(`number of type parameters and arguments must match: %d => %d`, tParams.Len(), len(root.TArgs))) + } + for i := 0; i < tParams.Len(); i++ { + replacements[tParams.At(i)] = root.TArgs[i] + } + + return &Resolver{ + tParams: tParams, + tArgs: root.TArgs, + nest: nest, + nestTParams: nestTParams, + nestTArgs: root.TNest, + replacements: replacements, + root: root, + subster: subst.New(tc, replacements), + selMemo: map[typesutil.Selection]typesutil.Selection{}, + } +} + +// TypeParams is the list of type parameters that this resolver will substitute. +func (r *Resolver) TypeParams() *types.TypeParamList { + if r == nil { + return nil + } + return r.tParams +} + +// TypeArgs is the list of type arguments that this resolver will resolve to. +func (r *Resolver) TypeArgs() []types.Type { + if r == nil { + return nil + } + return r.tArgs +} + +// Nest is the nesting function that this resolver will resolve types with. +// This will be null if the resolver is not for a nested context, +func (r *Resolver) Nest() *types.Func { + if r == nil { + return nil + } + return r.nest +} + +// NestTypeParams is the list of type parameters from the nesting function +// that this resolver will substitute. +func (r *Resolver) NestTypeParams() *types.TypeParamList { + if r == nil { + return nil + } + return r.nestTParams +} + +// NestTypeArgs is the list of type arguments from the nesting function +// that this resolver will resolve to. +func (r *Resolver) NestTypeArgs() []types.Type { + if r == nil { + return nil + } + return r.nestTArgs +} + +// 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 || 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 || 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)) + } +} + +// String gets a strings representation of the resolver for debugging. +func (r *Resolver) String() string { + if r == nil { + return `{}` + } + + parts := make([]string, 0, len(r.replacements)) + for tp, ta := range r.replacements { + parts = append(parts, fmt.Sprintf("%s->%s", tp, ta)) + } + sort.Strings(parts) + return `{` + strings.Join(parts, `, `) + `}` +} diff --git a/compiler/internal/typeparams/utils.go b/compiler/internal/typeparams/utils.go index 6930fbf23..0c3d1d60d 100644 --- a/compiler/internal/typeparams/utils.go +++ b/compiler/internal/typeparams/utils.go @@ -1,8 +1,7 @@ package typeparams import ( - "errors" - "fmt" + "go/token" "go/types" ) @@ -19,10 +18,77 @@ func SignatureTypeParams(sig *types.Signature) *types.TypeParamList { } } -var ( - errInstantiatesGenerics = errors.New("instantiates generic type or function") - errDefinesGenerics = errors.New("defines generic type or function") -) +// FindNestingFunc returns the function or method that the given object +// is nested in. Returns nil if the object was defined at the package level, +// the position is invalid, or if the object is a function or method. +// +// This may get different results for some specific object types +// (e.g. receivers, type parameters) depending on the Go version. +// In go1.19 and earlier, some objects are not nested in the function +// they are part of the definition of, but in later versions they are. +func FindNestingFunc(obj types.Object) *types.Func { + if obj == nil { + return nil + } + objPos := obj.Pos() + if objPos == token.NoPos { + return nil + } + + if _, ok := obj.(*types.Func); ok { + // Functions and methods are not nested in any other function. + return nil + } + + var pkgScope *types.Scope + if obj.Pkg() != nil { + pkgScope = obj.Pkg().Scope() + } + scope := obj.Parent() + if scope == nil { + // Some types have a nil parent scope, such as methods and field, and + // types created with `types.NewTypeName`. Instead find the innermost + // scope from the package to use as the object's parent scope. + if pkgScope == nil { + return nil + } + scope = pkgScope.Innermost(objPos) + } + + if scope == pkgScope { + // If the object is defined at the package level, + // we can shortcut this check and just return nil. + return nil + } + + // Walk up the scope chain to find the function or method that contains + // the object at the given position. + for scope != nil { + // Iterate over all objects declared in the scope. + for _, name := range scope.Names() { + d := scope.Lookup(name) + if fn, ok := d.(*types.Func); ok && fn.Scope() != nil && fn.Scope().Contains(objPos) { + return fn + } + + if named, ok := d.Type().(*types.Named); ok { + // Iterate over all methods of an object. + for i := 0; i < named.NumMethods(); i++ { + if m := named.Method(i); m != nil && m.Scope() != nil && m.Scope().Contains(objPos) { + return m + } + } + } + } + if scope == pkgScope { + // If we reached the package scope, stop searching. + // We don't need to check the universal scope. + break + } + scope = scope.Parent() + } + return nil +} // HasTypeParams returns true if object defines type parameters. // @@ -39,22 +105,79 @@ func HasTypeParams(typ types.Type) bool { } } -// RequiresGenericsSupport returns an error if the type-checked code depends on -// generics support. -func RequiresGenericsSupport(info *types.Info) error { - for ident := range info.Instances { - // Any instantiation means dependency on generics. - return fmt.Errorf("%w: %v", errInstantiatesGenerics, info.ObjectOf(ident)) +// isGeneric will search all the given types in `typ` and their subtypes for a +// *types.TypeParam. This will not check if a type could be generic, +// but if each instantiation is not completely concrete yet. +// The given `ignore` slice is used to ignore type params that are known not +// to be substituted yet, typically the nest type parameters. +// +// This does allow for named types to have lazily substituted underlying types, +// as returned by methods like `types.Instantiate`, +// meaning that the type `B[T]` may be instantiated to `B[int]` but still have +// the underlying type of `struct { t T }` instead of `struct { t int }`. +// +// This is useful to check for generics types like `X[B[T]]`, where +// `X` appears concrete because it is instantiated with the type argument `B[T]`, +// however the `T` inside `B[T]` is a type parameter making `X[B[T]]` a generic +// type since it required instantiation to a concrete type, e.g. `X[B[int]]`. +func isGeneric(ignore *types.TypeParamList, typ []types.Type) bool { + var containsTypeParam func(t types.Type) bool + + foreach := func(count int, getter func(index int) types.Type) bool { + for i := 0; i < count; i++ { + if containsTypeParam(getter(i)) { + return true + } + } + return false + } + + seen := make(map[types.Type]struct{}) + managed := make(map[types.Type]struct{}) + for i := ignore.Len() - 1; i >= 0; i-- { + managed[ignore.At(i)] = struct{}{} } + containsTypeParam = func(t types.Type) bool { + if _, ok := seen[t]; ok { + return false + } + seen[t] = struct{}{} - for _, obj := range info.Defs { - if obj == nil { - continue + if _, ok := managed[t]; ok { + return false } - if HasTypeParams(obj.Type()) { - return fmt.Errorf("%w: %v", errDefinesGenerics, obj) + + switch t := t.(type) { + case *types.TypeParam: + return true + case *types.Named: + if t.TypeParams().Len() != t.TypeArgs().Len() || + foreach(t.TypeArgs().Len(), func(i int) types.Type { return t.TypeArgs().At(i) }) { + return true + } + // Add type parameters to managed so that if they are encountered + // we know that they are just lazy substitutions for the checked type arguments. + for i := t.TypeParams().Len() - 1; i >= 0; i-- { + managed[t.TypeParams().At(i)] = struct{}{} + } + return containsTypeParam(t.Underlying()) + case *types.Struct: + return foreach(t.NumFields(), func(i int) types.Type { return t.Field(i).Type() }) + case *types.Interface: + return foreach(t.NumMethods(), func(i int) types.Type { return t.Method(i).Type() }) + case *types.Signature: + return foreach(t.Params().Len(), func(i int) types.Type { return t.Params().At(i).Type() }) || + foreach(t.Results().Len(), func(i int) types.Type { return t.Results().At(i).Type() }) + case *types.Map: + return containsTypeParam(t.Key()) || containsTypeParam(t.Elem()) + case interface{ Elem() types.Type }: + // Handles *types.Pointer, *types.Slice, *types.Array, *types.Chan + return containsTypeParam(t.Elem()) + default: + // Other types (e.g., basic types) do not contain type parameters. + return false } } - return nil + return foreach(len(typ), func(i int) types.Type { return typ[i] }) } diff --git a/compiler/internal/typeparams/utils_test.go b/compiler/internal/typeparams/utils_test.go index dda685273..dc8b30414 100644 --- a/compiler/internal/typeparams/utils_test.go +++ b/compiler/internal/typeparams/utils_test.go @@ -1,11 +1,13 @@ package typeparams import ( - "errors" + "fmt" + "go/ast" "go/token" "go/types" "testing" + "github.com/google/go-cmp/cmp" "github.com/gopherjs/gopherjs/internal/srctesting" ) @@ -62,59 +64,164 @@ func TestHasTypeParams(t *testing.T) { } } -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)) +func Test_FindNestingFunc(t *testing.T) { + src := `package main - err := RequiresGenericsSupport(info) - if !errors.Is(err, errDefinesGenerics) { - t.Errorf("Got: RequiresGenericsSupport() = %v. Want: %v", err, errDefinesGenerics) + type bob int + func (a bob) riker() any { + type bill struct{ b int } + return bill{b: int(a)} } - }) - 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)) + type milo[T any] struct{} + func (c *milo[U]) mario() any { + type homer struct{ d U } + return homer{} + } - err := RequiresGenericsSupport(info) - if !errors.Is(err, errDefinesGenerics) { - t.Errorf("Got: RequiresGenericsSupport() = %v. Want: %v", err, errDefinesGenerics) + func bart() any { + e := []milo[int]{{}} + f := &e[0] + return f.mario() } - }) - 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) + func jack() any { + type linus interface{ + interface { + marvin() + } + luke() + } + type owen interface { + linus + isaac() + } + return owen(nil) } - }) - 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)) + func bender() any { + charles := func() any { + type arthur struct{ h int } + return arthur{h: 42} + } + return charles() + } + + var ned = func() any { + type elmer struct{ i int } + return elmer{i: 42} + }() + + func garfield(count int) { + calvin: + for j := 0; j < count; j++ { + if j == 5 { + break calvin + } + } + }` + + f := srctesting.New(t) + file := f.Parse("main.go", src) + info, _ := f.Check("test", file) + + // Collect all objects and find nesting functions. + // The results will be ordered by position in the file. + results := []string{} + ast.Inspect(file, func(n ast.Node) bool { + if id, ok := n.(*ast.Ident); ok { + obj := info.ObjectOf(id) + if _, isVar := obj.(*types.Var); isVar { + // Skip variables, some variables (e.g. receivers) are not inside + // a function's scope in go1.19 but in later versions they are. + return true + } + if named, isNamed := obj.(*types.TypeName); isNamed { + if _, isTP := named.Type().(*types.TypeParam); isTP { + // Skip type parameters since they are not inside + // a function's scope in go1.19 but in later versions they are. + return true + } + } - err := RequiresGenericsSupport(info) - if err != nil { - t.Errorf("Got: RequiresGenericsSupport() = %v. Want: nil", err) + fn := FindNestingFunc(obj) + fnName := `` + if fn != nil { + fnName = fn.FullName() + } + results = append(results, fmt.Sprintf("%3d) %s => %s", id.Pos(), id.Name, fnName)) } + return true }) + + diff := cmp.Diff([]string{ + // package main (nil object) + ` 9) main => `, + + // type bob int + ` 22) bob => `, + ` 26) int => `, // use of basic + + // func (a bob) riker() any { ... } + ` 40) bob => `, + ` 45) riker => `, + ` 53) any => `, + ` 67) bill => (test.bob).riker`, // def + ` 82) int => `, + ` 98) bill => (test.bob).riker`, // use + `106) int => `, + + // type milo[T any] struct {} + `126) milo => `, + `133) any => `, + + // func (c *milo[U]) mario() any { ... } + `158) milo => `, + `167) mario => `, + `175) any => `, + `189) homer => (*test.milo[U]).mario`, // def + `219) homer => (*test.milo[U]).mario`, // use + + // func bart() any { ... } + `239) bart => `, + `246) any => `, + `262) milo => `, // use of non-local defined type + `267) int => `, + `302) mario => `, // use of method on non-local defined type + + // func jack() any { ... } + `322) jack => `, + `329) any => `, + `343) linus => test.jack`, // def + `381) marvin => `, // method def + `400) luke => `, // method def + `420) owen => test.jack`, // def + `441) linus => test.jack`, // use + `451) isaac => `, // method def + `474) owen => test.jack`, // use + `479) nil => `, // use of nil + + // func bender() any { ... } + `496) bender => `, + `505) any => `, + `532) any => `, + `547) arthur => test.bender`, // def inside func lit + `564) int => `, + `581) arthur => test.bender`, // use + + // var ned = func() any { ... } + `646) any => `, + `660) elmer => `, // def inside package-level func lit + `676) int => `, + `692) elmer => `, // use + + // func garfield(count int) { ... } + `719) garfield => `, + `734) int => `, + `744) calvin => `, // local label def + `811) calvin => `, // label break + }, results) + if len(diff) > 0 { + t.Errorf("FindNestingFunc() mismatch (-want +got):\n%s", diff) + } } diff --git a/compiler/jsFile/jsFile.go b/compiler/jsFile/jsFile.go new file mode 100644 index 000000000..b8ae9421f --- /dev/null +++ b/compiler/jsFile/jsFile.go @@ -0,0 +1,55 @@ +package jsFile + +import ( + "fmt" + "go/build" + "io" + "strings" + "time" + + "golang.org/x/tools/go/buildutil" +) + +// JSFile represents a *.inc.js file metadata and content. +type JSFile struct { + Path string // Full file path for the build context the file came from. + ModTime time.Time + Content []byte +} + +// JSFilesFromDir finds and loads any *.inc.js packages in the build context +// directory. +func JSFilesFromDir(bctx *build.Context, dir string) ([]JSFile, error) { + files, err := buildutil.ReadDir(bctx, dir) + if err != nil { + return nil, err + } + var jsFiles []JSFile + for _, file := range files { + if !strings.HasSuffix(file.Name(), ".inc.js") || file.IsDir() { + continue + } + if file.Name()[0] == '_' || file.Name()[0] == '.' { + continue // Skip "hidden" files that are typically ignored by the Go build system. + } + + path := buildutil.JoinPath(bctx, dir, file.Name()) + f, err := buildutil.OpenFile(bctx, path) + if err != nil { + return nil, fmt.Errorf("failed to open %s from %v: %w", path, bctx, err) + } + defer f.Close() + + content, err := io.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("failed to read %s from %v: %w", path, bctx, err) + } + + jsFiles = append(jsFiles, JSFile{ + Path: path, + ModTime: file.ModTime(), + Content: content, + }) + } + return jsFiles, nil +} diff --git a/compiler/linkname.go b/compiler/linkname/linkname.go similarity index 90% rename from compiler/linkname.go rename to compiler/linkname/linkname.go index 6dd93a709..8e0be1e7a 100644 --- a/compiler/linkname.go +++ b/compiler/linkname/linkname.go @@ -1,4 +1,4 @@ -package compiler +package linkname import ( "fmt" @@ -8,6 +8,7 @@ import ( "github.com/gopherjs/gopherjs/compiler/astutil" "github.com/gopherjs/gopherjs/compiler/internal/symbol" + "github.com/gopherjs/gopherjs/internal/errorList" ) // GoLinkname describes a go:linkname compiler directive found in the source code. @@ -114,8 +115,8 @@ func isMitigatedInsertLinkname(sym symbol.Name) bool { // - The local function referenced by the directive must have no body (in other // words, it can only "import" an external function implementation into the // local scope). -func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]GoLinkname, error) { - var errs ErrorList = nil +func ParseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]GoLinkname, error) { + var errs errorList.ErrorList = nil var directives []GoLinkname isUnsafe := astutil.ImportsUnsafe(file) @@ -157,7 +158,7 @@ func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]Go for _, cg := range file.Comments { for _, c := range cg.List { if err := processComment(c); err != nil { - errs = append(errs, ErrorAt(err, fset, c.Pos())) + errs = append(errs, errorAt(err, fset, c.Pos())) } } } @@ -165,15 +166,20 @@ func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]Go return directives, errs.ErrOrNil() } -// goLinknameSet is a utility that enables quick lookup of whether a decl is +// 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) +} + +// 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 { +type GoLinknameSet struct { byImplementation map[symbol.Name][]GoLinkname byReference map[symbol.Name]GoLinkname } // Add more GoLinkname directives into the set. -func (gls *goLinknameSet) Add(entries []GoLinkname) error { +func (gls *GoLinknameSet) Add(entries []GoLinkname) error { if gls.byImplementation == nil { gls.byImplementation = map[symbol.Name][]GoLinkname{} } @@ -193,7 +199,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 symbol.Name) bool { +func (gls *GoLinknameSet) IsImplementation(sym symbol.Name) bool { _, found := gls.byImplementation[sym] return found } @@ -201,7 +207,7 @@ func (gls *goLinknameSet) IsImplementation(sym symbol.Name) 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 symbol.Name) (symbol.Name, 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/linkname_test.go similarity index 98% rename from compiler/linkname_test.go rename to compiler/linkname/linkname_test.go index 9f991d394..0f1299407 100644 --- a/compiler/linkname_test.go +++ b/compiler/linkname/linkname_test.go @@ -1,4 +1,4 @@ -package compiler +package linkname import ( "go/ast" @@ -220,7 +220,7 @@ func TestParseGoLinknames(t *testing.T) { if len(test.pkgPath) > 0 { pkgPath = test.pkgPath } - directives, err := parseGoLinknames(fset, pkgPath, file) + directives, err := ParseGoLinknames(fset, pkgPath, file) if test.wantError != "" { if err == nil { diff --git a/compiler/natives/src/bufio/bufio_test.go b/compiler/natives/src/bufio/bufio_test.go index b97fe22f7..f0ddf0534 100644 --- a/compiler/natives/src/bufio/bufio_test.go +++ b/compiler/natives/src/bufio/bufio_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bufio_test diff --git a/compiler/natives/src/bytes/bytes.go b/compiler/natives/src/bytes/bytes.go index 1f74edc4f..15c014301 100644 --- a/compiler/natives/src/bytes/bytes.go +++ b/compiler/natives/src/bytes/bytes.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bytes diff --git a/compiler/natives/src/bytes/bytes_test.go b/compiler/natives/src/bytes/bytes_test.go index e9d0e1690..5a2277f59 100644 --- a/compiler/natives/src/bytes/bytes_test.go +++ b/compiler/natives/src/bytes/bytes_test.go @@ -1,11 +1,8 @@ //go:build js -// +build js package bytes_test -import ( - "testing" -) +import "testing" func dangerousSlice(t *testing.T) []byte { t.Skip("dangerousSlice relies on syscall.Getpagesize, which GopherJS doesn't implement") diff --git a/compiler/natives/src/compress/gzip/example_test.go b/compiler/natives/src/compress/gzip/example_test.go index b3f6fbe43..37b3e734c 100644 --- a/compiler/natives/src/compress/gzip/example_test.go +++ b/compiler/natives/src/compress/gzip/example_test.go @@ -1,11 +1,8 @@ //go:build js && wasm -// +build js,wasm package gzip_test -import ( - "fmt" -) +import "fmt" // The test relies on a local HTTP server, which is not supported under NodeJS. func Example_compressingReader() { diff --git a/compiler/natives/src/crypto/ecdh/nist.go b/compiler/natives/src/crypto/ecdh/nist.go deleted file mode 100644 index ecaa84d76..000000000 --- a/compiler/natives/src/crypto/ecdh/nist.go +++ /dev/null @@ -1,58 +0,0 @@ -//go:build js -// +build js - -package ecdh - -import ( - "crypto/internal/nistec" - "io" -) - -//gopherjs:purge for go1.20 without generics -type nistPoint[T any] interface{} - -// temporarily replacement of `nistCurve[Point nistPoint[Point]]` for go1.20 without generics. -type nistCurve struct { - name string - newPoint func() nistec.WrappedPoint - scalarOrder []byte -} - -//gopherjs:override-signature -func (c *nistCurve) String() string - -//gopherjs:override-signature -func (c *nistCurve) GenerateKey(rand io.Reader) (*PrivateKey, error) - -//gopherjs:override-signature -func (c *nistCurve) NewPrivateKey(key []byte) (*PrivateKey, error) - -//gopherjs:override-signature -func (c *nistCurve) privateKeyToPublicKey(key *PrivateKey) *PublicKey - -//gopherjs:override-signature -func (c *nistCurve) NewPublicKey(key []byte) (*PublicKey, error) - -//gopherjs:override-signature -func (c *nistCurve) ecdh(local *PrivateKey, remote *PublicKey) ([]byte, error) - -// temporarily replacement for go1.20 without generics. -var p256 = &nistCurve{ - name: "P-256", - newPoint: nistec.NewP256WrappedPoint, - scalarOrder: p256Order, -} - -// temporarily replacement for go1.20 without generics. -var p384 = &nistCurve{ - name: "P-384", - newPoint: nistec.NewP384WrappedPoint, - scalarOrder: p384Order, -} - -// temporarily replacement for go1.20 without generics. -var p521 = &nistCurve{ - name: "P-521", - newPoint: nistec.NewP521WrappedPoint, - scalarOrder: p521Order, -} diff --git a/compiler/natives/src/crypto/ecdsa/ecdsa.go b/compiler/natives/src/crypto/ecdsa/ecdsa.go deleted file mode 100644 index cf3da4ec8..000000000 --- a/compiler/natives/src/crypto/ecdsa/ecdsa.go +++ /dev/null @@ -1,98 +0,0 @@ -//go:build js -// +build js - -package ecdsa - -import ( - "crypto/elliptic" - "crypto/internal/bigmod" - "crypto/internal/nistec" - "io" - "math/big" -) - -//gopherjs:override-signature -func generateNISTEC(c *nistCurve, rand io.Reader) (*PrivateKey, error) - -//gopherjs:override-signature -func randomPoint(c *nistCurve, rand io.Reader) (k *bigmod.Nat, p nistec.WrappedPoint, err error) - -//gopherjs:override-signature -func signNISTEC(c *nistCurve, priv *PrivateKey, csprng io.Reader, hash []byte) (sig []byte, err error) - -//gopherjs:override-signature -func inverse(c *nistCurve, kInv, k *bigmod.Nat) - -//gopherjs:override-signature -func hashToNat(c *nistCurve, e *bigmod.Nat, hash []byte) - -//gopherjs:override-signature -func verifyNISTEC(c *nistCurve, pub *PublicKey, hash, sig []byte) bool - -//gopherjs:purge for go1.20 without generics -type nistPoint[T any] interface{} - -// temporarily replacement of `nistCurve[Point nistPoint[Point]]` for go1.20 without generics. -type nistCurve struct { - newPoint func() nistec.WrappedPoint - curve elliptic.Curve - N *bigmod.Modulus - nMinus2 []byte -} - -//gopherjs:override-signature -func (curve *nistCurve) pointFromAffine(x, y *big.Int) (p nistec.WrappedPoint, err error) - -//gopherjs:override-signature -func (curve *nistCurve) pointToAffine(p nistec.WrappedPoint) (x, y *big.Int, err error) - -var _p224 *nistCurve - -func p224() *nistCurve { - p224Once.Do(func() { - _p224 = &nistCurve{ - newPoint: nistec.NewP224WrappedPoint, - } - precomputeParams(_p224, elliptic.P224()) - }) - return _p224 -} - -var _p256 *nistCurve - -func p256() *nistCurve { - p256Once.Do(func() { - _p256 = &nistCurve{ - newPoint: nistec.NewP256WrappedPoint, - } - precomputeParams(_p256, elliptic.P256()) - }) - return _p256 -} - -var _p384 *nistCurve - -func p384() *nistCurve { - p384Once.Do(func() { - _p384 = &nistCurve{ - newPoint: nistec.NewP384WrappedPoint, - } - precomputeParams(_p384, elliptic.P384()) - }) - return _p384 -} - -var _p521 *nistCurve - -func p521() *nistCurve { - p521Once.Do(func() { - _p521 = &nistCurve{ - newPoint: nistec.NewP521WrappedPoint, - } - precomputeParams(_p521, elliptic.P521()) - }) - return _p521 -} - -//gopherjs:override-signature -func precomputeParams(c *nistCurve, curve elliptic.Curve) diff --git a/compiler/natives/src/crypto/ecdsa/ecdsa_test.go b/compiler/natives/src/crypto/ecdsa/ecdsa_test.go deleted file mode 100644 index efb4d7b5e..000000000 --- a/compiler/natives/src/crypto/ecdsa/ecdsa_test.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build js -// +build js - -package ecdsa - -import "testing" - -//gopherjs:override-signature -func testRandomPoint(t *testing.T, c *nistCurve) - -//gopherjs:override-signature -func testHashToNat(t *testing.T, c *nistCurve) diff --git a/compiler/natives/src/crypto/elliptic/nistec.go b/compiler/natives/src/crypto/elliptic/nistec.go deleted file mode 100644 index 326c602d5..000000000 --- a/compiler/natives/src/crypto/elliptic/nistec.go +++ /dev/null @@ -1,81 +0,0 @@ -//go:build js -// +build js - -package elliptic - -import ( - "crypto/internal/nistec" - "math/big" -) - -// nistPoint uses generics so must be removed for generic-less GopherJS. -// All the following code changes in this file are to make p224, p256, -// p521, and p384 still function correctly without this generic struct. -// -//gopherjs:purge for go1.19 without generics -type nistPoint[T any] interface{} - -// nistCurve replaces the generics with a version using the wrappedPoint -// interface, then update all the method signatures to also use wrappedPoint. -type nistCurve struct { - newPoint func() nistec.WrappedPoint - params *CurveParams -} - -//gopherjs:override-signature -func (curve *nistCurve) Params() *CurveParams - -//gopherjs:override-signature -func (curve *nistCurve) IsOnCurve(x, y *big.Int) bool - -//gopherjs:override-signature -func (curve *nistCurve) pointFromAffine(x, y *big.Int) (p nistec.WrappedPoint, err error) - -//gopherjs:override-signature -func (curve *nistCurve) pointToAffine(p nistec.WrappedPoint) (x, y *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) Add(x1, y1, x2, y2 *big.Int) (*big.Int, *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) Double(x1, y1 *big.Int) (*big.Int, *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) normalizeScalar(scalar []byte) []byte - -//gopherjs:override-signature -func (curve *nistCurve) ScalarMult(Bx, By *big.Int, scalar []byte) (*big.Int, *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) ScalarBaseMult(scalar []byte) (*big.Int, *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) CombinedMult(Px, Py *big.Int, s1, s2 []byte) (x, y *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) Unmarshal(data []byte) (x, y *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) UnmarshalCompressed(data []byte) (x, y *big.Int) - -var p224 = &nistCurve{ - newPoint: nistec.NewP224WrappedPoint, -} - -type p256Curve struct { - nistCurve -} - -var p256 = &p256Curve{ - nistCurve: nistCurve{ - newPoint: nistec.NewP256WrappedPoint, - }, -} - -var p521 = &nistCurve{ - newPoint: nistec.NewP521WrappedPoint, -} - -var p384 = &nistCurve{ - newPoint: nistec.NewP384WrappedPoint, -} diff --git a/compiler/natives/src/crypto/internal/alias/alias.go b/compiler/natives/src/crypto/internal/alias/alias.go index e6bb87536..b6b1bc2bb 100644 --- a/compiler/natives/src/crypto/internal/alias/alias.go +++ b/compiler/natives/src/crypto/internal/alias/alias.go @@ -1,5 +1,4 @@ //go:build js -// +build js package alias diff --git a/compiler/natives/src/crypto/internal/boring/aes.go b/compiler/natives/src/crypto/internal/boring/aes.go index e2a840440..9e9ce5fff 100644 --- a/compiler/natives/src/crypto/internal/boring/aes.go +++ b/compiler/natives/src/crypto/internal/boring/aes.go @@ -1,5 +1,4 @@ //go:build js -// +build js package boring diff --git a/compiler/natives/src/crypto/internal/boring/bbig/big.go b/compiler/natives/src/crypto/internal/boring/bbig/big.go index 3a726ba3c..170a6f983 100644 --- a/compiler/natives/src/crypto/internal/boring/bbig/big.go +++ b/compiler/natives/src/crypto/internal/boring/bbig/big.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bbig diff --git a/compiler/natives/src/crypto/internal/boring/bcache/cache.go b/compiler/natives/src/crypto/internal/boring/bcache/cache.go index 4c4e0dab6..a1aed8869 100644 --- a/compiler/natives/src/crypto/internal/boring/bcache/cache.go +++ b/compiler/natives/src/crypto/internal/boring/bcache/cache.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bcache diff --git a/compiler/natives/src/crypto/internal/boring/bcache/cache_test.go b/compiler/natives/src/crypto/internal/boring/bcache/cache_test.go index a23e975a0..fdf73aa67 100644 --- a/compiler/natives/src/crypto/internal/boring/bcache/cache_test.go +++ b/compiler/natives/src/crypto/internal/boring/bcache/cache_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bcache diff --git a/compiler/natives/src/crypto/internal/boring/sig/sig.go b/compiler/natives/src/crypto/internal/boring/sig/sig.go index 3eb2454aa..dab01104e 100644 --- a/compiler/natives/src/crypto/internal/boring/sig/sig.go +++ b/compiler/natives/src/crypto/internal/boring/sig/sig.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sig diff --git a/compiler/natives/src/crypto/internal/nistec/nistec_test.go b/compiler/natives/src/crypto/internal/nistec/nistec_test.go deleted file mode 100644 index ea91d7ed2..000000000 --- a/compiler/natives/src/crypto/internal/nistec/nistec_test.go +++ /dev/null @@ -1,89 +0,0 @@ -//go:build js -// +build js - -package nistec_test - -import ( - "crypto/elliptic" - "crypto/internal/nistec" - "testing" -) - -func TestAllocations(t *testing.T) { - t.Skip("testing.AllocsPerRun not supported in GopherJS") -} - -//gopherjs:purge -type nistPoint[T any] interface{} - -func TestEquivalents(t *testing.T) { - t.Run("P224", func(t *testing.T) { - testEquivalents(t, nistec.NewP224WrappedPoint, elliptic.P224()) - }) - t.Run("P256", func(t *testing.T) { - testEquivalents(t, nistec.NewP256WrappedPoint, elliptic.P256()) - }) - t.Run("P384", func(t *testing.T) { - testEquivalents(t, nistec.NewP384WrappedPoint, elliptic.P384()) - }) - t.Run("P521", func(t *testing.T) { - testEquivalents(t, nistec.NewP521WrappedPoint, elliptic.P521()) - }) -} - -//gopherjs:override-signature -func testEquivalents(t *testing.T, newPoint func() nistec.WrappedPoint, c elliptic.Curve) - -func TestScalarMult(t *testing.T) { - t.Run("P224", func(t *testing.T) { - testScalarMult(t, nistec.NewP224WrappedPoint, elliptic.P224()) - }) - t.Run("P256", func(t *testing.T) { - testScalarMult(t, nistec.NewP256WrappedPoint, elliptic.P256()) - }) - t.Run("P384", func(t *testing.T) { - testScalarMult(t, nistec.NewP384WrappedPoint, elliptic.P384()) - }) - t.Run("P521", func(t *testing.T) { - testScalarMult(t, nistec.NewP521WrappedPoint, elliptic.P521()) - }) -} - -//gopherjs:override-signature -func testScalarMult(t *testing.T, newPoint func() nistec.WrappedPoint, c elliptic.Curve) - -func BenchmarkScalarMult(b *testing.B) { - b.Run("P224", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP224WrappedPoint().SetGenerator(), 28) - }) - b.Run("P256", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP256WrappedPoint().SetGenerator(), 32) - }) - b.Run("P384", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP384WrappedPoint().SetGenerator(), 48) - }) - b.Run("P521", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP521WrappedPoint().SetGenerator(), 66) - }) -} - -//gopherjs:override-signature -func benchmarkScalarMult(b *testing.B, p nistec.WrappedPoint, scalarSize int) - -func BenchmarkScalarBaseMult(b *testing.B) { - b.Run("P224", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP224WrappedPoint().SetGenerator(), 28) - }) - b.Run("P256", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP256WrappedPoint().SetGenerator(), 32) - }) - b.Run("P384", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP384WrappedPoint().SetGenerator(), 48) - }) - b.Run("P521", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP521WrappedPoint().SetGenerator(), 66) - }) -} - -//gopherjs:override-signature -func benchmarkScalarBaseMult(b *testing.B, p nistec.WrappedPoint, scalarSize int) diff --git a/compiler/natives/src/crypto/internal/nistec/wrapper.go b/compiler/natives/src/crypto/internal/nistec/wrapper.go deleted file mode 100644 index afa2b7049..000000000 --- a/compiler/natives/src/crypto/internal/nistec/wrapper.go +++ /dev/null @@ -1,204 +0,0 @@ -//go:build js -// +build js - -package nistec - -// temporarily replacement of `nistPoint[T any]` for go1.20 without generics. -type WrappedPoint interface { - SetGenerator() WrappedPoint - Bytes() []byte - BytesX() ([]byte, error) - SetBytes(b []byte) (WrappedPoint, error) - Add(w1, w2 WrappedPoint) WrappedPoint - Double(w1 WrappedPoint) WrappedPoint - ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) - ScalarBaseMult(scalar []byte) (WrappedPoint, error) -} - -type p224Wrapper struct { - point *P224Point -} - -func wrapP224(point *P224Point) WrappedPoint { - return p224Wrapper{point: point} -} - -func NewP224WrappedPoint() WrappedPoint { - return wrapP224(NewP224Point()) -} - -func (w p224Wrapper) SetGenerator() WrappedPoint { - return wrapP224(w.point.SetGenerator()) -} - -func (w p224Wrapper) Bytes() []byte { - return w.point.Bytes() -} - -func (w p224Wrapper) BytesX() ([]byte, error) { - return w.point.BytesX() -} - -func (w p224Wrapper) SetBytes(b []byte) (WrappedPoint, error) { - p, err := w.point.SetBytes(b) - return wrapP224(p), err -} - -func (w p224Wrapper) Add(w1, w2 WrappedPoint) WrappedPoint { - return wrapP224(w.point.Add(w1.(p224Wrapper).point, w2.(p224Wrapper).point)) -} - -func (w p224Wrapper) Double(w1 WrappedPoint) WrappedPoint { - return wrapP224(w.point.Double(w1.(p224Wrapper).point)) -} - -func (w p224Wrapper) ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarMult(w1.(p224Wrapper).point, scalar) - return wrapP224(p), err -} - -func (w p224Wrapper) ScalarBaseMult(scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarBaseMult(scalar) - return wrapP224(p), err -} - -type p256Wrapper struct { - point *P256Point -} - -func wrapP256(point *P256Point) WrappedPoint { - return p256Wrapper{point: point} -} - -func NewP256WrappedPoint() WrappedPoint { - return wrapP256(NewP256Point()) -} - -func (w p256Wrapper) SetGenerator() WrappedPoint { - return wrapP256(w.point.SetGenerator()) -} - -func (w p256Wrapper) Bytes() []byte { - return w.point.Bytes() -} - -func (w p256Wrapper) BytesX() ([]byte, error) { - return w.point.BytesX() -} - -func (w p256Wrapper) SetBytes(b []byte) (WrappedPoint, error) { - p, err := w.point.SetBytes(b) - return wrapP256(p), err -} - -func (w p256Wrapper) Add(w1, w2 WrappedPoint) WrappedPoint { - return wrapP256(w.point.Add(w1.(p256Wrapper).point, w2.(p256Wrapper).point)) -} - -func (w p256Wrapper) Double(w1 WrappedPoint) WrappedPoint { - return wrapP256(w.point.Double(w1.(p256Wrapper).point)) -} - -func (w p256Wrapper) ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarMult(w1.(p256Wrapper).point, scalar) - return wrapP256(p), err -} - -func (w p256Wrapper) ScalarBaseMult(scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarBaseMult(scalar) - return wrapP256(p), err -} - -type p521Wrapper struct { - point *P521Point -} - -func wrapP521(point *P521Point) WrappedPoint { - return p521Wrapper{point: point} -} - -func NewP521WrappedPoint() WrappedPoint { - return wrapP521(NewP521Point()) -} - -func (w p521Wrapper) SetGenerator() WrappedPoint { - return wrapP521(w.point.SetGenerator()) -} - -func (w p521Wrapper) Bytes() []byte { - return w.point.Bytes() -} - -func (w p521Wrapper) BytesX() ([]byte, error) { - return w.point.BytesX() -} - -func (w p521Wrapper) SetBytes(b []byte) (WrappedPoint, error) { - p, err := w.point.SetBytes(b) - return wrapP521(p), err -} - -func (w p521Wrapper) Add(w1, w2 WrappedPoint) WrappedPoint { - return wrapP521(w.point.Add(w1.(p521Wrapper).point, w2.(p521Wrapper).point)) -} - -func (w p521Wrapper) Double(w1 WrappedPoint) WrappedPoint { - return wrapP521(w.point.Double(w1.(p521Wrapper).point)) -} - -func (w p521Wrapper) ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarMult(w1.(p521Wrapper).point, scalar) - return wrapP521(p), err -} - -func (w p521Wrapper) ScalarBaseMult(scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarBaseMult(scalar) - return wrapP521(p), err -} - -type p384Wrapper struct { - point *P384Point -} - -func wrapP384(point *P384Point) WrappedPoint { - return p384Wrapper{point: point} -} - -func NewP384WrappedPoint() WrappedPoint { - return wrapP384(NewP384Point()) -} - -func (w p384Wrapper) SetGenerator() WrappedPoint { - return wrapP384(w.point.SetGenerator()) -} - -func (w p384Wrapper) Bytes() []byte { - return w.point.Bytes() -} - -func (w p384Wrapper) BytesX() ([]byte, error) { - return w.point.BytesX() -} - -func (w p384Wrapper) SetBytes(b []byte) (WrappedPoint, error) { - p, err := w.point.SetBytes(b) - return wrapP384(p), err -} - -func (w p384Wrapper) Add(w1, w2 WrappedPoint) WrappedPoint { - return wrapP384(w.point.Add(w1.(p384Wrapper).point, w2.(p384Wrapper).point)) -} - -func (w p384Wrapper) Double(w1 WrappedPoint) WrappedPoint { - return wrapP384(w.point.Double(w1.(p384Wrapper).point)) -} - -func (w p384Wrapper) ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarMult(w1.(p384Wrapper).point, scalar) - return wrapP384(p), err -} - -func (w p384Wrapper) ScalarBaseMult(scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarBaseMult(scalar) - return wrapP384(p), err -} diff --git a/compiler/natives/src/crypto/rand/rand.go b/compiler/natives/src/crypto/rand/rand.go index 1c3631a02..4e4aab565 100644 --- a/compiler/natives/src/crypto/rand/rand.go +++ b/compiler/natives/src/crypto/rand/rand.go @@ -1,5 +1,4 @@ //go:build js -// +build js package rand diff --git a/compiler/natives/src/crypto/subtle/xor.go b/compiler/natives/src/crypto/subtle/xor.go index eccf98531..6b78bddf2 100644 --- a/compiler/natives/src/crypto/subtle/xor.go +++ b/compiler/natives/src/crypto/subtle/xor.go @@ -1,5 +1,4 @@ //go:build js -// +build js package subtle @@ -74,5 +73,7 @@ const supportsUnaligned = false //gopherjs:purge func xorBytes(dstb, xb, yb *byte, n int) +// TODO(grantnelson-wf): Check if this should be removed or not with generics. +// //gopherjs:purge func xorLoop[T byte | uintptr](dst, x, y []T) {} diff --git a/compiler/natives/src/crypto/tls/handshake_test.go b/compiler/natives/src/crypto/tls/handshake_test.go index c9e19d5ed..b5fe59a57 100644 --- a/compiler/natives/src/crypto/tls/handshake_test.go +++ b/compiler/natives/src/crypto/tls/handshake_test.go @@ -10,7 +10,7 @@ import ( // Same as upstream, except we check for GOARCH=ecmascript instead of wasm. // This override can be removed after https://github.com/golang/go/pull/51827 -// is available in the upstream (likely in Go 1.19). +// is available in the upstream (likely after Go 1.19). func TestServerHandshakeContextCancellation(t *testing.T) { c, s := localPipe(t) ctx, cancel := context.WithCancel(context.Background()) @@ -42,7 +42,7 @@ func TestServerHandshakeContextCancellation(t *testing.T) { // Same as upstream, except we check for GOARCH=ecmascript instead of wasm. // This override can be removed after https://github.com/golang/go/pull/51827 -// is available in the upstream (likely in Go 1.19). +// is available in the upstream (likely after Go 1.19). func TestClientHandshakeContextCancellation(t *testing.T) { c, s := localPipe(t) ctx, cancel := context.WithCancel(context.Background()) @@ -71,3 +71,15 @@ func TestClientHandshakeContextCancellation(t *testing.T) { t.Error("Client connection was not closed when the context was canceled") } } + +func TestVerifyConnection(t *testing.T) { + // This should be rechecked after upgrading to Go 1.20 or later. + // go1.19.13/src/crypto/tls/handshake_test.go:testRSACertificateIssuer has expired. + t.Skip("Skipping test that uses predefined certificate that expired in Jan 1st 2025") +} + +func TestResumptionKeepsOCSPAndSCT(t *testing.T) { + // This should be rechecked after upgrading to Go 1.20 or later. + // go1.19.13/src/crypto/tls/handshake_test.go:testRSACertificateIssuer has expired. + t.Skip("Skipping test that uses predefined certificate that expired in Jan 1st 2025") +} diff --git a/compiler/natives/src/database/sql/driver/driver_test.go b/compiler/natives/src/database/sql/driver/driver_test.go index 446da47c5..59ac867ef 100644 --- a/compiler/natives/src/database/sql/driver/driver_test.go +++ b/compiler/natives/src/database/sql/driver/driver_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package driver @@ -24,10 +23,10 @@ var valueConverterTests = []valueConverterTest{ {DefaultParameterConverter, (*int64)(nil), nil, ""}, {DefaultParameterConverter, &answer, answer, ""}, {DefaultParameterConverter, &now, now, ""}, - //{DefaultParameterConverter, i(9), int64(9), ""}, // TODO: Fix. + //{DefaultParameterConverter, i(9), int64(9), ""}, // TODO: Fix. Errors with `driver.defaultConverter(driver.i(9)) = 9 (driver.i); want 9 (int64)` {DefaultParameterConverter, f(0.1), float64(0.1), ""}, {DefaultParameterConverter, b(true), true, ""}, - //{DefaultParameterConverter, bs{1}, []byte{1}, ""}, // TODO: Fix. + //{DefaultParameterConverter, bs{1}, []byte{1}, ""}, // TODO: Fix. Errors with `driver.defaultConverter(driver.bs([1])) = [1] (driver.bs); want [1] ([]uint8)` {DefaultParameterConverter, s("a"), "a", ""}, {DefaultParameterConverter, is{1}, nil, "unsupported type driver.is, a slice of int"}, } diff --git a/compiler/natives/src/debug/pe/symbol.go b/compiler/natives/src/debug/pe/symbol.go index 798502ce3..7564b8f77 100644 --- a/compiler/natives/src/debug/pe/symbol.go +++ b/compiler/natives/src/debug/pe/symbol.go @@ -1,5 +1,4 @@ //go:build js -// +build js package pe diff --git a/compiler/natives/src/embed/embed.go b/compiler/natives/src/embed/embed.go index bb9738546..83cf729ac 100644 --- a/compiler/natives/src/embed/embed.go +++ b/compiler/natives/src/embed/embed.go @@ -1,5 +1,4 @@ //go:build js -// +build js package embed diff --git a/compiler/natives/src/encoding/gob/gob.go b/compiler/natives/src/encoding/gob/gob.go deleted file mode 100644 index 244f72ed7..000000000 --- a/compiler/natives/src/encoding/gob/gob.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:build js -// +build js - -package gob - -import ( - "reflect" - "sync" -) - -type typeInfo struct { - id typeId - encInit sync.Mutex - - // temporarily replacement of atomic.Pointer[encEngine] for go1.20 without generics. - encoder atomicEncEnginePointer - wire *wireType -} - -type atomicEncEnginePointer struct { - v *encEngine -} - -func (x *atomicEncEnginePointer) Load() *encEngine { return x.v } -func (x *atomicEncEnginePointer) Store(val *encEngine) { x.v = val } - -// temporarily replacement of growSlice[E any] for go1.20 without generics. -func growSlice(v reflect.Value, ps any, length int) { - vps := reflect.ValueOf(ps) // *[]E - vs := vps.Elem() // []E - zero := reflect.Zero(vs.Type().Elem()) - vs.Set(reflect.Append(vs, zero)) - cp := vs.Cap() - if cp > length { - cp = length - } - vs.Set(vs.Slice(0, cp)) - v.Set(vs) -} diff --git a/compiler/natives/src/encoding/gob/gob_test.go b/compiler/natives/src/encoding/gob/gob_test.go index a2f303ab6..f2b169376 100644 --- a/compiler/natives/src/encoding/gob/gob_test.go +++ b/compiler/natives/src/encoding/gob/gob_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package gob diff --git a/compiler/natives/src/encoding/json/stream_test.go b/compiler/natives/src/encoding/json/stream_test.go index adad8e153..484ee5c12 100644 --- a/compiler/natives/src/encoding/json/stream_test.go +++ b/compiler/natives/src/encoding/json/stream_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package json diff --git a/compiler/natives/src/fmt/fmt_test.go b/compiler/natives/src/fmt/fmt_test.go index 70797693b..1a1c3278c 100644 --- a/compiler/natives/src/fmt/fmt_test.go +++ b/compiler/natives/src/fmt/fmt_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package fmt_test diff --git a/compiler/natives/src/go/doc/doc_test.go b/compiler/natives/src/go/doc/doc_test.go deleted file mode 100644 index 4d35e880c..000000000 --- a/compiler/natives/src/go/doc/doc_test.go +++ /dev/null @@ -1,37 +0,0 @@ -//go:build js - -package doc - -import ( - "fmt" - "testing" -) - -func compareSlices(t *testing.T, name string, got, want interface{}, compareElem interface{}) { - // TODO(nevkontakte): Remove this override after generics are supported. - // https://github.com/gopherjs/gopherjs/issues/1013. - switch got.(type) { - case []*Func: - got := got.([]*Func) - want := want.([]*Func) - compareElem := compareElem.(func(t *testing.T, msg string, got, want *Func)) - if len(got) != len(want) { - t.Errorf("%s: got %d, want %d", name, len(got), len(want)) - } - for i := 0; i < len(got) && i < len(want); i++ { - compareElem(t, fmt.Sprintf("%s[%d]", name, i), got[i], want[i]) - } - case []*Type: - got := got.([]*Type) - want := want.([]*Type) - compareElem := compareElem.(func(t *testing.T, msg string, got, want *Type)) - if len(got) != len(want) { - t.Errorf("%s: got %d, want %d", name, len(got), len(want)) - } - for i := 0; i < len(got) && i < len(want); i++ { - compareElem(t, fmt.Sprintf("%s[%d]", name, i), got[i], want[i]) - } - default: - t.Errorf("unexpected argument type %T", got) - } -} diff --git a/compiler/natives/src/go/parser/parser_test.go b/compiler/natives/src/go/parser/parser_test.go index 7fded29fd..00f533233 100644 --- a/compiler/natives/src/go/parser/parser_test.go +++ b/compiler/natives/src/go/parser/parser_test.go @@ -2,9 +2,7 @@ package parser -import ( - "testing" -) +import "testing" func TestParseDepthLimit(t *testing.T) { t.Skip("causes call stack exhaustion on js/ecmascript") diff --git a/compiler/natives/src/go/token/position.go b/compiler/natives/src/go/token/position.go deleted file mode 100644 index 436c48380..000000000 --- a/compiler/natives/src/go/token/position.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build js -// +build js - -package token - -import "sync" - -type FileSet struct { - mutex sync.RWMutex - base int - files []*File - - // replaced atomic.Pointer[File] for go1.19 without generics. - last atomicFilePointer -} - -type atomicFilePointer struct { - v *File -} - -func (x *atomicFilePointer) Load() *File { return x.v } -func (x *atomicFilePointer) Store(val *File) { x.v = val } - -func (x *atomicFilePointer) CompareAndSwap(old, new *File) bool { - if x.v == old { - x.v = new - return true - } - return false -} diff --git a/compiler/natives/src/go/token/token_test.go b/compiler/natives/src/go/token/token_test.go index 335ee0776..9ef38348a 100644 --- a/compiler/natives/src/go/token/token_test.go +++ b/compiler/natives/src/go/token/token_test.go @@ -1,12 +1,9 @@ //go:build js -// +build js package token -import ( - "testing" -) +import "testing" func TestFileSetRace(t *testing.T) { - t.Skip() + t.Skip("Fails with: WaitGroup counter not zero") } diff --git a/compiler/natives/src/hash/maphash/maphash.go b/compiler/natives/src/hash/maphash/maphash.go index 5c982404f..7c2a68cf6 100644 --- a/compiler/natives/src/hash/maphash/maphash.go +++ b/compiler/natives/src/hash/maphash/maphash.go @@ -1,11 +1,8 @@ //go:build js -// +build js package maphash -import ( - _ "unsafe" // for linkname -) +import _ "unsafe" // for linkname // hashkey is similar how it is defined in runtime/alg.go for Go 1.19 // to be used in hash{32,64}.go to seed the hash function as part of diff --git a/compiler/natives/src/hash/maphash/maphash_test.go b/compiler/natives/src/hash/maphash/maphash_test.go new file mode 100644 index 000000000..a1e9f45a9 --- /dev/null +++ b/compiler/natives/src/hash/maphash/maphash_test.go @@ -0,0 +1,53 @@ +//go:build js + +package maphash + +import "testing" + +//gopherjs:keep-original +func TestSmhasherSmallKeys(t *testing.T) { + if !testing.Short() { + t.Skip("Causes a heap overflow in GopherJS when not --short") + // This test adds a lot of uint64 hashes into a map, + // (16,843,008 for long tests, 65,792 for short tests) + // inside `(s *hashSet) add(h uint64)` with `s.m[h] = struct{}{}`. + // This is to check the number of collisions in the hash function. + } + _gopherjs_original_TestSmhasherSmallKeys(t) +} + +//gopherjs:keep-original +func TestSmhasherZeros(t *testing.T) { + if !testing.Short() { + t.Skip("Too slow when not --short") + // This test creates a byte slice with 262,144 bytes for long tests + // and 1,024 for short tests filled by defualt with zeroes. + // Then it adds [:1], [:2], and so on upto the full slice. + } + _gopherjs_original_TestSmhasherZeros(t) +} + +func TestSmhasherTwoNonzero(t *testing.T) { + // The original skips if `runtime.GOARCH == "wasm"` which means we should skip too. + t.Skip("Too slow on wasm and JS") +} + +func TestSmhasherSparse(t *testing.T) { + // The original skips if `runtime.GOARCH == "wasm"` which means we should skip too. + t.Skip("Too slow on wasm and JS") +} + +func TestSmhasherPermutation(t *testing.T) { + // The original skips if `runtime.GOARCH == "wasm"` which means we should skip too. + t.Skip("Too slow on wasm and JS") +} + +func TestSmhasherAvalanche(t *testing.T) { + // The original skips if `runtime.GOARCH == "wasm"` which means we should skip too. + t.Skip("Too slow on wasm and JS") +} + +func TestSmhasherWindowed(t *testing.T) { + // The original skips if `runtime.GOARCH == "wasm"` which means we should skip too. + t.Skip("Too slow on wasm and JS") +} diff --git a/compiler/natives/src/internal/bytealg/bytealg.go b/compiler/natives/src/internal/bytealg/bytealg.go index dbcc6dec4..15a3c2d70 100644 --- a/compiler/natives/src/internal/bytealg/bytealg.go +++ b/compiler/natives/src/internal/bytealg/bytealg.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bytealg diff --git a/compiler/natives/src/internal/coverage/slicereader/slicereader.go b/compiler/natives/src/internal/coverage/slicereader/slicereader.go index 4346d7c97..5ddf9ab04 100644 --- a/compiler/natives/src/internal/coverage/slicereader/slicereader.go +++ b/compiler/natives/src/internal/coverage/slicereader/slicereader.go @@ -1,5 +1,4 @@ //go:build js -// +build js package slicereader diff --git a/compiler/natives/src/internal/cpu/cpu.go b/compiler/natives/src/internal/cpu/cpu.go index 794a58d6a..a8c25ea8e 100644 --- a/compiler/natives/src/internal/cpu/cpu.go +++ b/compiler/natives/src/internal/cpu/cpu.go @@ -1,5 +1,4 @@ //go:build js -// +build js package cpu diff --git a/compiler/natives/src/internal/fmtsort/fmtsort_test.go b/compiler/natives/src/internal/fmtsort/fmtsort_test.go index f45987d72..50b65bb45 100644 --- a/compiler/natives/src/internal/fmtsort/fmtsort_test.go +++ b/compiler/natives/src/internal/fmtsort/fmtsort_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package fmtsort_test diff --git a/compiler/natives/src/internal/godebug/godebug.go b/compiler/natives/src/internal/godebug/godebug.go index e43006c3f..3d7d719eb 100644 --- a/compiler/natives/src/internal/godebug/godebug.go +++ b/compiler/natives/src/internal/godebug/godebug.go @@ -1,87 +1,8 @@ //go:build js -// +build js package godebug -import ( - "sync" - _ "unsafe" // go:linkname -) - -type Setting struct { - name string - once sync.Once - - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - value *atomicStringPointer -} - -type atomicStringPointer struct { - v *string -} - -func (x *atomicStringPointer) Load() *string { return x.v } -func (x *atomicStringPointer) Store(val *string) { x.v = val } - -func (s *Setting) Value() string { - s.once.Do(func() { - v, ok := cache.Load(s.name) - if !ok { - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - p := new(atomicStringPointer) - p.Store(&empty) - v, _ = cache.LoadOrStore(s.name, p) - } - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - s.value = v.(*atomicStringPointer) - }) - return *s.value.Load() -} +import _ "unsafe" // go:linkname //go:linkname setUpdate runtime.godebug_setUpdate func setUpdate(update func(def, env string)) - -func update(def, env string) { - updateMu.Lock() - defer updateMu.Unlock() - - did := make(map[string]bool) - parse(did, env) - parse(did, def) - - cache.Range(func(name, v any) bool { - if !did[name.(string)] { - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - v.(*atomicStringPointer).Store(&empty) - } - return true - }) -} - -func parse(did map[string]bool, s string) { - end := len(s) - eq := -1 - for i := end - 1; i >= -1; i-- { - if i == -1 || s[i] == ',' { - if eq >= 0 { - name, value := s[i+1:eq], s[eq+1:end] - if !did[name] { - did[name] = true - v, ok := cache.Load(name) - if !ok { - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - p := new(atomicStringPointer) - p.Store(&empty) - v, _ = cache.LoadOrStore(name, p) - } - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - v.(*atomicStringPointer).Store(&value) - } - } - eq = -1 - end = i - } else if s[i] == '=' { - eq = i - } - } -} diff --git a/compiler/natives/src/internal/poll/semaphore.go b/compiler/natives/src/internal/poll/semaphore.go index 5e4f5ea8d..112a37768 100644 --- a/compiler/natives/src/internal/poll/semaphore.go +++ b/compiler/natives/src/internal/poll/semaphore.go @@ -1,11 +1,8 @@ //go:build js -// +build js package poll -import ( - _ "unsafe" // For go:linkname -) +import _ "unsafe" // For go:linkname //go:linkname runtime_Semacquire sync.runtime_Semacquire func runtime_Semacquire(s *uint32) diff --git a/compiler/natives/src/internal/reflectlite/all_test.go b/compiler/natives/src/internal/reflectlite/all_test.go deleted file mode 100644 index 4445189a0..000000000 --- a/compiler/natives/src/internal/reflectlite/all_test.go +++ /dev/null @@ -1,47 +0,0 @@ -//go:build js -// +build js - -package reflectlite_test - -import ( - "testing" - - . "internal/reflectlite" -) - -func TestTypes(t *testing.T) { - for i, tt := range typeTests { - if i == 30 { - continue - } - testReflectType(t, i, Field(ValueOf(tt.i), 0).Type(), tt.s) - } -} - -func TestNameBytesAreAligned(t *testing.T) { - t.Skip("TestNameBytesAreAligned") -} - -// `A` is used with `B[T any]` and is otherwise not needed. -// -//gopherjs:purge for go1.19 without generics -type ( - A struct{} - B[T any] struct{} -) - -// removing the name tests using `B[T any]` for go1.19 without generics -var nameTests = []nameTest{ - {(*int32)(nil), "int32"}, - {(*D1)(nil), "D1"}, - {(*[]D1)(nil), ""}, - {(*chan D1)(nil), ""}, - {(*func() D1)(nil), ""}, - {(*<-chan D1)(nil), ""}, - {(*chan<- D1)(nil), ""}, - {(*any)(nil), ""}, - {(*interface { - F() - })(nil), ""}, - {(*TheNameOfThisTypeIsExactly255BytesLongSoWhenTheCompilerPrependsTheReflectTestPackageNameAndExtraStarTheLinkerRuntimeAndReflectPackagesWillHaveToCorrectlyDecodeTheSecondLengthByte0123456789_0123456789_0123456789_0123456789_0123456789_012345678)(nil), "TheNameOfThisTypeIsExactly255BytesLongSoWhenTheCompilerPrependsTheReflectTestPackageNameAndExtraStarTheLinkerRuntimeAndReflectPackagesWillHaveToCorrectlyDecodeTheSecondLengthByte0123456789_0123456789_0123456789_0123456789_0123456789_012345678"}, -} diff --git a/compiler/natives/src/internal/reflectlite/export_test.go b/compiler/natives/src/internal/reflectlite/export_test.go index d663e65ba..c80ba65a8 100644 --- a/compiler/natives/src/internal/reflectlite/export_test.go +++ b/compiler/natives/src/internal/reflectlite/export_test.go @@ -1,11 +1,8 @@ //go:build js -// +build js package reflectlite -import ( - "unsafe" -) +import "unsafe" // Field returns the i'th field of the struct v. // It panics if v's Kind is not Struct or i is out of range. diff --git a/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go b/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go deleted file mode 100644 index 01504f582..000000000 --- a/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build js -// +build js - -package reflectlite_test - -import ( - "testing" -) - -func TestMirrorWithReflect(t *testing.T) { - t.Skip("TestMirrorWithReflect") -} diff --git a/compiler/natives/src/internal/reflectlite/reflectlite.go b/compiler/natives/src/internal/reflectlite/reflectlite.go index d48f15987..f55eb4437 100644 --- a/compiler/natives/src/internal/reflectlite/reflectlite.go +++ b/compiler/natives/src/internal/reflectlite/reflectlite.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflectlite diff --git a/compiler/natives/src/internal/reflectlite/swapper.go b/compiler/natives/src/internal/reflectlite/swapper.go index b8827c226..b324eb6e7 100644 --- a/compiler/natives/src/internal/reflectlite/swapper.go +++ b/compiler/natives/src/internal/reflectlite/swapper.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflectlite diff --git a/compiler/natives/src/internal/reflectlite/type.go b/compiler/natives/src/internal/reflectlite/type.go index 0a41e862e..949599725 100644 --- a/compiler/natives/src/internal/reflectlite/type.go +++ b/compiler/natives/src/internal/reflectlite/type.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflectlite diff --git a/compiler/natives/src/internal/reflectlite/utils.go b/compiler/natives/src/internal/reflectlite/utils.go index 1941f0d0e..c832bd4cf 100644 --- a/compiler/natives/src/internal/reflectlite/utils.go +++ b/compiler/natives/src/internal/reflectlite/utils.go @@ -1,11 +1,8 @@ //go:build js -// +build js package reflectlite -import ( - "unsafe" -) +import "unsafe" type ChanDir int diff --git a/compiler/natives/src/internal/reflectlite/value.go b/compiler/natives/src/internal/reflectlite/value.go index 32d310723..12f8d292f 100644 --- a/compiler/natives/src/internal/reflectlite/value.go +++ b/compiler/natives/src/internal/reflectlite/value.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflectlite diff --git a/compiler/natives/src/internal/unsafeheader/unsafeheader.go b/compiler/natives/src/internal/unsafeheader/unsafeheader.go index 4a7e43342..f4aa32e18 100644 --- a/compiler/natives/src/internal/unsafeheader/unsafeheader.go +++ b/compiler/natives/src/internal/unsafeheader/unsafeheader.go @@ -1,5 +1,4 @@ //go:build js -// +build js package unsafeheader diff --git a/compiler/natives/src/internal/unsafeheader/unsafeheader_test.go b/compiler/natives/src/internal/unsafeheader/unsafeheader_test.go index 52e814636..2376e3b91 100644 --- a/compiler/natives/src/internal/unsafeheader/unsafeheader_test.go +++ b/compiler/natives/src/internal/unsafeheader/unsafeheader_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package unsafeheader_test diff --git a/compiler/natives/src/io/io_test.go b/compiler/natives/src/io/io_test.go index d746b3709..36d70e3fa 100644 --- a/compiler/natives/src/io/io_test.go +++ b/compiler/natives/src/io/io_test.go @@ -1,14 +1,11 @@ //go:build js -// +build js package io_test -import ( - "testing" -) +import "testing" func TestMultiWriter_WriteStringSingleAlloc(t *testing.T) { - t.Skip() + t.Skip("testing.AllocsPerRun not supported in GopherJS") } func TestMultiReaderFreesExhaustedReaders(t *testing.T) { diff --git a/compiler/natives/src/math/big/big.go b/compiler/natives/src/math/big/big.go deleted file mode 100644 index 25512db31..000000000 --- a/compiler/natives/src/math/big/big.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build js -// +build js - -package big - -// TODO: This is a workaround for https://github.com/gopherjs/gopherjs/issues/652. -// -// Remove after that issue is resolved. -type Word uintptr diff --git a/compiler/natives/src/math/big/big_test.go b/compiler/natives/src/math/big/big_test.go index acad9a043..92989abe9 100644 --- a/compiler/natives/src/math/big/big_test.go +++ b/compiler/natives/src/math/big/big_test.go @@ -1,18 +1,9 @@ //go:build js -// +build js package big import "testing" -func TestBytes(t *testing.T) { - t.Skip("broken") -} - -func TestModSqrt(t *testing.T) { - t.Skip("slow") -} - func TestLinkerGC(t *testing.T) { t.Skip("The test is specific to GC's linker.") } diff --git a/compiler/natives/src/math/bits/bits.go b/compiler/natives/src/math/bits/bits.go index b434603a4..cf4b66df6 100644 --- a/compiler/natives/src/math/bits/bits.go +++ b/compiler/natives/src/math/bits/bits.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bits diff --git a/compiler/natives/src/math/math.go b/compiler/natives/src/math/math.go index b0ed2da0d..fb0777b12 100644 --- a/compiler/natives/src/math/math.go +++ b/compiler/natives/src/math/math.go @@ -1,11 +1,8 @@ //go:build js -// +build js package math -import ( - "github.com/gopherjs/gopherjs/js" -) +import "github.com/gopherjs/gopherjs/js" var ( math = js.Global.Get("Math") diff --git a/compiler/natives/src/math/math_test.go b/compiler/natives/src/math/math_test.go deleted file mode 100644 index eb62dd1d1..000000000 --- a/compiler/natives/src/math/math_test.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build js -// +build js - -package math_test - -import ( - "testing" -) - -// Slightly higher tolerances than upstream, otherwise TestGamma fails. -// TODO: Is there a better way to fix TestGamma? It's weird that only one test -// -// requires increasing tolerances. Perhaps there's a better fix? Maybe we -// should override TestGamma specifically and not the package-wide tolerances, -// because this will cause many other tests to be less accurate. Or maybe this -// is fine? -func close(a, b float64) bool { return tolerance(a, b, 4e-14) } -func veryclose(a, b float64) bool { return tolerance(a, b, 6e-15) } - -func testExp(t *testing.T, Exp func(float64) float64, name string) { - t.Skip("inaccurate") -} diff --git a/compiler/natives/src/math/rand/rand.go b/compiler/natives/src/math/rand/rand.go index 0dfb1b279..728fe492a 100644 --- a/compiler/natives/src/math/rand/rand.go +++ b/compiler/natives/src/math/rand/rand.go @@ -1,5 +1,4 @@ //go:build js -// +build js package rand diff --git a/compiler/natives/src/math/rand/rand_test.go b/compiler/natives/src/math/rand/rand_test.go index 2246dfec4..0073850a0 100644 --- a/compiler/natives/src/math/rand/rand_test.go +++ b/compiler/natives/src/math/rand/rand_test.go @@ -1,14 +1,9 @@ //go:build js -// +build js package rand_test import "testing" -func TestFloat32(t *testing.T) { - t.Skip("slow") -} - func TestConcurrent(t *testing.T) { t.Skip("using nosync") } diff --git a/compiler/natives/src/net/fastrand.go b/compiler/natives/src/net/fastrand.go index 8feafc78f..878f970a5 100644 --- a/compiler/natives/src/net/fastrand.go +++ b/compiler/natives/src/net/fastrand.go @@ -1,11 +1,8 @@ //go:build js -// +build js package net -import ( - _ "unsafe" // For go:linkname -) +import _ "unsafe" // For go:linkname //go:linkname fastrandu runtime.fastrandu func fastrandu() uint diff --git a/compiler/natives/src/net/fd_unix.go b/compiler/natives/src/net/fd_unix.go index d819677d6..706c5ac24 100644 --- a/compiler/natives/src/net/fd_unix.go +++ b/compiler/natives/src/net/fd_unix.go @@ -1,5 +1,4 @@ //go:build js -// +build js package net diff --git a/compiler/natives/src/net/http/client_test.go b/compiler/natives/src/net/http/client_test.go index b3739fc09..fe1e0028d 100644 --- a/compiler/natives/src/net/http/client_test.go +++ b/compiler/natives/src/net/http/client_test.go @@ -2,9 +2,7 @@ package http_test -import ( - "testing" -) +import "testing" func testClientTimeout(t *testing.T, mode testMode) { // The original test expects Client.Timeout error to be returned, but under diff --git a/compiler/natives/src/net/http/clientserver_test.go b/compiler/natives/src/net/http/clientserver_test.go index 39f1a2d73..a1b4d0d0b 100644 --- a/compiler/natives/src/net/http/clientserver_test.go +++ b/compiler/natives/src/net/http/clientserver_test.go @@ -1,16 +1,14 @@ //go:build js && wasm -// +build js,wasm package http_test -import ( - "testing" -) +import "testing" func testTransportGCRequest(t *testing.T, mode testMode, body bool) { t.Skip("The test relies on runtime.SetFinalizer(), which is not supported by GopherJS.") } func testWriteHeaderAfterWrite(t *testing.T, mode testMode, hijack bool) { + // See: https://github.com/gopherjs/gopherjs/issues/1085 t.Skip("GopherJS source maps don't preserve original function names in stack traces, which this test relied on.") } diff --git a/compiler/natives/src/net/http/cookiejar/example_test.go b/compiler/natives/src/net/http/cookiejar/example_test.go index 09de5d0cf..7171867c1 100644 --- a/compiler/natives/src/net/http/cookiejar/example_test.go +++ b/compiler/natives/src/net/http/cookiejar/example_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package cookiejar_test diff --git a/compiler/natives/src/net/http/http.go b/compiler/natives/src/net/http/http.go index f82c0363c..fbd7a8927 100644 --- a/compiler/natives/src/net/http/http.go +++ b/compiler/natives/src/net/http/http.go @@ -1,20 +1,14 @@ //go:build js -// +build js package http import ( "bufio" "bytes" - "context" - "crypto/tls" "errors" "io" - "net" "net/textproto" "strconv" - "sync" - "sync/atomic" "github.com/gopherjs/gopherjs/js" ) @@ -118,30 +112,3 @@ func (t *XHRTransport) CancelRequest(req *Request) { xhr.Call("abort") } } - -type conn struct { - server *Server - cancelCtx context.CancelFunc - rwc net.Conn - remoteAddr string - tlsState *tls.ConnectionState - werr error - r *connReader - bufr *bufio.Reader - bufw *bufio.Writer - lastMethod string - - // temporarily replacement of `atomic.Pointer[response]` for go1.20 without generics. - curReq atomicResponsePointer - - curState atomic.Uint64 - mu sync.Mutex - hijackedv bool -} - -type atomicResponsePointer struct { - v *response -} - -func (x *atomicResponsePointer) Load() *response { return x.v } -func (x *atomicResponsePointer) Store(val *response) { x.v = val } diff --git a/compiler/natives/src/net/http/http_wasm_test.go b/compiler/natives/src/net/http/http_wasm_test.go index d078c0ea3..4c10efd9a 100644 --- a/compiler/natives/src/net/http/http_wasm_test.go +++ b/compiler/natives/src/net/http/http_wasm_test.go @@ -1,5 +1,4 @@ //go:build js && wasm -// +build js,wasm package http diff --git a/compiler/natives/src/net/http/main_test.go b/compiler/natives/src/net/http/main_test.go index bb747d123..2b2e3ec32 100644 --- a/compiler/natives/src/net/http/main_test.go +++ b/compiler/natives/src/net/http/main_test.go @@ -1,5 +1,4 @@ //go:build js && wasm -// +build js,wasm package http_test diff --git a/compiler/natives/src/net/http/server_test.go b/compiler/natives/src/net/http/server_test.go index f55704dcf..7223713d9 100644 --- a/compiler/natives/src/net/http/server_test.go +++ b/compiler/natives/src/net/http/server_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package http_test @@ -8,3 +7,8 @@ import "testing" func TestTimeoutHandlerSuperfluousLogs(t *testing.T) { t.Skip("https://github.com/gopherjs/gopherjs/issues/1085") } + +func TestHTTP2WriteDeadlineExtendedOnNewRequest(t *testing.T) { + // Test depends on httptest.NewUnstartedServer + t.Skip("Network access not supported by GopherJS.") +} diff --git a/compiler/natives/src/net/http/transport_test.go b/compiler/natives/src/net/http/transport_test.go index a173e47e7..2ece96853 100644 --- a/compiler/natives/src/net/http/transport_test.go +++ b/compiler/natives/src/net/http/transport_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package http_test diff --git a/compiler/natives/src/net/netip/export_test.go b/compiler/natives/src/net/netip/export_test.go index 03b7cbe1b..fca2dc07c 100644 --- a/compiler/natives/src/net/netip/export_test.go +++ b/compiler/natives/src/net/netip/export_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package netip diff --git a/compiler/natives/src/net/netip/fuzz_test.go b/compiler/natives/src/net/netip/fuzz_test.go deleted file mode 100644 index f7359c5bb..000000000 --- a/compiler/natives/src/net/netip/fuzz_test.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build js -// +build js - -package netip_test - -import "testing" - -func checkStringParseRoundTrip(t *testing.T, x interface{}, parse interface{}) { - // TODO(nevkontakte): This function requires generics to function. - // Re-enable after https://github.com/gopherjs/gopherjs/issues/1013 is resolved. -} diff --git a/compiler/natives/src/net/netip/netip.go b/compiler/natives/src/net/netip/netip.go index 9d2b8b2d6..61b40fc24 100644 --- a/compiler/natives/src/net/netip/netip.go +++ b/compiler/natives/src/net/netip/netip.go @@ -1,5 +1,4 @@ //go:build js -// +build js package netip diff --git a/compiler/natives/src/net/netip/netip_test.go b/compiler/natives/src/net/netip/netip_test.go index 46b116c00..280929425 100644 --- a/compiler/natives/src/net/netip/netip_test.go +++ b/compiler/natives/src/net/netip/netip_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package netip_test diff --git a/compiler/natives/src/os/file.go b/compiler/natives/src/os/file.go index a3683b8b0..2df42121a 100644 --- a/compiler/natives/src/os/file.go +++ b/compiler/natives/src/os/file.go @@ -1,5 +1,4 @@ //go:build js -// +build js package os diff --git a/compiler/natives/src/os/os.go b/compiler/natives/src/os/os.go index 4adf5bb6e..824e56ca4 100644 --- a/compiler/natives/src/os/os.go +++ b/compiler/natives/src/os/os.go @@ -1,5 +1,4 @@ //go:build js -// +build js package os diff --git a/compiler/natives/src/os/signal/signal.go b/compiler/natives/src/os/signal/signal.go index fe38d22b2..b39453926 100644 --- a/compiler/natives/src/os/signal/signal.go +++ b/compiler/natives/src/os/signal/signal.go @@ -1,5 +1,4 @@ //go:build js -// +build js package signal diff --git a/compiler/natives/src/reflect/example_test.go b/compiler/natives/src/reflect/example_test.go index 0deab2ed5..65abf3dd6 100644 --- a/compiler/natives/src/reflect/example_test.go +++ b/compiler/natives/src/reflect/example_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflect_test diff --git a/compiler/natives/src/reflect/reflect.go b/compiler/natives/src/reflect/reflect.go index ce290ade6..17d3bc35a 100644 --- a/compiler/natives/src/reflect/reflect.go +++ b/compiler/natives/src/reflect/reflect.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflect @@ -1849,26 +1848,32 @@ func valueMethodName() string { var pc [5]uintptr n := runtime.Callers(1, pc[:]) frames := runtime.CallersFrames(pc[:n]) + valueTyp := TypeOf(Value{}) var frame runtime.Frame for more := true; more; { frame, more = frames.Next() name := frame.Function - // Function name extracted from the call stack can be different from // vanilla Go, so is not prefixed by "reflect.Value." as needed by the original. // See https://cs.opensource.google/go/go/+/refs/tags/go1.19.13:src/reflect/value.go;l=173-191 - // Here we try to fix stuff like "Object.$packages.reflect.Q.ptr.SetIterKey" - // into "reflect.Value.SetIterKey". // This workaround may become obsolete after // https://github.com/gopherjs/gopherjs/issues/1085 is resolved. - const prefix = `Object.$packages.reflect.` - if stringsHasPrefix(name, prefix) { - if idx := stringsLastIndex(name, '.'); idx >= 0 { - methodName := name[idx+1:] - if len(methodName) > 0 && 'A' <= methodName[0] && methodName[0] <= 'Z' { - return `reflect.Value.` + methodName - } + methodName := name + if idx := stringsLastIndex(methodName, '.'); idx >= 0 { + methodName = methodName[idx+1:] + } + const midDot = '·' // This is same `midDot` as in gopherjs/compiler/utils.go + if idx := stringsLastIndex(methodName, midDot); idx >= 0 { + methodName = methodName[idx+1:] + } + + // Since function name in the call stack doesn't contain receiver name, + // we are looking for the first exported function name that matches a + // known Value method. + if _, ok := valueTyp.MethodByName(methodName); ok { + if len(methodName) > 0 && 'A' <= methodName[0] && methodName[0] <= 'Z' { + return `reflect.Value.` + methodName } } } diff --git a/compiler/natives/src/reflect/reflect_test.go b/compiler/natives/src/reflect/reflect_test.go index 79bbe5385..03bdcdaac 100644 --- a/compiler/natives/src/reflect/reflect_test.go +++ b/compiler/natives/src/reflect/reflect_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflect_test @@ -285,16 +284,30 @@ func TestMethodCallValueCodePtr(t *testing.T) { t.Skip("methodValueCallCodePtr() is not applicable in GopherJS") } -//gopherjs:purge for go1.19 without generics -type ( - A struct{} - B[T any] struct{} -) +func TestStructOfTooLarge(t *testing.T) { + t.Skip("This test is dependent on field alignment to determine if a struct size would exceed virtual address space.") +} -func TestIssue50208(t *testing.T) { - t.Skip("This test required generics, which are not yet supported: https://github.com/gopherjs/gopherjs/issues/1013") +func TestSetLenCap(t *testing.T) { + t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085") } -func TestStructOfTooLarge(t *testing.T) { - t.Skip("This test is dependent on field alignment to determine if a struct size would exceed virtual address space.") +func TestSetPanic(t *testing.T) { + t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085") +} + +func TestCallPanic(t *testing.T) { + t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085") +} + +func TestValuePanic(t *testing.T) { + t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085") +} + +func TestSetIter(t *testing.T) { + t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085") +} + +func TestGrow(t *testing.T) { + t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085") } diff --git a/compiler/natives/src/reflect/swapper.go b/compiler/natives/src/reflect/swapper.go index 068f984ed..d951ff668 100644 --- a/compiler/natives/src/reflect/swapper.go +++ b/compiler/natives/src/reflect/swapper.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflect diff --git a/compiler/natives/src/regexp/regexp_test.go b/compiler/natives/src/regexp/regexp_test.go index 3a2d58d32..471281fa1 100644 --- a/compiler/natives/src/regexp/regexp_test.go +++ b/compiler/natives/src/regexp/regexp_test.go @@ -1,11 +1,8 @@ //go:build js -// +build js package regexp -import ( - "testing" -) +import "testing" //gopherjs:keep-original func TestOnePassCutoff(t *testing.T) { diff --git a/compiler/natives/src/runtime/debug/debug.go b/compiler/natives/src/runtime/debug/debug.go index b45da62bb..ed9c0e34f 100644 --- a/compiler/natives/src/runtime/debug/debug.go +++ b/compiler/natives/src/runtime/debug/debug.go @@ -1,8 +1,9 @@ //go:build js -// +build js package debug +import "time" + func setGCPercent(int32) int32 { // Not implemented. Return initial setting. return 100 @@ -13,3 +14,26 @@ func setMaxStack(bytes int) int { // The initial setting is 1 GB on 64-bit systems, 250 MB on 32-bit systems. return 250000000 } + +func readGCStats(pauses *[]time.Duration) { + // Not implemented. No GC stats available in this environment. +} + +func freeOSMemory() { + // Not implemented. No OS memory management in this environment. +} + +func setPanicOnFault(bool) bool { + // Not implemented. + return true +} + +func setMaxThreads(int) int { + // Not implemented. + return 10000 +} + +func setMemoryLimit(int64) int64 { + // Not implemented. + return 0 +} diff --git a/compiler/natives/src/runtime/debug/debug_test.go b/compiler/natives/src/runtime/debug/debug_test.go new file mode 100644 index 000000000..7c7bf855f --- /dev/null +++ b/compiler/natives/src/runtime/debug/debug_test.go @@ -0,0 +1,13 @@ +//go:build js + +package debug_test + +import "testing" + +func TestReadGCStats(t *testing.T) { + t.Skip(`This test uses runtime.GC(), which GopherJS doesn't support`) +} + +func TestFreeOSMemory(t *testing.T) { + t.Skip(`This test uses runtime.GC(), which GopherJS doesn't support`) +} diff --git a/compiler/natives/src/runtime/fastrand.go b/compiler/natives/src/runtime/fastrand.go index a5f2bdbb8..b902a5177 100644 --- a/compiler/natives/src/runtime/fastrand.go +++ b/compiler/natives/src/runtime/fastrand.go @@ -1,5 +1,4 @@ //go:build js -// +build js package runtime diff --git a/compiler/natives/src/runtime/pprof/pprof.go b/compiler/natives/src/runtime/pprof/pprof.go index f398ca21b..512ef1a5b 100644 --- a/compiler/natives/src/runtime/pprof/pprof.go +++ b/compiler/natives/src/runtime/pprof/pprof.go @@ -1,5 +1,4 @@ //go:build js -// +build js package pprof diff --git a/compiler/natives/src/runtime/runtime.go b/compiler/natives/src/runtime/runtime.go index 037e150b9..5185f034b 100644 --- a/compiler/natives/src/runtime/runtime.go +++ b/compiler/natives/src/runtime/runtime.go @@ -1,11 +1,8 @@ //go:build js -// +build js package runtime -import ( - "github.com/gopherjs/gopherjs/js" -) +import "github.com/gopherjs/gopherjs/js" const ( GOOS = "js" diff --git a/compiler/natives/src/strconv/atoi.go b/compiler/natives/src/strconv/atoi.go index 63ea9b732..1f26293a3 100644 --- a/compiler/natives/src/strconv/atoi.go +++ b/compiler/natives/src/strconv/atoi.go @@ -1,11 +1,8 @@ //go:build js -// +build js package strconv -import ( - "github.com/gopherjs/gopherjs/js" -) +import "github.com/gopherjs/gopherjs/js" const ( maxInt32 float64 = 1<<31 - 1 diff --git a/compiler/natives/src/strconv/itoa.go b/compiler/natives/src/strconv/itoa.go index c5440c78e..04dac0b71 100644 --- a/compiler/natives/src/strconv/itoa.go +++ b/compiler/natives/src/strconv/itoa.go @@ -1,11 +1,8 @@ //go:build js -// +build js package strconv -import ( - "github.com/gopherjs/gopherjs/js" -) +import "github.com/gopherjs/gopherjs/js" // Itoa in gopherjs is always a 32bit int so the native toString // always handles it successfully. diff --git a/compiler/natives/src/strings/strings.go b/compiler/natives/src/strings/strings.go index f7eef55af..81cae1c4b 100644 --- a/compiler/natives/src/strings/strings.go +++ b/compiler/natives/src/strings/strings.go @@ -1,5 +1,4 @@ //go:build js -// +build js package strings diff --git a/compiler/natives/src/strings/strings_test.go b/compiler/natives/src/strings/strings_test.go index 3b0775e63..72fbae740 100644 --- a/compiler/natives/src/strings/strings_test.go +++ b/compiler/natives/src/strings/strings_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package strings_test diff --git a/compiler/natives/src/sync/atomic/atomic.go b/compiler/natives/src/sync/atomic/atomic.go index d993f3b80..900f55dea 100644 --- a/compiler/natives/src/sync/atomic/atomic.go +++ b/compiler/natives/src/sync/atomic/atomic.go @@ -1,5 +1,4 @@ //go:build js -// +build js package atomic @@ -221,19 +220,28 @@ func sameType(x, y interface{}) bool { return js.InternalObject(x).Get("constructor") == js.InternalObject(y).Get("constructor") } -// Override pointer so that the type check in the source code is satisfied -// but remove the fields and methods for go1.20 without generics. -// See https://cs.opensource.google/go/go/+/refs/tags/go1.20.14:src/sync/atomic/type.go;l=40 -type Pointer[T any] struct{} +type Pointer[T any] struct { + v *T +} -//gopherjs:purge for go1.20 without generics -func (x *Pointer[T]) Load() *T +func (x *Pointer[T]) Load() *T { + return x.v +} -//gopherjs:purge for go1.20 without generics -func (x *Pointer[T]) Store(val *T) +func (x *Pointer[T]) Store(val *T) { + x.v = val +} -//gopherjs:purge for go1.20 without generics -func (x *Pointer[T]) Swap(new *T) (old *T) +func (x *Pointer[T]) Swap(new *T) (old *T) { + old = x.v + x.v = new + return old +} -//gopherjs:purge for go1.20 without generics -func (x *Pointer[T]) CompareAndSwap(old, new *T) (swapped bool) +func (x *Pointer[T]) CompareAndSwap(old, new *T) bool { + if x.v == old { + x.v = new + return true + } + return false +} diff --git a/compiler/natives/src/sync/atomic/atomic_test.go b/compiler/natives/src/sync/atomic/atomic_test.go index e1ec6086c..ee5a10be0 100644 --- a/compiler/natives/src/sync/atomic/atomic_test.go +++ b/compiler/natives/src/sync/atomic/atomic_test.go @@ -1,54 +1,11 @@ //go:build js -// +build js package atomic_test import ( "testing" - "unsafe" ) -//gopherjs:purge for go1.19 without generics -func testPointers() []unsafe.Pointer {} - -func TestSwapPointer(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestSwapPointerMethod(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestCompareAndSwapPointer(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestCompareAndSwapPointerMethod(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestLoadPointer(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestLoadPointerMethod(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestStorePointer(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestStorePointerMethod(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -//gopherjs:purge for go1.19 without generics -func hammerStoreLoadPointer(t *testing.T, paddr unsafe.Pointer) {} - -//gopherjs:purge for go1.19 without generics -func hammerStoreLoadPointerMethod(t *testing.T, paddr unsafe.Pointer) {} - func TestHammerStoreLoad(t *testing.T) { t.Skip("use of unsafe") } @@ -61,13 +18,6 @@ func TestAutoAligned64(t *testing.T) { t.Skip("GopherJS emulates atomics, which makes alignment irrelevant.") } -func TestNilDeref(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -//gopherjs:purge for go1.19 without generics -type List struct{} - func TestHammer32(t *testing.T) { t.Skip("use of unsafe") } diff --git a/compiler/natives/src/sync/cond.go b/compiler/natives/src/sync/cond.go index 916ace8f7..1f94d5284 100644 --- a/compiler/natives/src/sync/cond.go +++ b/compiler/natives/src/sync/cond.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync diff --git a/compiler/natives/src/sync/cond_test.go b/compiler/natives/src/sync/cond_test.go index 3b286ba49..e530b9625 100644 --- a/compiler/natives/src/sync/cond_test.go +++ b/compiler/natives/src/sync/cond_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync_test diff --git a/compiler/natives/src/sync/map.go b/compiler/natives/src/sync/map.go deleted file mode 100644 index 3f81b9b31..000000000 --- a/compiler/natives/src/sync/map.go +++ /dev/null @@ -1,48 +0,0 @@ -//go:build js -// +build js - -package sync - -type Map struct { - mu Mutex - - // replaced atomic.Pointer[readOnly] for go1.20 without generics. - read atomicReadOnlyPointer - - dirty map[any]*entry - misses int -} - -type atomicReadOnlyPointer struct { - v *readOnly -} - -func (x *atomicReadOnlyPointer) Load() *readOnly { return x.v } -func (x *atomicReadOnlyPointer) Store(val *readOnly) { x.v = val } - -type entry struct { - - // replaced atomic.Pointer[any] for go1.20 without generics. - p atomicAnyPointer -} - -type atomicAnyPointer struct { - v *any -} - -func (x *atomicAnyPointer) Load() *any { return x.v } -func (x *atomicAnyPointer) Store(val *any) { x.v = val } - -func (x *atomicAnyPointer) Swap(new *any) *any { - old := x.v - x.v = new - return old -} - -func (x *atomicAnyPointer) CompareAndSwap(old, new *any) bool { - if x.v == old { - x.v = new - return true - } - return false -} diff --git a/compiler/natives/src/sync/map_test.go b/compiler/natives/src/sync/map_test.go index 432096071..5e8460411 100644 --- a/compiler/natives/src/sync/map_test.go +++ b/compiler/natives/src/sync/map_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync_test diff --git a/compiler/natives/src/sync/pool.go b/compiler/natives/src/sync/pool.go index 9d3825e14..c865ca5dc 100644 --- a/compiler/natives/src/sync/pool.go +++ b/compiler/natives/src/sync/pool.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync diff --git a/compiler/natives/src/sync/pool_test.go b/compiler/natives/src/sync/pool_test.go index ea35fd136..09ab79b32 100644 --- a/compiler/natives/src/sync/pool_test.go +++ b/compiler/natives/src/sync/pool_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync_test diff --git a/compiler/natives/src/sync/sync.go b/compiler/natives/src/sync/sync.go index b37a9e9f9..1aa1c7adb 100644 --- a/compiler/natives/src/sync/sync.go +++ b/compiler/natives/src/sync/sync.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync diff --git a/compiler/natives/src/sync/waitgroup.go b/compiler/natives/src/sync/waitgroup.go index e1f20eeb6..99cb49ff7 100644 --- a/compiler/natives/src/sync/waitgroup.go +++ b/compiler/natives/src/sync/waitgroup.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync diff --git a/compiler/natives/src/syscall/fs_js.go b/compiler/natives/src/syscall/fs_js.go index 5a0a5a64d..23c2b2145 100644 --- a/compiler/natives/src/syscall/fs_js.go +++ b/compiler/natives/src/syscall/fs_js.go @@ -2,9 +2,7 @@ package syscall -import ( - "syscall/js" -) +import "syscall/js" // fsCall emulates a file system-related syscall via a corresponding NodeJS fs // API. diff --git a/compiler/natives/src/syscall/js/export_test.go b/compiler/natives/src/syscall/js/export_test.go index 8f030c4d7..fb133a150 100644 --- a/compiler/natives/src/syscall/js/export_test.go +++ b/compiler/natives/src/syscall/js/export_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package js diff --git a/compiler/natives/src/syscall/js/js.go b/compiler/natives/src/syscall/js/js.go index c0c18a614..03a09fde8 100644 --- a/compiler/natives/src/syscall/js/js.go +++ b/compiler/natives/src/syscall/js/js.go @@ -1,5 +1,4 @@ //go:build js -// +build js package js diff --git a/compiler/natives/src/syscall/js/js_test.go b/compiler/natives/src/syscall/js/js_test.go index 999266da2..4c6e091df 100644 --- a/compiler/natives/src/syscall/js/js_test.go +++ b/compiler/natives/src/syscall/js/js_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package js_test diff --git a/compiler/natives/src/syscall/legacy.go b/compiler/natives/src/syscall/legacy.go index beb99eb78..ad1df8bda 100644 --- a/compiler/natives/src/syscall/legacy.go +++ b/compiler/natives/src/syscall/legacy.go @@ -2,9 +2,7 @@ package syscall -import ( - "github.com/gopherjs/gopherjs/js" -) +import "github.com/gopherjs/gopherjs/js" var ( syscallModule *js.Object diff --git a/compiler/natives/src/testing/allocs_test.go b/compiler/natives/src/testing/allocs_test.go index 54c2f545b..e1f4cffba 100644 --- a/compiler/natives/src/testing/allocs_test.go +++ b/compiler/natives/src/testing/allocs_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package testing_test diff --git a/compiler/natives/src/testing/example.go b/compiler/natives/src/testing/example.go index b80ae2e99..ed137b8ce 100644 --- a/compiler/natives/src/testing/example.go +++ b/compiler/natives/src/testing/example.go @@ -1,5 +1,4 @@ //go:build js -// +build js package testing diff --git a/compiler/natives/src/testing/helper_test.go b/compiler/natives/src/testing/helper_test.go deleted file mode 100644 index 6815fd651..000000000 --- a/compiler/natives/src/testing/helper_test.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build js -// +build js - -package testing - -func TestTBHelper(t *T) { - t.Skip("GopherJS does not support generics yet.") -} diff --git a/compiler/natives/src/testing/helperfuncs_test.go b/compiler/natives/src/testing/helperfuncs_test.go deleted file mode 100644 index 54a1ee737..000000000 --- a/compiler/natives/src/testing/helperfuncs_test.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build js -// +build js - -package testing - -//gopherjs:purge for go1.19 without generics -func genericHelper[G any](t *T, msg string) - -//gopherjs:purge for go1.19 without generics -var genericIntHelper = genericHelper[int] - -//gopherjs:purge for go1.19 without generics (uses genericHelper) -func testHelper(t *T) diff --git a/compiler/natives/src/testing/quick/quick.go b/compiler/natives/src/testing/quick/quick.go index 51fa843aa..bd9bccf3e 100644 --- a/compiler/natives/src/testing/quick/quick.go +++ b/compiler/natives/src/testing/quick/quick.go @@ -13,12 +13,11 @@ var maxCountCap int = 0 // reasonable amount of time. This is a better compromise than disabling a slow // test entirely. // -// //gopherjs:keep-original -// func TestFoo(t *testing.T) { -// t.Cleanup(quick.GopherJSInternalMaxCountCap(100)) -// _gopherjs_original_TestFoo(t) -// } - +// //gopherjs:keep-original +// func TestFoo(t *testing.T) { +// t.Cleanup(quick.GopherJSInternalMaxCountCap(100)) +// _gopherjs_original_TestFoo(t) +// } func GopherJSInternalMaxCountCap(newCap int) (restore func()) { previousCap := maxCountCap maxCountCap = newCap diff --git a/compiler/natives/src/testing/sub_test.go b/compiler/natives/src/testing/sub_test.go index 1e9a79e47..300045db3 100644 --- a/compiler/natives/src/testing/sub_test.go +++ b/compiler/natives/src/testing/sub_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package testing diff --git a/compiler/natives/src/text/template/template.go b/compiler/natives/src/text/template/template.go index 056fe9c85..d5cf5de56 100644 --- a/compiler/natives/src/text/template/template.go +++ b/compiler/natives/src/text/template/template.go @@ -1,5 +1,4 @@ //go:build js -// +build js package template diff --git a/compiler/natives/src/time/export_test.go b/compiler/natives/src/time/export_test.go deleted file mode 100644 index 5cd3fc6ab..000000000 --- a/compiler/natives/src/time/export_test.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build js -// +build js - -package time - -// replaced `parseRFC3339[string]` for go1.20 temporarily without generics. -var ParseRFC3339 = func(s string, local *Location) (Time, bool) { - return parseRFC3339(s, local) -} diff --git a/compiler/natives/src/time/format.go b/compiler/natives/src/time/format.go deleted file mode 100644 index 0e1594c19..000000000 --- a/compiler/natives/src/time/format.go +++ /dev/null @@ -1,79 +0,0 @@ -//go:build js -// +build js - -package time - -// copied and replaced for go1.20 temporarily without generics. -func atoi(sAny any) (x int, err error) { - s := asBytes(sAny) - neg := false - if len(s) > 0 && (s[0] == '-' || s[0] == '+') { - neg = s[0] == '-' - s = s[1:] - } - q, remStr, err := leadingInt(s) - rem := []byte(remStr) - x = int(q) - if err != nil || len(rem) > 0 { - return 0, atoiError - } - if neg { - x = -x - } - return x, nil -} - -// copied and replaced for go1.20 temporarily without generics. -func isDigit(sAny any, i int) bool { - s := asBytes(sAny) - if len(s) <= i { - return false - } - c := s[i] - return '0' <= c && c <= '9' -} - -// copied and replaced for go1.20 temporarily without generics. -func parseNanoseconds(sAny any, nbytes int) (ns int, rangeErrString string, err error) { - value := asBytes(sAny) - if !commaOrPeriod(value[0]) { - err = errBad - return - } - if nbytes > 10 { - value = value[:10] - nbytes = 10 - } - if ns, err = atoi(value[1:nbytes]); err != nil { - return - } - if ns < 0 { - rangeErrString = "fractional second" - return - } - scaleDigits := 10 - nbytes - for i := 0; i < scaleDigits; i++ { - ns *= 10 - } - return -} - -// copied and replaced for go1.20 temporarily without generics. -func leadingInt(sAny any) (x uint64, rem string, err error) { - s := asBytes(sAny) - i := 0 - for ; i < len(s); i++ { - c := s[i] - if c < '0' || c > '9' { - break - } - if x > 1<<63/10 { - return 0, rem, errLeadingInt - } - x = x*10 + uint64(c) - '0' - if x > 1<<63 { - return 0, rem, errLeadingInt - } - } - return x, string(s[i:]), nil -} diff --git a/compiler/natives/src/time/format_rfc3339.go b/compiler/natives/src/time/format_rfc3339.go deleted file mode 100644 index 7c69bfc95..000000000 --- a/compiler/natives/src/time/format_rfc3339.go +++ /dev/null @@ -1,85 +0,0 @@ -//go:build js -// +build js - -package time - -import "errors" - -// added for go1.20 temporarily without generics. -func asBytes(s any) []byte { - switch t := s.(type) { - case []byte: - return t - case string: - return []byte(t) - default: - panic(errors.New(`unexpected type passed to asBytes, expected string or []bytes`)) - } -} - -// copied and replaced for go1.20 temporarily without generics. -func parseRFC3339(sAny any, local *Location) (Time, bool) { - s := asBytes(sAny) - ok := true - parseUint := func(s []byte, min, max int) (x int) { - for _, c := range s { - if c < '0' || '9' < c { - ok = false - return min - } - x = x*10 + int(c) - '0' - } - if x < min || max < x { - ok = false - return min - } - return x - } - - if len(s) < len("2006-01-02T15:04:05") { - return Time{}, false - } - year := parseUint(s[0:4], 0, 9999) - month := parseUint(s[5:7], 1, 12) - day := parseUint(s[8:10], 1, daysIn(Month(month), year)) - hour := parseUint(s[11:13], 0, 23) - min := parseUint(s[14:16], 0, 59) - sec := parseUint(s[17:19], 0, 59) - if !ok || !(s[4] == '-' && s[7] == '-' && s[10] == 'T' && s[13] == ':' && s[16] == ':') { - return Time{}, false - } - s = s[19:] - - var nsec int - if len(s) >= 2 && s[0] == '.' && isDigit(s, 1) { - n := 2 - for ; n < len(s) && isDigit(s, n); n++ { - } - nsec, _, _ = parseNanoseconds(s, n) - s = s[n:] - } - - t := Date(year, Month(month), day, hour, min, sec, nsec, UTC) - if len(s) != 1 || s[0] != 'Z' { - if len(s) != len("-07:00") { - return Time{}, false - } - hr := parseUint(s[1:3], 0, 23) - mm := parseUint(s[4:6], 0, 59) - if !ok || !((s[0] == '-' || s[0] == '+') && s[3] == ':') { - return Time{}, false - } - zoneOffset := (hr*60 + mm) * 60 - if s[0] == '-' { - zoneOffset *= -1 - } - t.addSec(-int64(zoneOffset)) - - if _, offset, _, _, _ := local.lookup(t.unixSec()); offset == zoneOffset { - t.setLoc(local) - } else { - t.setLoc(FixedZone("", zoneOffset)) - } - } - return t, true -} diff --git a/compiler/natives/src/time/time.go b/compiler/natives/src/time/time.go index 40c596e68..3b0cb0ef4 100644 --- a/compiler/natives/src/time/time.go +++ b/compiler/natives/src/time/time.go @@ -1,5 +1,4 @@ //go:build js -// +build js package time diff --git a/compiler/natives/src/time/time_test.go b/compiler/natives/src/time/time_test.go index 5d4119cc6..777fe11fe 100644 --- a/compiler/natives/src/time/time_test.go +++ b/compiler/natives/src/time/time_test.go @@ -1,16 +1,13 @@ //go:build js -// +build js package time_test -import ( - "testing" -) +import "testing" -func TestSleep(t *testing.T) { - t.Skip("time.Now() is not accurate enough for the test") +func TestZeroTimer(t *testing.T) { + t.Skip(`This test is very slow (about 19 mins)`) } -func TestEnvTZUsage(t *testing.T) { - t.Skip("TZ environment variable in not applicable in the browser context.") +func TestSleep(t *testing.T) { + t.Skip("time.Now() is not accurate enough for the test") } diff --git a/compiler/natives/src/time/zoneinfo_js.go b/compiler/natives/src/time/zoneinfo_js.go index 0101b95f9..2932b9cf0 100644 --- a/compiler/natives/src/time/zoneinfo_js.go +++ b/compiler/natives/src/time/zoneinfo_js.go @@ -1,5 +1,4 @@ //go:build js -// +build js package time diff --git a/compiler/natives/src/time/zoneinfo_unix_test.go b/compiler/natives/src/time/zoneinfo_unix_test.go new file mode 100644 index 000000000..8cb20416e --- /dev/null +++ b/compiler/natives/src/time/zoneinfo_unix_test.go @@ -0,0 +1,9 @@ +//go:build js && unix && !ios && !android + +package time_test + +import "testing" + +func TestEnvTZUsage(t *testing.T) { + t.Skip("TZ environment variable in not applicable in the browser context.") +} diff --git a/compiler/natives/src/unicode/unicode.go b/compiler/natives/src/unicode/unicode.go index a622c32f1..806f72bdb 100644 --- a/compiler/natives/src/unicode/unicode.go +++ b/compiler/natives/src/unicode/unicode.go @@ -1,5 +1,4 @@ //go:build js -// +build js package unicode diff --git a/compiler/natives/src/vendor/golang.org/x/crypto/internal/alias/alias.go b/compiler/natives/src/vendor/golang.org/x/crypto/internal/alias/alias.go index e6bb87536..b6b1bc2bb 100644 --- a/compiler/natives/src/vendor/golang.org/x/crypto/internal/alias/alias.go +++ b/compiler/natives/src/vendor/golang.org/x/crypto/internal/alias/alias.go @@ -1,5 +1,4 @@ //go:build js -// +build js package alias diff --git a/compiler/package.go b/compiler/package.go index 34387b5ab..2dcf19d60 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -1,26 +1,26 @@ package compiler import ( - "bytes" - "encoding/json" "fmt" "go/ast" "go/token" "go/types" "strings" - "time" - "github.com/gopherjs/gopherjs/compiler/analysis" + "golang.org/x/tools/go/types/typeutil" + + "github.com/gopherjs/gopherjs/compiler/internal/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/sources" "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" + "github.com/gopherjs/gopherjs/internal/errorList" ) // pkgContext maintains compiler context for a specific package. type pkgContext struct { *analysis.Info + dce.Collector additionalSelections map[*ast.SelectorExpr]typesutil.Selection typesCtx *types.Context @@ -35,10 +35,9 @@ type pkgContext struct { anonTypeMap typeutil.Map escapingVars map[*types.Var]bool indentation int - dependencies map[types.Object]bool minify bool fileSet *token.FileSet - errList ErrorList + errList errorList.ErrorList instanceSet *typeparams.PackageInstanceSets } @@ -53,6 +52,15 @@ func (pc *pkgContext) isMain() bool { // JavaScript code (as defined for `var` declarations). type funcContext struct { *analysis.FuncInfo + // Function instance this context corresponds to, or zero if the context is + // top-level or doesn't correspond to a function. For function literals, this + // is a synthetic object that assigns a unique identity to the function. + instance typeparams.Instance + // JavaScript identifier assigned to the function object (the word after the + // "function" keyword in the generated code). This identifier can be used + // within the function scope to reference the function object. It will also + // appear in the stack trace. + funcRef string // Surrounding package context. pkgCtx *pkgContext // Function context, surrounding this function definition. For package-level @@ -104,20 +112,15 @@ type funcContext struct { typeResolver *typeparams.Resolver // Mapping from function-level objects to JS variable names they have been assigned. objectNames map[types.Object]string + // Number of function literals encountered within the current function context. + funcLitCounter int } -func newRootCtx(tContext *types.Context, srcs sources, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(*types.Func) bool, minify bool) *funcContext { - tc := typeparams.Collector{ - TContext: tContext, - Info: typesInfo, - Instances: &typeparams.PackageInstanceSets{}, - } - tc.Scan(typesPkg, srcs.Files...) - pkgInfo := analysis.AnalyzePkg(srcs.Files, srcs.FileSet, typesInfo, typesPkg, isBlocking) +func newRootCtx(tContext *types.Context, srcs *sources.Sources, minify bool) *funcContext { funcCtx := &funcContext{ - FuncInfo: pkgInfo.InitFuncInfo, + FuncInfo: srcs.TypeInfo.InitFuncInfo, pkgCtx: &pkgContext{ - Info: pkgInfo, + Info: srcs.TypeInfo, additionalSelections: make(map[*ast.SelectorExpr]typesutil.Selection), typesCtx: tContext, @@ -125,10 +128,9 @@ func newRootCtx(tContext *types.Context, srcs sources, typesInfo *types.Info, ty varPtrNames: make(map[*types.Var]string), escapingVars: make(map[*types.Var]bool), indentation: 1, - dependencies: nil, minify: minify, fileSet: srcs.FileSet, - instanceSet: tc.Instances, + instanceSet: srcs.TypeInfo.InstanceSets, }, allVars: make(map[string]int), flowDatas: map[*types.Label]*flowData{nil: {}}, @@ -148,42 +150,11 @@ 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 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) -} - -// isBlocking returns true if an _imported_ function is blocking. It will panic -// if the function decl is not found in the imported package or the package -// hasn't been compiled yet. -// -// Note: see analysis.FuncInfo.Blocking if you need to determine if a function -// in the _current_ package is blocking. Usually available via functionContext -// object. -func (ic *ImportContext) isBlocking(f *types.Func) bool { - archive, err := ic.Import(f.Pkg().Path()) - if err != nil { - panic(err) - } - fullName := f.FullName() - for _, d := range archive.Declarations { - if string(d.FullName) == fullName { - return d.Blocking - } - } - panic(bailout(fmt.Errorf("can't determine if function %s is blocking: decl not found in package archive", fullName))) -} - // 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) { +// Provided sources must be prepared so that the type information has been determined, +// and the source files have been sorted by name to ensure reproducible JavaScript output. +func Compile(srcs *sources.Sources, tContext *types.Context, minify bool) (_ *Archive, err error) { defer func() { e := recover() if e == nil { @@ -191,39 +162,15 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor } if fe, ok := bailingOut(e); ok { // Orderly bailout, return whatever clues we already have. - fmt.Fprintf(fe, `building package %q`, importPath) + fmt.Fprintf(fe, `building package %q`, srcs.ImportPath) err = fe return } // Some other unexpected panic, catch the stack trace and return as an error. - err = bailout(fmt.Errorf("unexpected compiler panic while building package %q: %v", importPath, e)) + err = bailout(fmt.Errorf("unexpected compiler panic while building package %q: %v", srcs.ImportPath, e)) }() - srcs := sources{ - ImportPath: importPath, - Files: files, - FileSet: fileSet, - }.Sort() - - tContext := types.NewContext() - typesInfo, typesPkg, err := srcs.TypeCheck(importContext, tContext) - if err != nil { - return nil, 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) - } - 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 - } - - srcs = srcs.Simplified(typesInfo) - - rootCtx := newRootCtx(tContext, srcs, typesInfo, typesPkg, importContext.isBlocking, minify) + rootCtx := newRootCtx(tContext, srcs, minify) importedPaths, importDecls := rootCtx.importDecls() @@ -265,28 +212,81 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor return nil, rootCtx.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: srcs.ImportPath, - Name: typesPkg.Name(), + Name: srcs.Package.Name(), Imports: importedPaths, - ExportData: exportData.Bytes(), + Package: srcs.Package, Declarations: allDecls, - FileSet: encodedFileSet.Bytes(), + FileSet: srcs.FileSet, Minified: minify, - GoLinknames: goLinknames, - BuildTime: time.Now(), + GoLinknames: srcs.GoLinknames, }, nil } +// PrepareAllSources prepares all sources for compilation by +// parsing go linknames, type checking, sorting, simplifying, and +// performing cross package analysis. +// The results are stored in the provided sources. +// +// All sources must be given at the same time for cross package analysis to +// work correctly. For consistency, the sources should be sorted by import path. +func PrepareAllSources(allSources []*sources.Sources, importer sources.Importer, tContext *types.Context) error { + // Sort the files by name in each source to ensure consistent order of processing. + for _, srcs := range allSources { + srcs.Sort() + } + + // This will be performed recursively for all dependencies + // to get the packages for the sources. + // Since some packages might not be recursively reached via the root sources, + // e.g. runtime, we need to try to TypeCheck all of them here. + // Any sources that have already been type checked will no-op. + for _, srcs := range allSources { + if err := srcs.TypeCheck(importer, sizes32, tContext); err != nil { + return err + } + } + + // Extract all go:linkname compiler directives from the package source. + for _, srcs := range allSources { + if err := srcs.ParseGoLinknames(); err != nil { + return err + } + } + + // Simply the source files. + for _, srcs := range allSources { + srcs.Simplify() + } + + // Collect all the generic type instances from all the packages. + // This must be done for all sources prior to any analysis. + instances := &typeparams.PackageInstanceSets{} + tc := &typeparams.Collector{ + TContext: tContext, + Instances: instances, + } + for _, srcs := range allSources { + srcs.CollectInstances(tc) + } + tc.Finish() + + // Analyze the package to determine type parameters instances, blocking, + // and other type information. This will not populate the information. + for _, srcs := range allSources { + srcs.Analyze(importer, tContext, instances) + } + + // Propagate the analysis information across all packages. + allInfo := make([]*analysis.Info, len(allSources)) + for i, src := range allSources { + allInfo[i] = src.TypeInfo + } + analysis.PropagateAnalysis(allInfo) + return nil +} + func (fc *funcContext) initArgs(ty types.Type) string { switch t := ty.(type) { case *types.Array: @@ -328,11 +328,17 @@ func (fc *funcContext) initArgs(ty types.Type) string { if !field.Exported() { pkgPath = field.Pkg().Path() } - fields[i] = fmt.Sprintf(`{prop: "%s", name: %s, embedded: %t, exported: %t, typ: %s, tag: %s}`, fieldName(t, i), encodeString(field.Name()), field.Anonymous(), field.Exported(), fc.typeName(field.Type()), encodeString(t.Tag(i))) + ft := fc.fieldType(t, i) + fields[i] = fmt.Sprintf(`{prop: "%s", name: %s, embedded: %t, exported: %t, typ: %s, tag: %s}`, + fieldName(t, i), encodeString(field.Name()), field.Anonymous(), field.Exported(), fc.typeName(ft), encodeString(t.Tag(i))) } return fmt.Sprintf(`"%s", [%s]`, pkgPath, strings.Join(fields, ", ")) case *types.TypeParam: - err := bailout(fmt.Errorf(`%v has unexpected generic type parameter %T`, ty, ty)) + tr := fc.typeResolver.Substitute(ty) + if tr != ty { + return fc.initArgs(tr) + } + err := bailout(fmt.Errorf(`"%v" has unexpected generic type parameter %T`, ty, ty)) panic(err) default: err := bailout(fmt.Errorf("%v has unexpected type %T", ty, ty)) diff --git a/compiler/prelude/jsmapping.js b/compiler/prelude/jsmapping.js index f5317d626..db2c4bc64 100644 --- a/compiler/prelude/jsmapping.js +++ b/compiler/prelude/jsmapping.js @@ -392,16 +392,13 @@ var $internalize = (v, t, recv, seen, makeWrapper) => { } var n = new t.ptr(); for (var i = 0; i < t.fields.length; i++) { - var f = t.fields[i]; - - if (!f.exported) { - continue; - } - var jsProp = v[f.name]; - - n[f.prop] = $internalize(jsProp, f.typ, recv, seen, makeWrapper); + var f = t.fields[i]; + if (!f.exported) { + continue; + } + var jsProp = v[f.name]; + n[f.prop] = $internalize(jsProp, f.typ, recv, seen, makeWrapper); } - return n; } $throwRuntimeError("cannot internalize " + t.string); diff --git a/compiler/prelude/prelude.js b/compiler/prelude/prelude.js index 85db83727..d32f6d9bf 100644 --- a/compiler/prelude/prelude.js +++ b/compiler/prelude/prelude.js @@ -75,10 +75,10 @@ if (($global.process !== undefined) && $global.require) { } var $println = console.log -var $initAllLinknames = () => { +var $callForAllPackages = (methodName) => { var names = $keys($packages); for (var i = 0; i < names.length; i++) { - var f = $packages[names[i]]["$initLinknames"]; + var f = $packages[names[i]][methodName]; if (typeof f == 'function') { f(); } diff --git a/compiler/sources.go b/compiler/sources.go deleted file mode 100644 index e6c3710f4..000000000 --- a/compiler/sources.go +++ /dev/null @@ -1,123 +0,0 @@ -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/sources/sources.go b/compiler/sources/sources.go new file mode 100644 index 000000000..5dcf9e2a5 --- /dev/null +++ b/compiler/sources/sources.go @@ -0,0 +1,273 @@ +package sources + +import ( + "go/ast" + "go/token" + "go/types" + "sort" + "strings" + + "github.com/neelance/astrewrite" + + "github.com/gopherjs/gopherjs/compiler/internal/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/jsFile" + "github.com/gopherjs/gopherjs/compiler/linkname" + "github.com/gopherjs/gopherjs/internal/errorList" +) + +// Sources is a slice of parsed Go sources and additional data for a package. +// +// 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. + // Otherwise this must be the absolute import path for a package. + ImportPath string + + // Dir is the directory containing package sources + Dir string + + // Files is the parsed and augmented Go AST files for the package. + Files []*ast.File + + // FileSet is the file set for the parsed files. + FileSet *token.FileSet + + // JSFiles is the JavaScript files that are part of the package. + JSFiles []jsFile.JSFile + + // TypeInfo is the type information for this package. + // This is nil until set by Analyze. + TypeInfo *analysis.Info + + // baseInfo is the base type information for this package. + // This is nil until set by TypeCheck. + baseInfo *types.Info + + // Package is the types package for these source files. + // This is nil until set by TypeCheck. + Package *types.Package + + // GoLinknames is the set of Go linknames for this package. + // This is nil until set by ParseGoLinknames. + GoLinknames []linkname.GoLinkname +} + +type Importer func(path, srcDir string) (*Sources, error) + +// sort the Go 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 Files slice. +func (s *Sources) Sort() { + sort.Slice(s.Files, func(i, j int) bool { + return s.getFileName(s.Files[i]) > s.getFileName(s.Files[j]) + }) +} + +func (s *Sources) getFileName(file *ast.File) string { + return s.FileSet.File(file.Pos()).Name() +} + +// Simplify processed each Files entry with astrewrite.Simplify. +// +// Note this function mutates the original Files slice. +// This must be called after TypeCheck and before analyze since +// this will change the pointers in the AST. For example, the pointers +// to function literals will change, making it impossible to find them +// in the type information, if analyze is called first. +func (s *Sources) Simplify() { + for i, file := range s.Files { + s.Files[i] = astrewrite.Simplify(file, s.baseInfo, false) + } +} + +// TypeCheck the sources. Returns information about declared package types and +// type information for the supplied AST. +// This will set the Package field on the Sources. +// +// If the Package field is not nil, e.g. this function has already been run, +// this will be a no-op. +// +// This must be called prior to simplify to get the types.Info used by simplify. +func (s *Sources) TypeCheck(importer Importer, sizes types.Sizes, tContext *types.Context) error { + if s.Package != nil && s.baseInfo != nil { + // type checking has already been done so return early. + return nil + } + + 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.ErrorList + + pkgImporter := &packageImporter{ + srcDir: s.Dir, + importer: importer, + sizes: sizes, + tContext: tContext, + } + + config := &types.Config{ + Context: tContext, + Importer: pkgImporter, + Sizes: sizes, + 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 pkgImporter.Errors.ErrOrNil() != nil { + return pkgImporter.Errors.Trim(errLimit).ErrOrNil() + } + // Return any other type errors. + if typeErrs.ErrOrNil() != nil { + return typeErrs.Trim(errLimit).ErrOrNil() + } + // Any general errors that may have occurred during type checking. + if err != nil { + return err + } + + s.baseInfo = typesInfo + s.Package = typesPkg + return nil +} + +// CollectInstances will determine the type parameters instances for the package. +// +// This must be called before Analyze to have the type parameters instances +// needed during analysis. +// +// Note that once all the sources are collected, the collector needs to be +// finished to ensure all the instances are collected. +func (s *Sources) CollectInstances(tc *typeparams.Collector) { + tc.Scan(s.baseInfo, s.Package, s.Files...) +} + +// Analyze will determine the type parameters instances, blocking, +// and other type information for the package. +// This will set the TypeInfo and Instances fields on the Sources. +// +// This must be called after to simplify to ensure the pointers +// in the AST are still valid. +// The instances must be collected prior to this call. +// +// Note that at the end of this call the analysis information +// has NOT been propagated across packages yet. +func (s *Sources) Analyze(importer Importer, tContext *types.Context, instances *typeparams.PackageInstanceSets) { + infoImporter := func(path string) (*analysis.Info, error) { + srcs, err := importer(path, s.Dir) + if err != nil { + return nil, err + } + return srcs.TypeInfo, nil + } + s.TypeInfo = analysis.AnalyzePkg(s.Files, s.FileSet, s.baseInfo, tContext, s.Package, instances, infoImporter) +} + +// ParseGoLinknames extracts all //go:linkname compiler directive from the sources. +// +// This will set the GoLinknames field on the Sources. +func (s *Sources) ParseGoLinknames() error { + goLinknames := []linkname.GoLinkname{} + var errs errorList.ErrorList + for _, file := range s.Files { + found, err := linkname.ParseGoLinknames(s.FileSet, s.ImportPath, file) + errs = errs.Append(err) + goLinknames = append(goLinknames, found...) + } + if err := errs.ErrOrNil(); err != nil { + return err + } + s.GoLinknames = goLinknames + return nil +} + +// UnresolvedImports calculates the import paths of the package's dependencies +// based on all the imports in the augmented Go AST files. +// +// This is used to determine the unresolved imports that weren't in the +// PackageData.Imports slice since they were added during augmentation or +// during template generation. +// +// The given skip paths (typically those imports from PackageData.Imports) +// will not be returned in the results. +// This will not return any `*_test` packages in the results. +func (s *Sources) UnresolvedImports(skip ...string) []string { + seen := make(map[string]struct{}) + for _, sk := range skip { + seen[sk] = struct{}{} + } + imports := []string{} + for _, file := range s.Files { + for _, imp := range file.Imports { + path := strings.Trim(imp.Path.Value, `"`) + if _, ok := seen[path]; !ok { + if !strings.HasSuffix(path, "_test") { + imports = append(imports, path) + } + seen[path] = struct{}{} + } + } + } + sort.Strings(imports) + return imports +} + +// packageImporter implements go/types.Importer interface and +// wraps it to collect import errors. +type packageImporter struct { + srcDir string + importer Importer + sizes types.Sizes + tContext *types.Context + Errors errorList.ErrorList +} + +func (pi *packageImporter) Import(path string) (*types.Package, error) { + if path == "unsafe" { + return types.Unsafe, nil + } + + srcs, err := pi.importer(path, pi.srcDir) + if err != nil { + pi.Errors = pi.Errors.AppendDistinct(err) + return nil, err + } + + // If the sources doesn't have the package determined yet, get it now, + // otherwise this will be a no-op. + // This will recursively get the packages for all of it's dependencies too. + err = srcs.TypeCheck(pi.importer, pi.sizes, pi.tContext) + if err != nil { + pi.Errors = pi.Errors.AppendDistinct(err) + return nil, err + } + + return srcs.Package, nil +} + +// SortedSourcesSlice in place sorts the given slice of Sources by ImportPath. +// This will not change the order of the files within any Sources. +func SortedSourcesSlice(sourcesSlice []*Sources) { + sort.Slice(sourcesSlice, func(i, j int) bool { + return sourcesSlice[i].ImportPath < sourcesSlice[j].ImportPath + }) +} diff --git a/compiler/statements.go b/compiler/statements.go index 3d7210e47..5c7ce8769 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -9,9 +9,9 @@ import ( "go/types" "strings" - "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" "github.com/gopherjs/gopherjs/compiler/filter" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/typesutil" ) @@ -443,9 +443,10 @@ 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) + id := spec.(*ast.TypeSpec).Name + o := fc.pkgCtx.Defs[id].(*types.TypeName) fc.pkgCtx.typeNames.Add(o) - fc.DeclareDCEDep(o) + fc.pkgCtx.DeclareDCEDep(o, fc.instance.TArgs, nil) } case token.CONST: // skip, constants are inlined diff --git a/compiler/typesutil/typelist.go b/compiler/typesutil/typelist.go index 04d0d6869..768677365 100644 --- a/compiler/typesutil/typelist.go +++ b/compiler/typesutil/typelist.go @@ -18,3 +18,16 @@ func (tl TypeList) String() string { } return buf.String() } + +// Equal returns true if both lists of type arguments are identical. +func (tl TypeList) Equal(other TypeList) bool { + if len(tl) != len(other) { + return false + } + for i := range tl { + if !types.Identical(tl[i], other[i]) { + return false + } + } + return true +} diff --git a/compiler/utils.go b/compiler/utils.go index 7fec5b223..34d3f7960 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -18,11 +18,16 @@ import ( "text/template" "unicode" - "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) +// We use this character as a separator in synthetic identifiers instead of a +// regular dot. This character is safe for use in JS identifiers and helps to +// visually separate components of the name when it appears in a stack trace. +const midDot = "·" + // root returns the topmost function context corresponding to the package scope. func (fc *funcContext) root() *funcContext { if fc.isRoot() { @@ -102,43 +107,6 @@ func (fc *funcContext) Delayed(f func()) { fc.delayedOutput = fc.CatchOutput(0, f) } -// CollectDCEDeps captures a list of Go objects (types, functions, etc.) -// the code translated inside f() depends on. The returned list of identifiers -// can be used in dead-code elimination. -// -// Note that calling CollectDCEDeps() inside another CollectDCEDeps() call is -// not allowed. -func (fc *funcContext) CollectDCEDeps(f func()) []string { - if fc.pkgCtx.dependencies != nil { - panic(bailout(fmt.Errorf("called funcContext.CollectDependencies() inside another funcContext.CollectDependencies() call"))) - } - - fc.pkgCtx.dependencies = make(map[types.Object]bool) - defer func() { fc.pkgCtx.dependencies = nil }() - - f() - - var deps []string - for o := range fc.pkgCtx.dependencies { - qualifiedName := o.Pkg().Path() + "." + o.Name() - if typesutil.IsMethod(o) { - qualifiedName += "~" - } - deps = append(deps, qualifiedName) - } - sort.Strings(deps) - return deps -} - -// DeclareDCEDep records that the code that is currently being transpiled -// depends on a given Go object. -func (fc *funcContext) DeclareDCEDep(o types.Object) { - if fc.pkgCtx.dependencies == nil { - return // Dependencies are not being collected. - } - fc.pkgCtx.dependencies[o] = true -} - // expandTupleArgs converts a function call which argument is a tuple returned // by another function into a set of individual call arguments corresponding to // tuple elements. @@ -230,7 +198,7 @@ func (fc *funcContext) translateSelection(sel typesutil.Selection, pos token.Pos jsFieldName := s.Field(index).Name() for { fields = append(fields, fieldName(s, 0)) - ft := s.Field(0).Type() + ft := fc.fieldType(s, 0) if typesutil.IsJsObject(ft) { return fields, jsTag } @@ -247,7 +215,7 @@ func (fc *funcContext) translateSelection(sel typesutil.Selection, pos token.Pos } } fields = append(fields, fieldName(s, index)) - t = s.Field(index).Type() + t = fc.fieldType(s, index) } return fields, "" } @@ -376,6 +344,25 @@ func (fc *funcContext) newTypeIdent(name string, obj types.Object) *ast.Ident { return ident } +// newLitFuncName generates a new synthetic name for a function literal. +func (fc *funcContext) newLitFuncName() string { + fc.funcLitCounter++ + name := &strings.Builder{} + + // If function literal is defined inside another function, qualify its + // synthetic name with the outer function to make it easier to identify. + if fc.instance.Object != nil { + if recvType := typesutil.RecvType(fc.sig.Sig); recvType != nil { + name.WriteString(recvType.Obj().Name()) + name.WriteString(midDot) + } + name.WriteString(fc.instance.Object.Name()) + name.WriteString(midDot) + } + fmt.Fprintf(name, "func%d", fc.funcLitCounter) + return name.String() +} + func (fc *funcContext) setType(e ast.Expr, t types.Type) ast.Expr { fc.pkgCtx.Types[e] = types.TypeAndValue{Type: t} return e @@ -428,7 +415,12 @@ func (fc *funcContext) assignedObjectName(o types.Object) (name string, found bo // allocated as needed. func (fc *funcContext) objectName(o types.Object) string { if isPkgLevel(o) { - fc.DeclareDCEDep(o) + var nestTArgs []types.Type + if typeparams.FindNestingFunc(o) == fc.instance.Object { + // Only set the nest type arguments for objects nested in this funcContext. + nestTArgs = fc.instance.TArgs + } + fc.pkgCtx.DeclareDCEDep(o, nestTArgs, nil) if o.Pkg() != fc.pkgCtx.Pkg || (isVarOrConst(o) && o.Exported()) { return fc.pkgVar(o.Pkg()) + "." + o.Name() @@ -454,13 +446,16 @@ func (fc *funcContext) objectName(o types.Object) string { // knownInstances returns a list of known instantiations of the object. // -// For objects without type params always returns a single trivial instance. +// For objects without type params and not nested in a generic function or +// method, this always returns a single trivial instance. +// If the object is generic, or in a generic function or method, but there are +// no instances, then the object is unused and an empty list is returned. func (fc *funcContext) knownInstances(o types.Object) []typeparams.Instance { - if !typeparams.HasTypeParams(o.Type()) { + instances := fc.pkgCtx.instanceSet.Pkg(o.Pkg()).ForObj(o) + if len(instances) == 0 && !typeparams.HasTypeParams(o.Type()) { return []typeparams.Instance{{Object: o}} } - - return fc.pkgCtx.instanceSet.Pkg(o.Pkg()).ByObj()[o] + return instances } // instName returns a JS expression that refers to the provided instance of a @@ -471,7 +466,24 @@ func (fc *funcContext) instName(inst typeparams.Instance) string { if inst.IsTrivial() { return objName } - return fmt.Sprintf("%s[%d /* %v */]", objName, fc.pkgCtx.instanceSet.ID(inst), inst.TArgs) + fc.pkgCtx.DeclareDCEDep(inst.Object, inst.TNest, inst.TArgs) + label := inst.TypeParamsString(` /* `, ` */`) + return fmt.Sprintf("%s[%d%s]", objName, fc.pkgCtx.instanceSet.ID(inst), label) +} + +// methodName returns a JS identifier (specifically, object property name) +// corresponding to the given method. +func (fc *funcContext) methodName(fun *types.Func) string { + if fun.Type().(*types.Signature).Recv() == nil { + panic(fmt.Errorf("expected a method, got a standalone function %v", fun)) + } + name := fun.Name() + // Method names are scoped to their receiver type and guaranteed to be + // unique within that, so we only need to make sure it's not a reserved keyword + if reservedKeywords[name] { + name += "$" + } + return name } func (fc *funcContext) varPtrName(o *types.Var) string { @@ -501,18 +513,35 @@ func (fc *funcContext) typeName(ty types.Type) string { return "$error" } inst := typeparams.Instance{Object: t.Obj()} + + // Get type arguments for the type if there are any. for i := 0; i < t.TypeArgs().Len(); i++ { inst.TArgs = append(inst.TArgs, t.TypeArgs().At(i)) } + + // Get the nesting type arguments if there are any. + if fn := typeparams.FindNestingFunc(t.Obj()); fn != nil { + if fn.Scope().Contains(t.Obj().Pos()) { + tp := typeparams.SignatureTypeParams(fn.Type().(*types.Signature)) + tNest := make([]types.Type, tp.Len()) + for i := 0; i < tp.Len(); i++ { + tNest[i] = fc.typeResolver.Substitute(tp.At(i)) + } + inst.TNest = typesutil.TypeList(tNest) + } + } + return fc.instName(inst) case *types.Interface: if t.Empty() { return "$emptyInterface" } + case *types.TypeParam: + panic(fmt.Errorf("unexpected type parameter: %v", t)) } // For anonymous composite types, generate a synthetic package-level type - // declaration, which will be reused for all instances of this time. This + // declaration, which will be reused for all instances of this type. This // improves performance, since runtime won't have to synthesize the same type // repeatedly. anonType, ok := fc.pkgCtx.anonTypeMap.At(ty).(*types.TypeName) @@ -523,7 +552,9 @@ func (fc *funcContext) typeName(ty types.Type) string { fc.pkgCtx.anonTypes = append(fc.pkgCtx.anonTypes, anonType) fc.pkgCtx.anonTypeMap.Set(ty, anonType) } - fc.DeclareDCEDep(anonType) + // Since anonymous types are always package-level so they can be shared, + // don't pass in the function context (nest type parameters) to the DCE. + fc.pkgCtx.DeclareDCEDep(anonType, nil, nil) return anonType.Name() } @@ -572,6 +603,12 @@ func (fc *funcContext) typeOf(expr ast.Expr) types.Type { return fc.typeResolver.Substitute(typ) } +// fieldType returns the type of the i-th field of the given struct +// after substituting type parameters with concrete types for nested context. +func (fc *funcContext) fieldType(t *types.Struct, i int) types.Type { + return fc.typeResolver.Substitute(t.Field(i).Type()) +} + func (fc *funcContext) selectionOf(e *ast.SelectorExpr) (typesutil.Selection, bool) { if sel, ok := fc.pkgCtx.Selections[e]; ok { return fc.typeResolver.SubstituteSelection(sel), true @@ -894,7 +931,15 @@ func rangeCheck(pattern string, constantIndex, array bool) string { } func encodeIdent(name string) string { - return strings.Replace(url.QueryEscape(name), "%", "$", -1) + // Quick-and-dirty way to make any string safe for use as an identifier in JS. + name = url.QueryEscape(name) + // We use unicode middle dot as a visual separator in synthetic identifiers. + // It is safe for use in a JS identifier, so we un-encode it for readability. + name = strings.ReplaceAll(name, "%C2%B7", midDot) + // QueryEscape uses '%' before hex-codes of escaped characters, which is not + // allowed in a JS identifier, use '$' instead. + name = strings.ReplaceAll(name, "%", "$") + return name } // formatJSStructTagVal returns JavaScript code for accessing an object's property @@ -922,11 +967,6 @@ func formatJSStructTagVal(jsTag string) string { return "." + jsTag } -// 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) -} - // FatalError is an error compiler panics with when it encountered a fatal error. // // FatalError implements io.Writer, which can be used to record any free-form @@ -980,3 +1020,13 @@ func bailingOut(err interface{}) (*FatalError, bool) { fe, ok := err.(*FatalError) return fe, ok } + +func removeMatching[T comparable](haystack []T, needle T) []T { + var result []T + for _, el := range haystack { + if el != needle { + result = append(result, el) + } + } + return result +} diff --git a/go.mod b/go.mod index 4ace51ddf..0a364ce30 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,23 @@ module github.com/gopherjs/gopherjs go 1.20 require ( - github.com/evanw/esbuild v0.18.0 + github.com/evanw/esbuild v0.25.4 github.com/fsnotify/fsnotify v1.5.1 github.com/google/go-cmp v0.5.8 + github.com/msvitok77/goembed v0.3.5 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 github.com/sirupsen/logrus v1.8.1 - github.com/spf13/cobra v1.2.1 - github.com/spf13/pflag v1.0.5 - github.com/visualfc/goembed v0.3.3 - golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a + github.com/spf13/cobra v1.9.1 + github.com/spf13/pflag v1.0.6 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.16.0 ) -require github.com/inconshreveable/mousetrap v1.0.0 // indirect +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + golang.org/x/mod v0.14.0 // indirect +) diff --git a/go.sum b/go.sum index 8e69980d0..29dc8900b 100644 --- a/go.sum +++ b/go.sum @@ -1,600 +1,45 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanw/esbuild v0.18.0 h1:zJrquhC5ZiricRVQxMQTWqO8zYcV7F7OfUXstB9Ucbg= -github.com/evanw/esbuild v0.18.0/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/evanw/esbuild v0.25.4 h1:k1bTSim+usBG27w7BfOCorhgx3tO+6bAfMj5pR+6SKg= +github.com/evanw/esbuild v0.25.4/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -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.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= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/msvitok77/goembed v0.3.5 h1:SNdkLLipv4YGNVWCVCn+/N01aSp7Ga6/YOcB+kYxnhk= +github.com/msvitok77/goembed v0.3.5/go.mod h1:ycBNmh+53HrsZPQfWOJHYXbu7vLwb1QYdJISOyKlnnc= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c h1:bY6ktFuJkt+ZXkX0RChQch2FtHpWQLVS8Qo1YasiIVk= github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISeOcal5irEhJd1M+IrApc0PdcN7e7Aj4yuEnOrfQ= github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/visualfc/goembed v0.3.3 h1:pOL02L715tHKsLQVMcZz06tTzRDAHkJKJLRnCA22G9Q= -github.com/visualfc/goembed v0.3.3/go.mod h1:jCVCz/yTJGyslo6Hta+pYxWWBuq9ADCcIVZBTQ0/iVI= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -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= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -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.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= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -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.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= -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= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/compiler/errors.go b/internal/errorList/errorList.go similarity index 98% rename from compiler/errors.go rename to internal/errorList/errorList.go index 48aed48ec..531a0f4e0 100644 --- a/compiler/errors.go +++ b/internal/errorList/errorList.go @@ -1,4 +1,4 @@ -package compiler +package errorList import ( "errors" diff --git a/internal/experiments/experiments.go b/internal/experiments/experiments.go index 85abce562..fb81a2285 100644 --- a/internal/experiments/experiments.go +++ b/internal/experiments/experiments.go @@ -35,7 +35,7 @@ func init() { // Flags contains flags for currently supported experiments. type Flags struct { - Generics bool `flag:"generics"` + // e.g. Generics bool `flag:"generics"` } // parseFlags parses the `raw` flags string and populates flag values in the diff --git a/internal/govendor/subst/export.go b/internal/govendor/subst/export.go index 38e394bda..35e38473c 100644 --- a/internal/govendor/subst/export.go +++ b/internal/govendor/subst/export.go @@ -3,9 +3,7 @@ // type arguments. package subst -import ( - "go/types" -) +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 @@ -16,34 +14,32 @@ 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 { +// New creates a new Subster with a given a map from type parameters and the arguments +// that should be used to replace them. If the map is empty, nil is returned. +func New(tc *types.Context, replacements map[*types.TypeParam]types.Type) *Subster { + if len(replacements) == 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, - } + subst := makeSubster(tc, nil, nil, nil, false) + subst.replacements = replacements + return &Subster{impl: subst} } -// Type returns a version of typ with all references to type parameters replaced -// with the corresponding type arguments. +// 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) } + +// Types returns a version of ts with all references to type parameters +// replaced with the corresponding type arguments. +func (s *Subster) Types(ts []types.Type) []types.Type { + if s == nil { + return ts + } + return s.impl.types(ts) +} diff --git a/internal/govendor/subst/subst.go b/internal/govendor/subst/subst.go index 9020e94f9..2fd29bb81 100644 --- a/internal/govendor/subst/subst.go +++ b/internal/govendor/subst/subst.go @@ -2,69 +2,85 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Copy of https://cs.opensource.google/go/x/tools/+/refs/tags/v0.33.0:go/ssa/subst.go +// Any changes to this copy are labelled with GOPHERJS. package subst import ( "go/types" + + "golang.org/x/tools/go/types/typeutil" ) -// Type substituter for a fixed set of replacement types. +// subster defines a type substitution operation of a set of type parameters +// to type parameter free replacement types. Substitution is done within +// the context of a package-level function instantiation. *Named types +// declared in the function are unique to the instantiation. +// +// For example, given a parameterized function F +// +// func F[S, T any]() any { +// type X struct{ s S; next *X } +// var p *X +// return p +// } // -// A nil *subster is an valid, empty substitution map. It always acts as +// calling the instantiation F[string, int]() returns an interface +// value (*X[string,int], nil) where the underlying value of +// X[string,int] is a struct{s string; next *X[string,int]}. +// +// A nil *subster is a 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. +// +// Note: Some may find it helpful to think through some of the most +// complex substitution cases using lambda calculus inspired notation. +// subst.typ() solves evaluating a type expression E +// within the body of a function Fn[m] with the type parameters m +// once we have applied the type arguments N. +// We can succinctly write this as a function application: +// +// ((λm. E) N) +// +// go/types does not provide this interface directly. +// So what subster provides is a type substitution operation +// +// E[m:=N] 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 + origin *types.Func // types.Objects declared within this origin function are unique within this context + ctxt *types.Context // speeds up repeated instantiations + uniqueness typeutil.Map // determines the uniqueness of the instantiations within the function // 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 { +// fn is the generic function for which we are substituting. +func makeSubster(ctxt *types.Context, fn *types.Func, tparams *types.TypeParamList, targs []types.Type, debug bool) *subster { assert(tparams.Len() == len(targs), "makeSubster argument count must match") + // GOPHERJS: Made `fn` optional so that we can use this on package level types too. + var origin *types.Func + if fn != nil { + origin = fn.Origin() + } + subst := &subster{ replacements: make(map[*types.TypeParam]types.Type, tparams.Len()), cache: make(map[types.Type]types.Type), + origin: origin, 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) { @@ -78,12 +94,12 @@ func (subst *subster) typ(t types.Type) (res types.Type) { 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 + if r := subst.replacements[t]; r != nil { + return r + } + return t case *types.Basic: return t @@ -135,9 +151,17 @@ func (subst *subster) typ(t types.Type) (res types.Type) { case *types.Interface: return subst.interface_(t) + // GOPHERJS: Removed following case since types.Alias is not supported until go1.22. + // case *types.Alias: + // return subst.alias(t) + case *types.Named: return subst.named(t) + // GOPHERJS: Removed following case since the opaque type is specific to the SSA builder. + // case *opaqueType: + // return t // opaque types are never substituted + default: panic("unreachable") } @@ -187,7 +211,7 @@ func (subst *subster) struct_(t *types.Struct) *types.Struct { return t } -// varlist reutrns subst(in[i]) or return nils if subst(v[i]) == v[i] for all i. +// varlist returns 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++ { @@ -212,7 +236,7 @@ func (subst *subster) var_(v *types.Var) *types.Var { 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 types.NewParam(v.Pos(), v.Pkg(), v.Name(), typ) } } return v @@ -251,6 +275,8 @@ func (subst *subster) interface_(iface *types.Interface) *types.Interface { var methods []*types.Func initMethods := func(n int) { // copy first n explicit methods methods = make([]*types.Func, iface.NumExplicitMethods()) + // GOPHERJS: Replaced a range over count since that's not supported in go1.22 + // for i := range n { for i := 0; i < n; i++ { f := iface.ExplicitMethod(i) norecv := changeRecv(f.Type().(*types.Signature), nil) @@ -275,6 +301,8 @@ func (subst *subster) interface_(iface *types.Interface) *types.Interface { var embeds []types.Type initEmbeds := func(n int) { // copy first n embedded types embeds = make([]types.Type, iface.NumEmbeddeds()) + // GOPHERJS: Replaced a range over count since that's not supported in go1.22 + // for i := range n { for i := 0; i < n; i++ { embeds[i] = iface.EmbeddedType(i) } @@ -302,72 +330,156 @@ func (subst *subster) interface_(iface *types.Interface) *types.Interface { return types.NewInterfaceType(methods, embeds).Complete() } +// GOPHERJS: removed alias substitution since types.Alias is not supported until go1.22 +// func (subst *subster) alias(t *types.Alias) types.Type { ... } 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 + // A Named type is a user defined type. + // Ignoring generics, Named types are canonical: they are identical if + // and only if they have the same defining symbol. + // Generics complicate things, both if the type definition itself is + // parameterized, and if the type is defined within the scope of a + // parameterized function. In this case, two named types are identical if + // and only if their identifying symbols are identical, and all type + // arguments bindings in scope of the named type definition (including the + // type parameters of the definition itself) are equivalent. + // + // Notably: + // 1. For type definition type T[P1 any] struct{}, T[A] and T[B] are identical + // only if A and B are identical. + // 2. Inside the generic func Fn[m any]() any { type T struct{}; return T{} }, + // the result of Fn[A] and Fn[B] have identical type if and only if A and + // B are identical. + // 3. Both 1 and 2 could apply, such as in + // func F[m any]() any { type T[x any] struct{}; return T{} } + // + // A subster replaces type parameters within a function scope, and therefore must + // also replace free type parameters in the definitions of local types. + // + // Note: There are some detailed notes sprinkled throughout that borrow from + // lambda calculus notation. These contain some over simplifying math. + // + // LC: One way to think about subster is that it is a way of evaluating + // ((λm. E) N) as E[m:=N]. + // Each Named type t has an object *TypeName within a scope S that binds an + // underlying type expression U. U can refer to symbols within S (+ S's ancestors). + // Let x = t.TypeParams() and A = t.TypeArgs(). + // Each Named type t is then either: + // U where len(x) == 0 && len(A) == 0 + // λx. U where len(x) != 0 && len(A) == 0 + // ((λx. U) A) where len(x) == len(A) + // In each case, we will evaluate t[m:=N]. + tparams := t.TypeParams() // x + targs := t.TypeArgs() // A + + if !declaredWithin(t.Obj(), subst.origin) { + // t is declared outside of Fn[m]. + // + // In this case, we can skip substituting t.Underlying(). + // The underlying type cannot refer to the type parameters. + // + // LC: Let free(E) be the set of free type parameters in an expression E. + // Then whenever m ∉ free(E), then E = E[m:=N]. + // t ∉ Scope(fn) so therefore m ∉ free(U) and m ∩ x = ∅. + if targs.Len() == 0 { + // t has no type arguments. So it does not need to be instantiated. + // + // This is the normal case in real Go code, where t is not parameterized, + // declared at some package scope, and m is a TypeParam from a parameterized + // function F[m] or method. + // + // LC: m ∉ free(A) lets us conclude m ∉ free(t). So t=t[m:=N]. + return t } - // 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. + // t is declared outside of Fn[m] and has type arguments. + // The type arguments may contain type parameters m so + // substitute the type arguments, and instantiate the substituted + // type arguments. + // + // LC: Evaluate this as ((λx. U) A') where A' = A[m := N]. + newTArgs := subst.typelist(targs) + return subst.instantiate(t.Origin(), newTArgs) + } + + // t is declared within Fn[m]. + + if targs.Len() == 0 { // no type arguments? + assert(t == t.Origin(), "local parameterized type abstraction must be an origin type") + + // t has no type arguments. + // The underlying type of t may contain the function's type parameters, + // replace these, and create a new type. // // 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). + // subst, i.e. cache[t]=fresh, to preemptively replace t with fresh + // in recursive types during traversal. This both breaks infinite cycles + // and allows for constructing types with the replacement applied in + // subst.typ(U). // - // Example: - // func foo[T any]() { - // type linkedlist struct { - // next *linkedlist - // val T - // } - // } + // A new copy of the Named and Typename (and constraints) per function + // instantiation matches the semantics of Go, which treats all function + // instantiations F[N] as having distinct local types. // - // 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 + // LC: x.Len()=0 can be thought of as a special case of λx. U. + // LC: Evaluate (λx. U)[m:=N] as (λx'. U') where U'=U[x:=x',m:=N]. + tname := t.Obj() + obj := types.NewTypeName(tname.Pos(), tname.Pkg(), tname.Name(), nil) + fresh := types.NewNamed(obj, nil, nil) + var newTParams []*types.TypeParam + for i := 0; i < tparams.Len(); i++ { + cur := tparams.At(i) + cobj := cur.Obj() + cname := types.NewTypeName(cobj.Pos(), cobj.Pkg(), cobj.Name(), nil) + ntp := types.NewTypeParam(cname, nil) + // GOPHERJS: The following cache was removed because it causes a + // problem for recursive types, e.g. `type X[T any] Q[X[T]]`. + // When it sees the `X[T]` in `Q[X[T]]`, it creates a `subOrigin` + // (seen below) which caches the old `T` to the new `T'`. + // Then when creating `subTArgs` (also below), it will return + // `T'` via the cache instead of substituting `T` with the + // correct type argument. + // subst.cache[cur] = ntp + newTParams = append(newTParams, ntp) + } + fresh.SetTypeParams(newTParams) + subst.cache[t] = fresh + subst.cache[fresh] = fresh + fresh.SetUnderlying(subst.typ(t.Underlying())) + // Substitute into all of the constraints after they are created. + for i, ntp := range newTParams { + bound := tparams.At(i).Constraint() + ntp.SetConstraint(subst.typ(bound)) + } + return fresh } - 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 + + // t is defined within Fn[m] and t has type arguments (an instantiation). + // We reduce this to the two cases above: + // (1) substitute the function's type parameters into t.Origin(). + // (2) substitute t's type arguments A and instantiate the updated t.Origin() with these. + // + // LC: Evaluate ((λx. U) A)[m:=N] as (t' A') where t' = (λx. U)[m:=N] and A'=A [m:=N] + subOrigin := subst.typ(t.Origin()) + subTArgs := subst.typelist(targs) + return subst.instantiate(subOrigin, subTArgs) +} + +func (subst *subster) instantiate(orig types.Type, targs []types.Type) types.Type { + i, err := types.Instantiate(subst.ctxt, orig, targs, false) + assert(err == nil, "failed to Instantiate named (Named or Alias) type") + if c, _ := subst.uniqueness.At(i).(types.Type); c != nil { + return c.(types.Type) } - r, err := types.Instantiate(subst.ctxt, t.Origin(), insts, false) - assert(err == nil, "failed to Instantiate Named type") - return r + subst.uniqueness.Set(i, i) + return i +} + +func (subst *subster) typelist(l *types.TypeList) []types.Type { + res := make([]types.Type, l.Len()) + for i := 0; i < l.Len(); i++ { + res[i] = subst.typ(l.At(i)) + } + return res } func (subst *subster) signature(t *types.Signature) types.Type { @@ -466,6 +578,8 @@ func reaches(t types.Type, c map[types.Type]bool) (res bool) { return true } } + // GOPHERJS: Removed types.Alias from following case since it's not supported until go1.22. + // case *types.Named, *types.Alias: case *types.Named: return reaches(t.Underlying(), c) default: diff --git a/internal/govendor/subst/subst_test.go b/internal/govendor/subst/subst_test.go index 53fadbcf0..8f2f629a1 100644 --- a/internal/govendor/subst/subst_test.go +++ b/internal/govendor/subst/subst_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Copy of https://cs.opensource.google/go/x/tools/+/refs/tags/v0.33.0:go/ssa/subst_test.go package subst import ( @@ -16,6 +17,10 @@ func TestSubst(t *testing.T) { const source = ` package P +func within(){ + // Pretend that the instantiation happens within this function. +} + type t0 int func (t0) f() type t1 interface{ f() } @@ -55,6 +60,11 @@ var _ L[int] = Fn0[L[int]](nil) t.Fatal(err) } + within, _ := pkg.Scope().Lookup("within").(*types.Func) + if within == nil { + t.Fatal("Failed to find the function within()") + } + for _, test := range []struct { expr string // type expression of Named parameterized type args []string // type expressions of args for named @@ -94,7 +104,7 @@ var _ L[int] = Fn0[L[int]](nil) T := tv.Type.(*types.Named) - subst := makeSubster(types.NewContext(), nil, T.TypeParams(), targs, true) + subst := makeSubster(types.NewContext(), within, 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 index 22072e39f..edfa513ba 100644 --- a/internal/govendor/subst/util.go +++ b/internal/govendor/subst/util.go @@ -4,20 +4,47 @@ package subst -import "go/types" - -// This file defines a number of miscellaneous utility functions. - -//// Sanity checking utilities +import ( + "go/token" + "go/types" +) // assert panics with the mesage msg if p is false. // Avoid combining with expensive string formatting. +// From https://cs.opensource.google/go/x/tools/+/refs/tags/v0.33.0:go/ssa/util.go;l=28 func assert(p bool, msg string) { if !p { panic(msg) } } +// From https://cs.opensource.google/go/x/tools/+/refs/tags/v0.33.0:go/ssa/wrappers.go;l=262 func changeRecv(s *types.Signature, recv *types.Var) *types.Signature { return types.NewSignatureType(recv, nil, nil, s.Params(), s.Results(), s.Variadic()) } + +// declaredWithin reports whether an object is declared within a function. +// +// obj must not be a method or a field. +// From https://cs.opensource.google/go/x/tools/+/refs/tags/v0.33.0:go/ssa/util.go;l=145 +func declaredWithin(obj types.Object, fn *types.Func) bool { + // GOPHERJS: Made `fn` optional so that we can use this on package level types too. + if fn == nil { + return false + } + + if obj.Pos() != token.NoPos { + return fn.Scope().Contains(obj.Pos()) // trust the positions if they exist. + } + if fn.Pkg() != obj.Pkg() { + return false // fast path for different packages + } + + // Traverse Parent() scopes for fn.Scope(). + for p := obj.Parent(); p != nil; p = p.Parent() { + if p == fn.Scope() { + return true + } + } + return false +} diff --git a/internal/srctesting/srctesting.go b/internal/srctesting/srctesting.go index a74d31958..e4242991c 100644 --- a/internal/srctesting/srctesting.go +++ b/internal/srctesting/srctesting.go @@ -10,8 +10,11 @@ import ( "go/parser" "go/token" "go/types" + "path/filepath" "strings" "testing" + + "golang.org/x/tools/go/packages" ) // Fixture provides utilities for parsing and type checking Go code in tests. @@ -71,7 +74,7 @@ func (f *Fixture) Check(importPath string, files ...*ast.File) (*types.Info, *ty } pkg, err := config.Check(importPath, f.FileSet, files, info) if err != nil { - f.T.Fatalf("Filed to type check test source: %s", err) + f.T.Fatalf("Failed to type check test source: %s", err) } f.Packages[importPath] = pkg return info, pkg @@ -155,6 +158,9 @@ func LookupObj(pkg *types.Package, name string) types.Object { for len(path) > 0 { obj = scope.Lookup(path[0]) + if obj == nil { + panic(fmt.Sprintf("failed to find %q in %q", path[0], name)) + } path = path[1:] if fun, ok := obj.(*types.Func); ok { @@ -167,7 +173,112 @@ func LookupObj(pkg *types.Package, name string) types.Object { if len(path) > 0 { obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), path[0]) path = path[1:] + if fun, ok := obj.(*types.Func); ok { + scope = fun.Scope() + } } } return obj } + +type Source struct { + Name string + Contents []byte +} + +// ParseSources parses the given source files and returns the root package +// that contains the given source files. +// +// The source file should all be from the same package as the files for the +// root package. At least one source file must be given. +// The root package's path will be `command-line-arguments`. +// +// The auxiliary files can be for different packages but should have paths +// added to the source name so that they can be grouped together by package. +// To import an auxiliary package, the path should be prepended by +// `github.com/gopherjs/gopherjs/compiler`. +func ParseSources(t *testing.T, sourceFiles []Source, auxFiles []Source) *packages.Package { + t.Helper() + const mode = packages.NeedName | + packages.NeedFiles | + packages.NeedImports | + packages.NeedDeps | + packages.NeedTypes | + packages.NeedSyntax + + dir, err := filepath.Abs(`./`) + if err != nil { + t.Fatal(`error getting working directory:`, err) + } + + patterns := make([]string, len(sourceFiles)) + overlay := make(map[string][]byte, len(sourceFiles)) + for i, src := range sourceFiles { + filename := src.Name + patterns[i] = filename + absName := filepath.Join(dir, filename) + overlay[absName] = []byte(src.Contents) + } + for _, src := range auxFiles { + absName := filepath.Join(dir, src.Name) + overlay[absName] = []byte(src.Contents) + } + + config := &packages.Config{ + Mode: mode, + Overlay: overlay, + Dir: dir, + } + + pkgs, err := packages.Load(config, patterns...) + if err != nil { + t.Fatal(`error loading packages:`, err) + } + + hasErrors := false + packages.Visit(pkgs, nil, func(pkg *packages.Package) { + for _, err := range pkg.Errors { + hasErrors = true + t.Error(err) + } + }) + if hasErrors { + t.FailNow() + } + + if len(pkgs) != 1 { + t.Fatal(`expected one and only one root package but got`, len(pkgs)) + } + return pkgs[0] +} + +// GetNodeAtLineNo returns the first node of type N that starts on the given +// line in the given file. This helps lookup nodes that aren't named but +// are needed by a specific test. +func GetNodeAtLineNo[N ast.Node](file *ast.File, fSet *token.FileSet, lineNo int) N { + var node N + keepLooking := true + ast.Inspect(file, func(n ast.Node) bool { + if n == nil || !keepLooking { + return false + } + nodeLine := fSet.Position(n.Pos()).Line + switch { + case nodeLine < lineNo: + // We haven't reached the line yet, so check if we can skip over + // this whole node or if we should look inside it. + return fSet.Position(n.End()).Line >= lineNo + case nodeLine > lineNo: + // We went past it without finding it, so stop looking. + keepLooking = false + return false + default: // nodeLine == lineNo + if n, ok := n.(N); ok { + node = n + keepLooking = false + } + return keepLooking + } + }) + return node +} diff --git a/internal/sysutil/sysutil.go b/internal/sysutil/sysutil.go index e19eb02e1..8fc72403d 100644 --- a/internal/sysutil/sysutil.go +++ b/internal/sysutil/sysutil.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows // Package sysutil contains system-specific utilities. package sysutil diff --git a/internal/testmain/testmain.go b/internal/testmain/testmain.go index f1b3257d5..3de87d382 100644 --- a/internal/testmain/testmain.go +++ b/internal/testmain/testmain.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" "go/ast" - gobuild "go/build" + "go/build" "go/doc" "go/parser" "go/token" @@ -16,7 +16,6 @@ import ( "unicode" "unicode/utf8" - "github.com/gopherjs/gopherjs/build" "golang.org/x/tools/go/buildutil" ) @@ -66,7 +65,8 @@ func (ef ExampleFunc) Executable() bool { // TestMain is a helper type responsible for generation of the test main package. type TestMain struct { - Package *build.PackageData + Package *build.Package + Context *build.Context Tests []TestFunc Benchmarks []TestFunc Fuzz []TestFunc @@ -88,7 +88,7 @@ func (tm *TestMain) Scan(fset *token.FileSet) error { func (tm *TestMain) scanPkg(fset *token.FileSet, files []string, loc FuncLocation) error { for _, name := range files { srcPath := path.Join(tm.Package.Dir, name) - f, err := buildutil.OpenFile(tm.Package.InternalBuildContext(), srcPath) + f, err := buildutil.OpenFile(tm.Context, srcPath) if err != nil { return fmt.Errorf("failed to open source file %q: %w", srcPath, err) } @@ -158,7 +158,7 @@ func (tm *TestMain) scanFile(f *ast.File, loc FuncLocation) error { } // Synthesize main package for the tests. -func (tm *TestMain) Synthesize(fset *token.FileSet) (*build.PackageData, *ast.File, error) { +func (tm *TestMain) Synthesize(fset *token.FileSet) (*build.Package, *ast.File, error) { buf := &bytes.Buffer{} if err := testmainTmpl.Execute(buf, tm); err != nil { return nil, nil, fmt.Errorf("failed to generate testmain source for package %s: %w", tm.Package.ImportPath, err) @@ -167,12 +167,10 @@ func (tm *TestMain) Synthesize(fset *token.FileSet) (*build.PackageData, *ast.Fi if err != nil { return nil, nil, fmt.Errorf("failed to parse testmain source for package %s: %w", tm.Package.ImportPath, err) } - pkg := &build.PackageData{ - Package: &gobuild.Package{ - ImportPath: tm.Package.ImportPath + ".testmain", - Name: "main", - GoFiles: []string{"_testmain.go"}, - }, + pkg := &build.Package{ + ImportPath: tm.Package.ImportPath + ".testmain", + Name: "main", + GoFiles: []string{"_testmain.go"}, } return pkg, src, nil } diff --git a/internal/testmain/testmain_test.go b/internal/testmain/testmain_test.go index 01c92cc76..8e0b268d2 100644 --- a/internal/testmain/testmain_test.go +++ b/internal/testmain/testmain_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/gopherjs/gopherjs/build" "github.com/gopherjs/gopherjs/internal/srctesting" . "github.com/gopherjs/gopherjs/internal/testmain" @@ -21,7 +22,10 @@ func TestScan(t *testing.T) { fset := token.NewFileSet() - got := TestMain{Package: pkg} + got := TestMain{ + Package: pkg.Package, + Context: pkg.InternalBuildContext(), + } if err := got.Scan(fset); err != nil { t.Fatalf("Got: tm.Scan() returned error: %s. Want: no error.", err) } @@ -47,6 +51,7 @@ func TestScan(t *testing.T) { } opts := cmp.Options{ cmpopts.IgnoreFields(TestMain{}, "Package"), // Inputs. + cmpopts.IgnoreFields(TestMain{}, "Context"), } if diff := cmp.Diff(want, got, opts...); diff != "" { t.Errorf("List of test function is different from expected (-want,+got):\n%s", diff) @@ -54,9 +59,7 @@ func TestScan(t *testing.T) { } func TestSynthesize(t *testing.T) { - pkg := &build.PackageData{ - Package: &gobuild.Package{ImportPath: "foo/bar"}, - } + pkg := &gobuild.Package{ImportPath: "foo/bar"} tests := []struct { descr string diff --git a/js/js_test.go b/js/js_test.go index d7a904954..53fee6473 100644 --- a/js/js_test.go +++ b/js/js_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package js_test diff --git a/node-syscall/package-lock.json b/node-syscall/package-lock.json index df06938c6..fc07161a1 100644 --- a/node-syscall/package-lock.json +++ b/node-syscall/package-lock.json @@ -117,9 +117,10 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" diff --git a/package-lock.json b/package-lock.json index 8fa9be563..b8ba5e000 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,12 +6,6 @@ "": { "name": "gopherjs", "license": "BSD-2-Clause", - "dependencies": { - "source-map-support": "^0.5.19" - }, - "devDependencies": { - "uglify-es": "^3.3.9" - }, "optionalDependencies": { "syscall": "file:./node-syscall" } @@ -143,11 +137,6 @@ "concat-map": "0.0.1" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, "node_modules/cacache": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", @@ -204,12 +193,6 @@ "color-support": "bin.js" } }, - "node_modules/commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", - "dev": true - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -883,23 +866,6 @@ "node": ">= 10" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -983,23 +949,6 @@ "node": ">=8" } }, - "node_modules/uglify-es": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", - "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", - "deprecated": "support for ECMAScript is superseded by `uglify-js` as of v3.13.0", - "dev": true, - "dependencies": { - "commander": "~2.13.0", - "source-map": "~0.6.1" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -1061,6 +1010,7 @@ "optional": true }, "node-syscall": { + "name": "syscall", "hasInstallScript": true, "license": "BSD-2-Clause", "optional": true, diff --git a/package.json b/package.json index f276a4eb1..ec8add087 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,6 @@ { "name": "gopherjs", "license": "BSD-2-Clause", - "devDependencies": { - "uglify-es": "^3.3.9" - }, - "dependencies": { - "source-map-support": "^0.5.19" - }, "optionalDependencies": { "syscall": "file:./node-syscall" } diff --git a/tests/alias_test.go b/tests/alias_test.go index 9e791856e..ecd911d9d 100644 --- a/tests/alias_test.go +++ b/tests/alias_test.go @@ -1,8 +1,6 @@ package tests -import ( - "testing" -) +import "testing" type foo struct { a int diff --git a/tests/compiler_test.go b/tests/compiler_test.go index 7c72e3535..e33d24efe 100644 --- a/tests/compiler_test.go +++ b/tests/compiler_test.go @@ -1,8 +1,6 @@ package tests -import ( - "testing" -) +import "testing" func TestVariadicNil(t *testing.T) { t.Run("only variadic", func(t *testing.T) { diff --git a/tests/gencircle_test.go b/tests/gencircle_test.go new file mode 100644 index 000000000..d70bc1bc6 --- /dev/null +++ b/tests/gencircle_test.go @@ -0,0 +1,67 @@ +package tests_test + +import ( + "embed" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_GenCircle_Simple(t *testing.T) { runGenCircleTest(t, `simple`) } + +func Test_GenCircle_PingPong(t *testing.T) { runGenCircleTest(t, `pingpong`) } + +func Test_GenCircle_Burninate(t *testing.T) { runGenCircleTest(t, `burninate`) } + +func Test_GenCircle_CatBox(t *testing.T) { runGenCircleTest(t, `catbox`) } + +func Test_GenCircle_Trammel(t *testing.T) { runGenCircleTest(t, `trammel`) } + +// Cache buster: Keeping the tests from using cached results when only +// the test application files are changed. +// +//go:embed testdata/gencircle +var _ embed.FS + +func runGenCircleTest(t *testing.T, testPkg string) { + t.Helper() + if runtime.GOOS == `js` { + t.Skip(`test meant to be run using normal Go compiler (needs os/exec)`) + } + + const ( + basePath = `testdata/gencircle` + mainFile = `main.go` + outFile = `main.out` + ) + + mainPath := filepath.Join(basePath, testPkg, mainFile) + gotBytes, err := exec.Command(`gopherjs`, `run`, mainPath).CombinedOutput() + got := normalizeOut(gotBytes) + if err != nil { + t.Fatalf("error from exec: %v:\n%s", err, got) + } + + outPath := filepath.Join(basePath, testPkg, outFile) + wantBytes, err := os.ReadFile(outPath) + if err != nil { + t.Fatalf(`error reading .out file: %v`, err) + } + want := normalizeOut(wantBytes) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Got diff (-want,+got):\n%s", diff) + } +} + +func normalizeOut(b []byte) string { + s := string(b) + s = strings.ReplaceAll(s, "\r\n", "\n") + s = strings.ReplaceAll(s, "\r", "\n") + return s +} diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 203d33205..74a641901 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -1,5 +1,4 @@ //go:build ignore -// +build ignore // skip @@ -138,10 +137,12 @@ var knownFails = map[string]failReason{ "typeparam/chans.go": {category: neverTerminates, desc: "uses runtime.SetFinalizer() and runtime.GC()."}, "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"}, + // Failures related to the lack of nested type number indicators and deep nested types not printing correctly. + // For example, the following line from the test's outputs (see "typeparam/nested.out") + // `4,7: main.T·2[int;main.U·3[int;int]]` will currently output as `4,7: main.T[int;main.U[int]]` + // in GopherJS because we doesn't currently add the `·2` and `·3` indicators to the type names + // and the nested type arguments to deep nested type, e.g. `U[int;int]` is printed as `U[int]`. + "typeparam/nested.go": {category: other, desc: "incomplete support for nested type numbering"}, // These are new tests in Go 1.19 "typeparam/issue51521.go": {category: lowLevelRuntimeDifference, desc: "different panic message when calling a method on nil interface"}, @@ -163,7 +164,6 @@ const ( neverTerminates // Test never terminates (so avoid starting it). usesUnsupportedPackage // Test fails because it imports an unsupported package, e.g., "unsafe". requiresSourceMapSupport // Test fails without source map support (as configured in CI), because it tries to check filename/line number via runtime.Caller. - usesUnsupportedGenerics // Test uses generics (type parameters) that are not currently supported. compilerPanic unsureIfGopherJSSupportsThisFeature lowLevelRuntimeDifference // JavaScript runtime behaves differently from Go in ways that are difficult to work around. diff --git a/tests/js_test.go b/tests/js_test.go index 2d67fb99a..885ad24b9 100644 --- a/tests/js_test.go +++ b/tests/js_test.go @@ -1,5 +1,4 @@ //go:build js && !wasm -// +build js,!wasm package tests_test diff --git a/tests/map_js_test.go b/tests/map_js_test.go index c815661ab..04847d3d4 100644 --- a/tests/map_js_test.go +++ b/tests/map_js_test.go @@ -1,5 +1,4 @@ //go:build js && !wasm -// +build js,!wasm package tests diff --git a/tests/misc_test.go b/tests/misc_test.go index 8dc3be924..4122b8170 100644 --- a/tests/misc_test.go +++ b/tests/misc_test.go @@ -959,3 +959,28 @@ func TestFileSetSize(t *testing.T) { t.Errorf("Got: unsafe.Sizeof(token.FileSet{}) %v, Want: %v", n2, n1) } } + +// TestCrossPackageGenericFuncCalls ensures that generic functions from other +// packages can be called correctly. +func TestCrossPackageGenericFuncCalls(t *testing.T) { + var wantInt int + if got := otherpkg.Zero[int](); got != wantInt { + t.Errorf(`Got: otherpkg.Zero[int]() = %v, Want: %v`, got, wantInt) + } + + var wantStr string + if got := otherpkg.Zero[string](); got != wantStr { + t.Errorf(`Got: otherpkg.Zero[string]() = %q, Want: %q`, got, wantStr) + } +} + +// TestCrossPackageGenericCasting ensures that generic types from other +// packages can be used in a type cast. +// The cast looks like a function call but should be treated as a type conversion. +func TestCrossPackageGenericCasting(t *testing.T) { + fn := otherpkg.GetterHandle[int](otherpkg.Zero[int]) + var wantInt int + if got := fn(); got != wantInt { + t.Errorf(`Got: otherpkg.GetterHandle[int](otherpkg.Zero[int]) = %v, Want: %v`, got, wantInt) + } +} diff --git a/tests/otherpkg/otherpkg.go b/tests/otherpkg/otherpkg.go index 2b413e5f1..6f9499526 100644 --- a/tests/otherpkg/otherpkg.go +++ b/tests/otherpkg/otherpkg.go @@ -1,3 +1,10 @@ package otherpkg var Test float32 + +func Zero[T any]() T { + var zero T + return zero +} + +type GetterHandle[T any] func() T diff --git a/tests/syscall_test.go b/tests/syscall_test.go index 104800df7..ea880a218 100644 --- a/tests/syscall_test.go +++ b/tests/syscall_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package tests diff --git a/tests/testdata/gencircle/burninate/burnable/burnable.go b/tests/testdata/gencircle/burninate/burnable/burnable.go new file mode 100644 index 000000000..3a6171134 --- /dev/null +++ b/tests/testdata/gencircle/burninate/burnable/burnable.go @@ -0,0 +1,13 @@ +package burnable + +import "github.com/gopherjs/gopherjs/tests/testdata/gencircle/burninate/dragon" + +type Cottages struct{} + +func (c Cottages) String() string { + return `thatched-roof cottages` +} + +func Burn[B dragon.Burnable](d dragon.Trogdor[B], b B) { + d.Burninate(b) +} diff --git a/tests/testdata/gencircle/burninate/dragon/dragon.go b/tests/testdata/gencircle/burninate/dragon/dragon.go new file mode 100644 index 000000000..ddabc6a77 --- /dev/null +++ b/tests/testdata/gencircle/burninate/dragon/dragon.go @@ -0,0 +1,9 @@ +package dragon + +type Burnable interface{ String() string } + +type Trogdor[T Burnable] struct{} + +func (t Trogdor[T]) Burninate(target T) { + println("burninating the " + target.String()) +} diff --git a/tests/testdata/gencircle/burninate/main.go b/tests/testdata/gencircle/burninate/main.go new file mode 100644 index 000000000..594a5a4e9 --- /dev/null +++ b/tests/testdata/gencircle/burninate/main.go @@ -0,0 +1,17 @@ +// Test of instances of generic types inverted dependencies. +// The `burnable` imports `dragons` but the instance of `Trogdor` requires +// `burnable`. This is a simple check that the all packages are loaded before +// the types finish setting up. This is similar to the "simple" gencircle test +// except with generic functions and methods. +package main + +import ( + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/burninate/burnable" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/burninate/dragon" +) + +func main() { + d := dragon.Trogdor[burnable.Cottages]{} + b := burnable.Cottages{} + burnable.Burn(d, b) +} diff --git a/tests/testdata/gencircle/burninate/main.out b/tests/testdata/gencircle/burninate/main.out new file mode 100644 index 000000000..a9bbff9e2 --- /dev/null +++ b/tests/testdata/gencircle/burninate/main.out @@ -0,0 +1 @@ +burninating the thatched-roof cottages diff --git a/tests/testdata/gencircle/catbox/box/box.go b/tests/testdata/gencircle/catbox/box/box.go new file mode 100644 index 000000000..06e909d85 --- /dev/null +++ b/tests/testdata/gencircle/catbox/box/box.go @@ -0,0 +1,17 @@ +package box + +type Unboxer[T any] interface { + Unbox() T +} + +type boxImp[T any] struct { + whatsInTheBox T +} + +func Box[T any](value T) Unboxer[T] { + return &boxImp[T]{whatsInTheBox: value} +} + +func (b *boxImp[T]) Unbox() T { + return b.whatsInTheBox +} diff --git a/tests/testdata/gencircle/catbox/cat/cat.go b/tests/testdata/gencircle/catbox/cat/cat.go new file mode 100644 index 000000000..f85f87751 --- /dev/null +++ b/tests/testdata/gencircle/catbox/cat/cat.go @@ -0,0 +1,5 @@ +package cat + +type Cat struct { + Name string +} diff --git a/tests/testdata/gencircle/catbox/collections/stack.go b/tests/testdata/gencircle/catbox/collections/stack.go new file mode 100644 index 000000000..9e22a24fb --- /dev/null +++ b/tests/testdata/gencircle/catbox/collections/stack.go @@ -0,0 +1,23 @@ +package collections + +type Stack[T any] struct{ values []T } + +func NewStack[T any]() *Stack[T] { + return &Stack[T]{} +} + +func (s *Stack[T]) Count() int { + return len(s.values) +} + +func (s *Stack[T]) Push(value T) { + s.values = append(s.values, value) +} + +func (s *Stack[T]) Pop() (value T) { + if len(s.values) > 0 { + maxIndex := len(s.values) - 1 + s.values, value = s.values[:maxIndex], s.values[maxIndex] + } + return +} diff --git a/tests/testdata/gencircle/catbox/main.go b/tests/testdata/gencircle/catbox/main.go new file mode 100644 index 000000000..97d4166ad --- /dev/null +++ b/tests/testdata/gencircle/catbox/main.go @@ -0,0 +1,19 @@ +// Test of instances of generic types causing dependencies. +// In this tests cat, box, and collections do not import each other directly +// but the main causes instances requiring `collections` to need `box` and +// `box` to need `cat` (thus `collections` indirectly needs `cat`). +// This test is also an attempt at a more realistic scenario. +package main + +import ( + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/catbox/box" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/catbox/cat" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/catbox/collections" +) + +func main() { + s := collections.NewStack[box.Unboxer[cat.Cat]]() + s.Push(box.Box(cat.Cat{Name: "Erwin"})) + s.Push(box.Box(cat.Cat{Name: "Dirac"})) + println(s.Pop().Unbox().Name) +} diff --git a/tests/testdata/gencircle/catbox/main.out b/tests/testdata/gencircle/catbox/main.out new file mode 100644 index 000000000..8415433d3 --- /dev/null +++ b/tests/testdata/gencircle/catbox/main.out @@ -0,0 +1 @@ +Dirac diff --git a/tests/testdata/gencircle/pingpong/cat/cat.go b/tests/testdata/gencircle/pingpong/cat/cat.go new file mode 100644 index 000000000..326b4a736 --- /dev/null +++ b/tests/testdata/gencircle/pingpong/cat/cat.go @@ -0,0 +1,16 @@ +package cat + +import "github.com/gopherjs/gopherjs/tests/testdata/gencircle/pingpong/collections" + +type Cat[H collections.Hasher] struct { + Name string +} + +func (c Cat[H]) Hash() uint { + var zero H + var h collections.Hasher = zero + for _, v := range c.Name { + h = h.Add(uint(v)) + } + return h.Sum() +} diff --git a/tests/testdata/gencircle/pingpong/collections/hashes.go b/tests/testdata/gencircle/pingpong/collections/hashes.go new file mode 100644 index 000000000..1b2c13ab6 --- /dev/null +++ b/tests/testdata/gencircle/pingpong/collections/hashes.go @@ -0,0 +1,21 @@ +package collections + +type Hasher interface { + Add(value uint) Hasher + Sum() uint +} + +type Hashable interface { + Hash() uint +} + +type BadHasher struct{ value uint } + +func (h BadHasher) Add(value uint) Hasher { + h.value += value + return h +} + +func (h BadHasher) Sum() uint { + return h.value +} diff --git a/tests/testdata/gencircle/pingpong/collections/hashmap.go b/tests/testdata/gencircle/pingpong/collections/hashmap.go new file mode 100644 index 000000000..0ab025415 --- /dev/null +++ b/tests/testdata/gencircle/pingpong/collections/hashmap.go @@ -0,0 +1,17 @@ +package collections + +// HashSet keeps a set of non-nil elements that have unique hashes. +type HashSet[E Hashable] struct { + data map[uint]E +} + +func (s *HashSet[E]) Add(e E) { + if s.data == nil { + s.data = map[uint]E{} + } + s.data[e.Hash()] = e +} + +func (s *HashSet[E]) Count() int { + return len(s.data) +} diff --git a/tests/testdata/gencircle/pingpong/main.go b/tests/testdata/gencircle/pingpong/main.go new file mode 100644 index 000000000..6c1bb24e1 --- /dev/null +++ b/tests/testdata/gencircle/pingpong/main.go @@ -0,0 +1,20 @@ +// Test of instances of generic types inverse dependencies. +// This is designed to test when types from package A is used around a type +// from package B, e.g. A.X[B.Y[A.Z]]. The type interfaces bounce back and +// forth between two packages. This means that A can not be simply +// run before B nor after A. The generics have to handle A needing B and +// B needing A to resolve a instances of generic types. +package main + +import ( + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/pingpong/cat" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/pingpong/collections" +) + +func main() { + s := collections.HashSet[cat.Cat[collections.BadHasher]]{} + s.Add(cat.Cat[collections.BadHasher]{Name: "Fluffy"}) + s.Add(cat.Cat[collections.BadHasher]{Name: "Mittens"}) + s.Add(cat.Cat[collections.BadHasher]{Name: "Whiskers"}) + println(s.Count(), "elements") +} diff --git a/tests/testdata/gencircle/pingpong/main.out b/tests/testdata/gencircle/pingpong/main.out new file mode 100644 index 000000000..deb6def3e --- /dev/null +++ b/tests/testdata/gencircle/pingpong/main.out @@ -0,0 +1 @@ +3 elements diff --git a/tests/testdata/gencircle/simple/bar/bar.go b/tests/testdata/gencircle/simple/bar/bar.go new file mode 100644 index 000000000..4ef580194 --- /dev/null +++ b/tests/testdata/gencircle/simple/bar/bar.go @@ -0,0 +1,5 @@ +package bar + +type Bar[G any] struct { + Next *G +} diff --git a/tests/testdata/gencircle/simple/foo/foo.go b/tests/testdata/gencircle/simple/foo/foo.go new file mode 100644 index 000000000..3bb5b2b3c --- /dev/null +++ b/tests/testdata/gencircle/simple/foo/foo.go @@ -0,0 +1,8 @@ +package foo + +import "github.com/gopherjs/gopherjs/tests/testdata/gencircle/simple/bar" + +type Entity struct { + Ref bar.Bar[Entity] + Name string +} diff --git a/tests/testdata/gencircle/simple/main.go b/tests/testdata/gencircle/simple/main.go new file mode 100644 index 000000000..bb180e195 --- /dev/null +++ b/tests/testdata/gencircle/simple/main.go @@ -0,0 +1,21 @@ +// Test of instances of generic types causing inverted dependencies. +// `bar.Bar[Entity]` requires `foo.Entity` but is imported in `foo`, meaning +// that `bar` is added to the package list prior `foo`. The setup of types +// must allow for `foo` to be added before `bar` types to lookup `foo`. +package main + +import ( + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/simple/bar" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/simple/foo" +) + +func main() { + e := foo.Entity{ + Ref: bar.Bar[foo.Entity]{ + Next: &foo.Entity{ + Name: `I am Next`, + }, + }, + } + println(e.Ref.Next.Name) +} diff --git a/tests/testdata/gencircle/simple/main.out b/tests/testdata/gencircle/simple/main.out new file mode 100644 index 000000000..0cd6369bf --- /dev/null +++ b/tests/testdata/gencircle/simple/main.out @@ -0,0 +1 @@ +I am Next diff --git a/tests/testdata/gencircle/trammel/cmp/cmp.go b/tests/testdata/gencircle/trammel/cmp/cmp.go new file mode 100644 index 000000000..873dbb51e --- /dev/null +++ b/tests/testdata/gencircle/trammel/cmp/cmp.go @@ -0,0 +1,10 @@ +package cmp + +// Barrowed from the cmp package in the Go standard library +// that isn't available until go1.21. +type Ordered interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | + ~float32 | ~float64 | + ~string +} diff --git a/tests/testdata/gencircle/trammel/collections/collections.go b/tests/testdata/gencircle/trammel/collections/collections.go new file mode 100644 index 000000000..f3f33f0e7 --- /dev/null +++ b/tests/testdata/gencircle/trammel/collections/collections.go @@ -0,0 +1,31 @@ +package collections + +func Populate[K comparable, V any, SK ~[]K, SV ~[]V, M ~map[K]V](m M, keys SK, values SV) { + // Lots of type parameters with parameters referencing each other. + for i, k := range keys { + if i < len(values) { + m[k] = values[i] + } else { + var zero V + m[k] = zero + } + } +} + +func KeysAndValues[K comparable, V any, M ~map[K]V](m M) struct { + Keys []K + Values []V +} { + keys := make([]K, 0, len(m)) + values := make([]V, 0, len(m)) + for k, v := range m { + keys = append(keys, k) + values = append(values, v) + } + // nested generic type that has a type parameter and nest type parameter. + type result[T any] struct { + Keys []T + Values []V + } + return result[K]{Keys: keys, Values: values} +} diff --git a/tests/testdata/gencircle/trammel/main.go b/tests/testdata/gencircle/trammel/main.go new file mode 100644 index 000000000..ce93763c8 --- /dev/null +++ b/tests/testdata/gencircle/trammel/main.go @@ -0,0 +1,38 @@ +// This is a test of several different kinds of generics. +// It is purposefully overly complecated for testing purposes. +// This integration test is similar to `compiler.Test_CrossPackageAnalysis`. + +package main + +import ( + "fmt" + + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/cmp" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/collections" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/stable" +) + +type StableMap[K cmp.Ordered, V any] map[K]V + +func (m StableMap[K, V]) String() string { + return stable.MapString(m, func(k K, v V) string { + return fmt.Sprintf(`%v: %v`, k, v) + }) +} + +type SIMap = StableMap[string, int] +type ISMap = StableMap[int, string] + +func main() { + m1 := SIMap{} + collections.Populate(m1, + []string{"one", "two", "three"}, + []int{1, 2, 3}) + println(m1.String()) + + m2 := ISMap{} + collections.Populate(m2, + []int{4, 5, 6}, + []string{"four", "five", "six"}) + println(m2.String()) +} diff --git a/tests/testdata/gencircle/trammel/main.out b/tests/testdata/gencircle/trammel/main.out new file mode 100644 index 000000000..7a0ccfbda --- /dev/null +++ b/tests/testdata/gencircle/trammel/main.out @@ -0,0 +1,2 @@ +{one: 1, three: 3, two: 2} +{4: four, 5: five, 6: six} diff --git a/tests/testdata/gencircle/trammel/sorts/sorts.go b/tests/testdata/gencircle/trammel/sorts/sorts.go new file mode 100644 index 000000000..d43550cd1 --- /dev/null +++ b/tests/testdata/gencircle/trammel/sorts/sorts.go @@ -0,0 +1,30 @@ +package sorts + +import "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/cmp" + +func Pair[K cmp.Ordered, V any, SK ~[]K, SV ~[]V](k SK, v SV) { + Bubble(struct { // non-generic struct in a generic context. + len func() int + less func(i, j int) bool + swap func(i, j int) + }{ + len: func() int { return len(k) }, + less: func(i, j int) bool { return k[i] < k[j] }, + swap: func(i, j int) { k[i], v[i], k[j], v[j] = k[j], v[j], k[i], v[i] }, + }) +} + +func Bubble(f struct { + len func() int + less func(i, j int) bool + swap func(i, j int) +}) { + length := f.len() + for i := 0; i < length; i++ { + for j := i + 1; j < length; j++ { + if f.less(j, i) { + f.swap(i, j) + } + } + } +} diff --git a/tests/testdata/gencircle/trammel/stable/mapString.go b/tests/testdata/gencircle/trammel/stable/mapString.go new file mode 100644 index 000000000..7204e1f3b --- /dev/null +++ b/tests/testdata/gencircle/trammel/stable/mapString.go @@ -0,0 +1,43 @@ +package stable + +import ( + "strings" + + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/cmp" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/collections" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/sorts" +) + +func MapString[K cmp.Ordered, V any, M ~map[K]V](m M, stringer func(K, V) string) string { + // Function parameter with type parameters arguments. + result := collections.KeysAndValues(m) + keys := result.Keys + values := result.Values + + sorts.Pair(keys, values) + + parts := zipper(keys, values, stringer) + return `{` + strings.Join(parts, `, `) + `}` +} + +func zipper[TIn1, TIn2, TOut any, SIn1 ~[]TIn1, SIn2 ~[]TIn2, F ~func(TIn1, TIn2) TOut](s1 SIn1, s2 SIn2, merge F) []TOut { + // Overly complex type parameters including a generic function type. + min := len(s1) + if len(s2) < min { + min = len(s2) + } + result := make([]any, min) + for i := 0; i < min; i++ { + result[i] = merge(s1[i], s2[i]) + } + return castSlice[any, TOut](result) +} + +func castSlice[TIn, TOut any, SIn ~[]TIn, SOut []TOut](s SIn) SOut { + result := make(SOut, len(s)) + for i, v := range s { + // Using a type parameter to cast the type. + result[i] = any(v).(TOut) + } + return result +} diff --git a/tests/testdata/legacy_syscall/main.go b/tests/testdata/legacy_syscall/main.go index 75ba22f6b..359d737af 100644 --- a/tests/testdata/legacy_syscall/main.go +++ b/tests/testdata/legacy_syscall/main.go @@ -1,5 +1,4 @@ //go:build legacy_syscall && gopherjs -// +build legacy_syscall,gopherjs // This program tests GopherJS's ability to perform raw syscalls using the // deprecated node_syscall extension. See TestLegacySyscall. diff --git a/tool.go b/tool.go index 9c467d04f..cd5a6dd0a 100644 --- a/tool.go +++ b/tool.go @@ -4,10 +4,8 @@ import ( "bytes" "errors" "fmt" - "go/ast" "go/build" "go/scanner" - "go/token" "go/types" "io" "net" @@ -28,8 +26,8 @@ import ( gbuild "github.com/gopherjs/gopherjs/build" "github.com/gopherjs/gopherjs/build/cache" "github.com/gopherjs/gopherjs/compiler" + "github.com/gopherjs/gopherjs/internal/errorList" "github.com/gopherjs/gopherjs/internal/sysutil" - "github.com/gopherjs/gopherjs/internal/testmain" "github.com/neelance/sourcemap" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -147,7 +145,7 @@ func main() { if err != nil { return err } - archive, err := s.BuildPackage(pkg) + archive, err := s.BuildProject(pkg) if err != nil { return err } @@ -214,8 +212,7 @@ func main() { if err != nil { return err } - - archive, err := s.BuildPackage(pkg) + archive, err := s.BuildProject(pkg) if err != nil { return err } @@ -375,27 +372,8 @@ func main() { return err } - _, err = s.BuildPackage(pkg.TestPackage()) - if err != nil { - return err - } - _, err = s.BuildPackage(pkg.XTestPackage()) - if err != nil { - return err - } - - fset := token.NewFileSet() - tests := testmain.TestMain{Package: pkg} - tests.Scan(fset) - mainPkg, mainFile, err := tests.Synthesize(fset) - if err != nil { - return fmt.Errorf("failed to generate testmain package for %s: %w", pkg.ImportPath, err) - } - importContext := &compiler.ImportContext{ - Packages: s.Types, - Import: s.ImportResolverFor(mainPkg), - } - mainPkgArchive, err := compiler.Compile(mainPkg.ImportPath, []*ast.File{mainFile}, fset, importContext, options.Minify) + pkg.IsTest = true + mainPkgArchive, err := s.BuildProject(pkg) if err != nil { return fmt.Errorf("failed to compile testmain package for %s: %w", pkg.ImportPath, err) } @@ -668,7 +646,7 @@ func (fs serveCommandFileSystem) Open(requestName string) (http.File, error) { buf := new(bytes.Buffer) browserErrors := new(bytes.Buffer) err := func() error { - archive, err := s.BuildPackage(pkg) + archive, err := s.BuildProject(pkg) if err != nil { return err } @@ -677,7 +655,7 @@ func (fs serveCommandFileSystem) Open(requestName string) (http.File, error) { m := &sourcemap.Map{File: base + ".js"} sourceMapFilter.MappingCallback = s.SourceMappingCallback(m) - deps, err := compiler.ImportDependencies(archive, s.BuildImportPath) + deps, err := compiler.ImportDependencies(archive, s.ImportResolverFor("")) if err != nil { return err } @@ -793,7 +771,7 @@ func handleError(err error, options *gbuild.Options, browserErrors *bytes.Buffer switch err := err.(type) { case nil: return 0 - case compiler.ErrorList: + case errorList.ErrorList: for _, entry := range err { printError(entry, options, browserErrors) } @@ -842,13 +820,7 @@ func sprintError(err error) string { func runNode(script string, args []string, dir string, quiet bool, out io.Writer) error { var allArgs []string if b, _ := strconv.ParseBool(os.Getenv("SOURCE_MAP_SUPPORT")); os.Getenv("SOURCE_MAP_SUPPORT") == "" || b { - allArgs = []string{"--require", "source-map-support/register"} - if err := exec.Command("node", "--require", "source-map-support/register", "--eval", "").Run(); err != nil { - if !quiet { - fmt.Fprintln(os.Stderr, "gopherjs: Source maps disabled. Install source-map-support module for nice stack traces. See https://github.com/gopherjs/gopherjs#gopherjs-run-gopherjs-test.") - } - allArgs = []string{} - } + allArgs = []string{"--enable-source-maps"} } if runtime.GOOS != "windows" {