Skip to content

Commit

Permalink
function/stdlib: element accepts negative indices
Browse files Browse the repository at this point in the history
The abstraction offered by this function is that of an infinitely-long list
consisting of repeating copies of the input list, but previously we
restricted it only to positive indices.

Now we'll also treat the indices before zero as a negative extension of
the infinite list, for consistency.

Technically the list is not actually infinite but rather constrained by
the range of an int64 in Go. Ideally we'd relax that too and allow any
whole number that's in the full range of cty.Number, but there's no clear
benefit to that beyond a theoretical idea of correctness and so we'll
save that one for a later change if we discover a real use-case for it.
  • Loading branch information
apparentlymart committed Jun 10, 2024
1 parent 15a9d85 commit 7b73cce
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# 1.14.5 (Unreleased)

* `function/stdlib`: The `element` function now accepts negative indices, extending the illusion of an infinitely-long list into the negative direction too.

# 1.14.4 (March 20, 2024)

Expand Down
13 changes: 3 additions & 10 deletions cty/function/stdlib/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,6 @@ var ElementFunc = function.New(&function.Spec{
},
Type: func(args []cty.Value) (cty.Type, error) {
list := args[0]
index := args[1]
if index.IsKnown() {
if index.LessThan(cty.NumberIntVal(0)).True() {
return cty.DynamicPseudoType, fmt.Errorf("cannot use element function with a negative index")
}
}

listTy := list.Type()
switch {
Expand Down Expand Up @@ -189,10 +183,6 @@ var ElementFunc = function.New(&function.Spec{
return cty.DynamicVal, fmt.Errorf("invalid index: %s", err)
}

if args[1].LessThan(cty.NumberIntVal(0)).True() {
return cty.DynamicVal, fmt.Errorf("cannot use element function with a negative index")
}

input, marks := args[0].Unmark()
if !input.IsKnown() {
return cty.UnknownVal(retType), nil
Expand All @@ -203,6 +193,9 @@ var ElementFunc = function.New(&function.Spec{
return cty.DynamicVal, errors.New("cannot use element function with an empty list")
}
index = index % l
if index < 0 {
index += l
}

// We did all the necessary type checks in the type function above,
// so this is guaranteed not to fail.
Expand Down
42 changes: 39 additions & 3 deletions cty/function/stdlib/collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,30 @@ func TestElement(t *testing.T) {
cty.StringVal("quick"),
false,
},
{ // negative index counts from the end of the list
listOfStrings,
cty.NumberIntVal(-1),
cty.StringVal("fox"),
false,
},
{ // negative index can be out of bounds too
listOfStrings,
cty.NumberIntVal(-6),
cty.StringVal("brown"),
false,
},
{ // minimum valid index
listOfStrings,
cty.NumberIntVal(-9223372036854775808),
cty.StringVal("the"),
false,
},
{ // maximum valid index
listOfStrings,
cty.NumberIntVal(9223372036854775807),
cty.StringVal("fox"),
false,
},
{ // list of lists
cty.ListVal([]cty.Value{listOfStrings, listOfStrings}),
cty.NumberIntVal(0),
Expand Down Expand Up @@ -1207,16 +1231,28 @@ func TestElement(t *testing.T) {
},
{
listOfStrings,
cty.NumberIntVal(-1),
cty.StringVal("brown"), // definitely not an index
cty.DynamicVal,
true, // index cannot be a negative number
true,
},
{
listOfStrings,
cty.StringVal("brown"), // definitely not an index
cty.NumberFloatVal(0.5),
cty.DynamicVal,
true,
},
{ // index out of bounds of int64
listOfStrings,
cty.MustParseNumberVal("-9223372036854775809"),
cty.StringVal("the"),
true,
},
{ // index out of bounds of int64
listOfStrings,
cty.MustParseNumberVal("9223372036854775808"),
cty.StringVal("fox"),
true,
},
}

for _, test := range tests {
Expand Down

0 comments on commit 7b73cce

Please sign in to comment.