Index: cmd/sockdrawer/cluster.go =================================================================== new file mode 100644 --- /dev/null +++ b/cmd/sockdrawer/cluster.go @@ -0,0 +1,162 @@ +package main + +// This file defines the cluster graph. + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +type cluster struct { + id int + importPath string // declared name, e.g. "runtime/internal/core" + name string // short import name, e.g. "_core" + nodes map[*node]bool + scope map[string]*node // maps package-level names to decls + outputFiles map[string]*outputFile // output file data, keyed by file base name +} + +func (c *cluster) finish() { + // mark applies n's cluster to all nodes reachable from it that + // don't have a cluster assignment yet. + var mark func(n *node) + mark = func(n *node) { + for s := range n.succs { + if s.cluster == nil { + s.cluster = n.cluster + n.cluster.nodes[s] = true + if debug { + fmt.Printf("\t%-50s (indirect)\n", s) + } + mark(s) + } + } + } + + var first, prev *node + for n := range c.nodes { + if first == nil { + first = n + } + if prev != nil { + mark(n) + } + prev = n + } + if prev != nil { + mark(first) + } + + c.outputFiles = make(map[string]*outputFile) +} + +func loadClusterFile(filename string, nodes []*node) ([]*cluster, error) { + clusterNames := map[string]bool{"residue": true} + + byName := make(map[string]*node) + for _, n := range nodes { + byName[n.name] = n + } + + f, err := os.Open(*clusterFile) + if err != nil { + return nil, err + } + in := bufio.NewScanner(f) + var linenum int + var c *cluster + var clusters []*cluster + for in.Scan() { + linenum++ + line := strings.TrimSpace(in.Text()) + if i := strings.IndexByte(line, '#'); i>= 0 { + line = strings.TrimSpace(line[:i]) // strip comments + } + if line == "" { + continue // skip blanks + } + if strings.HasPrefix(line, "= ") { + if c != nil { + c.finish() + } + + c = &cluster{ + id: len(clusters), + importPath: line[2:], + nodes: make(map[*node]bool), + } + if clusterNames[c.importPath] { + fmt.Fprintf(os.Stderr, + "%s:%d: warning: duplicate cluster name: %s; ignoring\n", + *clusterFile, linenum, c.importPath) + continue + } + clusters = append(clusters, c) + if debug { + fmt.Printf("\n# cluster %s\n", c.importPath) + } + continue + } + if c == nil { + fmt.Fprintf(os.Stderr, + "%s:%d: warning: node before '= cluster' marker; ignoring\n", + *clusterFile, linenum) + continue + } + + n := byName[line] + if n == nil { + fmt.Fprintf(os.Stderr, + "%s:%d: warning: can't find node %q; ignoring\n", + *clusterFile, linenum, line) + } else if n.cluster != nil { + fmt.Fprintf(os.Stderr, + "%s:%d: warning: node %q appears in clusters %q and %q; ignoring\n", + *clusterFile, linenum, line, n.cluster.importPath, c.importPath) + } else { + n.cluster = c + if debug { + fmt.Printf("\t%s\n", n) + } + c.nodes[n] = true + } + } + if c != nil { + c.finish() + } + + f.Close() + if err := in.Err(); err != nil { + return nil, err + } + + return clusters, nil +} + +func addResidualCluster(nodes []*node, clusters []*cluster) []*cluster { + // The final cluster, residue, includes all other nodes. + c := &cluster{ + id: len(clusters), + importPath: "residue", + nodes: make(map[*node]bool), + } + if debug { + fmt.Printf("\n# cluster %s\n", c.importPath) + } + for _, n := range nodes { + if n.cluster == nil { + n.cluster = c + if debug { + fmt.Printf("\t%-50s\n", n) + } + c.nodes[n] = true + } + } + c.finish() + if len(c.nodes)> 0 { + clusters = append(clusters, c) + } + return clusters +} Index: cmd/sockdrawer/doc.go =================================================================== new file mode 100644 --- /dev/null +++ b/cmd/sockdrawer/doc.go @@ -0,0 +1,171 @@ +/* +The sockdrawer command is an analysis and visualization tool to help +you reorganize a complex Go package into several simpler ones. + +Overview + +sockdrawer operates on three kinds of graphs at different levels of +abstraction. The lowest level is the NODE GRAPH. A node is a +package-level declaration of a named entity (func, var, const or type). + +An entire constant declaration is treated as a single node, even if it +contains multiple "specs" each defining multiple names, since constants +so grouped are typically closely related; an important special case is +an enumerated set data type. Also, we treat each "spec" of a var or +type declaration as a single node. + + func f() // a func node + const ( a, b = 0, 1; c = 0 ) // a single const node + var ( + a, b = 0, 1 // a single var node + c = 0 // another var node + ) + type ( x int; y int ) // a single type node + +Each reference to a package-level entity E forms an edge in the node +graph, from the node in which it appears to the node E. For example: + + var x int + var y = x // edge y -> x + func f() int { return y } // edge f -> y + +Each method declaration depends on its receiver named type; in addition +we add an edge from each receiver type to its methods: + + type T int // edge T -> T.f + func (T) f() // edge T.f -> T + +to ensure that a type and its methods stay together. + +The node graph is highly cyclic, and obviously all nodes in a cycle must +belong to the same package for the package import graph to remain +acyclic. + +So, we compute the second graph, the SCNODE GRAPH. In essence, the +scnode graph is the graph of strongly connected components (SCCs) of the +(ordinary) node graph. By construction, the scnode graph is acyclic. + +We optionally perform an optimization at this point, which is to fuse +single-predecessor scnodes with their sole predecessor, as this tends to +reduce clutter in big graphs. This means that the scnodes are no longer +true SCCs; however, the scnode graph remains acyclic. + +We define a valid PARTITION P of the scnode graph as a mapping from +scnodes to CLUSTERS such that the projection of the scnode graph using +mapping P is an acyclic graph. This third graph is the CLUSTER GRAPH. + +Every partition represents a valid refactoring of the original package +into hypothetical subpackages, each cluster being a subpackage. Two +partitions define the extreme ends of a spectrum: the MINIMAL partition +maps every scnode to a single cluster; it represents the status quo, a +monolithic package. The MAXIMAL partition maps each scnode to a unique +cluster; this breaks the package up into an impractically large number +of small fragments. The ideal partition lies somewhere in between. + + +Clusters file + +The --clusters= argument specifies a CLUSTERS FILE that constrains +the partition algorithm. The file consists of a number of stanzas, each +assigning an import path to a cluster ("mypkg/internal/util") and +assigning a set of initial nodes ({x, y, z}) to it: + + = mypkg/internal/util + x + y # this is a comment + z + +Order of stanzas is important: clusters must be be declared bottom to +top. After each stanza, all nodes transitively reachable (via the node +graph) from that cluster are assigned to that cluster, if they have not +yet been assigned to some other cluster. Thus we need only mention the +root nodes of the cluster, not all its internal nodes. A warning is +reported if a node mentioned in a stanza already belongs to a previously +defined cluster. + +There is an implicit cluster, "residue", that holds all remaining nodes +after the clusters defined by the file have been processed. Initially, +when the clusters file is empty, the residue cluster contains the entire +package. (It is logically at the top.) The task for the user is to +iteratively define new clusters until the residue becomes empty. + + +Visualization + +When sockdrawer is run, it analyzes the source package, builds the node +graph and the scgraph, loads the clusters file, computes the clusters for +every node, and then emits SVG renderings of the three levels of graphs, +with nodes colors coded as follows: + + green = cluster (candidate subpackage) + pink = scnode (strong component of size> 1) + blue = node (func/type/var/const decl) + +The graphs of all clusters, a DAG, has green nodes; clicking one takes +you to the graph over scnodes for that cluster, also a DAG. Each pink +node in this graph represents a cyclical bunch of the node graph, +collapsed together for ease of viewing. Each blue node here represents a +singleton SCC, a single declaration; singular SCCs are replaced by +their sole element for simplicity. + +Clicking a pink (plural) scnode shows the cyclical portion of the node +graph that it represents. (If the fusion optimization was enabled, it +may not be fully cyclic.) All of its nodes are blue. + +Clicking a blue node shows the definition of that node in godoc. +(The godoc server's base URL is specified by the --godoc flag.) + + +Workflow + +Initially, all nodes belong to the "residue" cluster. (GraphViz graph +rendering can be slow for the first several iterations. A large monitor +is essential.) + +The sockdrawer user's task when decomposing a package into clusters is +to identify the lowest-hanging fruit (so to speak) in the residue +cluster---a coherent group of related scnodes at the bottom of the +graph---and to "snip off" a bunch at the "stem" by appending a new +stanza to the clusters file and listing the roots of that bunch in the +stanza, and then to re-run the tool. + + +Nodes may be added to an existing stanza if appropriate, but if they are +added to a cluster that is "too low", this may create conflicts; keep an +eye out for warnings. + +This process continues iteratively until the residue has become empty +and the sets of clusters are satisfactory. + +The tool prints the assignments of nodes to clusters: the "shopping +list" for the refactoring work. Clusters should be split off into +subpackages in dependency order, lowest first. + + +Caveats + +The analysis chooses a single configuration, such as linux/amd64. +Declarations for other configurations (e.g. windows/arm) will be absent +from the node graph. + +There may be some excessively large SCCs in the node graph that reflect +a circularity in the design. For the purposes of analysis, you can +break them arbitrarily by commenting out some code, though more thought +will be required for a principled fix (e.g. dependency injection). + + +TODO + +- Document the refactoring. +- Make pretty and stable names for anonymous nodes such as: + func init() {...} + var _ int = ... + Currently their names are very sensitive to lexical perturbations. +- Infer more constraints from co-located declarations. Most of the stuff + in the runtime's residue could be disposed of this way. +- Analyze the package's *_test.go files too. If they define an external + test package, we'll have to deal with two packages at once. +- Write tests. + +*/ +package main Index: cmd/sockdrawer/dot.go =================================================================== new file mode 100644 --- /dev/null +++ b/cmd/sockdrawer/dot.go @@ -0,0 +1,189 @@ +package main + +// This file emits renderings of all three levels of graphs as SVG files. + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func renderGraphs(clusters []*cluster, scgraph map[*scnode]bool) error { + fmt.Fprintln(os.Stderr, "Rendering graphs") + if err := os.MkdirAll(*graphdir, 0755); err != nil { + return err + } + + // Write the graph of clusters. + base := "clusters" + if err := writeClusters(base+".dot", clusters); err != nil { + return err + } + if err := runDot(base+".dot", base+".svg"); err != nil { + return err + } + fmt.Fprintf(os.Stderr, "\nRun:\n\t%% browser %s\n", + filepath.Join(*graphdir, base+".svg")) + + return nil +} + +// writeClusters writes to dotfile the graph (DAG) of clusters. +// It also generates all subgraphs. +func writeClusters(dotfile string, clusters []*cluster) (err error) { + f, err := os.Create(filepath.Join(*graphdir, dotfile)) + if err != nil { + return err + } + defer func() { + if closeErr := f.Close(); err == nil { + err = closeErr + } + }() + + fmt.Fprintln(f, "digraph clusters {") + fmt.Fprintln(f, ` node [shape="box",style="rounded,filled",fillcolor="#e0ffe0"];`) + fmt.Fprintln(f, ` edge [arrowhead="open"];`) + fmt.Fprintln(f, ` labelloc="t"; label="All clusters\n\n";`) + for _, c := range clusters { + base := fmt.Sprintf("cluster%d", c.id) + + // nodes + // NB: %q is not quite the graphviz quoting function. + fmt.Fprintf(f, " n%d [URL=%q,label=%q];\n", c.id, base+".svg", + strings.Replace(c.importPath, "/", "/\n", -1)) + + // Find scnodes of nodes of this cluster. + scnodes := make(map[*scnode]bool) + for n := range c.nodes { + scnodes[n.scc] = true + } + + // Project edges from SCC graph onto clusters. + succs := make(map[*cluster]bool) + for s := range scnodes { + for succ := range s.succs { + if succ.cluster != c { + succs[succ.cluster] = true + } + } + } + + // edges + for succ := range succs { + fmt.Fprintf(f, " n%d -> n%d;\n", c.id, succ.id) + } + + if err := writeSCCs(c.importPath, base+".dot", scnodes); err != nil { + return err + } + if err := runDot(base+".dot", base+".svg"); err != nil { + return err + } + } + fmt.Fprintln(f, "}") + return nil +} + +// writeSCCs writes to dotfile the graph (DAG) of SCCs for a single cluster. +// It also generates all subgraphs. +func writeSCCs(name, dotfile string, scgraph map[*scnode]bool) (err error) { + f, err := os.Create(filepath.Join(*graphdir, dotfile)) + if err != nil { + return err + } + defer func() { + if closeErr := f.Close(); err == nil { + err = closeErr + } + }() + + fmt.Fprintln(f, "digraph scgraph {") + fmt.Fprintln(f, ` graph [rankdir=LR];`) + fmt.Fprintln(f, ` edge [arrowhead="open"];`) + fmt.Fprintf(f, ` labelloc="t"; label="Cluster: %s\n\n";`, name) + fmt.Fprintln(f, ` node [shape="box",style=filled];`) + for s := range scgraph { + // nodes + var url, color string + if len(s.nodes) == 1 { + for n := range s.nodes { + url = n.godocURL() + } + color = "#f0e0ff" + } else { + base := fmt.Sprintf("scc%d", s.id) + if err := writeNodes(base+".dot", s.String(), s.nodes); err != nil { + return err + } + if err := runDot(base+".dot", base+".svg"); err != nil { + return err + } + + url = base + ".svg" + color = "#e0f0ff" + } + // NB: %q is not quite the graphviz quoting function. + fmt.Fprintf(f, " n%d [fillcolor=%q,URL=%q,label=%q];\n", s.id, color, url, s.String()) + + // intra-cluster edges + for succ := range s.succs { + if succ.cluster == s.cluster { + fmt.Fprintf(f, " n%d -> n%d;\n", s.id, succ.id) + } else { + // TODO(adonovan): show inter-cluster edges? + // Probably too much. + } + } + } + fmt.Fprintln(f, "}") + return nil +} + +// writeNodes writes to dotfile the graph (strongly connected) of nodes +// (package-level named entities) for a single non-trivial SCC. +func writeNodes(dotfile, name string, graph map[*node]bool) (err error) { + f, err := os.Create(filepath.Join(*graphdir, dotfile)) + if err != nil { + return err + } + defer func() { + if closeErr := f.Close(); err == nil { + err = closeErr + } + }() + + // TODO(adonovan): use hash-value numbering to merge nodes of + // equivalent topology (same set of succs/preds). + + fmt.Fprintln(f, "digraph scgraph {") + fmt.Fprintln(f, ` edge [arrowhead="open"];`) + fmt.Fprintf(f, ` labelloc="t"; label="Strongly connected component: %s\n\n";`, name) + fmt.Fprintln(f, ` node [shape="box",style=filled,fillcolor="#f0e0ff"];`) + + for n := range graph { + // nodes + // NB: %q is not quite the graphviz quoting function. + fmt.Fprintf(f, " n%d [URL=%q,label=%q];\n", n.id, n.godocURL(), n.String()) + + // TODO(adonovan): display two edges a-->b and b-->a as + // a single double-headed one. + + // SCC-internal edges (ignoring synthetic edges from annotations) + for succ, real := range n.succs { + if real && succ.scc.id == n.scc.id { + fmt.Fprintf(f, " n%d -> n%d;\n", n.id, succ.id) + } + } + } + fmt.Fprintln(f, "}") + return nil +} + +func runDot(dotfile, svgfile string) error { + cmd := exec.Command("/bin/sh", "-c", "/usr/bin/dot -Tsvg "+filepath.Join(*graphdir, dotfile)+">"+filepath.Join(*graphdir, svgfile)) + cmd.Stderr = os.Stderr + return cmd.Run() +} Index: cmd/sockdrawer/fmt.clusters =================================================================== new file mode 100644 --- /dev/null +++ b/cmd/sockdrawer/fmt.clusters @@ -0,0 +1,27 @@ + +# Cluster definitions for a hypothetical split of the fmt package. + += util +buffer +parsenum +func4ドル # init function + += print +Errorf +Print +Printf +Println +Sprint +Sprintf +Sprintln + += scan +Fscan +Fscanf +Fscanln +Sscan +Sscanf +Sscanln +Scan +Scanf +Scanln Index: cmd/sockdrawer/main.go =================================================================== new file mode 100644 --- /dev/null +++ b/cmd/sockdrawer/main.go @@ -0,0 +1,186 @@ +package main + +// This file defines the main control flow. + +/* + Usage examples: + + Display: + % ./sockdrawer -clusters golang.org/x/tools/cmd/sockdrawer/runtime.clusters \ + -godoc http://adonovan.nyc.corp:4999 -fuse -graphdir=out runtime + + Refactor: + % ./sockdrawer -clusters golang.org/x/tools/cmd/sockdrawer/runtime.clusters \ + -outdir=/tmp/src runtime + % find /tmp/src/ -name \*.go -exec sed -i -e 's?//go:.*?//go-redacted?' {} \; + % GOPATH=/tmp command go build -gcflags "-e" residue + +*/ + +// TODO(adonovan): lots on the refactoring side; see refactor.go. + +import ( + "flag" + "fmt" + "go/parser" + "go/token" + "os" + "path/filepath" + "sort" + + "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/types" +) + +const debug = false + +var ( + clusterFile = flag.String("clusters", "", "File containing cluster annotations") + print = flag.Bool("print", false, "Print the partition to stdout") + outdir = flag.String("outdir", "", "enable package splitting, using this output directory") + graphdir = flag.String("graphdir", "", "enable graph rendering, using this output directory") + fuse = flag.Bool("fuse", false, "fuse each single-predecessor SCC with its sole predecessor; this reduces the complexity of the output graphs") + godoc = flag.String("godoc", "http://localhost:4999", "base URL for godoc server") +) + +const Usage = `Usage: sockdrawer -clusters=file [flags...] + +sockdrawer is a tool for splitting a package into two or more subpackages. + +Partition flags: + -clusters=file Load the cluster definitions from the specified file. + +Display flags: + -print Print the partition in text form to the standard output. + -graphdir=dir Render graphs of the proposed split to this directory. + -godoc=url In rendered graphs, emit links to godoc at this address. + -fuse Display each single-predecessor SCC fused to its sole predecessor. + +Refactoring flags: + -outdir=dir Split the package into subpackages, writing them here. +` + loader.FromArgsUsage + +func main() { + flag.Parse() + args := flag.Args() + if err := doMain(args); err != nil { + fmt.Fprintf(os.Stderr, "sockdrawer: %s\n", err) + os.Exit(1) + } +} + +func doMain(args []string) error { + conf := loader.Config{ + SourceImports: true, + ParserMode: parser.ParseComments, + } + + if len(args) == 0 { + fmt.Fprintln(os.Stderr, Usage) + return nil + } + + // Use the initial packages from the command line. + // TODO(adonovan): support *_test.go files too. + _, err := conf.FromArgs(args, false /*FIXME*/) + if err != nil { + return err + } + + // Typecheck only the necessary function bodies. + // TODO(adonovan): opt: type-check only the bodies of functions + // with the initial packages. + conf.TypeCheckFuncBodies = func(p string) bool { return true } + + // Load, parse and type-check the whole program. + iprog, err := conf.Load() + if err != nil { + return err + } + + // TODO(adonovan): fix: generalize to multiple packages, or at least, + // one package plus its external test package. + info := iprog.InitialPackages()[0] + return sockdrawer(conf.Fset, info) +} + +type organizer struct { + fset *token.FileSet + info *loader.PackageInfo + nodes []*node // nodes for top-level decls/specs, in lexical order + nodesByObj map[types.Object]*node +} + +func sockdrawer(fset *token.FileSet, info *loader.PackageInfo) error { + o := organizer{ + fset: fset, + info: info, + nodesByObj: make(map[types.Object]*node), + } + + // Using the AST and Ident-to-Object mapping, + // build the dependency graph over package-level nodes. + o.buildNodeGraph() + + // Load the clusters file, if any, + // and compute the implied partition. + var clusters []*cluster // topological order + if f := *clusterFile; f != "" { + var err error + if clusters, err = loadClusterFile(f, o.nodes); err != nil { + return err + } + } + clusters = addResidualCluster(o.nodes, clusters) + + // Print the partition? + if *print { + // Use the same format as the clusters file. + fmt.Printf("# Package: %q\n", info.Pkg.Path()) + fmt.Printf("# Initial cluster file: %q\n", *clusterFile) + fmt.Printf("# %d nodes in %d clusters\n", len(o.nodes), len(clusters)) + fmt.Println() + + for _, c := range clusters { + var ss []string + for n := range c.nodes { + posn := n.o.fset.Position(n.syntax.Pos()) + base := filepath.Base(posn.Filename) + // Comment out concrete method nodes since they can't be + // specified in cluster file syntax. + // (They're tied to their receiver type's cluster anyway.) + var comment string + if n.recv != nil { + comment = "# " + } + ss = append(ss, fmt.Sprintf("%s%-40s# %s:%d", comment, n.name, base, posn.Line)) + } + sort.Strings(ss) + fmt.Printf("= %s\n", c.importPath) + for _, s := range ss { + fmt.Println(s) + } + fmt.Println() + } + } + + // Display partition graphically? + if *graphdir != "" { + // Compute the strong component graph to + // simplify the displayed output. + scgraph := o.makeSCGraph(*fuse) + + if err := renderGraphs(clusters, scgraph); err != nil { + return err + } + } + + // Do the refactoring? + if *outdir != "" { + if err := o.refactor(clusters); err != nil { + return err + } + } + + return nil +} Index: cmd/sockdrawer/nodegraph.go =================================================================== new file mode 100644 --- /dev/null +++ b/cmd/sockdrawer/nodegraph.go @@ -0,0 +1,303 @@ +package main + +// This file defines node and constructs the node graph. + +import ( + "bytes" + "fmt" + "go/ast" + "go/token" + "os" + "path/filepath" + "reflect" + "strings" + + "golang.org/x/tools/go/types" +) + +// A node represents a top-level declaration (including methods). +// An entire const declaration is a single node. +// An entire var or type "spec" is a single node. +// +// Examples: +// func f() // FuncDecl node +// func (T) f() {...} // FuncDecl node (method) +// func init() {...} // FuncDecl node (no types.Object) +// type ( +// T int // TypeSpec node +// U int // TypeSpec node +// ) +// type T int // TypeDecl node +// const ( a, b = 0, 1; c = 0 ) // GenDecl(CONST) node (multiple objects) +// var x = f() // GenDecl(VAR) node +// var x, y = f() // GenDecl(VAR) node (multiple objects) +// var _ T = C(0) // GenDecl(VAR) node (no object) +// +type node struct { + o *organizer + id int // zero-based ordinal, lexical order + name string // unique name, as used in clusters file + syntax ast.Node // ast.Decl, or ast.Spec if var/type in group + uses map[*ast.Ident]types.Object // uses of pkg- and file-scope objects + objects []types.Object // declared objects in lexical order; blanks omitted + recv types.Type // receiver type, iff concrete method decl + succs, preds map[*node]bool // node graph adjacency sets + scc *scnode // SCC to which this node belongs + cluster *cluster // cluster to which this node belongs + + // renaming state: + mustExport bool // node must be exported to other clusters + imports map[interface{}]bool // existing (*PkgName) and new (*cluster) dependencies + text []byte // text, after renaming +} + +func (n *node) String() string { + var buf bytes.Buffer + buf.WriteString(n.name) + if nobj := len(n.objects); nobj> 1 { + fmt.Fprintf(&buf, " + %d", nobj-1) + } + return buf.String() +} + +func (n *node) godocURL() string { + posn := n.o.fset.Position(n.syntax.Pos()) + i := strings.Index(posn.Filename, "/src/") // TODO(adonovan): fix hack + + selLen := 1 + switch syntax := n.syntax.(type) { + case *ast.FuncDecl: + selLen = len("func") + case *ast.GenDecl: + switch syntax.Tok { + case token.CONST: + selLen = len("const") + case token.VAR: + selLen = len("var") + case token.TYPE: + selLen = len("type") + } + case *ast.TypeSpec: + // For "type (...; x T; ...)", select "x". + selLen = len(syntax.Name.Name) + case *ast.ValueSpec: + // For "var (...; x, y = ...)", select "x, y". + selLen = int(syntax.Names[len(syntax.Names)-1].End() - syntax.Names[0].Pos()) + } + return fmt.Sprintf("%s/%s?s=%d:%d#L%d", *godoc, + posn.Filename[i+1:], posn.Offset, posn.Offset+selLen, posn.Line) +} + +func (n *node) exportedness() int { + for _, obj := range n.objects { + if obj.Exported() { + return 1 + } + } + return 0 +} + +func addEdge(from, to *node) { + if from == to { + return // skip self-edges + } + from.succs[to] = true + to.preds[from] = true +} + +func (o *organizer) buildNodeGraph() { + if debug { + fmt.Fprintf(os.Stderr, "\n\n\n==== %s ====\n\n\n", o.info.Pkg.Path()) + } + + // -- Pass 1: Defs ---------------------------------------------------- + + for _, f := range o.info.Files { + // These two vars are used for generation symbol names: + // e.g. "func$alg.3", for the third init function in runtime/alg.go + base := strings.TrimSuffix(filepath.Base(o.fset.Position(f.Pos()).Filename), ".go") + var seq int + + forEachDecl(f, func(syntax ast.Node, parent *ast.GenDecl) { + n := &node{ + o: o, + id: len(o.nodes), + syntax: syntax, + uses: make(map[*ast.Ident]types.Object), + succs: make(map[*node]bool), + preds: make(map[*node]bool), + } + + // Visit the top-level AST, associating with n + // every object declared within it that could + // possibly be references outside it, including: + // - package-level objects (const/func/var/type) + // - concrete methods + // - struct fields (consider y in "var x struct{y int}") + // - abstract methods (consider y in "var x interface{y()}") + ast.Inspect(syntax, func(syntax ast.Node) bool { + if id, ok := syntax.(*ast.Ident); ok { + // Definition of package-level object, + // or struct field or interface method? + if obj := o.info.Info.Defs[id]; obj != nil { + if isPackageLevel(obj) { + // package-level object + n.objects = append(n.objects, obj) + } else if v, ok := obj.(*types.Var); ok && v.IsField() { + // struct field + } else if _, ok := obj.(*types.Func); ok { + // method or init function + recv := methodRecv(obj) + if recv != nil && !isInterface(methodRecv(obj)) { + // concrete method + n.recv = recv + n.objects = append(n.objects, obj) + } + } else { + return true // ignore + } + o.nodesByObj[obj] = n + } + } + return true + }) + + // Name the node. + if n.objects != nil { + // Only the first object (in lexical order) of a group + // (e.g. a const decl) is used for the node label. + n.name = n.objects[0].Name() + + // concrete method decl? + if n.recv != nil { + n.name = fmt.Sprintf("(%s).%s", + types.TypeString(o.info.Pkg, n.recv), n.name) + } + } else { + // e.g. blank identifier, or func init. + seq++ + n.name = defaultName(syntax, base, seq) + } + + o.nodes = append(o.nodes, n) + }) + } + + // -- Pass 2: Refs ---------------------------------------------------- + + // Gather references from this syntax tree to other + // top-level trees, and create graph edges for them. + // (Also gather refs to existing import names in 'uses'.) + for _, n := range o.nodes { + ast.Inspect(n.syntax, func(syntax ast.Node) bool { + if id, ok := syntax.(*ast.Ident); ok { + if obj, ok := o.info.Info.Uses[id]; ok { + if n2, ok := o.nodesByObj[obj]; ok { + addEdge(n, n2) + n.uses[id] = obj + } else if _, ok := obj.(*types.PkgName); ok { + n.uses[id] = obj + } + } + } + return true + }) + + // To ensure methods and receiver types stay together, + // we add edges to each method from its receiver type. + if n.recv != nil { + addEdge(o.nodesByObj[recvTypeName(n.recv)], n) + } + } + + if debug { + fmt.Fprintf(os.Stderr, "\t%d nodes\n", len(o.nodes)) + } +} + +// -- util ------------------------------------------------------------- + +// defaultName invents a reasonably stable temporary name for syntax +// based on its kind and sequence number within its file. +func defaultName(syntax ast.Node, base string, seq int) string { + // No object: func init, or blank identifier. + var kind string + switch syntax := syntax.(type) { + case *ast.FuncDecl: + // e.g. func init() + kind = "func" + case *ast.ValueSpec: + // e.g. var ( _ int ) + kind = "var" + case *ast.GenDecl: + switch syntax.Tok { + case token.CONST: + kind = "const" // e.g. const _ int + case token.VAR: + kind = "var" // e.g. var _ int + case token.TYPE: + kind = "type" // e.g. type _ int + } + default: + // can't happen? + kind = reflect.TypeOf(syntax).String() + } + return fmt.Sprintf("%s$%s.%d", kind, base, seq) +} + +// forEachDecl calls fn for each syntax tree (decl or spec) in the file +// that should have its own node. If syntax is a VarSpec or TypeSpec in +// a group, parent is the enclosing decl. +func forEachDecl(file *ast.File, fn func(syntax ast.Node, parent *ast.GenDecl)) { + for _, decl := range file.Decls { + switch decl := decl.(type) { + case *ast.GenDecl: + switch decl.Tok { + case token.CONST: + // treat decl as one node + fn(decl, nil) + + case token.VAR, token.TYPE: + if decl.Lparen != 0 { + // group decl: each spec gets its own node + for _, spec := range decl.Specs { + fn(spec, decl) + } + } else { + // singleton: one node for entire decl + fn(decl, nil) + } + } + + case *ast.FuncDecl: + // funcs (but not methods) get their own node + fn(decl, nil) + } + } +} + +func recvTypeName(T types.Type) *types.TypeName { + if ptr, ok := T.(*types.Pointer); ok { + T = ptr.Elem() + } + return T.(*types.Named).Obj() +} + +// methodRecv returns the receiver type of obj, +// if it's a method, or nil otherwise. +// TODO(adonovan): move this to go/types. It gets re-invented a lot. +func methodRecv(obj types.Object) types.Type { + if obj, ok := obj.(*types.Func); ok { + recv := obj.Type().(*types.Signature).Recv() + if recv != nil { + return recv.Type() + } + } + return nil +} + +// isInterface reports whether T's underlying type is an interface. +func isInterface(T types.Type) bool { + _, ok := T.Underlying().(*types.Interface) + return ok +} Index: cmd/sockdrawer/refactor.go =================================================================== new file mode 100644 --- /dev/null +++ b/cmd/sockdrawer/refactor.go @@ -0,0 +1,465 @@ +package main + +// This file defines the refactoring. + +// TODO(adonovan): fix: +// - exported API functions may be moved into internal subpackages, +// making them invisible. We'll need shims/delegates for func and const. +// Types and vars are trickier. +// - use nice import names (e.g. core not _core) when it would be unambiguous to do so. +// - preserve comments before/in import decls. +// - look at files for non-linux/amd64 platforms +// - deal with assembly, compiler entrypoints +// - check for all conflicts: struct fields, concrete methods, interface methods. +// - check for definition conflicts at file scope +// - check for field definition conflicts +// - check for (abstract and concrete) method definition conflicts +// - check for renamed package-level types used as embedded fields, etc. +// - check for reference conflicts (hard) +// - emit 'git mv' commands so that new files are treated as moves, not adds. +// - struct literals T{1,2} may need field names T{X:1, Y:2}. + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io/ioutil" + "os" + "path" + "path/filepath" + "sort" + "strings" + "unicode" + "unicode/utf8" + + "golang.org/x/tools/go/types" +) + +func (o *organizer) refactor(clusters []*cluster) error { + // new names for objects that must become exported + exportNames := make(map[types.Object]string) + export := func(obj types.Object) { + if !ast.IsExported(obj.Name()) { + if _, ok := exportNames[obj]; !ok { + exportNames[obj] = exportedName(obj.Name()) + } + } + } + + // Find objects requiring a name change for export: + // the heads of node-graph edges that span clusters. + for _, n := range o.nodes { + for succ := range n.succs { + if n.cluster != succ.cluster { + if !succ.mustExport { + succ.mustExport = true + for _, obj := range succ.objects { + export(obj) + } + } + } + } + } + + // Fix up package-level definition conflicts in each cluster. + for _, c := range clusters { + // For now, all import names will be "_" + the last segment. + // TODO(adonovan): avoid _ when not needed and make sure + // the last segment is a valid identifier. + // Alternatively, apply gorename on a file-by-file basis + // to eliminate the underscores. + + c.name = "_" + path.Base(c.importPath) // (default) + c.scope = make(map[string]*node) + for n := range c.nodes { + for _, obj := range n.objects { + if !isPackageLevel(obj) { + continue + } + // NB: only exported symbols may conflict. + // That may change when we deal with imports. + name := obj.Name() + if new, ok := exportNames[obj]; ok { + name = new + } + if prev := c.scope[name]; prev != nil { + fmt.Fprintf(os.Stderr, "%s: warning: exporting %s\n", + o.fset.Position(n.syntax.Pos()), + obj.Name()) + fmt.Fprintf(os.Stderr, "%s: \twould conflict with %s; adding 'X' prefix.\n", + o.fset.Position(prev.syntax.Pos()), name) + + // TODO(adonovan): fix: use a unique prefix + // that never appears in the package! + name = "X" + name + exportNames[obj] = name + } + c.scope[name] = n + } + } + } + + // Mark selectables (fields and methods) for export if they + // are ever referenced from outside their defining package. + // TODO(adonovan): fix: must compute consequences (a la gorename). + for _, n := range o.nodes { + for _, obj := range n.uses { + if v, ok := obj.(*types.Var); ok && v.IsField() { + // field + } else if f, ok := obj.(*types.Func); ok && methodRecv(f) != nil { + // method + } else { + continue + } + // obj is a field or method + + // inter-cluster reference? + if o.nodesByObj[obj].cluster != n.cluster { + export(obj) + } + } + } + + // Inspect referring identifiers within each node. + // Compute import dependencies (existing and new packages). + // Qualify inter-cluster references with the new package name. + for _, n := range o.nodes { + for id, obj := range n.uses { + // existing import dependency? + if pkgName, ok := obj.(*types.PkgName); ok { + n.addImport(pkgName) + continue + } + + name := id.Name + if new, ok := exportNames[obj]; ok { + name = new + } + + // Cross-package reference to package-level entity? + // + // TODO(adonovan): fix: check the lexical + // structure to see if the name is free. If + // not, uniquify n2.cluster.name. For now, + // globally qualify; later, uniquify it only as + // needed on a per-cluster basis. + if isPackageLevel(obj) { + n2 := o.nodesByObj[obj] + if n2.cluster != n.cluster { + // qualify the identifier + name = n2.cluster.name + "." + name + n.addImport(n2.cluster) + + } + } + + id.Name = name + } + } + + // Modify defining identifiers for exported objects. + for id, obj := range o.info.Defs { + if new, ok := exportNames[obj]; ok { + id.Name = new + } + } + + // Split the source files into files in subpackages. + if err := o.split(); err != nil { + return err + } + + // Now write the clusters out: + var failed bool + fmt.Fprintf(os.Stderr, "Writing refactored output...\n") + for _, c := range clusters { + dir := filepath.Join(*outdir, c.importPath) + fmt.Fprintf(os.Stderr, "\t%s", dir) + if err := os.MkdirAll(dir, 0755); err != nil { + fmt.Fprintf(os.Stderr, ": %v", err) + failed = true + } else { + // Create an empty .s file in each new package; + // this causes gc to suppress "missing function + // body" errors until link time. + ioutil.WriteFile(filepath.Join(dir, "dummy.s"), nil, 0666) + + for base, out := range c.outputFiles { + filename := filepath.Join(dir, base) + if err := out.writeFile(filename); err != nil { + fmt.Fprintf(os.Stderr, ": %v", err) + failed = true + } + } + } + fmt.Fprintln(os.Stderr) + } + if failed { + return fmt.Errorf("there were I/O errors") + } + return nil +} + +// split writes the (modified) AST for each node to the output file to +// which it belongs, in lexical order. +// +func (o *organizer) split() error { + // TODO(adonovan): fix: look at other uses too: references to + // interface methods and struct fields. + + // Now we pretty-print the modified syntax trees, split the text + // into node-sized chunks (along with preceding + // whitespace/comments), and append each chunk to the relevant + // (split) files belonging to each cluster. + // + // To back, split the text into node-sized chunks and attach the + // text of each one to the appropriate node's text. + // + // We do this one file at a time, splitting the pretty text into + // declarations, with order determined by forEachDecl again, for + // consistency. This way each decl corresponds to o.nodes[i]. + // + var i int // node index + for _, f := range o.info.Files { + filename := o.fset.Position(f.Pos()).Filename + filebase := filepath.Base(filename) + + // Print each file and parse it back. + var buf bytes.Buffer + if err := format.Node(&buf, o.fset, f); err != nil { + return fmt.Errorf("pretty-printing %s failed: %v", filename, err) + } + + fset2 := token.NewFileSet() + f2, err := parser.ParseFile(fset2, filename, &buf, parser.ParseComments) + if err != nil { + return fmt.Errorf("parsing of pretty-printed %s failed: %v", filename, err) + } + text := buf.Bytes() + + // All text operations are newline-terminated. + + // Record the initial comment that runs from the start + // of the file up (but not including) the package decl. + // Each output file will get a copy of it, plus a + // package decl appropriate to its cluster. + initialComment := text[:int(f2.Package)-fset2.File(f2.Pos()).Base()] + + // Skip to beyond the import block. + // + // TODO(adonovan): fix: don't discard comments between + // the package decl and the import decl. (Fortunately + // "runtime" uses few imports.) + pos := f2.Name.End() // after package decl + for _, decl := range f2.Decls { + if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.IMPORT { + pos = decl.End() + } + } + offset := fset2.Position(pos).Offset // offset of end of previous decl + offset = withNewline(text, offset) + + var enterGroupText []byte // current group's opening whitespace and "var (" + + // Map parsed pretty decls back to their corresponding nodes. + forEachDecl(f2, func(syntax ast.Node, parent *ast.GenDecl) { + // Find node and cluster corresponding to syntax. + // (Careful: methods have no node of their own, + // so we can't use o.nodes[i].) + n := o.nodes[i] + i++ + out := n.cluster.file(filebase) + out.addImportsFor(n) + + // first time writing to this file? + if out.head.Len() == 0 { + out.head.Write(initialComment) + // TODO(adonovan): fix: think about the + // leading \n. Is it sound w.r.t. both + // package documentation (which doesn't + // want it) and +build comments (which + // need it)? + fmt.Fprintf(&out.head, "package %s\n\n", + path.Base(n.cluster.importPath)) + } + + // Handle transitions into/out of group decls: + // var(...), type(...). + if parent == nil { + // syntax is a complete decl + + // leaving previous group + if out.groupDecl != nil { + out.body.WriteString(")\n") + out.groupDecl = nil + } + } else { + // syntax is one var or type spec in a group decl + + // first spec of group? + if syntax == parent.Specs[0] { + // save preceding whitespace and "var (" + lparen := fset2.Position(parent.Lparen).Offset + lparen = withNewline(text, lparen) + enterGroupText = text[offset:lparen] + offset = lparen + } + + // has group changed? + if parent != out.groupDecl { + // leave previous group + if out.groupDecl != nil { + out.body.WriteString(")\n") + } + + // enter new group + out.body.Write(enterGroupText) + out.groupDecl = parent + } + } + // The final implicit "leaving group" transition for + // each file is handled by (*cluster).writeFile. + + // TODO(adonovan): fix: don't discard comments + // at the end of each file; copy them to all + // output files. + + // Emit node syntax. + // Emit in all text since the end of the last decl. + end := fset2.Position(syntax.End()).Offset + end = withNewline(text, end) + out.body.Write(text[offset:end]) + offset = end + + // last spec of group? + if parent != nil && syntax == parent.Specs[len(parent.Specs)-1] { + // consume input up to ')' + rparen := fset2.Position(parent.Rparen).Offset + rparen = withNewline(text, rparen) + offset = rparen + } + }) + } + if i != len(o.nodes) { + panic("internal error") + } + return nil +} + +func withNewline(data []byte, i int) int { + for ; i < len(data); i++ { + if data[i] == '\n' { + return i + 1 + } + } + return i +} + +func (n *node) addImport(imp interface{}) { + if n.imports == nil { + n.imports = make(map[interface{}]bool) + } + n.imports[imp] = true +} + +// outputFile holds state for each output file. +type outputFile struct { + head, body bytes.Buffer // head is package decl + cluster imports + imports map[interface{}]bool // union of node.imports + groupDecl ast.Decl // previous group decl, if any +} + +func (out *outputFile) addImportsFor(n *node) { + if out.imports == nil { + out.imports = make(map[interface{}]bool) + } + for imp := range n.imports { + out.imports[imp] = true + } +} + +func (c *cluster) file(base string) *outputFile { + f := c.outputFiles[base] + if f == nil { + f = new(outputFile) + c.outputFiles[base] = f + } + return f +} + +// writeFile writes the outputFile data to the specified file. +func (out *outputFile) writeFile(filename string) error { + // Add necessary imports to head. + if len(out.imports)> 0 { + var importLines []string + for imp := range out.imports { + var name, importPath string + switch imp := imp.(type) { + case *types.PkgName: + name = imp.Name() + importPath = imp.Imported().Path() + case *cluster: + name = imp.name + importPath = imp.importPath + } + var spec string + if name == path.Base(importPath) { + spec = fmt.Sprintf("\t%q\n", importPath) + } else { + spec = fmt.Sprintf("\t%s %q\n", name, importPath) + } + importLines = append(importLines, spec) + } + sort.Strings(importLines) + fmt.Fprintf(&out.head, "import (\n") + for _, imp := range importLines { + out.head.WriteString(imp) + } + fmt.Fprintf(&out.head, ")\n") + } + + // Implement final state transition. + if out.groupDecl != nil { + // leaving var or type(...) decl + out.body.WriteString(")\n") + } + + // Write formatted head and data to filename. + out.head.Write(out.body.Bytes()) + data := out.head.Bytes() + + // Run it through gofmt. + data, err := format.Source(data) + if err != nil { + return fmt.Errorf("failed to gofmt %s: %v", filename, err) + } + + return ioutil.WriteFile(filename, data, 0666) +} + +// exportName returns the corresponding exported name for a non-exported identifier. +func exportedName(name string) string { + // Underscores are used to avoid conflicts with keywords + // (e.g. _func) or built-in identifiers (e.g. _string), + // or to suppress export of uppercase names (e.g. _ESRCH). + // Strip them off. + name = strings.TrimLeft(name, "_") + + r, size := utf8.DecodeRuneInString(name) + name = string(unicode.ToUpper(r)) + name[size:] // "foo" -> "Foo" + + if !unicode.IsLetter(r) { + name = "X" + name // e.g. "_64bit" -> "X64bit" + // TODO(adonovan): fix: result may yet conflict. + } + return name +} + +// -- from refactor/rename -- + +func isPackageLevel(obj types.Object) bool { + return obj.Pkg().Scope().Lookup(obj.Name()) == obj +} Index: cmd/sockdrawer/runtime.clusters =================================================================== new file mode 100644 --- /dev/null +++ b/cmd/sockdrawer/runtime.clusters @@ -0,0 +1,264 @@ + +# Cluster definitions for a hypothetical split of the "runtime" package. + +# The core cluster. Mostly data types from runtime2.go. +# NOTES: +# - some common utils (add, hex) should be copy/pasted. +# - asminit needs to be broken up. += runtime/internal/core +g +m +fing +g0 +m0 +mutex +note +setg +needm +itab +eface +slice +asminit +signote +sigset_none +sigset +sigaltstack +stack +hex +memclr +add +cgomal +ptrSize +_PageShift +funcval +gcstats +uintreg +goos_windows +goos_plan9 +lastg + +# NB: we must break the edges from lock/unlock to gothrow to +# avoid pulling in a mass of stuff! += runtime/internal/lock +lock +unlock + +# panic, traceback, but also: signals, memory allocation, parts of GC, scheduler +# NB: we must split setsig -> sighandler edge to avoid a huge +# cycle from throw -> dopanic -> dopanic_m -> crash -> setsig -> +# sighandler -> sigprof -> cpuproftick -> MASSES (startm, allocm, +# schedule) += runtime/internal/throw +gothrow + += runtime/internal/sched +sched +schedt +gosched_m +block +addtimer + += runtime/internal/gc +GC + += runtime/internal/prof +MemProfileRecord +SetBlockProfileRate +SetCPUProfileRate +MemProfile +BlockProfile +ThreadCreateProfile +ReadMemStats +func$mem.1 # init func for sizeof_C_MStats + += runtime/internal/channels +newselect +selectrecv +selectrecv2 +selectrecvImpl +chansend1 +selectnbsend +selectnbrecv +selectnbrecv2 +reflect_chanrecv +chanrecv1 +chanrecv2 +selectgo +selectDir + += runtime/internal/hash +hash +func$alg.1 # init function for algarray; pulls in random +int64Hash +stringHash +efaceHash +int32Hash +bytesHash +ifaceHash + += runtime/internal/heapdump +writeHeapDump +dumpfd +dumpotherroot + += runtime/internal/maps +hmap +bmap +hiter +mapaccess1_fast32 +mapaccess2_fast32 +mapaccess1_fast64 +mapaccess2_fast64 +mapaccess1_faststr +mapaccess2_faststr +mapaccess1 +mapaccess2 +mapaccessK +mapiterinit +reflect_mapiternext +makemap +mapassign1 +mapdelete + += runtime/internal/netpoll +netpollarm +netpollblockcommit +netpollblock +netpollcheckerr +netpollclose +netpollClose +netpollDeadline +netpolldeadlineimpl +netpollinit +netpollopen +netpollOpen +netpollReadDeadline +netpollReset +netpollServerInit +netpollSetDeadline +netpollUnblock +netpollWaitCanceled +netpollWait +netpollWriteDeadline + += runtime/internal/ifacestuff +convI2I +convT2E +convT2I +convI2E +assertI2T +assertI2T2 +assertI2TOK +assertE2T +assertE2T2 +assertE2TOK +assertI2E +assertI2E2 +assertI2I +assertI2I2 +assertE2I +assertE2I2 +assertE2E +assertE2E2 + += runtime/internal/vdso +args + += runtime/internal/printf +printf +snprintf + += runtime/internal/strings +concatstring2 +concatstring3 +concatstring4 +concatstring5 +gostring + += runtime/internal/fp # (totally disconnected) +fsub64 +fadd64c +fneg64c +f32to64 +fintto64 +fdiv64 +fmul64 +f64toint +f64to32 +sqrt +posinf +isnan + += runtime/internal/schedinit # another tangle in its own right... +schedinit + += runtime/internal/finalize +SetFinalizer + += runtime/internal/cgo +cmalloc +cfree +cgocallbackg +weak_cgo_allocate + += runtime/internal/sync +syncsemacquire +syncsemrelease +syncsemcheck + += runtime/internal/check # runtime assertions +check + += runtime/internal/stackwb +writebarrierptr +writebarrierptr_nostore +writebarrierstring +writebarrierslice +writebarrieriface +writebarrierfat +writebarriercopy +writebarrierfat01 +writebarrierfat10 +writebarrierfat11 +writebarrierfat001 +writebarrierfat010 +writebarrierfat011 +writebarrierfat100 +writebarrierfat101 +writebarrierfat110 +writebarrierfat111 +writebarrierfat0001 +writebarrierfat0010 +writebarrierfat0011 +writebarrierfat0100 +writebarrierfat0101 +writebarrierfat0110 +writebarrierfat0111 +writebarrierfat1000 +writebarrierfat1001 +writebarrierfat1010 +writebarrierfat1011 +writebarrierfat1100 +writebarrierfat1101 +writebarrierfat1110 +writebarrierfat1111 + += runtime/internal/defers +Goexit +deferreturn +gopanic +func$panic.1 # init of _defer + += runtime/internal/seq # string and slice alloc stuff +makeslice +growslice +slicebytetostringtmp +slicebytetostring +slicecopy +slicestringcopy +slicerunetostring +intstring +gostringw +stringiter +rawruneslice +stringiter2 Index: cmd/sockdrawer/scgraph.go =================================================================== new file mode 100644 --- /dev/null +++ b/cmd/sockdrawer/scgraph.go @@ -0,0 +1,195 @@ +package main + +// This file defines the strong-component graph. +// (It is used only to simplify the renderings.) + +import ( + "bytes" + "fmt" + "os" + "sort" +) + +// An scnode is a node in the scnode graph. +// It is (approximately; see -fuse) an SCC of the node graph. +type scnode struct { + id int // unique id + nodes map[*node]bool // elements of this SCC + succs, preds map[*scnode]bool // scnode graph adjacency sets + cluster *cluster // the cluster to which this SCC belongs +} + +const maxLines = 8 // maximum number of lines in a label + +func (s *scnode) String() string { + var buf bytes.Buffer + // Order nodes by exportedness and in-degree. + order := make([]*node, 0, len(s.nodes)) + for n := range s.nodes { + order = append(order, n) + } + sort.Sort(byExportednessAndInDegree(order)) + for i, n := range order { + if i> 0 { + buf.WriteByte('\n') + } + if i == maxLines-1 && len(order)> maxLines { + fmt.Fprintf(&buf, "+ %d more", len(order)-i) + break + } + buf.WriteString(n.String()) + } + return buf.String() +} + +type byExportednessAndInDegree []*node + +func (b byExportednessAndInDegree) Len() int { return len(b) } +func (b byExportednessAndInDegree) Less(i, j int) bool { + if r := b[i].exportedness() - b[j].exportedness(); r != 0 { + return r> 0 + } + if r := len(b[i].preds) - len(b[j].preds); r != 0 { + return r> 0 + } + return false +} +func (b byExportednessAndInDegree) Swap(i, j int) { b[i], b[j] = b[j], b[i] } + +func (o *organizer) makeSCGraph(fuse bool) map[*scnode]bool { + // Kosaraju's algorithm---Tarjan is overkill here. + + // Forward pass. + S := make([]*node, 0, len(o.nodes)) // postorder stack + seen := make(map[*node]bool) + var visit func(n *node) + visit = func(n *node) { + if !seen[n] { + seen[n] = true + for s := range n.succs { + visit(s) + } + S = append(S, n) + } + } + + for _, n := range o.nodes { + visit(n) + } + + // Reverse pass. + var current *scnode + seen = make(map[*node]bool) + var rvisit func(d *node) + rvisit = func(d *node) { + if !seen[d] { + seen[d] = true + current.nodes[d] = true + d.scc = current + for p := range d.preds { + rvisit(p) + } + } + } + scnodes := make(map[*scnode]bool) + for len(S)> 0 { + top := S[len(S)-1] + S = S[:len(S)-1] // pop + if !seen[top] { + current = &scnode{ + id: len(scnodes), + cluster: top.cluster, + nodes: make(map[*node]bool), + succs: make(map[*scnode]bool), + preds: make(map[*scnode]bool), + } + rvisit(top) + scnodes[current] = true + } + } + + // Build the strong-component DAG by + // projecting the edges of the node graph, + // discarding self-edges. + for s := range scnodes { + for n := range s.nodes { + for pred := range n.preds { + if s != pred.scc { + s.preds[pred.scc] = true + } + } + for succ := range n.succs { + if s != succ.scc { + s.succs[succ.scc] = true + } + } + } + } + + if debug { + fmt.Fprintf(os.Stderr, "\t%d SCCs\n", len(scnodes)) + } + + // TODO(adonovan): do we still need this? + if fuse { + // Now fold each single-predecessor scnode into that predecessor. + // Iterate until a fixed point is reached. + // + // Example: a -> b -> c + // b -> d + // Becomes: ab -> c + // ab -> d + // Then: abcd + // + // Since the loop conserves predecessor count for all + // non-deleted scnodes, the algorithm is order-invariant. + for { + var changed bool + for b := range scnodes { + if b == nil || len(b.preds) != 1 { + continue + } + var a *scnode + for a = range b.preds { + } + // a is sole predecessor of b + if a.cluster != b.cluster { + // don't fuse SCCs belonging to different clusters! + continue + } + + changed = true + + b.preds = nil + delete(a.succs, b) + + // a gets all b's nodes + for n := range b.nodes { + a.nodes[n] = true + n.scc = a + } + b.nodes = nil + + // a gets all b's succs + for c := range b.succs { + a.succs[c] = true + c.preds[a] = true + delete(c.preds, b) + } + b.succs = nil + + delete(scnodes, b) + } + if !changed { + break + } + } + + if debug { + fmt.Fprintf(os.Stderr, "\t%d SCCs (excluding single-predecessor ones)\n", + len(scnodes)) + } + } + + return scnodes +}

AltStyle によって変換されたページ (->オリジナル) /