Skip to content

Commit c4afec2

Browse files
griesemergopherbot
authored andcommitted
go/types, types2: consider shared methods when unifying against interfaces
When unifying two types A and B where one or both of them are interfaces, consider the shared method signatures in unification. 1) If a defined interface (an interface with a type name) is unified with another (defined) interface, currently they must originate in the same type declaration (same origin) for unification to succeed. This is more restrictive than necessary for assignments: when interfaces are assigned to each other, corresponding methods must match, but the interfaces don't have to be identical. In unification, we don't know which direction the assignment is happening (or if we have an assignment in the first place), but in any case one interface must implement the other. Thus, we check that one interface has a subset of the methods of the other and that corresponding method signatures unify. The assignment or instantiation may still not be possible but that will be checked when instantiation and parameter passing is checked. If two interfaces are compared as part of another type during unification, the types must be equal. If they are not, unifying a method subset may still succeed (and possibly produce more type arguments), but that is ok: again, subsequent instantiation and assignment will fail if the types are indeed not identical. 2) In a non-interface type is unified with an interface, currently unification fails. If this unification is a consequence of an assignment (parameter passing), this is again too restrictive: the non-interface type must only implement the interface (possibly among other type set requirements). In any case, all methods of the interface type must be present in the non-interface type and unify with the corresponding interface methods. If they don't, unification will fail either way. If they do, we may infer additional type arguments. Again, the resulting types may still not be correct but that will be determined by the instantiation and parameter passing or assignment checks. If the non-interface type and the interface type appear as component of another type, unification may now produce additional type arguments. But that is again ok because the respective types won't pass instantiation or assignment checks since they are different types. This CL introduces a new Config flag, EnableInterfaceInference, to enable this new behavior. If not set, unification remains unchanged. To be able to test the flag durign unification, a *Checker is passed and stored with the unifier. For #60353. Fixes #41176. Fixes #57192. Change-Id: I6b167a9afa378d0682e9b101d9d86f5777308af7 Reviewed-on: https://go-review.googlesource.com/c/go/+/497015 TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Robert Griesemer <[email protected]> Run-TryBot: Robert Griesemer <[email protected]> Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Robert Griesemer <[email protected]>
1 parent 74af79b commit c4afec2

File tree

11 files changed

+364
-18
lines changed

11 files changed

+364
-18
lines changed

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

+5
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ type Config struct {
169169
// If DisableUnusedImportCheck is set, packages are not checked
170170
// for unused imports.
171171
DisableUnusedImportCheck bool
172+
173+
// If EnableInterfaceInference is set, type inference uses
174+
// shared methods for improved type inference involving
175+
// interfaces.
176+
EnableInterfaceInference bool
172177
}
173178

