Skip to content

Commit a1a6540

Browse files
committed
go/types: implement deduplication of instances using the Environment
Implement deduplication of type instances via the Environment by recording a map of unique IDs for *Named types. This avoids an issue with the existing type hash, where qualified names alone were not sufficient to differentiate two *Named types that have the same fully qualified name but which are distinct pointers. It also allows us to drop the scope accounting for local types. A subtle bug is also fixed in subst.go, where the instance t was passed to typeHash rather than t.orig. Change-Id: I85639ccc1c9bfee470babd2fc85375484c8ed0b9 Reviewed-on: https://go-review.googlesource.com/c/go/+/344390 Trust: Robert Findley <[email protected]> Run-TryBot: Robert Findley <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Robert Griesemer <[email protected]>
1 parent 409434d commit a1a6540

File tree

8 files changed

+203
-90
lines changed

8 files changed

+203
-90
lines changed

src/go/types/check.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ type Checker struct {
8989
nextID uint64 // unique Id for type parameters (first valid Id is 1)
9090
objMap map[Object]*declInfo // maps package-level objects and (non-interface) methods to declaration info
9191
impMap map[importKey]*Package // maps (import path, source directory) to (complete or fake) package
92-
typMap map[string]*Named // maps an instantiated named type hash to a *Named type
92+
env *Environment // for deduplicating identical instances
9393

9494
// pkgPathMap maps package names to the set of distinct import paths we've
9595
// seen for that name, anywhere in the import graph. It is used for
@@ -192,7 +192,7 @@ func NewChecker(conf *Config, fset *token.FileSet, pkg *Package, info *Info) *Ch
192192
version: version,
193193
objMap: make(map[Object]*declInfo),
194194
impMap: make(map[importKey]*Package),
195-
typMap: make(map[string]*Named),
195+
env: NewEnvironment(),
196196
}
197197
}
198198

src/go/types/decl.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ func (check *Checker) validType(typ Type, path []Object) typeInfo {
316316
}
317317

