| // 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 implements Andersen's analysis, an inclusion-based |
| pointer analysis algorithm first described in (Andersen, 1994). |
| |
| A pointer analysis relates every pointer expression in a whole program |
| to the set of memory locations to which it might point. This |
| information can be used to construct a call graph of the program that |
| precisely represents the destinations of dynamic function and method |
| calls. It can also be used to determine, for example, which pairs of |
| channel operations operate on the same channel. |
| |
| The package allows the client to request a set of expressions of |
| interest for which the points-to information will be returned once the |
| analysis is complete. In addition, the client may request that a |
| callgraph is constructed. The example program in example_test.go |
| demonstrates both of these features. Clients should not request more |
| information than they need since it may increase the cost of the |
| analysis significantly. |
| |
| |
| CLASSIFICATION |
| |
| Our algorithm is INCLUSION-BASED: the points-to sets for x and y will |
| be related by pts(y) ⊇ pts(x) if the program contains the statement |
| y = x. |
| |
| It is FLOW-INSENSITIVE: it ignores all control flow constructs and the |
| order of statements in a program. It is therefore a "MAY ALIAS" |
| analysis: its facts are of the form "P may/may not point to L", |
| not "P must point to L". |
| |
| It is FIELD-SENSITIVE: it builds separate points-to sets for distinct |
| fields, such as x and y in struct { x, y *int }. |
| |
| It is mostly CONTEXT-INSENSITIVE: most functions are analyzed once, |
| so values can flow in at one call to the function and return out at |
| another. Only some smaller functions are analyzed with consideration |
| of their calling context. |
| |
| It has a CONTEXT-SENSITIVE HEAP: objects are named by both allocation |
| site and context, so the objects returned by two distinct calls to f: |
| func f() *T { return new(T) } |
| are distinguished up to the limits of the calling context. |
| |
| It is a WHOLE PROGRAM analysis: it requires SSA-form IR for the |
| complete Go program and summaries for native code. |
| |
| See the (Hind, PASTE'01) survey paper for an explanation of these terms. |
| |
| |
| SOUNDNESS |
| |
| The analysis is fully sound when invoked on pure Go programs that do not |
| use reflection or unsafe.Pointer conversions. In other words, if there |
| is any possible execution of the program in which pointer P may point to |
| object O, the analysis will report that fact. |
| |
| |
| REFLECTION |
| |
| By default, the "reflect" library is ignored by the analysis, as if all |
| its functions were no-ops, but if the client enables the Reflection flag, |
| the analysis will make a reasonable attempt to model the effects of |
| calls into this library. However, this comes at a significant |
| performance cost, and not all features of that library are yet |
| implemented. In addition, some simplifying approximations must be made |
| to ensure that the analysis terminates; for example, reflection can be |
| used to construct an infinite set of types and values of those types, |
| but the analysis arbitrarily bounds the depth of such types. |
| |
| Most but not all reflection operations are supported. |
| In particular, addressable reflect.Values are not yet implemented, so |
| operations such as (reflect.Value).Set have no analytic effect. |
| |
| |
| UNSAFE POINTER CONVERSIONS |
| |
| The pointer analysis makes no attempt to understand aliasing between the |
| operand x and result y of an unsafe.Pointer conversion: |
| y = (*T)(unsafe.Pointer(x)) |
| It is as if the conversion allocated an entirely new object: |
| y = new(T) |
| |
| |
| NATIVE CODE |
| |
| The analysis cannot model the aliasing effects of functions written in |
| languages other than Go, such as runtime intrinsics in C or assembly, or |
| code accessed via cgo. The result is as if such functions are no-ops. |
| However, various important intrinsics are understood by the analysis, |
| along with built-ins such as append. |
| |
| The analysis currently provides no way for users to specify the aliasing |
| effects of native code. |
| |
| ------------------------------------------------------------------------ |
| |
| IMPLEMENTATION |
| |
| The remaining documentation is intended for package maintainers and |
| pointer analysis specialists. Maintainers should have a solid |
| understanding of the referenced papers (especially those by H&L and PKH) |
| before making making significant changes. |
| |
| The implementation is similar to that described in (Pearce et al, |
| PASTE'04). Unlike many algorithms which interleave constraint |
| generation and solving, constructing the callgraph as they go, this |
| implementation for the most part observes a phase ordering (generation |
| before solving), with only simple (copy) constraints being generated |
| during solving. (The exception is reflection, which creates various |
| constraints during solving as new types flow to reflect.Value |
| operations.) This improves the traction of presolver optimisations, |
| but imposes certain restrictions, e.g. potential context sensitivity |
| is limited since all variants must be created a priori. |
| |
| |
| TERMINOLOGY |
| |
| A type is said to be "pointer-like" if it is a reference to an object. |
| Pointer-like types include pointers and also interfaces, maps, channels, |
| functions and slices. |
| |
| We occasionally use C's x->f notation to distinguish the case where x |
| is a struct pointer from x.f where is a struct value. |
| |
| Pointer analysis literature (and our comments) often uses the notation |
| dst=*src+offset to mean something different than what it means in Go. |
| It means: for each node index p in pts(src), the node index p+offset is |
| in pts(dst). Similarly *dst+offset=src is used for store constraints |
| and dst=src+offset for offset-address constraints. |
| |
| |
| NODES |
| |
| Nodes are the key datastructure of the analysis, and have a dual role: |
| they represent both constraint variables (equivalence classes of |
| pointers) and members of points-to sets (things that can be pointed |
| at, i.e. "labels"). |
| |
| Nodes are naturally numbered. The numbering enables compact |
| representations of sets of nodes such as bitvectors (or BDDs); and the |
| ordering enables a very cheap way to group related nodes together. For |
| example, passing n parameters consists of generating n parallel |
| constraints from caller+i to callee+i for 0<=i<n. |
| |
| The zero nodeid means "not a pointer". For simplicity, we generate flow |
| constraints even for non-pointer types such as int. The pointer |
| equivalence (PE) presolver optimization detects which variables cannot |
| point to anything; this includes not only all variables of non-pointer |
| types (such as int) but also variables of pointer-like types if they are |
| always nil, or are parameters to a function that is never called. |
| |
| Each node represents a scalar part of a value or object. |
| Aggregate types (structs, tuples, arrays) are recursively flattened |
| out into a sequential list of scalar component types, and all the |
| elements of an array are represented by a single node. (The |
| flattening of a basic type is a list containing a single node.) |
| |
| Nodes are connected into a graph with various kinds of labelled edges: |
| simple edges (or copy constraints) represent value flow. Complex |
| edges (load, store, etc) trigger the creation of new simple edges |
| during the solving phase. |
| |
| |
| OBJECTS |
| |
| Conceptually, an "object" is a contiguous sequence of nodes denoting |
| an addressable location: something that a pointer can point to. The |
| first node of an object has a non-nil obj field containing information |
| about the allocation: its size, context, and ssa.Value. |
| |
| Objects include: |
| - functions and globals; |
| - variable allocations in the stack frame or heap; |
| - maps, channels and slices created by calls to make(); |
| - allocations to construct an interface; |
| - allocations caused by conversions, e.g. []byte(str). |
| - arrays allocated by calls to append(); |
| |
| Many objects have no Go types. For example, the func, map and chan type |
| kinds in Go are all varieties of pointers, but their respective objects |
| are actual functions (executable code), maps (hash tables), and channels |
| (synchronized queues). Given the way we model interfaces, they too are |
| pointers to "tagged" objects with no Go type. And an *ssa.Global denotes |
| the address of a global variable, but the object for a Global is the |
| actual data. So, the types of an ssa.Value that creates an object is |
| "off by one indirection": a pointer to the object. |
| |
| The individual nodes of an object are sometimes referred to as "labels". |
| |
| For uniformity, all objects have a non-zero number of fields, even those |
| of the empty type struct{}. (All arrays are treated as if of length 1, |
| so there are no empty arrays. The empty tuple is never address-taken, |
| so is never an object.) |
| |
| |
| TAGGED OBJECTS |
| |
| An tagged object has the following layout: |
| |
| T -- obj.flags ⊇ {otTagged} |
| v |
| ... |
| |
| The T node's typ field is the dynamic type of the "payload": the value |
| v which follows, flattened out. The T node's obj has the otTagged |
| flag. |
| |
| Tagged objects are needed when generalizing across types: interfaces, |
| reflect.Values, reflect.Types. Each of these three types is modelled |
| as a pointer that exclusively points to tagged objects. |
| |
| Tagged objects may be indirect (obj.flags ⊇ {otIndirect}) meaning that |
| the value v is not of type T but *T; this is used only for |
| reflect.Values that represent lvalues. (These are not implemented yet.) |
| |
| |
| ANALYSIS ABSTRACTION OF EACH TYPE |
| |
| Variables of the following "scalar" types may be represented by a |
| single node: basic types, pointers, channels, maps, slices, 'func' |
| pointers, interfaces. |
| |
| Pointers |
| Nothing to say here, oddly. |
| |
| Basic types (bool, string, numbers, unsafe.Pointer) |
| Currently all fields in the flattening of a type, including |
| non-pointer basic types such as int, are represented in objects and |
| values. Though non-pointer nodes within values are uninteresting, |
| non-pointer nodes in objects may be useful (if address-taken) |
| because they permit the analysis to deduce, in this example, |
| |
| var s struct{ ...; x int; ... } |
| p := &s.x |
| |
| that p points to s.x. If we ignored such object fields, we could only |
| say that p points somewhere within s. |
| |
| All other basic types are ignored. Expressions of these types have |
| zero nodeid, and fields of these types within aggregate other types |
| are omitted. |
| |
| unsafe.Pointers are not modelled as pointers, so a conversion of an |
| unsafe.Pointer to *T is (unsoundly) treated equivalent to new(T). |
| |
| Channels |
| An expression of type 'chan T' is a kind of pointer that points |
| exclusively to channel objects, i.e. objects created by MakeChan (or |
| reflection). |
| |
| 'chan T' is treated like *T. |
| *ssa.MakeChan is treated as equivalent to new(T). |
| *ssa.Send and receive (*ssa.UnOp(ARROW)) and are equivalent to store |
| and load. |
| |
| Maps |
| An expression of type 'map[K]V' is a kind of pointer that points |
| exclusively to map objects, i.e. objects created by MakeMap (or |
| reflection). |
| |
| map K[V] is treated like *M where M = struct{k K; v V}. |
| *ssa.MakeMap is equivalent to new(M). |
| *ssa.MapUpdate is equivalent to *y=x where *y and x have type M. |
| *ssa.Lookup is equivalent to y=x.v where x has type *M. |
| |
| Slices |
| A slice []T, which dynamically resembles a struct{array *T, len, cap int}, |
| is treated as if it were just a *T pointer; the len and cap fields are |
| ignored. |
| |
| *ssa.MakeSlice is treated like new([1]T): an allocation of a |
| singleton array. |
| *ssa.Index on a slice is equivalent to a load. |
| *ssa.IndexAddr on a slice returns the address of the sole element of the |
| slice, i.e. the same address. |
| *ssa.Slice is treated as a simple copy. |
| |
| Functions |
| An expression of type 'func...' is a kind of pointer that points |
| exclusively to function objects. |
| |
| A function object has the following layout: |
| |
| identity -- typ:*types.Signature; obj.flags ⊇ {otFunction} |
| params_0 -- (the receiver, if a method) |
| ... |
| params_n-1 |
| results_0 |
| ... |
| results_m-1 |
| |
| There may be multiple function objects for the same *ssa.Function |
| due to context-sensitive treatment of some functions. |
| |
| The first node is the function's identity node. |
| Associated with every callsite is a special "targets" variable, |
| whose pts() contains the identity node of each function to which |
| the call may dispatch. Identity words are not otherwise used during |
| the analysis, but we construct the call graph from the pts() |
| solution for such nodes. |
| |
| The following block of contiguous nodes represents the flattened-out |
| types of the parameters ("P-block") and results ("R-block") of the |
| function object. |
| |
| The treatment of free variables of closures (*ssa.FreeVar) is like |
| that of global variables; it is not context-sensitive. |
| *ssa.MakeClosure instructions create copy edges to Captures. |
| |
| A Go value of type 'func' (i.e. a pointer to one or more functions) |
| is a pointer whose pts() contains function objects. The valueNode() |
| for an *ssa.Function returns a singleton for that function. |
| |
| Interfaces |
| An expression of type 'interface{...}' is a kind of pointer that |
| points exclusively to tagged objects. All tagged objects pointed to |
| by an interface are direct (the otIndirect flag is clear) and |
| concrete (the tag type T is not itself an interface type). The |
| associated ssa.Value for an interface's tagged objects may be an |
| *ssa.MakeInterface instruction, or nil if the tagged object was |
| created by an instrinsic (e.g. reflection). |
| |
| Constructing an interface value causes generation of constraints for |
| all of the concrete type's methods; we can't tell a priori which |
| ones may be called. |
| |
| TypeAssert y = x.(T) is implemented by a dynamic constraint |
| triggered by each tagged object O added to pts(x): a typeFilter |
| constraint if T is an interface type, or an untag constraint if T is |
| a concrete type. A typeFilter tests whether O.typ implements T; if |
| so, O is added to pts(y). An untagFilter tests whether O.typ is |
| assignable to T,and if so, a copy edge O.v -> y is added. |
| |
| ChangeInterface is a simple copy because the representation of |
| tagged objects is independent of the interface type (in contrast |
| to the "method tables" approach used by the gc runtime). |
| |
| y := Invoke x.m(...) is implemented by allocating contiguous P/R |
| blocks for the callsite and adding a dynamic rule triggered by each |
| tagged object added to pts(x). The rule adds param/results copy |
| edges to/from each discovered concrete method. |
| |
| (Q. Why do we model an interface as a pointer to a pair of type and |
| value, rather than as a pair of a pointer to type and a pointer to |
| value? |
| A. Control-flow joins would merge interfaces ({T1}, {V1}) and ({T2}, |
| {V2}) to make ({T1,T2}, {V1,V2}), leading to the infeasible and |
| type-unsafe combination (T1,V2). Treating the value and its concrete |
| type as inseparable makes the analysis type-safe.) |
| |
| reflect.Value |
| A reflect.Value is modelled very similar to an interface{}, i.e. as |
| a pointer exclusively to tagged objects, but with two generalizations. |
| |
| 1) a reflect.Value that represents an lvalue points to an indirect |
| (obj.flags ⊇ {otIndirect}) tagged object, which has a similar |
| layout to an tagged object except that the value is a pointer to |
| the dynamic type. Indirect tagged objects preserve the correct |
| aliasing so that mutations made by (reflect.Value).Set can be |
| observed. |
| |
| Indirect objects only arise when an lvalue is derived from an |
| rvalue by indirection, e.g. the following code: |
| |
| type S struct { X T } |
| var s S |
| var i interface{} = &s // i points to a *S-tagged object (from MakeInterface) |
| v1 := reflect.ValueOf(i) // v1 points to same *S-tagged object as i |
| v2 := v1.Elem() // v2 points to an indirect S-tagged object, pointing to s |
| v3 := v2.FieldByName("X") // v3 points to an indirect int-tagged object, pointing to s.X |
| v3.Set(y) // pts(s.X) ⊇ pts(y) |
| |
| Whether indirect or not, the concrete type of the tagged object |
| corresponds to the user-visible dynamic type, and the existence |
| of a pointer is an implementation detail. |
| |
| (NB: indirect tagged objects are not yet implemented) |
| |
| 2) The dynamic type tag of a tagged object pointed to by a |
| reflect.Value may be an interface type; it need not be concrete. |
| |
| This arises in code such as this: |
| tEface := reflect.TypeOf(new(interface{}).Elem() // interface{} |
| eface := reflect.Zero(tEface) |
| pts(eface) is a singleton containing an interface{}-tagged |
| object. That tagged object's payload is an interface{} value, |
| i.e. the pts of the payload contains only concrete-tagged |
| objects, although in this example it's the zero interface{} value, |
| so its pts is empty. |
| |
| reflect.Type |
| Just as in the real "reflect" library, we represent a reflect.Type |
| as an interface whose sole implementation is the concrete type, |
| *reflect.rtype. (This choice is forced on us by go/types: clients |
| cannot fabricate types with arbitrary method sets.) |
| |
| rtype instances are canonical: there is at most one per dynamic |
| type. (rtypes are in fact large structs but since identity is all |
| that matters, we represent them by a single node.) |
| |
| The payload of each *rtype-tagged object is an *rtype pointer that |
| points to exactly one such canonical rtype object. We exploit this |
| by setting the node.typ of the payload to the dynamic type, not |
| '*rtype'. This saves us an indirection in each resolution rule. As |
| an optimisation, *rtype-tagged objects are canonicalized too. |
| |
| |
| Aggregate types: |
| |
| Aggregate types are treated as if all directly contained |
| aggregates are recursively flattened out. |
| |
| Structs |
| *ssa.Field y = x.f creates a simple edge to y from x's node at f's offset. |
| |
| *ssa.FieldAddr y = &x->f requires a dynamic closure rule to create |
| simple edges for each struct discovered in pts(x). |
| |
| The nodes of a struct consist of a special 'identity' node (whose |
| type is that of the struct itself), followed by the nodes for all |
| the struct's fields, recursively flattened out. A pointer to the |
| struct is a pointer to its identity node. That node allows us to |
| distinguish a pointer to a struct from a pointer to its first field. |
| |
| Field offsets are logical field offsets (plus one for the identity |
| node), so the sizes of the fields can be ignored by the analysis. |
| |
| (The identity node is non-traditional but enables the distiction |
| described above, which is valuable for code comprehension tools. |
| Typical pointer analyses for C, whose purpose is compiler |
| optimization, must soundly model unsafe.Pointer (void*) conversions, |
| and this requires fidelity to the actual memory layout using physical |
| field offsets.) |
| |
| *ssa.Field y = x.f creates a simple edge to y from x's node at f's offset. |
| |
| *ssa.FieldAddr y = &x->f requires a dynamic closure rule to create |
| simple edges for each struct discovered in pts(x). |
| |
| Arrays |
| We model an array by an identity node (whose type is that of the |
| array itself) followed by a node representing all the elements of |
| the array; the analysis does not distinguish elements with different |
| indices. Effectively, an array is treated like struct{elem T}, a |
| load y=x[i] like y=x.elem, and a store x[i]=y like x.elem=y; the |
| index i is ignored. |
| |
| A pointer to an array is pointer to its identity node. (A slice is |
| also a pointer to an array's identity node.) The identity node |
| allows us to distinguish a pointer to an array from a pointer to one |
| of its elements, but it is rather costly because it introduces more |
| offset constraints into the system. Furthermore, sound treatment of |
| unsafe.Pointer would require us to dispense with this node. |
| |
| Arrays may be allocated by Alloc, by make([]T), by calls to append, |
| and via reflection. |
| |
| Tuples (T, ...) |
| Tuples are treated like structs with naturally numbered fields. |
| *ssa.Extract is analogous to *ssa.Field. |
| |
| However, tuples have no identity field since by construction, they |
| cannot be address-taken. |
| |
| |
| FUNCTION CALLS |
| |
| There are three kinds of function call: |
| (1) static "call"-mode calls of functions. |
| (2) dynamic "call"-mode calls of functions. |
| (3) dynamic "invoke"-mode calls of interface methods. |
| Cases 1 and 2 apply equally to methods and standalone functions. |
| |
| Static calls. |
| A static call consists three steps: |
| - finding the function object of the callee; |
| - creating copy edges from the actual parameter value nodes to the |
| P-block in the function object (this includes the receiver if |
| the callee is a method); |
| - creating copy edges from the R-block in the function object to |
| the value nodes for the result of the call. |
| |
| A static function call is little more than two struct value copies |
| between the P/R blocks of caller and callee: |
| |
| callee.P = caller.P |
| caller.R = callee.R |
| |
| Context sensitivity |
| |
| Static calls (alone) may be treated context sensitively, |
| i.e. each callsite may cause a distinct re-analysis of the |
| callee, improving precision. Our current context-sensitivity |
| policy treats all intrinsics and getter/setter methods in this |
| manner since such functions are small and seem like an obvious |
| source of spurious confluences, though this has not yet been |
| evaluated. |
| |
| Dynamic function calls |
| |
| Dynamic calls work in a similar manner except that the creation of |
| copy edges occurs dynamically, in a similar fashion to a pair of |
| struct copies in which the callee is indirect: |
| |
| callee->P = caller.P |
| caller.R = callee->R |
| |
| (Recall that the function object's P- and R-blocks are contiguous.) |
| |
| Interface method invocation |
| |
| For invoke-mode calls, we create a params/results block for the |
| callsite and attach a dynamic closure rule to the interface. For |
| each new tagged object that flows to the interface, we look up |
| the concrete method, find its function object, and connect its P/R |
| blocks to the callsite's P/R blocks, adding copy edges to the graph |
| during solving. |
| |
| Recording call targets |
| |
| The analysis notifies its clients of each callsite it encounters, |
| passing a CallSite interface. Among other things, the CallSite |
| contains a synthetic constraint variable ("targets") whose |
| points-to solution includes the set of all function objects to |
| which the call may dispatch. |
| |
| It is via this mechanism that the callgraph is made available. |
| Clients may also elect to be notified of callgraph edges directly; |
| internally this just iterates all "targets" variables' pts(·)s. |
| |
| |
| PRESOLVER |
| |
| We implement Hash-Value Numbering (HVN), a pre-solver constraint |
| optimization described in Hardekopf & Lin, SAS'07. This is documented |
| in more detail in hvn.go. We intend to add its cousins HR and HU in |
| future. |
| |
| |
| SOLVER |
| |
| The solver is currently a naive Andersen-style implementation; it does |
| not perform online cycle detection, though we plan to add solver |
| optimisations such as Hybrid- and Lazy- Cycle Detection from (Hardekopf |
| & Lin, PLDI'07). |
| |
| It uses difference propagation (Pearce et al, SQC'04) to avoid |
| redundant re-triggering of closure rules for values already seen. |
| |
| Points-to sets are represented using sparse bit vectors (similar to |
| those used in LLVM and gcc), which are more space- and time-efficient |
| than sets based on Go's built-in map type or dense bit vectors. |
| |
| Nodes are permuted prior to solving so that object nodes (which may |
| appear in points-to sets) are lower numbered than non-object (var) |
| nodes. This improves the density of the set over which the PTSs |
| range, and thus the efficiency of the representation. |
| |
| Partly thanks to avoiding map iteration, the execution of the solver is |
| 100% deterministic, a great help during debugging. |
| |
| |
| FURTHER READING |
| |
| Andersen, L. O. 1994. Program analysis and specialization for the C |
| programming language. Ph.D. dissertation. DIKU, University of |
| Copenhagen. |
| |
| David J. Pearce, Paul H. J. Kelly, and Chris Hankin. 2004. Efficient |
| field-sensitive pointer analysis for C. In Proceedings of the 5th ACM |
| SIGPLAN-SIGSOFT workshop on Program analysis for software tools and |
| engineering (PASTE '04). ACM, New York, NY, USA, 37-42. |
| http://doi.acm.org/10.1145/996821.996835 |
| |
| David J. Pearce, Paul H. J. Kelly, and Chris Hankin. 2004. Online |
| Cycle Detection and Difference Propagation: Applications to Pointer |
| Analysis. Software Quality Control 12, 4 (December 2004), 311-337. |
| http://dx.doi.org/10.1023/B:SQJO.0000039791.93071.a2 |
| |
| David Grove and Craig Chambers. 2001. A framework for call graph |
| construction algorithms. ACM Trans. Program. Lang. Syst. 23, 6 |
| (November 2001), 685-746. |
| http://doi.acm.org/10.1145/506315.506316 |
| |
| Ben Hardekopf and Calvin Lin. 2007. The ant and the grasshopper: fast |
| and accurate pointer analysis for millions of lines of code. In |
| Proceedings of the 2007 ACM SIGPLAN conference on Programming language |
| design and implementation (PLDI '07). ACM, New York, NY, USA, 290-299. |
| http://doi.acm.org/10.1145/1250734.1250767 |
| |
| Ben Hardekopf and Calvin Lin. 2007. Exploiting pointer and location |
| equivalence to optimize pointer analysis. In Proceedings of the 14th |
| international conference on Static Analysis (SAS'07), Hanne Riis |
| Nielson and Gilberto Filé (Eds.). Springer-Verlag, Berlin, Heidelberg, |
| 265-280. |
| |
| Atanas Rountev and Satish Chandra. 2000. Off-line variable substitution |
| for scaling points-to analysis. In Proceedings of the ACM SIGPLAN 2000 |
| conference on Programming language design and implementation (PLDI '00). |
| ACM, New York, NY, USA, 47-56. DOI=10.1145/349299.349310 |
| http://doi.acm.org/10.1145/349299.349310 |
| |
| */ |
| package pointer // import "golang.org/x/tools/go/pointer" |