aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/zclconf/go-cty/cty/convert/unify.go
blob: 53ebbfe08a15fa20666fb7a2939f2941616ce5c7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
package convert

import (
	"github.com/zclconf/go-cty/cty"
)

// The current unify implementation is somewhat inefficient, but we accept this
// under the assumption that it will generally be used with small numbers of
// types and with types of reasonable complexity. However, it does have a
// "happy path" where all of the given types are equal.
//
// This function is likely to have poor performance in cases where any given
// types are very complex (lots of deeply-nested structures) or if the list
// of types itself is very large. In particular, it will walk the nested type
// structure under the given types several times, especially when given a
// list of types for which unification is not possible, since each permutation
// will be tried to determine that result.
func unify(types []cty.Type, unsafe bool) (cty.Type, []Conversion) {
	if len(types) == 0 {
		// Degenerate case
		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
	// our result type. We'll now walk through these and choose the first
	// one we encounter for which conversions exist for all source types.
	conversions := make([]Conversion, len(types))
Preferences:
	for _, wantTypeIdx := range prefOrder {
		wantType := types[wantTypeIdx]
		for i, tryType := range types {
			if i == wantTypeIdx {
				// Don't need to convert our wanted type to itself
				conversions[i] = nil
				continue
			}

			if tryType.Equals(wantType) {
				conversions[i] = nil
				continue
			}

			if unsafe {
				conversions[i] = GetConversionUnsafe(tryType, wantType)
			} else {
				conversions[i] = GetConversion(tryType, wantType)
			}

			if conversions[i] == nil {
				// wantType is not a suitable unification type, so we'll
				// try the next one in our preference order.
				continue Preferences
			}
		}

		return wantType, conversions
	}

	// 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 {
			return cty.NilType, nil
		}
	}
	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
}