diff --git a/ast/elementtype.go b/ast/elementtype.go index 1ceee39f3e..2369cf5080 100644 --- a/ast/elementtype.go +++ b/ast/elementtype.go @@ -85,4 +85,5 @@ const ( ElementTypeForceExpression ElementTypePathExpression ElementTypeAttachExpression + ElementTypeStringTemplateExpression ) diff --git a/ast/elementtype_string.go b/ast/elementtype_string.go index 9f2e36d996..07044923bb 100644 --- a/ast/elementtype_string.go +++ b/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/ast/expression.go b/ast/expression.go index 9c14f3924d..cb72d9d8c8 100644 --- a/ast/expression.go +++ b/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/ast/expression_extractor.go b/ast/expression_extractor.go index 002ff7794d..e3b52c71e8 100644 --- a/ast/expression_extractor.go +++ b/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/ast/precedence.go b/ast/precedence.go index 3e42f6a8f1..fcc78d259f 100644 --- a/ast/precedence.go +++ b/ast/precedence.go @@ -83,6 +83,7 @@ const ( // - BoolExpression // - NilExpression // - StringExpression + // - StringTemplateExpression // - IntegerExpression // - FixedPointExpression // - ArrayExpression diff --git a/ast/string_template_test.go b/ast/string_template_test.go new file mode 100644 index 0000000000..ebfc9e422f --- /dev/null +++ b/ast/string_template_test.go @@ -0,0 +1,48 @@ +/* + * 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/ast/visitor.go b/ast/visitor.go index dba16b2678..b4d946ec08 100644 --- a/ast/visitor.go +++ b/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/common/memorykind.go b/common/memorykind.go index 06e6748e9a..0754c397f5 100644 --- a/common/memorykind.go +++ b/common/memorykind.go @@ -204,6 +204,7 @@ const ( MemoryKindIntegerExpression MemoryKindFixedPointExpression MemoryKindArrayExpression + MemoryKindStringTemplateExpression MemoryKindDictionaryExpression MemoryKindIdentifierExpression MemoryKindInvocationExpression diff --git a/common/memorykind_string.go b/common/memorykind_string.go index 9b022e9495..67d6a0b8f8 100644 --- a/common/memorykind_string.go +++ b/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/common/metering.go b/common/metering.go index 43dce778d8..ec728f87cd 100644 --- a/common/metering.go +++ b/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/compiler/compiler.go b/compiler/compiler.go index e879b50d46..d7fefc2a14 100644 --- a/compiler/compiler.go +++ b/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/interpreter/interpreter.go b/interpreter/interpreter.go index 2835c13112..a163efb01f 100644 --- a/interpreter/interpreter.go +++ b/interpreter/interpreter.go @@ -4912,6 +4912,10 @@ func (interpreter *Interpreter) GetInterfaceType( typeID TypeID, ) (*sema.InterfaceType, error) { if location == nil { + var interfaceType = sema.NativeInterfaceTypes[qualifiedIdentifier] + if interfaceType != nil { + return interfaceType, nil + } return nil, InterfaceMissingLocationError{ QualifiedIdentifier: qualifiedIdentifier, } diff --git a/interpreter/interpreter_expression.go b/interpreter/interpreter_expression.go index 60cf329af1..5fecffec0e 100644 --- a/interpreter/interpreter_expression.go +++ b/interpreter/interpreter_expression.go @@ -20,6 +20,7 @@ package interpreter import ( "math/big" + "strings" "time" "github.com/onflow/atree" @@ -969,6 +970,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/interpreter/misc_test.go b/interpreter/misc_test.go index 9c8e7c8886..8ec9d08532 100644 --- a/interpreter/misc_test.go +++ b/interpreter/misc_test.go @@ -12380,3 +12380,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), + ) + }) +} diff --git a/interpreter/struct_stringer_test.go b/interpreter/struct_stringer_test.go new file mode 100644 index 0000000000..d81ea4e7a4 --- /dev/null +++ b/interpreter/struct_stringer_test.go @@ -0,0 +1,125 @@ +/* + * 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 interpreter_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/interpreter" + . "github.com/onflow/cadence/test_utils/interpreter_utils" +) + +func TestStringerBasic(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + access(all) + struct Example: StructStringer { + view fun toString(): String { + return "example" + } + } + fun test(): String { + return Example().toString() + } + `) + + result, err := inter.Invoke("test") + require.NoError(t, err) + + RequireValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("example"), + result, + ) +} + +func TestStringerBuiltIn(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + access(all) + fun test(): String { + let v = 1 + return v.toString() + } + `) + + result, err := inter.Invoke("test") + require.NoError(t, err) + + RequireValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("1"), + result, + ) +} + +func TestStringerCast(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + access(all) + fun test(): String { + var s = 1 + var somevalue = s as {StructStringer} + return somevalue.toString() + } + `) + + result, err := inter.Invoke("test") + require.NoError(t, err) + + RequireValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("1"), + result, + ) +} + +func TestStringerAsValue(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + access(all) + fun test(): String { + var v = Type<{StructStringer}>() + return v.identifier + } + `) + + result, err := inter.Invoke("test") + require.NoError(t, err) + + RequireValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("{StructStringer}"), + result, + ) +} diff --git a/parser/expression.go b/parser/expression.go index fef98c5f68..8499f95e60 100644 --- a/parser/expression.go +++ b/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/parser/expression_test.go b/parser/expression_test.go index 28e92f6404..172884de18 100644 --- a/parser/expression_test.go +++ b/parser/expression_test.go @@ -6042,6 +6042,462 @@ func TestParseStringWithUnicode(t *testing.T) { 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}, + }, + } + + 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}, + }, + } + + 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) + 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) + 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) + 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}, + }, + } + + 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) + 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) + 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) + 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) + 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}, + }, + } + + 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}, + }, + } + + 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}, + }, + } + + AssertEqualWithDiff(t, expected, actual) + }) +} + func TestParseNilCoalescing(t *testing.T) { t.Parallel() diff --git a/parser/lexer/lexer.go b/parser/lexer/lexer.go index 2a59bc9b52..8501b69240 100644 --- a/parser/lexer/lexer.go +++ b/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() { @@ -404,8 +417,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/parser/lexer/lexer_test.go b/parser/lexer/lexer_test.go index c529b750ce..3a30d729e4 100644 --- a/parser/lexer/lexer_test.go +++ b/parser/lexer/lexer_test.go @@ -1017,6 +1017,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/parser/lexer/state.go b/parser/lexer/state.go index 8bafc0e476..2dde3e1fd9 100644 --- a/parser/lexer/state.go +++ b/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/parser/lexer/tokentype.go b/parser/lexer/tokentype.go index 4310f312de..627c47055e 100644 --- a/parser/lexer/tokentype.go +++ b/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/sema/bool_type.go b/sema/bool_type.go index 36f1db3e70..1c19660bd8 100644 --- a/sema/bool_type.go +++ b/sema/bool_type.go @@ -31,6 +31,9 @@ var BoolType = &SimpleType{ Comparable: true, Exportable: true, Importable: true, + conformances: []*InterfaceType{ + StructStringerType, + }, } var BoolTypeAnnotation = NewTypeAnnotation(BoolType) diff --git a/sema/character.cdc b/sema/character.cdc index 7b54afdf5d..3532e4713a 100644 --- a/sema/character.cdc +++ b/sema/character.cdc @@ -1,6 +1,6 @@ access(all) -struct Character: Storable, Primitive, Equatable, Comparable, Exportable, Importable { +struct Character: Storable, Primitive, Equatable, Comparable, Exportable, Importable, StructStringer { /// The byte array of the UTF-8 encoding. access(all) diff --git a/sema/character.gen.go b/sema/character.gen.go index b0b10694f2..308e2e0dc4 100644 --- a/sema/character.gen.go +++ b/sema/character.gen.go @@ -59,6 +59,7 @@ var CharacterType = &SimpleType{ Exportable: true, Importable: true, ContainFields: false, + conformances: []*InterfaceType{StructStringerType}, } func init() { diff --git a/sema/check_string_template_expression.go b/sema/check_string_template_expression.go new file mode 100644 index 0000000000..44aa746d0a --- /dev/null +++ b/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/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/sema/gen/main.go b/sema/gen/main.go index f524af2894..49fdaf24b6 100644 --- a/sema/gen/main.go +++ b/sema/gen/main.go @@ -164,6 +164,9 @@ type typeDecl struct { memberDeclarations []ast.Declaration nestedTypes []*typeDecl hasConstructor bool + + // used in simpleType generation + conformances []*sema.InterfaceType } type generator struct { @@ -429,9 +432,40 @@ func (g *generator) addConstructorDocStringDeclaration( ) } -func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ struct{}) { +func (g *generator) VisitCompositeOrInterfaceDeclaration(decl ast.ConformingDeclaration) (_ struct{}) { + var compositeKind common.CompositeKind + var typeName string + var typeDec *typeDecl + var members []ast.Declaration + var conformances []*ast.NominalType + var isInterfaceType bool + + switch actualDecl := decl.(type) { + case *ast.CompositeDeclaration: + compositeKind = actualDecl.Kind() + typeName = actualDecl.Identifier.Identifier + typeDec = &typeDecl{ + typeName: typeName, + fullTypeName: g.newFullTypeName(typeName), + compositeKind: compositeKind, + } + members = actualDecl.Members.Declarations() + conformances = actualDecl.Conformances + isInterfaceType = false + case *ast.InterfaceDeclaration: + compositeKind = actualDecl.Kind() + typeName = actualDecl.Identifier.Identifier + typeDec = &typeDecl{ + typeName: typeName, + fullTypeName: g.newFullTypeName(typeName), + compositeKind: compositeKind, + } + members = actualDecl.Members.Declarations() + isInterfaceType = true + default: + panic("Expected composite or interface declaration") + } - compositeKind := decl.CompositeKind switch compositeKind { case common.CompositeKindStructure, common.CompositeKindResource, @@ -441,25 +475,17 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ panic(fmt.Sprintf("%s declarations are not supported", compositeKind.Name())) } - typeName := decl.Identifier.Identifier - - typeDecl := &typeDecl{ - typeName: typeName, - fullTypeName: g.newFullTypeName(typeName), - compositeKind: compositeKind, - } - if len(g.typeStack) > 0 { parentType := g.typeStack[len(g.typeStack)-1] parentType.nestedTypes = append( parentType.nestedTypes, - typeDecl, + typeDec, ) } g.typeStack = append( g.typeStack, - typeDecl, + typeDec, ) defer func() { // Pop @@ -473,6 +499,8 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ // Check if the declaration is explicitly marked to be generated as a composite type. if _, ok := g.leadingPragma["compositeType"]; ok { generateSimpleType = false + } else if isInterfaceType { + generateSimpleType = false } else { // If not, decide what to generate depending on the type. @@ -492,13 +520,13 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ } } - for _, memberDeclaration := range decl.Members.Declarations() { + for _, memberDeclaration := range members { generateDeclaration(g, memberDeclaration) // Visiting unsupported declarations panics, // so only supported member declarations are added - typeDecl.memberDeclarations = append( - typeDecl.memberDeclarations, + typeDec.memberDeclarations = append( + typeDec.memberDeclarations, memberDeclaration, ) @@ -514,7 +542,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ } } - for _, conformance := range decl.Conformances { + for _, conformance := range conformances { switch conformance.Identifier.Identifier { case "Storable": if !generateSimpleType { @@ -523,7 +551,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ g.currentTypeID(), )) } - typeDecl.storable = true + typeDec.storable = true case "Primitive": if !generateSimpleType { @@ -532,7 +560,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ g.currentTypeID(), )) } - typeDecl.primitive = true + typeDec.primitive = true case "Equatable": if !generateSimpleType { @@ -541,7 +569,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ g.currentTypeID(), )) } - typeDecl.equatable = true + typeDec.equatable = true case "Comparable": if !generateSimpleType { @@ -550,7 +578,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ g.currentTypeID(), )) } - typeDecl.comparable = true + typeDec.comparable = true case "Exportable": if !generateSimpleType { @@ -559,10 +587,10 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ g.currentTypeID(), )) } - typeDecl.exportable = true + typeDec.exportable = true case "Importable": - typeDecl.importable = true + typeDec.importable = true case "ContainFields": if !generateSimpleType { @@ -571,18 +599,20 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ g.currentTypeID(), )) } - typeDecl.memberAccessible = true + typeDec.memberAccessible = true + case "StructStringer": + typeDec.conformances = append(typeDec.conformances, sema.StructStringerType) } } var typeVarDecl dst.Expr if generateSimpleType { - typeVarDecl = simpleTypeLiteral(typeDecl) + typeVarDecl = simpleTypeLiteral(typeDec) } else { - typeVarDecl = compositeTypeExpr(typeDecl) + typeVarDecl = compositeOrInterfaceTypeExpr(typeDec, isInterfaceType) } - fullTypeName := typeDecl.fullTypeName + fullTypeName := typeDec.fullTypeName tyVarName := typeVarName(fullTypeName) @@ -597,7 +627,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ ), ) - memberDeclarations := typeDecl.memberDeclarations + memberDeclarations := typeDec.memberDeclarations if len(memberDeclarations) > 0 { @@ -700,7 +730,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ }, } - if typeDecl.hasConstructor { + if typeDec.hasConstructor { stmts = append( stmts, &dst.AssignStmt{ @@ -736,8 +766,12 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ return } -func (*generator) VisitInterfaceDeclaration(_ *ast.InterfaceDeclaration) struct{} { - panic("interface declarations are not supported") +func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ struct{}) { + return g.VisitCompositeOrInterfaceDeclaration(decl) +} + +func (g *generator) VisitInterfaceDeclaration(decl *ast.InterfaceDeclaration) (_ struct{}) { + return g.VisitCompositeOrInterfaceDeclaration(decl) } func (*generator) VisitAttachmentDeclaration(_ *ast.AttachmentDeclaration) struct{} { @@ -1591,6 +1625,9 @@ func simpleTypeLiteral(ty *typeDecl) dst.Expr { // Comparable: false, // Exportable: false, // Importable: false, + // comformances: []*InterfaceType { + // StructStringerType, + // } //} isResource := ty.compositeKind == common.CompositeKindResource @@ -1609,6 +1646,33 @@ func simpleTypeLiteral(ty *typeDecl) dst.Expr { goKeyValue("ContainFields", goBoolLit(ty.memberAccessible)), } + if len(ty.conformances) > 0 { + var elts = []dst.Expr{} + for _, conformance := range ty.conformances { + var name = "" + switch conformance { + case sema.StructStringerType: + name = "StructStringerType" + default: + panic("Unsupported conformance typeID") + } + elts = append(elts, &dst.Ident{ + Name: name, + Path: semaPath, + }) + } + elements = append(elements, goKeyValue("conformances", &dst.CompositeLit{ + Type: &dst.ArrayType{ + Elt: &dst.StarExpr{ + X: &dst.Ident{ + Name: "InterfaceType", + }, + }, + }, + Elts: elts, + })) + } + return &dst.UnaryExpr{ Op: token.AND, X: &dst.CompositeLit{ @@ -1971,10 +2035,10 @@ func stringMemberResolverMapType() *dst.MapType { } } -func compositeTypeExpr(ty *typeDecl) dst.Expr { +func compositeOrInterfaceTypeExpr(ty *typeDecl, isInterfaceType bool) dst.Expr { // func() *CompositeType { - // var t = &CompositeType{ + // var t = &CompositeType { // Identifier: FooTypeName, // Kind: common.CompositeKindStructure, // ImportableBuiltin: false, @@ -1985,13 +2049,23 @@ func compositeTypeExpr(ty *typeDecl) dst.Expr { // return t // }() + // func() *InterfaceType { + // var t = &InterfaceType{ + // Identifier: FooTypeName, + // CompositeKind: common.CompositeKindStructure, + // } + // + // t.SetNestedType(FooBarTypeName, FooBarType) + // return t + // }() + const typeVarName = "t" statements := []dst.Stmt{ &dst.DeclStmt{ Decl: goVarDecl( typeVarName, - compositeTypeLiteral(ty), + compositeOrInterfaceTypeLiteral(ty, isInterfaceType), ), }, } @@ -2023,6 +2097,11 @@ func compositeTypeExpr(ty *typeDecl) dst.Expr { }, ) + name := "CompositeType" + if isInterfaceType { + name = "InterfaceType" + } + return &dst.CallExpr{ Fun: &dst.FuncLit{ Type: &dst.FuncType{ @@ -2032,7 +2111,7 @@ func compositeTypeExpr(ty *typeDecl) dst.Expr { { Type: &dst.StarExpr{ X: &dst.Ident{ - Name: "CompositeType", + Name: name, Path: semaPath, }, }, @@ -2047,21 +2126,30 @@ func compositeTypeExpr(ty *typeDecl) dst.Expr { } } -func compositeTypeLiteral(ty *typeDecl) dst.Expr { +func compositeOrInterfaceTypeLiteral(ty *typeDecl, isInterfaceType bool) dst.Expr { kind := compositeKindExpr(ty.compositeKind) elements := []dst.Expr{ goKeyValue("Identifier", typeNameVarIdent(ty.fullTypeName)), - goKeyValue("Kind", kind), - goKeyValue("ImportableBuiltin", goBoolLit(ty.importable)), - goKeyValue("HasComputedMembers", goBoolLit(true)), + } + + name := "InterfaceType" + if isInterfaceType { + elements = append(elements, + goKeyValue("CompositeKind", kind)) + } else { + name = "CompositeType" + elements = append(elements, + goKeyValue("Kind", kind), + goKeyValue("ImportableBuiltin", goBoolLit(ty.importable)), + goKeyValue("HasComputedMembers", goBoolLit(true))) } return &dst.UnaryExpr{ Op: token.AND, X: &dst.CompositeLit{ Type: &dst.Ident{ - Name: "CompositeType", + Name: name, Path: semaPath, }, Elts: elements, diff --git a/sema/gen/testdata/simple_interface/test.cdc b/sema/gen/testdata/simple_interface/test.cdc new file mode 100644 index 0000000000..388ef59051 --- /dev/null +++ b/sema/gen/testdata/simple_interface/test.cdc @@ -0,0 +1 @@ +access(all) struct interface Test {} diff --git a/sema/gen/testdata/simple_interface/test.golden.go b/sema/gen/testdata/simple_interface/test.golden.go new file mode 100644 index 0000000000..c976eafded --- /dev/null +++ b/sema/gen/testdata/simple_interface/test.golden.go @@ -0,0 +1,36 @@ +// Code generated from testdata/simple_interface/test.cdc. DO NOT EDIT. +/* + * 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 simple_interface + +import ( + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/sema" +) + +const TestTypeName = "Test" + +var TestType = func() *sema.InterfaceType { + var t = &sema.InterfaceType{ + Identifier: TestTypeName, + CompositeKind: common.CompositeKindStructure, + } + + return t +}() diff --git a/sema/path_type.go b/sema/path_type.go index 4334054f60..042be61502 100644 --- a/sema/path_type.go +++ b/sema/path_type.go @@ -31,6 +31,9 @@ var PathType = &SimpleType{ Comparable: false, Exportable: true, Importable: true, + conformances: []*InterfaceType{ + StructStringerType, + }, } var PathTypeAnnotation = NewTypeAnnotation(PathType) @@ -48,6 +51,9 @@ var StoragePathType = &SimpleType{ Comparable: false, Exportable: true, Importable: true, + conformances: []*InterfaceType{ + StructStringerType, + }, } var StoragePathTypeAnnotation = NewTypeAnnotation(StoragePathType) @@ -65,6 +71,9 @@ var CapabilityPathType = &SimpleType{ Comparable: false, Exportable: true, Importable: true, + conformances: []*InterfaceType{ + StructStringerType, + }, } var CapabilityPathTypeAnnotation = NewTypeAnnotation(CapabilityPathType) @@ -82,6 +91,9 @@ var PublicPathType = &SimpleType{ Comparable: false, Exportable: true, Importable: true, + conformances: []*InterfaceType{ + StructStringerType, + }, } var PublicPathTypeAnnotation = NewTypeAnnotation(PublicPathType) @@ -99,6 +111,9 @@ var PrivatePathType = &SimpleType{ Comparable: false, Exportable: true, Importable: true, + conformances: []*InterfaceType{ + StructStringerType, + }, } var PrivatePathTypeAnnotation = NewTypeAnnotation(PrivatePathType) diff --git a/sema/simple_type.go b/sema/simple_type.go index f05db593c5..db8f8da6bb 100644 --- a/sema/simple_type.go +++ b/sema/simple_type.go @@ -51,6 +51,12 @@ type SimpleType struct { Primitive bool IsResource bool ContainFields bool + + // allow simple types to define a set of interfaces it conforms to + // e.g. StructStringer + conformances []*InterfaceType + effectiveInterfaceConformanceSet *InterfaceSet + effectiveInterfaceConformanceSetOnce sync.Once } var _ Type = &SimpleType{} @@ -195,3 +201,18 @@ func (t *SimpleType) CompositeKind() common.CompositeKind { func (t *SimpleType) CheckInstantiated(_ ast.HasPosition, _ common.MemoryGauge, _ func(err error)) { // NO-OP } + +func (t *SimpleType) EffectiveInterfaceConformanceSet() *InterfaceSet { + t.initializeEffectiveInterfaceConformanceSet() + return t.effectiveInterfaceConformanceSet +} + +func (t *SimpleType) initializeEffectiveInterfaceConformanceSet() { + t.effectiveInterfaceConformanceSetOnce.Do(func() { + t.effectiveInterfaceConformanceSet = NewInterfaceSet() + + for _, conformance := range t.conformances { + t.effectiveInterfaceConformanceSet.Add(conformance) + } + }) +} diff --git a/sema/string_test.go b/sema/string_test.go index 5faa94b3d9..cc46bc786d 100644 --- a/sema/string_test.go +++ b/sema/string_test.go @@ -698,3 +698,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/sema/string_type.go b/sema/string_type.go index 37a3707561..a3091cb248 100644 --- a/sema/string_type.go +++ b/sema/string_type.go @@ -143,6 +143,9 @@ var StringType = &SimpleType{ }, IndexingType: IntegerType, }, + conformances: []*InterfaceType{ + StructStringerType, + }, } var StringTypeAnnotation = NewTypeAnnotation(StringType) diff --git a/sema/struct_stringer.cdc b/sema/struct_stringer.cdc new file mode 100644 index 0000000000..224765fd90 --- /dev/null +++ b/sema/struct_stringer.cdc @@ -0,0 +1,7 @@ +/// StructStringer is an interface implemented by all the string convertible structs. +access(all) +struct interface StructStringer { + /// Returns the string representation of this object. + access(all) + view fun toString(): String +} diff --git a/sema/struct_stringer.gen.go b/sema/struct_stringer.gen.go new file mode 100644 index 0000000000..7f23bd22df --- /dev/null +++ b/sema/struct_stringer.gen.go @@ -0,0 +1,64 @@ +// Code generated from struct_stringer.cdc. DO NOT EDIT. +/* + * 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/ast" + "github.com/onflow/cadence/common" +) + +const StructStringerTypeToStringFunctionName = "toString" + +var StructStringerTypeToStringFunctionType = &FunctionType{ + Purity: FunctionPurityView, + ReturnTypeAnnotation: NewTypeAnnotation( + StringType, + ), +} + +const StructStringerTypeToStringFunctionDocString = ` +Returns the string representation of this object. +` + +const StructStringerTypeName = "StructStringer" + +var StructStringerType = func() *InterfaceType { + var t = &InterfaceType{ + Identifier: StructStringerTypeName, + CompositeKind: common.CompositeKindStructure, + } + + return t +}() + +func init() { + var members = []*Member{ + NewUnmeteredFunctionMember( + StructStringerType, + PrimitiveAccess(ast.AccessAll), + StructStringerTypeToStringFunctionName, + StructStringerTypeToStringFunctionType, + StructStringerTypeToStringFunctionDocString, + ), + } + + StructStringerType.Members = MembersAsMap(members) + StructStringerType.Fields = MembersFieldNames(members) +} diff --git a/sema/struct_stringer.go b/sema/struct_stringer.go new file mode 100644 index 0000000000..a91c8796c7 --- /dev/null +++ b/sema/struct_stringer.go @@ -0,0 +1,21 @@ +/* + * 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 + +//go:generate go run ./gen struct_stringer.cdc struct_stringer.gen.go diff --git a/sema/struct_stringer_test.go b/sema/struct_stringer_test.go new file mode 100644 index 0000000000..119f25471d --- /dev/null +++ b/sema/struct_stringer_test.go @@ -0,0 +1,76 @@ +/* + * 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_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/onflow/cadence/sema" + . "github.com/onflow/cadence/test_utils/sema_utils" +) + +func TestCheckStringer(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + let a: {StructStringer} = 1 + let b: {StructStringer} = false + let c: {StructStringer} = "hey" + access(all) + struct Foo: StructStringer { + view fun toString(): String { + return "foo" + } + } + let d: {StructStringer} = Foo() + let e: {StructStringer} = /public/foo + `) + + assert.NoError(t, err) +} + +func TestCheckInvalidStringer(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + resource R {} + + let a: {StructStringer} = <-create R() + let b: {StructStringer} = [<-create R()] + let c: {StructStringer} = {1: true} + struct Foo: StructStringer {} + struct Bar: StructStringer { + fun toString(): String { + return "bar" + } + } + `) + + errs := RequireCheckerErrors(t, err, 5) + + assert.IsType(t, &sema.TypeMismatchError{}, errs[0]) + assert.IsType(t, &sema.TypeMismatchError{}, errs[1]) + assert.IsType(t, &sema.TypeMismatchError{}, errs[2]) + assert.IsType(t, &sema.ConformanceError{}, errs[3]) + assert.IsType(t, &sema.ConformanceError{}, errs[4]) +} diff --git a/sema/type.go b/sema/type.go index c8d65c6d9b..9a13a2e762 100644 --- a/sema/type.go +++ b/sema/type.go @@ -312,6 +312,12 @@ func TypeActivationNestedType(typeActivation *VariableActivation, qualifiedIdent return ty } +// ConformingType is a type that can conform to interfaces +type ConformingType interface { + Type + EffectiveInterfaceConformanceSet() *InterfaceSet +} + // CompositeKindedType is a type which has a composite kind type CompositeKindedType interface { Type @@ -1197,7 +1203,9 @@ var _ IntegerRangedType = &NumericType{} var _ SaturatingArithmeticType = &NumericType{} func NewNumericType(typeName string) *NumericType { - return &NumericType{name: typeName} + return &NumericType{ + name: typeName, + } } func (t *NumericType) Tag() TypeTag { @@ -1375,6 +1383,17 @@ func (*NumericType) CheckInstantiated(_ ast.HasPosition, _ common.MemoryGauge, _ // NO-OP } +var numericTypeEffectiveInterfaceConformanceSet *InterfaceSet + +func init() { + numericTypeEffectiveInterfaceConformanceSet = NewInterfaceSet() + numericTypeEffectiveInterfaceConformanceSet.Add(StructStringerType) +} + +func (t *NumericType) EffectiveInterfaceConformanceSet() *InterfaceSet { + return numericTypeEffectiveInterfaceConformanceSet +} + // FixedPointNumericType represents all the types in the fixed-point range. type FixedPointNumericType struct { maxFractional *big.Int @@ -4210,6 +4229,7 @@ func init() { DeploymentResultType, HashableStructType, &InclusiveRangeType{}, + StructStringerType, }, ) @@ -7405,6 +7425,16 @@ func (t *AddressType) initializeMemberResolvers() { }) } +var addressTypeEffectiveInterfaceConformanceSet *InterfaceSet + +func init() { + addressTypeEffectiveInterfaceConformanceSet = NewInterfaceSet() + addressTypeEffectiveInterfaceConformanceSet.Add(StructStringerType) +} +func (t *AddressType) AddressInterfaceConformanceSet() *InterfaceSet { + return numericTypeEffectiveInterfaceConformanceSet +} + func IsPrimitiveOrContainerOfPrimitive(referencedType Type) bool { switch ty := referencedType.(type) { case *VariableSizedType: @@ -7821,7 +7851,7 @@ func checkSubTypeWithoutEquality(subType Type, superType Type) bool { IsSubsetOf(intersectionSubtype.EffectiveInterfaceConformanceSet()) } - case *CompositeType: + case ConformingType: // A type `T` // is a subtype of an intersection type `AnyResource{Us}` / `AnyStruct{Us}` / `Any{Us}`: // if `T` is a subtype of the intersection supertype, @@ -9511,10 +9541,49 @@ func (t *EntitlementMapType) CheckInstantiated(_ ast.HasPosition, _ common.Memor // NO-OP } +func extractNativeTypes( + types []Type, +) { + for len(types) > 0 { + lastIndex := len(types) - 1 + curType := types[lastIndex] + types[lastIndex] = nil + types = types[:lastIndex] + + switch actualType := curType.(type) { + case *CompositeType: + NativeCompositeTypes[actualType.QualifiedIdentifier()] = actualType + + nestedTypes := actualType.NestedTypes + if nestedTypes == nil { + continue + } + + nestedTypes.Foreach(func(_ string, nestedType Type) { + types = append(types, nestedType) + }) + case *InterfaceType: + NativeInterfaceTypes[actualType.QualifiedIdentifier()] = actualType + + nestedTypes := actualType.NestedTypes + if nestedTypes == nil { + continue + } + + nestedTypes.Foreach(func(_ string, nestedType Type) { + types = append(types, nestedType) + }) + default: + panic("Expected only composite or interface type") + } + + } +} + var NativeCompositeTypes = map[string]*CompositeType{} func init() { - compositeTypes := []*CompositeType{ + compositeTypes := []Type{ AccountKeyType, PublicKeyType, HashAlgorithmType, @@ -9523,26 +9592,15 @@ func init() { DeploymentResultType, } - for len(compositeTypes) > 0 { - lastIndex := len(compositeTypes) - 1 - compositeType := compositeTypes[lastIndex] - compositeTypes[lastIndex] = nil - compositeTypes = compositeTypes[:lastIndex] - - NativeCompositeTypes[compositeType.QualifiedIdentifier()] = compositeType + extractNativeTypes(compositeTypes) +} - nestedTypes := compositeType.NestedTypes - if nestedTypes == nil { - continue - } +var NativeInterfaceTypes = map[string]*InterfaceType{} - nestedTypes.Foreach(func(_ string, nestedType Type) { - nestedCompositeType, ok := nestedType.(*CompositeType) - if !ok { - return - } - - compositeTypes = append(compositeTypes, nestedCompositeType) - }) +func init() { + interfaceTypes := []Type{ + StructStringerType, } + + extractNativeTypes(interfaceTypes) } diff --git a/sema/type_test.go b/sema/type_test.go index 1ede9831c9..9d2cf503ac 100644 --- a/sema/type_test.go +++ b/sema/type_test.go @@ -2378,6 +2378,10 @@ func TestTypeInclusions(t *testing.T) { return } + if _, ok := typ.(*InterfaceType); ok { + return + } + if typ.IsResourceType() { return }