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