[dev.typeparams] go/types: use type terms to represent unions

This is a straightforward port of CL 338092 to go/types.

Change-Id: I414ec0ad95648c201e85fd2b4f494b1206c658e7
Reviewed-on: https://go-review.googlesource.com/c/go/+/339674
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Griesemer <gri@golang.org>
This commit is contained in:
Rob Findley 2021-08-03 21:10:42 -04:00 committed by Robert Findley
parent 880ab6209e
commit ed3667d079
11 changed files with 130 additions and 79 deletions

View file

@ -303,7 +303,7 @@ func (w *tpWalker) isParameterized(typ Type) (res bool) {
} }
case *Union: case *Union:
return w.isParameterizedList(t.types) return w.isParameterizedTermList(t.terms)
case *Signature: case *Signature:
// t.tparams may not be nil if we are looking at a signature // t.tparams may not be nil if we are looking at a signature
@ -331,7 +331,7 @@ func (w *tpWalker) isParameterized(typ Type) (res bool) {
return w.isParameterized(t.elem) return w.isParameterized(t.elem)
case *Named: case *Named:
return w.isParameterizedList(t.targs) return w.isParameterizedTypeList(t.targs)
case *TypeParam: case *TypeParam:
// t must be one of w.tparams // t must be one of w.tparams
@ -344,7 +344,7 @@ func (w *tpWalker) isParameterized(typ Type) (res bool) {
return false return false
} }
func (w *tpWalker) isParameterizedList(list []Type) bool { func (w *tpWalker) isParameterizedTypeList(list []Type) bool {
for _, t := range list { for _, t := range list {
if w.isParameterized(t) { if w.isParameterized(t) {
return true return true
@ -353,6 +353,15 @@ func (w *tpWalker) isParameterizedList(list []Type) bool {
return false return false
} }
func (w *tpWalker) isParameterizedTermList(list []*term) bool {
for _, t := range list {
if w.isParameterized(t.typ) {
return true
}
}
return false
}
// inferB returns the list of actual type arguments inferred from the type parameters' // inferB returns the list of actual type arguments inferred from the type parameters'
// bounds and an initial set of type arguments. If type inference is impossible because // bounds and an initial set of type arguments. If type inference is impossible because
// unification fails, an error is reported if report is set to true, the resulting types // unification fails, an error is reported if report is set to true, the resulting types
@ -461,7 +470,8 @@ func (check *Checker) structuralType(constraint Type) Type {
if u, _ := types.(*Union); u != nil { if u, _ := types.(*Union); u != nil {
if u.NumTerms() == 1 { if u.NumTerms() == 1 {
// TODO(gri) do we need to respect tilde? // TODO(gri) do we need to respect tilde?
return u.types[0] t, _ := u.Term(0)
return t
} }
return nil return nil
} }

View file

@ -34,7 +34,7 @@ func (t *Interface) is(f func(Type, bool) bool) bool {
// TODO(gri) should settle on top or nil to represent this case // TODO(gri) should settle on top or nil to represent this case
return false // we must have at least one type! (was bug) return false // we must have at least one type! (was bug)
case *Union: case *Union:
return t.is(func(typ Type, tilde bool) bool { return f(typ, tilde) }) return t.is(func(t *term) bool { return f(t.typ, t.tilde) })
default: default:
return f(t, false) return f(t, false)
} }
@ -266,8 +266,8 @@ func (check *Checker) interfaceType(ityp *Interface, iface *ast.InterfaceType, d
// (don't sort embeddeds: they must correspond to *embedPos entries) // (don't sort embeddeds: they must correspond to *embedPos entries)
// Compute type set with a non-nil *Checker as soon as possible // Compute type set with a non-nil *Checker as soon as possible
// to report any errors. Subsequent uses of type sets should be // to report any errors. Subsequent uses of type sets will use
// using this computed type set and won't need to pass in a *Checker. // this computed type set and won't need to pass in a *Checker.
check.later(func() { computeTypeSet(check, iface.Pos(), ityp) }) check.later(func() { computeTypeSet(check, iface.Pos(), ityp) })
} }

View file

@ -255,13 +255,13 @@ func (x *operand) assignableTo(check *Checker, T Type, reason *string) (bool, er
// x is an untyped value representable by a value of type T. // x is an untyped value representable by a value of type T.
if isUntyped(Vu) { if isUntyped(Vu) {
if t, ok := Tu.(*Union); ok { if t, ok := Tu.(*Union); ok {
return t.is(func(t Type, tilde bool) bool { return t.is(func(t *term) bool {
// TODO(gri) this could probably be more efficient // TODO(gri) this could probably be more efficient
if tilde { if t.tilde {
// TODO(gri) We need to check assignability // TODO(gri) We need to check assignability
// for the underlying type of x. // for the underlying type of x.
} }
ok, _ := x.assignableTo(check, t, reason) ok, _ := x.assignableTo(check, t.typ, reason)
return ok return ok
}), _IncompatibleAssign }), _IncompatibleAssign
} }

View file

@ -238,20 +238,8 @@ func identical(x, y Type, cmpTags bool, p *ifacePair) bool {
// types - each type appears exactly once. Thus, two union types // types - each type appears exactly once. Thus, two union types
// must contain the same number of types to have chance of // must contain the same number of types to have chance of
// being equal. // being equal.
if y, ok := y.(*Union); ok && x.NumTerms() == y.NumTerms() { if y, ok := y.(*Union); ok {
// Every type in x.types must be in y.types. return identicalTerms(x.terms, y.terms)
// Quadratic algorithm, but probably good enough for now.
// TODO(gri) we need a fast quick type ID/hash for all types.
L:
for i, xt := range x.types {
for j, yt := range y.types {
if Identical(xt, yt) && x.tilde[i] == y.tilde[j] {
continue L // x is in y.types
}
}
return false // x is not in y.types
}
return true
} }
case *Interface: case *Interface:

View file

@ -26,12 +26,13 @@ func TestSizeof(t *testing.T) {
{Pointer{}, 8, 16}, {Pointer{}, 8, 16},
{Tuple{}, 12, 24}, {Tuple{}, 12, 24},
{Signature{}, 28, 56}, {Signature{}, 28, 56},
{Union{}, 24, 48}, {Union{}, 12, 24},
{Interface{}, 40, 80}, {Interface{}, 40, 80},
{Map{}, 16, 32}, {Map{}, 16, 32},
{Chan{}, 12, 24}, {Chan{}, 12, 24},
{Named{}, 80, 152}, {Named{}, 80, 152},
{TypeParam{}, 28, 48}, {TypeParam{}, 28, 48},
{term{}, 12, 24},
{top{}, 0, 0}, {top{}, 0, 0},
// Objects // Objects

View file

@ -148,12 +148,12 @@ func (subst *subster) typ(typ Type) Type {
} }
case *Union: case *Union:
types, copied := subst.typeList(t.types) terms, copied := subst.termList(t.terms)
if copied { if copied {
// TODO(gri) Remove duplicates that may have crept in after substitution // TODO(gri) Remove duplicates that may have crept in after substitution
// (unlikely but possible). This matters for the Identical // (unlikely but possible). This matters for the Identical
// predicate on unions. // predicate on unions.
return newUnion(types, t.tilde) return &Union{terms}
} }
case *Interface: case *Interface:
@ -393,3 +393,21 @@ func (subst *subster) typeList(in []Type) (out []Type, copied bool) {
} }
return return
} }
func (subst *subster) termList(in []*term) (out []*term, copied bool) {
out = in
for i, t := range in {
if u := subst.typ(t.typ); u != t.typ {
if !copied {
// first function that got substituted => allocate new out slice
// and copy all functions
new := make([]*term, len(in))
copy(new, out)
out = new
copied = true
}
out[i] = &term{t.tilde, u}
}
}
return
}

View file

@ -31,9 +31,9 @@ type (
_ interface{int|~ /* ERROR duplicate term int */ int } _ interface{int|~ /* ERROR duplicate term int */ int }
_ interface{~int|~ /* ERROR duplicate term int */ int } _ interface{~int|~ /* ERROR duplicate term int */ int }
// For now we do not permit interfaces with ~ or in unions. // For now we do not permit interfaces with methods in unions.
_ interface{~ /* ERROR cannot use interface */ interface{}} _ interface{~ /* ERROR invalid use of ~ */ interface{}}
_ interface{int|interface /* ERROR cannot use interface */ {}} _ interface{int|interface /* ERROR cannot use .* in union */ { m() }}
) )
type ( type (

View file

@ -60,7 +60,7 @@ func optype(typ Type) Type {
// If we have a union with a single entry, ignore // If we have a union with a single entry, ignore
// any tilde because under(~t) == under(t). // any tilde because under(~t) == under(t).
if u, _ := a.(*Union); u != nil && u.NumTerms() == 1 { if u, _ := a.(*Union); u != nil && u.NumTerms() == 1 {
a = u.types[0] a, _ = u.Term(0)
} }
if a != typ { if a != typ {
// a != typ and a is a type parameter => under(a) != typ, so this is ok // a != typ and a is a type parameter => under(a) != typ, so this is ok

View file

@ -43,6 +43,13 @@ func (s *TypeSet) IsComparable() bool {
return s.comparable && tcomparable return s.comparable && tcomparable
} }
// TODO(gri) IsTypeSet is not a great name. Find a better one.
// IsTypeSet reports whether the type set s is represented by a finite set of underlying types.
func (s *TypeSet) IsTypeSet() bool {
return !s.comparable && len(s.methods) == 0
}
// NumMethods returns the number of methods available. // NumMethods returns the number of methods available.
func (s *TypeSet) NumMethods() int { return len(s.methods) } func (s *TypeSet) NumMethods() int { return len(s.methods) }

View file

@ -163,14 +163,14 @@ func writeType(buf *bytes.Buffer, typ Type, qf Qualifier, visited []Type) {
buf.WriteString("⊥") buf.WriteString("⊥")
break break
} }
for i, e := range t.types { for i, t := range t.terms {
if i > 0 { if i > 0 {
buf.WriteByte('|') buf.WriteByte('|')
} }
if t.tilde[i] { if t.tilde {
buf.WriteByte('~') buf.WriteByte('~')
} }
writeType(buf, e, qf, visited) writeType(buf, t.typ, qf, visited)
} }
case *Interface: case *Interface:

View file

@ -13,10 +13,8 @@ import (
// API // API
// A Union represents a union of terms. // A Union represents a union of terms.
// A term is a type with a ~ (tilde) flag.
type Union struct { type Union struct {
types []Type // types are unique terms []*term
tilde []bool // if tilde[i] is set, terms[i] is of the form ~T
} }
// NewUnion returns a new Union type with the given terms (types[i], tilde[i]). // NewUnion returns a new Union type with the given terms (types[i], tilde[i]).
@ -24,9 +22,9 @@ type Union struct {
// of no types. // of no types.
func NewUnion(types []Type, tilde []bool) *Union { return newUnion(types, tilde) } func NewUnion(types []Type, tilde []bool) *Union { return newUnion(types, tilde) }
func (u *Union) IsEmpty() bool { return len(u.types) == 0 } func (u *Union) IsEmpty() bool { return len(u.terms) == 0 }
func (u *Union) NumTerms() int { return len(u.types) } func (u *Union) NumTerms() int { return len(u.terms) }
func (u *Union) Term(i int) (Type, bool) { return u.types[i], u.tilde[i] } func (u *Union) Term(i int) (Type, bool) { t := u.terms[i]; return t.typ, t.tilde }
func (u *Union) Underlying() Type { return u } func (u *Union) Underlying() Type { return u }
func (u *Union) String() string { return TypeString(u, nil) } func (u *Union) String() string { return TypeString(u, nil) }
@ -42,18 +40,20 @@ func newUnion(types []Type, tilde []bool) *Union {
return emptyUnion return emptyUnion
} }
t := new(Union) t := new(Union)
t.types = types t.terms = make([]*term, len(types))
t.tilde = tilde for i, typ := range types {
t.terms[i] = &term{tilde[i], typ}
}
return t return t
} }
// is reports whether f returned true for all terms (type, tilde) of u. // is reports whether f returns true for all terms of u.
func (u *Union) is(f func(Type, bool) bool) bool { func (u *Union) is(f func(*term) bool) bool {
if u.IsEmpty() { if u.IsEmpty() {
return false return false
} }
for i, t := range u.types { for _, t := range u.terms {
if !f(t, u.tilde[i]) { if !f(t) {
return false return false
} }
} }
@ -65,8 +65,8 @@ func (u *Union) underIs(f func(Type) bool) bool {
if u.IsEmpty() { if u.IsEmpty() {
return false return false
} }
for _, t := range u.types { for _, t := range u.terms {
if !f(under(t)) { if !f(under(t.typ)) {
return false return false
} }
} }
@ -86,7 +86,7 @@ func parseUnion(check *Checker, tlist []ast.Expr) Type {
} }
// Ensure that each type is only present once in the type list. // Ensure that each type is only present once in the type list.
// It's ok to do this check at the end because it's not a requirement // It's ok to do this check later because it's not a requirement
// for correctness of the code. // for correctness of the code.
// Note: This is a quadratic algorithm, but unions tend to be short. // Note: This is a quadratic algorithm, but unions tend to be short.
check.later(func() { check.later(func() {
@ -99,7 +99,7 @@ func parseUnion(check *Checker, tlist []ast.Expr) Type {
x := tlist[i] x := tlist[i]
pos := x.Pos() pos := x.Pos()
// We may not know the position of x if it was a typechecker- // We may not know the position of x if it was a typechecker-
// introduced ~T type of a type list entry T. Use the position // introduced ~T term for a type list entry T. Use the position
// of T instead. // of T instead.
// TODO(rfindley) remove this test once we don't support type lists anymore // TODO(rfindley) remove this test once we don't support type lists anymore
if !pos.IsValid() { if !pos.IsValid() {
@ -109,13 +109,24 @@ func parseUnion(check *Checker, tlist []ast.Expr) Type {
} }
u := under(t) u := under(t)
if tilde[i] && !Identical(u, t) { f, _ := u.(*Interface)
check.errorf(x, _Todo, "invalid use of ~ (underlying type of %s is %s)", t, u) if tilde[i] {
continue // don't report another error for t if f != nil {
check.errorf(x, _Todo, "invalid use of ~ (%s is an interface)", t)
continue // don't report another error for t
}
if !Identical(u, t) {
check.errorf(x, _Todo, "invalid use of ~ (underlying type of %s is %s)", t, u)
continue // don't report another error for t
}
} }
if _, ok := u.(*Interface); ok {
// A single type with a ~ is a single-term union. // Stand-alone embedded interfaces are ok and are handled by the single-type case
check.errorf(atPos(pos), _Todo, "cannot use interface %s with ~ or inside a union (implementation restriction)", t) // in the beginning. Embedded interfaces with tilde are excluded above. If we reach
// here, we must have at least two terms in the union.
if f != nil && !f.typeSet().IsTypeSet() {
check.errorf(atPos(pos), _Todo, "cannot use %s in union (interface contains methods)", t)
continue // don't report another error for t continue // don't report another error for t
} }
@ -167,25 +178,7 @@ func intersect(x, y Type) (r Type) {
yu, _ := y.(*Union) yu, _ := y.(*Union)
switch { switch {
case xu != nil && yu != nil: case xu != nil && yu != nil:
// Quadratic algorithm, but good enough for now. return &Union{intersectTerms(xu.terms, yu.terms)}
// TODO(gri) fix asymptotic performance
var types []Type
var tilde []bool
for j, y := range yu.types {
yt := yu.tilde[j]
if r, rt := xu.intersect(y, yt); r != nil {
// Terms x[i] and y[j] match: Select the one that
// is not a ~t because that is the intersection
// type. If both are ~t, they are identical:
// T ∩ T = T
// T ∩ ~t = T
// ~t ∩ T = T
// ~t ∩ ~t = ~t
types = append(types, r)
tilde = append(tilde, rt)
}
}
return newUnion(types, tilde)
case xu != nil: case xu != nil:
if r, _ := xu.intersect(y, false); r != nil { if r, _ := xu.intersect(y, false); r != nil {
@ -219,14 +212,16 @@ func includes(list []Type, typ Type) bool {
// intersect computes the intersection of the union u and term (y, yt) // intersect computes the intersection of the union u and term (y, yt)
// and returns the intersection term, if any. Otherwise the result is // and returns the intersection term, if any. Otherwise the result is
// (nil, false). // (nil, false).
// TODO(gri) this needs to cleaned up/removed once we switch to lazy
// union type set computation.
func (u *Union) intersect(y Type, yt bool) (Type, bool) { func (u *Union) intersect(y Type, yt bool) (Type, bool) {
under_y := under(y) under_y := under(y)
for i, x := range u.types { for _, x := range u.terms {
xt := u.tilde[i] xt := x.tilde
// determine which types xx, yy to compare // determine which types xx, yy to compare
xx := x xx := x.typ
if yt { if yt {
xx = under(x) xx = under(xx)
} }
yy := y yy := y
if xt { if xt {
@ -242,3 +237,35 @@ func (u *Union) intersect(y Type, yt bool) (Type, bool) {
} }
return nil, false return nil, false
} }
func identicalTerms(list1, list2 []*term) bool {
if len(list1) != len(list2) {
return false
}
// Every term in list1 must be in list2.
// Quadratic algorithm, but probably good enough for now.
// TODO(gri) we need a fast quick type ID/hash for all types.
L:
for _, x := range list1 {
for _, y := range list2 {
if x.equal(y) {
continue L // x is in list2
}
}
return false
}
return true
}
func intersectTerms(list1, list2 []*term) (list []*term) {
// Quadratic algorithm, but good enough for now.
// TODO(gri) fix asymptotic performance
for _, x := range list1 {
for _, y := range list2 {
if r := x.intersect(y); r != nil {
list = append(list, r)
}
}
}
return
}