go/types: implement predeclared contract "comparable"

The comparable contract defines a magic method "==" which
enables comparisons with == and != . Comparable types
automagically implement this method.

TODO: If a type is not comparable but a comparable type is
expected, the error message can be confusing (missing ==
method).

Change-Id: Ie0d89b87c36d83549f7d869c18dd9786151adbae
This commit is contained in:
Robert Griesemer 2020-03-11 17:02:27 -07:00
parent e6f000d2f9
commit 10b59aa539
6 changed files with 127 additions and 10 deletions

View File

@ -15,7 +15,9 @@ TODO
----------------------------------------------------------------------------------------------------
KNOWN ISSUES
- 'comparable' predeclared contract is not implemented yet
- parameterized contract embedding is not working correctly
- a non-comparable type used as type argument for a comparable contract leads to sub-optimal error
message (referring to missing method ==)
- iteration over generic variables doesn't report certain channel errors (see TODOs in code)
- cannot handle mutually recursive parameterized interfaces using themselves as type bounds
- contract instantiation requires the type arguments to be type parameters from the type of function

View File

@ -81,3 +81,33 @@ contract _(T) {
Graph /* ERROR not a contract */ (T, T)
( /* ERROR not a contract */ []int)
}
// The predeclared contract "comparable" describes all types
// that support == and != comparisons.
func cmp1(type T comparable)(x, y T) bool {
var _ map[T]int // T is comparable so can use it as map key
return x == y || x != y
}
func _() {
_ = cmp1(int)
_ = cmp1(float32)(1, 2)
_ = cmp1(42, 53)
_ = cmp1(struct{})
_ = cmp1([ /* ERROR does not satisfy */ ]byte)
}
func cmp2(type T1 comparable, T2 comparable)(x1, y1 T1, x2, y2 T2) bool {
return x1 == y1 || x2 != y2
}
contract compareTwo(A, B) {
comparable(A)
comparable(B)
}
// This does not work at the moment due to a contract embedding bug.
// TODO(gri) fix this
// func cmp3(type T1, T2 compareTwo)(x1, y1 T1, x2, y2 T2) bool {
// return x1 == y1 || x2 != y2
// }

View File

@ -341,6 +341,10 @@ func (check *Checker) missingMethod(V Type, T *Interface, static bool) (method,
// we must have a method (not a field of matching function type)
f, _ := obj.(*Func)
if f == nil {
// if m is the magic method == and V is comparable, we're ok
if m.name == "==" && Comparable(V) {
continue
}
return m, nil
}

View File

@ -86,7 +86,10 @@ func Comparable(T Type) bool {
case *Array:
return Comparable(t.elem)
case *TypeParam:
return t.Interface().is(Comparable)
iface := t.Interface()
// If the magic method == exists, the type parameter is comparable.
_, m := lookupMethod(iface.allMethods, nil, "==")
return m != nil || iface.is(Comparable)
}
return false
}

View File

@ -4,16 +4,48 @@
package main
contract anInt(T) {
T int
type C(type T) interface {
m()
}
contract twoInt(K, _) {
anInt(K)
type W(type T) interface {
C(T)
}
func f(type K, V twoInt)()
func _(type T W(T))(x T) {
// x.m() // TODO(gri) this doesn't work at the moment - embedding bug
}
func _ () {
f(int, int)()
}
/*
contract C(T) {
T m()
}
contract W(T) {
C(T)
}
func _(type T W)(x T) {
x.m()
}
*/
/*
contract comparable(T) {
T foo() bool
}
contract compareTwo(A, B) {
comparable(A)
//comparable(B)
}
func _(type T1, T2 compareTwo)(x1, y1 T1) bool {
// return x1 == y1
return x1.foo()
}
func _(type T comparable)(x, y T) bool {
return x == y || x != y
}
*/

View File

@ -188,6 +188,51 @@ func DefPredeclaredTestFuncs() {
def(newBuiltin(_Trace))
}
func defPredeclaredContracts() {
// The "comparable" contract can be envisioned as defined like
//
// contract comparable(T) {
// == (T) untyped bool
// != (T) untyped bool
// }
//
// == and != cannot be user-declared but we can declare
// a magic method == and check for its presence when needed.
// (A simpler approach that simply looks for a magic type
// bound interface is problematic: comparable might be embedded,
// which in turn leads to the embedding of the magic type bound
// interface and then we cannot easily look for that interface
// anymore.)
// Define interface { ==() }. We don't care about the signature
// for == so leave it empty except for the receiver, which is
// set up later to match the usual interface method assumptions.
sig := new(Signature)
eql := NewFunc(token.NoPos, nil, "==", sig)
iface := NewInterfaceType([]*Func{eql}, nil).Complete()
// The interface is parameterized with a single
// type parameter to match the comparable contract.
pname := NewTypeName(token.NoPos, nil, "T", nil)
pname.typ = &TypeParam{0, pname, 0, &emptyInterface}
// The type bound interface needs a name so we can attach the
// type parameter and to match the usual set up of contracts.
iname := NewTypeName(token.NoPos, nil, "comparable_bound", nil)
named := NewNamed(iname, iface, nil)
named.tparams = []*TypeName{pname}
sig.recv = NewVar(token.NoPos, nil, "", named) // complete == signature
// set up the contract
obj := NewContract(token.NoPos, nil, "comparable")
obj.typ = new(contractType) // mark contract as fully set up
obj.color_ = black
obj.TParams = named.tparams
obj.Bounds = []*Named{named}
def(obj)
}
func init() {
Universe = NewScope(nil, token.NoPos, token.NoPos, "universe")
Unsafe = NewPackage("unsafe", "unsafe")
@ -197,6 +242,7 @@ func init() {
defPredeclaredConsts()
defPredeclaredNil()
defPredeclaredFuncs()
defPredeclaredContracts()
universeIota = Universe.Lookup("iota").(*Const)
universeByte = Universe.Lookup("byte").(*TypeName).typ.(*Basic)