318318
case *Named:
319-
t.expand(check.typMap)
319+
t.expand(check.env)
320320
// don't touch the type if it is from a different package or the Universe scope
321321
// (doing so would lead to a race condition - was issue #35049)
322322
if t.obj.pkg != check.pkg {

src/go/types/environment.go

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2021 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 types
6+
7+
import "sync"
8+
9+
// An Environment is an opaque type checking environment. It may be used to
10+
// share identical type instances across type-checked packages or calls to
11+
// Instantiate.
12+
//
13+
// It is safe for concurrent use.
14+
type Environment struct {
15+
mu sync.Mutex
16+
typeMap map[string]*Named // type hash -> instance
17+
nextID int // next unique ID
18+
seen map[*Named]int // assigned unique IDs
19+
}
20+
21+
// NewEnvironment creates a new Environment.
22+
func NewEnvironment() *Environment {
23+
return &Environment{
24+
typeMap: make(map[string]*Named),
25+
seen: make(map[*Named]int),
26+
}
27+
}
28+
29+
// TODO(rfindley): move Environment.typeHash here.
30+
31+
// typeForHash returns the recorded type for the type hash h, if it exists.
32+
// If no type exists for h and n is non-nil, n is recorded for h.
33+
func (env *Environment) typeForHash(h string, n *Named) *Named {
34+
env.mu.Lock()
35+
defer env.mu.Unlock()
36+
if existing := env.typeMap[h]; existing != nil {
37+
return existing
38+
}
39+
if n != nil {
40+
env.typeMap[h] = n
41+
}
42+
return n
43+
}
44+
45+
// idForType returns a unique ID for the pointer n.
46+
func (env *Environment) idForType(n *Named) int {
47+
env.mu.Lock()
48+
defer env.mu.Unlock()
49+
id, ok := env.seen[n]
50+
if !ok {
51+
id = env.nextID
52+
env.seen[n] = id
53+
env.nextID++
54+
}
55+
return id
56+
}

src/go/types/instantiate.go

+14-24
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,6 @@ import (
1313
"go/token"
1414
)
1515

16-
// An Environment is an opaque type checking environment. It may be used to
17-
// share identical type instances across type checked packages or calls to
18-
// Instantiate.
19-
//
20-
// Currently, Environment is just a placeholder and has no effect on
21-
// instantiation.
22-
type Environment struct {
23-
// Environment is currently un-implemented, because our instantiatedHash
24-
// logic doesn't correctly handle Named type identity across multiple
25-
// packages.
26-
// TODO(rfindley): implement this.
27-
}
28-
2916
// Instantiate instantiates the type typ with the given type arguments targs.
3017
// typ must be a *Named or a *Signature type, and its number of type parameters
3118
// must match the number of provided type arguments. The result is a new,
@@ -44,7 +31,7 @@ type Environment struct {
4431
// TODO(rfindley): change this function to also return an error if lengths of
4532
// tparams and targs do not match.
4633
func Instantiate(env *Environment, typ Type, targs []Type, validate bool) (Type, error) {
47-
inst := (*Checker)(nil).instance(token.NoPos, typ, targs)
34+
inst := (*Checker)(nil).instance(token.NoPos, typ, targs, env)
4835

4936
var err error
5037
if validate {
@@ -84,7 +71,7 @@ func (check *Checker) instantiate(pos token.Pos, typ Type, targs []Type, posList
8471
}()
8572
}
8673

87-
inst := check.instance(pos, typ, targs)
74+
inst := check.instance(pos, typ, targs, check.env)
8875

8976
assert(len(posList) <= len(targs))
9077
check.later(func() {
@@ -116,23 +103,26 @@ func (check *Checker) instantiate(pos token.Pos, typ Type, targs []Type, posList
116103
// instance creates a type or function instance using the given original type
117104
// typ and arguments targs. For Named types the resulting instance will be
118105
// unexpanded.
119-
func (check *Checker) instance(pos token.Pos, typ Type, targs []Type) Type {
106+
func (check *Checker) instance(pos token.Pos, typ Type, targs []Type, env *Environment) Type {
120107
switch t := typ.(type) {
121108
case *Named:
122-
h := typeHash(t, targs)
123-
if check != nil {
124-
// typ may already have been instantiated with identical type arguments.
125-
// In that case, re-use the existing instance.
126-
if named := check.typMap[h]; named != nil {
109+
var h string
110+
if env != nil {
111+
h = env.typeHash(t, targs)
112+
// typ may already have been instantiated with identical type arguments. In
113+
// that case, re-use the existing instance.
114+
if named := env.typeForHash(h, nil); named != nil {
127115
return named
128116
}
129117
}
130118
tname := NewTypeName(pos, t.obj.pkg, t.obj.name, nil)
131119
named := check.newNamed(tname, t, nil, nil, nil) // methods and tparams are set when named is loaded
132120
named.targs = NewTypeList(targs)
133121
named.instPos = &pos
134-
if check != nil {
135-
check.typMap[h] = named
122+
if env != nil {
123+
// It's possible that we've lost a race to add named to the environment.
124+
// In this case, use whichever instance is recorded in the environment.
125+
named = env.typeForHash(h, named)
136126
}
137127
return named
138128

@@ -144,7 +134,7 @@ func (check *Checker) instance(pos token.Pos, typ Type, targs []Type) Type {
144134
if tparams.Len() == 0 {
145135
return typ // nothing to do (minor optimization)
146136
}
147-
sig := check.subst(pos, typ, makeSubstMap(tparams.list(), targs), nil).(*Signature)
137+
sig := check.subst(pos, typ, makeSubstMap(tparams.list(), targs), env).(*Signature)
148138
// If the signature doesn't use its type parameters, subst
149139
// will not make a copy. In that case, make a copy now (so
150140
// we can set tparams to nil w/o causing side-effects).

src/go/types/instantiate_test.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2021 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 types_test
6+
7+
import (
8+
. "go/types"
9+
"testing"
10+
)
11+
12+
func TestInstantiateEquality(t *testing.T) {
13+
const src = genericPkg + "p; type T[P any] int"
14+
15+
pkg, err := pkgFor(".", src, nil)
16+
if err != nil {
17+
t.Fatal(err)
18+
}
19+
20+
T := pkg.Scope().Lookup("T").Type().(*Named)
21+
22+
// Instantiating the same type twice should result in pointer-equivalent
23+
// instances.
24+
env := NewEnvironment()
25+
res1, err := Instantiate(env, T, []Type{Typ[Int]}, false)
26+
if err != nil {
27+
t.Fatal(err)
28+
}
29+
res2, err := Instantiate(env, T, []Type{Typ[Int]}, false)
30+
if err != nil {
31+
t.Fatal(err)
32+
}
33+
34+
if res1 != res2 {
35+
t.Errorf("first instance (%s) not pointer-equivalent to second instance (%s)", res1, res2)
36+
}
37+
}
38+
39+
func TestInstantiateNonEquality(t *testing.T) {
40+
const src = genericPkg + "p; type T[P any] int"
41+
42+
pkg1, err := pkgFor(".", src, nil)
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
pkg2, err := pkgFor(".", src, nil)
47+
if err != nil {
48+
t.Fatal(err)
49+
}
50+
51+
// We consider T1 and T2 to be distinct types, so their instances should not
52+
// be deduplicated by the environment.
53+
T1 := pkg1.Scope().Lookup("T").Type().(*Named)
54+
T2 := pkg2.Scope().Lookup("T").Type().(*Named)
55+
56+
env := NewEnvironment()
57+
res1, err := Instantiate(env, T1, []Type{Typ[Int]}, false)
58+
if err != nil {
59+
t.Fatal(err)
60+
}
61+
res2, err := Instantiate(env, T2, []Type{Typ[Int]}, false)
62+
if err != nil {
63+
t.Fatal(err)
64+
}
65+
66+
if res1 == res2 {
67+
t.Errorf("instance from pkg1 (%s) is pointer-equivalent to instance from pkg2 (%s)", res1, res2)
68+
}
69+
if Identical(res1, res2) {
70+
t.Errorf("instance from pkg1 (%s) is identical to instance from pkg2 (%s)", res1, res2)
71+
}
72+
}

src/go/types/named.go

+14-8
Original file line numberDiff line numberDiff line change
@@ -242,27 +242,33 @@ func (n *Named) setUnderlying(typ Type) {
242242

243243
// expand ensures that the underlying type of n is instantiated.
244244
// The underlying type will be Typ[Invalid] if there was an error.
245-
func (n *Named) expand(typMap map[string]*Named) *Named {
245+
func (n *Named) expand(env *Environment) *Named {
246246
if n.instPos != nil {
247247
// n must be loaded before instantiation, in order to have accurate
248248
// tparams. This is done implicitly by the call to n.TParams, but making it
249249
// explicit is harmless: load is idempotent.
250250
n.load()
251251
var u Type
252252
if n.check.validateTArgLen(*n.instPos, n.tparams.Len(), n.targs.Len()) {
253-
if typMap == nil {
253+
// TODO(rfindley): handling an optional Checker and Environment here (and
254+
// in subst) feels overly complicated. Can we simplify?
255+
if env == nil {
254256
if n.check != nil {
255-
typMap = n.check.typMap
257+
env = n.check.env
256258
} else {
257259
// If we're instantiating lazily, we might be outside the scope of a
258260
// type-checking pass. In that case we won't have a pre-existing
259-
// typMap, but don't want to create a duplicate of the current instance
260-
// in the process of expansion.
261-
h := typeHash(n.orig, n.targs.list())
262-
typMap = map[string]*Named{h: n}
261+
// environment, but don't want to create a duplicate of the current
262+
// instance in the process of expansion.
263+
env = NewEnvironment()
263264
}
265+
h := env.typeHash(n.orig, n.targs.list())
266+
// add the instance to the environment to avoid infinite recursion.
267+
// addInstance may return a different, existing instance, but we
268+
// shouldn't return that instance from expand.
269+
env.typeForHash(h, n)
264270
}
265-
u = n.check.subst(*n.instPos, n.orig.underlying, makeSubstMap(n.TParams().list(), n.targs.list()), typMap)
271+
u = n.check.subst(*n.instPos, n.orig.underlying, makeSubstMap(n.TParams().list(), n.targs.list()), env)
266272
} else {
267273
u = Typ[Invalid]
268274
}

src/go/types/subst.go

+23-21
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ func (m substMap) lookup(tpar *TypeParam) Type {
4343
// that it doesn't modify the incoming type. If a substitution took place, the
4444
// result type is different from the incoming type.
4545
//
46-
// If the given typMap is non-nil, it is used in lieu of check.typMap.
47-
func (check *Checker) subst(pos token.Pos, typ Type, smap substMap, typMap map[string]*Named) Type {
46+
// If the given environment is non-nil, it is used in lieu of check.env.
47+
func (check *Checker) subst(pos token.Pos, typ Type, smap substMap, env *Environment) Type {
4848
if smap.empty() {
4949
return typ
5050
}
@@ -64,27 +64,27 @@ func (check *Checker) subst(pos token.Pos, typ Type, smap substMap, typMap map[s
6464

6565
if check != nil {
6666
subst.check = check
67-
if typMap == nil {
68-
typMap = check.typMap
67+
if env == nil {
68+
env = check.env
6969
}
7070
}
71-
if typMap == nil {
71+
if env == nil {
7272
// If we don't have a *Checker and its global type map,
7373
// use a local version. Besides avoiding duplicate work,
7474
// the type map prevents infinite recursive substitution
7575
// for recursive types (example: type T[P any] *T[P]).
76-
typMap = make(map[string]*Named)
76+
env = NewEnvironment()
7777
}
78-
subst.typMap = typMap
78+
subst.env = env
7979

8080
return subst.typ(typ)
8181
}
8282

8383
type subster struct {
84-
pos token.Pos
85-
smap substMap
86-
check *Checker // nil if called via Instantiate
87-
typMap map[string]*Named
84+
pos token.Pos
85+
smap substMap
86+
check *Checker // nil if called via Instantiate
87+
env *Environment
8888
}
8989

9090
func (subst *subster) typ(typ Type) Type {
@@ -217,25 +217,25 @@ func (subst *subster) typ(typ Type) Type {
217217
}
218218

219219
// before creating a new named type, check if we have this one already
220-
h := typeHash(t, newTArgs)
220+
h := subst.env.typeHash(t.orig, newTArgs)
221221
dump(">>> new type hash: %s", h)
222-
if named, found := subst.typMap[h]; found {
222+
if named := subst.env.typeForHash(h, nil); named != nil {
223223
dump(">>> found %s", named)
224224
return named
225225
}
226226

227-
// Create a new named type and populate typMap to avoid endless recursion.
228-
// The position used here is irrelevant because validation only occurs on t
229-
// (we don't call validType on named), but we use subst.pos to help with
230-
// debugging.
227+
// Create a new named type and populate the environment to avoid endless
228+
// recursion. The position used here is irrelevant because validation only
229+
// occurs on t (we don't call validType on named), but we use subst.pos to
230+
// help with debugging.
231231
tname := NewTypeName(subst.pos, t.obj.pkg, t.obj.name, nil)
232232
t.load()
233233
// It's ok to provide a nil *Checker because the newly created type
234234
// doesn't need to be (lazily) expanded; it's expanded below.
235235
named := (*Checker)(nil).newNamed(tname, t.orig, nil, t.tparams, t.methods) // t is loaded, so tparams and methods are available
236236
named.targs = NewTypeList(newTArgs)
237-
subst.typMap[h] = named
238-
t.expand(subst.typMap) // must happen after typMap update to avoid infinite recursion
237+
subst.env.typeForHash(h, named)
238+
t.expand(subst.env) // must happen after env update to avoid infinite recursion
239239

240240
// do the substitution
241241
dump(">>> subst %s with %s (new: %s)", t.underlying, subst.smap, newTArgs)
@@ -260,14 +260,16 @@ func (subst *subster) typ(typ Type) Type {
260260
// type hash: types that are identical produce identical string representations.
261261
// If typ is a *Named type and targs is not empty, typ is printed as if it were
262262
// instantiated with targs.
263-
func typeHash(typ Type, targs []Type) string {
263+
func (env *Environment) typeHash(typ Type, targs []Type) string {
264+
assert(env != nil)
264265
assert(typ != nil)
265266
var buf bytes.Buffer
266267

267-
h := newTypeHasher(&buf)
268+
h := newTypeHasher(&buf, env)
268269
if named, _ := typ.(*Named); named != nil && len(targs) > 0 {
269270
// Don't use WriteType because we need to use the provided targs
270271
// and not any targs that might already be with the *Named type.
272+
h.typePrefix(named)
271273
h.typeName(named.obj)
272274
h.typeList(targs)
273275
} else {

0 commit comments

Comments
 (0)