diff --git a/runtime/ast/elementtype.go b/runtime/ast/elementtype.go index 1ceee39f3e..2369cf5080 100644 --- a/runtime/ast/elementtype.go +++ b/runtime/ast/elementtype.go @@ -85,4 +85,5 @@ const ( ElementTypeForceExpression ElementTypePathExpression ElementTypeAttachExpression + ElementTypeStringTemplateExpression ) diff --git a/runtime/ast/elementtype_string.go b/runtime/ast/elementtype_string.go index 9f2e36d996..07044923bb 100644 --- a/runtime/ast/elementtype_string.go +++ b/runtime/ast/elementtype_string.go @@ -60,11 +60,12 @@ func _() { _ = x[ElementTypeForceExpression-49] _ = x[ElementTypePathExpression-50] _ = x[ElementTypeAttachExpression-51] + _ = x[ElementTypeStringTemplateExpression-52] } -const _ElementType_name = "ElementTypeUnknownElementTypeProgramElementTypeBlockElementTypeFunctionBlockElementTypeFunctionDeclarationElementTypeSpecialFunctionDeclarationElementTypeCompositeDeclarationElementTypeInterfaceDeclarationElementTypeEntitlementDeclarationElementTypeEntitlementMappingDeclarationElementTypeAttachmentDeclarationElementTypeFieldDeclarationElementTypeEnumCaseDeclarationElementTypePragmaDeclarationElementTypeImportDeclarationElementTypeTransactionDeclarationElementTypeReturnStatementElementTypeBreakStatementElementTypeContinueStatementElementTypeIfStatementElementTypeSwitchStatementElementTypeWhileStatementElementTypeForStatementElementTypeEmitStatementElementTypeVariableDeclarationElementTypeAssignmentStatementElementTypeSwapStatementElementTypeExpressionStatementElementTypeRemoveStatementElementTypeVoidExpressionElementTypeBoolExpressionElementTypeNilExpressionElementTypeIntegerExpressionElementTypeFixedPointExpressionElementTypeArrayExpressionElementTypeDictionaryExpressionElementTypeIdentifierExpressionElementTypeInvocationExpressionElementTypeMemberExpressionElementTypeIndexExpressionElementTypeConditionalExpressionElementTypeUnaryExpressionElementTypeBinaryExpressionElementTypeFunctionExpressionElementTypeStringExpressionElementTypeCastingExpressionElementTypeCreateExpressionElementTypeDestroyExpressionElementTypeReferenceExpressionElementTypeForceExpressionElementTypePathExpressionElementTypeAttachExpression" +const _ElementType_name = "ElementTypeUnknownElementTypeProgramElementTypeBlockElementTypeFunctionBlockElementTypeFunctionDeclarationElementTypeSpecialFunctionDeclarationElementTypeCompositeDeclarationElementTypeInterfaceDeclarationElementTypeEntitlementDeclarationElementTypeEntitlementMappingDeclarationElementTypeAttachmentDeclarationElementTypeFieldDeclarationElementTypeEnumCaseDeclarationElementTypePragmaDeclarationElementTypeImportDeclarationElementTypeTransactionDeclarationElementTypeReturnStatementElementTypeBreakStatementElementTypeContinueStatementElementTypeIfStatementElementTypeSwitchStatementElementTypeWhileStatementElementTypeForStatementElementTypeEmitStatementElementTypeVariableDeclarationElementTypeAssignmentStatementElementTypeSwapStatementElementTypeExpressionStatementElementTypeRemoveStatementElementTypeVoidExpressionElementTypeBoolExpressionElementTypeNilExpressionElementTypeIntegerExpressionElementTypeFixedPointExpressionElementTypeArrayExpressionElementTypeDictionaryExpressionElementTypeIdentifierExpressionElementTypeInvocationExpressionElementTypeMemberExpressionElementTypeIndexExpressionElementTypeConditionalExpressionElementTypeUnaryExpressionElementTypeBinaryExpressionElementTypeFunctionExpressionElementTypeStringExpressionElementTypeCastingExpressionElementTypeCreateExpressionElementTypeDestroyExpressionElementTypeReferenceExpressionElementTypeForceExpressionElementTypePathExpressionElementTypeAttachExpressionElementTypeStringTemplateExpression" -var _ElementType_index = [...]uint16{0, 18, 36, 52, 76, 106, 143, 174, 205, 238, 278, 310, 337, 367, 395, 423, 456, 482, 507, 535, 557, 583, 608, 631, 655, 685, 715, 739, 769, 795, 820, 845, 869, 897, 928, 954, 985, 1016, 1047, 1074, 1100, 1132, 1158, 1185, 1214, 1241, 1269, 1296, 1324, 1354, 1380, 1405, 1432} +var _ElementType_index = [...]uint16{0, 18, 36, 52, 76, 106, 143, 174, 205, 238, 278, 310, 337, 367, 395, 423, 456, 482, 507, 535, 557, 583, 608, 631, 655, 685, 715, 739, 769, 795, 820, 845, 869, 897, 928, 954, 985, 1016, 1047, 1074, 1100, 1132, 1158, 1185, 1214, 1241, 1269, 1296, 1324, 1354, 1380, 1405, 1432, 1467} func (i ElementType) String() string { if i >= ElementType(len(_ElementType_index)-1) { diff --git a/runtime/ast/expression.go b/runtime/ast/expression.go index 0142b92b20..7f6fe4b502 100644 --- a/runtime/ast/expression.go +++ b/runtime/ast/expression.go @@ -220,6 +220,81 @@ func (*StringExpression) precedence() precedence { return precedenceLiteral } +// StringTemplateExpression + +type StringTemplateExpression struct { + // Values and Expressions are assumed to be interleaved, V[0] + E[0] + V[1] + ... + E[n-1] + V[n] + // this is enforced in the parser e.g. "a\(b)c" will be parsed as follows + // Values: []string{"a","c"} + // Expressions: []Expression{b} + Values []string + Expressions []Expression + Range +} + +var _ Expression = &StringTemplateExpression{} + +func NewStringTemplateExpression( + gauge common.MemoryGauge, + values []string, + exprs []Expression, + exprRange Range, +) *StringTemplateExpression { + common.UseMemory(gauge, common.NewStringTemplateExpressionMemoryUsage(len(values)+len(exprs))) + if len(values) != len(exprs)+1 { + // assert string template alternating structure + panic(errors.NewUnreachableError()) + } + return &StringTemplateExpression{ + Values: values, + Expressions: exprs, + Range: exprRange, + } +} + +var _ Element = &StringExpression{} +var _ Expression = &StringExpression{} + +func (*StringTemplateExpression) ElementType() ElementType { + return ElementTypeStringTemplateExpression +} + +func (*StringTemplateExpression) isExpression() {} + +func (*StringTemplateExpression) isIfStatementTest() {} + +func (e *StringTemplateExpression) Walk(walkChild func(Element)) { + walkExpressions(walkChild, e.Expressions) +} + +func (e *StringTemplateExpression) String() string { + return Prettier(e) +} + +func (e *StringTemplateExpression) Doc() prettier.Doc { + if len(e.Expressions) == 0 { + return prettier.Text(QuoteString(e.Values[0])) + } + + // TODO: must reproduce expressions as literals + panic("not implemented") +} + +func (e *StringTemplateExpression) MarshalJSON() ([]byte, error) { + type Alias StringTemplateExpression + return json.Marshal(&struct { + *Alias + Type string + }{ + Type: "StringTemplateExpression", + Alias: (*Alias)(e), + }) +} + +func (*StringTemplateExpression) precedence() precedence { + return precedenceLiteral +} + // IntegerExpression type IntegerExpression struct { diff --git a/runtime/ast/expression_extractor.go b/runtime/ast/expression_extractor.go index 777dc8e4d3..4039b0370b 100644 --- a/runtime/ast/expression_extractor.go +++ b/runtime/ast/expression_extractor.go @@ -48,6 +48,10 @@ type StringExtractor interface { ExtractString(extractor *ExpressionExtractor, expression *StringExpression) ExpressionExtraction } +type StringTemplateExtractor interface { + ExtractStringTemplate(extractor *ExpressionExtractor, expression *StringTemplateExpression) ExpressionExtraction +} + type ArrayExtractor interface { ExtractArray(extractor *ExpressionExtractor, expression *ArrayExpression) ExpressionExtraction } @@ -117,31 +121,32 @@ type AttachExtractor interface { } type ExpressionExtractor struct { - IndexExtractor IndexExtractor - ForceExtractor ForceExtractor - BoolExtractor BoolExtractor - NilExtractor NilExtractor - IntExtractor IntExtractor - FixedPointExtractor FixedPointExtractor - StringExtractor StringExtractor - ArrayExtractor ArrayExtractor - DictionaryExtractor DictionaryExtractor - IdentifierExtractor IdentifierExtractor - AttachExtractor AttachExtractor - MemoryGauge common.MemoryGauge - VoidExtractor VoidExtractor - UnaryExtractor UnaryExtractor - ConditionalExtractor ConditionalExtractor - InvocationExtractor InvocationExtractor - BinaryExtractor BinaryExtractor - FunctionExtractor FunctionExtractor - CastingExtractor CastingExtractor - CreateExtractor CreateExtractor - DestroyExtractor DestroyExtractor - ReferenceExtractor ReferenceExtractor - MemberExtractor MemberExtractor - PathExtractor PathExtractor - nextIdentifier int + IndexExtractor IndexExtractor + ForceExtractor ForceExtractor + BoolExtractor BoolExtractor + NilExtractor NilExtractor + IntExtractor IntExtractor + FixedPointExtractor FixedPointExtractor + StringExtractor StringExtractor + StringTemplateExtractor StringTemplateExtractor + ArrayExtractor ArrayExtractor + DictionaryExtractor DictionaryExtractor + IdentifierExtractor IdentifierExtractor + AttachExtractor AttachExtractor + MemoryGauge common.MemoryGauge + VoidExtractor VoidExtractor + UnaryExtractor UnaryExtractor + ConditionalExtractor ConditionalExtractor + InvocationExtractor InvocationExtractor + BinaryExtractor BinaryExtractor + FunctionExtractor FunctionExtractor + CastingExtractor CastingExtractor + CreateExtractor CreateExtractor + DestroyExtractor DestroyExtractor + ReferenceExtractor ReferenceExtractor + MemberExtractor MemberExtractor + PathExtractor PathExtractor + nextIdentifier int } var _ ExpressionVisitor[ExpressionExtraction] = &ExpressionExtractor{} @@ -271,6 +276,35 @@ func (extractor *ExpressionExtractor) ExtractString(expression *StringExpression return rewriteExpressionAsIs(expression) } +func (extractor *ExpressionExtractor) VisitStringTemplateExpression(expression *StringTemplateExpression) ExpressionExtraction { + + // delegate to child extractor, if any, + // or call default implementation + + if extractor.StringTemplateExtractor != nil { + return extractor.StringTemplateExtractor.ExtractStringTemplate(extractor, expression) + } + return extractor.ExtractStringTemplate(expression) +} + +func (extractor *ExpressionExtractor) ExtractStringTemplate(expression *StringTemplateExpression) ExpressionExtraction { + + // copy the expression + newExpression := *expression + + // rewrite all value expressions + + rewrittenExpressions, extractedExpressions := + extractor.VisitExpressions(expression.Expressions) + + newExpression.Expressions = rewrittenExpressions + + return ExpressionExtraction{ + RewrittenExpression: &newExpression, + ExtractedExpressions: extractedExpressions, + } +} + func (extractor *ExpressionExtractor) VisitArrayExpression(expression *ArrayExpression) ExpressionExtraction { // delegate to child extractor, if any, diff --git a/runtime/ast/precedence.go b/runtime/ast/precedence.go index 3e42f6a8f1..fcc78d259f 100644 --- a/runtime/ast/precedence.go +++ b/runtime/ast/precedence.go @@ -83,6 +83,7 @@ const ( // - BoolExpression // - NilExpression // - StringExpression + // - StringTemplateExpression // - IntegerExpression // - FixedPointExpression // - ArrayExpression diff --git a/runtime/ast/string_template_test.go b/runtime/ast/string_template_test.go new file mode 100644 index 0000000000..c6167aaffb --- /dev/null +++ b/runtime/ast/string_template_test.go @@ -0,0 +1,47 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Flow Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ast + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/turbolent/prettier" +) + +func TestStringTemplate_Doc(t *testing.T) { + + t.Parallel() + + stmt := &StringTemplateExpression{ + Values: []string{ + "abc", + }, + Expressions: []Expression{}, + Range: Range{ + StartPos: Position{Offset: 4, Line: 2, Column: 3}, + EndPos: Position{Offset: 11, Line: 2, Column: 10}, + }, + } + + assert.Equal(t, + prettier.Text("\"abc\""), + stmt.Doc(), + ) +} diff --git a/runtime/ast/visitor.go b/runtime/ast/visitor.go index c18fdd797f..a2fea22db0 100644 --- a/runtime/ast/visitor.go +++ b/runtime/ast/visitor.go @@ -183,6 +183,7 @@ type ExpressionVisitor[T any] interface { VisitNilExpression(*NilExpression) T VisitBoolExpression(*BoolExpression) T VisitStringExpression(*StringExpression) T + VisitStringTemplateExpression(*StringTemplateExpression) T VisitIntegerExpression(*IntegerExpression) T VisitFixedPointExpression(*FixedPointExpression) T VisitDictionaryExpression(*DictionaryExpression) T @@ -219,6 +220,9 @@ func AcceptExpression[T any](expression Expression, visitor ExpressionVisitor[T] case ElementTypeStringExpression: return visitor.VisitStringExpression(expression.(*StringExpression)) + case ElementTypeStringTemplateExpression: + return visitor.VisitStringTemplateExpression(expression.(*StringTemplateExpression)) + case ElementTypeIntegerExpression: return visitor.VisitIntegerExpression(expression.(*IntegerExpression)) diff --git a/runtime/common/memorykind.go b/runtime/common/memorykind.go index 06e6748e9a..0754c397f5 100644 --- a/runtime/common/memorykind.go +++ b/runtime/common/memorykind.go @@ -204,6 +204,7 @@ const ( MemoryKindIntegerExpression MemoryKindFixedPointExpression MemoryKindArrayExpression + MemoryKindStringTemplateExpression MemoryKindDictionaryExpression MemoryKindIdentifierExpression MemoryKindInvocationExpression diff --git a/runtime/common/memorykind_string.go b/runtime/common/memorykind_string.go index 9b022e9495..67d6a0b8f8 100644 --- a/runtime/common/memorykind_string.go +++ b/runtime/common/memorykind_string.go @@ -165,56 +165,57 @@ func _() { _ = x[MemoryKindIntegerExpression-154] _ = x[MemoryKindFixedPointExpression-155] _ = x[MemoryKindArrayExpression-156] - _ = x[MemoryKindDictionaryExpression-157] - _ = x[MemoryKindIdentifierExpression-158] - _ = x[MemoryKindInvocationExpression-159] - _ = x[MemoryKindMemberExpression-160] - _ = x[MemoryKindIndexExpression-161] - _ = x[MemoryKindConditionalExpression-162] - _ = x[MemoryKindUnaryExpression-163] - _ = x[MemoryKindBinaryExpression-164] - _ = x[MemoryKindFunctionExpression-165] - _ = x[MemoryKindCastingExpression-166] - _ = x[MemoryKindCreateExpression-167] - _ = x[MemoryKindDestroyExpression-168] - _ = x[MemoryKindReferenceExpression-169] - _ = x[MemoryKindForceExpression-170] - _ = x[MemoryKindPathExpression-171] - _ = x[MemoryKindAttachExpression-172] - _ = x[MemoryKindConstantSizedType-173] - _ = x[MemoryKindDictionaryType-174] - _ = x[MemoryKindFunctionType-175] - _ = x[MemoryKindInstantiationType-176] - _ = x[MemoryKindNominalType-177] - _ = x[MemoryKindOptionalType-178] - _ = x[MemoryKindReferenceType-179] - _ = x[MemoryKindIntersectionType-180] - _ = x[MemoryKindVariableSizedType-181] - _ = x[MemoryKindPosition-182] - _ = x[MemoryKindRange-183] - _ = x[MemoryKindElaboration-184] - _ = x[MemoryKindActivation-185] - _ = x[MemoryKindActivationEntries-186] - _ = x[MemoryKindVariableSizedSemaType-187] - _ = x[MemoryKindConstantSizedSemaType-188] - _ = x[MemoryKindDictionarySemaType-189] - _ = x[MemoryKindOptionalSemaType-190] - _ = x[MemoryKindIntersectionSemaType-191] - _ = x[MemoryKindReferenceSemaType-192] - _ = x[MemoryKindEntitlementSemaType-193] - _ = x[MemoryKindEntitlementMapSemaType-194] - _ = x[MemoryKindEntitlementRelationSemaType-195] - _ = x[MemoryKindCapabilitySemaType-196] - _ = x[MemoryKindInclusiveRangeSemaType-197] - _ = x[MemoryKindOrderedMap-198] - _ = x[MemoryKindOrderedMapEntryList-199] - _ = x[MemoryKindOrderedMapEntry-200] - _ = x[MemoryKindLast-201] + _ = x[MemoryKindStringTemplateExpression-157] + _ = x[MemoryKindDictionaryExpression-158] + _ = x[MemoryKindIdentifierExpression-159] + _ = x[MemoryKindInvocationExpression-160] + _ = x[MemoryKindMemberExpression-161] + _ = x[MemoryKindIndexExpression-162] + _ = x[MemoryKindConditionalExpression-163] + _ = x[MemoryKindUnaryExpression-164] + _ = x[MemoryKindBinaryExpression-165] + _ = x[MemoryKindFunctionExpression-166] + _ = x[MemoryKindCastingExpression-167] + _ = x[MemoryKindCreateExpression-168] + _ = x[MemoryKindDestroyExpression-169] + _ = x[MemoryKindReferenceExpression-170] + _ = x[MemoryKindForceExpression-171] + _ = x[MemoryKindPathExpression-172] + _ = x[MemoryKindAttachExpression-173] + _ = x[MemoryKindConstantSizedType-174] + _ = x[MemoryKindDictionaryType-175] + _ = x[MemoryKindFunctionType-176] + _ = x[MemoryKindInstantiationType-177] + _ = x[MemoryKindNominalType-178] + _ = x[MemoryKindOptionalType-179] + _ = x[MemoryKindReferenceType-180] + _ = x[MemoryKindIntersectionType-181] + _ = x[MemoryKindVariableSizedType-182] + _ = x[MemoryKindPosition-183] + _ = x[MemoryKindRange-184] + _ = x[MemoryKindElaboration-185] + _ = x[MemoryKindActivation-186] + _ = x[MemoryKindActivationEntries-187] + _ = x[MemoryKindVariableSizedSemaType-188] + _ = x[MemoryKindConstantSizedSemaType-189] + _ = x[MemoryKindDictionarySemaType-190] + _ = x[MemoryKindOptionalSemaType-191] + _ = x[MemoryKindIntersectionSemaType-192] + _ = x[MemoryKindReferenceSemaType-193] + _ = x[MemoryKindEntitlementSemaType-194] + _ = x[MemoryKindEntitlementMapSemaType-195] + _ = x[MemoryKindEntitlementRelationSemaType-196] + _ = x[MemoryKindCapabilitySemaType-197] + _ = x[MemoryKindInclusiveRangeSemaType-198] + _ = x[MemoryKindOrderedMap-199] + _ = x[MemoryKindOrderedMapEntryList-200] + _ = x[MemoryKindOrderedMapEntry-201] + _ = x[MemoryKindLast-202] } -const _MemoryKind_name = "UnknownAddressValueStringValueCharacterValueNumberValueArrayValueBaseDictionaryValueBaseCompositeValueBaseSimpleCompositeValueBaseOptionalValueTypeValuePathValueCapabilityValueStorageReferenceValueEphemeralReferenceValueInterpretedFunctionValueHostFunctionValueBoundFunctionValueBigIntSimpleCompositeValuePublishedValueStorageCapabilityControllerValueAccountCapabilityControllerValueAtreeArrayDataSlabAtreeArrayMetaDataSlabAtreeArrayElementOverheadAtreeMapDataSlabAtreeMapMetaDataSlabAtreeMapElementOverheadAtreeMapPreAllocatedElementAtreeEncodedSlabPrimitiveStaticTypeCompositeStaticTypeInterfaceStaticTypeVariableSizedStaticTypeConstantSizedStaticTypeDictionaryStaticTypeInclusiveRangeStaticTypeOptionalStaticTypeIntersectionStaticTypeEntitlementSetStaticAccessEntitlementMapStaticAccessReferenceStaticTypeCapabilityStaticTypeFunctionStaticTypeCadenceVoidValueCadenceOptionalValueCadenceBoolValueCadenceStringValueCadenceCharacterValueCadenceAddressValueCadenceIntValueCadenceNumberValueCadenceArrayValueBaseCadenceArrayValueLengthCadenceDictionaryValueCadenceInclusiveRangeValueCadenceKeyValuePairCadenceStructValueBaseCadenceStructValueSizeCadenceResourceValueBaseCadenceAttachmentValueBaseCadenceResourceValueSizeCadenceAttachmentValueSizeCadenceEventValueBaseCadenceEventValueSizeCadenceContractValueBaseCadenceContractValueSizeCadenceEnumValueBaseCadenceEnumValueSizeCadencePathValueCadenceTypeValueCadenceCapabilityValueCadenceDeprecatedPathCapabilityTypeCadenceFunctionValueCadenceOptionalTypeCadenceDeprecatedRestrictedTypeCadenceVariableSizedArrayTypeCadenceConstantSizedArrayTypeCadenceDictionaryTypeCadenceInclusiveRangeTypeCadenceFieldCadenceParameterCadenceTypeParameterCadenceStructTypeCadenceResourceTypeCadenceAttachmentTypeCadenceEventTypeCadenceContractTypeCadenceStructInterfaceTypeCadenceResourceInterfaceTypeCadenceContractInterfaceTypeCadenceFunctionTypeCadenceEntitlementSetAccessCadenceEntitlementMapAccessCadenceReferenceTypeCadenceIntersectionTypeCadenceCapabilityTypeCadenceEnumTypeRawStringAddressLocationBytesVariableCompositeTypeInfoCompositeFieldInvocationStorageMapStorageKeyTypeTokenErrorTokenSpaceTokenProgramIdentifierArgumentBlockFunctionBlockParameterParameterListTypeParameterTypeParameterListTransferMembersTypeAnnotationDictionaryEntryFunctionDeclarationCompositeDeclarationAttachmentDeclarationInterfaceDeclarationEntitlementDeclarationEntitlementMappingElementEntitlementMappingDeclarationEnumCaseDeclarationFieldDeclarationTransactionDeclarationImportDeclarationVariableDeclarationSpecialFunctionDeclarationPragmaDeclarationAssignmentStatementBreakStatementContinueStatementEmitStatementExpressionStatementForStatementIfStatementReturnStatementSwapStatementSwitchStatementWhileStatementRemoveStatementBooleanExpressionVoidExpressionNilExpressionStringExpressionIntegerExpressionFixedPointExpressionArrayExpressionDictionaryExpressionIdentifierExpressionInvocationExpressionMemberExpressionIndexExpressionConditionalExpressionUnaryExpressionBinaryExpressionFunctionExpressionCastingExpressionCreateExpressionDestroyExpressionReferenceExpressionForceExpressionPathExpressionAttachExpressionConstantSizedTypeDictionaryTypeFunctionTypeInstantiationTypeNominalTypeOptionalTypeReferenceTypeIntersectionTypeVariableSizedTypePositionRangeElaborationActivationActivationEntriesVariableSizedSemaTypeConstantSizedSemaTypeDictionarySemaTypeOptionalSemaTypeIntersectionSemaTypeReferenceSemaTypeEntitlementSemaTypeEntitlementMapSemaTypeEntitlementRelationSemaTypeCapabilitySemaTypeInclusiveRangeSemaTypeOrderedMapOrderedMapEntryListOrderedMapEntryLast" +const _MemoryKind_name = "UnknownAddressValueStringValueCharacterValueNumberValueArrayValueBaseDictionaryValueBaseCompositeValueBaseSimpleCompositeValueBaseOptionalValueTypeValuePathValueCapabilityValueStorageReferenceValueEphemeralReferenceValueInterpretedFunctionValueHostFunctionValueBoundFunctionValueBigIntSimpleCompositeValuePublishedValueStorageCapabilityControllerValueAccountCapabilityControllerValueAtreeArrayDataSlabAtreeArrayMetaDataSlabAtreeArrayElementOverheadAtreeMapDataSlabAtreeMapMetaDataSlabAtreeMapElementOverheadAtreeMapPreAllocatedElementAtreeEncodedSlabPrimitiveStaticTypeCompositeStaticTypeInterfaceStaticTypeVariableSizedStaticTypeConstantSizedStaticTypeDictionaryStaticTypeInclusiveRangeStaticTypeOptionalStaticTypeIntersectionStaticTypeEntitlementSetStaticAccessEntitlementMapStaticAccessReferenceStaticTypeCapabilityStaticTypeFunctionStaticTypeCadenceVoidValueCadenceOptionalValueCadenceBoolValueCadenceStringValueCadenceCharacterValueCadenceAddressValueCadenceIntValueCadenceNumberValueCadenceArrayValueBaseCadenceArrayValueLengthCadenceDictionaryValueCadenceInclusiveRangeValueCadenceKeyValuePairCadenceStructValueBaseCadenceStructValueSizeCadenceResourceValueBaseCadenceAttachmentValueBaseCadenceResourceValueSizeCadenceAttachmentValueSizeCadenceEventValueBaseCadenceEventValueSizeCadenceContractValueBaseCadenceContractValueSizeCadenceEnumValueBaseCadenceEnumValueSizeCadencePathValueCadenceTypeValueCadenceCapabilityValueCadenceDeprecatedPathCapabilityTypeCadenceFunctionValueCadenceOptionalTypeCadenceDeprecatedRestrictedTypeCadenceVariableSizedArrayTypeCadenceConstantSizedArrayTypeCadenceDictionaryTypeCadenceInclusiveRangeTypeCadenceFieldCadenceParameterCadenceTypeParameterCadenceStructTypeCadenceResourceTypeCadenceAttachmentTypeCadenceEventTypeCadenceContractTypeCadenceStructInterfaceTypeCadenceResourceInterfaceTypeCadenceContractInterfaceTypeCadenceFunctionTypeCadenceEntitlementSetAccessCadenceEntitlementMapAccessCadenceReferenceTypeCadenceIntersectionTypeCadenceCapabilityTypeCadenceEnumTypeRawStringAddressLocationBytesVariableCompositeTypeInfoCompositeFieldInvocationStorageMapStorageKeyTypeTokenErrorTokenSpaceTokenProgramIdentifierArgumentBlockFunctionBlockParameterParameterListTypeParameterTypeParameterListTransferMembersTypeAnnotationDictionaryEntryFunctionDeclarationCompositeDeclarationAttachmentDeclarationInterfaceDeclarationEntitlementDeclarationEntitlementMappingElementEntitlementMappingDeclarationEnumCaseDeclarationFieldDeclarationTransactionDeclarationImportDeclarationVariableDeclarationSpecialFunctionDeclarationPragmaDeclarationAssignmentStatementBreakStatementContinueStatementEmitStatementExpressionStatementForStatementIfStatementReturnStatementSwapStatementSwitchStatementWhileStatementRemoveStatementBooleanExpressionVoidExpressionNilExpressionStringExpressionIntegerExpressionFixedPointExpressionArrayExpressionStringTemplateExpressionDictionaryExpressionIdentifierExpressionInvocationExpressionMemberExpressionIndexExpressionConditionalExpressionUnaryExpressionBinaryExpressionFunctionExpressionCastingExpressionCreateExpressionDestroyExpressionReferenceExpressionForceExpressionPathExpressionAttachExpressionConstantSizedTypeDictionaryTypeFunctionTypeInstantiationTypeNominalTypeOptionalTypeReferenceTypeIntersectionTypeVariableSizedTypePositionRangeElaborationActivationActivationEntriesVariableSizedSemaTypeConstantSizedSemaTypeDictionarySemaTypeOptionalSemaTypeIntersectionSemaTypeReferenceSemaTypeEntitlementSemaTypeEntitlementMapSemaTypeEntitlementRelationSemaTypeCapabilitySemaTypeInclusiveRangeSemaTypeOrderedMapOrderedMapEntryListOrderedMapEntryLast" -var _MemoryKind_index = [...]uint16{0, 7, 19, 30, 44, 55, 69, 88, 106, 130, 143, 152, 161, 176, 197, 220, 244, 261, 279, 285, 305, 319, 351, 383, 401, 423, 448, 464, 484, 507, 534, 550, 569, 588, 607, 630, 653, 673, 697, 715, 737, 763, 789, 808, 828, 846, 862, 882, 898, 916, 937, 956, 971, 989, 1010, 1033, 1055, 1081, 1100, 1122, 1144, 1168, 1194, 1218, 1244, 1265, 1286, 1310, 1334, 1354, 1374, 1390, 1406, 1428, 1463, 1483, 1502, 1533, 1562, 1591, 1612, 1637, 1649, 1665, 1685, 1702, 1721, 1742, 1758, 1777, 1803, 1831, 1859, 1878, 1905, 1932, 1952, 1975, 1996, 2011, 2020, 2035, 2040, 2048, 2065, 2079, 2089, 2099, 2109, 2118, 2128, 2138, 2145, 2155, 2163, 2168, 2181, 2190, 2203, 2216, 2233, 2241, 2248, 2262, 2277, 2296, 2316, 2337, 2357, 2379, 2404, 2433, 2452, 2468, 2490, 2507, 2526, 2552, 2569, 2588, 2602, 2619, 2632, 2651, 2663, 2674, 2689, 2702, 2717, 2731, 2746, 2763, 2777, 2790, 2806, 2823, 2843, 2858, 2878, 2898, 2918, 2934, 2949, 2970, 2985, 3001, 3019, 3036, 3052, 3069, 3088, 3103, 3117, 3133, 3150, 3164, 3176, 3193, 3204, 3216, 3229, 3245, 3262, 3270, 3275, 3286, 3296, 3313, 3334, 3355, 3373, 3389, 3409, 3426, 3445, 3467, 3494, 3512, 3534, 3544, 3563, 3578, 3582} +var _MemoryKind_index = [...]uint16{0, 7, 19, 30, 44, 55, 69, 88, 106, 130, 143, 152, 161, 176, 197, 220, 244, 261, 279, 285, 305, 319, 351, 383, 401, 423, 448, 464, 484, 507, 534, 550, 569, 588, 607, 630, 653, 673, 697, 715, 737, 763, 789, 808, 828, 846, 862, 882, 898, 916, 937, 956, 971, 989, 1010, 1033, 1055, 1081, 1100, 1122, 1144, 1168, 1194, 1218, 1244, 1265, 1286, 1310, 1334, 1354, 1374, 1390, 1406, 1428, 1463, 1483, 1502, 1533, 1562, 1591, 1612, 1637, 1649, 1665, 1685, 1702, 1721, 1742, 1758, 1777, 1803, 1831, 1859, 1878, 1905, 1932, 1952, 1975, 1996, 2011, 2020, 2035, 2040, 2048, 2065, 2079, 2089, 2099, 2109, 2118, 2128, 2138, 2145, 2155, 2163, 2168, 2181, 2190, 2203, 2216, 2233, 2241, 2248, 2262, 2277, 2296, 2316, 2337, 2357, 2379, 2404, 2433, 2452, 2468, 2490, 2507, 2526, 2552, 2569, 2588, 2602, 2619, 2632, 2651, 2663, 2674, 2689, 2702, 2717, 2731, 2746, 2763, 2777, 2790, 2806, 2823, 2843, 2858, 2882, 2902, 2922, 2942, 2958, 2973, 2994, 3009, 3025, 3043, 3060, 3076, 3093, 3112, 3127, 3141, 3157, 3174, 3188, 3200, 3217, 3228, 3240, 3253, 3269, 3286, 3294, 3299, 3310, 3320, 3337, 3358, 3379, 3397, 3413, 3433, 3450, 3469, 3491, 3518, 3536, 3558, 3568, 3587, 3602, 3606} func (i MemoryKind) String() string { if i >= MemoryKind(len(_MemoryKind_index)-1) { diff --git a/runtime/common/metering.go b/runtime/common/metering.go index a4b4afc2dd..e6e6249571 100644 --- a/runtime/common/metering.go +++ b/runtime/common/metering.go @@ -795,6 +795,13 @@ func NewArrayExpressionMemoryUsage(length int) MemoryUsage { } } +func NewStringTemplateExpressionMemoryUsage(length int) MemoryUsage { + return MemoryUsage{ + Kind: MemoryKindStringTemplateExpression, + Amount: uint64(length), + } +} + func NewDictionaryExpressionMemoryUsage(length int) MemoryUsage { return MemoryUsage{ Kind: MemoryKindDictionaryExpression, diff --git a/runtime/compiler/compiler.go b/runtime/compiler/compiler.go index 342333adaa..6e052f4a17 100644 --- a/runtime/compiler/compiler.go +++ b/runtime/compiler/compiler.go @@ -246,6 +246,11 @@ func (compiler *Compiler) VisitFunctionExpression(_ *ast.FunctionExpression) ir. panic(errors.NewUnreachableError()) } +func (compiler *Compiler) VisitStringTemplateExpression(e *ast.StringTemplateExpression) ir.Expr { + // TODO + panic(errors.NewUnreachableError()) +} + func (compiler *Compiler) VisitStringExpression(e *ast.StringExpression) ir.Expr { return &ir.Const{ Constant: ir.String{ diff --git a/runtime/interpreter/interpreter_expression.go b/runtime/interpreter/interpreter_expression.go index d2894749bf..13e2f192e3 100644 --- a/runtime/interpreter/interpreter_expression.go +++ b/runtime/interpreter/interpreter_expression.go @@ -20,6 +20,7 @@ package interpreter import ( "math/big" + "strings" "time" "github.com/onflow/atree" @@ -957,6 +958,28 @@ func (interpreter *Interpreter) VisitStringExpression(expression *ast.StringExpr return NewUnmeteredStringValue(expression.Value) } +func (interpreter *Interpreter) VisitStringTemplateExpression(expression *ast.StringTemplateExpression) Value { + values := interpreter.visitExpressionsNonCopying(expression.Expressions) + + var builder strings.Builder + for i, str := range expression.Values { + builder.WriteString(str) + if i < len(values) { + // switch on value instead of type + switch value := values[i].(type) { + case *StringValue: + builder.WriteString(value.Str) + case CharacterValue: + builder.WriteString(value.Str) + default: + builder.WriteString(value.String()) + } + } + } + + return NewUnmeteredStringValue(builder.String()) +} + func (interpreter *Interpreter) VisitArrayExpression(expression *ast.ArrayExpression) Value { values := interpreter.visitExpressionsNonCopying(expression.Values) diff --git a/runtime/parser/expression.go b/runtime/parser/expression.go index 41f44df7a7..4d2dbbeef1 100644 --- a/runtime/parser/expression.go +++ b/runtime/parser/expression.go @@ -433,19 +433,6 @@ func init() { }, }) - defineExpr(literalExpr{ - tokenType: lexer.TokenString, - nullDenotation: func(p *parser, token lexer.Token) (ast.Expression, error) { - literal := p.tokenSource(token) - parsedString := parseStringLiteral(p, literal) - return ast.NewStringExpression( - p.memoryGauge, - parsedString, - token.Range, - ), nil - }, - }) - defineExpr(prefixExpr{ tokenType: lexer.TokenMinus, bindingPower: exprLeftBindingPowerUnaryPrefix, @@ -510,6 +497,7 @@ func init() { defineNestedExpression() defineInvocationExpression() defineArrayExpression() + defineStringExpression() defineDictionaryExpression() defineIndexExpression() definePathExpression() @@ -1144,6 +1132,107 @@ func defineNestedExpression() { ) } +func defineStringExpression() { + setExprNullDenotation( + lexer.TokenString, + func(p *parser, startToken lexer.Token) (ast.Expression, error) { + var literals []string + var values []ast.Expression + curToken := startToken + endToken := startToken + + // check for start " of string literal + literal := p.tokenSource(curToken) + length := len(literal) + if length == 0 { + p.reportSyntaxError("invalid end of string literal: missing '\"'") + return ast.NewStringExpression( + p.memoryGauge, + "", + startToken.Range, + ), nil + } + + if length >= 1 { + first := literal[0] + if first != '"' { + p.reportSyntaxError("invalid start of string literal: expected '\"', got %q", first) + } + } + + // flag for ending " check + missingEnd := true + + for curToken.Is(lexer.TokenString) { + literal = p.tokenSource(curToken) + + // remove quotation marks if they exist + if curToken == startToken { + literal = literal[1:] + } + + length = len(literal) + if length >= 1 && literal[length-1] == '"' { + literal = literal[:length-1] + missingEnd = false + } + + parsedString := parseStringLiteralContent(p, literal) + literals = append(literals, parsedString) + endToken = curToken + + // parser already points to next token + curToken = p.current + if curToken.Is(lexer.TokenStringTemplate) { + // advance to the expression + p.next() + value, err := parseExpression(p, lowestBindingPower) + // consider invalid expression first + if err != nil { + return nil, err + } + // limit string templates to identifiers only + if _, ok := value.(*ast.IdentifierExpression); !ok { + return nil, p.syntaxError("expected identifier got: %s", value.String()) + } + _, err = p.mustOne(lexer.TokenParenClose) + if err != nil { + return nil, err + } + values = append(values, value) + // parser already points to next token + curToken = p.current + // safely call next because this should always be a string + p.next() + missingEnd = true + } + } + + // check for end " of string literal + if missingEnd { + p.reportSyntaxError("invalid end of string literal: missing '\"'") + } + + if len(values) == 0 { + return ast.NewStringExpression( + p.memoryGauge, + literals[0], // must exist + startToken.Range, + ), nil + } else { + return ast.NewStringTemplateExpression( + p.memoryGauge, + literals, values, + ast.NewRange(p.memoryGauge, + startToken.StartPos, + endToken.EndPos), + ), nil + } + + }, + ) +} + func defineArrayExpression() { setExprNullDenotation( lexer.TokenBracketOpen, diff --git a/runtime/parser/expression_test.go b/runtime/parser/expression_test.go index eb8b348c45..e2377cf934 100644 --- a/runtime/parser/expression_test.go +++ b/runtime/parser/expression_test.go @@ -6055,6 +6055,462 @@ func TestParseStringWithUnicode(t *testing.T) { utils.AssertEqualWithDiff(t, expected, actual) } +func TestParseStringTemplate(t *testing.T) { + + t.Parallel() + + t.Run("simple", func(t *testing.T) { + + t.Parallel() + + actual, errs := testParseExpression(` + "\(test)" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + + expected := &ast.StringTemplateExpression{ + Values: []string{ + "", + "", + }, + Expressions: []ast.Expression{ + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + EndPos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) + }) + + t.Run("multi", func(t *testing.T) { + + t.Parallel() + + actual, errs := testParseExpression(` + "this is a test \(abc)\(def) test" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + + expected := &ast.StringTemplateExpression{ + Values: []string{ + "this is a test ", + "", + " test", + }, + Expressions: []ast.Expression{ + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "abc", + Pos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "def", + Pos: ast.Position{Offset: 27, Line: 2, Column: 27}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + EndPos: ast.Position{Offset: 36, Line: 2, Column: 36}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) + }) + + t.Run("missing end", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "this is a test \(FOO) + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of string literal: missing '\"'", + Pos: ast.Position{Offset: 27, Line: 2, Column: 27}, + }, + }, + errs, + ) + }) + + t.Run("invalid identifier", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "\(.)" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token in expression: '.'", + Pos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + errs, + ) + }) + + t.Run("invalid, num", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "\(2 + 2) is a" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected identifier got: 2 + 2", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + }, + errs, + ) + }) + + t.Run("valid, nested identifier", func(t *testing.T) { + + t.Parallel() + + actual, errs := testParseExpression(` + "\((a))" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + + expected := &ast.StringTemplateExpression{ + Values: []string{ + "", + "", + }, + Expressions: []ast.Expression{ + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 5, Line: 2, Column: 4}, + EndPos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) + + }) + t.Run("invalid, empty", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "\()" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token in expression: ')'", + Pos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + errs, + ) + }) + + t.Run("invalid, function identifier", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "\(add())" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected identifier got: add()", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + }, + errs, + ) + }) + + t.Run("invalid, unbalanced paren", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "\(add" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected token ')'", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + }, + errs, + ) + }) + + t.Run("invalid, nested templates", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "outer string \( "\(inner template)" )" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected token ')'", + Pos: ast.Position{Offset: 30, Line: 2, Column: 29}, + }, + }, + errs, + ) + }) + + t.Run("valid, alternating", func(t *testing.T) { + + t.Parallel() + + actual, errs := testParseExpression(` + "a\(b)c" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + + expected := &ast.StringTemplateExpression{ + Values: []string{ + "a", + "c", + }, + Expressions: []ast.Expression{ + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 4, Line: 2, Column: 3}, + EndPos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) + }) + + t.Run("valid, surrounded", func(t *testing.T) { + + t.Parallel() + + actual, errs := testParseExpression(` + "\(a)b\(c)" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + + expected := &ast.StringTemplateExpression{ + Values: []string{ + "", + "b", + "", + }, + Expressions: []ast.Expression{ + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "c", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 4, Line: 2, Column: 3}, + EndPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) + }) + + t.Run("valid, adjacent", func(t *testing.T) { + + t.Parallel() + + actual, errs := testParseExpression(` + "\(a)\(b)\(c)" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + + expected := &ast.StringTemplateExpression{ + Values: []string{ + "", + "", + "", + "", + }, + Expressions: []ast.Expression{ + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 11, Line: 2, Column: 11}, + }, + }, + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "c", + Pos: ast.Position{Offset: 15, Line: 2, Column: 16}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 4, Line: 2, Column: 3}, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 18}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) + }) +} + func TestParseNilCoalescing(t *testing.T) { t.Parallel() diff --git a/runtime/parser/lexer/lexer.go b/runtime/parser/lexer/lexer.go index 7b69245ce2..08f689be0e 100644 --- a/runtime/parser/lexer/lexer.go +++ b/runtime/parser/lexer/lexer.go @@ -49,6 +49,13 @@ type position struct { column int } +type lexerMode uint8 + +const ( + lexerModeNormal lexerMode = iota + lexerModeStringInterpolation +) + type lexer struct { // memoryGauge is used for metering memory usage memoryGauge common.MemoryGauge @@ -74,6 +81,10 @@ type lexer struct { prev rune // canBackup indicates whether stepping back is allowed canBackup bool + // lexer mode is used for string templates + mode lexerMode + // counts the number of unclosed brackets for string templates \((())) + openBrackets int } var _ TokenStream = &lexer{} @@ -130,6 +141,8 @@ func (l *lexer) clear() { l.cursor = 0 l.tokens = l.tokens[:0] l.tokenCount = 0 + l.mode = lexerModeNormal + l.openBrackets = 0 } func (l *lexer) Reclaim() { @@ -407,8 +420,19 @@ func (l *lexer) scanString(quote rune) { l.backupOne() return case '\\': + // might have to backup twice due to string template + tmpBackupOffset := l.prevEndOffset + tmpBackup := l.prev r = l.next() switch r { + case '(': + // string template, stop and set mode + l.mode = lexerModeStringInterpolation + // no need to update prev values because these next tokens will not backup + l.endOffset = tmpBackupOffset + l.current = tmpBackup + l.canBackup = false + return case '\n', EOF: // NOTE: invalid end of string handled by parser l.backupOne() diff --git a/runtime/parser/lexer/lexer_test.go b/runtime/parser/lexer/lexer_test.go index 51f8f53f34..f2a77db4b2 100644 --- a/runtime/parser/lexer/lexer_test.go +++ b/runtime/parser/lexer/lexer_test.go @@ -1014,6 +1014,448 @@ func TestLexString(t *testing.T) { ) }) + t.Run("valid, string template", func(t *testing.T) { + testLex(t, + `"\(abc).length"`, + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: `"`, + }, + { + Token: Token{ + Type: TokenStringTemplate, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: `\(`, + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + Source: `abc`, + }, + { + Token: Token{ + Type: TokenParenClose, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Source: `)`, + }, + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + Source: `.length"`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + EndPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + }, + }, + }, + ) + }) + + t.Run("valid, not a string template", func(t *testing.T) { + testLex(t, + `"(1.00)"`, + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: `"(1.00)"`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + ) + }) + + t.Run("invalid, number string template", func(t *testing.T) { + testLex(t, + `"\(7)"`, + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: `"`, + }, + { + Token: Token{ + Type: TokenStringTemplate, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: `\(`, + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: `7`, + }, + { + Token: Token{ + Type: TokenParenClose, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: `)`, + }, + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + Source: `"`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + }, + }, + ) + }) + + t.Run("invalid identifier string template", func(t *testing.T) { + testLex(t, + `"\(a+2)"`, + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: `"`, + }, + { + Token: Token{ + Type: TokenStringTemplate, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: `\(`, + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: `a`, + }, + { + Token: Token{ + Type: TokenPlus, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: `+`, + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + Source: `2`, + }, + { + Token: Token{ + Type: TokenParenClose, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Source: `)`, + }, + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: `"`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + ) + }) + + t.Run("valid, multi string template", func(t *testing.T) { + testLex(t, + `"\(a)\(b)"`, + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: `"`, + }, + { + Token: Token{ + Type: TokenStringTemplate, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: `\(`, + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: `a`, + }, + { + Token: Token{ + Type: TokenParenClose, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: `)`, + }, + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 4}, + }, + }, + Source: ``, + }, + { + Token: Token{ + Type: TokenStringTemplate, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 6}, + }, + }, + Source: `\(`, + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 7}, + }, + }, + Source: `b`, + }, + { + Token: Token{ + Type: TokenParenClose, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 9, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 8}, + }, + }, + Source: `)`, + }, + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 10, Offset: 9}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 9}, + }, + }, + Source: `"`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 11, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 10}, + }, + }, + }, + }, + ) + }) + + t.Run("valid, nested brackets", func(t *testing.T) { + testLex(t, + `"\((a))"`, + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: `"`, + }, + { + Token: Token{ + Type: TokenStringTemplate, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: `\(`, + }, + { + Token: Token{ + Type: TokenParenOpen, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: `(`, + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: `a`, + }, + { + Token: Token{ + Type: TokenParenClose, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + Source: `)`, + }, + { + Token: Token{ + Type: TokenParenClose, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Source: `)`, + }, + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: `"`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + ) + }) + t.Run("invalid, empty, not terminated at line end", func(t *testing.T) { testLex(t, "\"\n", diff --git a/runtime/parser/lexer/state.go b/runtime/parser/lexer/state.go index 4b252d6f09..08558c6b78 100644 --- a/runtime/parser/lexer/state.go +++ b/runtime/parser/lexer/state.go @@ -56,9 +56,20 @@ func rootState(l *lexer) stateFn { case '%': l.emitType(TokenPercent) case '(': + if l.mode == lexerModeStringInterpolation { + // it is necessary to balance brackets when generating tokens for string templates to know when to change modes + l.openBrackets++ + } l.emitType(TokenParenOpen) case ')': l.emitType(TokenParenClose) + if l.mode == lexerModeStringInterpolation { + l.openBrackets-- + if l.openBrackets == 0 { + l.mode = lexerModeNormal + return stringState + } + } case '{': l.emitType(TokenBraceOpen) case '}': @@ -118,6 +129,17 @@ func rootState(l *lexer) stateFn { return numberState case '"': return stringState + case '\\': + if l.mode == lexerModeStringInterpolation { + r = l.next() + switch r { + case '(': + l.emitType(TokenStringTemplate) + l.openBrackets++ + } + } else { + return l.error(fmt.Errorf("unrecognized character: %#U", r)) + } case '/': r = l.next() switch r { diff --git a/runtime/parser/lexer/tokentype.go b/runtime/parser/lexer/tokentype.go index 0a15c19b6f..d2aa45fa8e 100644 --- a/runtime/parser/lexer/tokentype.go +++ b/runtime/parser/lexer/tokentype.go @@ -82,6 +82,7 @@ const ( TokenAsExclamationMark TokenAsQuestionMark TokenPragma + TokenStringTemplate // NOTE: not an actual token, must be last item TokenMax ) @@ -205,6 +206,8 @@ func (t TokenType) String() string { return `'as?'` case TokenPragma: return `'#'` + case TokenStringTemplate: + return `'\('` default: panic(errors.NewUnreachableError()) } diff --git a/runtime/sema/check_string_template_expression.go b/runtime/sema/check_string_template_expression.go new file mode 100644 index 0000000000..6cbd8d80ec --- /dev/null +++ b/runtime/sema/check_string_template_expression.go @@ -0,0 +1,62 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Flow Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sema + +import "github.com/onflow/cadence/runtime/ast" + +// All number types, addresses, path types, bool, strings and characters are supported in string template +func isValidStringTemplateValue(valueType Type) bool { + switch valueType { + case TheAddressType, + StringType, + BoolType, + CharacterType: + return true + default: + return IsSubType(valueType, NumberType) || + IsSubType(valueType, PathType) + } +} + +func (checker *Checker) VisitStringTemplateExpression(stringTemplateExpression *ast.StringTemplateExpression) Type { + + // visit all elements + + var elementType Type + + elementCount := len(stringTemplateExpression.Expressions) + + if elementCount > 0 { + for _, element := range stringTemplateExpression.Expressions { + valueType := checker.VisitExpression(element, stringTemplateExpression, elementType) + + if !isValidStringTemplateValue(valueType) { + checker.report( + &TypeMismatchWithDescriptionError{ + ActualType: valueType, + ExpectedTypeDescription: "a type with built-in toString() or bool", + Range: ast.NewRangeFromPositioned(checker.memoryGauge, element), + }, + ) + } + } + } + + return StringType +} diff --git a/runtime/tests/checker/string_test.go b/runtime/tests/checker/string_test.go index d857185350..84e305d3b3 100644 --- a/runtime/tests/checker/string_test.go +++ b/runtime/tests/checker/string_test.go @@ -697,3 +697,94 @@ func TestCheckStringCount(t *testing.T) { require.NoError(t, err) }) } + +func TestCheckStringTemplate(t *testing.T) { + + t.Parallel() + + t.Run("valid, int", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + let a = 1 + let x: String = "The value of a is: \(a)" + `) + + require.NoError(t, err) + }) + + t.Run("valid, string", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + let a = "abc def" + let x: String = "\(a) ghi" + `) + + require.NoError(t, err) + }) + + t.Run("invalid, struct", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + access(all) + struct SomeStruct {} + let a = SomeStruct() + let x: String = "\(a)" + `) + + errs := RequireCheckerErrors(t, err, 1) + + assert.IsType(t, &sema.TypeMismatchWithDescriptionError{}, errs[0]) + }) + + t.Run("invalid, array", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + let x: [AnyStruct] = ["tmp", 1] + let y = "\(x)" + `) + + errs := RequireCheckerErrors(t, err, 1) + + assert.IsType(t, &sema.TypeMismatchWithDescriptionError{}, errs[0]) + }) + + t.Run("invalid, missing variable", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + let x: String = "\(a)" + `) + + errs := RequireCheckerErrors(t, err, 2) + + assert.IsType(t, &sema.NotDeclaredError{}, errs[0]) + assert.IsType(t, &sema.TypeMismatchWithDescriptionError{}, errs[1]) + }) + + t.Run("invalid, resource", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + access(all) resource TestResource {} + fun test(): String { + var x <- create TestResource() + var y = "\(x)" + destroy x + return y + } + `) + + errs := RequireCheckerErrors(t, err, 1) + + assert.IsType(t, &sema.TypeMismatchWithDescriptionError{}, errs[0]) + }) +} diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index f525e8d6ff..bf1f3a5c30 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12283,3 +12283,133 @@ func TestInterpretOptionalAddressInConditional(t *testing.T) { value, ) } + +func TestInterpretStringTemplates(t *testing.T) { + + t.Parallel() + + t.Run("int", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let x = 123 + let y = "x = \(x)" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredIntValueFromInt64(123), + inter.Globals.Get("x").GetValue(inter), + ) + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("x = 123"), + inter.Globals.Get("y").GetValue(inter), + ) + }) + + t.Run("multiple", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let x = 123.321 + let y = "abc" + let z = "\(y) and \(x)" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("abc and 123.32100000"), + inter.Globals.Get("z").GetValue(inter), + ) + }) + + t.Run("nested template", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let x = "{}" + let y = "[\(x)]" + let z = "(\(y))" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("([{}])"), + inter.Globals.Get("z").GetValue(inter), + ) + }) + + t.Run("boolean", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let x = false + let y = "\(x)" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("false"), + inter.Globals.Get("y").GetValue(inter), + ) + }) + + t.Run("func extracted", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let add = fun(): Int { + return 2+2 + } + let y = add() + let x: String = "\(y)" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("4"), + inter.Globals.Get("x").GetValue(inter), + ) + }) + + t.Run("path expr", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let a = /public/foo + let x = "file at \(a)" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("file at /public/foo"), + inter.Globals.Get("x").GetValue(inter), + ) + }) + + t.Run("consecutive", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let c = "C" + let a: Character = "A" + let n = "N" + let x = "\(c)\(a)\(n)" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("CAN"), + inter.Globals.Get("x").GetValue(inter), + ) + }) +}