// 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 pointer // This file implements Hash-Value Numbering (HVN), a pre-solver // constraint optimization described in Hardekopf & Lin, SAS'07 (see // doc.go) that analyses the graph topology to determine which sets of // variables are "pointer equivalent" (PE), i.e. must have identical // points-to sets in the solution. // // A separate ("offline") graph is constructed. Its nodes are those of // the main-graph, plus an additional node *X for each pointer node X. // With this graph we can reason about the unknown points-to set of // dereferenced pointers. (We do not generalize this to represent // unknown fields x->f, perhaps because such fields would be numerous, // though it might be worth an experiment.) // // Nodes whose points-to relations are not entirely captured by the // graph are marked as "indirect": the *X nodes, the parameters of // address-taken functions (which includes all functions in method // sets), or nodes updated by the solver rules for reflection, etc. // // All addr (y=&x) nodes are initially assigned a pointer-equivalence // (PE) label equal to x's nodeid in the main graph. (These are the // only PE labels that are less than len(a.nodes).) // // All offsetAddr (y=&x.f) constraints are initially assigned a PE // label; such labels are memoized, keyed by (x, f), so that equivalent // nodes y as assigned the same label. // // Then we process each strongly connected component (SCC) of the graph // in topological order, assigning it a PE label based on the set P of // PE labels that flow to it from its immediate dependencies. // // If any node in P is "indirect", the entire SCC is assigned a fresh PE // label. Otherwise: // // |P|=0 if P is empty, all nodes in the SCC are non-pointers (e.g. // uninitialized variables, or formal params of dead functions) // and the SCC is assigned the PE label of zero. // // |P|=1 if P is a singleton, the SCC is assigned the same label as the // sole element of P. // // |P|>1 if P contains multiple labels, a unique label representing P is // invented and recorded in an hash table, so that other // equivalent SCCs may also be assigned this label, akin to // conventional hash-value numbering in a compiler. // // Finally, a renumbering is computed such that each node is replaced by // the lowest-numbered node with the same PE label. All constraints are // renumbered, and any resulting duplicates are eliminated. // // The only nodes that are not renumbered are the objects x in addr // (y=&x) constraints, since the ids of these nodes (and fields derived // from them via offsetAddr rules) are the elements of all points-to // sets, so they must remain as they are if we want the same solution. // // The solverStates (node.solve) for nodes in the same equivalence class // are linked together so that all nodes in the class have the same // solution. This avoids the need to renumber nodeids buried in // Queries, cgnodes, etc (like (*analysis).renumber() does) since only // the solution is needed. // // The result of HVN is that the number of distinct nodes and // constraints is reduced, but the solution is identical (almost---see // CROSS-CHECK below). In particular, both linear and cyclic chains of // copies are each replaced by a single node. // // Nodes and constraints created "online" (e.g. while solving reflection // constraints) are not subject to this optimization. // // PERFORMANCE // // In two benchmarks (guru and godoc), HVN eliminates about two thirds // of nodes, the majority accounted for by non-pointers: nodes of // non-pointer type, pointers that remain nil, formal parameters of dead // functions, nodes of untracked types, etc. It also reduces the number // of constraints, also by about two thirds, and the solving time by // 30--42%, although we must pay about 15% for the running time of HVN // itself. The benefit is greater for larger applications. // // There are many possible optimizations to improve the performance: // * Use fewer than 1:1 onodes to main graph nodes: many of the onodes // we create are not needed. // * HU (HVN with Union---see paper): coalesce "union" peLabels when // their expanded-out sets are equal. // * HR (HVN with deReference---see paper): this will require that we // apply HVN until fixed point, which may need more bookkeeping of the // correspondance of main nodes to onodes. // * Location Equivalence (see paper): have points-to sets contain not // locations but location-equivalence class labels, each representing // a set of locations. // * HVN with field-sensitive ref: model each of the fields of a // pointer-to-struct. // // CROSS-CHECK // // To verify the soundness of the optimization, when the // debugHVNCrossCheck option is enabled, we run the solver twice, once // before and once after running HVN, dumping the solution to disk, and // then we compare the results. If they are not identical, the analysis // panics. // // The solution dumped to disk includes only the N*N submatrix of the // complete solution where N is the number of nodes after generation. // In other words, we ignore pointer variables and objects created by // the solver itself, since their numbering depends on the solver order, // which is affected by the optimization. In any case, that's the only // part the client cares about. // // The cross-check is too strict and may fail spuriously. Although the // H&L paper describing HVN states that the solutions obtained should be // identical, this is not the case in practice because HVN can collapse // cycles involving *p even when pts(p)={}. Consider this example // distilled from testdata/hello.go: // // var x T // func f(p **T) { // t0 = *p // ... // t1 = φ(t0, &x) // *p = t1 // } // // If f is dead code, we get: // unoptimized: pts(p)={} pts(t0)={} pts(t1)={&x} // optimized: pts(p)={} pts(t0)=pts(t1)=pts(*p)={&x} // // It's hard to argue that this is a bug: the result is sound and the // loss of precision is inconsequential---f is dead code, after all. // But unfortunately it limits the usefulness of the cross-check since // failures must be carefully analyzed. Ben Hardekopf suggests (in // personal correspondence) some approaches to mitigating it: // // If there is a node with an HVN points-to set that is a superset // of the NORM points-to set, then either it's a bug or it's a // result of this issue. If it's a result of this issue, then in // the offline constraint graph there should be a REF node inside // some cycle that reaches this node, and in the NORM solution the // pointer being dereferenced by that REF node should be the empty // set. If that isn't true then this is a bug. If it is true, then // you can further check that in the NORM solution the "extra" // points-to info in the HVN solution does in fact come from that // purported cycle (if it doesn't, then this is still a bug). If // you're doing the further check then you'll need to do it for // each "extra" points-to element in the HVN points-to set. // // There are probably ways to optimize these checks by taking // advantage of graph properties. For example, extraneous points-to // info will flow through the graph and end up in many // nodes. Rather than checking every node with extra info, you // could probably work out the "origin point" of the extra info and // just check there. Note that the check in the first bullet is // looking for soundness bugs, while the check in the second bullet // is looking for precision bugs; depending on your needs, you may // care more about one than the other. // // which we should evaluate. The cross-check is nonetheless invaluable // for all but one of the programs in the pointer_test suite. import ( "fmt" "go/types" "io" "reflect" "golang.org/x/tools/container/intsets" ) // A peLabel is a pointer-equivalence label: two nodes with the same // peLabel have identical points-to solutions. // // The numbers are allocated consecutively like so: // 0 not a pointer // 1..N-1 addrConstraints (equals the constraint's .src field, hence sparse) // ... offsetAddr constraints // ... SCCs (with indirect nodes or multiple inputs) // // Each PE label denotes a set of pointers containing a single addr, a // single offsetAddr, or some set of other PE labels. // type peLabel int type hvn struct { a *analysis N int // len(a.nodes) immediately after constraint generation log io.Writer // (optional) log of HVN lemmas onodes []*onode // nodes of the offline graph label peLabel // the next available PE label hvnLabel map[string]peLabel // hash-value numbering (PE label) for each set of onodeids stack []onodeid // DFS stack index int32 // next onode.index, from Tarjan's SCC algorithm // For each distinct offsetAddrConstraint (src, offset) pair, // offsetAddrLabels records a unique PE label >= N. offsetAddrLabels map[offsetAddr]peLabel } // The index of an node in the offline graph. // (Currently the first N align with the main nodes, // but this may change with HRU.) type onodeid uint32 // An onode is a node in the offline constraint graph. // (Where ambiguous, members of analysis.nodes are referred to as // "main graph" nodes.) // // Edges in the offline constraint graph (edges and implicit) point to // the source, i.e. against the flow of values: they are dependencies. // Implicit edges are used for SCC computation, but not for gathering // incoming labels. // type onode struct { rep onodeid // index of representative of SCC in offline constraint graph edges intsets.Sparse // constraint edges X-->Y (this onode is X) implicit intsets.Sparse // implicit edges *X-->*Y (this onode is X) peLabels intsets.Sparse // set of peLabels are pointer-equivalent to this one indirect bool // node has points-to relations not represented in graph // Tarjan's SCC algorithm index, lowlink int32 // Tarjan numbering scc int32 // -ve => on stack; 0 => unvisited; +ve => node is root of a found SCC } type offsetAddr struct { ptr nodeid offset uint32 } // nextLabel issues the next unused pointer-equivalence label. func (h *hvn) nextLabel() peLabel { h.label++ return h.label } // ref(X) returns the index of the onode for *X. func (h *hvn) ref(id onodeid) onodeid { return id + onodeid(len(h.a.nodes)) } // hvn computes pointer-equivalence labels (peLabels) using the Hash-based // Value Numbering (HVN) algorithm described in Hardekopf & Lin, SAS'07. // func (a *analysis) hvn() { start("HVN") if a.log != nil { fmt.Fprintf(a.log, "\n\n==== Pointer equivalence optimization\n\n") } h := hvn{ a: a, N: len(a.nodes), log: a.log, hvnLabel: make(map[string]peLabel), offsetAddrLabels: make(map[offsetAddr]peLabel), } if h.log != nil { fmt.Fprintf(h.log, "\nCreating offline graph nodes...\n") } // Create offline nodes. The first N nodes correspond to main // graph nodes; the next N are their corresponding ref() nodes. h.onodes = make([]*onode, 2*h.N) for id := range a.nodes { id := onodeid(id) h.onodes[id] = &onode{} h.onodes[h.ref(id)] = &onode{indirect: true} } // Each node initially represents just itself. for id, o := range h.onodes { o.rep = onodeid(id) } h.markIndirectNodes() // Reserve the first N PE labels for addrConstraints. h.label = peLabel(h.N) // Add offline constraint edges. if h.log != nil { fmt.Fprintf(h.log, "\nAdding offline graph edges...\n") } for _, c := range a.constraints { if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "; %s\n", c) } c.presolve(&h) } // Find and collapse SCCs. if h.log != nil { fmt.Fprintf(h.log, "\nFinding SCCs...\n") } h.index = 1 for id, o := range h.onodes { if id > 0 && o.index == 0 { // Start depth-first search at each unvisited node. h.visit(onodeid(id)) } } // Dump the solution // (NB: somewhat redundant with logging from simplify().) if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\nPointer equivalences:\n") for id, o := range h.onodes { if id == 0 { continue } if id == int(h.N) { fmt.Fprintf(h.log, "---\n") } fmt.Fprintf(h.log, "o%d\t", id) if o.rep != onodeid(id) { fmt.Fprintf(h.log, "rep=o%d", o.rep) } else { fmt.Fprintf(h.log, "p%d", o.peLabels.Min()) if o.indirect { fmt.Fprint(h.log, " indirect") } } fmt.Fprintln(h.log) } } // Simplify the main constraint graph h.simplify() a.showCounts() stop("HVN") } // ---- constraint-specific rules ---- // dst := &src func (c *addrConstraint) presolve(h *hvn) { // Each object (src) is an initial PE label. label := peLabel(c.src) // label < N if debugHVNVerbose && h.log != nil { // duplicate log messages are possible fmt.Fprintf(h.log, "\tcreate p%d: {&n%d}\n", label, c.src) } odst := onodeid(c.dst) osrc := onodeid(c.src) // Assign dst this label. h.onodes[odst].peLabels.Insert(int(label)) if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\to%d has p%d\n", odst, label) } h.addImplicitEdge(h.ref(odst), osrc) // *dst ~~> src. } // dst = src func (c *copyConstraint) presolve(h *hvn) { odst := onodeid(c.dst) osrc := onodeid(c.src) h.addEdge(odst, osrc) // dst --> src h.addImplicitEdge(h.ref(odst), h.ref(osrc)) // *dst ~~> *src } // dst = *src + offset func (c *loadConstraint) presolve(h *hvn) { odst := onodeid(c.dst) osrc := onodeid(c.src) if c.offset == 0 { h.addEdge(odst, h.ref(osrc)) // dst --> *src } else { // We don't interpret load-with-offset, e.g. results // of map value lookup, R-block of dynamic call, slice // copy/append, reflection. h.markIndirect(odst, "load with offset") } } // *dst + offset = src func (c *storeConstraint) presolve(h *hvn) { odst := onodeid(c.dst) osrc := onodeid(c.src) if c.offset == 0 { h.onodes[h.ref(odst)].edges.Insert(int(osrc)) // *dst --> src if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\to%d --> o%d\n", h.ref(odst), osrc) } } else { // We don't interpret store-with-offset. // See discussion of soundness at markIndirectNodes. } } // dst = &src.offset func (c *offsetAddrConstraint) presolve(h *hvn) { // Give each distinct (addr, offset) pair a fresh PE label. // The cache performs CSE, effectively. key := offsetAddr{c.src, c.offset} label, ok := h.offsetAddrLabels[key] if !ok { label = h.nextLabel() h.offsetAddrLabels[key] = label if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\tcreate p%d: {&n%d.#%d}\n", label, c.src, c.offset) } } // Assign dst this label. h.onodes[c.dst].peLabels.Insert(int(label)) if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\to%d has p%d\n", c.dst, label) } } // dst = src.(typ) where typ is an interface func (c *typeFilterConstraint) presolve(h *hvn) { h.markIndirect(onodeid(c.dst), "typeFilter result") } // dst = src.(typ) where typ is concrete func (c *untagConstraint) presolve(h *hvn) { odst := onodeid(c.dst) for end := odst + onodeid(h.a.sizeof(c.typ)); odst < end; odst++ { h.markIndirect(odst, "untag result") } } // dst = src.method(c.params...) func (c *invokeConstraint) presolve(h *hvn) { // All methods are address-taken functions, so // their formal P-blocks were already marked indirect. // Mark the caller's targets node as indirect. sig := c.method.Type().(*types.Signature) id := c.params h.markIndirect(onodeid(c.params), "invoke targets node") id++ id += nodeid(h.a.sizeof(sig.Params())) // Mark the caller's R-block as indirect. end := id + nodeid(h.a.sizeof(sig.Results())) for id < end { h.markIndirect(onodeid(id), "invoke R-block") id++ } } // markIndirectNodes marks as indirect nodes whose points-to relations // are not entirely captured by the offline graph, including: // // (a) All address-taken nodes (including the following nodes within // the same object). This is described in the paper. // // The most subtle cause of indirect nodes is the generation of // store-with-offset constraints since the offline graph doesn't // represent them. A global audit of constraint generation reveals the // following uses of store-with-offset: // // (b) genDynamicCall, for P-blocks of dynamically called functions, // to which dynamic copy edges will be added to them during // solving: from storeConstraint for standalone functions, // and from invokeConstraint for methods. // All such P-blocks must be marked indirect. // (c) MakeUpdate, to update the value part of a map object. // All MakeMap objects's value parts must be marked indirect. // (d) copyElems, to update the destination array. // All array elements must be marked indirect. // // Not all indirect marking happens here. ref() nodes are marked // indirect at construction, and each constraint's presolve() method may // mark additional nodes. // func (h *hvn) markIndirectNodes() { // (a) all address-taken nodes, plus all nodes following them // within the same object, since these may be indirectly // stored or address-taken. for _, c := range h.a.constraints { if c, ok := c.(*addrConstraint); ok { start := h.a.enclosingObj(c.src) end := start + nodeid(h.a.nodes[start].obj.size) for id := c.src; id < end; id++ { h.markIndirect(onodeid(id), "A-T object") } } } // (b) P-blocks of all address-taken functions. for id := 0; id < h.N; id++ { obj := h.a.nodes[id].obj // TODO(adonovan): opt: if obj.cgn.fn is a method and // obj.cgn is not its shared contour, this is an // "inlined" static method call. We needn't consider it // address-taken since no invokeConstraint will affect it. if obj != nil && obj.flags&otFunction != 0 && h.a.atFuncs[obj.cgn.fn] { // address-taken function if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "n%d is address-taken: %s\n", id, obj.cgn.fn) } h.markIndirect(onodeid(id), "A-T func identity") id++ sig := obj.cgn.fn.Signature psize := h.a.sizeof(sig.Params()) if sig.Recv() != nil { psize += h.a.sizeof(sig.Recv().Type()) } for end := id + int(psize); id < end; id++ { h.markIndirect(onodeid(id), "A-T func P-block") } id-- continue } } // (c) all map objects' value fields. for _, id := range h.a.mapValues { h.markIndirect(onodeid(id), "makemap.value") } // (d) all array element objects. // TODO(adonovan): opt: can we do better? for id := 0; id < h.N; id++ { // Identity node for an object of array type? if tArray, ok := h.a.nodes[id].typ.(*types.Array); ok { // Mark the array element nodes indirect. // (Skip past the identity field.) for range h.a.flatten(tArray.Elem()) { id++ h.markIndirect(onodeid(id), "array elem") } } } } func (h *hvn) markIndirect(oid onodeid, comment string) { h.onodes[oid].indirect = true if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\to%d is indirect: %s\n", oid, comment) } } // Adds an edge dst-->src. // Note the unusual convention: edges are dependency (contraflow) edges. func (h *hvn) addEdge(odst, osrc onodeid) { h.onodes[odst].edges.Insert(int(osrc)) if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\to%d --> o%d\n", odst, osrc) } } func (h *hvn) addImplicitEdge(odst, osrc onodeid) { h.onodes[odst].implicit.Insert(int(osrc)) if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\to%d ~~> o%d\n", odst, osrc) } } // visit implements the depth-first search of Tarjan's SCC algorithm. // Precondition: x is canonical. func (h *hvn) visit(x onodeid) { h.checkCanonical(x) xo := h.onodes[x] xo.index = h.index xo.lowlink = h.index h.index++ h.stack = append(h.stack, x) // push assert(xo.scc == 0, "node revisited") xo.scc = -1 var deps []int deps = xo.edges.AppendTo(deps) deps = xo.implicit.AppendTo(deps) for _, y := range deps { // Loop invariant: x is canonical. y := h.find(onodeid(y)) if x == y { continue // nodes already coalesced } xo := h.onodes[x] yo := h.onodes[y] switch { case yo.scc > 0: // y is already a collapsed SCC case yo.scc < 0: // y is on the stack, and thus in the current SCC. if yo.index < xo.lowlink { xo.lowlink = yo.index } default: // y is unvisited; visit it now. h.visit(y) // Note: x and y are now non-canonical. x = h.find(onodeid(x)) if yo.lowlink < xo.lowlink { xo.lowlink = yo.lowlink } } } h.checkCanonical(x) // Is x the root of an SCC? if xo.lowlink == xo.index { // Coalesce all nodes in the SCC. if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "scc o%d\n", x) } for { // Pop y from stack. i := len(h.stack) - 1 y := h.stack[i] h.stack = h.stack[:i] h.checkCanonical(x) xo := h.onodes[x] h.checkCanonical(y) yo := h.onodes[y] if xo == yo { // SCC is complete. xo.scc = 1 h.labelSCC(x) break } h.coalesce(x, y) } } } // Precondition: x is canonical. func (h *hvn) labelSCC(x onodeid) { h.checkCanonical(x) xo := h.onodes[x] xpe := &xo.peLabels // All indirect nodes get new labels. if xo.indirect { label := h.nextLabel() if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\tcreate p%d: indirect SCC\n", label) fmt.Fprintf(h.log, "\to%d has p%d\n", x, label) } // Remove pre-labeling, in case a direct pre-labeled node was // merged with an indirect one. xpe.Clear() xpe.Insert(int(label)) return } // Invariant: all peLabels sets are non-empty. // Those that are logically empty contain zero as their sole element. // No other sets contains zero. // Find all labels coming in to the coalesced SCC node. for _, y := range xo.edges.AppendTo(nil) { y := h.find(onodeid(y)) if y == x { continue // already coalesced } ype := &h.onodes[y].peLabels if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\tedge from o%d = %s\n", y, ype) } if ype.IsEmpty() { if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\tnode has no PE label\n") } } assert(!ype.IsEmpty(), "incoming node has no PE label") if ype.Has(0) { // {0} represents a non-pointer. assert(ype.Len() == 1, "PE set contains {0, ...}") } else { xpe.UnionWith(ype) } } switch xpe.Len() { case 0: // SCC has no incoming non-zero PE labels: it is a non-pointer. xpe.Insert(0) case 1: // already a singleton default: // SCC has multiple incoming non-zero PE labels. // Find the canonical label representing this set. // We use String() as a fingerprint consistent with Equals(). key := xpe.String() label, ok := h.hvnLabel[key] if !ok { label = h.nextLabel() if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\tcreate p%d: union %s\n", label, xpe.String()) } h.hvnLabel[key] = label } xpe.Clear() xpe.Insert(int(label)) } if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\to%d has p%d\n", x, xpe.Min()) } } // coalesce combines two nodes in the offline constraint graph. // Precondition: x and y are canonical. func (h *hvn) coalesce(x, y onodeid) { xo := h.onodes[x] yo := h.onodes[y] // x becomes y's canonical representative. yo.rep = x if debugHVNVerbose && h.log != nil { fmt.Fprintf(h.log, "\tcoalesce o%d into o%d\n", y, x) } // x accumulates y's edges. xo.edges.UnionWith(&yo.edges) yo.edges.Clear() // x accumulates y's implicit edges. xo.implicit.UnionWith(&yo.implicit) yo.implicit.Clear() // x accumulates y's pointer-equivalence labels. xo.peLabels.UnionWith(&yo.peLabels) yo.peLabels.Clear() // x accumulates y's indirect flag. if yo.indirect { xo.indirect = true } } // simplify computes a degenerate renumbering of nodeids from the PE // labels assigned by the hvn, and uses it to simplify the main // constraint graph, eliminating non-pointer nodes and duplicate // constraints. // func (h *hvn) simplify() { // canon maps each peLabel to its canonical main node. canon := make([]nodeid, h.label) for i := range canon { canon[i] = nodeid(h.N) // indicates "unset" } // mapping maps each main node index to the index of the canonical node. mapping := make([]nodeid, len(h.a.nodes)) for id := range h.a.nodes { id := nodeid(id) if id == 0 { canon[0] = 0 mapping[0] = 0 continue } oid := h.find(onodeid(id)) peLabels := &h.onodes[oid].peLabels assert(peLabels.Len() == 1, "PE class is not a singleton") label := peLabel(peLabels.Min()) canonId := canon[label] if canonId == nodeid(h.N) { // id becomes the representative of the PE label. canonId = id canon[label] = canonId if h.a.log != nil { fmt.Fprintf(h.a.log, "\tpts(n%d) is canonical : \t(%s)\n", id, h.a.nodes[id].typ) } } else { // Link the solver states for the two nodes. assert(h.a.nodes[canonId].solve != nil, "missing solver state") h.a.nodes[id].solve = h.a.nodes[canonId].solve if h.a.log != nil { // TODO(adonovan): debug: reorganize the log so it prints // one line: // pe y = x1, ..., xn // for each canonical y. Requires allocation. fmt.Fprintf(h.a.log, "\tpts(n%d) = pts(n%d) : %s\n", id, canonId, h.a.nodes[id].typ) } } mapping[id] = canonId } // Renumber the constraints, eliminate duplicates, and eliminate // any containing non-pointers (n0). addrs := make(map[addrConstraint]bool) copys := make(map[copyConstraint]bool) loads := make(map[loadConstraint]bool) stores := make(map[storeConstraint]bool) offsetAddrs := make(map[offsetAddrConstraint]bool) untags := make(map[untagConstraint]bool) typeFilters := make(map[typeFilterConstraint]bool) invokes := make(map[invokeConstraint]bool) nbefore := len(h.a.constraints) cc := h.a.constraints[:0] // in-situ compaction for _, c := range h.a.constraints { // Renumber. switch c := c.(type) { case *addrConstraint: // Don't renumber c.src since it is the label of // an addressable object and will appear in PT sets. c.dst = mapping[c.dst] default: c.renumber(mapping) } if c.ptr() == 0 { continue // skip: constraint attached to non-pointer } var dup bool switch c := c.(type) { case *addrConstraint: _, dup = addrs[*c] addrs[*c] = true case *copyConstraint: if c.src == c.dst { continue // skip degenerate copies } if c.src == 0 { continue // skip copy from non-pointer } _, dup = copys[*c] copys[*c] = true case *loadConstraint: if c.src == 0 { continue // skip load from non-pointer } _, dup = loads[*c] loads[*c] = true case *storeConstraint: if c.src == 0 { continue // skip store from non-pointer } _, dup = stores[*c] stores[*c] = true case *offsetAddrConstraint: if c.src == 0 { continue // skip offset from non-pointer } _, dup = offsetAddrs[*c] offsetAddrs[*c] = true case *untagConstraint: if c.src == 0 { continue // skip untag of non-pointer } _, dup = untags[*c] untags[*c] = true case *typeFilterConstraint: if c.src == 0 { continue // skip filter of non-pointer } _, dup = typeFilters[*c] typeFilters[*c] = true case *invokeConstraint: if c.params == 0 { panic("non-pointer invoke.params") } if c.iface == 0 { continue // skip invoke on non-pointer } _, dup = invokes[*c] invokes[*c] = true default: // We don't bother de-duping advanced constraints // (e.g. reflection) since they are uncommon. // Eliminate constraints containing non-pointer nodeids. // // We use reflection to find the fields to avoid // adding yet another method to constraint. // // TODO(adonovan): experiment with a constraint // method that returns a slice of pointers to // nodeids fields to enable uniform iteration; // the renumber() method could be removed and // implemented using the new one. // // TODO(adonovan): opt: this is unsound since // some constraints still have an effect if one // of the operands is zero: rVCall, rVMapIndex, // rvSetMapIndex. Handle them specially. rtNodeid := reflect.TypeOf(nodeid(0)) x := reflect.ValueOf(c).Elem() for i, nf := 0, x.NumField(); i < nf; i++ { f := x.Field(i) if f.Type() == rtNodeid { if f.Uint() == 0 { dup = true // skip it break } } } } if dup { continue // skip duplicates } cc = append(cc, c) } h.a.constraints = cc if h.log != nil { fmt.Fprintf(h.log, "#constraints: was %d, now %d\n", nbefore, len(h.a.constraints)) } } // find returns the canonical onodeid for x. // (The onodes form a disjoint set forest.) func (h *hvn) find(x onodeid) onodeid { // TODO(adonovan): opt: this is a CPU hotspot. Try "union by rank". xo := h.onodes[x] rep := xo.rep if rep != x { rep = h.find(rep) // simple path compression xo.rep = rep } return rep } func (h *hvn) checkCanonical(x onodeid) { if debugHVN { assert(x == h.find(x), "not canonical") } } func assert(p bool, msg string) { if debugHVN && !p { panic("assertion failed: " + msg) } }