diff --git a/go/types/simplify.go b/go/types/simplify.go index 16e34fb7ad..47e221a338 100644 --- a/go/types/simplify.go +++ b/go/types/simplify.go @@ -1,10 +1,8 @@ package types -import ( - "github.com/attic-labs/noms/go/d" -) +import "github.com/attic-labs/noms/go/d" -// makeSimplifiedUnion returns a type that is a supertype of all the input types, but is much +// makeSimplifiedType returns a type that is a supertype of all the input types but is much // smaller and less complex than a straight union of all those types would be. // // The resulting type is guaranteed to: @@ -29,12 +27,12 @@ import ( // - The map group is collapsed like so: // {Map|Map...} -> Map // - Each struct group is collapsed like so: -// {struct{foo:number,bar:string}, struct{bar:bool, baz:blob}} -> +// {struct{foo:number,bar:string}, struct{bar:blob, baz:bool}} -> // struct{bar:string|blob} // // Anytime any of the above cases generates a union as output, the same process // is applied to that union recursively. -func makeSimplifiedUnion(in ...*Type) *Type { +func makeSimplifiedType(in ...*Type) *Type { ts := make(typeset, len(in)) for _, t := range in { // De-cycle so that we handle cycles explicitly below. Otherwise, we would implicitly crawl @@ -43,8 +41,32 @@ func makeSimplifiedUnion(in ...*Type) *Type { ts[t] = struct{}{} } - // makeSimplifiedUnionImpl de-cycles internally. - return makeSimplifiedUnionImpl(ts) + // Impl de-cycles internally. + return makeSimplifiedTypeImpl(ts, false) +} + +// makeSimplifiedType2 returns a type that results from merging all input types. +// +// The result is similar makeSimplifiedType with one exception: +// +// Each matching Struct found in the input will be merged into a single struct containing all fields +// found in the input structs. +// +// Each struct is expanded like so: +// {struct{foo:number,bar:string}, struct{bar:blob, baz:bool}} -> +// struct{foo:number, bar:string|blob, baz:bool} +// +func makeSimplifedType2(in ...*Type) *Type { + ts := make(typeset, len(in)) + for _, t := range in { + // De-cycle so that we handle cycles explicitly below. Otherwise, we would implicitly crawl + // cycles and recurse forever. + t := ToUnresolvedType(t) + ts[t] = struct{}{} + } + + // Impl de-cycles internally. + return makeSimplifiedTypeImpl(ts, true) } // typeset is a helper that aggregates the unique set of input types for this algorithm, flattening @@ -70,10 +92,10 @@ func newTypeset(t ...*Type) typeset { return ts } -// makeSimplifiedUnionImpl is an implementation detail of makeSimplifiedUnion. +// makeSimplifiedTypeImpl is an implementation detail. // Warning: Do not call this directly. It assumes its input has been de-cycled using // ToUnresolvedType() and will infinitely recurse on cyclic types otherwise. -func makeSimplifiedUnionImpl(in typeset) *Type { +func makeSimplifiedTypeImpl(in typeset, merge bool) *Type { type how struct { k NomsKind n string @@ -111,15 +133,15 @@ func makeSimplifiedUnionImpl(in typeset) *Type { var r *Type switch h.k { case RefKind: - r = simplifyRefs(ts) + r = simplifyRefs(ts, merge) case SetKind: - r = simplifySets(ts) + r = simplifySets(ts, merge) case ListKind: - r = simplifyLists(ts) + r = simplifyLists(ts, merge) case MapKind: - r = simplifyMaps(ts) + r = simplifyMaps(ts, merge) case StructKind: - r = simplifyStructs(h.n, ts) + r = simplifyStructs(h.n, ts, merge) } out = append(out, r) } @@ -138,28 +160,28 @@ func makeSimplifiedUnionImpl(in typeset) *Type { return staticTypeCache.makeUnionType(out...) } -func simplifyRefs(ts typeset) *Type { - return simplifyContainers(RefKind, MakeRefType, ts) +func simplifyRefs(ts typeset, merge bool) *Type { + return simplifyContainers(RefKind, MakeRefType, ts, merge) } -func simplifySets(ts typeset) *Type { - return simplifyContainers(SetKind, MakeSetType, ts) +func simplifySets(ts typeset, merge bool) *Type { + return simplifyContainers(SetKind, MakeSetType, ts, merge) } -func simplifyLists(ts typeset) *Type { - return simplifyContainers(ListKind, MakeListType, ts) +func simplifyLists(ts typeset, merge bool) *Type { + return simplifyContainers(ListKind, MakeListType, ts, merge) } -func simplifyContainers(expectedKind NomsKind, makeContainer func(elem *Type) *Type, ts typeset) *Type { +func simplifyContainers(expectedKind NomsKind, makeContainer func(elem *Type) *Type, ts typeset, merge bool) *Type { elemTypes := make(typeset, len(ts)) for t := range ts { d.Chk.True(expectedKind == t.Kind()) elemTypes.Add(t.Desc.(CompoundDesc).ElemTypes[0]) } - return makeContainer(makeSimplifiedUnionImpl(elemTypes)) + return makeContainer(makeSimplifiedTypeImpl(elemTypes, merge)) } -func simplifyMaps(ts typeset) *Type { +func simplifyMaps(ts typeset, merge bool) *Type { keyTypes := make(typeset, len(ts)) valTypes := make(typeset, len(ts)) for t := range ts { @@ -169,11 +191,14 @@ func simplifyMaps(ts typeset) *Type { valTypes.Add(desc.ElemTypes[1]) } return MakeMapType( - makeSimplifiedUnionImpl(keyTypes), - makeSimplifiedUnionImpl(valTypes)) + makeSimplifiedTypeImpl(keyTypes, merge), + makeSimplifiedTypeImpl(valTypes, merge)) } -func simplifyStructs(expectedName string, ts typeset) *Type { +func simplifyStructs(expectedName string, ts typeset, merge bool) *Type { + if merge { + return mergeStructs(expectedName, ts) + } commonFields := map[string]typeset{} first := true @@ -202,7 +227,31 @@ func simplifyStructs(expectedName string, ts typeset) *Type { fm := make(FieldMap, len(ts)) for n, ts := range commonFields { - fm[n] = makeSimplifiedUnionImpl(ts) + fm[n] = makeSimplifiedTypeImpl(ts, false) + } + + return MakeStructTypeFromFields(expectedName, fm) +} + +func mergeStructs(expectedName string, ts typeset) *Type { + unionFields := map[string]typeset{} + for t := range ts { + d.Chk.True(StructKind == t.Kind()) + desc := t.Desc.(StructDesc) + d.Chk.True(expectedName == desc.Name) + for _, f := range desc.fields { + ts, ok := unionFields[f.name] + if !ok { + ts = typeset{} + } + ts.Add(f.t) + unionFields[f.name] = ts + } + } + + fm := make(FieldMap, len(ts)) + for n, ts := range unionFields { + fm[n] = makeSimplifiedTypeImpl(ts, true) } return MakeStructTypeFromFields(expectedName, fm) diff --git a/go/types/simplify_test.go b/go/types/simplify_test.go index 4f3c89113e..636e262df5 100644 --- a/go/types/simplify_test.go +++ b/go/types/simplify_test.go @@ -8,22 +8,25 @@ import ( // testing strategy // - test simplifying each kind in isolation, both shallow and deep -// - test makeSimplifiedUnion +// - test merging structs in isolation, both shallow and deep +// - test makeSupertype // - pass one type only // - test that instances are properly deduplicated // - test union flattening // - test grouping of the various kinds // - test cycles +// - test makeMergedType +// - test structs and structs nested in collections func TestSimplifyHelpers(t *testing.T) { - structSimplifier := func(n string) func(typeset) *Type { - return func(ts typeset) *Type { - return simplifyStructs(n, ts) + structSimplifier := func(n string) func(typeset, bool) *Type { + return func(ts typeset, merge bool) *Type { + return simplifyStructs(n, ts, merge) } } cases := []struct { - f func(typeset) *Type + f func(typeset, bool) *Type in []*Type out *Type }{ @@ -129,10 +132,76 @@ func TestSimplifyHelpers(t *testing.T) { []*Type{MakeStructTypeFromFields("A", FieldMap{"foo": BoolType}), MakeStructTypeFromFields("A", FieldMap{"foo": StringType})}, MakeStructTypeFromFields("A", FieldMap{"foo": MakeUnionType(BoolType, StringType)})}, + + // map, map + // -> map + {simplifyMaps, + []*Type{MakeMapType(StringType, MakeStructTypeFromFields("A", FieldMap{"foo": StringType})), + MakeMapType(StringType, MakeStructTypeFromFields("A", FieldMap{"foo": StringType, "bar": BoolType})), + }, + MakeMapType(StringType, MakeStructTypeFromFields("A", FieldMap{"foo": StringType}))}, } for i, c := range cases { - act := c.f(newTypeset(c.in...)) + act := c.f(newTypeset(c.in...), false /*don't merge*/) + assert.True(t, c.out.Equals(act), "Test case as position %d - got %s", i, act.Describe()) + } +} + +func TestMergeHelpers(t *testing.T) { + structSimplifier := func(n string) func(typeset, bool) *Type { + return func(ts typeset, merge bool) *Type { + return simplifyStructs(n, ts, merge) + } + } + + cases := []struct { + f func(typeset, bool) *Type + in []*Type + out *Type + }{ + // struct{foo:bool} -> struct{foo:bool} + {structSimplifier(""), + []*Type{MakeStructTypeFromFields("", FieldMap{"foo": BoolType})}, + MakeStructTypeFromFields("", FieldMap{"foo": BoolType})}, + // struct{foo:bool}|struct{foo:number} -> struct{foo:bool|number} + {structSimplifier(""), + []*Type{MakeStructTypeFromFields("", FieldMap{"foo": BoolType}), + MakeStructTypeFromFields("", FieldMap{"foo": StringType})}, + MakeStructTypeFromFields("", FieldMap{"foo": MakeUnionType(BoolType, StringType)})}, + // struct{foo:bool}|struct{foo:bool,bar:number} -> struct{foo:bool,bar:number} + {structSimplifier(""), + []*Type{MakeStructTypeFromFields("", FieldMap{"foo": BoolType}), + MakeStructTypeFromFields("", FieldMap{"foo": BoolType, "bar": NumberType})}, + MakeStructTypeFromFields("", FieldMap{"foo": BoolType, "bar": NumberType})}, + // struct{foo:bool}|struct{bar:number} -> struct{foo:bool,bar:number} + {structSimplifier(""), + []*Type{MakeStructTypeFromFields("", FieldMap{"foo": BoolType}), + MakeStructTypeFromFields("", FieldMap{"bar": NumberType})}, + MakeStructTypeFromFields("", FieldMap{"foo": BoolType, "bar": NumberType})}, + // struct{foo:ref}|struct{foo:ref} -> struct{foo:ref} + {structSimplifier(""), + []*Type{MakeStructTypeFromFields("", FieldMap{"foo": MakeRefType(BoolType)}), + MakeStructTypeFromFields("", FieldMap{"foo": MakeRefType(NumberType)})}, + MakeStructTypeFromFields("", FieldMap{"foo": MakeRefType(MakeUnionType(BoolType, NumberType))})}, + + // struct A{foo:bool}|struct A{foo:string} -> struct A{foo:bool|string} + {structSimplifier("A"), + []*Type{MakeStructTypeFromFields("A", FieldMap{"foo": BoolType}), + MakeStructTypeFromFields("A", FieldMap{"foo": StringType})}, + MakeStructTypeFromFields("A", FieldMap{"foo": MakeUnionType(BoolType, StringType)})}, + + // map, map + // -> map + {simplifyMaps, + []*Type{MakeMapType(StringType, MakeStructTypeFromFields("A", FieldMap{"foo": StringType})), + MakeMapType(StringType, MakeStructTypeFromFields("A", FieldMap{"bar": BoolType})), + }, + MakeMapType(StringType, MakeStructTypeFromFields("A", FieldMap{"foo": StringType, "bar": BoolType}))}, + } + + for i, c := range cases { + act := c.f(newTypeset(c.in...), true /* merge */) assert.True(t, c.out.Equals(act), "Test case as position %d - got %s", i, act.Describe()) } } @@ -228,7 +297,54 @@ func TestMakeSimplifiedUnion(t *testing.T) { } for i, c := range cases { - act := makeSimplifiedUnion(c.in...) + act := makeSimplifiedType(c.in...) + assert.True(t, c.out.Equals(act), "Test case as position %d - got %s, expected %s", i, act.Describe(), c.out.Describe()) + } +} + +func TestMakeMergedType(t *testing.T) { + cycleType := MakeStructTypeFromFields("", FieldMap{"self": MakeCycleType(0)}) + + // TODO: Why is this first step necessary? + cycleType = ToUnresolvedType(cycleType) + cycleType = resolveStructCycles(cycleType, nil) + + cases := []struct { + in []*Type + out *Type + }{ + // {struct{foo:number}} -> struct{foo:number} + {[]*Type{MakeStructTypeFromFields("", FieldMap{"foo": NumberType})}, + MakeStructTypeFromFields("", FieldMap{"foo": NumberType})}, + // {struct{foo:number}, struct{foo:string}} -> struct{foo:number|string} + {[]*Type{MakeStructTypeFromFields("", FieldMap{"foo": NumberType}), + MakeStructTypeFromFields("", FieldMap{"foo": StringType})}, + MakeStructTypeFromFields("", FieldMap{"foo": MakeUnionType(NumberType, StringType)})}, + + // {bool,string,ref,ref,ref>,ref>, + // struct{foo:number},struct{bar:string},struct A{foo:number}} -> + // bool|string|ref>|struct{}|struct A{foo:number} + { + []*Type{ + BoolType, StringType, + MakeRefType(BoolType), MakeRefType(StringType), + MakeRefType(MakeSetType(BoolType)), MakeRefType(MakeSetType(StringType)), + MakeStructTypeFromFields("", FieldMap{"foo": NumberType}), + MakeStructTypeFromFields("", FieldMap{"bar": StringType}), + MakeStructTypeFromFields("A", FieldMap{"foo": StringType}), + }, + MakeUnionType( + BoolType, StringType, + MakeRefType(MakeUnionType(BoolType, StringType, + MakeSetType(MakeUnionType(BoolType, StringType)))), + MakeStructTypeFromFields("", FieldMap{"foo": NumberType, "bar": StringType}), + MakeStructTypeFromFields("A", FieldMap{"foo": StringType}), + ), + }, + } + + for i, c := range cases { + act := makeSimplifedType2(c.in...) assert.True(t, c.out.Equals(act), "Test case as position %d - got %s, expected %s", i, act.Describe(), c.out.Describe()) } } diff --git a/go/types/type_cache.go b/go/types/type_cache.go index a1b42e87c6..1f3e7ac7c2 100644 --- a/go/types/type_cache.go +++ b/go/types/type_cache.go @@ -534,7 +534,11 @@ func MakeStructType(name string, fieldNames []string, fieldTypes []*Type) *Type } func MakeUnionType(elemTypes ...*Type) *Type { - return makeSimplifiedUnion(elemTypes...) + return makeSimplifiedType(elemTypes...) +} + +func MakeUnionType2(elemTypes ...*Type) *Type { + return makeSimplifedType2(elemTypes...) } func MakeCycleType(level uint32) *Type {