lang/funcs: Short-circuit if start or end index is unknown

Previously the type-selection codepath for an input tuple referred
unconditionally to the start and end index values. In the Type callback,
only the types of the arguments are guaranteed to be known, so any access
to the values must be guarded with an .IsKnown check, or else the function
will not short-circuit properly when an unknown value is passed.

Now we will check the start and end indices are in range when we have
enough information to do so, but we'll return an approximate result if
either is unknown.
This commit is contained in:
Martin Atkins 2019-05-16 17:16:24 -07:00
parent 02d03d2d25
commit 742deca3e9
2 changed files with 149 additions and 22 deletions

View File

@ -1064,11 +1064,11 @@ var SliceFunc = function.New(&function.Spec{
Type: cty.DynamicPseudoType, Type: cty.DynamicPseudoType,
}, },
{ {
Name: "startIndex", Name: "start_index",
Type: cty.Number, Type: cty.Number,
}, },
{ {
Name: "endIndex", Name: "end_index",
Type: cty.Number, Type: cty.Number,
}, },
}, },
@ -1076,25 +1076,39 @@ var SliceFunc = function.New(&function.Spec{
arg := args[0] arg := args[0]
argTy := arg.Type() argTy := arg.Type()
if argTy.IsSetType() {
return cty.NilType, function.NewArgErrorf(0, "cannot slice a set, because its elements do not have indices; use the tolist function to force conversion to list if the ordering of the result is not important")
}
if !argTy.IsListType() && !argTy.IsTupleType() { if !argTy.IsListType() && !argTy.IsTupleType() {
return cty.NilType, errors.New("cannot slice a set, because its elements do not have indices; use the tolist function to force conversion to list if the ordering of the result is not important") return cty.NilType, function.NewArgErrorf(0, "must be a list or tuple value")
}
startIndex, endIndex, idxsKnown, err := sliceIndexes(args)
if err != nil {
return cty.NilType, err
} }
if argTy.IsListType() { if argTy.IsListType() {
return argTy, nil return argTy, nil
} }
startIndex, endIndex, err := sliceIndexes(args, args[0].LengthInt()) if !idxsKnown {
if err != nil { // If we don't know our start/end indices then we can't predict
return cty.NilType, err // the result type if we're planning to return a tuple.
return cty.DynamicPseudoType, nil
} }
return cty.Tuple(argTy.TupleElementTypes()[startIndex:endIndex]), nil return cty.Tuple(argTy.TupleElementTypes()[startIndex:endIndex]), nil
}, },
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
inputList := args[0] inputList := args[0]
startIndex, endIndex, err := sliceIndexes(args, inputList.LengthInt()) if retType == cty.DynamicPseudoType {
return cty.DynamicVal, nil
}
// we ignore idxsKnown return value here because the indices are always
// known here, or else the call would've short-circuited.
startIndex, endIndex, _, err := sliceIndexes(args)
if err != nil { if err != nil {
return cty.NilVal, err return cty.NilVal, err
} }
@ -1116,26 +1130,45 @@ var SliceFunc = function.New(&function.Spec{
}, },
}) })
func sliceIndexes(args []cty.Value, max int) (int, int, error) { func sliceIndexes(args []cty.Value) (int, int, bool, error) {
var startIndex, endIndex int var startIndex, endIndex, length int
var startKnown, endKnown, lengthKnown bool
if args[0].Type().IsTupleType() || args[0].IsKnown() { // if it's a tuple then we always know the length by the type, but lists must be known
length = args[0].LengthInt()
lengthKnown = true
}
if args[1].IsKnown() {
if err := gocty.FromCtyValue(args[1], &startIndex); err != nil { if err := gocty.FromCtyValue(args[1], &startIndex); err != nil {
return 0, 0, fmt.Errorf("invalid start index: %s", err) return 0, 0, false, function.NewArgErrorf(1, "invalid start index: %s", err)
} }
if err := gocty.FromCtyValue(args[2], &endIndex); err != nil {
return 0, 0, fmt.Errorf("invalid start index: %s", err)
}
if startIndex < 0 { if startIndex < 0 {
return 0, 0, errors.New("from index must be greater than or equal to 0") return 0, 0, false, function.NewArgErrorf(1, "start index must not be less than zero")
} }
if endIndex > max { if lengthKnown && startIndex > length {
return 0, 0, errors.New("to index must be less than or equal to the length of the input list") return 0, 0, false, function.NewArgErrorf(1, "start index must not be greater than the length of the list")
} }
startKnown = true
}
if args[2].IsKnown() {
if err := gocty.FromCtyValue(args[2], &endIndex); err != nil {
return 0, 0, false, function.NewArgErrorf(2, "invalid end index: %s", err)
}
if endIndex < 0 {
return 0, 0, false, function.NewArgErrorf(2, "end index must not be less than zero")
}
if lengthKnown && endIndex > length {
return 0, 0, false, function.NewArgErrorf(2, "end index must not be greater than the length of the list")
}
endKnown = true
}
if startKnown && endKnown {
if startIndex > endIndex { if startIndex > endIndex {
return 0, 0, errors.New("from index must be less than or equal to index") return 0, 0, false, function.NewArgErrorf(1, "start index must not be greater than end index")
} }
return startIndex, endIndex, nil }
return startIndex, endIndex, startKnown && endKnown, nil
} }
// TransposeFunc contructs a function that takes a map of lists of strings and // TransposeFunc contructs a function that takes a map of lists of strings and

