Implement TypesIntersect(a, b *types.Type) bool (#3223)

* Implement `TypesIntersect(a, b *types.Type) bool`

fixes: #3203

* Respond to review:
- Require at least one key type to match for map intersction
- Do parallel scan of field when comparing struct
- Add more tests for unions and nested collections

* Rename to ContainCommonSupertype
This commit is contained in:
Eric Halpern
2017-02-24 11:23:47 -08:00
committed by GitHub
parent 1f25ceced8
commit a355d619fc
2 changed files with 264 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
package types
import "github.com/attic-labs/noms/go/d"
// ContainCommonSupertype returns true if it's possible to synthesize
// a non-trivial (i.e. not empty) supertype from types |a| and |b|.
//
// It is useful for determining whether a subset of values can be extracted
// from one object to produce another object.
//
// The rules for determining whether |a| and |b| intersect are:
// - if either type is Value, return true
// - if either type is Union, return true iff at least one variant of |a| intersects with one variant of |b|
// - if |a| & |b| are not the same kind, return false
// - else
// - if both are structs, return true iff their names are equal or one name is "", they share a field name
// and the type of that field intersects
// - if both are refs, sets or lists, return true iff the element type intersects
// - if both are maps, return true iff they have a key with the same type and value types that intersect
// - else return true
func ContainCommonSupertype(a, b *Type) bool {
// Avoid cycles internally.
return containCommonSupertypeImpl(ToUnresolvedType(a), ToUnresolvedType(b))
}
func containCommonSupertypeImpl(a, b *Type) bool {
if a.Kind() == ValueKind || b.Kind() == ValueKind {
return true
}
if a.Kind() == UnionKind || b.Kind() == UnionKind {
return unionsIntersect(a, b)
}
if a.Kind() != b.Kind() {
return false
}
switch k := a.Kind(); k {
case StructKind:
return structsIntersect(a, b)
case ListKind, SetKind, RefKind:
return containersIntersect(k, a, b)
case MapKind:
return mapsIntersect(a, b)
default:
return true
}
}
// Checks for intersection between types that may be unions. If either or
// both is a union, union, tests all types for intersection.
func unionsIntersect(a, b *Type) bool {
aTypes, bTypes := typeList(a), typeList(b)
for _, t := range aTypes {
for _, u := range bTypes {
if containCommonSupertypeImpl(t, u) {
return true
}
}
}
return false
}
// if |t| is a union, returns all types represented; otherwise returns |t|
func typeList(t *Type) typeSlice {
if t.Desc.Kind() == UnionKind {
return t.Desc.(CompoundDesc).ElemTypes
}
return typeSlice{t}
}
func containersIntersect(kind NomsKind, a, b *Type) bool {
d.Chk.True(kind == a.Desc.Kind() && kind == b.Desc.Kind())
return containCommonSupertypeImpl(a.Desc.(CompoundDesc).ElemTypes[0], b.Desc.(CompoundDesc).ElemTypes[0])
}
func mapsIntersect(a, b *Type) bool {
// true if a and b are the same or (if either is a union) there is
// common type between them.
hasCommonType := func(a, b *Type) bool {
aTypes, bTypes := typeList(a), typeList(b)
for _, t := range aTypes {
for _, u := range bTypes {
if t == u {
return true
}
}
}
return false
}
d.Chk.True(MapKind == a.Desc.Kind() && MapKind == b.Desc.Kind())
aDesc, bDesc := a.Desc.(CompoundDesc), b.Desc.(CompoundDesc)
if !hasCommonType(aDesc.ElemTypes[0], bDesc.ElemTypes[0]) {
return false
}
return containCommonSupertypeImpl(aDesc.ElemTypes[1], bDesc.ElemTypes[1])
}
func structsIntersect(a, b *Type) bool {
d.Chk.True(StructKind == a.Kind() && StructKind == b.Kind())
aDesc := a.Desc.(StructDesc)
bDesc := b.Desc.(StructDesc)
// must be either the same name or one has no name
if aDesc.Name != bDesc.Name && !(aDesc.Name == "" || bDesc.Name == "") {
return false
}
for i, j := 0, 0; i < len(aDesc.fields) && j < len(bDesc.fields); {
aName, bName := aDesc.fields[i].name, bDesc.fields[j].name
if aName < bName {
i++
} else if bName < aName {
j++
} else if !containCommonSupertypeImpl(aDesc.fields[i].t, bDesc.fields[j].t) {
i++
j++
} else {
return true
}
}
return false
}

View File

@@ -0,0 +1,142 @@
package types
import (
"testing"
"github.com/attic-labs/testify/assert"
)
func TestContainCommonSupertype(t *testing.T) {
cases := []struct {
a, b *Type
out bool
}{
// bool & any -> true
{ValueType, StringType, true},
// ref<bool> & ref<bool> -> true
{MakeRefType(BoolType), MakeRefType(BoolType), true},
// ref<number> & ref<string> -> false
{MakeRefType(NumberType), MakeRefType(StringType), false},
// set<bool> & set<bool> -> true
{MakeSetType(BoolType), MakeSetType(BoolType), true},
// set<bool> & set<string> -> false
{MakeSetType(BoolType), MakeSetType(StringType), false},
// list<blob> & list<blob> -> true
{MakeListType(BlobType), MakeListType(BlobType), true},
// list<blob> & list<string> -> false
{MakeListType(BlobType), MakeListType(StringType), false},
// list<blob|string|number> & list<string|bool> -> true
{MakeListType(MakeUnionType(BlobType, StringType, NumberType)), MakeListType(MakeUnionType(StringType, BoolType)), true},
// list<blob|string> & list<number|bool> -> false
{MakeListType(MakeUnionType(BlobType, StringType)), MakeListType(MakeUnionType(NumberType, BoolType)), false},
// map<bool,bool> & map<bool,bool> -> true
{MakeMapType(BoolType, BoolType), MakeMapType(BoolType, BoolType), true},
// map<bool,bool> & map<bool,string> -> false
{MakeMapType(BoolType, BoolType), MakeMapType(BoolType, StringType), false},
// map<bool,bool> & map<string,bool> -> false
{MakeMapType(BoolType, BoolType), MakeMapType(StringType, BoolType), false},
// map<bool,bool> & map<string,bool> -> false
{MakeMapType(BoolType, BoolType), MakeMapType(StringType, BoolType), false},
// map<struct{foo:string},bool> & map<struct{foo:string,bar:string},bool> -> false
{MakeMapType(MakeStructTypeFromFields("", FieldMap{"foo": StringType}), BoolType),
MakeMapType(MakeStructTypeFromFields("", FieldMap{"foo": StringType, "bar": StringType}), BoolType), false},
// map<string|blob,string> & map<number|string,string> -> true
{MakeMapType(MakeUnionType(StringType, BlobType), StringType),
MakeMapType(MakeUnionType(NumberType, StringType), StringType), true},
// map<blob|bool,string> & map<number|string,string> -> false
{MakeMapType(MakeUnionType(BlobType, BoolType), StringType),
MakeMapType(MakeUnionType(NumberType, StringType), StringType), false},
// bool & string|bool|blob -> true
{BoolType, MakeUnionType(StringType, BoolType, BlobType), true},
// string|bool|blob & blob -> true
{MakeUnionType(StringType, BoolType, BlobType), BlobType, true},
// string|bool|blob & number|blob|string -> true
{MakeUnionType(StringType, BoolType, BlobType), MakeUnionType(NumberType, BlobType, StringType), true},
// struct{foo:bool} & struct{foo:bool} -> true
{MakeStructTypeFromFields("", FieldMap{"foo": BoolType}),
MakeStructTypeFromFields("", FieldMap{"foo": BoolType}), true},
// struct{foo:bool} & struct{foo:number} -> false
{MakeStructTypeFromFields("", FieldMap{"foo": BoolType}),
MakeStructTypeFromFields("", FieldMap{"foo": StringType}), false},
// struct{foo:bool} & struct{foo:bool,bar:number} -> true
{MakeStructTypeFromFields("", FieldMap{"foo": BoolType}),
MakeStructTypeFromFields("", FieldMap{"foo": BoolType, "bar": NumberType}), true},
// struct{foo:ref<bool>} & struct{foo:ref<number>} -> false
{MakeStructTypeFromFields("", FieldMap{"foo": MakeRefType(BoolType)}),
MakeStructTypeFromFields("", FieldMap{"foo": MakeRefType(NumberType)}), false},
// struct{foo:ref<bool>} & struct{foo:ref<number|bool>} -> true
{MakeStructTypeFromFields("", FieldMap{"foo": MakeRefType(BoolType)}),
MakeStructTypeFromFields("", FieldMap{"foo": MakeRefType(MakeUnionType(NumberType, BoolType))}), true},
// struct A{foo:bool} & struct A{foo:bool, baz:string} -> true
{MakeStructTypeFromFields("A", FieldMap{"foo": BoolType}),
MakeStructTypeFromFields("A", FieldMap{"foo": BoolType, "baz": StringType}), true},
// struct A{foo:bool, stuff:set<String|Blob>} & struct A{foo:bool, stuff:set<String>} -> true
{MakeStructTypeFromFields("A", FieldMap{"foo": BoolType, "stuff": MakeSetType(MakeUnionType(StringType, BlobType))}),
MakeStructTypeFromFields("A", FieldMap{"foo": BoolType, "stuff": MakeSetType(StringType)}), true},
// struct A{stuff:set<String|Blob>} & struct A{foo:bool, stuff:set<Number>} -> false
{MakeStructTypeFromFields("A", FieldMap{"foo": BoolType, "stuff": MakeSetType(MakeUnionType(StringType, BlobType))}),
MakeStructTypeFromFields("A", FieldMap{"stuff": MakeSetType(NumberType)}), false},
// struct A{foo:bool} & struct {foo:bool} -> true
{MakeStructTypeFromFields("A", FieldMap{"foo": BoolType}),
MakeStructTypeFromFields("", FieldMap{"foo": BoolType}), true},
// struct {foo:bool} & struct A{foo:bool} -> false
{MakeStructTypeFromFields("", FieldMap{"foo": BoolType}),
MakeStructTypeFromFields("A", FieldMap{"foo": BoolType}), true},
// struct A{foo:bool} & struct B{foo:bool} -> false
{MakeStructTypeFromFields("A", FieldMap{"foo": BoolType}),
MakeStructTypeFromFields("B", FieldMap{"foo": BoolType}), false},
// map<string, struct A{foo:string}> & map<string, struct A{foo:string, bar:bool}> -> true
{MakeMapType(StringType, MakeStructTypeFromFields("A", FieldMap{"foo": StringType})),
MakeMapType(StringType, MakeStructTypeFromFields("A", FieldMap{"foo": StringType, "bar": BoolType})), true},
// struct{foo: string} & struct{foo: string|blob} -> true
{MakeStructTypeFromFields("", FieldMap{"foo": StringType}),
MakeStructTypeFromFields("", FieldMap{"foo": MakeUnionType(StringType, BlobType)}), true},
// struct{foo: string}|struct{foo: blob} & struct{foo: string|blob} -> true
{MakeUnionType(
MakeStructTypeFromFields("", FieldMap{"foo": StringType}),
MakeStructTypeFromFields("", FieldMap{"foo": BlobType}),
), MakeStructTypeFromFields("", FieldMap{"foo": MakeUnionType(StringType, BlobType)}), true},
// struct{foo: string}|struct{foo: blob} & struct{foo: number|bool} -> false
{MakeUnionType(
MakeStructTypeFromFields("", FieldMap{"foo": StringType}),
MakeStructTypeFromFields("", FieldMap{"foo": BlobType}),
), MakeStructTypeFromFields("", FieldMap{"foo": MakeUnionType(NumberType, BoolType)}), false},
// map<struct{x:number, y:number}, struct A{foo:string}> & map<struct{x:number, y:number}, struct A{foo:string, bar:bool}> -> true
{
MakeMapType(
MakeStructTypeFromFields("", FieldMap{"x": NumberType, "y": NumberType}),
MakeStructTypeFromFields("A", FieldMap{"foo": StringType})),
MakeMapType(
MakeStructTypeFromFields("", FieldMap{"x": NumberType, "y": NumberType}),
MakeStructTypeFromFields("A", FieldMap{"foo": StringType, "bar": BoolType})),
true,
},
// map<struct{x:number, y:number}, struct A{foo:string}> & map<struct{x:number, y:number}, struct A{foo:string, bar:bool}> -> true
{
MakeMapType(
MakeStructTypeFromFields("", FieldMap{"x": NumberType, "y": NumberType}),
MakeStructTypeFromFields("A", FieldMap{"foo": StringType})),
MakeMapType(
MakeStructTypeFromFields("", FieldMap{"x": NumberType, "y": NumberType}),
MakeStructTypeFromFields("A", FieldMap{"foo": StringType, "bar": BoolType})),
true,
},
// struct A{self:A} & struct A{self:A, foo:Number} -> true
{MakeStructTypeFromFields("A", FieldMap{"self": MakeCycleType(0)}),
MakeStructTypeFromFields("A", FieldMap{"self": MakeCycleType(0), "foo": NumberType}), true},
}
for i, c := range cases {
act := ContainCommonSupertype(c.a, c.b)
assert.Equal(t, c.out, act, "Test case at position %d; \n\ta:%s\n\tb:%s", i, c.a.Describe(), c.b.Describe())
}
}