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..2efc783155 100644 --- a/runtime/ast/expression.go +++ b/runtime/ast/expression.go @@ -220,6 +220,64 @@ func (*StringExpression) precedence() precedence { return precedenceLiteral } +// StringTemplateExpression + +type StringTemplateExpression struct { + Values []string + Expressions []Expression + Range +} + +var _ Expression = &StringTemplateExpression{} + +func NewStringTemplateExpression(gauge common.MemoryGauge, values []string, exprs []Expression, exprRange Range) *StringTemplateExpression { + // STRINGTODO: change to be similar to array memory usage? + common.UseMemory(gauge, common.StringExpressionMemoryUsage) + 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 (*StringTemplateExpression) Walk(_ func(Element)) { + // NO-OP +} + +func (e *StringTemplateExpression) String() string { + return Prettier(e) +} + +func (e *StringTemplateExpression) Doc() prettier.Doc { + return prettier.Text(QuoteString("String template")) +} + +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/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/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..264b39edf8 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,33 @@ func (interpreter *Interpreter) VisitStringExpression(expression *ast.StringExpr return NewUnmeteredStringValue(expression.Value) } +func (interpreter *Interpreter) VisitStringTemplateExpression(expression *ast.StringTemplateExpression) Value { + values := interpreter.visitExpressionsNonCopying(expression.Expressions) + + templatesType := interpreter.Program.Elaboration.StringTemplateExpressionTypes(expression) + argumentTypes := templatesType.ArgumentTypes + + var builder strings.Builder + for i, str := range expression.Values { + builder.WriteString(str) + if i < len(values) { + // STRINGTODO: is this how the conversion should happen? + s := values[i].String() + switch argumentTypes[i] { + case sema.StringType: + // remove quotations + s = s[1 : len(s)-1] + builder.WriteString(s) + default: + builder.WriteString(s) + } + } + } + + // STRINGTODO: already metered as a string constant in parser? + 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..1fc35e3468 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,112 @@ 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 + + // early check for start " of string literal because of string templates + 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 late end " check + missingEnd := true + + for curToken.Is(lexer.TokenString) { + // this loop alternates between parsing a string and then a string template + // it is expected that any valid StringTemplateExpression will end with a string + literal = p.tokenSource(curToken) + length = len(literal) + + 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) { + // move on to what is after the $ + p.next() + + // check if $identifier or ${expression} + var isCurly = p.current.Is(lexer.TokenBraceOpen) + if !isCurly && !p.current.Is(lexer.TokenIdentifier) { + return nil, p.syntaxError("expected an identifier got: %s", p.currentTokenSource()) + } + if isCurly { + p.next() // move on to expression + } + value, err := parseExpression(p, lowestBindingPower) + if isCurly { + _, err = p.mustOne(lexer.TokenBraceClose) + } + 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 + } + } + + // late check for end " of string literal because of string templates + 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..4d6ed93ba8 100644 --- a/runtime/parser/expression_test.go +++ b/runtime/parser/expression_test.go @@ -6055,6 +6055,222 @@ 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: 5, Line: 2, Column: 4}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + EndPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + } + + 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: 20, Line: 2, Column: 19}, + }, + }, + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "def", + Pos: ast.Position{Offset: 24, Line: 2, Column: 24}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + EndPos: ast.Position{Offset: 32, Line: 2, Column: 32}, + }, + } + + 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: 25, Line: 2, Column: 25}, + }, + }, + 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: "expected an identifier got: $", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + 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 an identifier got: (", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + errs, + ) + }) + + t.Run("valid, expression", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "2+2 = ${2+2}" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + }) + + t.Run("invalid, missing brace", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "2+2 = ${2+2" + `) + + 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: 17, Line: 2, Column: 16}, + }, + }, + errs, + ) + }) +} + func TestParseNilCoalescing(t *testing.T) { t.Parallel() diff --git a/runtime/parser/lexer/lexer.go b/runtime/parser/lexer/lexer.go index 7b69245ce2..e5e224f850 100644 --- a/runtime/parser/lexer/lexer.go +++ b/runtime/parser/lexer/lexer.go @@ -49,6 +49,14 @@ type position struct { column int } +type LexerMode int + +const ( + NORMAL = iota + STR_IDENTIFIER + STR_EXPRESSION +) + type lexer struct { // memoryGauge is used for metering memory usage memoryGauge common.MemoryGauge @@ -74,6 +82,8 @@ type lexer struct { prev rune // canBackup indicates whether stepping back is allowed canBackup bool + // lexer mode is used for string templates + mode LexerMode } var _ TokenStream = &lexer{} @@ -130,6 +140,7 @@ func (l *lexer) clear() { l.cursor = 0 l.tokens = l.tokens[:0] l.tokenCount = 0 + l.mode = NORMAL } func (l *lexer) Reclaim() { @@ -414,6 +425,11 @@ func (l *lexer) scanString(quote rune) { l.backupOne() return } + case '$': + // string template, stop and set mode + l.backupOne() + l.mode = STR_IDENTIFIER + return } r = l.next() } diff --git a/runtime/parser/lexer/lexer_test.go b/runtime/parser/lexer/lexer_test.go index 51f8f53f34..173d7615e3 100644 --- a/runtime/parser/lexer/lexer_test.go +++ b/runtime/parser/lexer/lexer_test.go @@ -1014,6 +1014,341 @@ 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: 1, Offset: 1}, + }, + }, + Source: `$`, + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: `abc`, + }, + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + Source: `.length"`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + EndPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + }, + }, + }, + }, + ) + }) + + t.Run("invalid, number string template", func(t *testing.T) { + testLex(t, + `"$1"`, + []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: 1, Offset: 1}, + }, + }, + Source: `$`, + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: `1`, + }, + { + Token: Token{ + Type: TokenString, + 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: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + }, + ) + }) + + t.Run("invalid, 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: 1, Offset: 1}, + }, + }, + Source: `$`, + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: `a`, + }, + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Source: ` + 2`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + }, + }, + ) + }) + + 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: 1, Offset: 1}, + }, + }, + Source: `$`, + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: `a`, + }, + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 2}, + }, + }, + Source: ``, + }, + { + Token: Token{ + Type: TokenStringTemplate, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 3}, + }, + }, + Source: `$`, + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 4}, + }, + }, + Source: `b`, + }, + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 5}, + }, + }, + Source: `"`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 6}, + }, + }, + }, + }, + ) + }) + + t.Run("valid, str expr 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: 1, Offset: 1}, + }, + }, + Source: `$`, + }, + { + Token: Token{ + Type: TokenBraceOpen, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + 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: TokenBraceClose, + 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("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..315ff25b07 100644 --- a/runtime/parser/lexer/state.go +++ b/runtime/parser/lexer/state.go @@ -40,6 +40,12 @@ func rootState(l *lexer) stateFn { switch r { case EOF: return nil + case '$': + if l.mode == STR_EXPRESSION || l.mode == STR_IDENTIFIER { + l.emitType(TokenStringTemplate) + } else { + return l.error(fmt.Errorf("unrecognized character: %#U", r)) + } case '+': l.emitType(TokenPlus) case '-': @@ -61,8 +67,17 @@ func rootState(l *lexer) stateFn { l.emitType(TokenParenClose) case '{': l.emitType(TokenBraceOpen) + if l.mode == STR_IDENTIFIER { + l.mode = STR_EXPRESSION + } else if l.mode == STR_EXPRESSION { + return l.error(fmt.Errorf("string template cannot contain {")) + } case '}': l.emitType(TokenBraceClose) + if l.mode == STR_EXPRESSION { + l.mode = NORMAL + return stringState + } case '[': l.emitType(TokenBracketOpen) case ']': @@ -296,6 +311,10 @@ func identifierState(l *lexer) stateFn { } } l.emitType(TokenIdentifier) + if l.mode == STR_IDENTIFIER { + l.mode = NORMAL + return stringState + } return rootState } diff --git a/runtime/parser/lexer/tokentype.go b/runtime/parser/lexer/tokentype.go index 0a15c19b6f..a7b3cb2f92 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..276b8c0f14 --- /dev/null +++ b/runtime/sema/check_string_template_expression.go @@ -0,0 +1,52 @@ +/* + * 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" + +func (checker *Checker) VisitStringTemplateExpression(stringTemplateExpression *ast.StringTemplateExpression) Type { + + // visit all elements + + var elementType Type + + elementCount := len(stringTemplateExpression.Expressions) + + var argumentTypes []Type + if elementCount > 0 { + argumentTypes = make([]Type, elementCount) + + for i, element := range stringTemplateExpression.Expressions { + valueType := checker.VisitExpression(element, stringTemplateExpression, elementType) + + argumentTypes[i] = valueType + + checker.checkResourceMoveOperation(element, valueType) + } + } + + checker.Elaboration.SetStringTemplateExpressionTypes( + stringTemplateExpression, + StringTemplateExpressionTypes{ + ArgumentTypes: argumentTypes, + }, + ) + + return StringType +} diff --git a/runtime/sema/elaboration.go b/runtime/sema/elaboration.go index b6b025eef0..d2ef948a78 100644 --- a/runtime/sema/elaboration.go +++ b/runtime/sema/elaboration.go @@ -79,6 +79,10 @@ type ArrayExpressionTypes struct { ArgumentTypes []Type } +type StringTemplateExpressionTypes struct { + ArgumentTypes []Type +} + type DictionaryExpressionTypes struct { DictionaryType *DictionaryType EntryTypes []DictionaryEntryType @@ -140,6 +144,7 @@ type Elaboration struct { dictionaryExpressionTypes map[*ast.DictionaryExpression]DictionaryExpressionTypes integerExpressionTypes map[*ast.IntegerExpression]Type stringExpressionTypes map[*ast.StringExpression]Type + stringTemplateExpressionTypes map[*ast.StringTemplateExpression]StringTemplateExpressionTypes returnStatementTypes map[*ast.ReturnStatement]ReturnStatementTypes functionDeclarationFunctionTypes map[*ast.FunctionDeclaration]*FunctionType variableDeclarationTypes map[*ast.VariableDeclaration]VariableDeclarationTypes @@ -480,6 +485,21 @@ func (e *Elaboration) SetStringExpressionType(expression *ast.StringExpression, e.stringExpressionTypes[expression] = ty } +func (e *Elaboration) StringTemplateExpressionTypes(expression *ast.StringTemplateExpression) (types StringTemplateExpressionTypes) { + if e.stringTemplateExpressionTypes == nil { + return + } + // default, Elaboration.SetStringExpressionType + return e.stringTemplateExpressionTypes[expression] +} + +func (e *Elaboration) SetStringTemplateExpressionTypes(expression *ast.StringTemplateExpression, types StringTemplateExpressionTypes) { + if e.stringTemplateExpressionTypes == nil { + e.stringTemplateExpressionTypes = map[*ast.StringTemplateExpression]StringTemplateExpressionTypes{} + } + e.stringTemplateExpressionTypes[expression] = types +} + func (e *Elaboration) ReturnStatementTypes(statement *ast.ReturnStatement) (types ReturnStatementTypes) { if e.returnStatementTypes == nil { return diff --git a/runtime/tests/checker/string_test.go b/runtime/tests/checker/string_test.go index d857185350..ee2609758f 100644 --- a/runtime/tests/checker/string_test.go +++ b/runtime/tests/checker/string_test.go @@ -697,3 +697,59 @@ 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("valid, struct", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + access(all) + struct SomeStruct {} + let a = SomeStruct() + let x: String = "$a" + `) + + require.NoError(t, err) + }) + + t.Run("invalid, missing variable", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + let x: String = "$a" + `) + + errs := RequireCheckerErrors(t, err, 1) + + assert.IsType(t, &sema.NotDeclaredError{}, errs[0]) + }) +} diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index f525e8d6ff..ff428b4dde 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12283,3 +12283,168 @@ 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("struct", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + access(all) + struct SomeStruct {} + let a = SomeStruct() + let x: String = "$a" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("S.test.SomeStruct()"), + inter.Globals.Get("x").GetValue(inter), + ) + }) + + t.Run("func", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let add = fun(): Int { + return 2+2 + } + let x: String = "$add()" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("fun(): Int()"), + inter.Globals.Get("x").GetValue(inter), + ) + }) + + t.Run("func", 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("expression", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let x: String = "${2+2}" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("4"), + inter.Globals.Get("x").GetValue(inter), + ) + }) + + t.Run("expr func", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let add = fun(): Int { + return 2+2 + } + let x: String = "${add()}" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("4"), + inter.Globals.Get("x").GetValue(inter), + ) + }) + + t.Run("simple expr", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let y: String = "abcde" + let x: String = "$y.length = ${y.length}" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("abcde.length = 5"), + inter.Globals.Get("x").GetValue(inter), + ) + }) +}