@ -21,6 +21,39 @@ func unify(types []cty.Type, unsafe bool) (cty.Type, []Conversion) {
return cty . NilType , nil
}
// If all of the given types are of the same structural kind, we may be
// able to construct a new type that they can all be unified to, even if
// that is not one of the given types. We must try this before the general
// behavior below because in unsafe mode we can convert an object type to
// a subset of that type, which would be a much less useful conversion for
// unification purposes.
{
objectCt := 0
tupleCt := 0
dynamicCt := 0
for _ , ty := range types {
switch {
case ty . IsObjectType ( ) :
objectCt ++
case ty . IsTupleType ( ) :
tupleCt ++
case ty == cty . DynamicPseudoType :
dynamicCt ++
default :
break
}
}
switch {
case objectCt > 0 && ( objectCt + dynamicCt ) == len ( types ) :
return unifyObjectTypes ( types , unsafe , dynamicCt > 0 )
case tupleCt > 0 && ( tupleCt + dynamicCt ) == len ( types ) :
return unifyTupleTypes ( types , unsafe , dynamicCt > 0 )
case objectCt > 0 && tupleCt > 0 :
// Can never unify object and tuple types since they have incompatible kinds
return cty . NilType , nil
}
}
prefOrder := sortTypes ( types )
// sortTypes gives us an order where earlier items are preferable as
@ -58,9 +91,225 @@ Preferences:
return wantType , conversions
}
// TODO: For structural types, try to invent a new type that they
// can all be unified to, by unifying their respective attributes.
// If we fall out here, no unification is possible
return cty . NilType , nil
}
func unifyObjectTypes ( types [ ] cty . Type , unsafe bool , hasDynamic bool ) ( cty . Type , [ ] Conversion ) {
// If we had any dynamic types in the input here then we can't predict
// what path we'll take through here once these become known types, so
// we'll conservatively produce DynamicVal for these.
if hasDynamic {
return unifyAllAsDynamic ( types )
}
// There are two different ways we can succeed here:
// - If all of the given object types have the same set of attribute names
// and the corresponding types are all unifyable, then we construct that
// type.
// - If the given object types have different attribute names or their
// corresponding types are not unifyable, we'll instead try to unify
// all of the attribute types together to produce a map type.
//
// Our unification behavior is intentionally stricter than our conversion
// behavior for subset object types because user intent is different with
// unification use-cases: it makes sense to allow {"foo":true} to convert
// to emptyobjectval, but unifying an object with an attribute with the
// empty object type should be an error because unifying to the empty
// object type would be suprising and useless.
firstAttrs := types [ 0 ] . AttributeTypes ( )
for _ , ty := range types [ 1 : ] {
thisAttrs := ty . AttributeTypes ( )
if len ( thisAttrs ) != len ( firstAttrs ) {
// If number of attributes is different then there can be no
// object type in common.
return unifyObjectTypesToMap ( types , unsafe )
}
for name := range thisAttrs {
if _ , ok := firstAttrs [ name ] ; ! ok {
// If attribute names don't exactly match then there can be
// no object type in common.
return unifyObjectTypesToMap ( types , unsafe )
}
}
}
// If we get here then we've proven that all of the given object types
// have exactly the same set of attribute names, though the types may
// differ.
retAtys := make ( map [ string ] cty . Type )
atysAcross := make ( [ ] cty . Type , len ( types ) )
for name := range firstAttrs {
for i , ty := range types {
atysAcross [ i ] = ty . AttributeType ( name )
}
retAtys [ name ] , _ = unify ( atysAcross , unsafe )
if retAtys [ name ] == cty . NilType {
// Cannot unify this attribute alone, which means that unification
// of everything down to a map type can't be possible either.
return cty . NilType , nil
}
}
retTy := cty . Object ( retAtys )
conversions := make ( [ ] Conversion , len ( types ) )
for i , ty := range types {
if ty . Equals ( retTy ) {
continue
}
if unsafe {
conversions [ i ] = GetConversionUnsafe ( ty , retTy )
} else {
conversions [ i ] = GetConversion ( ty , retTy )
}
if conversions [ i ] == nil {
// Shouldn't be reachable, since we were able to unify
return unifyObjectTypesToMap ( types , unsafe )
}
}
return retTy , conversions
}
func unifyObjectTypesToMap ( types [ ] cty . Type , unsafe bool ) ( cty . Type , [ ] Conversion ) {
// This is our fallback case for unifyObjectTypes, where we see if we can
// construct a map type that can accept all of the attribute types.
var atys [ ] cty . Type
for _ , ty := range types {
for _ , aty := range ty . AttributeTypes ( ) {
atys = append ( atys , aty )
}
}
ety , _ := unify ( atys , unsafe )
if ety == cty . NilType {
return cty . NilType , nil
}
retTy := cty . Map ( ety )
conversions := make ( [ ] Conversion , len ( types ) )
for i , ty := range types {
if ty . Equals ( retTy ) {
continue
}
if unsafe {
conversions [ i ] = GetConversionUnsafe ( ty , retTy )
} else {
conversions [ i ] = GetConversion ( ty , retTy )
}
if conversions [ i ] == nil {
// Shouldn't be reachable, since we were able to unify
return unifyObjectTypesToMap ( types , unsafe )
}
}
return retTy , conversions
}
func unifyTupleTypes ( types [ ] cty . Type , unsafe bool , hasDynamic bool ) ( cty . Type , [ ] Conversion ) {
// If we had any dynamic types in the input here then we can't predict
// what path we'll take through here once these become known types, so
// we'll conservatively produce DynamicVal for these.
if hasDynamic {
return unifyAllAsDynamic ( types )
}
// There are two different ways we can succeed here:
// - If all of the given tuple types have the same sequence of element types
// and the corresponding types are all unifyable, then we construct that
// type.
// - If the given tuple types have different element types or their
// corresponding types are not unifyable, we'll instead try to unify
// all of the elements types together to produce a list type.
firstEtys := types [ 0 ] . TupleElementTypes ( )
for _ , ty := range types [ 1 : ] {
thisEtys := ty . TupleElementTypes ( )
if len ( thisEtys ) != len ( firstEtys ) {
// If number of elements is different then there can be no
// tuple type in common.
return unifyTupleTypesToList ( types , unsafe )
}
}
// If we get here then we've proven that all of the given tuple types
// have the same number of elements, though the types may differ.
retEtys := make ( [ ] cty . Type , len ( firstEtys ) )
atysAcross := make ( [ ] cty . Type , len ( types ) )
for idx := range firstEtys {
for tyI , ty := range types {
atysAcross [ tyI ] = ty . TupleElementTypes ( ) [ idx ]
}
retEtys [ idx ] , _ = unify ( atysAcross , unsafe )
if retEtys [ idx ] == cty . NilType {
// Cannot unify this element alone, which means that unification
// of everything down to a map type can't be possible either.
return cty . NilType , nil
}
}
retTy := cty . Tuple ( retEtys )
conversions := make ( [ ] Conversion , len ( types ) )
for i , ty := range types {
if ty . Equals ( retTy ) {
continue
}
if unsafe {
conversions [ i ] = GetConversionUnsafe ( ty , retTy )
} else {
conversions [ i ] = GetConversion ( ty , retTy )
}
if conversions [ i ] == nil {
// Shouldn't be reachable, since we were able to unify
return unifyTupleTypesToList ( types , unsafe )
}
}
return retTy , conversions
}
func unifyTupleTypesToList ( types [ ] cty . Type , unsafe bool ) ( cty . Type , [ ] Conversion ) {
// This is our fallback case for unifyTupleTypes, where we see if we can
// construct a list type that can accept all of the element types.
var etys [ ] cty . Type
for _ , ty := range types {
for _ , ety := range ty . TupleElementTypes ( ) {
etys = append ( etys , ety )
}
}
ety , _ := unify ( etys , unsafe )
if ety == cty . NilType {
return cty . NilType , nil
}
retTy := cty . List ( ety )
conversions := make ( [ ] Conversion , len ( types ) )
for i , ty := range types {
if ty . Equals ( retTy ) {
continue
}
if unsafe {
conversions [ i ] = GetConversionUnsafe ( ty , retTy )
} else {
conversions [ i ] = GetConversion ( ty , retTy )
}
if conversions [ i ] == nil {
// Shouldn't be reachable, since we were able to unify
return unifyObjectTypesToMap ( types , unsafe )
}
}
return retTy , conversions
}
func unifyAllAsDynamic ( types [ ] cty . Type ) ( cty . Type , [ ] Conversion ) {
conversions := make ( [ ] Conversion , len ( types ) )
for i := range conversions {
conversions [ i ] = func ( cty . Value ) ( cty . Value , error ) {
return cty . DynamicVal , nil
}
}
return cty . DynamicPseudoType , conversions
}