Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce map in Fixed/Variable sized Array types #2737

Merged
merged 5 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions runtime/interpreter/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -2482,6 +2482,35 @@ func (v *ArrayValue) GetMember(interpreter *Interpreter, locationRange LocationR
)
},
)

case sema.ArrayTypeMapFunctionName:
return NewHostFunctionValue(
interpreter,
sema.ArrayMapFunctionType(
interpreter,
v.SemaType(interpreter),
),
func(invocation Invocation) Value {
interpreter := invocation.Interpreter

funcArgument, ok := invocation.Arguments[0].(FunctionValue)
if !ok {
panic(errors.NewUnreachableError())
}

transformFunctionType, ok := invocation.ArgumentTypes[0].(*sema.FunctionType)
if !ok {
panic(errors.NewUnreachableError())
}

return v.Map(
interpreter,
invocation.LocationRange,
funcArgument,
transformFunctionType,
)
},
)
}

return nil
Expand Down Expand Up @@ -3058,6 +3087,85 @@ func (v *ArrayValue) Filter(
)
}

func (v *ArrayValue) Map(
interpreter *Interpreter,
locationRange LocationRange,
procedure FunctionValue,
transformFunctionType *sema.FunctionType,
) Value {

elementTypeSlice := []sema.Type{v.semaType.ElementType(false)}
iterationInvocation := func(arrayElement Value) Invocation {
return NewInvocation(
interpreter,
nil,
nil,
[]Value{arrayElement},
elementTypeSlice,
nil,
locationRange,
)
}

procedureStaticType, ok := ConvertSemaToStaticType(interpreter, transformFunctionType).(FunctionStaticType)
if !ok {
panic(errors.NewUnreachableError())
}
returnType := procedureStaticType.ReturnType(interpreter)

var returnArrayStaticType ArrayStaticType
switch v.Type.(type) {
case VariableSizedStaticType:
returnArrayStaticType = NewVariableSizedStaticType(
interpreter,
returnType,
)
case ConstantSizedStaticType:
returnArrayStaticType = NewConstantSizedStaticType(
interpreter,
returnType,
int64(v.Count()),
)
default:
panic(errors.NewUnreachableError())
}

iterator, err := v.array.Iterator()
if err != nil {
panic(errors.NewExternalError(err))
}

return NewArrayValueWithIterator(
interpreter,
returnArrayStaticType,
common.ZeroAddress,
uint64(v.Count()),
func() Value {

atreeValue, err := iterator.Next()
if err != nil {
panic(errors.NewExternalError(err))
}

if atreeValue == nil {
return nil
}

value := MustConvertStoredValue(interpreter, atreeValue)

mappedValue := procedure.invoke(iterationInvocation(value))
return mappedValue.Transfer(
interpreter,
locationRange,
atree.Address{},
false,
nil,
nil,
)
},
)
}

// NumberValue
type NumberValue interface {
ComparableValue
Expand Down
81 changes: 81 additions & 0 deletions runtime/sema/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -1802,6 +1802,12 @@ Returns a new array whose elements are filtered by applying the filter function
Available if the array element type is not resource-kinded.
`

const ArrayTypeMapFunctionName = "map"

const arrayTypeMapFunctionDocString = `
Returns a new array whose elements are produced by applying the mapper function on each element of the original array.
`

func getArrayMembers(arrayType ArrayType) map[string]MemberResolver {

members := map[string]MemberResolver{
Expand Down Expand Up @@ -1945,6 +1951,31 @@ func getArrayMembers(arrayType ArrayType) map[string]MemberResolver {
)
},
},
ArrayTypeMapFunctionName: {
Kind: common.DeclarationKindFunction,
Resolve: func(memoryGauge common.MemoryGauge, identifier string, targetRange ast.Range, report func(error)) *Member {
elementType := arrayType.ElementType(false)

// TODO: maybe allow for resource element type as a reference.
if elementType.IsResourceType() {
report(
&InvalidResourceArrayMemberError{
Name: identifier,
DeclarationKind: common.DeclarationKindFunction,
Range: targetRange,
},
)
}

return NewPublicFunctionMember(
memoryGauge,
arrayType,
identifier,
ArrayMapFunctionType(memoryGauge, arrayType),
arrayTypeMapFunctionDocString,
)
},
},
}

// TODO: maybe still return members but report a helpful error?
Expand Down Expand Up @@ -2289,6 +2320,56 @@ func ArrayFilterFunctionType(memoryGauge common.MemoryGauge, elementType Type) *
}
}

func ArrayMapFunctionType(memoryGauge common.MemoryGauge, arrayType ArrayType) *FunctionType {
// For [T] or [T; N]
// fun map(_ function: ((T): U)): [U]
// or
// fun map(_ function: ((T): U)): [U; N]

typeParameter := &TypeParameter{
Name: "U",
}

typeU := &GenericType{
TypeParameter: typeParameter,
}

var returnArrayType Type
switch arrayType := arrayType.(type) {
case *VariableSizedType:
returnArrayType = NewVariableSizedType(memoryGauge, typeU)
case *ConstantSizedType:
returnArrayType = NewConstantSizedType(memoryGauge, typeU, arrayType.Size)
default:
panic(errors.NewUnreachableError())
}

// transformFuncType: elementType -> U
transformFuncType := &FunctionType{
Parameters: []Parameter{
{
Identifier: "element",
TypeAnnotation: NewTypeAnnotation(arrayType.ElementType(false)),
},
},
ReturnTypeAnnotation: NewTypeAnnotation(typeU),
}

return &FunctionType{
TypeParameters: []*TypeParameter{
typeParameter,
},
Parameters: []Parameter{
{
Label: ArgumentLabelNotRequired,
Identifier: "transform",
TypeAnnotation: NewTypeAnnotation(transformFuncType),
},
},
ReturnTypeAnnotation: NewTypeAnnotation(returnArrayType),
}
}

// VariableSizedType is a variable sized array type
type VariableSizedType struct {
Type Type
Expand Down
98 changes: 98 additions & 0 deletions runtime/tests/checker/arrays_dictionaries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,104 @@ func TestCheckResourceArrayFilterInvalid(t *testing.T) {
assert.IsType(t, &sema.InvalidResourceArrayMemberError{}, errs[0])
}

func TestCheckArrayMap(t *testing.T) {

t.Parallel()

_, err := ParseAndCheck(t, `
fun test() {
let x = [1, 2, 3]
let trueForEven =
fun (_ x: Int): Bool {
return x % 2 == 0
}

let y: [Bool] = x.map(trueForEven)
}

fun testFixedSize() {
let x : [Int; 5] = [1, 2, 3, 21, 30]
let trueForEvenInt =
fun (_ x: Int): Bool {
return x % 2 == 0
}

let y: [Bool; 5] = x.map(trueForEvenInt)
}
`)

require.NoError(t, err)
}

func TestCheckArrayMapInvalidArgs(t *testing.T) {

t.Parallel()

testInvalidArgs := func(code string, expectedErrors []sema.SemanticError) {
_, err := ParseAndCheck(t, code)

errs := RequireCheckerErrors(t, err, len(expectedErrors))

for i, e := range expectedErrors {
assert.IsType(t, e, errs[i])
}
}

testInvalidArgs(`
fun test() {
let x = [1, 2, 3]
let y = x.map(100)
}
`,
[]sema.SemanticError{
&sema.TypeMismatchError{},
&sema.TypeParameterTypeInferenceError{}, // since we're not passing a function.
},
)

testInvalidArgs(`
fun test() {
let x = [1, 2, 3]
let trueForEvenInt16 =
fun (_ x: Int16): Bool {
return x % 2 == 0
}

let y: [Bool] = x.map(trueForEvenInt16)
}
`,
[]sema.SemanticError{
&sema.TypeMismatchError{},
},
)
}

func TestCheckResourceArrayMapInvalid(t *testing.T) {

t.Parallel()

_, err := ParseAndCheck(t, `
resource X {}

fun test(): [Bool] {
let xs <- [<-create X()]
let allResources =
fun (_ x: @X): Bool {
destroy x
return true
}

let mappedXs: [Bool] = xs.map(allResources)
destroy xs
return mappedXs
}
`)

errs := RequireCheckerErrors(t, err, 1)

assert.IsType(t, &sema.InvalidResourceArrayMemberError{}, errs[0])
}

func TestCheckArrayContains(t *testing.T) {

t.Parallel()
Expand Down
Loading