Skip to content

Commit

Permalink
internal/typeparams: add a generic form of types.AssignableTo
Browse files Browse the repository at this point in the history
For some use-cases, it is helpful to compute predicates between
uninstantiated generic types.

This CL implements one such predicate for types.AssignableTo, a helper
GenericAssignableTo which reports if, for generic types V and T with the
same number of type parameters, all instantiations V[A_1, ..., A_N] are
assignable to the corresponding instantiation T[A_1, ..., A_N].

For #50887
For #50447

Change-Id: I7a0550fba05666bb2375d478d5390a123e09f556
Reviewed-on: https://go-review.googlesource.com/c/tools/+/383094
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Robert Griesemer <gri@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
  • Loading branch information
findleyr committed Feb 8, 2022
1 parent 6f0b9da commit d15fe1d
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 0 deletions.
69 changes: 69 additions & 0 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,72 @@ func OriginMethod(fn *types.Func) *types.Func {
gfn, _, _ := types.LookupFieldOrMethod(orig, true, fn.Pkg(), fn.Name())
return gfn.(*types.Func)
}

// GenericAssignableTo is a generalization of types.AssignableTo that
// implements the following rule for uninstantiated generic types:
//
// If V and T are generic named types, then V is considered assignable to T if,
// for every possible instantation of V[A_1, ..., A_N], the instantiation
// T[A_1, ..., A_N] is valid and V[A_1, ..., A_N] implements T[A_1, ..., A_N].
//
// If T has structural constraints, they must be satisfied by V.
//
// For example, consider the following type declarations:
//
// type Interface[T any] interface {
// Accept(T)
// }
//
// type Container[T any] struct {
// Element T
// }
//
// func (c Container[T]) Accept(t T) { c.Element = t }
//
// In this case, GenericAssignableTo reports that instantiations of Container
// are assignable to the corresponding instantiation of Interface.
func GenericAssignableTo(ctxt *Context, V, T types.Type) bool {
// If V and T are not both named, or do not have matching non-empty type
// parameter lists, fall back on types.AssignableTo.

VN, Vnamed := V.(*types.Named)
TN, Tnamed := T.(*types.Named)
if !Vnamed || !Tnamed {
return types.AssignableTo(V, T)
}

vtparams := ForNamed(VN)
ttparams := ForNamed(TN)
if vtparams.Len() == 0 || vtparams.Len() != ttparams.Len() || NamedTypeArgs(VN).Len() != 0 || NamedTypeArgs(TN).Len() != 0 {
return types.AssignableTo(V, T)
}

// V and T have the same (non-zero) number of type params. Instantiate both
// with the type parameters of V. This must always succeed for V, and will
// succeed for T if and only if the type set of each type parameter of V is a
// subset of the type set of the corresponding type parameter of T, meaning
// that every instantiation of V corresponds to a valid instantiation of T.

// Minor optimization: ensure we share a context across the two
// instantiations below.
if ctxt == nil {
ctxt = NewContext()
}

var targs []types.Type
for i := 0; i < vtparams.Len(); i++ {
targs = append(targs, vtparams.At(i))
}

vinst, err := Instantiate(ctxt, V, targs, true)
if err != nil {
panic("type parameters should satisfy their own constraints")
}

tinst, err := Instantiate(ctxt, T, targs, true)
if err != nil {
return false
}

return types.AssignableTo(vinst, tinst)
}
73 changes: 73 additions & 0 deletions common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,76 @@ func TestOriginMethodUses(t *testing.T) {
})
}
}

