Skip to content

Commit bfc94c9

Browse files
committed
go/ssa: extract type recursion into a helper package
This change moves ssa.forEachReachable into internal/typesinternal.ForEachElement, simplifies the signature (by internalizing the type map part) and adds a test. There are two copies of this algorithm (the other in go/callgraph/rta), and we will need it again in ssautil.Reachable (see golang/go#69291). A follow-up change will make the copy in rta delegate to this one (small steps). Also, make ssa.Program.RuntimeTypes do the type analysis when it is called, instead of doing it eagerly each time we encounter a MakeInterface instruction. This should reduce eventually costs since RuntimeTypes shouldn't be needed: it was added for the pointer analysis (since deleted) and it used by ssautil.AllFunctions (to be replaced, see golang/go#69291), and in both cases it is the wrong tool for the job because: (a) it is more precise to accumulate runtime types in the subset of the program of interest, while doing some kind of reachability fixed-point computation; and (b) its use in AllFunctions is unsound because although it accounts for all (too many!) MakeInterface operations it does not account for types exposed through public API (see the proposed replacement, ssautil.Reachable) when analyzing incomplete programs. Updates golang/go#69291 Change-Id: Ib369278e50295b9287fe95c06169b81425193e90 Reviewed-on: https://go-review.googlesource.com/c/tools/+/610939 Reviewed-by: Tim King <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 1dc949f commit bfc94c9

File tree

6 files changed

+320
-132
lines changed

6 files changed

+320
-132
lines changed

go/ssa/builder.go

+10-10
Original file line numberDiff line numberDiff line change
@@ -3104,17 +3104,17 @@ func (b *builder) buildYieldFunc(fn *Function) {
31043104
fn.finishBody()
31053105
}
31063106

3107-
// addRuntimeType records t as a runtime type,
3108-
// along with all types derivable from it using reflection.
3107+
// addMakeInterfaceType records non-interface type t as the type of
3108+
// the operand a MakeInterface operation, for [Program.RuntimeTypes].
31093109
//
3110-
// Acquires prog.runtimeTypesMu.
3111-
func addRuntimeType(prog *Program, t types.Type) {
3112-
prog.runtimeTypesMu.Lock()
3113-
defer prog.runtimeTypesMu.Unlock()
3114-
forEachReachable(&prog.MethodSets, t, func(t types.Type) bool {
3115-
prev, _ := prog.runtimeTypes.Set(t, true).(bool)
3116-
return !prev // already seen?
3117-
})
3110+
// Acquires prog.makeInterfaceTypesMu.
3111+
func addMakeInterfaceType(prog *Program, t types.Type) {
3112+
prog.makeInterfaceTypesMu.Lock()
3113+
defer prog.makeInterfaceTypesMu.Unlock()
3114+
if prog.makeInterfaceTypes == nil {
3115+
prog.makeInterfaceTypes = make(map[types.Type]unit)
3116+
}
3117+
prog.makeInterfaceTypes[t] = unit{}
31183118
}
31193119

31203120
// Build calls Package.Build for each package in prog.

go/ssa/emit.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ func emitConv(f *Function, val Value, typ types.Type) Value {
249249
// non-parameterized, as they are the set of runtime types.
250250
t := val.Type()
251251
if f.typeparams.Len() == 0 || !f.Prog.isParameterized(t) {
252-
addRuntimeType(f.Prog, t)
252+
addMakeInterfaceType(f.Prog, t)
253253
}
254254

255255
mi := &MakeInterface{X: val}

go/ssa/methods.go

+18-119
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"go/types"
1212

1313
"golang.org/x/tools/go/types/typeutil"
14-
"golang.org/x/tools/internal/aliases"
14+
"golang.org/x/tools/internal/typesinternal"
1515
)
1616

1717
// MethodValue returns the Function implementing method sel, building
@@ -158,124 +158,23 @@ type methodSet struct {
158158
//
159159
// Thread-safe.
160160
//
161-
// Acquires prog.runtimeTypesMu.
161+
// Acquires prog.makeInterfaceTypesMu.
162162
func (prog *Program) RuntimeTypes() []types.Type {
163-
prog.runtimeTypesMu.Lock()
164-
defer prog.runtimeTypesMu.Unlock()
165-
return prog.runtimeTypes.Keys()
166-
}
167-
168-
// forEachReachable calls f for type T and each type reachable from
169-
// its type through reflection.
170-
//
171-
// The function f must use memoization to break cycles and
172-
// return false when the type has already been visited.
173-
//
174-
// TODO(adonovan): publish in typeutil and share with go/callgraph/rta.
175-
func forEachReachable(msets *typeutil.MethodSetCache, T types.Type, f func(types.Type) bool) {
176-
var visit func(T types.Type, skip bool)
177-
visit = func(T types.Type, skip bool) {
178-
if !skip {
179-
if !f(T) {
180-
return
181-
}
182-
}
183-
184-
// Recursion over signatures of each method.
185-
tmset := msets.MethodSet(T)
186-
for i := 0; i < tmset.Len(); i++ {
187-
sig := tmset.At(i).Type().(*types.Signature)
188-
// It is tempting to call visit(sig, false)
189-
// but, as noted in golang.org/cl/65450043,
190-
// the Signature.Recv field is ignored by
191-
// types.Identical and typeutil.Map, which
192-
// is confusing at best.
193-
//
194-
// More importantly, the true signature rtype
195-
// reachable from a method using reflection
196-
// has no receiver but an extra ordinary parameter.
197-
// For the Read method of io.Reader we want:
198-
// func(Reader, []byte) (int, error)
199-
// but here sig is:
200-
// func([]byte) (int, error)
201-
// with .Recv = Reader (though it is hard to
202-
// notice because it doesn't affect Signature.String
203-
// or types.Identical).
204-
//
205-
// TODO(adonovan): construct and visit the correct
206-
// non-method signature with an extra parameter
207-
// (though since unnamed func types have no methods
208-
// there is essentially no actual demand for this).
209-
//
210-
// TODO(adonovan): document whether or not it is
211-
// safe to skip non-exported methods (as RTA does).
212-
visit(sig.Params(), true) // skip the Tuple
213-
visit(sig.Results(), true) // skip the Tuple
214-
}
215-
216-
switch T := T.(type) {
217-
case *aliases.Alias:
218-
visit(aliases.Unalias(T), skip) // emulates the pre-Alias behavior
219-
220-
case *types.Basic:
221-
// nop
222-
223-
case *types.Interface:
224-
// nop---handled by recursion over method set.
225-
226-
case *types.Pointer:
227-
visit(T.Elem(), false)
228-
229-
case *types.Slice:
230-
visit(T.Elem(), false)
231-
232-
case *types.Chan:
233-
visit(T.Elem(), false)
234-
235-
case *types.Map:
236-
visit(T.Key(), false)
237-
visit(T.Elem(), false)
238-
239-
case *types.Signature:
240-
if T.Recv() != nil {
241-
panic(fmt.Sprintf("Signature %s has Recv %s", T, T.Recv()))
242-
}
243-
visit(T.Params(), true) // skip the Tuple
244-
visit(T.Results(), true) // skip the Tuple
245-
246-
case *types.Named:
247-
// A pointer-to-named type can be derived from a named
248-
// type via reflection. It may have methods too.
249-
visit(types.NewPointer(T), false)
250-
251-
// Consider 'type T struct{S}' where S has methods.
252-
// Reflection provides no way to get from T to struct{S},
253-
// only to S, so the method set of struct{S} is unwanted,
254-
// so set 'skip' flag during recursion.
255-
visit(T.Underlying(), true) // skip the unnamed type
256-
257-
case *types.Array:
258-
visit(T.Elem(), false)
259-
260-
case *types.Struct:
261-
for i, n := 0, T.NumFields(); i < n; i++ {
262-
// TODO(adonovan): document whether or not
263-
// it is safe to skip non-exported fields.
264-
visit(T.Field(i).Type(), false)
265-
}
266-
267-
case *types.Tuple:
268-
for i, n := 0, T.Len(); i < n; i++ {
269-
visit(T.At(i).Type(), false)
270-
}
271-
272-
case *types.TypeParam, *types.Union:
273-
// forEachReachable must not be called on parameterized types.
274-
panic(T)
275-
276-
default:
277-
panic(T)
278-
}
163+
prog.makeInterfaceTypesMu.Lock()
164+
defer prog.makeInterfaceTypesMu.Unlock()
165+
166+
// Compute the derived types on demand, since many SSA clients
167+
// never call RuntimeTypes, and those that do typically call
168+
// it once (often within ssautil.AllFunctions, which will
169+
// eventually not use it; see Go issue #69291.) This
170+
// eliminates the need to eagerly compute all the element
171+
// types during SSA building.
172+
var runtimeTypes []types.Type
173+
add := func(t types.Type) { runtimeTypes = append(runtimeTypes, t) }
174+
var set typeutil.Map // for de-duping identical types
175+
for t := range prog.makeInterfaceTypes {
176+
typesinternal.ForEachElement(&set, &prog.MethodSets, t, add)
279177
}
280-
visit(T, false)
178+
179+
return runtimeTypes
281180
}

go/ssa/ssa.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ type Program struct {
3737
hasParamsMu sync.Mutex
3838
hasParams typeparams.Free
3939

40-
runtimeTypesMu sync.Mutex
41-
runtimeTypes typeutil.Map // set of runtime types (from MakeInterface)
40+
// set of concrete types used as MakeInterface operands
41+
makeInterfaceTypesMu sync.Mutex
42+
makeInterfaceTypes map[types.Type]unit // (may contain redundant identical types)
4243

4344
// objectMethods is a memoization of objectMethod
4445
// to avoid creation of duplicate methods from type information.

internal/typesinternal/element.go

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package typesinternal
6+
7+
import (
8+
"fmt"
9+
"go/types"
10+
11+
"golang.org/x/tools/go/types/typeutil"
12+
"golang.org/x/tools/internal/aliases"
13+
)
14+
15+
// ForEachElement calls f for type T and each type reachable from its
16+
// type through reflection. It does this by recursively stripping off
17+
// type constructors; in addition, for each named type N, the type *N
18+
// is added to the result as it may have additional methods.
19+
//
20+
// The caller must provide an initially empty set used to de-duplicate
21+
// identical types, potentially across multiple calls to ForEachElement.
22+
// (Its final value holds all the elements seen, matching the arguments
23+
// passed to f.)
24+
//
25+
// TODO(adonovan): share/harmonize with go/callgraph/rta.
26+
func ForEachElement(rtypes *typeutil.Map, msets *typeutil.MethodSetCache, T types.Type, f func(types.Type)) {
27+
var visit func(T types.Type, skip bool)
28+
visit = func(T types.Type, skip bool) {
29+
if !skip {
30+
if seen, _ := rtypes.Set(T, true).(bool); seen {
31+
return // de-dup
32+
}
33+
34+
f(T) // notify caller of new element type
35+
}
36+
37+
// Recursion over signatures of each method.
38+
tmset := msets.MethodSet(T)
39+
for i := 0; i < tmset.Len(); i++ {
40+
sig := tmset.At(i).Type().(*types.Signature)
41+
// It is tempting to call visit(sig, false)
42+
// but, as noted in golang.org/cl/65450043,
43+
// the Signature.Recv field is ignored by
44+
// types.Identical and typeutil.Map, which
45+
// is confusing at best.
46+
//
47+
// More importantly, the true signature rtype
48+
// reachable from a method using reflection
49+
// has no receiver but an extra ordinary parameter.
50+
// For the Read method of io.Reader we want:
51+
// func(Reader, []byte) (int, error)
52+
// but here sig is:
53+
// func([]byte) (int, error)
54+
// with .Recv = Reader (though it is hard to
55+
// notice because it doesn't affect Signature.String
56+
// or types.Identical).
57+
//
58+
// TODO(adonovan): construct and visit the correct
59+
// non-method signature with an extra parameter
60+
// (though since unnamed func types have no methods
61+
// there is essentially no actual demand for this).
62+
//
63+
// TODO(adonovan): document whether or not it is
64+
// safe to skip non-exported methods (as RTA does).
65+
visit(sig.Params(), true) // skip the Tuple
66+
visit(sig.Results(), true) // skip the Tuple
67+
}
68+
69+
switch T := T.(type) {
70+
case *aliases.Alias:
71+
visit(aliases.Unalias(T), skip) // emulates the pre-Alias behavior
72+
73+
case *types.Basic:
74+
// nop
75+
76+
case *types.Interface:
77+
// nop---handled by recursion over method set.
78+
79+
case *types.Pointer:
80+
visit(T.Elem(), false)
81+
82+
case *types.Slice:
83+
visit(T.Elem(), false)
84+
85+
case *types.Chan:
86+
visit(T.Elem(), false)
87+
88+
case *types.Map:
89+
visit(T.Key(), false)
90+
visit(T.Elem(), false)
91+
92+
case *types.Signature:
93+
if T.Recv() != nil {
94+
panic(fmt.Sprintf("Signature %s has Recv %s", T, T.Recv()))
95+
}
96+
visit(T.Params(), true) // skip the Tuple
97+
visit(T.Results(), true) // skip the Tuple
98+
99+
case *types.Named:
100+
// A pointer-to-named type can be derived from a named
101+
// type via reflection. It may have methods too.
102+
visit(types.NewPointer(T), false)
103+
104+
// Consider 'type T struct{S}' where S has methods.
105+
// Reflection provides no way to get from T to struct{S},
106+
// only to S, so the method set of struct{S} is unwanted,
107+
// so set 'skip' flag during recursion.
108+
visit(T.Underlying(), true) // skip the unnamed type
109+
110+
case *types.Array:
111+
visit(T.Elem(), false)
112+
113+
case *types.Struct:
114+
for i, n := 0, T.NumFields(); i < n; i++ {
115+
// TODO(adonovan): document whether or not
116+
// it is safe to skip non-exported fields.
117+
visit(T.Field(i).Type(), false)
118+
}
119+
120+
case *types.Tuple:
121+
for i, n := 0, T.Len(); i < n; i++ {
122+
visit(T.At(i).Type(), false)
123+
}
124+
125+
case *types.TypeParam, *types.Union:
126+
// forEachReachable must not be called on parameterized types.
127+
panic(T)
128+
129+
default:
130+
panic(T)
131+
}
132+
}
133+
visit(T, false)
134+
}

0 commit comments

Comments
 (0)