// Query runs a single oracle query. // // args specify the main package in (*loader.Config).FromArgs syntax. // mode is the query mode ("callers", etc). // ptalog is the (optional) pointer-analysis log file. // buildContext is the go/build configuration for locating packages. // reflection determines whether to model reflection soundly (currently slow). // // Clients that intend to perform multiple queries against the same // analysis scope should use this pattern instead: // // conf := loader.Config{Build: buildContext} // ... populate config, e.g. conf.FromArgs(args) ... // iprog, err := conf.Load() // if err != nil { ... } // o, err := oracle.New(iprog, nil, false) // if err != nil { ... } // for ... { // qpos, err := oracle.ParseQueryPos(imp, pos, needExact) // if err != nil { ... } // // res, err := o.Query(mode, qpos) // if err != nil { ... } // // // use res // } // // TODO(adonovan): the ideal 'needsExact' parameter for ParseQueryPos // depends on the query mode; how should we expose this? // func Query(args []string, mode, pos string, ptalog io.Writer, buildContext *build.Context, reflection bool) (*Result, error) { if mode == "what" { // Bypass package loading, type checking, SSA construction. return what(pos, buildContext) } minfo := findMode(mode) if minfo == nil { return nil, fmt.Errorf("invalid mode type: %q", mode) } conf := loader.Config{Build: buildContext} // Determine initial packages. args, err := conf.FromArgs(args, true) if err != nil { return nil, err } if len(args) > 0 { return nil, fmt.Errorf("surplus arguments: %q", args) } // For queries needing only a single typed package, // reduce the analysis scope to that package. if minfo.needs&(needSSA|needRetainTypeInfo) == 0 { reduceScope(pos, &conf) } // TODO(adonovan): report type errors to the user via Serial // types, not stderr? // conf.TypeChecker.Error = func(err error) { // E := err.(types.Error) // fmt.Fprintf(os.Stderr, "%s: %s\n", E.Fset.Position(E.Pos), E.Msg) // } // Load/parse/type-check the program. iprog, err := conf.Load() if err != nil { return nil, err } o, err := newOracle(iprog, ptalog, minfo.needs, reflection) if err != nil { return nil, err } qpos, err := ParseQueryPos(iprog, pos, minfo.needs&needExactPos != 0) if err != nil && minfo.needs&(needPos|needExactPos) != 0 { return nil, err } // SSA is built and we have the QueryPos. // Release the other ASTs and type info to the GC. iprog = nil return o.query(minfo, qpos) }
// Run runs program analysis and computes the resulting markup, // populating *result in a thread-safe manner, first with type // information then later with pointer analysis information if // enabled by the pta flag. // func Run(pta bool, result *Result) { conf := loader.Config{ AllowErrors: true, } // Silence the default error handler. // Don't print all errors; we'll report just // one per errant package later. conf.TypeChecker.Error = func(e error) {} var roots, args []string // roots[i] ends with os.PathSeparator // Enumerate packages in $GOROOT. root := filepath.Join(runtime.GOROOT(), "src") + string(os.PathSeparator) roots = append(roots, root) args = allPackages(root) log.Printf("GOROOT=%s: %s\n", root, args) // Enumerate packages in $GOPATH. for i, dir := range filepath.SplitList(build.Default.GOPATH) { root := filepath.Join(dir, "src") + string(os.PathSeparator) roots = append(roots, root) pkgs := allPackages(root) log.Printf("GOPATH[%d]=%s: %s\n", i, root, pkgs) args = append(args, pkgs...) } // Uncomment to make startup quicker during debugging. //args = []string{"github.com/fzipp/pythia/internal/tools/cmd/godoc"} //args = []string{"fmt"} if _, err := conf.FromArgs(args, true); err != nil { // TODO(adonovan): degrade gracefully, not fail totally. // (The crippling case is a parse error in an external test file.) result.setStatusf("Analysis failed: %s.", err) // import error return } result.setStatusf("Loading and type-checking packages...") iprog, err := conf.Load() if iprog != nil { // Report only the first error of each package. for _, info := range iprog.AllPackages { for _, err := range info.Errors { fmt.Fprintln(os.Stderr, err) break } } log.Printf("Loaded %d packages.", len(iprog.AllPackages)) } if err != nil { result.setStatusf("Loading failed: %s.\n", err) return } // Create SSA-form program representation. // Only the transitively error-free packages are used. prog := ssa.Create(iprog, ssa.GlobalDebug) // Compute the set of main packages, including testmain. allPackages := prog.AllPackages() var mainPkgs []*ssa.Package if testmain := prog.CreateTestMainPackage(allPackages...); testmain != nil { mainPkgs = append(mainPkgs, testmain) if p := testmain.Const("packages"); p != nil { log.Printf("Tested packages: %v", exact.StringVal(p.Value.Value)) } } for _, pkg := range allPackages { if pkg.Object.Name() == "main" && pkg.Func("main") != nil { mainPkgs = append(mainPkgs, pkg) } } log.Print("Transitively error-free main packages: ", mainPkgs) // Build SSA code for bodies of all functions in the whole program. result.setStatusf("Constructing SSA form...") prog.BuildAll() log.Print("SSA construction complete") a := analysis{ result: result, prog: prog, pcgs: make(map[*ssa.Package]*packageCallGraph), } // Build a mapping from openable filenames to godoc file URLs, // i.e. "/src/" plus path relative to GOROOT/src or GOPATH[i]/src. a.path2url = make(map[string]string) for _, info := range iprog.AllPackages { nextfile: for _, f := range info.Files { if f.Pos() == 0 { continue // e.g. files generated by cgo } abs := iprog.Fset.File(f.Pos()).Name() // Find the root to which this file belongs. for _, root := range roots { rel := strings.TrimPrefix(abs, root) if len(rel) < len(abs) { a.path2url[abs] = "/src/" + filepath.ToSlash(rel) continue nextfile } } log.Printf("Can't locate file %s (package %q) beneath any root", abs, info.Pkg.Path()) } } // Add links for scanner, parser, type-checker errors. // TODO(adonovan): fix: these links can overlap with // identifier markup, causing the renderer to emit some // characters twice. errors := make(map[token.Position][]string) for _, info := range iprog.AllPackages { for _, err := range info.Errors { switch err := err.(type) { case types.Error: posn := a.prog.Fset.Position(err.Pos) errors[posn] = append(errors[posn], err.Msg) case scanner.ErrorList: for _, e := range err { errors[e.Pos] = append(errors[e.Pos], e.Msg) } default: log.Printf("Package %q has error (%T) without position: %v\n", info.Pkg.Path(), err, err) } } } for posn, errs := range errors { fi, offset := a.fileAndOffsetPosn(posn) fi.addLink(errorLink{ start: offset, msg: strings.Join(errs, "\n"), }) } // ---------- type-based analyses ---------- // Compute the all-pairs IMPLEMENTS relation. // Collect all named types, even local types // (which can have methods via promotion) // and the built-in "error". errorType := types.Universe.Lookup("error").Type().(*types.Named) a.allNamed = append(a.allNamed, errorType) for _, info := range iprog.AllPackages { for _, obj := range info.Defs { if obj, ok := obj.(*types.TypeName); ok { a.allNamed = append(a.allNamed, obj.Type().(*types.Named)) } } } log.Print("Computing implements relation...") facts := computeImplements(&a.prog.MethodSets, a.allNamed) // Add the type-based analysis results. log.Print("Extracting type info...") for _, info := range iprog.AllPackages { a.doTypeInfo(info, facts) } a.visitInstrs(pta) result.setStatusf("Type analysis complete.") if pta { a.pointer(mainPkgs) } }
func TestStdlib(t *testing.T) { // Load, parse and type-check the program. t0 := time.Now() alloc0 := bytesAllocated() // Load, parse and type-check the program. ctxt := build.Default // copy ctxt.GOPATH = "" // disable GOPATH conf := loader.Config{Build: &ctxt} if _, err := conf.FromArgs(buildutil.AllPackages(conf.Build), true); err != nil { t.Errorf("FromArgs failed: %v", err) return } iprog, err := conf.Load() if err != nil { t.Fatalf("Load failed: %v", err) } t1 := time.Now() alloc1 := bytesAllocated() // Create SSA packages. var mode ssa.BuilderMode // Comment out these lines during benchmarking. Approx SSA build costs are noted. mode |= ssa.SanityCheckFunctions // + 2% space, + 4% time mode |= ssa.GlobalDebug // +30% space, +18% time prog := ssa.Create(iprog, mode) t2 := time.Now() // Build SSA. prog.BuildAll() t3 := time.Now() alloc3 := bytesAllocated() numPkgs := len(prog.AllPackages()) if want := 140; numPkgs < want { t.Errorf("Loaded only %d packages, want at least %d", numPkgs, want) } // Keep iprog reachable until after we've measured memory usage. if len(iprog.AllPackages) == 0 { print() // unreachable } allFuncs := ssautil.AllFunctions(prog) // Check that all non-synthetic functions have distinct names. // Synthetic wrappers for exported methods should be distinct too, // except for unexported ones (explained at (*Function).RelString). byName := make(map[string]*ssa.Function) for fn := range allFuncs { if fn.Synthetic == "" || ast.IsExported(fn.Name()) { str := fn.String() prev := byName[str] byName[str] = fn if prev != nil { t.Errorf("%s: duplicate function named %s", prog.Fset.Position(fn.Pos()), str) t.Errorf("%s: (previously defined here)", prog.Fset.Position(prev.Pos())) } } } // Dump some statistics. var numInstrs int for fn := range allFuncs { for _, b := range fn.Blocks { numInstrs += len(b.Instrs) } } // determine line count var lineCount int prog.Fset.Iterate(func(f *token.File) bool { lineCount += f.LineCount() return true }) // NB: when benchmarking, don't forget to clear the debug + // sanity builder flags for better performance. t.Log("GOMAXPROCS: ", runtime.GOMAXPROCS(0)) t.Log("#Source lines: ", lineCount) t.Log("Load/parse/typecheck: ", t1.Sub(t0)) t.Log("SSA create: ", t2.Sub(t1)) t.Log("SSA build: ", t3.Sub(t2)) // SSA stats: t.Log("#Packages: ", numPkgs) t.Log("#Functions: ", len(allFuncs)) t.Log("#Instructions: ", numInstrs) t.Log("#MB AST+types: ", int64(alloc1-alloc0)/1e6) t.Log("#MB SSA: ", int64(alloc3-alloc1)/1e6) }
func TestStdlib(t *testing.T) { if !*runStdlibTest { t.Skip("skipping (slow) stdlib test (use --stdlib)") } // Load, parse and type-check the program. ctxt := build.Default // copy ctxt.GOPATH = "" // disable GOPATH conf := loader.Config{Build: &ctxt} if _, err := conf.FromArgs(buildutil.AllPackages(conf.Build), true); err != nil { t.Errorf("FromArgs failed: %v", err) return } iprog, err := conf.Load() if err != nil { t.Fatalf("Load failed: %v", err) } // Create SSA packages. prog := ssa.Create(iprog, 0) prog.BuildAll() numPkgs := len(prog.AllPackages()) if want := 240; numPkgs < want { t.Errorf("Loaded only %d packages, want at least %d", numPkgs, want) } // Determine the set of packages/tests to analyze. var testPkgs []*ssa.Package for _, info := range iprog.InitialPackages() { testPkgs = append(testPkgs, prog.Package(info.Pkg)) } testmain := prog.CreateTestMainPackage(testPkgs...) if testmain == nil { t.Fatal("analysis scope has tests") } // Run the analysis. config := &Config{ Reflection: false, // TODO(adonovan): fix remaining bug in rVCallConstraint, then enable. BuildCallGraph: true, Mains: []*ssa.Package{testmain}, } // TODO(adonovan): add some query values (affects track bits). t0 := time.Now() result, err := Analyze(config) if err != nil { t.Fatal(err) // internal error in pointer analysis } _ = result // TODO(adonovan): measure something t1 := time.Now() // Dump some statistics. allFuncs := ssautil.AllFunctions(prog) var numInstrs int for fn := range allFuncs { for _, b := range fn.Blocks { numInstrs += len(b.Instrs) } } // determine line count var lineCount int prog.Fset.Iterate(func(f *token.File) bool { lineCount += f.LineCount() return true }) t.Log("#Source lines: ", lineCount) t.Log("#Instructions: ", numInstrs) t.Log("Pointer analysis: ", t1.Sub(t0)) }
func run(t *testing.T, dir, input string, success successPredicate) bool { fmt.Printf("Input: %s\n", input) start := time.Now() var inputs []string for _, i := range strings.Split(input, " ") { if strings.HasSuffix(i, ".go") { i = dir + i } inputs = append(inputs, i) } var conf loader.Config if _, err := conf.FromArgs(inputs, true); err != nil { t.Errorf("FromArgs(%s) failed: %s", inputs, err) return false } conf.Import("runtime") // Print a helpful hint if we don't make it to the end. var hint string defer func() { if hint != "" { fmt.Println("FAIL") fmt.Println(hint) } else { fmt.Println("PASS") } interp.CapturedOutput = nil }() hint = fmt.Sprintf("To dump SSA representation, run:\n%% go build golang.org/x/tools/cmd/ssadump && ./ssadump -build=CFP %s\n", input) iprog, err := conf.Load() if err != nil { t.Errorf("conf.Load(%s) failed: %s", inputs, err) return false } prog := ssa.Create(iprog, ssa.SanityCheckFunctions) prog.BuildAll() var mainPkg *ssa.Package var initialPkgs []*ssa.Package for _, info := range iprog.InitialPackages() { if info.Pkg.Path() == "runtime" { continue // not an initial package } p := prog.Package(info.Pkg) initialPkgs = append(initialPkgs, p) if mainPkg == nil && p.Func("main") != nil { mainPkg = p } } if mainPkg == nil { testmainPkg := prog.CreateTestMainPackage(initialPkgs...) if testmainPkg == nil { t.Errorf("CreateTestMainPackage(%s) returned nil", mainPkg) return false } if testmainPkg.Func("main") == nil { t.Errorf("synthetic testmain package has no main") return false } mainPkg = testmainPkg } var out bytes.Buffer interp.CapturedOutput = &out hint = fmt.Sprintf("To trace execution, run:\n%% go build golang.org/x/tools/cmd/ssadump && ./ssadump -build=C -run --interp=T %s\n", input) exitCode := interp.Interpret(mainPkg, 0, &types.StdSizes{8, 8}, inputs[0], []string{}) // The definition of success varies with each file. if err := success(exitCode, out.String()); err != nil { t.Errorf("interp.Interpret(%s) failed: %s", inputs, err) return false } hint = "" // call off the hounds if false { fmt.Println(input, time.Since(start)) // test profiling } return true }