func TestGenericAssignableTo(t *testing.T) {
testenv.NeedsGo1Point(t, 18)

tests := []struct {
src string
want bool
}{
// The inciting issue: golang/go#50887.
{`
type T[P any] interface {
Accept(P)
}
type V[Q any] struct {
Element Q
}
func (c V[Q]) Accept(q Q) { c.Element = q }
`, true},

// Various permutations on constraints and signatures.
{`type T[P ~int] interface{ A(P) }; type V[Q int] int; func (V[Q]) A(Q) {}`, true},
{`type T[P int] interface{ A(P) }; type V[Q ~int] int; func (V[Q]) A(Q) {}`, false},
{`type T[P int|string] interface{ A(P) }; type V[Q int] int; func (V[Q]) A(Q) {}`, true},
{`type T[P any] interface{ A(P) }; type V[Q any] int; func (V[Q]) A(Q, Q) {}`, false},
{`type T[P any] interface{ int; A(P) }; type V[Q any] int; func (V[Q]) A(Q) {}`, false},

// Various structural restrictions on T.
{`type T[P any] interface{ ~int; A(P) }; type V[Q any] int; func (V[Q]) A(Q) {}`, true},
{`type T[P any] interface{ ~int|string; A(P) }; type V[Q any] int; func (V[Q]) A(Q) {}`, true},
{`type T[P any] interface{ int; A(P) }; type V[Q int] int; func (V[Q]) A(Q) {}`, false},

// Various recursive constraints.
{`type T[P ~struct{ f *P }] interface{ A(P) }; type V[Q ~struct{ f *Q }] int; func (V[Q]) A(Q) {}`, true},
{`type T[P ~struct{ f *P }] interface{ A(P) }; type V[Q ~struct{ g *Q }] int; func (V[Q]) A(Q) {}`, false},
{`type T[P ~*X, X any] interface{ A(P) X }; type V[Q ~*Y, Y any] int; func (V[Q, Y]) A(Q) (y Y) { return }`, true},
{`type T[P ~*X, X any] interface{ A(P) X }; type V[Q ~**Y, Y any] int; func (V[Q, Y]) A(Q) (y Y) { return }`, false},
{`type T[P, X any] interface{ A(P) X }; type V[Q ~*Y, Y any] int; func (V[Q, Y]) A(Q) (y Y) { return }`, true},
{`type T[P ~*X, X any] interface{ A(P) X }; type V[Q, Y any] int; func (V[Q, Y]) A(Q) (y Y) { return }`, false},
{`type T[P, X any] interface{ A(P) X }; type V[Q, Y any] int; func (V[Q, Y]) A(Q) (y Y) { return }`, true},

// In this test case, we reverse the type parameters in the signature of V.A
{`type T[P, X any] interface{ A(P) X }; type V[Q, Y any] int; func (V[Q, Y]) A(Y) (y Q) { return }`, false},
// It would be nice to return true here: V can only be instantiated with
// [int, int], so the identity of the type parameters should not matter.
{`type T[P, X any] interface{ A(P) X }; type V[Q, Y int] int; func (V[Q, Y]) A(Y) (y Q) { return }`, false},
}

for _, test := range tests {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "p.go", "package p; "+test.src, 0)
if err != nil {
t.Fatalf("%s:\n%v", test.src, err)
}
var conf types.Config
pkg, err := conf.Check("p", fset, []*ast.File{f}, nil)
if err != nil {
t.Fatalf("%s:\n%v", test.src, err)
}

V := pkg.Scope().Lookup("V").Type()
T := pkg.Scope().Lookup("T").Type()

if types.AssignableTo(V, T) {
t.Fatal("AssignableTo")
}

if got := GenericAssignableTo(nil, V, T); got != test.want {
t.Fatalf("%s:\nGenericAssignableTo(%v, %v) = %v, want %v", test.src, V, T, got, test.want)
}
}
}
5 changes: 5 additions & 0 deletions typeparams_go117.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ func GetInstances(info *types.Info) map[*ast.Ident]Instance { return nil }
// this Go version.
type Context struct{}

// NewContext returns a placeholder Context instance.
func NewContext() *Context {
return &Context{}
}

// Instantiate is unsupported on this Go version, and panics.
func Instantiate(ctxt *Context, typ types.Type, targs []types.Type, validate bool) (types.Type, error) {
unsupported()
Expand Down
5 changes: 5 additions & 0 deletions typeparams_go118.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ func GetInstances(info *types.Info) map[*ast.Ident]Instance {
// Context is an alias for types.Context.
type Context = types.Context

// NewContext calls types.NewContext.
func NewContext() *Context {
return types.NewContext()
}

// Instantiate calls types.Instantiate.
func Instantiate(ctxt *Context, typ types.Type, targs []types.Type, validate bool) (types.Type, error) {
return types.Instantiate(ctxt, typ, targs, validate)
Expand Down

0 comments on commit d15fe1d

Please sign in to comment.