174179
func srcimporter_setUsesCgo(conf *Config) {

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

+1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ func testFiles(t *testing.T, filenames []string, srcs [][]byte, colDelta uint, m
126126
flags := flag.NewFlagSet("", flag.PanicOnError)
127127
flags.StringVar(&conf.GoVersion, "lang", "", "")
128128
flags.BoolVar(&conf.FakeImportC, "fakeImportC", false, "")
129+
flags.BoolVar(&conf.EnableInterfaceInference, "EnableInterfaceInference", false, "")
129130
if err := parseFlags(srcs[0], flags); err != nil {
130131
t.Fatal(err)
131132
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func (check *Checker) infer(pos syntax.Pos, tparams []*TypeParam, targs []Type,
9696
// Unify parameter and argument types for generic parameters with typed arguments
9797
// and collect the indices of generic parameters with untyped arguments.
9898
// Terminology: generic parameter = function parameter with a type-parameterized type
99-
u := newUnifier(tparams, targs)
99+
u := check.newUnifier(tparams, targs)
100100

101101
errorf := func(kind string, tpar, targ Type, arg *operand) {
102102
// provide a better error message if we can

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

+148-6
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const (
6767
// corresponding types inferred for each type parameter.
6868
// A unifier is created by calling newUnifier.
6969
type unifier struct {
70+
check *Checker
7071
// handles maps each type parameter to its inferred type through
7172
// an indirection *Type called (inferred type) "handle".
7273
// Initially, each type parameter has its own, separate handle,
@@ -84,7 +85,7 @@ type unifier struct {
8485
// and corresponding type argument lists. The type argument list may be shorter
8586
// than the type parameter list, and it may contain nil types. Matching type
8687
// parameters and arguments must have the same index.
87-
func newUnifier(tparams []*TypeParam, targs []Type) *unifier {
88+
func (check *Checker) newUnifier(tparams []*TypeParam, targs []Type) *unifier {
8889
assert(len(tparams) >= len(targs))
8990
handles := make(map[*TypeParam]*Type, len(tparams))
9091
// Allocate all handles up-front: in a correct program, all type parameters
@@ -98,7 +99,7 @@ func newUnifier(tparams []*TypeParam, targs []Type) *unifier {
9899
}
99100
handles[x] = &t
100101
}
101-
return &unifier{handles, 0}
102+
return &unifier{check, handles, 0}
102103
}
103104

104105
// unify attempts to unify x and y and reports whether it succeeded.
@@ -280,7 +281,9 @@ func (u *unifier) nify(x, y Type, p *ifacePair) (result bool) {
280281
// the same type structure are permitted as long as at least one of them
281282
// is not a defined type. To accommodate for that possibility, we continue
282283
// unification with the underlying type of a defined type if the other type
283-
// is a type literal.
284+
// is a type literal. However, if the type literal is an interface and we
285+
// set EnableInterfaceInference, we continue with the defined type because
286+
// otherwise we may lose its methods.
284287
// We also continue if the other type is a basic type because basic types
285288
// are valid underlying types and may appear as core types of type constraints.
286289
// If we exclude them, inferred defined types for type parameters may not
@@ -292,7 +295,7 @@ func (u *unifier) nify(x, y Type, p *ifacePair) (result bool) {
292295
// we will fail at function instantiation or argument assignment time.
293296
//
294297
// If we have at least one defined type, there is one in y.
295-
if ny, _ := y.(*Named); ny != nil && isTypeLit(x) {
298+
if ny, _ := y.(*Named); ny != nil && isTypeLit(x) && !(u.check.conf.EnableInterfaceInference && IsInterface(x)) {
296299
if traceInference {
297300
u.tracef("%s ≡ under %s", x, ny)
298301
}
@@ -356,6 +359,104 @@ func (u *unifier) nify(x, y Type, p *ifacePair) (result bool) {
356359
x, y = y, x
357360
}
358361

362+
// If EnableInterfaceInference is set and both types are interfaces, one
363+
// interface must have a subset of the methods of the other and corresponding
364+
// method signatures must unify.
365+
// If only one type is an interface, all its methods must be present in the
366+
// other type and corresponding method signatures must unify.
367+
if u.check.conf.EnableInterfaceInference {
368+
xi, _ := x.(*Interface)
369+
yi, _ := y.(*Interface)
370+
// If we have two interfaces, check the type terms for equivalence,
371+
// and unify common methods if possible.
372+
if xi != nil && yi != nil {
373+
xset := xi.typeSet()
374+
yset := yi.typeSet()
375+
if xset.comparable != yset.comparable {
376+
return false
377+
}
378+
// For now we require terms to be equal.
379+
// We should be able to relax this as well, eventually.
380+
if !xset.terms.equal(yset.terms) {
381+
return false
382+
}
383+
// Interface types are the only types where cycles can occur
384+
// that are not "terminated" via named types; and such cycles
385+
// can only be created via method parameter types that are
386+
// anonymous interfaces (directly or indirectly) embedding
387+
// the current interface. Example:
388+
//
389+
// type T interface {
390+
// m() interface{T}
391+
// }
392+
//
393+
// If two such (differently named) interfaces are compared,
394+
// endless recursion occurs if the cycle is not detected.
395+
//
396+
// If x and y were compared before, they must be equal
397+
// (if they were not, the recursion would have stopped);
398+
// search the ifacePair stack for the same pair.
399+
//
400+
// This is a quadratic algorithm, but in practice these stacks
401+
// are extremely short (bounded by the nesting depth of interface
402+
// type declarations that recur via parameter types, an extremely
403+
// rare occurrence). An alternative implementation might use a
404+
// "visited" map, but that is probably less efficient overall.
405+
q := &ifacePair{xi, yi, p}
406+
for p != nil {
407+
if p.identical(q) {
408+
return true // same pair was compared before
409+
}
410+
p = p.prev
411+
}
412+
// The method set of x must be a subset of the method set
413+
// of y or vice versa, and the common methods must unify.
414+
xmethods := xset.methods
415+
ymethods := yset.methods
416+
// The smaller method set must be the subset, if it exists.
417+
if len(xmethods) > len(ymethods) {
418+
xmethods, ymethods = ymethods, xmethods
419+
}
420+
// len(xmethods) <= len(ymethods)
421+
// Collect the ymethods in a map for quick lookup.
422+
ymap := make(map[string]*Func, len(ymethods))
423+
for _, ym := range ymethods {
424+
ymap[ym.Id()] = ym
425+
}
426+
// All xmethods must exist in ymethods and corresponding signatures must unify.
427+
for _, xm := range xmethods {
428+
if ym := ymap[xm.Id()]; ym == nil || !u.nify(xm.typ, ym.typ, p) {
429+
return false
430+
}
431+
}
432+
return true
433+
}
434+
435+
// We don't have two interfaces. If we have one, make sure it's in xi.
436+
if yi != nil {
437+
xi = yi
438+
y = x
439+
}
440+
441+
// If we have one interface, at a minimum each of the interface methods
442+
// must be implemented and thus unify with a corresponding method from
443+
// the non-interface type, otherwise unification fails.
444+
if xi != nil {
445+
// All xi methods must exist in y and corresponding signatures must unify.
446+
xmethods := xi.typeSet().methods
447+
for _, xm := range xmethods {
448+
obj, _, _ := LookupFieldOrMethod(y, false, xm.pkg, xm.name)
449+
if ym, _ := obj.(*Func); ym == nil || !u.nify(xm.typ, ym.typ, p) {
450+
return false
451+
}
452+
}
453+
return true
454+
}
455+
456+
// Neither x nor y are interface types.
457+
// They must be structurally equivalent to unify.
458+
}
459+
359460
switch x := x.(type) {
360461
case *Basic:
361462
// Basic types are singletons except for the rune and byte
@@ -436,6 +537,8 @@ func (u *unifier) nify(x, y Type, p *ifacePair) (result bool) {
436537
}
437538

438539
case *Interface:
540+
assert(!u.check.conf.EnableInterfaceInference) // handled before this switch
541+
439542
// Two interface types unify if they have the same set of methods with
440543
// the same names, and corresponding function types unify.
441544
// Lower-case method names from different packages are always different.
@@ -508,10 +611,49 @@ func (u *unifier) nify(x, y Type, p *ifacePair) (result bool) {
508611
}
509612

510613
case *Named:
511-
// Two named types unify if their type names originate
614+
// Two defined types unify if their type names originate
512615
// in the same type declaration. If they are instantiated,
513616
// their type argument lists must unify.
514617
if y, ok := y.(*Named); ok {
618+
sameOrig := indenticalOrigin(x, y)
619+
if u.check.conf.EnableInterfaceInference {
620+
xu := x.under()
621+
yu := y.under()
622+
xi, _ := xu.(*Interface)
623+
yi, _ := yu.(*Interface)
624+
// If one or both defined types are interfaces, use interface unification,
625+
// unless they originated in the same type declaration.
626+
if xi != nil && yi != nil {
627+
// If both interfaces originate in the same declaration,
628+
// their methods unify if the type parameters unify.
629+
// Unify the type parameters rather than the methods in
630+
// case the type parameters are not used in the methods
631+
// (and to preserve existing behavior in this case).
632+
if sameOrig {
633+
xargs := x.TypeArgs().list()
634+
yargs := y.TypeArgs().list()
635+
assert(len(xargs) == len(yargs))
636+
for i, xarg := range xargs {
637+
if !u.nify(xarg, yargs[i], p) {
638+
return false
639+
}
640+
}
641+
return true
642+
}
643+
return u.nify(xu, yu, p)
644+
}
645+
// We don't have two interfaces. If we have one, make sure it's in xi.
646+
if yi != nil {
647+
xi = yi
648+
y = x
649+
}
650+
// If xi is an interface, use interface unification.
651+
if xi != nil {
652+
return u.nify(xi, y, p)
653+
}
654+
// In all other cases, the type arguments and origins must match.
655+
}
656+
515657
// Check type arguments before origins so they unify
516658
// even if the origins don't match; for better error
517659
// messages (see go.dev/issue/53692).
@@ -525,7 +667,7 @@ func (u *unifier) nify(x, y Type, p *ifacePair) (result bool) {
525667
return false
526668
}
527669
}
528-
return indenticalOrigin(x, y)
670+
return sameOrig
529671
}
530672

531673
case *TypeParam:

src/go/types/api.go

+5
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ type Config struct {
170170
// If DisableUnusedImportCheck is set, packages are not checked
171171
// for unused imports.
172172
DisableUnusedImportCheck bool
173+
174+
// If _EnableInterfaceInference is set, type inference uses
175+
// shared methods for improved type inference involving
176+
// interfaces.
177+
_EnableInterfaceInference bool
173178
}
174179

175180
func srcimporter_setUsesCgo(conf *Config) {

src/go/types/check_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ func testFiles(t *testing.T, filenames []string, srcs [][]byte, manual bool, opt
137137
flags := flag.NewFlagSet("", flag.PanicOnError)
138138
flags.StringVar(&conf.GoVersion, "lang", "", "")
139139
flags.BoolVar(&conf.FakeImportC, "fakeImportC", false, "")
140+
flags.BoolVar(boolFieldAddr(&conf, "_EnableInterfaceInference"), "EnableInterfaceInference", false, "")
140141
if err := parseFlags(srcs[0], flags); err != nil {
141142
t.Fatal(err)
142143
}

src/go/types/generate_test.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,13 @@ var filemap = map[string]action{
137137
"typeterm_test.go": nil,
138138
"typeterm.go": nil,
139139
"under.go": nil,
140-
"unify.go": fixSprintf,
141-
"universe.go": fixGlobalTypVarDecl,
142-
"util_test.go": fixTokenPos,
143-
"validtype.go": nil,
140+
"unify.go": func(f *ast.File) {
141+
fixSprintf(f)
142+
renameIdent(f, "EnableInterfaceInference", "_EnableInterfaceInference")
143+
},
144+
"universe.go": fixGlobalTypVarDecl,
145+
"util_test.go": fixTokenPos,
146+
"validtype.go": nil,
144147
}
145148

146149
// TODO(gri) We should be able to make these rewriters more configurable/composable.

src/go/types/infer.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)