1package convert 2 3import ( 4 "github.com/zclconf/go-cty/cty" 5) 6 7// The current unify implementation is somewhat inefficient, but we accept this 8// under the assumption that it will generally be used with small numbers of 9// types and with types of reasonable complexity. However, it does have a 10// "happy path" where all of the given types are equal. 11// 12// This function is likely to have poor performance in cases where any given 13// types are very complex (lots of deeply-nested structures) or if the list 14// of types itself is very large. In particular, it will walk the nested type 15// structure under the given types several times, especially when given a 16// list of types for which unification is not possible, since each permutation 17// will be tried to determine that result. 18func unify(types []cty.Type, unsafe bool) (cty.Type, []Conversion) { 19 if len(types) == 0 { 20 // Degenerate case 21 return cty.NilType, nil 22 } 23 24 // If all of the given types are of the same structural kind, we may be 25 // able to construct a new type that they can all be unified to, even if 26 // that is not one of the given types. We must try this before the general 27 // behavior below because in unsafe mode we can convert an object type to 28 // a subset of that type, which would be a much less useful conversion for 29 // unification purposes. 30 { 31 mapCt := 0 32 listCt := 0 33 setCt := 0 34 objectCt := 0 35 tupleCt := 0 36 dynamicCt := 0 37 for _, ty := range types { 38 switch { 39 case ty.IsMapType(): 40 mapCt++ 41 case ty.IsListType(): 42 listCt++ 43 case ty.IsSetType(): 44 setCt++ 45 case ty.IsObjectType(): 46 objectCt++ 47 case ty.IsTupleType(): 48 tupleCt++ 49 case ty == cty.DynamicPseudoType: 50 dynamicCt++ 51 default: 52 break 53 } 54 } 55 switch { 56 case mapCt > 0 && (mapCt+dynamicCt) == len(types): 57 return unifyCollectionTypes(cty.Map, types, unsafe, dynamicCt > 0) 58 59 case mapCt > 0 && (mapCt+objectCt+dynamicCt) == len(types): 60 // Objects often contain map data, but are not directly typed as 61 // such due to language constructs or function types. Try to unify 62 // them as maps first before falling back to heterogeneous type 63 // conversion. 64 ty, convs := unifyObjectsAsMaps(types, unsafe) 65 // If we got a map back, we know the unification was successful. 66 if ty.IsMapType() { 67 return ty, convs 68 } 69 case listCt > 0 && (listCt+dynamicCt) == len(types): 70 return unifyCollectionTypes(cty.List, types, unsafe, dynamicCt > 0) 71 case listCt > 0 && (listCt+tupleCt+dynamicCt) == len(types): 72 // Tuples are often lists in disguise, and we may be able to 73 // unify them as such. 74 ty, convs := unifyTuplesAsList(types, unsafe) 75 // if we got a list back, we know the unification was successful. 76 // Otherwise we will fall back to the heterogeneous type codepath. 77 if ty.IsListType() { 78 return ty, convs 79 } 80 case setCt > 0 && (setCt+dynamicCt) == len(types): 81 return unifyCollectionTypes(cty.Set, types, unsafe, dynamicCt > 0) 82 case objectCt > 0 && (objectCt+dynamicCt) == len(types): 83 return unifyObjectTypes(types, unsafe, dynamicCt > 0) 84 case tupleCt > 0 && (tupleCt+dynamicCt) == len(types): 85 return unifyTupleTypes(types, unsafe, dynamicCt > 0) 86 case objectCt > 0 && tupleCt > 0: 87 // Can never unify object and tuple types since they have incompatible kinds 88 return cty.NilType, nil 89 } 90 } 91 92 prefOrder := sortTypes(types) 93 94 // sortTypes gives us an order where earlier items are preferable as 95 // our result type. We'll now walk through these and choose the first 96 // one we encounter for which conversions exist for all source types. 97 conversions := make([]Conversion, len(types)) 98Preferences: 99 for _, wantTypeIdx := range prefOrder { 100 wantType := types[wantTypeIdx] 101 for i, tryType := range types { 102 if i == wantTypeIdx { 103 // Don't need to convert our wanted type to itself 104 conversions[i] = nil 105 continue 106 } 107 108 if tryType.Equals(wantType) { 109 conversions[i] = nil 110 continue 111 } 112 113 if unsafe { 114 conversions[i] = GetConversionUnsafe(tryType, wantType) 115 } else { 116 conversions[i] = GetConversion(tryType, wantType) 117 } 118 119 if conversions[i] == nil { 120 // wantType is not a suitable unification type, so we'll 121 // try the next one in our preference order. 122 continue Preferences 123 } 124 } 125 126 return wantType, conversions 127 } 128 129 // If we fall out here, no unification is possible 130 return cty.NilType, nil 131} 132 133// unifyTuplesAsList attempts to first see if the tuples unify as lists, then 134// re-unifies the given types with the list in place of the tuples. 135func unifyTuplesAsList(types []cty.Type, unsafe bool) (cty.Type, []Conversion) { 136 var tuples []cty.Type 137 var tupleIdxs []int 138 for i, t := range types { 139 if t.IsTupleType() { 140 tuples = append(tuples, t) 141 tupleIdxs = append(tupleIdxs, i) 142 } 143 } 144 145 ty, tupleConvs := unifyTupleTypesToList(tuples, unsafe) 146 if !ty.IsListType() { 147 return cty.NilType, nil 148 } 149 150 // the tuples themselves unified as a list, get the overall 151 // unification with this list type instead of the tuple. 152 // make a copy of the types, so we can fallback to the standard 153 // codepath if something went wrong 154 listed := make([]cty.Type, len(types)) 155 copy(listed, types) 156 for _, idx := range tupleIdxs { 157 listed[idx] = ty 158 } 159 160 newTy, convs := unify(listed, unsafe) 161 if !newTy.IsListType() { 162 return cty.NilType, nil 163 } 164 165 // we have a good conversion, wrap the nested tuple conversions. 166 // We know the tuple conversion is not nil, because we went from tuple to 167 // list 168 for i, idx := range tupleIdxs { 169 listConv := convs[idx] 170 tupleConv := tupleConvs[i] 171 172 if listConv == nil { 173 convs[idx] = tupleConv 174 continue 175 } 176 177 convs[idx] = func(in cty.Value) (out cty.Value, err error) { 178 out, err = tupleConv(in) 179 if err != nil { 180 return out, err 181 } 182 183 return listConv(in) 184 } 185 } 186 187 return newTy, convs 188} 189 190// unifyObjectsAsMaps attempts to first see if the objects unify as maps, then 191// re-unifies the given types with the map in place of the objects. 192func unifyObjectsAsMaps(types []cty.Type, unsafe bool) (cty.Type, []Conversion) { 193 var objs []cty.Type 194 var objIdxs []int 195 for i, t := range types { 196 if t.IsObjectType() { 197 objs = append(objs, t) 198 objIdxs = append(objIdxs, i) 199 } 200 } 201 202 ty, objConvs := unifyObjectTypesToMap(objs, unsafe) 203 if !ty.IsMapType() { 204 return cty.NilType, nil 205 } 206 207 // the objects themselves unified as a map, get the overall 208 // unification with this map type instead of the object. 209 // Make a copy of the types, so we can fallback to the standard codepath if 210 // something went wrong without changing the original types. 211 mapped := make([]cty.Type, len(types)) 212 copy(mapped, types) 213 for _, idx := range objIdxs { 214 mapped[idx] = ty 215 } 216 217 newTy, convs := unify(mapped, unsafe) 218 if !newTy.IsMapType() { 219 return cty.NilType, nil 220 } 221 222 // we have a good conversion, so wrap the nested object conversions. 223 // We know the object conversion is not nil, because we went from object to 224 // map. 225 for i, idx := range objIdxs { 226 mapConv := convs[idx] 227 objConv := objConvs[i] 228 229 if mapConv == nil { 230 convs[idx] = objConv 231 continue 232 } 233 234 convs[idx] = func(in cty.Value) (out cty.Value, err error) { 235 out, err = objConv(in) 236 if err != nil { 237 return out, err 238 } 239 240 return mapConv(in) 241 } 242 } 243 244 return newTy, convs 245} 246 247func unifyCollectionTypes(collectionType func(cty.Type) cty.Type, types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) { 248 // If we had any dynamic types in the input here then we can't predict 249 // what path we'll take through here once these become known types, so 250 // we'll conservatively produce DynamicVal for these. 251 if hasDynamic { 252 return unifyAllAsDynamic(types) 253 } 254 255 elemTypes := make([]cty.Type, 0, len(types)) 256 for _, ty := range types { 257 elemTypes = append(elemTypes, ty.ElementType()) 258 } 259 retElemType, _ := unify(elemTypes, unsafe) 260 if retElemType == cty.NilType { 261 return cty.NilType, nil 262 } 263 264 retTy := collectionType(retElemType) 265 266 conversions := make([]Conversion, len(types)) 267 for i, ty := range types { 268 if ty.Equals(retTy) { 269 continue 270 } 271 if unsafe { 272 conversions[i] = GetConversionUnsafe(ty, retTy) 273 } else { 274 conversions[i] = GetConversion(ty, retTy) 275 } 276 if conversions[i] == nil { 277 // Shouldn't be reachable, since we were able to unify 278 return cty.NilType, nil 279 } 280 } 281 282 return retTy, conversions 283} 284 285func unifyObjectTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) { 286 // If we had any dynamic types in the input here then we can't predict 287 // what path we'll take through here once these become known types, so 288 // we'll conservatively produce DynamicVal for these. 289 if hasDynamic { 290 return unifyAllAsDynamic(types) 291 } 292 293 // There are two different ways we can succeed here: 294 // - If all of the given object types have the same set of attribute names 295 // and the corresponding types are all unifyable, then we construct that 296 // type. 297 // - If the given object types have different attribute names or their 298 // corresponding types are not unifyable, we'll instead try to unify 299 // all of the attribute types together to produce a map type. 300 // 301 // Our unification behavior is intentionally stricter than our conversion 302 // behavior for subset object types because user intent is different with 303 // unification use-cases: it makes sense to allow {"foo":true} to convert 304 // to emptyobjectval, but unifying an object with an attribute with the 305 // empty object type should be an error because unifying to the empty 306 // object type would be suprising and useless. 307 308 firstAttrs := types[0].AttributeTypes() 309 for _, ty := range types[1:] { 310 thisAttrs := ty.AttributeTypes() 311 if len(thisAttrs) != len(firstAttrs) { 312 // If number of attributes is different then there can be no 313 // object type in common. 314 return unifyObjectTypesToMap(types, unsafe) 315 } 316 for name := range thisAttrs { 317 if _, ok := firstAttrs[name]; !ok { 318 // If attribute names don't exactly match then there can be 319 // no object type in common. 320 return unifyObjectTypesToMap(types, unsafe) 321 } 322 } 323 } 324 325 // If we get here then we've proven that all of the given object types 326 // have exactly the same set of attribute names, though the types may 327 // differ. 328 retAtys := make(map[string]cty.Type) 329 atysAcross := make([]cty.Type, len(types)) 330 for name := range firstAttrs { 331 for i, ty := range types { 332 atysAcross[i] = ty.AttributeType(name) 333 } 334 retAtys[name], _ = unify(atysAcross, unsafe) 335 if retAtys[name] == cty.NilType { 336 // Cannot unify this attribute alone, which means that unification 337 // of everything down to a map type can't be possible either. 338 return cty.NilType, nil 339 } 340 } 341 retTy := cty.Object(retAtys) 342 343 conversions := make([]Conversion, len(types)) 344 for i, ty := range types { 345 if ty.Equals(retTy) { 346 continue 347 } 348 if unsafe { 349 conversions[i] = GetConversionUnsafe(ty, retTy) 350 } else { 351 conversions[i] = GetConversion(ty, retTy) 352 } 353 if conversions[i] == nil { 354 // Shouldn't be reachable, since we were able to unify 355 return unifyObjectTypesToMap(types, unsafe) 356 } 357 } 358 359 return retTy, conversions 360} 361 362func unifyObjectTypesToMap(types []cty.Type, unsafe bool) (cty.Type, []Conversion) { 363 // This is our fallback case for unifyObjectTypes, where we see if we can 364 // construct a map type that can accept all of the attribute types. 365 366 var atys []cty.Type 367 for _, ty := range types { 368 for _, aty := range ty.AttributeTypes() { 369 atys = append(atys, aty) 370 } 371 } 372 373 ety, _ := unify(atys, unsafe) 374 if ety == cty.NilType { 375 return cty.NilType, nil 376 } 377 378 retTy := cty.Map(ety) 379 conversions := make([]Conversion, len(types)) 380 for i, ty := range types { 381 if ty.Equals(retTy) { 382 continue 383 } 384 if unsafe { 385 conversions[i] = GetConversionUnsafe(ty, retTy) 386 } else { 387 conversions[i] = GetConversion(ty, retTy) 388 } 389 if conversions[i] == nil { 390 return cty.NilType, nil 391 } 392 } 393 return retTy, conversions 394} 395 396func unifyTupleTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) { 397 // If we had any dynamic types in the input here then we can't predict 398 // what path we'll take through here once these become known types, so 399 // we'll conservatively produce DynamicVal for these. 400 if hasDynamic { 401 return unifyAllAsDynamic(types) 402 } 403 404 // There are two different ways we can succeed here: 405 // - If all of the given tuple types have the same sequence of element types 406 // and the corresponding types are all unifyable, then we construct that 407 // type. 408 // - If the given tuple types have different element types or their 409 // corresponding types are not unifyable, we'll instead try to unify 410 // all of the elements types together to produce a list type. 411 412 firstEtys := types[0].TupleElementTypes() 413 for _, ty := range types[1:] { 414 thisEtys := ty.TupleElementTypes() 415 if len(thisEtys) != len(firstEtys) { 416 // If number of elements is different then there can be no 417 // tuple type in common. 418 return unifyTupleTypesToList(types, unsafe) 419 } 420 } 421 422 // If we get here then we've proven that all of the given tuple types 423 // have the same number of elements, though the types may differ. 424 retEtys := make([]cty.Type, len(firstEtys)) 425 atysAcross := make([]cty.Type, len(types)) 426 for idx := range firstEtys { 427 for tyI, ty := range types { 428 atysAcross[tyI] = ty.TupleElementTypes()[idx] 429 } 430 retEtys[idx], _ = unify(atysAcross, unsafe) 431 if retEtys[idx] == cty.NilType { 432 // Cannot unify this element alone, which means that unification 433 // of everything down to a map type can't be possible either. 434 return cty.NilType, nil 435 } 436 } 437 retTy := cty.Tuple(retEtys) 438 439 conversions := make([]Conversion, len(types)) 440 for i, ty := range types { 441 if ty.Equals(retTy) { 442 continue 443 } 444 if unsafe { 445 conversions[i] = GetConversionUnsafe(ty, retTy) 446 } else { 447 conversions[i] = GetConversion(ty, retTy) 448 } 449 if conversions[i] == nil { 450 // Shouldn't be reachable, since we were able to unify 451 return unifyTupleTypesToList(types, unsafe) 452 } 453 } 454 455 return retTy, conversions 456} 457 458func unifyTupleTypesToList(types []cty.Type, unsafe bool) (cty.Type, []Conversion) { 459 // This is our fallback case for unifyTupleTypes, where we see if we can 460 // construct a list type that can accept all of the element types. 461 462 var etys []cty.Type 463 for _, ty := range types { 464 for _, ety := range ty.TupleElementTypes() { 465 etys = append(etys, ety) 466 } 467 } 468 469 ety, _ := unify(etys, unsafe) 470 if ety == cty.NilType { 471 return cty.NilType, nil 472 } 473 474 retTy := cty.List(ety) 475 conversions := make([]Conversion, len(types)) 476 for i, ty := range types { 477 if ty.Equals(retTy) { 478 continue 479 } 480 if unsafe { 481 conversions[i] = GetConversionUnsafe(ty, retTy) 482 } else { 483 conversions[i] = GetConversion(ty, retTy) 484 } 485 if conversions[i] == nil { 486 // Shouldn't be reachable, since we were able to unify 487 return unifyObjectTypesToMap(types, unsafe) 488 } 489 } 490 return retTy, conversions 491} 492 493func unifyAllAsDynamic(types []cty.Type) (cty.Type, []Conversion) { 494 conversions := make([]Conversion, len(types)) 495 for i := range conversions { 496 conversions[i] = func(cty.Value) (cty.Value, error) { 497 return cty.DynamicVal, nil 498 } 499 } 500 return cty.DynamicPseudoType, conversions 501} 502