From 3f2f9a7e70872991cf616768309677aa3d5fb1e8 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 6 Sep 2013 18:13:57 -0400 Subject: [PATCH] go.tools/importer: generalize command-line syntax. Motivation: pointer analysis tools (like the oracle) want the user to specify a set of initial packages, like 'go test'. This change enables the user to specify a set of packages on the command line using importer.LoadInitialPackages(args). Each argument is interpreted as either: - a comma-separated list of *.go source files together comprising one non-importable ad-hoc package. e.g. "src/pkg/net/http/triv.go" gives us [main]. - an import path, denoting both the imported package and its non-importable external test package, if any. e.g. "fmt" gives us [fmt, fmt_test]. Current type-checker limitations mean that only the first import path may contribute tests: multiple packages augmented by *_test.go files could create import cycles, which 'go test' avoids by building a separate executable for each one. That approach is less attractive for static analysis. Details: (many files touched, but importer.go is the crux) importer: - PackageInfo.Importable boolean indicates whether package is importable. - un-expose Importer.Packages; expose AllPackages() instead. - CreatePackageFromArgs has become LoadInitialPackages. - imports() moved to util.go, renamed importsOf(). - InitialPackagesUsage usage message exported to clients. - the package name for ad-hoc packages now comes from the 'package' decl, not "main". ssa.Program: - added CreatePackages() method - PackagesByPath un-exposed, renamed 'imported'. - expose AllPackages and ImportedPackage accessors. oracle: - describe: explain and workaround a go/types bug. Misc: - Removed various unnecessary error.Error() calls in Printf args. R=crawshaw CC=golang-dev https://golang.org/cl/13579043 --- cmd/oracle/main.go | 11 +- cmd/ssadump/main.go | 30 +- importer/importer.go | 522 +++++++++++++----- importer/pkginfo.go | 1 + importer/source.go | 2 +- importer/source_test.go | 14 +- importer/util.go | 97 ++-- oracle/describe.go | 36 +- oracle/oracle.go | 34 +- oracle/testdata/src/main/describe-json.golden | 4 +- oracle/testdata/src/main/describe.golden | 4 +- oracle/testdata/src/main/imports.golden | 2 +- oracle/testdata/src/main/peers.golden | 2 +- pointer/analysis.go | 2 +- pointer/pointer_test.go | 20 +- ssa/builder.go | 2 +- ssa/builder_test.go | 35 +- ssa/create.go | 52 +- ssa/example_test.go | 16 +- ssa/interp/interp.go | 13 +- ssa/interp/interp_test.go | 14 +- ssa/source_test.go | 34 +- ssa/ssa.go | 10 +- ssa/stdlib_test.go | 31 +- ssa/testmain.go | 2 +- ssa/visit.go | 2 +- 26 files changed, 664 insertions(+), 328 deletions(-) diff --git a/cmd/oracle/main.go b/cmd/oracle/main.go index 7bad67b0..64b26360 100644 --- a/cmd/oracle/main.go +++ b/cmd/oracle/main.go @@ -25,6 +25,7 @@ import ( "runtime" "runtime/pprof" + "code.google.com/p/go.tools/importer" "code.google.com/p/go.tools/oracle" ) @@ -41,13 +42,13 @@ var ptalogFlag = flag.String("ptalog", "", var formatFlag = flag.String("format", "plain", "Output format: 'plain' or 'json'.") const usage = `Go source code oracle. -Usage: oracle [ ...] [ ...] [ ...] +Usage: oracle [ ...] ... Use -help flag to display options. Examples: % oracle -pos=hello.go:#123 hello.go % oracle -pos=hello.go:#123-#456 hello.go -` +` + importer.InitialPackagesUsage var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") @@ -76,7 +77,7 @@ func main() { var ptalog io.Writer if *ptalogFlag != "" { if f, err := os.Create(*ptalogFlag); err != nil { - log.Fatalf(err.Error()) + log.Fatal(err.Error()) } else { buf := bufio.NewWriter(f) ptalog = buf @@ -106,7 +107,7 @@ func main() { // Ask the oracle. res, err := oracle.Query(args, *modeFlag, *posFlag, ptalog, &build.Default) if err != nil { - fmt.Fprintf(os.Stderr, "Error: %s\n", err) + fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } @@ -115,7 +116,7 @@ func main() { case "json": b, err := json.Marshal(res) if err != nil { - fmt.Fprintf(os.Stderr, "JSON error: %s\n", err.Error()) + fmt.Fprintf(os.Stderr, "JSON error: %s\n", err) os.Exit(1) } var buf bytes.Buffer diff --git a/cmd/ssadump/main.go b/cmd/ssadump/main.go index e35e3ca7..b0bca11c 100644 --- a/cmd/ssadump/main.go +++ b/cmd/ssadump/main.go @@ -123,22 +123,36 @@ func main() { // Load, parse and type-check the program. imp := importer.New(&impctx) - info, args, err := importer.CreatePackageFromArgs(imp, args) + infos, args, err := imp.LoadInitialPackages(args) if err != nil { - log.Fatal(err.Error()) + log.Fatal(err) } // Create and build SSA-form program representation. prog := ssa.NewProgram(imp.Fset, mode) - for _, info := range imp.Packages { - prog.CreatePackage(info).SetDebugMode(debugMode) + if err := prog.CreatePackages(imp); err != nil { + log.Fatal(err) + } + if debugMode { + for _, pkg := range prog.AllPackages() { + pkg.SetDebugMode(true) + } } prog.BuildAll() - prog.Package(info.Pkg).CreateTestMainFunction() // TODO(adonovan): remove hack - - // Run the interpreter. + // Run the interpreter on the first package with a main function. if *runFlag { - interp.Interpret(prog.Package(info.Pkg), interpMode, info.Pkg.Path(), args) + var main *ssa.Package + for _, info := range infos { + pkg := prog.Package(info.Pkg) + if pkg.Func("main") != nil || pkg.CreateTestMainFunction() != nil { + main = pkg + break + } + } + if main == nil { + log.Fatal("No main function") + } + interp.Interpret(main, interpMode, main.Object.Path(), args) } } diff --git a/importer/importer.go b/importer/importer.go index cd004987..27251c57 100644 --- a/importer/importer.go +++ b/importer/importer.go @@ -6,49 +6,83 @@ // type-checks packages of Go code plus their transitive closure, and // retains both the ASTs and the derived facts. // -// TODO(adonovan): document and test this package better, with examples. -// Currently it's covered by the ssa/ tests. +// CONCEPTS AND TERMINOLOGY +// +// An AD-HOC package is one specified as a set of source files on the +// command line. In the simplest case, it may consist of a single file +// such as src/pkg/net/http/triv.go. +// +// EXTERNAL TEST packages are those comprised of a set of *_test.go +// files all with the same 'package foo_test' declaration, all in the +// same directory. (go/build.Package calls these files XTestFiles.) +// +// An IMPORTABLE package is one that can be referred to by some import +// spec. Ad-hoc packages and external test packages are non-importable. +// The importer and its clients must be careful not to assume that +// the import path of a package may be used for a name-based lookup. +// For example, a pointer analysis scope may consist of two initial +// (ad-hoc) packages both called "main". +// +// An AUGMENTED package is an importable package P plus all the +// *_test.go files with same 'package foo' declaration as P. +// (go/build.Package calls these files TestFiles.) +// An external test package may depend upon members of the augmented +// package that are not in the unaugmented package, such as functions +// that expose internals. (See bufio/export_test.go for an example.) +// So, the importer must ensure that for each external test package +// it loads, it also augments the corresponding non-test package. +// +// The import graph over n unaugmented packages must be acyclic; the +// import graph over n-1 unaugmented packages plus one augmented +// package must also be acyclic. ('go test' relies on this.) But the +// import graph over n augmented packages may contain cycles, and +// currently, go/types is incapable of handling such inputs, so the +// Importer will only augment (and create an external test package +// for) the first import path specified on the command-line. +// +// TODO(adonovan): more tests. // package importer import ( + "errors" "fmt" "go/ast" "go/build" "go/token" "os" - "strconv" + "strings" "sync" "code.google.com/p/go.tools/go/exact" "code.google.com/p/go.tools/go/types" ) -// An Importer's methods are not thread-safe unless specified. +// An Importer's exported methods are not thread-safe. type Importer struct { - config *Config // the client configuration - Fset *token.FileSet // position info for all files seen - Packages map[string]*PackageInfo // successfully imported packages keyed by import path - errors map[string]error // cache of errors by import path - - mu sync.Mutex // guards 'packages' map - packages map[string]*pkgInfo // all attempted imported packages, keyed by import path + Fset *token.FileSet // position info for all files seen + config *Config // the client configuration + augment map[string]bool // packages to be augmented by TestFiles when imported + allPackagesMu sync.Mutex // guards 'allPackages' during internal concurrency + allPackages []*PackageInfo // all packages, including non-importable ones + importedMu sync.Mutex // guards 'imported' + imported map[string]*importInfo // all imported packages (incl. failures) by import path } -// pkgInfo holds internal per-package information. -type pkgInfo struct { - info *PackageInfo // success info - err error // failure info +// importInfo holds internal information about each import path. +type importInfo struct { + path string // import path + info *PackageInfo // results of typechecking (including type errors) + err error // reason for failure to construct a package ready chan struct{} // channel close is notification of ready state } // Config specifies the configuration for the importer. -// type Config struct { // TypeChecker contains options relating to the type checker. // The Importer will override any user-supplied values for its // Error and Import fields; other fields will be passed - // through to the type checker. + // through to the type checker. All callbacks must be thread-safe. TypeChecker types.Config // If Build is non-nil, it is used to satisfy imports. @@ -68,151 +102,165 @@ type Config struct { // func New(config *Config) *Importer { imp := &Importer{ - config: config, Fset: token.NewFileSet(), - packages: make(map[string]*pkgInfo), - Packages: make(map[string]*PackageInfo), - errors: make(map[string]error), + config: config, + augment: make(map[string]bool), + imported: make(map[string]*importInfo), } + // TODO(adonovan): get typechecker to supply us with a source + // position, then pass errors back to the application + // (e.g. oracle). imp.config.TypeChecker.Error = func(e error) { fmt.Fprintln(os.Stderr, e) } imp.config.TypeChecker.Import = imp.doImport return imp } -// doImport loads the typechecker package identified by path -// Implements the types.Importer prototype. +// AllPackages returns a new slice containing all packages loaded by +// importer imp. // -func (imp *Importer) doImport(imports map[string]*types.Package, path string) (pkg *types.Package, err error) { +func (imp *Importer) AllPackages() []*PackageInfo { + return append([]*PackageInfo(nil), imp.allPackages...) +} + +func (imp *Importer) addPackage(info *PackageInfo) { + imp.allPackagesMu.Lock() + imp.allPackages = append(imp.allPackages, info) + imp.allPackagesMu.Unlock() +} + +// doImport imports the package denoted by path. +// It implements the types.Importer prototype. +// +// imports is the import map of the importing package, later +// accessible as types.Package.Imports(). If non-nil, doImport will +// update it to include this import. (It may be nil in recursive +// calls for prefetching.) +// +// It returns an error if a package could not be created +// (e.g. go/build or parse error), but type errors are reported via +// the types.Config.Error callback (the first of which is also saved +// in the package's PackageInfo). +// +// Idempotent and thread-safe, but assumes that no two concurrent +// calls will provide the same 'imports' map. +// +func (imp *Importer) doImport(imports map[string]*types.Package, path string) (*types.Package, error) { // Package unsafe is handled specially, and has no PackageInfo. + // TODO(adonovan): a fake empty package would make things simpler. if path == "unsafe" { return types.Unsafe, nil } - // Load the source/binary for 'path', type-check it, construct - // a PackageInfo and update our map (imp.Packages) and the - // type-checker's map (imports). - // TODO(adonovan): make it the type-checker's job to - // consult/update the 'imports' map. Then we won't need it. - var info *PackageInfo - if imp.config.Build != nil { - info, err = imp.LoadPackage(path) - } else { - if info, ok := imp.Packages[path]; ok { - imports[path] = info.Pkg - pkg = info.Pkg - return // positive cache hit - } - - if err = imp.errors[path]; err != nil { - return // negative cache hit - } - - if pkg, err = types.GcImport(imports, path); err == nil { - info = &PackageInfo{Pkg: pkg} - imp.Packages[path] = info - } + info, err := imp.doImport0(imports, path) + if err != nil { + return nil, err } - if err == nil { - // Cache success. - pkg = info.Pkg - imports[path] = pkg - return pkg, nil + if imports != nil { + // Update the package's imports map, whether success or failure. + // + // types.Package.Imports() is used by PackageInfo.Imports and + // thence by ssa.builder. + // TODO(gri): given that it doesn't specify whether it + // contains direct or transitive dependencies, it probably + // shouldn't be exposed. This package can make its own + // arrangements to implement PackageInfo.Imports(). + imports[path] = info.Pkg } - // Cache failure - imp.errors[path] = err - return nil, err + return info.Pkg, nil } -// imports returns the set of paths imported by the specified files. -func imports(p string, files []*ast.File) map[string]bool { - imports := make(map[string]bool) -outer: - for _, file := range files { - for _, decl := range file.Decls { - if decl, ok := decl.(*ast.GenDecl); ok { - if decl.Tok != token.IMPORT { - break outer // stop at the first non-import - } - for _, spec := range decl.Specs { - spec := spec.(*ast.ImportSpec) - if path, _ := strconv.Unquote(spec.Path.Value); path != "C" { - imports[path] = true - } - } - } else { - break outer // stop at the first non-import - } - } - } - return imports -} - -// LoadPackage loads the package of the specified import path, -// performs type-checking, and returns the corresponding -// PackageInfo. -// -// Precondition: Importer.config.Build != nil. -// Idempotent and thread-safe. -// -// TODO(adonovan): fix: violated in call from CreatePackageFromArgs! -// TODO(adonovan): rethink this API. -// -func (imp *Importer) LoadPackage(importPath string) (*PackageInfo, error) { - if imp.config.Build == nil { - panic("Importer.LoadPackage without a go/build.Config") - } - - imp.mu.Lock() - pi, ok := imp.packages[importPath] +// Like doImport, but returns a PackageInfo. +// Precondition: path != "unsafe". +func (imp *Importer) doImport0(imports map[string]*types.Package, path string) (*PackageInfo, error) { + imp.importedMu.Lock() + ii, ok := imp.imported[path] if !ok { - // First request for this pkgInfo: - // create a new one in !ready state. - pi = &pkgInfo{ready: make(chan struct{})} - imp.packages[importPath] = pi - imp.mu.Unlock() + ii = &importInfo{path: path, ready: make(chan struct{})} + imp.imported[path] = ii + } + imp.importedMu.Unlock() - // Load/parse the package. - if files, err := loadPackage(imp.config.Build, imp.Fset, importPath); err == nil { - // Kick off asynchronous I/O and parsing for the imports. - for path := range imports(importPath, files) { - go func(path string) { imp.LoadPackage(path) }(path) - } - - // Type-check the package. - pi.info, pi.err = imp.CreateSourcePackage(importPath, files) + if !ok { + // Find and create the actual package. + if imp.config.Build != nil { + imp.importSource(path, ii) } else { - pi.err = err + imp.importBinary(imports, ii) } + ii.info.Importable = true - close(pi.ready) // enter ready state and wake up waiters + close(ii.ready) // enter ready state and wake up waiters } else { - imp.mu.Unlock() - - <-pi.ready // wait for ready condition. + <-ii.ready // wait for ready condition } - // Invariant: pi is ready. + // Invariant: ii is ready. - if pi.err != nil { - return nil, pi.err - } - return pi.info, nil + return ii.info, ii.err } -// CreateSourcePackage invokes the type-checker on files and returns a -// PackageInfo containing the resulting type-checker package, the -// ASTs, and other type information. +// importBinary implements package loading from object files from the +// gc compiler. +// +func (imp *Importer) importBinary(imports map[string]*types.Package, ii *importInfo) { + pkg, err := types.GcImport(imports, ii.path) + if pkg != nil { + ii.info = &PackageInfo{Pkg: pkg} + imp.addPackage(ii.info) + } else { + ii.err = err + } +} + +// importSource implements package loading by parsing Go source files +// located by go/build. +// +func (imp *Importer) importSource(path string, ii *importInfo) { + which := "g" // load *.go files + if imp.augment[path] { + which = "gt" // augment package by in-package *_test.go files + } + if files, err := parsePackageFiles(imp.config.Build, imp.Fset, path, which); err == nil { + // Prefetch the imports asynchronously. + for path := range importsOf(path, files) { + go func(path string) { imp.doImport(nil, path) }(path) + } + + // Type-check the package. + ii.info = imp.typeCheck(path, files) + + // We needn't wait for the prefetching goroutines to + // finish. Each one either runs quickly and populates + // the imported map, in which case the type checker + // will wait for the map entry to become ready; or, it + // runs slowly, even after we return, but then becomes + // just another map waiter, in which case it won't + // mutate anything. + } else { + ii.err = err + } +} + +// typeCheck invokes the type-checker on files and returns a +// PackageInfo containing the resulting types.Package, the ASTs, and +// other type information. // // The order of files determines the package initialization order. // -// importPath is the full name under which this package is known, such -// as appears in an import declaration. e.g. "sync/atomic". +// path is the full name under which this package is known, such as +// appears in an import declaration. e.g. "sync/atomic". It need not +// be unique; for example, it is possible to construct two distinct +// packages both named "main". // -// Postcondition: for result (info, err), info.Err == err. +// The resulting package is added to imp.allPackages, but is not +// importable unless it is inserted in the imp.imported map. // -func (imp *Importer) CreateSourcePackage(importPath string, files []*ast.File) (*PackageInfo, error) { +// This function always succeeds, but the package may contain type +// errors; the first of these is recorded in PackageInfo.Err. +// +func (imp *Importer) typeCheck(path string, files []*ast.File) *PackageInfo { info := &PackageInfo{ Files: files, Info: types.Info{ @@ -224,7 +272,221 @@ func (imp *Importer) CreateSourcePackage(importPath string, files []*ast.File) ( Selections: make(map[*ast.SelectorExpr]*types.Selection), }, } - info.Pkg, info.Err = imp.config.TypeChecker.Check(importPath, imp.Fset, files, &info.Info) - imp.Packages[importPath] = info - return info, info.Err + info.Pkg, info.Err = imp.config.TypeChecker.Check(path, imp.Fset, files, &info.Info) + imp.addPackage(info) + return info +} + +// LoadMainPackage creates and type-checks a package called "main" from +// the specified list of parsed files, importing its dependencies. +// +// The resulting package is not importable, i.e. no 'import' spec can +// resolve to it. LoadMainPackage is provided as an aid to testing. +// +// LoadMainPackage never fails, but the resulting package may contain +// type errors. +// +func (imp *Importer) LoadMainPackage(files ...*ast.File) *PackageInfo { + return imp.typeCheck("main", files) +} + +// InitialPackagesUsage is a partial usage message that client +// applications may wish to include in their -help output. +const InitialPackagesUsage = ` + is a list of arguments denoting a set of initial pacakges. +Each argument may take one of two forms: + +1. A comma-separated list of *.go source files. + + All of the specified files are loaded, parsed and type-checked + as a single package. The name of the package is taken from the + files' package declarations, which must all be equal. All the + files must belong to the same directory. + +2. An import path. + + The package's directory is found relative to the $GOROOT and + $GOPATH using similar logic to 'go build', and the *.go files in + that directory are loaded and parsed, and type-checked as a single + package. + + In addition, all *_test.go files in the directory are then loaded + and parsed. Those files whose package declaration equals that of + the non-*_test.go files are included in the primary package. Test + files whose package declaration ends with "_test" are type-checked + as another package, the 'external' test package, so that a single + import path may denote two packages. This behaviour may be + disabled by prefixing the import path with "notest:", + e.g. "notest:fmt". + + Due to current limitations in the type-checker, only the first + import path of the command line will contribute any tests. + +A '--' argument terminates the list of packages. +` + +// LoadInitialPackages interprets args as a set of packages, loads +// those packages and their dependencies, and returns them. +// +// It is intended for use in command-line interfaces that require a +// set of initial packages to be specified; see InitialPackagesUsage +// message for details. +// +// The second result parameter returns the list of unconsumed +// arguments. +// +// It is an error to specify no packages. +// +// Precondition: LoadInitialPackages cannot be called after any +// previous calls to Load* on the same importer. +// +func (imp *Importer) LoadInitialPackages(args []string) ([]*PackageInfo, []string, error) { + // The "augmentation" mechanism requires that we mark all + // packages to be augmented before we import a single one. + if len(imp.allPackages) > 0 { + return nil, nil, errors.New("LoadInitialPackages called on non-pristine Importer") + } + + // We use two passes. The first parses the files for each + // non-importable package and discovers the set of importable + // packages that require augmentation by in-package _test.go + // files. The second creates the ad-hoc packages and imports + // the importable ones. + // + // This is necessary to ensure that all packages requiring + // augmentation are known before before any package is + // imported. + + // Pass 1: parse the sets of files for each package. + var pkgs []*initialPkg + for len(args) > 0 { + arg := args[0] + args = args[1:] + if arg == "--" { + break // consume "--" and return the remaining args + } + + if strings.HasSuffix(arg, ".go") { + // Assume arg is a comma-separated list of *.go files + // comprising a single package. + pkg, err := initialPackageFromFiles(imp.Fset, arg) + if err != nil { + return nil, nil, err + } + pkgs = append(pkgs, pkg) + + } else { + // Assume arg is a directory name denoting a + // package, perhaps plus an external test + // package unless prefixed by "notest:". + path := strings.TrimPrefix(arg, "notest:") + + if path == "unsafe" { + continue // ignore; has no PackageInfo + } + + pkg := &initialPkg{ + path: path, + importable: true, + } + pkgs = append(pkgs, pkg) + + if path != arg { + continue // had "notest:" prefix + } + + if imp.config.Build == nil { + continue // can't locate *_test.go files + } + + // TODO(adonovan): due to limitations of the current type + // checker design, we can augment at most one package. + if len(imp.augment) > 0 { + continue // don't attempt a second + } + + // Load the external test package. + xtestFiles, err := parsePackageFiles(imp.config.Build, imp.Fset, path, "x") + if err != nil { + return nil, nil, err + } + if len(xtestFiles) > 0 { + pkgs = append(pkgs, &initialPkg{ + path: path + "_test", + importable: false, + files: xtestFiles, + }) + } + + // Mark the non-xtest package for augmentation with + // in-package *_test.go files when we import it below. + imp.augment[pkg.path] = true + } + } + + // Pass 2: type-check each set of files to make a package. + var infos []*PackageInfo + for _, pkg := range pkgs { + var info *PackageInfo + if pkg.importable { + // import package + var err error + info, err = imp.doImport0(nil, pkg.path) + if err != nil { + return nil, nil, err // e.g. parse error (but not type error) + } + } else { + // create package + info = imp.typeCheck(pkg.path, pkg.files) + } + infos = append(infos, info) + } + + if len(pkgs) == 0 { + return nil, nil, errors.New("no *.go source files nor packages were specified") + } + + return infos, args, nil +} + +type initialPkg struct { + path string // the package's import path + importable bool // add package to import map false for main and xtests) + files []*ast.File // set of files (non-importable packages only) +} + +// initialPackageFromFiles returns an initialPkg, given a +// comma-separated list of *.go source files belonging to the same +// directory and possessing the same 'package decl'. +// +func initialPackageFromFiles(fset *token.FileSet, arg string) (*initialPkg, error) { + filenames := strings.Split(arg, ",") + for _, filename := range filenames { + if !strings.HasSuffix(filename, ".go") { + return nil, fmt.Errorf("not a *.go source file: %q", filename) + } + } + + files, err := ParseFiles(fset, ".", filenames...) + if err != nil { + return nil, err + } + + // Take the package name from the 'package decl' in each file, + // all of which must match. + pkgname := files[0].Name.Name + for i, file := range files[1:] { + if pn := file.Name.Name; pn != pkgname { + err := fmt.Errorf("can't load package: found packages %s (%s) and %s (%s)", + pkgname, filenames[0], pn, filenames[i]) + return nil, err + } + // TODO(adonovan): check dirnames are equal, like 'go build' does. + } + + return &initialPkg{ + path: pkgname, + importable: false, + files: files, + }, nil } diff --git a/importer/pkginfo.go b/importer/pkginfo.go index e032d3e1..72f40ea5 100644 --- a/importer/pkginfo.go +++ b/importer/pkginfo.go @@ -23,6 +23,7 @@ import ( // type PackageInfo struct { Pkg *types.Package + Importable bool // true if 'import "Pkg.Path()"' would resolve to this Err error // non-nil if the package had static errors Files []*ast.File // abstract syntax for the package's files types.Info // type-checker deductions. diff --git a/importer/source.go b/importer/source.go index d7b0cbf0..3279bff9 100644 --- a/importer/source.go +++ b/importer/source.go @@ -640,7 +640,7 @@ func tokenFileContainsPos(f *token.File, pos token.Pos) bool { // The result is (nil, nil, false) if not found. // func (imp *Importer) PathEnclosingInterval(start, end token.Pos) (pkg *PackageInfo, path []ast.Node, exact bool) { - for _, info := range imp.Packages { + for _, info := range imp.allPackages { for _, f := range info.Files { if !tokenFileContainsPos(imp.Fset.File(f.Package), start) { continue diff --git a/importer/source_test.go b/importer/source_test.go index 509cbf6a..743f4c16 100644 --- a/importer/source_test.go +++ b/importer/source_test.go @@ -249,17 +249,13 @@ func TestEnclosingFunction(t *testing.T) { t.Errorf("EnclosingFunction(%q) not exact", test.substr) continue } - info, err := imp.CreateSourcePackage("main", []*ast.File{f}) - if err != nil { - t.Error(err.Error()) + mainInfo := imp.LoadMainPackage(f) + prog := ssa.NewProgram(imp.Fset, 0) + if err := prog.CreatePackages(imp); err != nil { + t.Error(err) continue } - - prog := ssa.NewProgram(imp.Fset, 0) - for _, info := range imp.Packages { - prog.CreatePackage(info) - } - pkg := prog.Package(info.Pkg) + pkg := prog.Package(mainInfo.Pkg) pkg.Build() name := "(none)" diff --git a/importer/util.go b/importer/util.go index bc153f37..d66a7366 100644 --- a/importer/util.go +++ b/importer/util.go @@ -8,55 +8,16 @@ package importer // and used by it. import ( - "errors" "go/ast" "go/build" "go/parser" "go/token" "os" "path/filepath" - "strings" + "strconv" "sync" ) -// CreatePackageFromArgs builds an initial Package from a list of -// command-line arguments. -// If args is a list of *.go files, they are parsed and type-checked. -// If args is a Go package import path, that package is imported. -// The rest result contains the suffix of args that were not consumed. -// -// This utility is provided to facilitate construction of command-line -// tools with a consistent user interface. -// -func CreatePackageFromArgs(imp *Importer, args []string) (info *PackageInfo, rest []string, err error) { - switch { - case len(args) == 0: - return nil, nil, errors.New("no *.go source files nor package name was specified.") - - case strings.HasSuffix(args[0], ".go"): - // % tool a.go b.go ... - // Leading consecutive *.go arguments constitute main package. - i := 1 - for ; i < len(args) && strings.HasSuffix(args[i], ".go"); i++ { - } - var files []*ast.File - files, err = ParseFiles(imp.Fset, ".", args[:i]...) - rest = args[i:] - if err == nil { - info, err = imp.CreateSourcePackage("main", files) - } - - default: - // % tool my/package ... - // First argument is import path of main package. - pkgname := args[0] - info, err = imp.LoadPackage(pkgname) - rest = args[1:] - } - - return -} - var cwd string func init() { @@ -67,9 +28,15 @@ func init() { } } -// loadPackage ascertains which files belong to package path, then -// loads, parses and returns them. -func loadPackage(ctxt *build.Context, fset *token.FileSet, path string) (files []*ast.File, err error) { +// parsePackageFiles enumerates the files belonging to package path, +// then loads, parses and returns them. +// +// 'which' is a list of flags indicating which files to include: +// 'g': include non-test *.go source files (GoFiles) +// 't': include in-package *_test.go source files (TestGoFiles) +// 'x': include external *_test.go source files. (XTestGoFiles) +// +func parsePackageFiles(ctxt *build.Context, fset *token.FileSet, path string, which string) ([]*ast.File, error) { // Set the "!cgo" go/build tag, preferring (dummy) Go to // native C implementations of net.cgoLookupHost et al. ctxt2 := *ctxt @@ -82,9 +49,25 @@ func loadPackage(ctxt *build.Context, fset *token.FileSet, path string) (files [ return nil, nil // empty directory } if err != nil { - return // import failed + return nil, err // import failed } - return ParseFiles(fset, bp.Dir, bp.GoFiles...) + + var filenames []string + for _, c := range which { + var s []string + switch c { + case 'g': + s = bp.GoFiles + case 't': + s = bp.TestGoFiles + case 'x': + s = bp.XTestGoFiles + default: + panic(c) + } + filenames = append(filenames, s...) + } + return ParseFiles(fset, bp.Dir, filenames...) } // ParseFiles parses the Go source files files within directory dir @@ -132,3 +115,27 @@ func unparen(e ast.Expr) ast.Expr { func unreachable() { panic("unreachable") } + +// importsOf returns the set of paths imported by the specified files. +func importsOf(p string, files []*ast.File) map[string]bool { + imports := make(map[string]bool) +outer: + for _, file := range files { + for _, decl := range file.Decls { + if decl, ok := decl.(*ast.GenDecl); ok { + if decl.Tok != token.IMPORT { + break outer // stop at the first non-import + } + for _, spec := range decl.Specs { + spec := spec.(*ast.ImportSpec) + if path, _ := strconv.Unquote(spec.Path.Value); path != "C" { + imports[path] = true + } + } + } else { + break outer // stop at the first non-import + } + } + } + return imports +} diff --git a/oracle/describe.go b/oracle/describe.go index dc409dba..49ce9b9d 100644 --- a/oracle/describe.go +++ b/oracle/describe.go @@ -693,22 +693,41 @@ func (r *describeTypeResult) toJSON(res *json.Result, fset *token.FileSet) { func describePackage(o *oracle, path []ast.Node) (*describePackageResult, error) { var description string + var pkg *ssa.Package var importPath string switch n := path[0].(type) { case *ast.ImportSpec: - // importPath = o.queryPkgInfo.ObjectOf(n.Name).(*types.Package).Path() - // description = "import of package " + importPath - // TODO(gri): o.queryPkgInfo.ObjectOf(n.Name) may be nil. - // e.g. "fmt" import in cmd/oracle/main.go. Why? - // Workaround: + // Most ImportSpecs have no .Name Ident so we can't + // use ObjectOf. + // We could use the types.Info.Implicits mechanism, + // but it's easier just to look it up by name. description = "import of package " + n.Path.Value importPath, _ = strconv.Unquote(n.Path.Value) + pkg = o.prog.ImportedPackage(importPath) case *ast.Ident: - importPath = o.queryPkgInfo.ObjectOf(n).(*types.Package).Path() + obj := o.queryPkgInfo.ObjectOf(n).(*types.Package) + importPath = obj.Path() + if _, isDef := path[1].(*ast.File); isDef { + // e.g. package id + pkg = o.prog.Package(obj) description = fmt.Sprintf("definition of package %q", importPath) } else { + // e.g. import id + // or id.F() + + // TODO(gri): go/types internally creates a new + // Package object for each import, so the packages + // for 'package x' and 'import "x"' differ! + // + // Here, this should be an invariant, but is not: + // o.prog.ImportedPackage(obj.Path()).Pkg == obj + // + // So we must use the name of the non-canonical package + // to do another lookup. + pkg = o.prog.ImportedPackage(importPath) + description = fmt.Sprintf("reference to package %q", importPath) } if importPath == "" { @@ -722,8 +741,9 @@ func describePackage(o *oracle, path []ast.Node) (*describePackageResult, error) } var members []*describeMember - // NB: package "unsafe" has no object. - if pkg := o.prog.PackagesByPath[importPath]; pkg != nil { + // NB: "unsafe" has no ssa.Package + // TODO(adonovan): simplify by using types.Packages not ssa.Packages. + if pkg != nil { // Compute set of exported package members in lexicographic order. var names []string for name := range pkg.Members { diff --git a/oracle/oracle.go b/oracle/oracle.go index 899c0c2d..ee452b71 100644 --- a/oracle/oracle.go +++ b/oracle/oracle.go @@ -123,7 +123,7 @@ func Query(args []string, mode, pos string, ptalog io.Writer, buildContext *buil minfo, ok := modes[mode] if !ok { if mode == "" { - return nil, errors.New("you must specify a -mode to perform") + return nil, errors.New("you must specify a -mode of query to perform") } return nil, fmt.Errorf("invalid mode type: %q", mode) } @@ -155,9 +155,12 @@ func Query(args []string, mode, pos string, ptalog io.Writer, buildContext *buil // Load/parse/type-check program from args. start := time.Now() - initialPkgInfo, _, err := importer.CreatePackageFromArgs(imp, args) + initialPkgInfos, args, err := imp.LoadInitialPackages(args) if err != nil { - return nil, err // I/O, parser or type error + return nil, err // I/O or parser error + } + if len(args) > 0 { + return nil, fmt.Errorf("surplus arguments: %q", args) } o.timers["load/parse/type"] = time.Since(start) @@ -184,21 +187,26 @@ func Query(args []string, mode, pos string, ptalog io.Writer, buildContext *buil if minfo.needs&SSA != 0 { start = time.Now() - // All packages. - for _, info := range imp.Packages { - o.prog.CreatePackage(info) // create ssa.Package + // Create SSA packages. + if err := o.prog.CreatePackages(imp); err != nil { + return nil, o.errorf(false, "%s", err) } - // Initial package (specified on command line) - initialPkg := o.prog.Package(initialPkgInfo.Pkg) + // Initial packages (specified on command line) + for _, info := range initialPkgInfos { + initialPkg := o.prog.Package(info.Pkg) - // Add package to the pointer analysis scope. - if initialPkg.Func("main") == nil { - if initialPkg.CreateTestMainFunction() == nil { - return nil, o.errorf(false, "analysis scope has no main() entry points") + // Add package to the pointer analysis scope. + if initialPkg.Func("main") == nil { + // TODO(adonovan): to simulate 'go test' more faithfully, we + // should build a single synthetic testmain package, + // not synthetic main functions to many packages. + if initialPkg.CreateTestMainFunction() == nil { + return nil, o.errorf(false, "analysis scope has no main() entry points") + } } + o.config.Mains = append(o.config.Mains, initialPkg) } - o.config.Mains = append(o.config.Mains, initialPkg) // Query package. if o.queryPkgInfo != nil { diff --git a/oracle/testdata/src/main/describe-json.golden b/oracle/testdata/src/main/describe-json.golden index 3a28beb4..79408feb 100644 --- a/oracle/testdata/src/main/describe-json.golden +++ b/oracle/testdata/src/main/describe-json.golden @@ -2,11 +2,11 @@ { "mode": "describe", "describe": { - "desc": "definition of package \"main\"", + "desc": "definition of package \"describe\"", "pos": "testdata/src/main/describe-json.go:1:9", "detail": "package", "package": { - "path": "main", + "path": "describe", "members": [ { "name": "C", diff --git a/oracle/testdata/src/main/describe.golden b/oracle/testdata/src/main/describe.golden index 07f64997..70376e71 100644 --- a/oracle/testdata/src/main/describe.golden +++ b/oracle/testdata/src/main/describe.golden @@ -1,5 +1,5 @@ -------- @describe pkgdecl -------- -definition of package "main" +definition of package "describe" type C int method (*describe.C) f() type D struct{} @@ -126,7 +126,7 @@ binary - operation of constant value -2 -------- @describe map-lookup,ok -------- index expression of type (*int, bool) -no points-to information: can't locate SSA Value for expression in main.main +no points-to information: can't locate SSA Value for expression in describe.main -------- @describe mapval -------- reference to var mapval *int diff --git a/oracle/testdata/src/main/imports.golden b/oracle/testdata/src/main/imports.golden index 73f5f9df..b482fb4e 100644 --- a/oracle/testdata/src/main/imports.golden +++ b/oracle/testdata/src/main/imports.golden @@ -34,7 +34,7 @@ defined here reference to var p *int defined here value may point to these labels: - main.a + imports.a -------- @describe ref-pkg -------- reference to package "lib" diff --git a/oracle/testdata/src/main/peers.golden b/oracle/testdata/src/main/peers.golden index 81133848..3b5a3980 100644 --- a/oracle/testdata/src/main/peers.golden +++ b/oracle/testdata/src/main/peers.golden @@ -33,7 +33,7 @@ This channel of type chan *int may be: reference to var rA *int defined here value may point to these labels: - main.a2 + peers.a2 a1 -------- @peers peer-recv-chB -------- diff --git a/pointer/analysis.go b/pointer/analysis.go index dee75a82..36654f24 100644 --- a/pointer/analysis.go +++ b/pointer/analysis.go @@ -195,7 +195,7 @@ func Analyze(config *Config) CallGraphNode { work: makeMapWorklist(), } - if reflect := a.prog.PackagesByPath["reflect"]; reflect != nil { + if reflect := a.prog.ImportedPackage("reflect"); reflect != nil { a.reflectValueObj = reflect.Object.Scope().Lookup("Value") a.reflectRtypeObj = reflect.Object.Scope().Lookup("rtype") a.reflectRtype = types.NewPointer(a.reflectRtypeObj.Type()) diff --git a/pointer/pointer_test.go b/pointer/pointer_test.go index c05b504d..575f5456 100644 --- a/pointer/pointer_test.go +++ b/pointer/pointer_test.go @@ -11,7 +11,6 @@ package pointer_test import ( "bytes" "fmt" - "go/ast" "go/build" "go/parser" "go/token" @@ -171,21 +170,18 @@ func doOneInput(input, filename string) bool { if err != nil { // TODO(adonovan): err is a scanner error list; // display all errors not just first? - fmt.Println(err.Error()) + fmt.Println(err) return false } - // Type checking. - info, err := imp.CreateSourcePackage("main", []*ast.File{f}) - if err != nil { - fmt.Println(err.Error()) - return false - } + // Load main package and its dependencies. + info := imp.LoadMainPackage(f) // SSA creation + building. prog := ssa.NewProgram(imp.Fset, ssa.SanityCheckFunctions) - for _, info := range imp.Packages { - prog.CreatePackage(info) + if err := prog.CreatePackages(imp); err != nil { + fmt.Println(err) + return false } prog.BuildAll() @@ -522,7 +518,7 @@ func TestInput(t *testing.T) { wd, err := os.Getwd() if err != nil { - t.Errorf("os.Getwd: %s", err.Error()) + t.Errorf("os.Getwd: %s", err) return } @@ -535,7 +531,7 @@ func TestInput(t *testing.T) { for _, filename := range inputs { content, err := ioutil.ReadFile(filename) if err != nil { - t.Errorf("couldn't read file '%s': %s", filename, err.Error()) + t.Errorf("couldn't read file '%s': %s", filename, err) continue } diff --git a/ssa/builder.go b/ssa/builder.go index 06bac219..6400f6e9 100644 --- a/ssa/builder.go +++ b/ssa/builder.go @@ -2305,7 +2305,7 @@ func (b *builder) buildFuncDecl(pkg *Package, decl *ast.FuncDecl) { // func (prog *Program) BuildAll() { var wg sync.WaitGroup - for _, p := range prog.PackagesByPath { + for _, p := range prog.packages { if prog.mode&BuildSerially != 0 { p.Build() } else { diff --git a/ssa/builder_test.go b/ssa/builder_test.go index 72f58f30..8f5d6e7c 100644 --- a/ssa/builder_test.go +++ b/ssa/builder_test.go @@ -5,13 +5,13 @@ package ssa_test import ( - "code.google.com/p/go.tools/go/types" - "code.google.com/p/go.tools/importer" - "code.google.com/p/go.tools/ssa" - "go/ast" "go/parser" "strings" "testing" + + "code.google.com/p/go.tools/go/types" + "code.google.com/p/go.tools/importer" + "code.google.com/p/go.tools/ssa" ) func isEmpty(f *ssa.Function) bool { return f.Blocks == nil } @@ -38,7 +38,7 @@ func main() { w.Write(nil) // interface invoke of external declared method } ` - imp := importer.New(new(importer.Config)) // no Loader; uses GC importer + imp := importer.New(new(importer.Config)) // no go/build.Context; uses GC importer f, err := parser.ParseFile(imp.Fset, "", test, parser.DeclarationErrors) if err != nil { @@ -46,26 +46,24 @@ func main() { return } - info, err := imp.CreateSourcePackage("main", []*ast.File{f}) - if err != nil { - t.Error(err.Error()) - return - } + mainInfo := imp.LoadMainPackage(f) prog := ssa.NewProgram(imp.Fset, ssa.SanityCheckFunctions) - for _, info := range imp.Packages { - prog.CreatePackage(info) + if err := prog.CreatePackages(imp); err != nil { + t.Error(err) + return } - mainPkg := prog.Package(info.Pkg) + mainPkg := prog.Package(mainInfo.Pkg) mainPkg.Build() // Only the main package and its immediate dependencies are loaded. deps := []string{"bytes", "io", "testing"} - if len(prog.PackagesByPath) != 1+len(deps) { - t.Errorf("unexpected set of loaded packages: %q", prog.PackagesByPath) + all := prog.AllPackages() + if len(all) != 1+len(deps) { + t.Errorf("unexpected set of loaded packages: %q", all) } for _, path := range deps { - pkg, _ := prog.PackagesByPath[path] + pkg := prog.ImportedPackage(path) if pkg == nil { t.Errorf("package not loaded: %q", path) continue @@ -97,11 +95,6 @@ func main() { if !isExt { t.Fatalf("unexpected name type in main package: %s", mem) } - if _, ok := mem.Type().Underlying().(*types.Interface); ok { - // TODO(adonovan): workaround bug in types.MethodSet - // whereby mset(*I) is nonempty if I is an interface. - continue - } mset := types.NewPointer(mem.Type()).MethodSet() for i, n := 0, mset.Len(); i < n; i++ { m := prog.Method(mset.At(i)) diff --git a/ssa/create.go b/ssa/create.go index 9af3fe27..47e51160 100644 --- a/ssa/create.go +++ b/ssa/create.go @@ -12,6 +12,7 @@ import ( "go/ast" "go/token" "os" + "strings" "code.google.com/p/go.tools/go/types" "code.google.com/p/go.tools/importer" @@ -40,7 +41,7 @@ const ( func NewProgram(fset *token.FileSet, mode BuilderMode) *Program { prog := &Program{ Fset: fset, - PackagesByPath: make(map[string]*Package), + imported: make(map[string]*Package), packages: make(map[*types.Package]*Package), builtins: make(map[types.Object]*Builtin), boundMethodWrappers: make(map[*types.Func]*Function), @@ -246,7 +247,9 @@ func (prog *Program) CreatePackage(info *importer.PackageInfo) *Package { p.DumpTo(os.Stderr) } - prog.PackagesByPath[info.Pkg.Path()] = p + if info.Importable { + prog.imported[info.Pkg.Path()] = p + } prog.packages[p.Object] = p if prog.mode&SanityCheckFunctions != 0 { @@ -255,3 +258,48 @@ func (prog *Program) CreatePackage(info *importer.PackageInfo) *Package { return p } + +// CreatePackages creates SSA Packages for all error-free packages +// loaded by the specified Importer. +// +// If all packages were error-free, it is safe to call +// prog.BuildAll(), and nil is returned. Otherwise an error is +// returned. +// +func (prog *Program) CreatePackages(imp *importer.Importer) error { + var errpkgs []string + for _, info := range imp.AllPackages() { + if info.Err != nil { + errpkgs = append(errpkgs, info.Pkg.Path()) + } else { + prog.CreatePackage(info) + } + } + if errpkgs != nil { + return fmt.Errorf("couldn't create these SSA packages due to type errors: %s", + strings.Join(errpkgs, ", ")) + } + return nil +} + +// AllPackages returns a new slice containing all packages in the +// program prog in unspecified order. +// +func (prog *Program) AllPackages() []*Package { + pkgs := make([]*Package, 0, len(prog.packages)) + for _, pkg := range prog.packages { + pkgs = append(pkgs, pkg) + } + return pkgs +} + +// ImportedPackage returns the importable SSA Package whose import +// path is path, or nil if no such SSA package has been created. +// +// Not all packages are importable. For example, no import +// declaration can resolve to the x_test package created by 'go test' +// or the ad-hoc main package created 'go build foo.go'. +// +func (prog *Program) ImportedPackage(path string) *Package { + return prog.imported[path] +} diff --git a/ssa/example_test.go b/ssa/example_test.go index b915a366..bdf2b3df 100644 --- a/ssa/example_test.go +++ b/ssa/example_test.go @@ -6,7 +6,6 @@ package ssa_test import ( "fmt" - "go/ast" "go/build" "go/parser" "os" @@ -48,24 +47,21 @@ func main() { // Parse the input file. file, err := parser.ParseFile(imp.Fset, "hello.go", hello, parser.DeclarationErrors) if err != nil { - fmt.Print(err.Error()) // parse error + fmt.Print(err) // parse error return } // Create a "main" package containing one file. - info, err := imp.CreateSourcePackage("main", []*ast.File{file}) - if err != nil { - fmt.Print(err.Error()) // type error - return - } + mainInfo := imp.LoadMainPackage(file) // Create SSA-form program representation. var mode ssa.BuilderMode prog := ssa.NewProgram(imp.Fset, mode) - for _, info := range imp.Packages { - prog.CreatePackage(info) + if err := prog.CreatePackages(imp); err != nil { + fmt.Print(err) // type error in some package + return } - mainPkg := prog.Package(info.Pkg) + mainPkg := prog.Package(mainInfo.Pkg) // Print out the package. mainPkg.DumpTo(os.Stdout) diff --git a/ssa/interp/interp.go b/ssa/interp/interp.go index a74dd1e0..2f206e0c 100644 --- a/ssa/interp/interp.go +++ b/ssa/interp/interp.go @@ -1,3 +1,7 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + // Package ssa/interp defines an interpreter for the SSA // representation of Go programs. // @@ -390,10 +394,11 @@ func prepareCall(fr *frame, call *ssa.CallCommon) (fn value, args []value) { if recv.t == nil { panic("method invoked on nil interface") } - fn = lookupMethod(fr.i, recv.t, call.Method) - if fn == nil { + if f := lookupMethod(fr.i, recv.t, call.Method); f == nil { // Unreachable in well-typed programs. panic(fmt.Sprintf("method set for dynamic type %v does not contain %s", recv.t, call.Method)) + } else { + fn = f } args = append(args, copyVal(recv.v)) } @@ -545,7 +550,7 @@ func Interpret(mainpkg *ssa.Package, mode Mode, filename string, args []string) } initReflect(i) - for importPath, pkg := range i.prog.PackagesByPath { + for _, pkg := range i.prog.AllPackages() { // Initialize global storage. for _, m := range pkg.Members { switch v := m.(type) { @@ -556,7 +561,7 @@ func Interpret(mainpkg *ssa.Package, mode Mode, filename string, args []string) } // Ad-hoc initialization for magic system variables. - switch importPath { + switch pkg.Object.Path() { case "syscall": var envs []value for _, s := range os.Environ() { diff --git a/ssa/interp/interp_test.go b/ssa/interp/interp_test.go index 69250ef4..26619cfd 100644 --- a/ssa/interp/interp_test.go +++ b/ssa/interp/interp_test.go @@ -166,6 +166,7 @@ func run(t *testing.T, dir, input string) bool { } imp := importer.New(&importer.Config{Build: &build.Default}) + // TODO(adonovan): use LoadInitialPackages, then un-export ParseFiles. files, err := importer.ParseFiles(imp.Fset, ".", inputs...) if err != nil { t.Errorf("ssa.ParseFiles(%s) failed: %s", inputs, err.Error()) @@ -184,19 +185,16 @@ func run(t *testing.T, dir, input string) bool { }() hint = fmt.Sprintf("To dump SSA representation, run:\n%% go build code.google.com/p/go.tools/cmd/ssadump; ./ssadump -build=CFP %s\n", input) - info, err := imp.CreateSourcePackage("main", files) - if err != nil { - t.Errorf("importer.CreateSourcePackage(%s) failed: %s", inputs, err) - return false - } + mainInfo := imp.LoadMainPackage(files...) prog := ssa.NewProgram(imp.Fset, ssa.SanityCheckFunctions) - for _, info := range imp.Packages { - prog.CreatePackage(info) + if err := prog.CreatePackages(imp); err != nil { + t.Errorf("CreatePackages failed: %s", err) + return false } prog.BuildAll() - mainPkg := prog.Package(info.Pkg) + mainPkg := prog.Package(mainInfo.Pkg) mainPkg.CreateTestMainFunction() // (no-op if main already exists) hint = fmt.Sprintf("To trace execution, run:\n%% go build code.google.com/p/go.tools/cmd/ssadump; ./ssadump -build=C -run --interp=T %s\n", input) diff --git a/ssa/source_test.go b/ssa/source_test.go index 7dd52ccc..fc72290f 100644 --- a/ssa/source_test.go +++ b/ssa/source_test.go @@ -46,17 +46,14 @@ func TestObjValueLookup(t *testing.T) { } } - info, err := imp.CreateSourcePackage("main", []*ast.File{f}) - if err != nil { - t.Error(err.Error()) - return - } + mainInfo := imp.LoadMainPackage(f) prog := ssa.NewProgram(imp.Fset, 0 /*|ssa.LogFunctions*/) - for _, info := range imp.Packages { - prog.CreatePackage(info) + if err := prog.CreatePackages(imp); err != nil { + t.Error(err) + return } - mainPkg := prog.Package(info.Pkg) + mainPkg := prog.Package(mainInfo.Pkg) mainPkg.SetDebugMode(true) mainPkg.Build() @@ -66,7 +63,7 @@ func TestObjValueLookup(t *testing.T) { ast.Inspect(f, func(n ast.Node) bool { if id, ok := n.(*ast.Ident); ok { ids = append(ids, id) - if obj := info.ObjectOf(id); obj != nil { + if obj := mainInfo.ObjectOf(id); obj != nil { objs[obj] = true } } @@ -87,7 +84,7 @@ func TestObjValueLookup(t *testing.T) { // Check invariants for var objects. // The result varies based on the specific Ident. for _, id := range ids { - if obj, ok := info.ObjectOf(id).(*types.Var); ok { + if obj, ok := mainInfo.ObjectOf(id).(*types.Var); ok { ref, _ := importer.PathEnclosingInterval(f, id.Pos(), id.Pos()) pos := imp.Fset.Position(id.Pos()) exp := expectations[fmt.Sprintf("%s:%d", id.Name, pos.Line)] @@ -197,17 +194,14 @@ func TestValueForExpr(t *testing.T) { return } - info, err := imp.CreateSourcePackage("main", []*ast.File{f}) - if err != nil { - t.Error(err.Error()) - return - } + mainInfo := imp.LoadMainPackage(f) prog := ssa.NewProgram(imp.Fset, 0) - for _, info := range imp.Packages { - prog.CreatePackage(info) + if err := prog.CreatePackages(imp); err != nil { + t.Error(err) + return } - mainPkg := prog.Package(info.Pkg) + mainPkg := prog.Package(mainInfo.Pkg) mainPkg.SetDebugMode(true) mainPkg.Build() @@ -251,8 +245,8 @@ func TestValueForExpr(t *testing.T) { t.Errorf("%s: got value %q, want %q", position, got, want) } if v != nil { - if !types.IsIdentical(v.Type(), info.TypeOf(e)) { - t.Errorf("%s: got type %s, want %s", position, info.TypeOf(e), v.Type()) + if !types.IsIdentical(v.Type(), mainInfo.TypeOf(e)) { + t.Errorf("%s: got type %s, want %s", position, mainInfo.TypeOf(e), v.Type()) } } } diff --git a/ssa/ssa.go b/ssa/ssa.go index 25d22d7a..2610eadc 100644 --- a/ssa/ssa.go +++ b/ssa/ssa.go @@ -22,11 +22,11 @@ import ( // A Program is a partial or complete Go program converted to SSA form. // type Program struct { - Fset *token.FileSet // position information for the files of this Program - PackagesByPath map[string]*Package // all loaded Packages, keyed by import path - packages map[*types.Package]*Package // all loaded Packages, keyed by object - builtins map[types.Object]*Builtin // all built-in functions, keyed by typechecker objects. - mode BuilderMode // set of mode bits for SSA construction + Fset *token.FileSet // position information for the files of this Program + imported map[string]*Package // all importable Packages, keyed by import path + packages map[*types.Package]*Package // all loaded Packages, keyed by object + builtins map[types.Object]*Builtin // all built-in functions, keyed by typechecker objects. + mode BuilderMode // set of mode bits for SSA construction methodsMu sync.Mutex // guards the following maps: methodSets typemap.M // maps type to its concrete methodSet diff --git a/ssa/stdlib_test.go b/ssa/stdlib_test.go index 1d3fb0a8..bf36db86 100644 --- a/ssa/stdlib_test.go +++ b/ssa/stdlib_test.go @@ -55,13 +55,11 @@ func TestStdlib(t *testing.T) { // Load, parse and type-check the program. t0 := time.Now() - var hasErrors bool imp := importer.New(&impctx) - for _, importPath := range allPackages() { - if _, err := imp.LoadPackage(importPath); err != nil { - t.Errorf("LoadPackage(%s): %s", importPath, err) - hasErrors = true - } + + if _, _, err := imp.LoadInitialPackages(allPackages()); err != nil { + t.Errorf("LoadInitialPackages failed: %s", err) + return } t1 := time.Now() @@ -73,25 +71,26 @@ func TestStdlib(t *testing.T) { // Create SSA packages. prog := ssa.NewProgram(imp.Fset, ssa.SanityCheckFunctions) - for _, info := range imp.Packages { - if info.Err == nil { - prog.CreatePackage(info).SetDebugMode(debugMode) - } + if err := prog.CreatePackages(imp); err != nil { + t.Errorf("CreatePackages failed: %s", err) + return + } + // Enable debug mode globally. + for _, info := range imp.AllPackages() { + prog.Package(info.Pkg).SetDebugMode(debugMode) } t2 := time.Now() // Build SSA IR... if it's safe. - if !hasErrors { - prog.BuildAll() - } + prog.BuildAll() t3 := time.Now() runtime.GC() runtime.ReadMemStats(&memstats) - numPkgs := len(prog.PackagesByPath) + numPkgs := len(prog.AllPackages()) if want := 140; numPkgs < want { t.Errorf("Loaded only %d packages, want at least %d", numPkgs, want) } @@ -108,9 +107,7 @@ func TestStdlib(t *testing.T) { t.Log("GOMAXPROCS: ", runtime.GOMAXPROCS(0)) t.Log("Load/parse/typecheck: ", t1.Sub(t0)) t.Log("SSA create: ", t2.Sub(t1)) - if !hasErrors { - t.Log("SSA build: ", t3.Sub(t2)) - } + t.Log("SSA build: ", t3.Sub(t2)) // SSA stats: t.Log("#Packages: ", numPkgs) diff --git a/ssa/testmain.go b/ssa/testmain.go index 9b735dfc..e9ab5716 100644 --- a/ssa/testmain.go +++ b/ssa/testmain.go @@ -38,7 +38,7 @@ func (pkg *Package) CreateTestMainFunction() *Function { Pkg: pkg, } - testingPkg := pkg.Prog.PackagesByPath["testing"] + testingPkg := pkg.Prog.ImportedPackage("testing") if testingPkg == nil { // If it doesn't import "testing", it can't be a test. // TODO(adonovan): but it might contain Examples. diff --git a/ssa/visit.go b/ssa/visit.go index eccddcf5..3b74f6de 100644 --- a/ssa/visit.go +++ b/ssa/visit.go @@ -35,7 +35,7 @@ type visitor struct { } func (visit *visitor) program() { - for _, pkg := range visit.prog.PackagesByPath { + for _, pkg := range visit.prog.AllPackages() { for _, mem := range pkg.Members { switch mem := mem.(type) { case *Function: