diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index 6d96bebbe5..6b11e3737a 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -2429,6 +2429,20 @@ func (v *ArrayValue) GetMember(interpreter *Interpreter, locationRange LocationR ) }, ) + + case sema.ArrayTypeReverseFunctionName: + return NewHostFunctionValue( + interpreter, + sema.ArrayReverseFunctionType( + v.SemaType(interpreter), + ), + func(invocation Invocation) Value { + return v.Reverse( + invocation.Interpreter, + invocation.LocationRange, + ) + }, + ) } return nil @@ -2900,6 +2914,38 @@ func (v *ArrayValue) Slice( ) } +func (v *ArrayValue) Reverse( + interpreter *Interpreter, + locationRange LocationRange, +) Value { + count := v.Count() + index := count - 1 + + return NewArrayValueWithIterator( + interpreter, + v.Type, + common.ZeroAddress, + uint64(count), + func() Value { + if index < 0 { + return nil + } + + value := v.Get(interpreter, locationRange, index) + index-- + + return value.Transfer( + interpreter, + locationRange, + atree.Address{}, + false, + nil, + nil, + ) + }, + ) +} + // NumberValue type NumberValue interface { ComparableValue diff --git a/runtime/sema/type.go b/runtime/sema/type.go index b5a6d31c54..e57342f501 100644 --- a/runtime/sema/type.go +++ b/runtime/sema/type.go @@ -1788,6 +1788,13 @@ It does not modify the original array. If either of the parameters are out of the bounds of the array, or the indices are invalid (` + "`from > upTo`" + `), then the function will fail. ` +const ArrayTypeReverseFunctionName = "reverse" + +const arrayTypeReverseFunctionDocString = ` +Returns a new array with contents in the reversed order. +Available if the array element type is not resource-kinded. +` + func getArrayMembers(arrayType ArrayType) map[string]MemberResolver { members := map[string]MemberResolver{ @@ -1881,6 +1888,31 @@ func getArrayMembers(arrayType ArrayType) map[string]MemberResolver { ) }, }, + ArrayTypeReverseFunctionName: { + Kind: common.DeclarationKindFunction, + Resolve: func(memoryGauge common.MemoryGauge, identifier string, targetRange ast.Range, report func(error)) *Member { + elementType := arrayType.ElementType(false) + + // It is impossible for a resource to be present in two arrays. + if elementType.IsResourceType() { + report( + &InvalidResourceArrayMemberError{ + Name: identifier, + DeclarationKind: common.DeclarationKindFunction, + Range: targetRange, + }, + ) + } + + return NewPublicFunctionMember( + memoryGauge, + arrayType, + identifier, + ArrayReverseFunctionType(arrayType), + arrayTypeReverseFunctionDocString, + ) + }, + }, } // TODO: maybe still return members but report a helpful error? @@ -2193,6 +2225,13 @@ func ArraySliceFunctionType(elementType Type) *FunctionType { } } +func ArrayReverseFunctionType(arrayType ArrayType) *FunctionType { + return &FunctionType{ + Parameters: []Parameter{}, + ReturnTypeAnnotation: NewTypeAnnotation(arrayType), + } +} + // VariableSizedType is a variable sized array type type VariableSizedType struct { Type Type diff --git a/runtime/tests/checker/arrays_dictionaries_test.go b/runtime/tests/checker/arrays_dictionaries_test.go index ad551e260b..a7d47a673e 100644 --- a/runtime/tests/checker/arrays_dictionaries_test.go +++ b/runtime/tests/checker/arrays_dictionaries_test.go @@ -1078,6 +1078,56 @@ func TestCheckInvalidResourceFirstIndex(t *testing.T) { assert.IsType(t, &sema.ResourceLossError{}, errs[2]) } +func TestCheckArrayReverse(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + fun test() { + let x = [1, 2, 3] + let y = x.reverse() + } + `) + + require.NoError(t, err) +} + +func TestCheckArrayReverseInvalidArgs(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + fun test() { + let x = [1, 2, 3] + let y = x.reverse(100) + } + `) + + errs := RequireCheckerErrors(t, err, 1) + + assert.IsType(t, &sema.ExcessiveArgumentsError{}, errs[0]) +} + +func TestCheckResourceArrayReverseInvalid(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + resource X {} + + fun test(): @[X] { + let xs <- [<-create X()] + let revxs <-xs.reverse() + destroy xs + return <- revxs + } + `) + + errs := RequireCheckerErrors(t, err, 1) + + assert.IsType(t, &sema.InvalidResourceArrayMemberError{}, errs[0]) +} + func TestCheckArrayContains(t *testing.T) { t.Parallel() diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index c90f6e181e..2ef3540596 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -10507,6 +10507,240 @@ func TestInterpretArrayFirstIndexDoesNotExist(t *testing.T) { ) } +func TestInterpretArrayReverse(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let xs = [1, 2, 3, 100, 200] + let ys = [100, 467, 297, 23] + let xs_fixed: [Int; 5] = [1, 2, 3, 100, 200] + let ys_fixed: [Int; 4] = [100, 467, 297, 23] + let emptyVals: [Int] = [] + let emptyVals_fixed: [Int; 0] = [] + + fun reversexs(): [Int] { + return xs.reverse() + } + fun originalxs(): [Int] { + return xs + } + + fun reverseys(): [Int] { + return ys.reverse() + } + fun originalys(): [Int] { + return ys + } + + fun reversexs_fixed(): [Int; 5] { + return xs_fixed.reverse() + } + fun originalxs_fixed(): [Int; 5] { + return xs_fixed + } + + fun reverseys_fixed(): [Int; 4] { + return ys_fixed.reverse() + } + fun originalys_fixed(): [Int; 4] { + return ys_fixed + } + + fun reverseempty(): [Int] { + return emptyVals.reverse() + } + fun originalempty(): [Int] { + return emptyVals + } + + fun reverseempty_fixed(): [Int; 0] { + return emptyVals_fixed.reverse() + } + fun originalempty_fixed(): [Int; 0] { + return emptyVals_fixed + } + + pub struct TestStruct { + pub var test: Int + + init(_ t: Int) { + self.test = t + } + } + + let sa = [TestStruct(1), TestStruct(2), TestStruct(3)] + let sa_fixed: [TestStruct; 3] = [TestStruct(1), TestStruct(2), TestStruct(3)] + + fun reversesa(): [Int] { + let sa_rev = sa.reverse() + + let res: [Int] = []; + for s in sa_rev { + res.append(s.test) + } + + return res + } + fun originalsa(): [Int] { + let res: [Int] = []; + for s in sa { + res.append(s.test) + } + + return res + } + + fun reversesa_fixed(): [Int] { + let sa_rev = sa_fixed.reverse() + + let res: [Int] = []; + for s in sa_rev { + res.append(s.test) + } + + return res + } + fun originalsa_fixed(): [Int] { + let res: [Int] = []; + for s in sa_fixed { + res.append(s.test) + } + + return res + } + `) + + runValidCase := func(t *testing.T, reverseFuncName, originalFuncName string, reversedArray, originalArray *interpreter.ArrayValue) { + val, err := inter.Invoke(reverseFuncName) + require.NoError(t, err) + + AssertValuesEqual( + t, + inter, + reversedArray, + val, + ) + + origVal, err := inter.Invoke(originalFuncName) + require.NoError(t, err) + + // Original array remains unchanged + AssertValuesEqual( + t, + inter, + originalArray, + origVal, + ) + } + + for _, suffix := range []string{"_fixed", ""} { + fixed := suffix == "_fixed" + + var arrayType interpreter.ArrayStaticType + if fixed { + arrayType = &interpreter.ConstantSizedStaticType{ + Type: interpreter.PrimitiveStaticTypeInt, + } + } else { + arrayType = &interpreter.VariableSizedStaticType{ + Type: interpreter.PrimitiveStaticTypeInt, + } + } + + setFixedSize := func(size int64) { + if fixed { + constSized, ok := arrayType.(*interpreter.ConstantSizedStaticType) + assert.True(t, ok) + + constSized.Size = size + } + } + + setFixedSize(0) + runValidCase(t, "reverseempty"+suffix, "originalempty"+suffix, + interpreter.NewArrayValue( + inter, + interpreter.EmptyLocationRange, + arrayType, + common.ZeroAddress, + ), interpreter.NewArrayValue( + inter, + interpreter.EmptyLocationRange, + arrayType, + common.ZeroAddress, + )) + + setFixedSize(5) + runValidCase(t, "reversexs"+suffix, "originalxs"+suffix, + interpreter.NewArrayValue( + inter, + interpreter.EmptyLocationRange, + arrayType, + common.ZeroAddress, + interpreter.NewUnmeteredIntValueFromInt64(200), + interpreter.NewUnmeteredIntValueFromInt64(100), + interpreter.NewUnmeteredIntValueFromInt64(3), + interpreter.NewUnmeteredIntValueFromInt64(2), + interpreter.NewUnmeteredIntValueFromInt64(1), + ), interpreter.NewArrayValue( + inter, + interpreter.EmptyLocationRange, + arrayType, + common.ZeroAddress, + interpreter.NewUnmeteredIntValueFromInt64(1), + interpreter.NewUnmeteredIntValueFromInt64(2), + interpreter.NewUnmeteredIntValueFromInt64(3), + interpreter.NewUnmeteredIntValueFromInt64(100), + interpreter.NewUnmeteredIntValueFromInt64(200), + )) + + setFixedSize(4) + runValidCase(t, "reverseys"+suffix, "originalys"+suffix, + interpreter.NewArrayValue( + inter, + interpreter.EmptyLocationRange, + arrayType, + common.ZeroAddress, + interpreter.NewUnmeteredIntValueFromInt64(23), + interpreter.NewUnmeteredIntValueFromInt64(297), + interpreter.NewUnmeteredIntValueFromInt64(467), + interpreter.NewUnmeteredIntValueFromInt64(100), + ), interpreter.NewArrayValue( + inter, + interpreter.EmptyLocationRange, + arrayType, + common.ZeroAddress, + interpreter.NewUnmeteredIntValueFromInt64(100), + interpreter.NewUnmeteredIntValueFromInt64(467), + interpreter.NewUnmeteredIntValueFromInt64(297), + interpreter.NewUnmeteredIntValueFromInt64(23), + )) + + runValidCase(t, "reversesa"+suffix, "originalsa"+suffix, + interpreter.NewArrayValue( + inter, + interpreter.EmptyLocationRange, + &interpreter.VariableSizedStaticType{ + Type: interpreter.PrimitiveStaticTypeInt, + }, + common.ZeroAddress, + interpreter.NewUnmeteredIntValueFromInt64(3), + interpreter.NewUnmeteredIntValueFromInt64(2), + interpreter.NewUnmeteredIntValueFromInt64(1), + ), interpreter.NewArrayValue( + inter, + interpreter.EmptyLocationRange, + &interpreter.VariableSizedStaticType{ + Type: interpreter.PrimitiveStaticTypeInt, + }, + common.ZeroAddress, + interpreter.NewUnmeteredIntValueFromInt64(1), + interpreter.NewUnmeteredIntValueFromInt64(2), + interpreter.NewUnmeteredIntValueFromInt64(3), + )) + } +} + func TestInterpretOptionalReference(t *testing.T) { t.Parallel()