Skip to content

Commit 07eca49

Browse files
committed
go/types, types2: use type nest to detect type cycles (fix validType)
validType was using a global type info map to detect invalid recursive types, which was incorrect. Instead, change the algorithm as follows: - Rather than using a "seen" (or typeInfo) map which is cumbersome to update correctly, use the stack of embedding types (the type nest) to check whether a type is embedded within itself, directly or indirectly. - Use Identical for type comparisons which correctly considers identity of instantiated generic types. - As before, maintain the full path of types leading to a cycle. But unlike before, track the named types rather than their objects, for a smaller slice ([]*Named rather than []Object), and convert to an object list only when needed for error reporting. - As an optimization, keep track of valid *Named types (Checker.valids). This prevents pathological cases from consuming excessive computation time. - Add clarifying comments and document invariants. Based on earlier insights by David Chase (see also CL 408818). Fixes #52698. Change-Id: I5e4598c58afcf4ab987a426c5c4b7b28bdfcf5ea Reviewed-on: https://go-review.googlesource.com/c/go/+/409694 Reviewed-by: Robert Findley <rfindley@google.com> Run-TryBot: Robert Griesemer <gri@google.com> Reviewed-by: David Chase <drchase@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
1 parent 770146d commit 07eca49

File tree

6 files changed

+445
-94
lines changed

6 files changed

+445
-94
lines changed

src/cmd/compile/internal/types2/check.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ type Checker struct {
9898
nextID uint64 // unique Id for type parameters (first valid Id is 1)
9999
objMap map[Object]*declInfo // maps package-level objects and (non-interface) methods to declaration info
100100
impMap map[importKey]*Package // maps (import path, source directory) to (complete or fake) package
101-
infoMap map[*Named]typeInfo // maps named types to their associated type info (for cycle detection)
101+
valids instanceLookup // valid *Named (incl. instantiated) types per the validType check
102102

103103
// pkgPathMap maps package names to the set of distinct import paths we've
104104
// seen for that name, anywhere in the import graph. It is used for
@@ -241,7 +241,6 @@ func NewChecker(conf *Config, pkg *Package, info *Info) *Checker {
241241
version: version,
242242
objMap: make(map[Object]*declInfo),
243243
impMap: make(map[importKey]*Package),
244-
infoMap: make(map[*Named]typeInfo),
245244
}
246245
}
247246

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2022 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 p
6+
7+
// correctness check: ensure that cycles through generic instantiations are detected
8+
type T[P any] struct {
9+
_ P
10+
}
11+
12+
type S /* ERROR illegal cycle */ struct {
13+
_ T[S]
14+
}
15+
16+
// simplified test 1
17+
18+
var _ A1[A1[string]]
19+
20+
type A1[P any] struct {
21+
_ B1[P]
22+
}
23+
24+
type B1[P any] struct {
25+
_ P
26+
}
27+
28+
// simplified test 2
29+
var _ B2[A2]
30+
31+
type A2 struct {
32+
_ B2[string]
33+
}
34+
35+
type B2[P any] struct {
36+
_ C2[P]
37+
}
38+
39+
type C2[P any] struct {
40+
_ P
41+
}
42+
43+
// test case from issue
44+
type T23 interface {
45+
~struct {
46+
Field0 T13[T15]
47+
}
48+
}
49+
50+
type T1[P1 interface {
51+
}] struct {
52+
Field2 P1
53+
}
54+
55+
type T13[P2 interface {
56+
}] struct {
57+
Field2 T1[P2]
58+
}
59+
60+
type T15 struct {
61+
Field0 T13[string]
62+
}

src/cmd/compile/internal/types2/validtype.go

Lines changed: 165 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,24 @@
55
package types2
66

77
// validType verifies that the given type does not "expand" indefinitely
8-
// producing a cycle in the type graph. Cycles are detected by marking
9-
// defined types.
8+
// producing a cycle in the type graph.
109
// (Cycles involving alias types, as in "type A = [10]A" are detected
1110
// earlier, via the objDecl cycle detection mechanism.)
1211
func (check *Checker) validType(typ *Named) {
13-
check.validType0(typ, nil, nil)
12+
check.validType0(typ, nil, nil, nil)
1413
}
1514

