.gitignore added
[dotfiles/.git] / .config / coc / extensions / coc-go-data / tools / pkg / mod / golang.org / x / tools@v0.1.1-0.20210319172145-bda8f5cee399 / go / pointer / hvn.go
diff --git a/.config/coc/extensions/coc-go-data/tools/pkg/mod/golang.org/x/tools@v0.1.1-0.20210319172145-bda8f5cee399/go/pointer/hvn.go b/.config/coc/extensions/coc-go-data/tools/pkg/mod/golang.org/x/tools@v0.1.1-0.20210319172145-bda8f5cee399/go/pointer/hvn.go
new file mode 100644 (file)
index 0000000..52fd479
--- /dev/null
@@ -0,0 +1,972 @@
+// 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
+//   correspondence 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)
+               }
+       }
+       // 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)
+       }
+}