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 String.join function #2762

Merged
merged 6 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
5 changes: 1 addition & 4 deletions runtime/interpreter/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,10 +402,7 @@ func (v TypeValue) GetMember(interpreter *Interpreter, _ LocationRange, name str
if staticType != nil {
typeID = string(staticType.ID())
}
memoryUsage := common.MemoryUsage{
Kind: common.MemoryKindStringValue,
Amount: uint64(len(typeID)),
}
memoryUsage := common.NewStringMemoryUsage(len(typeID))
return NewStringValue(interpreter, memoryUsage, func() string {
return typeID
})
Expand Down
79 changes: 73 additions & 6 deletions runtime/interpreter/value_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,14 @@

inter := invocation.Interpreter

common.UseMemory(inter,
common.MemoryUsage{
Kind: common.MemoryKindStringValue,
Amount: 1,
},
)
// NewStringMemoryUsage already accounts for empty string.
common.UseMemory(inter, common.NewStringMemoryUsage(0))
var builder strings.Builder

argument.Iterate(inter, func(element Value) (resume bool) {
character := element.(CharacterValue)
// Construct directly instead of using NewStringMemoryUsage to avoid
// having to decrement by 1 due to double counting of empty string.
common.UseMemory(inter,
common.MemoryUsage{
Kind: common.MemoryKindStringValue,
Expand All @@ -108,6 +106,67 @@
return NewUnmeteredStringValue(builder.String())
}

func stringFunctionJoin(invocation Invocation) Value {
stringArray, ok := invocation.Arguments[0].(*ArrayValue)
if !ok {
panic(errors.NewUnreachableError())

Check warning on line 112 in runtime/interpreter/value_string.go

View check run for this annotation

Codecov / codecov/patch

runtime/interpreter/value_string.go#L112

Added line #L112 was not covered by tests
}

inter := invocation.Interpreter

switch stringArray.Count() {
case 0:
return EmptyString
case 1:
return stringArray.Get(inter, invocation.LocationRange, 0)
}

separator, ok := invocation.Arguments[1].(*StringValue)
if !ok {
panic(errors.NewUnreachableError())

Check warning on line 126 in runtime/interpreter/value_string.go

View check run for this annotation

Codecov / codecov/patch

runtime/interpreter/value_string.go#L126

Added line #L126 was not covered by tests
}

// NewStringMemoryUsage already accounts for empty string.
common.UseMemory(inter, common.NewStringMemoryUsage(0))
var builder strings.Builder
first := true

stringArray.Iterate(inter, func(element Value) (resume bool) {
// Add separator
if !first {
// Construct directly instead of using NewStringMemoryUsage to avoid
// having to decrement by 1 due to double counting of empty string.
common.UseMemory(inter,
common.MemoryUsage{
Kind: common.MemoryKindStringValue,
Amount: uint64(len(separator.Str)),
},
darkdrag00nv2 marked this conversation as resolved.
Show resolved Hide resolved
)
builder.WriteString(separator.Str)
}
first = false

str, ok := element.(*StringValue)
if !ok {
panic(errors.NewUnreachableError())

Check warning on line 151 in runtime/interpreter/value_string.go

View check run for this annotation

Codecov / codecov/patch

runtime/interpreter/value_string.go#L151

Added line #L151 was not covered by tests
}

// Construct directly instead of using NewStringMemoryUsage to avoid
// having to decrement by 1 due to double counting of empty string.
common.UseMemory(inter,
common.MemoryUsage{
Kind: common.MemoryKindStringValue,
Amount: uint64(len(str.Str)),
},
darkdrag00nv2 marked this conversation as resolved.
Show resolved Hide resolved
)
builder.WriteString(str.Str)

return true
})

return NewUnmeteredStringValue(builder.String())
}

// stringFunction is the `String` function. It is stateless, hence it can be re-used across interpreters.
var stringFunction = func() Value {
functionValue := NewUnmeteredHostFunctionValue(
Expand Down Expand Up @@ -150,5 +209,13 @@
),
)

addMember(
sema.StringTypeJoinFunctionName,
NewUnmeteredHostFunctionValue(
sema.StringTypeJoinFunctionType,
stringFunctionJoin,
),
)

return functionValue
}()
30 changes: 30 additions & 0 deletions runtime/sema/string_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ const StringTypeFromCharactersFunctionDocString = `
Returns a string from the given array of characters
`

const StringTypeJoinFunctionName = "join"
const StringTypeJoinFunctionDocString = `
Returns a string after joining the array of strings with the provided separator.
`

// StringType represents the string type
var StringType = &SimpleType{
Name: "String",
Expand Down Expand Up @@ -242,6 +247,13 @@ var StringFunctionType = func() *FunctionType {
StringTypeFromCharactersFunctionDocString,
))

addMember(NewUnmeteredPublicFunctionMember(
functionType,
StringTypeJoinFunctionName,
StringTypeJoinFunctionType,
StringTypeJoinFunctionDocString,
))

BaseValueActivation.Set(
typeName,
baseFunctionVariable(
Expand Down Expand Up @@ -298,3 +310,21 @@ var StringTypeFromCharactersFunctionType = &FunctionType{
StringType,
),
}

var StringTypeJoinFunctionType = &FunctionType{
Parameters: []Parameter{
{
Label: ArgumentLabelNotRequired,
Identifier: "strs",
TypeAnnotation: NewTypeAnnotation(&VariableSizedType{
Type: StringType,
}),
},
{
Identifier: "separator",
TypeAnnotation: NewTypeAnnotation(StringType),
},
},
ReturnTypeAnnotation: NewTypeAnnotation(StringType),
Arity: &Arity{Min: 2, Max: 2},
SupunS marked this conversation as resolved.
Show resolved Hide resolved
}
54 changes: 54 additions & 0 deletions runtime/tests/checker/string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,57 @@ func TestCheckStringToLower(t *testing.T) {
RequireGlobalValue(t, checker.Elaboration, "x"),
)
}

func TestCheckStringJoin(t *testing.T) {

t.Parallel()

checker, err := ParseAndCheck(t, `
let s = String.join(["👪", "❤️", "Abc"], separator: "/")
`)
require.NoError(t, err)

assert.Equal(t,
sema.StringType,
RequireGlobalValue(t, checker.Elaboration, "s"),
)
}

func TestCheckStringJoinTypeMismatchStrs(t *testing.T) {

t.Parallel()

_, err := ParseAndCheck(t, `
let s = String.join([1], separator: "/")
`)

errs := RequireCheckerErrors(t, err, 1)

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

func TestCheckStringJoinTypeMismatchSeparator(t *testing.T) {

t.Parallel()

_, err := ParseAndCheck(t, `
let s = String.join(["Abc", "1"], separator: 1234)
`)

errs := RequireCheckerErrors(t, err, 1)

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

func TestCheckStringJoinTypeMissingArgumentLabelSeparator(t *testing.T) {

t.Parallel()

_, err := ParseAndCheck(t, `
let s = String.join(["👪", "❤️", "Abc"], "/")
`)

errs := RequireCheckerErrors(t, err, 1)

assert.IsType(t, &sema.MissingArgumentLabelError{}, errs[0])
}
37 changes: 37 additions & 0 deletions runtime/tests/interpreter/string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,3 +462,40 @@ func TestInterpretCompareCharacters(t *testing.T) {
inter.Globals.Get("z").GetValue(),
)
}

func TestInterpretStringJoin(t *testing.T) {

t.Parallel()

inter := parseCheckAndInterpret(t, `
fun test(): String {
return String.join(["👪", "❤️"], separator: "//")
}

fun testEmptyArray(): String {
return String.join([], separator: "//")
}

fun testSingletonArray(): String {
return String.join(["pqrS"], separator: "//")
}
`)

testCase := func(t *testing.T, funcName string, expected *interpreter.StringValue) {
t.Run(funcName, func(t *testing.T) {
result, err := inter.Invoke(funcName)
require.NoError(t, err)

RequireValuesEqual(
t,
inter,
expected,
result,
)
})
}

testCase(t, "test", interpreter.NewUnmeteredStringValue("👪//❤️"))
testCase(t, "testEmptyArray", interpreter.NewUnmeteredStringValue(""))
testCase(t, "testSingletonArray", interpreter.NewUnmeteredStringValue("pqrS"))
}
Loading