16-
type typeInfo uint
17-
1815
// validType0 checks if the given type is valid. If typ is a type parameter
1916
// its value is looked up in the provided environment. The environment is
2017
// nil if typ is not part of (the RHS of) an instantiated type, in that case
2118
// any type parameter encountered must be from an enclosing function and can
22-
// be ignored. The path is the list of type names that lead to the current typ.
23-
func (check *Checker) validType0(typ Type, env *tparamEnv, path []Object) typeInfo {
24-
const (
25-
unknown typeInfo = iota
26-
marked
27-
valid
28-
invalid
29-
)
30-
19+
// be ignored. The nest list describes the stack (the "nest in memory") of
20+
// types which contain (or embed in the case of interfaces) other types. For
21+
// instance, a struct named S which contains a field of named type F contains
22+
// (the memory of) F in S, leading to the nest S->F. If a type appears in its
23+
// own nest (say S->F->S) we have an invalid recursive type. The path list is
24+
// the full path of named types in a cycle, it is only needed for error reporting.
25+
func (check *Checker) validType0(typ Type, env *tparamEnv, nest, path []*Named) bool {
3126
switch t := typ.(type) {
3227
case nil:
3328
// We should never see a nil type but be conservative and panic
@@ -37,74 +32,109 @@ func (check *Checker) validType0(typ Type, env *tparamEnv, path []Object) typeIn
3732
}
3833

3934
case *Array:
40-
return check.validType0(t.elem, env, path)
35+
return check.validType0(t.elem, env, nest, path)
4136

4237
case *Struct:
4338
for _, f := range t.fields {
44-
if check.validType0(f.typ, env, path) == invalid {
45-
return invalid
39+
if !check.validType0(f.typ, env, nest, path) {
40+
return false
4641
}
4742
}
4843

4944
case *Union:
5045
for _, t := range t.terms {
51-
if check.validType0(t.typ, env, path) == invalid {
52-
return invalid
46+
if !check.validType0(t.typ, env, nest, path) {
47+
return false
5348
}
5449
}
5550

5651
case *Interface:
5752
for _, etyp := range t.embeddeds {
58-
if check.validType0(etyp, env, path) == invalid {
59-
return invalid
53+
if !check.validType0(etyp, env, nest, path) {
54+
return false
6055
}
6156
}
6257

6358
case *Named:
59+
// Exit early if we already know t is valid.
60+
// This is purely an optimization but it prevents excessive computation
61+
// times in pathological cases such as testdata/fixedbugs/issue6977.go.
62+
// (Note: The valids map could also be allocated locally, once for each
63+
// validType call.)
64+
if check.valids.lookup(t) != nil {
65+
break
66+
}
67+
6468
// Don't report a 2nd error if we already know the type is invalid
6569
// (e.g., if a cycle was detected earlier, via under).
6670
// Note: ensure that t.orig is fully resolved by calling Underlying().
6771
if t.Underlying() == Typ[Invalid] {
68-
check.infoMap[t] = invalid
69-
return invalid
72+
return false
7073
}
7174

72-
switch check.infoMap[t] {
73-
case unknown:
74-
check.infoMap[t] = marked
75-
check.infoMap[t] = check.validType0(t.Origin().fromRHS, env.push(t), append(path, t.obj))
76-
case marked:
77-
// We have seen type t before and thus must have a cycle.
78-
check.infoMap[t] = invalid
79-
// t cannot be in an imported package otherwise that package
80-
// would have reported a type cycle and couldn't have been
81-
// imported in the first place.
82-
assert(t.obj.pkg == check.pkg)
83-
t.underlying = Typ[Invalid] // t is in the current package (no race possibility)
84-
// Find the starting point of the cycle and report it.
85-
for i, tn := range path {
86-
if tn == t.obj {
87-
check.cycleError(path[i:])
88-
return invalid
75+
// If the current type t is also found in nest, (the memory of) t is
76+
// embedded in itself, indicating an invalid recursive type.
77+
for _, e := range nest {
78+
if Identical(e, t) {
79+
// t cannot be in an imported package otherwise that package
80+
// would have reported a type cycle and couldn't have been
81+
// imported in the first place.
82+
assert(t.obj.pkg == check.pkg)
83+
t.underlying = Typ[Invalid] // t is in the current package (no race possibility)
84+
// Find the starting point of the cycle and report it.
85+
// Because each type in nest must also appear in path (see invariant below),
86+
// type t must be in path since it was found in nest. But not every type in path
87+
// is in nest. Specifically t may appear in path with an earlier index than the
88+
// index of t in nest. Search again.
89+
for start, p := range path {
90+
if Identical(p, t) {
91+
check.cycleError(makeObjList(path[start:]))
92+
return false
93+
}
8994
}
95+
panic("cycle start not found")
9096
}
91-
panic("cycle start not found")
9297
}
93-
return check.infoMap[t]
98+
99+
// No cycle was found. Check the RHS of t.
100+
// Every type added to nest is also added to path; thus every type that is in nest
101+
// must also be in path (invariant). But not every type in path is in nest, since
102+
// nest may be pruned (see below, *TypeParam case).
103+
if !check.validType0(t.Origin().fromRHS, env.push(t), append(nest, t), append(path, t)) {
104+
return false
105+
}
106+
107+
check.valids.add(t) // t is valid
94108

95109
case *TypeParam:
96110
// A type parameter stands for the type (argument) it was instantiated with.
97111
// Check the corresponding type argument for validity if we have one.
98112
if env != nil {
99113
if targ := env.tmap[t]; targ != nil {
100114
// Type arguments found in targ must be looked
101-
// up in the enclosing environment env.link.
102-
return check.validType0(targ, env.link, path)
115+
// up in the enclosing environment env.link. The
116+
// type argument must be valid in the enclosing
117+
// type (where the current type was instantiated),
118+
// hence we must check targ's validity in the type
119+
// nest excluding the current (instantiated) type
120+
// (see the example at the end of this file).
121+
// For error reporting we keep the full path.
122+
return check.validType0(targ, env.link, nest[:len(nest)-1], path)
103123
}
104124
}
105125
}
106126

107-
return valid
127+
return true
128+
}
129+
130+
// makeObjList returns the list of type name objects for the given
131+
// list of named types.
132+
func makeObjList(tlist []*Named) []Object {
133+
olist := make([]Object, len(tlist))
134+
for i, t := range tlist {
135+
olist[i] = t.obj
136+
}
137+
return olist
108138
}
109139

110140
// A tparamEnv provides the environment for looking up the type arguments
@@ -146,3 +176,93 @@ func (env *tparamEnv) push(typ *Named) *tparamEnv {
146176
// same information should be available via the path:
147177
// We should be able to just walk the path backwards
148178
// and find the type arguments in the instance objects.
179+
180+
// Here is an example illustrating why we need to exclude the
181+
// instantiated type from nest when evaluating the validity of
182+
// a type parameter. Given the declarations
183+
//
184+
// var _ A[A[string]]
185+
//
186+
// type A[P any] struct { _ B[P] }
187+
// type B[P any] struct { _ P }
188+
//
189+
// we want to determine if the type A[A[string]] is valid.
190+
// We start evaluating A[A[string]] outside any type nest:
191+
//
192+
// A[A[string]]
193+
// nest =
194+
// path =
195+
//
196+
// The RHS of A is now evaluated in the A[A[string]] nest:
197+
//
198+
// struct{_ B[P₁]}
199+
// nest = A[A[string]]
200+
// path = A[A[string]]
201+
//
202+
// The struct has a single field of type B[P₁] with which
203+
// we continue:
204+
//
205+
// B[P₁]
206+
// nest = A[A[string]]
207+
// path = A[A[string]]
208+
//
209+
// struct{_ P₂}
210+
// nest = A[A[string]]->B[P]
211+
// path = A[A[string]]->B[P]
212+
//
213+
// Eventutally we reach the type parameter P of type B (P₂):
214+
//
215+
// P₂
216+
// nest = A[A[string]]->B[P]
217+
// path = A[A[string]]->B[P]
218+
//
219+
// The type argument for P of B is the type parameter P of A (P₁).
220+
// It must be evaluated in the type nest that existed when B was
221+
// instantiated:
222+
//
223+
// P₁
224+
// nest = A[A[string]] <== type nest at B's instantiation time
225+
// path = A[A[string]]->B[P]
226+
//
227+
// If we'd use the current nest it would correspond to the path
228+
// which will be wrong as we will see shortly. P's type argument
229+
// is A[string], which again must be evaluated in the type nest
230+
// that existed when A was instantiated with A[string]. That type
231+
// nest is empty:
232+
//
233+
// A[string]
234+
// nest = <== type nest at A's instantiation time
235+
// path = A[A[string]]->B[P]
236+
//
237+
// Evaluation then proceeds as before for A[string]:
238+
//
239+
// struct{_ B[P₁]}
240+
// nest = A[string]
241+
// path = A[A[string]]->B[P]->A[string]
242+
//
243+
// Now we reach B[P] again. If we had not adjusted nest, it would
244+
// correspond to path, and we would find B[P] in nest, indicating
245+
// a cycle, which would clearly be wrong since there's no cycle in
246+
// A[string]:
247+
//
248+
// B[P₁]
249+
// nest = A[string]
250+
// path = A[A[string]]->B[P]->A[string] <== path contains B[P]!
251+
//
252+
// But because we use the correct type nest, evaluation proceeds without
253+
// errors and we get the evaluation sequence:
254+
//
255+
// struct{_ P₂}
256+
// nest = A[string]->B[P]
257+
// path = A[A[string]]->B[P]->A[string]->B[P]
258+
// P₂
259+
// nest = A[string]->B[P]
260+
// path = A[A[string]]->B[P]->A[string]->B[P]
261+
// P₁
262+
// nest = A[string]
263+
// path = A[A[string]]->B[P]->A[string]->B[P]
264+
// string
265+
// nest =
266+
// path = A[A[string]]->B[P]->A[string]->B[P]
267+
//
268+
// At this point we're done and A[A[string]] and is valid.

src/go/types/check.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ type Checker struct {
105105
nextID uint64 // unique Id for type parameters (first valid Id is 1)
106106
objMap map[Object]*declInfo // maps package-level objects and (non-interface) methods to declaration info
107107
impMap map[importKey]*Package // maps (import path, source directory) to (complete or fake) package
108-
infoMap map[*Named]typeInfo // maps named types to their associated type info (for cycle detection)
108+
valids instanceLookup // valid *Named (incl. instantiated) types per the validType check
109109

110110
// pkgPathMap maps package names to the set of distinct import paths we've
111111
// seen for that name, anywhere in the import graph. It is used for
@@ -249,7 +249,6 @@ func NewChecker(conf *Config, fset *token.FileSet, pkg *Package, info *Info) *Ch
249249
version: version,
250250
objMap: make(map[Object]*declInfo),
251251
impMap: make(map[importKey]*Package),
252-
infoMap: make(map[*Named]typeInfo),
253252
}
254253
}
255254

0 commit comments

Comments
 (0)