Skip to content

Commit e9ad14f

Browse files
function: Don't call function Impl if we didn't call Type
By default cty function calls "short circuit" -- skip calling the Type function and just immediately return cty.DynamicPseudoType -- if any of the arguments are cty.DynamicVal. However, in that case we were previously only skipping the call to Type but yet still expecting Impl to be able to run. That's incorrect because Impl functions should be able to treat Type as a "guard" and be guaranteed that Impl will never run if Type failed. To fix that hole we'll now track whether we skipped calling Type, and if so we'll also skip calling Impl and just immediately return an unknown value. Individual functions can still opt out of this behavior by declaring on or more of their parameters as AllowDynamicType: true, in which case their own Type function will get to decide how to handle that situation.
1 parent 0401e09 commit e9ad14f

File tree

3 files changed

+47
-18
lines changed

3 files changed

+47
-18
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# 1.13.1 (Unreleased)
2+
3+
* `function`: If a function parameter that doesn't declare `AllowDynamicType: true` recieves a `cty.DynamicVal`, the function system would previously just skip calling the function's `Type` callback and treat the result type as unknown. However, the `Call` method was then still calling a function's `Impl` callback anyway, which violated the usual contract that `Type` acts as a guard for `Impl` so `Impl` doesn't have to repeat type-checking already done in `Type`: it's only valid to call `Impl` if `Type` was previosly called _and_ it succeeded.
4+
5+
The function system will now skip calling `Impl` if it skips calling `Type`, immediately returning `cty.DynamicVal` in that case. Individual functions can opt out of this behavior by marking one or more of their parameters as `AllowDynamicType: true` and then handling that situation manually inside the `Type` and `Impl` callbacks.
6+
7+
As a result of this problem, some of the `function/stdlib` functions were not correctly handling `cty.DynamicVal` arguments after being extended to support refinements in the v1.13.0 release, causing unexpected errors or panics when calling them. Those functions are fixed indirectly by this change, since their callbacks will no longer run at all in those cases, as was true before they were extended to support refinements.
8+
19
# 1.13.0 (February 23, 2023)
210

311
## Upgrade Notes

cty/function/function.go

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -122,20 +122,13 @@ func (f Function) ReturnType(argTypes []cty.Type) (cty.Type, error) {
122122
return f.ReturnTypeForValues(vals)
123123
}
124124

125-
// ReturnTypeForValues is similar to ReturnType but can be used if the caller
126-
// already knows the values of some or all of the arguments, in which case
127-
// the function may be able to determine a more definite result if its
128-
// return type depends on the argument *values*.
129-
//
130-
// For any arguments whose values are not known, pass an Unknown value of
131-
// the appropriate type.
132-
func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error) {
125+
func (f Function) returnTypeForValues(args []cty.Value) (ty cty.Type, dynTypedArgs bool, err error) {
133126
var posArgs []cty.Value
134127
var varArgs []cty.Value
135128

136129
if f.spec.VarParam == nil {
137130
if len(args) != len(f.spec.Params) {
138-
return cty.Type{}, fmt.Errorf(
131+
return cty.Type{}, false, fmt.Errorf(
139132
"wrong number of arguments (%d required; %d given)",
140133
len(f.spec.Params), len(args),
141134
)
@@ -145,7 +138,7 @@ func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error)
145138
varArgs = nil
146139
} else {
147140
if len(args) < len(f.spec.Params) {
148-
return cty.Type{}, fmt.Errorf(
141+
return cty.Type{}, false, fmt.Errorf(
149142
"wrong number of arguments (at least %d required; %d given)",
150143
len(f.spec.Params), len(args),
151144
)
@@ -174,7 +167,7 @@ func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error)
174167
}
175168

176169
if val.IsNull() && !spec.AllowNull {
177-
return cty.Type{}, NewArgErrorf(i, "argument must not be null")
170+
return cty.Type{}, false, NewArgErrorf(i, "argument must not be null")
178171
}
179172

180173
// AllowUnknown is ignored for type-checking, since we expect to be
@@ -184,13 +177,13 @@ func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error)
184177

185178
if val.Type() == cty.DynamicPseudoType {
186179
if !spec.AllowDynamicType {
187-
return cty.DynamicPseudoType, nil
180+
return cty.DynamicPseudoType, true, nil
188181
}
189182
} else if errs := val.Type().TestConformance(spec.Type); errs != nil {
190183
// For now we'll just return the first error in the set, since
191184
// we don't have a good way to return the whole list here.
192185
// Would be good to do something better at some point...
193-
return cty.Type{}, NewArgError(i, errs[0])
186+
return cty.Type{}, false, NewArgError(i, errs[0])
194187
}
195188
}
196189

@@ -209,18 +202,18 @@ func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error)
209202
}
210203