View File

@ -2595,6 +2595,16 @@ func TestSlice(t *testing.T) {
}), }),
false, false,
}, },
{ // unknown tuple slice
cty.UnknownVal(tuple.Type()),
cty.NumberIntVal(1),
cty.NumberIntVal(3),
cty.UnknownVal(cty.Tuple([]cty.Type{
cty.Number,
cty.List(cty.String),
})),
false,
},
{ // empty list slice { // empty list slice
listOfStrings, listOfStrings,
cty.NumberIntVal(2), cty.NumberIntVal(2),
@ -2609,6 +2619,90 @@ func TestSlice(t *testing.T) {
cty.EmptyTupleVal, cty.EmptyTupleVal,
false, false,
}, },
{ // list with unknown start offset
listOfStrings,
cty.UnknownVal(cty.Number),
cty.NumberIntVal(2),
cty.UnknownVal(cty.List(cty.String)),
false,
},
{ // list with unknown start offset but end out of bounds
listOfStrings,
cty.UnknownVal(cty.Number),
cty.NumberIntVal(200),
cty.UnknownVal(cty.List(cty.String)),
true,
},
{ // list with unknown start offset but end < 0
listOfStrings,
cty.UnknownVal(cty.Number),
cty.NumberIntVal(-4),
cty.UnknownVal(cty.List(cty.String)),
true,
},
{ // list with unknown end offset
listOfStrings,
cty.UnknownVal(cty.Number),
cty.NumberIntVal(0),
cty.UnknownVal(cty.List(cty.String)),
false,
},
{ // list with unknown end offset but start out of bounds
listOfStrings,
cty.UnknownVal(cty.Number),
cty.NumberIntVal(200),
cty.UnknownVal(cty.List(cty.String)),
true,
},
{ // list with unknown end offset but start < 0
listOfStrings,
cty.UnknownVal(cty.Number),
cty.NumberIntVal(-3),
cty.UnknownVal(cty.List(cty.String)),
true,
},
{ // tuple slice with unknown start offset
tuple,
cty.UnknownVal(cty.Number),
cty.NumberIntVal(3),
cty.DynamicVal,
false,
},
{ // tuple slice with unknown start offset but end out of bounds
tuple,
cty.UnknownVal(cty.Number),
cty.NumberIntVal(200),
cty.DynamicVal,
true,
},
{ // tuple slice with unknown start offset but end < 0
tuple,
cty.UnknownVal(cty.Number),
cty.NumberIntVal(-20),
cty.DynamicVal,
true,
},
{ // tuple slice with unknown end offset
tuple,
cty.NumberIntVal(0),
cty.UnknownVal(cty.Number),
cty.DynamicVal,
false,
},
{ // tuple slice with unknown end offset but start < 0
tuple,
cty.NumberIntVal(-2),
cty.UnknownVal(cty.Number),
cty.DynamicVal,
true,
},
{ // tuple slice with unknown end offset but start out of bounds
tuple,
cty.NumberIntVal(200),
cty.UnknownVal(cty.Number),
cty.DynamicVal,
true,
},
} }
for i, test := range tests { for i, test := range tests {