211204
if val.IsNull() && !spec.AllowNull {
212-
return cty.Type{}, NewArgErrorf(realI, "argument must not be null")
205+
return cty.Type{}, false, NewArgErrorf(realI, "argument must not be null")
213206
}
214207

215208
if val.Type() == cty.DynamicPseudoType {
216209
if !spec.AllowDynamicType {
217-
return cty.DynamicPseudoType, nil
210+
return cty.DynamicPseudoType, true, nil
218211
}
219212
} else if errs := val.Type().TestConformance(spec.Type); errs != nil {
220213
// For now we'll just return the first error in the set, since
221214
// we don't have a good way to return the whole list here.
222215
// Would be good to do something better at some point...
223-
return cty.Type{}, NewArgError(i, errs[0])
216+
return cty.Type{}, false, NewArgError(i, errs[0])
224217
}
225218
}
226219
}
@@ -234,17 +227,37 @@ func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error)
234227
}
235228
}()
236229

237-
return f.spec.Type(args)
230+
ty, err = f.spec.Type(args)
231+
return ty, false, err
232+
}
233+
234+
// ReturnTypeForValues is similar to ReturnType but can be used if the caller
235+
// already knows the values of some or all of the arguments, in which case
236+
// the function may be able to determine a more definite result if its
237+
// return type depends on the argument *values*.
238+
//
239+
// For any arguments whose values are not known, pass an Unknown value of
240+
// the appropriate type.
241+
func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error) {
242+
ty, _, err = f.returnTypeForValues(args)
243+
return ty, err
238244
}
239245

240246
// Call actually calls the function with the given arguments, which must
241247
// conform to the function's parameter specification or an error will be
242248
// returned.
243249
func (f Function) Call(args []cty.Value) (val cty.Value, err error) {
244-
expectedType, err := f.ReturnTypeForValues(args)
250+
expectedType, dynTypeArgs, err := f.returnTypeForValues(args)
245251
if err != nil {
246252
return cty.NilVal, err
247253
}
254+
if dynTypeArgs {
255+
// returnTypeForValues sets this if any argument was inexactly typed
256+
// and the corresponding parameter did not indicate it could deal with
257+
// that. In that case we also avoid calling the implementation function
258+
// because it will also typically not be ready to deal with that case.
259+
return cty.UnknownVal(expectedType), nil
260+
}
248261

249262
if refineResult := f.spec.RefineResult; refineResult != nil {
250263
// If this function has a refinement callback then we'll refine

cty/function/stdlib/collection_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2498,6 +2498,14 @@ func TestSetproduct(t *testing.T) {
24982498
cty.UnknownVal(cty.Set(cty.Tuple([]cty.Type{cty.String, cty.Bool}))).RefineNotNull().WithMarks(cty.NewValueMarks("a", "b")),
24992499
``,
25002500
},
2501+
{
2502+
[]cty.Value{
2503+
cty.SetVal([]cty.Value{cty.True}),
2504+
cty.DynamicVal,
2505+
},
2506+
cty.DynamicVal,
2507+
``,
2508+
},
25012509

25022510
// If the inputs have unknown lengths but have length refinements then
25032511
// we can potentially refine our unknown result too.

0 commit comments

Comments
 (0)