From d0783263f5ea99bf77398b031529be4d85e8d7ea Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 18 Sep 2024 11:08:02 -0400 Subject: [PATCH 01/38] Modify lexer to support modes for string templates. --- runtime/parser/lexer/lexer.go | 15 ++++ runtime/parser/lexer/lexer_test.go | 114 +++++++++++++++++++++++++++++ runtime/parser/lexer/state.go | 10 +++ runtime/parser/lexer/tokentype.go | 3 + 4 files changed, 142 insertions(+) diff --git a/runtime/parser/lexer/lexer.go b/runtime/parser/lexer/lexer.go index 7b69245ce2..5e9caf2f79 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{} @@ -414,6 +424,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..4206edd22e 100644 --- a/runtime/parser/lexer/lexer_test.go +++ b/runtime/parser/lexer/lexer_test.go @@ -1014,6 +1014,120 @@ 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, 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, 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..7e9561ba32 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 '-': @@ -296,6 +302,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()) } From 7ebe45bee7b77f940b46920a39bbe9f864f6e404 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 18 Sep 2024 12:53:33 -0400 Subject: [PATCH 02/38] Add parsing support for string templates. --- runtime/ast/expression.go | 57 ++++++++++++++++ runtime/ast/precedence.go | 1 + runtime/parser/expression.go | 106 ++++++++++++++++++++++++++---- runtime/parser/expression_test.go | 64 ++++++++++++++++++ 4 files changed, 215 insertions(+), 13 deletions(-) diff --git a/runtime/ast/expression.go b/runtime/ast/expression.go index 0142b92b20..800cf44668 100644 --- a/runtime/ast/expression.go +++ b/runtime/ast/expression.go @@ -220,6 +220,63 @@ 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 { + common.UseMemory(gauge, common.StringExpressionMemoryUsage) + return &StringTemplateExpression{ + Values: values, + Expressions: exprs, + Range: exprRange, + } +} + +var _ Element = &StringExpression{} +var _ Expression = &StringExpression{} + +func (*StringTemplateExpression) ElementType() ElementType { + return ElementTypeStringExpression +} + +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/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/parser/expression.go b/runtime/parser/expression.go index 41f44df7a7..0cf9186704 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,98 @@ 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) { + literal = p.tokenSource(curToken) + length = len(literal) + + if length >= 1 && literal[0] == '"' { + 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) { + p.next() + // advance to the expression + value, err := parseExpression(p, lowestBindingPower) + 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..9f12f47cec 100644 --- a/runtime/parser/expression_test.go +++ b/runtime/parser/expression_test.go @@ -6055,6 +6055,70 @@ func TestParseStringWithUnicode(t *testing.T) { utils.AssertEqualWithDiff(t, expected, actual) } +func TestParseStringTemplates(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: 24, Line: 2, Column: 23}, + }, + }, + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "def", + Pos: ast.Position{Offset: 29, Line: 2, Column: 28}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, + EndPos: ast.Position{Offset: 37, Line: 2, Column: 36}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) +} + +func TestParseStringTemplateFail(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) +} + func TestParseNilCoalescing(t *testing.T) { t.Parallel() From 0795b4c02b55b5eb59e5b7fbedb64367750f4319 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 18 Sep 2024 15:58:10 -0400 Subject: [PATCH 03/38] Checking and interpreting for string templates. --- runtime/ast/elementtype.go | 1 + runtime/ast/expression.go | 2 +- runtime/ast/expression_extractor.go | 84 +++++++++++++------ runtime/ast/visitor.go | 4 + runtime/interpreter/interpreter_expression.go | 28 +++++++ runtime/parser/expression.go | 11 +-- runtime/parser/expression_test.go | 41 ++++++++- .../sema/check_string_template_expression.go | 52 ++++++++++++ runtime/sema/elaboration.go | 20 +++++ runtime/tests/checker/string_test.go | 42 ++++++++++ 10 files changed, 253 insertions(+), 32 deletions(-) create mode 100644 runtime/sema/check_string_template_expression.go 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/expression.go b/runtime/ast/expression.go index 800cf44668..2e2ee2f7f2 100644 --- a/runtime/ast/expression.go +++ b/runtime/ast/expression.go @@ -243,7 +243,7 @@ var _ Element = &StringExpression{} var _ Expression = &StringExpression{} func (*StringTemplateExpression) ElementType() ElementType { - return ElementTypeStringExpression + return ElementTypeStringTemplateExpression } func (*StringTemplateExpression) isExpression() {} 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/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/interpreter/interpreter_expression.go b/runtime/interpreter/interpreter_expression.go index d2894749bf..4d25ab9c11 100644 --- a/runtime/interpreter/interpreter_expression.go +++ b/runtime/interpreter/interpreter_expression.go @@ -957,6 +957,34 @@ func (interpreter *Interpreter) VisitStringExpression(expression *ast.StringExpr return NewUnmeteredStringValue(expression.Value) } +func (interpreter *Interpreter) VisitStringTemplateExpression(expression *ast.StringTemplateExpression) Value { + values := interpreter.visitExpressionsNonCopying(expression.Expressions) + + templateExpressionTypes := interpreter.Program.Elaboration.StringTemplateExpressionTypes(expression) + argumentTypes := templateExpressionTypes.ArgumentTypes + + var copies []Value + + count := len(values) + if count > 0 { + copies = make([]Value, count) + for i, argument := range values { + argumentType := argumentTypes[i] + argumentExpression := expression.Expressions[i] + locationRange := LocationRange{ + Location: interpreter.Location, + HasPosition: argumentExpression, + } + copies[i] = interpreter.transferAndConvert(argument, argumentType, sema.StringType, locationRange) + } + } + + result := "" + + // NOTE: already metered in lexer/parser + return NewUnmeteredStringValue(result) +} + 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 0cf9186704..45290a3093 100644 --- a/runtime/parser/expression.go +++ b/runtime/parser/expression.go @@ -1167,16 +1167,17 @@ func defineStringExpression() { literal = p.tokenSource(curToken) length = len(literal) - if length >= 1 && literal[0] == '"' { - literal = literal[1:] - length = len(literal) - } - + // this order of removal matters in case the token is the single quotation " if length >= 1 && literal[length-1] == '"' { literal = literal[:length-1] + length = len(literal) missingEnd = false } + if length >= 1 && literal[0] == '"' { + literal = literal[1:] + } + parsedString := parseStringLiteralContent(p, literal) literals = append(literals, parsedString) endToken = curToken diff --git a/runtime/parser/expression_test.go b/runtime/parser/expression_test.go index 9f12f47cec..95ea2cb792 100644 --- a/runtime/parser/expression_test.go +++ b/runtime/parser/expression_test.go @@ -6055,7 +6055,46 @@ func TestParseStringWithUnicode(t *testing.T) { utils.AssertEqualWithDiff(t, expected, actual) } -func TestParseStringTemplates(t *testing.T) { +func TestParseStringTemplateSimple(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: 9, Line: 2, Column: 8}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, + EndPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) +} + +func TestParseStringTemplateMulti(t *testing.T) { t.Parallel() 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..de16b37bcc 100644 --- a/runtime/tests/checker/string_test.go +++ b/runtime/tests/checker/string_test.go @@ -697,3 +697,45 @@ 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, 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]) + }) +} From 32f1c1d734adfd3ea7b4201933155c84cd305ba1 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Thu, 19 Sep 2024 11:13:05 -0400 Subject: [PATCH 04/38] Interpret string templates with basic tests --- runtime/ast/expression.go | 1 + runtime/interpreter/interpreter_expression.go | 38 ++++++------ runtime/tests/interpreter/interpreter_test.go | 61 +++++++++++++++++++ 3 files changed, 81 insertions(+), 19 deletions(-) diff --git a/runtime/ast/expression.go b/runtime/ast/expression.go index 2e2ee2f7f2..2efc783155 100644 --- a/runtime/ast/expression.go +++ b/runtime/ast/expression.go @@ -231,6 +231,7 @@ type StringTemplateExpression struct { 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, diff --git a/runtime/interpreter/interpreter_expression.go b/runtime/interpreter/interpreter_expression.go index 4d25ab9c11..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" @@ -960,29 +961,28 @@ func (interpreter *Interpreter) VisitStringExpression(expression *ast.StringExpr func (interpreter *Interpreter) VisitStringTemplateExpression(expression *ast.StringTemplateExpression) Value { values := interpreter.visitExpressionsNonCopying(expression.Expressions) - templateExpressionTypes := interpreter.Program.Elaboration.StringTemplateExpressionTypes(expression) - argumentTypes := templateExpressionTypes.ArgumentTypes - - var copies []Value - - count := len(values) - if count > 0 { - copies = make([]Value, count) - for i, argument := range values { - argumentType := argumentTypes[i] - argumentExpression := expression.Expressions[i] - locationRange := LocationRange{ - Location: interpreter.Location, - HasPosition: argumentExpression, + 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) } - copies[i] = interpreter.transferAndConvert(argument, argumentType, sema.StringType, locationRange) } } - result := "" - - // NOTE: already metered in lexer/parser - return NewUnmeteredStringValue(result) + // STRINGTODO: already metered as a string constant in parser? + return NewUnmeteredStringValue(builder.String()) } func (interpreter *Interpreter) VisitArrayExpression(expression *ast.ArrayExpression) Value { diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index f525e8d6ff..86e89ac73b 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12283,3 +12283,64 @@ 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), + ) + }) +} From 6a36212d1a3f71280eb15684fb2f66d623fa5db1 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Thu, 19 Sep 2024 11:34:12 -0400 Subject: [PATCH 05/38] Add placeholder for compiler linting. --- runtime/compiler/compiler.go | 5 +++++ 1 file changed, 5 insertions(+) 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{ From d90bbf689b8f88367180088fc3c0ce9a5cb58270 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Thu, 19 Sep 2024 11:54:20 -0400 Subject: [PATCH 06/38] Fix parsing of invalid strings. --- runtime/parser/expression.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/runtime/parser/expression.go b/runtime/parser/expression.go index 45290a3093..e3b6bfface 100644 --- a/runtime/parser/expression.go +++ b/runtime/parser/expression.go @@ -1167,15 +1167,14 @@ func defineStringExpression() { literal = p.tokenSource(curToken) length = len(literal) - // this order of removal matters in case the token is the single quotation " - if length >= 1 && literal[length-1] == '"' { - literal = literal[:length-1] + if curToken == startToken { + literal = literal[1:] length = len(literal) - missingEnd = false } - if length >= 1 && literal[0] == '"' { - literal = literal[1:] + if length >= 1 && literal[length-1] == '"' { + literal = literal[:length-1] + missingEnd = false } parsedString := parseStringLiteralContent(p, literal) From 81c300460114580adaaaec9eb4aaf82c95236f96 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Thu, 19 Sep 2024 12:38:39 -0400 Subject: [PATCH 07/38] Run stringer to update elementtype file. --- runtime/ast/elementtype_string.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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) { From 99f4ae46cc3371e7f82561062de8d8113ea4f78f Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Fri, 20 Sep 2024 13:59:56 -0400 Subject: [PATCH 08/38] Improve error messages and tests --- runtime/parser/expression.go | 3 + runtime/parser/expression_test.go | 214 ++++++++++++------ runtime/parser/lexer/lexer_test.go | 146 +++++++++++- runtime/tests/checker/string_test.go | 14 ++ runtime/tests/interpreter/interpreter_test.go | 55 +++++ 5 files changed, 358 insertions(+), 74 deletions(-) diff --git a/runtime/parser/expression.go b/runtime/parser/expression.go index e3b6bfface..01d9dba5d1 100644 --- a/runtime/parser/expression.go +++ b/runtime/parser/expression.go @@ -1186,6 +1186,9 @@ func defineStringExpression() { if curToken.Is(lexer.TokenStringTemplate) { p.next() // advance to the expression + if !p.current.Is(lexer.TokenIdentifier) { + return nil, p.syntaxError("expected an identifier got: %s", p.currentTokenSource()) + } value, err := parseExpression(p, lowestBindingPower) if err != nil { return nil, err diff --git a/runtime/parser/expression_test.go b/runtime/parser/expression_test.go index 95ea2cb792..dabaff5057 100644 --- a/runtime/parser/expression_test.go +++ b/runtime/parser/expression_test.go @@ -6055,107 +6055,175 @@ func TestParseStringWithUnicode(t *testing.T) { utils.AssertEqualWithDiff(t, expected, actual) } -func TestParseStringTemplateSimple(t *testing.T) { +func TestParseStringTemplate(t *testing.T) { t.Parallel() - actual, errs := testParseExpression(` - "$test" - `) + t.Run("simple", func(t *testing.T) { - var err error - if len(errs) > 0 { - err = Error{ - Errors: errs, + t.Parallel() + + actual, errs := testParseExpression(` + "$test" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } } - } - require.NoError(t, err) + require.NoError(t, err) - expected := &ast.StringTemplateExpression{ - Values: []string{ - "", - "", - }, - Expressions: []ast.Expression{ - &ast.IdentifierExpression{ - Identifier: ast.Identifier{ - Identifier: "test", - Pos: ast.Position{Offset: 9, Line: 2, Column: 8}, + 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: 7, Line: 2, Column: 6}, - EndPos: ast.Position{Offset: 13, Line: 2, Column: 12}, - }, - } + 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) -} + utils.AssertEqualWithDiff(t, expected, actual) + }) -func TestParseStringTemplateMulti(t *testing.T) { + t.Run("multi", func(t *testing.T) { - t.Parallel() + t.Parallel() - actual, errs := testParseExpression(` - "this is a test $abc $def test" - `) + actual, errs := testParseExpression(` + "this is a test $abc$def test" + `) - var err error - if len(errs) > 0 { - err = Error{ - Errors: errs, + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } } - } - require.NoError(t, err) + 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: 24, Line: 2, Column: 23}, + 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}, + }, }, }, - &ast.IdentifierExpression{ - Identifier: ast.Identifier{ - Identifier: "def", - Pos: ast.Position{Offset: 29, Line: 2, Column: 28}, + 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}, }, }, - }, - Range: ast.Range{ - StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, - EndPos: ast.Position{Offset: 37, Line: 2, Column: 36}, - }, - } + errs, + ) + }) - utils.AssertEqualWithDiff(t, expected, actual) -} + t.Run("invalid identifier", func(t *testing.T) { -func TestParseStringTemplateFail(t *testing.T) { + t.Parallel() - t.Parallel() + _, errs := testParseExpression(` + "$$" + `) - _, errs := testParseExpression(` - "this is a test $FOO - `) + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } - 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) + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected an identifier got: (", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + errs, + ) + }) } func TestParseNilCoalescing(t *testing.T) { diff --git a/runtime/parser/lexer/lexer_test.go b/runtime/parser/lexer/lexer_test.go index 4206edd22e..643f57e902 100644 --- a/runtime/parser/lexer/lexer_test.go +++ b/runtime/parser/lexer/lexer_test.go @@ -1071,7 +1071,7 @@ func TestLexString(t *testing.T) { ) }) - t.Run("invalid, string template", func(t *testing.T) { + t.Run("invalid, number string template", func(t *testing.T) { testLex(t, `"$1"`, []token{ @@ -1128,6 +1128,150 @@ func TestLexString(t *testing.T) { ) }) + 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("invalid, empty, not terminated at line end", func(t *testing.T) { testLex(t, "\"\n", diff --git a/runtime/tests/checker/string_test.go b/runtime/tests/checker/string_test.go index de16b37bcc..ee2609758f 100644 --- a/runtime/tests/checker/string_test.go +++ b/runtime/tests/checker/string_test.go @@ -726,6 +726,20 @@ func TestCheckStringTemplate(t *testing.T) { 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() diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index 86e89ac73b..3669f17960 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12343,4 +12343,59 @@ func TestInterpretStringTemplates(t *testing.T) { 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), + ) + }) } From cfa540ecd8299df8190919ae76669b6361353473 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Mon, 23 Sep 2024 12:07:45 -0400 Subject: [PATCH 09/38] Allow escaping of string template $ and add tests. --- runtime/parser/expression.go | 2 ++ runtime/parser/expression_test.go | 28 +++++++++++++++++++ runtime/parser/lexer/lexer_test.go | 27 ++++++++++++++++++ runtime/tests/interpreter/interpreter_test.go | 16 +++++++++++ 4 files changed, 73 insertions(+) diff --git a/runtime/parser/expression.go b/runtime/parser/expression.go index 01d9dba5d1..9e93990e0b 100644 --- a/runtime/parser/expression.go +++ b/runtime/parser/expression.go @@ -1735,6 +1735,8 @@ func parseStringLiteralContent(p *parser, s []byte) (result string) { builder.WriteByte('\t') case '"': builder.WriteByte('"') + case '$': + builder.WriteByte('$') case '\'': builder.WriteByte('\'') case '\\': diff --git a/runtime/parser/expression_test.go b/runtime/parser/expression_test.go index dabaff5057..b7b8489a5f 100644 --- a/runtime/parser/expression_test.go +++ b/runtime/parser/expression_test.go @@ -6098,6 +6098,34 @@ func TestParseStringTemplate(t *testing.T) { utils.AssertEqualWithDiff(t, expected, actual) }) + t.Run("escaped", func(t *testing.T) { + + t.Parallel() + + actual, errs := testParseExpression(` + "\$1.00" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + + expected := &ast.StringExpression{ + Value: "$1.00", + Range: ast.Range{ + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + EndPos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) + }) + t.Run("multi", func(t *testing.T) { t.Parallel() diff --git a/runtime/parser/lexer/lexer_test.go b/runtime/parser/lexer/lexer_test.go index 643f57e902..fddebcf064 100644 --- a/runtime/parser/lexer/lexer_test.go +++ b/runtime/parser/lexer/lexer_test.go @@ -1071,6 +1071,33 @@ func TestLexString(t *testing.T) { ) }) + t.Run("valid, escaped 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, `"$1"`, diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index 3669f17960..74e49cf968 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12398,4 +12398,20 @@ func TestInterpretStringTemplates(t *testing.T) { inter.Globals.Get("x").GetValue(inter), ) }) + + t.Run("escaped", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + let x = 123 + let y = "x is worth \$$x" + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("x is worth $123"), + inter.Globals.Get("y").GetValue(inter), + ) + }) } From 0cc3a365ae7f3022506f81c8be315881e6e1c9bf Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Mon, 23 Sep 2024 12:41:54 -0400 Subject: [PATCH 10/38] Reset lexer state. --- runtime/parser/lexer/lexer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/parser/lexer/lexer.go b/runtime/parser/lexer/lexer.go index 5e9caf2f79..e5e224f850 100644 --- a/runtime/parser/lexer/lexer.go +++ b/runtime/parser/lexer/lexer.go @@ -140,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() { From 942eb034392aa100179fb3d77d403badd5f96419 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Tue, 24 Sep 2024 11:40:27 -0400 Subject: [PATCH 11/38] Change string template token to \( for backwards compatibility. --- runtime/parser/expression.go | 4 + runtime/parser/expression_test.go | 109 +++++--- runtime/parser/lexer/lexer.go | 20 +- runtime/parser/lexer/lexer_test.go | 253 ++++++++++++++---- runtime/parser/lexer/state.go | 32 ++- runtime/parser/lexer/tokentype.go | 2 +- runtime/tests/checker/string_test.go | 27 +- runtime/tests/interpreter/interpreter_test.go | 64 +++-- 8 files changed, 373 insertions(+), 138 deletions(-) diff --git a/runtime/parser/expression.go b/runtime/parser/expression.go index 9e93990e0b..3537de9b93 100644 --- a/runtime/parser/expression.go +++ b/runtime/parser/expression.go @@ -1193,6 +1193,10 @@ func defineStringExpression() { if err != nil { return nil, err } + _, err = p.mustOne(lexer.TokenParenClose) + if err != nil { + return nil, err + } values = append(values, value) // parser already points to next token curToken = p.current diff --git a/runtime/parser/expression_test.go b/runtime/parser/expression_test.go index b7b8489a5f..2f78ddfe59 100644 --- a/runtime/parser/expression_test.go +++ b/runtime/parser/expression_test.go @@ -6064,7 +6064,7 @@ func TestParseStringTemplate(t *testing.T) { t.Parallel() actual, errs := testParseExpression(` - "$test" + "\(test)" `) var err error @@ -6085,41 +6085,13 @@ func TestParseStringTemplate(t *testing.T) { &ast.IdentifierExpression{ Identifier: ast.Identifier{ Identifier: "test", - Pos: ast.Position{Offset: 5, Line: 2, Column: 4}, + 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: 9, Line: 2, Column: 8}, - }, - } - - utils.AssertEqualWithDiff(t, expected, actual) - }) - - t.Run("escaped", func(t *testing.T) { - - t.Parallel() - - actual, errs := testParseExpression(` - "\$1.00" - `) - - var err error - if len(errs) > 0 { - err = Error{ - Errors: errs, - } - } - - require.NoError(t, err) - - expected := &ast.StringExpression{ - Value: "$1.00", - Range: ast.Range{ - StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, - EndPos: ast.Position{Offset: 10, Line: 2, Column: 9}, + EndPos: ast.Position{Offset: 11, Line: 2, Column: 10}, }, } @@ -6131,7 +6103,7 @@ func TestParseStringTemplate(t *testing.T) { t.Parallel() actual, errs := testParseExpression(` - "this is a test $abc$def test" + "this is a test \(abc)\(def) test" `) var err error @@ -6153,19 +6125,19 @@ func TestParseStringTemplate(t *testing.T) { &ast.IdentifierExpression{ Identifier: ast.Identifier{ Identifier: "abc", - Pos: ast.Position{Offset: 20, Line: 2, Column: 19}, + Pos: ast.Position{Offset: 21, Line: 2, Column: 20}, }, }, &ast.IdentifierExpression{ Identifier: ast.Identifier{ Identifier: "def", - Pos: ast.Position{Offset: 24, Line: 2, Column: 24}, + 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: 32, Line: 2, Column: 32}, + EndPos: ast.Position{Offset: 36, Line: 2, Column: 36}, }, } @@ -6177,7 +6149,7 @@ func TestParseStringTemplate(t *testing.T) { t.Parallel() _, errs := testParseExpression(` - "this is a test $FOO + "this is a test \(FOO) `) var err error @@ -6192,7 +6164,7 @@ func TestParseStringTemplate(t *testing.T) { []error{ &SyntaxError{ Message: "invalid end of string literal: missing '\"'", - Pos: ast.Position{Offset: 25, Line: 2, Column: 25}, + Pos: ast.Position{Offset: 27, Line: 2, Column: 27}, }, }, errs, @@ -6204,7 +6176,7 @@ func TestParseStringTemplate(t *testing.T) { t.Parallel() _, errs := testParseExpression(` - "$$" + "\(.)" `) var err error @@ -6218,8 +6190,8 @@ func TestParseStringTemplate(t *testing.T) { utils.AssertEqualWithDiff(t, []error{ &SyntaxError{ - Message: "expected an identifier got: $", - Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + Message: "expected an identifier got: .", + Pos: ast.Position{Offset: 8, Line: 2, Column: 7}, }, }, errs, @@ -6231,7 +6203,34 @@ func TestParseStringTemplate(t *testing.T) { t.Parallel() _, errs := testParseExpression(` - "$(2 + 2) is a" + "\(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: 2", + Pos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + errs, + ) + }) + + t.Run("invalid, nested identifier", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "\((a))" `) var err error @@ -6246,7 +6245,33 @@ func TestParseStringTemplate(t *testing.T) { []error{ &SyntaxError{ Message: "expected an identifier got: (", - Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + Pos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + errs, + ) + }) + t.Run("invalid, empty", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "\()" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected an identifier got: )", + Pos: ast.Position{Offset: 8, Line: 2, Column: 7}, }, }, errs, diff --git a/runtime/parser/lexer/lexer.go b/runtime/parser/lexer/lexer.go index e5e224f850..6b9c0714b8 100644 --- a/runtime/parser/lexer/lexer.go +++ b/runtime/parser/lexer/lexer.go @@ -54,7 +54,6 @@ type LexerMode int const ( NORMAL = iota STR_IDENTIFIER - STR_EXPRESSION ) type lexer struct { @@ -84,6 +83,8 @@ type lexer struct { 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{} @@ -141,6 +142,7 @@ func (l *lexer) clear() { l.tokens = l.tokens[:0] l.tokenCount = 0 l.mode = NORMAL + l.openBrackets = 0 } func (l *lexer) Reclaim() { @@ -418,18 +420,24 @@ 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 = STR_IDENTIFIER + // 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() 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 fddebcf064..f2a77db4b2 100644 --- a/runtime/parser/lexer/lexer_test.go +++ b/runtime/parser/lexer/lexer_test.go @@ -1016,7 +1016,7 @@ func TestLexString(t *testing.T) { t.Run("valid, string template", func(t *testing.T) { testLex(t, - `"$abc.length"`, + `"\(abc).length"`, []token{ { Token: Token{ @@ -1033,27 +1033,37 @@ func TestLexString(t *testing.T) { Type: TokenStringTemplate, Range: ast.Range{ StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, - EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, }, }, - Source: `$`, + 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}, + 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: 5, Offset: 5}, - EndPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 14, Offset: 14}, }, }, Source: `.length"`, @@ -1062,8 +1072,8 @@ func TestLexString(t *testing.T) { 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}, + StartPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + EndPos: ast.Position{Line: 1, Column: 15, Offset: 15}, }, }, }, @@ -1071,9 +1081,9 @@ func TestLexString(t *testing.T) { ) }) - t.Run("valid, escaped string template", func(t *testing.T) { + t.Run("valid, not a string template", func(t *testing.T) { testLex(t, - `"\$1.00"`, + `"(1.00)"`, []token{ { Token: Token{ @@ -1083,7 +1093,7 @@ func TestLexString(t *testing.T) { EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, }, }, - Source: `"\$1.00"`, + Source: `"(1.00)"`, }, { Token: Token{ @@ -1100,7 +1110,7 @@ func TestLexString(t *testing.T) { t.Run("invalid, number string template", func(t *testing.T) { testLex(t, - `"$1"`, + `"\(7)"`, []token{ { Token: Token{ @@ -1117,27 +1127,37 @@ func TestLexString(t *testing.T) { Type: TokenStringTemplate, Range: ast.Range{ StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, - EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, }, }, - Source: `$`, + 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}, + 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: `1`, + Source: `)`, }, { 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}, + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, }, }, Source: `"`, @@ -1146,8 +1166,8 @@ func TestLexString(t *testing.T) { 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}, + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, }, }, }, @@ -1155,9 +1175,9 @@ func TestLexString(t *testing.T) { ) }) - t.Run("invalid, string template", func(t *testing.T) { + t.Run("invalid identifier string template", func(t *testing.T) { testLex(t, - `"$a + 2`, + `"\(a+2)"`, []token{ { Token: Token{ @@ -1174,39 +1194,69 @@ func TestLexString(t *testing.T) { Type: TokenStringTemplate, Range: ast.Range{ StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, - EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, }, }, - Source: `$`, + 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}, + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, }, }, Source: `a`, }, { Token: Token{ - Type: TokenString, + Type: TokenPlus, Range: ast.Range{ - StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + 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: ` + 2`, + Source: `)`, }, { Token: Token{ - Type: TokenEOF, + 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}, + }, + }, }, }, ) @@ -1214,7 +1264,7 @@ func TestLexString(t *testing.T) { t.Run("valid, multi string template", func(t *testing.T) { testLex(t, - `"$a$b"`, + `"\(a)\(b)"`, []token{ { Token: Token{ @@ -1231,27 +1281,37 @@ func TestLexString(t *testing.T) { Type: TokenStringTemplate, Range: ast.Range{ StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, - EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, }, }, - Source: `$`, + 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}, + 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: 3, Offset: 3}, - EndPos: ast.Position{Line: 1, Column: 3, Offset: 2}, + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 4}, }, }, Source: ``, @@ -1260,28 +1320,38 @@ func TestLexString(t *testing.T) { 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}, + StartPos: ast.Position{Line: 1, Column: 6, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 6}, }, }, - Source: `$`, + 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}, + 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: 6, Offset: 5}, - EndPos: ast.Position{Line: 1, Column: 6, Offset: 5}, + StartPos: ast.Position{Line: 1, Column: 10, Offset: 9}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 9}, }, }, Source: `"`, @@ -1290,8 +1360,95 @@ func TestLexString(t *testing.T) { 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}, + 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}, }, }, }, diff --git a/runtime/parser/lexer/state.go b/runtime/parser/lexer/state.go index 7e9561ba32..f2775b81dc 100644 --- a/runtime/parser/lexer/state.go +++ b/runtime/parser/lexer/state.go @@ -40,12 +40,6 @@ 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 '-': @@ -62,9 +56,20 @@ func rootState(l *lexer) stateFn { case '%': l.emitType(TokenPercent) case '(': + if l.mode == STR_IDENTIFIER { + // 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 == STR_IDENTIFIER { + l.openBrackets-- + if l.openBrackets == 0 { + l.mode = NORMAL + return stringState + } + } case '{': l.emitType(TokenBraceOpen) case '}': @@ -124,6 +129,17 @@ func rootState(l *lexer) stateFn { return numberState case '"': return stringState + case '\\': + if l.mode == STR_IDENTIFIER { + 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 { @@ -302,10 +318,6 @@ 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 a7b3cb2f92..d2aa45fa8e 100644 --- a/runtime/parser/lexer/tokentype.go +++ b/runtime/parser/lexer/tokentype.go @@ -207,7 +207,7 @@ func (t TokenType) String() string { case TokenPragma: return `'#'` case TokenStringTemplate: - return `'$'` + return `'\('` default: panic(errors.NewUnreachableError()) } diff --git a/runtime/tests/checker/string_test.go b/runtime/tests/checker/string_test.go index ee2609758f..572c4ebc20 100644 --- a/runtime/tests/checker/string_test.go +++ b/runtime/tests/checker/string_test.go @@ -708,7 +708,7 @@ func TestCheckStringTemplate(t *testing.T) { _, err := ParseAndCheck(t, ` let a = 1 - let x: String = "The value of a is: $a" + let x: String = "The value of a is: \(a)" `) require.NoError(t, err) @@ -720,7 +720,7 @@ func TestCheckStringTemplate(t *testing.T) { _, err := ParseAndCheck(t, ` let a = "abc def" - let x: String = "$a ghi" + let x: String = "\(a) ghi" `) require.NoError(t, err) @@ -734,7 +734,7 @@ func TestCheckStringTemplate(t *testing.T) { access(all) struct SomeStruct {} let a = SomeStruct() - let x: String = "$a" + let x: String = "\(a)" `) require.NoError(t, err) @@ -745,11 +745,30 @@ func TestCheckStringTemplate(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` - let x: String = "$a" + let x: String = "\(a)" `) errs := RequireCheckerErrors(t, err, 1) assert.IsType(t, &sema.NotDeclaredError{}, errs[0]) }) + + 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.MissingMoveOperationError{}, errs[0]) + }) } diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index 74e49cf968..3c24c70571 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12292,8 +12292,8 @@ func TestInterpretStringTemplates(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - let x = 123 - let y = "x = $x" + let x = 123 + let y = "x = \(x)" `) AssertValuesEqual( @@ -12314,9 +12314,9 @@ func TestInterpretStringTemplates(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - let x = 123.321 - let y = "abc" - let z = "$y and $x" + let x = 123.321 + let y = "abc" + let z = "\(y) and \(x)" `) AssertValuesEqual( @@ -12331,9 +12331,9 @@ func TestInterpretStringTemplates(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - let x = "{}" - let y = "[$x]" - let z = "($y)" + let x = "{}" + let y = "[\(x)]" + let z = "(\(y))" `) AssertValuesEqual( @@ -12348,10 +12348,10 @@ func TestInterpretStringTemplates(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - access(all) - struct SomeStruct {} - let a = SomeStruct() - let x: String = "$a" + access(all) + struct SomeStruct {} + let a = SomeStruct() + let x: String = "\(a)" `) AssertValuesEqual( @@ -12366,16 +12366,16 @@ func TestInterpretStringTemplates(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - let add = fun(): Int { - return 2+2 - } - let x: String = "$add()" + let add = fun(): Int { + return 2+2 + } + let x: String = "\(add())" `) AssertValuesEqual( t, inter, - interpreter.NewUnmeteredStringValue("fun(): Int()"), + interpreter.NewUnmeteredStringValue("4"), inter.Globals.Get("x").GetValue(inter), ) }) @@ -12384,11 +12384,11 @@ func TestInterpretStringTemplates(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - let add = fun(): Int { - return 2+2 - } - let y = add() - let x: String = "$y" + let add = fun(): Int { + return 2+2 + } + let y = add() + let x: String = "\(y)" `) AssertValuesEqual( @@ -12399,19 +12399,29 @@ func TestInterpretStringTemplates(t *testing.T) { ) }) - t.Run("escaped", func(t *testing.T) { + t.Run("resource reference", func(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - let x = 123 - let y = "x is worth \$$x" + resource R {} + + fun test(): String { + let r <- create R() + let ref = &r as &R + let y = "\(ref)" + destroy r + return y + } `) + value, err := inter.Invoke("test") + require.NoError(t, err) + AssertValuesEqual( t, inter, - interpreter.NewUnmeteredStringValue("x is worth $123"), - inter.Globals.Get("y").GetValue(inter), + interpreter.NewUnmeteredStringValue("S.test.R(uuid: 1)"), + value, ) }) } From e3d152d10540198f04fb77a6b857e7efcfabd099 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Tue, 24 Sep 2024 17:21:17 -0400 Subject: [PATCH 12/38] Restrict supported types in template. --- .../sema/check_string_template_expression.go | 12 +++++ runtime/tests/checker/string_test.go | 45 +++++++++++++------ runtime/tests/interpreter/interpreter_test.go | 38 +++------------- 3 files changed, 48 insertions(+), 47 deletions(-) diff --git a/runtime/sema/check_string_template_expression.go b/runtime/sema/check_string_template_expression.go index 276b8c0f14..e2f865391f 100644 --- a/runtime/sema/check_string_template_expression.go +++ b/runtime/sema/check_string_template_expression.go @@ -37,6 +37,18 @@ func (checker *Checker) VisitStringTemplateExpression(stringTemplateExpression * argumentTypes[i] = valueType + // All number types, addresses, path types, bool and strings are supported in string template + if !(IsSubType(valueType, NumberType) || IsSubType(valueType, TheAddressType) || + IsSubType(valueType, PathType) || IsSubType(valueType, StringType) || IsSubType(valueType, BoolType)) { + checker.report( + &TypeMismatchWithDescriptionError{ + ActualType: valueType, + ExpectedTypeDescription: "a type with built-in toString() or bool", + Range: ast.NewRangeFromPositioned(checker.memoryGauge, element), + }, + ) + } + checker.checkResourceMoveOperation(element, valueType) } } diff --git a/runtime/tests/checker/string_test.go b/runtime/tests/checker/string_test.go index 572c4ebc20..313d8f08ee 100644 --- a/runtime/tests/checker/string_test.go +++ b/runtime/tests/checker/string_test.go @@ -707,8 +707,8 @@ func TestCheckStringTemplate(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` - let a = 1 - let x: String = "The value of a is: \(a)" + let a = 1 + let x: String = "The value of a is: \(a)" `) require.NoError(t, err) @@ -719,25 +719,40 @@ func TestCheckStringTemplate(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` - let a = "abc def" - let x: String = "\(a) ghi" + let a = "abc def" + let x: String = "\(a) ghi" `) require.NoError(t, err) }) - t.Run("valid, struct", func(t *testing.T) { + t.Run("invalid, struct", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` - access(all) - struct SomeStruct {} - let a = SomeStruct() - let x: String = "\(a)" + access(all) + struct SomeStruct {} + let a = SomeStruct() + let x: String = "\(a)" `) - require.NoError(t, err) + 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) { @@ -745,12 +760,13 @@ func TestCheckStringTemplate(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` - let x: String = "\(a)" + let x: String = "\(a)" `) - errs := RequireCheckerErrors(t, err, 1) + 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) { @@ -767,8 +783,9 @@ func TestCheckStringTemplate(t *testing.T) { } `) - errs := RequireCheckerErrors(t, err, 1) + errs := RequireCheckerErrors(t, err, 2) - assert.IsType(t, &sema.MissingMoveOperationError{}, errs[0]) + assert.IsType(t, &sema.TypeMismatchWithDescriptionError{}, errs[0]) + assert.IsType(t, &sema.MissingMoveOperationError{}, errs[1]) }) } diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index 3c24c70571..69f203652f 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12344,21 +12344,19 @@ func TestInterpretStringTemplates(t *testing.T) { ) }) - t.Run("struct", func(t *testing.T) { + t.Run("boolean", func(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` - access(all) - struct SomeStruct {} - let a = SomeStruct() - let x: String = "\(a)" + let x = false + let y = "\(x)" `) AssertValuesEqual( t, inter, - interpreter.NewUnmeteredStringValue("S.test.SomeStruct()"), - inter.Globals.Get("x").GetValue(inter), + interpreter.NewUnmeteredStringValue("false"), + inter.Globals.Get("y").GetValue(inter), ) }) @@ -12398,30 +12396,4 @@ func TestInterpretStringTemplates(t *testing.T) { inter.Globals.Get("x").GetValue(inter), ) }) - - t.Run("resource reference", func(t *testing.T) { - t.Parallel() - - inter := parseCheckAndInterpret(t, ` - resource R {} - - fun test(): String { - let r <- create R() - let ref = &r as &R - let y = "\(ref)" - destroy r - return y - } - `) - - value, err := inter.Invoke("test") - require.NoError(t, err) - - AssertValuesEqual( - t, - inter, - interpreter.NewUnmeteredStringValue("S.test.R(uuid: 1)"), - value, - ) - }) } From 1ec5b7eb7c230831d7927cf1476e583945bae4a4 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 25 Sep 2024 10:26:04 -0400 Subject: [PATCH 13/38] Limit string templates to identifiers only. --- runtime/parser/expression.go | 10 +-- runtime/parser/expression_test.go | 64 +++++++++++++++---- runtime/tests/interpreter/interpreter_test.go | 20 +----- 3 files changed, 59 insertions(+), 35 deletions(-) diff --git a/runtime/parser/expression.go b/runtime/parser/expression.go index 3537de9b93..395a6befa3 100644 --- a/runtime/parser/expression.go +++ b/runtime/parser/expression.go @@ -1184,15 +1184,17 @@ func defineStringExpression() { // parser already points to next token curToken = p.current if curToken.Is(lexer.TokenStringTemplate) { - p.next() // advance to the expression - if !p.current.Is(lexer.TokenIdentifier) { - return nil, p.syntaxError("expected an identifier got: %s", p.currentTokenSource()) - } + 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 diff --git a/runtime/parser/expression_test.go b/runtime/parser/expression_test.go index 2f78ddfe59..3804c13ce6 100644 --- a/runtime/parser/expression_test.go +++ b/runtime/parser/expression_test.go @@ -6190,8 +6190,8 @@ func TestParseStringTemplate(t *testing.T) { utils.AssertEqualWithDiff(t, []error{ &SyntaxError{ - Message: "expected an identifier got: .", - Pos: ast.Position{Offset: 8, Line: 2, Column: 7}, + Message: "unexpected token in expression: '.'", + Pos: ast.Position{Offset: 9, Line: 2, Column: 8}, }, }, errs, @@ -6217,19 +6217,19 @@ func TestParseStringTemplate(t *testing.T) { utils.AssertEqualWithDiff(t, []error{ &SyntaxError{ - Message: "expected an identifier got: 2", - Pos: ast.Position{Offset: 8, Line: 2, Column: 7}, + Message: "expected identifier got: 2 + 2", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, }, }, errs, ) }) - t.Run("invalid, nested identifier", func(t *testing.T) { + t.Run("valid, nested identifier", func(t *testing.T) { t.Parallel() - _, errs := testParseExpression(` + actual, errs := testParseExpression(` "\((a))" `) @@ -6240,23 +6240,63 @@ func TestParseStringTemplate(t *testing.T) { } } + require.NoError(t, err) + + expected := &ast.StringTemplateExpression{ + Values: []string{ + "", + "", + }, + Expressions: []ast.Expression{ + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 5, Line: 2, Column: 4}, + EndPos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) + + }) + t.Run("invalid, empty", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "\()" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + require.Error(t, err) utils.AssertEqualWithDiff(t, []error{ &SyntaxError{ - Message: "expected an identifier got: (", - Pos: ast.Position{Offset: 8, Line: 2, Column: 7}, + Message: "unexpected token in expression: ')'", + Pos: ast.Position{Offset: 9, Line: 2, Column: 8}, }, }, errs, ) }) - t.Run("invalid, empty", func(t *testing.T) { + + t.Run("invalid, function identifier", func(t *testing.T) { t.Parallel() _, errs := testParseExpression(` - "\()" + "\(add())" `) var err error @@ -6270,8 +6310,8 @@ func TestParseStringTemplate(t *testing.T) { utils.AssertEqualWithDiff(t, []error{ &SyntaxError{ - Message: "expected an identifier got: )", - Pos: ast.Position{Offset: 8, Line: 2, Column: 7}, + Message: "expected identifier got: add()", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, }, }, errs, diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index 69f203652f..728011fbd7 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12360,25 +12360,7 @@ func TestInterpretStringTemplates(t *testing.T) { ) }) - 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("4"), - inter.Globals.Get("x").GetValue(inter), - ) - }) - - t.Run("func", func(t *testing.T) { + t.Run("func extracted", func(t *testing.T) { t.Parallel() inter := parseCheckAndInterpret(t, ` From 1ae7fe9123e2c2160abcba72a19fd9f04e1ed661 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 25 Sep 2024 13:25:20 -0400 Subject: [PATCH 14/38] Clean up changes. --- runtime/ast/expression.go | 1 - runtime/interpreter/interpreter_expression.go | 3 +-- runtime/parser/expression.go | 9 ++++----- runtime/sema/check_string_template_expression.go | 8 ++++---- runtime/tests/interpreter/interpreter_test.go | 16 ++++++++++++++++ 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/runtime/ast/expression.go b/runtime/ast/expression.go index 2efc783155..2e2ee2f7f2 100644 --- a/runtime/ast/expression.go +++ b/runtime/ast/expression.go @@ -231,7 +231,6 @@ type StringTemplateExpression struct { 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, diff --git a/runtime/interpreter/interpreter_expression.go b/runtime/interpreter/interpreter_expression.go index 264b39edf8..05272cb606 100644 --- a/runtime/interpreter/interpreter_expression.go +++ b/runtime/interpreter/interpreter_expression.go @@ -968,7 +968,7 @@ func (interpreter *Interpreter) VisitStringTemplateExpression(expression *ast.St for i, str := range expression.Values { builder.WriteString(str) if i < len(values) { - // STRINGTODO: is this how the conversion should happen? + // this is equivalent to toString() for supported types s := values[i].String() switch argumentTypes[i] { case sema.StringType: @@ -981,7 +981,6 @@ func (interpreter *Interpreter) VisitStringTemplateExpression(expression *ast.St } } - // STRINGTODO: already metered as a string constant in parser? return NewUnmeteredStringValue(builder.String()) } diff --git a/runtime/parser/expression.go b/runtime/parser/expression.go index 395a6befa3..78a53c77ad 100644 --- a/runtime/parser/expression.go +++ b/runtime/parser/expression.go @@ -1141,7 +1141,7 @@ func defineStringExpression() { curToken := startToken endToken := startToken - // early check for start " of string literal because of string templates + // check for start " of string literal literal := p.tokenSource(curToken) length := len(literal) if length == 0 { @@ -1160,13 +1160,14 @@ func defineStringExpression() { } } - // flag for late end " check + // flag for ending " check missingEnd := true for curToken.Is(lexer.TokenString) { literal = p.tokenSource(curToken) length = len(literal) + // remove quotation marks if they exist if curToken == startToken { literal = literal[1:] length = len(literal) @@ -1208,7 +1209,7 @@ func defineStringExpression() { } } - // late check for end " of string literal because of string templates + // check for end " of string literal if missingEnd { p.reportSyntaxError("invalid end of string literal: missing '\"'") } @@ -1741,8 +1742,6 @@ func parseStringLiteralContent(p *parser, s []byte) (result string) { builder.WriteByte('\t') case '"': builder.WriteByte('"') - case '$': - builder.WriteByte('$') case '\'': builder.WriteByte('\'') case '\\': diff --git a/runtime/sema/check_string_template_expression.go b/runtime/sema/check_string_template_expression.go index e2f865391f..7a7eac8b3d 100644 --- a/runtime/sema/check_string_template_expression.go +++ b/runtime/sema/check_string_template_expression.go @@ -38,8 +38,10 @@ func (checker *Checker) VisitStringTemplateExpression(stringTemplateExpression * argumentTypes[i] = valueType // All number types, addresses, path types, bool and strings are supported in string template - if !(IsSubType(valueType, NumberType) || IsSubType(valueType, TheAddressType) || - IsSubType(valueType, PathType) || IsSubType(valueType, StringType) || IsSubType(valueType, BoolType)) { + if IsSubType(valueType, NumberType) || IsSubType(valueType, TheAddressType) || + IsSubType(valueType, PathType) || IsSubType(valueType, StringType) || IsSubType(valueType, BoolType) { + checker.checkResourceMoveOperation(element, valueType) + } else { checker.report( &TypeMismatchWithDescriptionError{ ActualType: valueType, @@ -48,8 +50,6 @@ func (checker *Checker) VisitStringTemplateExpression(stringTemplateExpression * }, ) } - - checker.checkResourceMoveOperation(element, valueType) } } diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index 728011fbd7..855888eea0 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12378,4 +12378,20 @@ func TestInterpretStringTemplates(t *testing.T) { 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), + ) + }) } From 8799cac377654e613af872b7ed8e043cfa0f7e06 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 25 Sep 2024 13:39:43 -0400 Subject: [PATCH 15/38] Fix checker test. --- runtime/tests/checker/string_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/runtime/tests/checker/string_test.go b/runtime/tests/checker/string_test.go index 313d8f08ee..4bea07a7dd 100644 --- a/runtime/tests/checker/string_test.go +++ b/runtime/tests/checker/string_test.go @@ -783,9 +783,8 @@ func TestCheckStringTemplate(t *testing.T) { } `) - errs := RequireCheckerErrors(t, err, 2) + errs := RequireCheckerErrors(t, err, 1) assert.IsType(t, &sema.TypeMismatchWithDescriptionError{}, errs[0]) - assert.IsType(t, &sema.MissingMoveOperationError{}, errs[1]) }) } From 67d950b66be06ff201aca4ee10bcdabba9999822 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Thu, 26 Sep 2024 12:26:20 -0400 Subject: [PATCH 16/38] Initial test of stringer interface. --- runtime/sema/stringer.go | 65 +++++++++++++++++++++++++ runtime/sema/type.go | 18 +++++++ runtime/tests/checker/stringer_test.go | 66 ++++++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 runtime/sema/stringer.go create mode 100644 runtime/tests/checker/stringer_test.go diff --git a/runtime/sema/stringer.go b/runtime/sema/stringer.go new file mode 100644 index 0000000000..67fef4abaf --- /dev/null +++ b/runtime/sema/stringer.go @@ -0,0 +1,65 @@ +/* + * 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" + "github.com/onflow/cadence/runtime/common" +) + +const StringerTypeToStringFunctionName = "toString" + +var StringerTypeToStringFunctionType = &FunctionType{ + Purity: FunctionPurityView, + ReturnTypeAnnotation: NewTypeAnnotation( + StringType, + ), +} + +const StringerTypeToStringFunctionDocString = ` + Returns this object as a String. + ` + +const StringerTypeName = "Stringer" + +var StringerType = InterfaceType{ + Location: nil, + Identifier: StringerTypeName, + CompositeKind: common.CompositeKindStructure, + Members: &StringMemberOrderedMap{}, +} + +func init() { + StringerType.Members.Set(StringerTypeToStringFunctionName, NewUnmeteredFunctionMember( + &StringerType, + PrimitiveAccess(ast.AccessAll), + StringerTypeToStringFunctionName, + StringerTypeToStringFunctionType, + StringerTypeToStringFunctionDocString, + )) + StringerType.memberResolvers = MembersAsResolvers([]*Member{ + NewUnmeteredFunctionMember( + &StringerType, + PrimitiveAccess(ast.AccessAll), + StringerTypeToStringFunctionName, + StringerTypeToStringFunctionType, + StringerTypeToStringFunctionDocString, + ), + }) +} diff --git a/runtime/sema/type.go b/runtime/sema/type.go index 42d72439ae..f659ccf4ac 100644 --- a/runtime/sema/type.go +++ b/runtime/sema/type.go @@ -4198,6 +4198,7 @@ func init() { DeploymentResultType, HashableStructType, &InclusiveRangeType{}, + &StringerType, }, ) @@ -4899,6 +4900,17 @@ func IsHashableStructType(t Type) bool { } } +// which simple types conform to stringer interface (except Bool?) +func IsStringerType(t Type) bool { + switch t { + case BoolType, CharacterType, StringType: + return true + default: + return IsSubType(t, NumberType) || + IsSubType(t, PathType) || IsSubType(t, TheAddressType) + } +} + func (t *CompositeType) GetBaseType() Type { return t.baseType } @@ -7825,6 +7837,12 @@ func checkSubTypeWithoutEquality(subType Type, superType Type) bool { IsSubsetOf(typedSubType.EffectiveInterfaceConformanceSet()) } + // STRINGERTODO: other options? how to make existing simple types + // conform to an intersection type? + if typedSuperType.Types[0].Identifier == StringerTypeName { + return IsStringerType(subType) + } + default: // Supertype (intersection) has a non-Any* legacy type diff --git a/runtime/tests/checker/stringer_test.go b/runtime/tests/checker/stringer_test.go new file mode 100644 index 0000000000..a8d0c9cdbd --- /dev/null +++ b/runtime/tests/checker/stringer_test.go @@ -0,0 +1,66 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * 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 checker + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/onflow/cadence/runtime/sema" +) + +func TestCheckStringer(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + let a: {Stringer} = 1 + let b: {Stringer} = false + let c: {Stringer} = "hey" + access(all) + struct Foo: Stringer { + view fun toString():String { + return "foo" + } + } + let d: {Stringer} = Foo() + `) + + assert.NoError(t, err) +} + +func TestCheckInvalidStringer(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + resource R {} + + let a: {Stringer} = <-create R() + let b: {Stringer} = [<-create R()] + let c: {Stringer} = {1: true} + `) + + errs := RequireCheckerErrors(t, err, 3) + + assert.IsType(t, &sema.TypeMismatchError{}, errs[0]) + assert.IsType(t, &sema.TypeMismatchError{}, errs[1]) + assert.IsType(t, &sema.TypeMismatchError{}, errs[2]) +} From 6d60b9d94f9ece80b8c56e1286e00256484fb379 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Thu, 26 Sep 2024 14:15:02 -0400 Subject: [PATCH 17/38] Add test for custom toString. --- runtime/tests/interpreter/stringer_test.go | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 runtime/tests/interpreter/stringer_test.go diff --git a/runtime/tests/interpreter/stringer_test.go b/runtime/tests/interpreter/stringer_test.go new file mode 100644 index 0000000000..2ebf40cabd --- /dev/null +++ b/runtime/tests/interpreter/stringer_test.go @@ -0,0 +1,54 @@ +/* + * 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/onflow/cadence/runtime/interpreter" + . "github.com/onflow/cadence/runtime/tests/utils" + "github.com/stretchr/testify/require" +) + +func TestStringerBasic(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + access(all) + struct Example: Stringer { + 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, + ) +} From 6298e1fb49df434c397ae6405aef1558c495a119 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Thu, 26 Sep 2024 14:55:35 -0400 Subject: [PATCH 18/38] Add test for conformance error. --- runtime/tests/checker/stringer_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/runtime/tests/checker/stringer_test.go b/runtime/tests/checker/stringer_test.go index a8d0c9cdbd..e9b953f4e3 100644 --- a/runtime/tests/checker/stringer_test.go +++ b/runtime/tests/checker/stringer_test.go @@ -56,11 +56,13 @@ func TestCheckInvalidStringer(t *testing.T) { let a: {Stringer} = <-create R() let b: {Stringer} = [<-create R()] let c: {Stringer} = {1: true} + struct Foo: Stringer {} `) - errs := RequireCheckerErrors(t, err, 3) + errs := RequireCheckerErrors(t, err, 4) 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]) } From aa514550ad5eca99db4f43bd99f643f4ef79bdad Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Fri, 27 Sep 2024 11:44:41 -0400 Subject: [PATCH 19/38] Fix tests for native interface types. --- runtime/interpreter/interpreter.go | 4 ++ runtime/sema/stringer.go | 52 ++++++++++------------ runtime/sema/type.go | 33 +++++++++++++- runtime/sema/type_test.go | 4 ++ runtime/tests/interpreter/stringer_test.go | 26 ++++++++++- 5 files changed, 88 insertions(+), 31 deletions(-) diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index a99f854410..b1576c1216 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -4866,6 +4866,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/runtime/sema/stringer.go b/runtime/sema/stringer.go index 67fef4abaf..1331fdc3eb 100644 --- a/runtime/sema/stringer.go +++ b/runtime/sema/stringer.go @@ -23,43 +23,37 @@ import ( "github.com/onflow/cadence/runtime/common" ) -const StringerTypeToStringFunctionName = "toString" +const StringerTypeName = "Stringer" -var StringerTypeToStringFunctionType = &FunctionType{ - Purity: FunctionPurityView, - ReturnTypeAnnotation: NewTypeAnnotation( - StringType, - ), -} +var StringerType = func() *InterfaceType { -const StringerTypeToStringFunctionDocString = ` - Returns this object as a String. - ` + stringerType := &InterfaceType{ + Identifier: StringerTypeName, + CompositeKind: common.CompositeKindStructure, + Members: &StringMemberOrderedMap{}, + } -const StringerTypeName = "Stringer" + const StringerTypeToStringFunctionDocString = `Returns this object as a String.` -var StringerType = InterfaceType{ - Location: nil, - Identifier: StringerTypeName, - CompositeKind: common.CompositeKindStructure, - Members: &StringMemberOrderedMap{}, -} + const StringerTypeToStringFunctionName = "toString" -func init() { - StringerType.Members.Set(StringerTypeToStringFunctionName, NewUnmeteredFunctionMember( - &StringerType, - PrimitiveAccess(ast.AccessAll), - StringerTypeToStringFunctionName, - StringerTypeToStringFunctionType, - StringerTypeToStringFunctionDocString, - )) - StringerType.memberResolvers = MembersAsResolvers([]*Member{ + var StringerTypeToStringFunctionType = &FunctionType{ + Purity: FunctionPurityView, + ReturnTypeAnnotation: NewTypeAnnotation( + StringType, + ), + } + + var members = []*Member{ NewUnmeteredFunctionMember( - &StringerType, + stringerType, PrimitiveAccess(ast.AccessAll), StringerTypeToStringFunctionName, StringerTypeToStringFunctionType, StringerTypeToStringFunctionDocString, ), - }) -} + } + + stringerType.Members = MembersAsMap(members) + return stringerType +}() diff --git a/runtime/sema/type.go b/runtime/sema/type.go index f659ccf4ac..c9ab715c7f 100644 --- a/runtime/sema/type.go +++ b/runtime/sema/type.go @@ -4198,7 +4198,7 @@ func init() { DeploymentResultType, HashableStructType, &InclusiveRangeType{}, - &StringerType, + StringerType, }, ) @@ -9552,3 +9552,34 @@ func init() { }) } } + +var NativeInterfaceTypes = map[string]*InterfaceType{} + +func init() { + interfaceTypes := []*InterfaceType{ + StringerType, + } + + for len(interfaceTypes) > 0 { + lastIndex := len(interfaceTypes) - 1 + interfaceType := interfaceTypes[lastIndex] + interfaceTypes[lastIndex] = nil + interfaceTypes = interfaceTypes[:lastIndex] + + NativeInterfaceTypes[interfaceType.QualifiedIdentifier()] = interfaceType + + nestedTypes := interfaceType.NestedTypes + if nestedTypes == nil { + continue + } + + nestedTypes.Foreach(func(_ string, nestedType Type) { + nestedInterfaceType, ok := nestedType.(*InterfaceType) + if !ok { + return + } + + interfaceTypes = append(interfaceTypes, nestedInterfaceType) + }) + } +} diff --git a/runtime/sema/type_test.go b/runtime/sema/type_test.go index 0f0bcf40cf..ebaca4f36b 100644 --- a/runtime/sema/type_test.go +++ b/runtime/sema/type_test.go @@ -2378,6 +2378,10 @@ func TestTypeInclusions(t *testing.T) { return } + if _, ok := typ.(*InterfaceType); ok { + return + } + if typ.IsResourceType() { return } diff --git a/runtime/tests/interpreter/stringer_test.go b/runtime/tests/interpreter/stringer_test.go index 2ebf40cabd..fbd6da1388 100644 --- a/runtime/tests/interpreter/stringer_test.go +++ b/runtime/tests/interpreter/stringer_test.go @@ -21,9 +21,10 @@ package interpreter_test import ( "testing" + "github.com/stretchr/testify/require" + "github.com/onflow/cadence/runtime/interpreter" . "github.com/onflow/cadence/runtime/tests/utils" - "github.com/stretchr/testify/require" ) func TestStringerBasic(t *testing.T) { @@ -52,3 +53,26 @@ func TestStringerBasic(t *testing.T) { 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, + ) +} From bda6b28fb29cff50e635a3e9d94b926290001b33 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 2 Oct 2024 10:58:51 -0400 Subject: [PATCH 20/38] Refactor code from review. --- runtime/ast/expression.go | 9 +- runtime/common/memorykind.go | 1 + runtime/common/memorykind_string.go | 95 ++++++++++--------- runtime/common/metering.go | 7 ++ runtime/interpreter/interpreter_expression.go | 18 ++-- runtime/parser/expression_test.go | 54 +++++++++++ runtime/parser/lexer/lexer.go | 10 +- runtime/parser/lexer/state.go | 6 +- .../sema/check_string_template_expression.go | 12 ++- runtime/tests/interpreter/interpreter_test.go | 18 ++++ 10 files changed, 157 insertions(+), 73 deletions(-) diff --git a/runtime/ast/expression.go b/runtime/ast/expression.go index 2e2ee2f7f2..b8bf93e86d 100644 --- a/runtime/ast/expression.go +++ b/runtime/ast/expression.go @@ -230,8 +230,13 @@ type StringTemplateExpression struct { var _ Expression = &StringTemplateExpression{} -func NewStringTemplateExpression(gauge common.MemoryGauge, values []string, exprs []Expression, exprRange Range) *StringTemplateExpression { - common.UseMemory(gauge, common.StringExpressionMemoryUsage) +func NewStringTemplateExpression( + gauge common.MemoryGauge, + values []string, + exprs []Expression, + exprRange Range, +) *StringTemplateExpression { + common.UseMemory(gauge, common.NewStringTemplateExpressionMemoryUsage(len(values)+len(exprs))) return &StringTemplateExpression{ Values: values, Expressions: exprs, diff --git a/runtime/common/memorykind.go b/runtime/common/memorykind.go index 06e6748e9a..0754c397f5 100644 --- a/runtime/common/memorykind.go +++ b/runtime/common/memorykind.go @@ -204,6 +204,7 @@ const ( MemoryKindIntegerExpression MemoryKindFixedPointExpression MemoryKindArrayExpression + MemoryKindStringTemplateExpression MemoryKindDictionaryExpression MemoryKindIdentifierExpression MemoryKindInvocationExpression diff --git a/runtime/common/memorykind_string.go b/runtime/common/memorykind_string.go index 9b022e9495..67d6a0b8f8 100644 --- a/runtime/common/memorykind_string.go +++ b/runtime/common/memorykind_string.go @@ -165,56 +165,57 @@ func _() { _ = x[MemoryKindIntegerExpression-154] _ = x[MemoryKindFixedPointExpression-155] _ = x[MemoryKindArrayExpression-156] - _ = x[MemoryKindDictionaryExpression-157] - _ = x[MemoryKindIdentifierExpression-158] - _ = x[MemoryKindInvocationExpression-159] - _ = x[MemoryKindMemberExpression-160] - _ = x[MemoryKindIndexExpression-161] - _ = x[MemoryKindConditionalExpression-162] - _ = x[MemoryKindUnaryExpression-163] - _ = x[MemoryKindBinaryExpression-164] - _ = x[MemoryKindFunctionExpression-165] - _ = x[MemoryKindCastingExpression-166] - _ = x[MemoryKindCreateExpression-167] - _ = x[MemoryKindDestroyExpression-168] - _ = x[MemoryKindReferenceExpression-169] - _ = x[MemoryKindForceExpression-170] - _ = x[MemoryKindPathExpression-171] - _ = x[MemoryKindAttachExpression-172] - _ = x[MemoryKindConstantSizedType-173] - _ = x[MemoryKindDictionaryType-174] - _ = x[MemoryKindFunctionType-175] - _ = x[MemoryKindInstantiationType-176] - _ = x[MemoryKindNominalType-177] - _ = x[MemoryKindOptionalType-178] - _ = x[MemoryKindReferenceType-179] - _ = x[MemoryKindIntersectionType-180] - _ = x[MemoryKindVariableSizedType-181] - _ = x[MemoryKindPosition-182] - _ = x[MemoryKindRange-183] - _ = x[MemoryKindElaboration-184] - _ = x[MemoryKindActivation-185] - _ = x[MemoryKindActivationEntries-186] - _ = x[MemoryKindVariableSizedSemaType-187] - _ = x[MemoryKindConstantSizedSemaType-188] - _ = x[MemoryKindDictionarySemaType-189] - _ = x[MemoryKindOptionalSemaType-190] - _ = x[MemoryKindIntersectionSemaType-191] - _ = x[MemoryKindReferenceSemaType-192] - _ = x[MemoryKindEntitlementSemaType-193] - _ = x[MemoryKindEntitlementMapSemaType-194] - _ = x[MemoryKindEntitlementRelationSemaType-195] - _ = x[MemoryKindCapabilitySemaType-196] - _ = x[MemoryKindInclusiveRangeSemaType-197] - _ = x[MemoryKindOrderedMap-198] - _ = x[MemoryKindOrderedMapEntryList-199] - _ = x[MemoryKindOrderedMapEntry-200] - _ = x[MemoryKindLast-201] + _ = x[MemoryKindStringTemplateExpression-157] + _ = x[MemoryKindDictionaryExpression-158] + _ = x[MemoryKindIdentifierExpression-159] + _ = x[MemoryKindInvocationExpression-160] + _ = x[MemoryKindMemberExpression-161] + _ = x[MemoryKindIndexExpression-162] + _ = x[MemoryKindConditionalExpression-163] + _ = x[MemoryKindUnaryExpression-164] + _ = x[MemoryKindBinaryExpression-165] + _ = x[MemoryKindFunctionExpression-166] + _ = x[MemoryKindCastingExpression-167] + _ = x[MemoryKindCreateExpression-168] + _ = x[MemoryKindDestroyExpression-169] + _ = x[MemoryKindReferenceExpression-170] + _ = x[MemoryKindForceExpression-171] + _ = x[MemoryKindPathExpression-172] + _ = x[MemoryKindAttachExpression-173] + _ = x[MemoryKindConstantSizedType-174] + _ = x[MemoryKindDictionaryType-175] + _ = x[MemoryKindFunctionType-176] + _ = x[MemoryKindInstantiationType-177] + _ = x[MemoryKindNominalType-178] + _ = x[MemoryKindOptionalType-179] + _ = x[MemoryKindReferenceType-180] + _ = x[MemoryKindIntersectionType-181] + _ = x[MemoryKindVariableSizedType-182] + _ = x[MemoryKindPosition-183] + _ = x[MemoryKindRange-184] + _ = x[MemoryKindElaboration-185] + _ = x[MemoryKindActivation-186] + _ = x[MemoryKindActivationEntries-187] + _ = x[MemoryKindVariableSizedSemaType-188] + _ = x[MemoryKindConstantSizedSemaType-189] + _ = x[MemoryKindDictionarySemaType-190] + _ = x[MemoryKindOptionalSemaType-191] + _ = x[MemoryKindIntersectionSemaType-192] + _ = x[MemoryKindReferenceSemaType-193] + _ = x[MemoryKindEntitlementSemaType-194] + _ = x[MemoryKindEntitlementMapSemaType-195] + _ = x[MemoryKindEntitlementRelationSemaType-196] + _ = x[MemoryKindCapabilitySemaType-197] + _ = x[MemoryKindInclusiveRangeSemaType-198] + _ = x[MemoryKindOrderedMap-199] + _ = x[MemoryKindOrderedMapEntryList-200] + _ = x[MemoryKindOrderedMapEntry-201] + _ = x[MemoryKindLast-202] } -const _MemoryKind_name = "UnknownAddressValueStringValueCharacterValueNumberValueArrayValueBaseDictionaryValueBaseCompositeValueBaseSimpleCompositeValueBaseOptionalValueTypeValuePathValueCapabilityValueStorageReferenceValueEphemeralReferenceValueInterpretedFunctionValueHostFunctionValueBoundFunctionValueBigIntSimpleCompositeValuePublishedValueStorageCapabilityControllerValueAccountCapabilityControllerValueAtreeArrayDataSlabAtreeArrayMetaDataSlabAtreeArrayElementOverheadAtreeMapDataSlabAtreeMapMetaDataSlabAtreeMapElementOverheadAtreeMapPreAllocatedElementAtreeEncodedSlabPrimitiveStaticTypeCompositeStaticTypeInterfaceStaticTypeVariableSizedStaticTypeConstantSizedStaticTypeDictionaryStaticTypeInclusiveRangeStaticTypeOptionalStaticTypeIntersectionStaticTypeEntitlementSetStaticAccessEntitlementMapStaticAccessReferenceStaticTypeCapabilityStaticTypeFunctionStaticTypeCadenceVoidValueCadenceOptionalValueCadenceBoolValueCadenceStringValueCadenceCharacterValueCadenceAddressValueCadenceIntValueCadenceNumberValueCadenceArrayValueBaseCadenceArrayValueLengthCadenceDictionaryValueCadenceInclusiveRangeValueCadenceKeyValuePairCadenceStructValueBaseCadenceStructValueSizeCadenceResourceValueBaseCadenceAttachmentValueBaseCadenceResourceValueSizeCadenceAttachmentValueSizeCadenceEventValueBaseCadenceEventValueSizeCadenceContractValueBaseCadenceContractValueSizeCadenceEnumValueBaseCadenceEnumValueSizeCadencePathValueCadenceTypeValueCadenceCapabilityValueCadenceDeprecatedPathCapabilityTypeCadenceFunctionValueCadenceOptionalTypeCadenceDeprecatedRestrictedTypeCadenceVariableSizedArrayTypeCadenceConstantSizedArrayTypeCadenceDictionaryTypeCadenceInclusiveRangeTypeCadenceFieldCadenceParameterCadenceTypeParameterCadenceStructTypeCadenceResourceTypeCadenceAttachmentTypeCadenceEventTypeCadenceContractTypeCadenceStructInterfaceTypeCadenceResourceInterfaceTypeCadenceContractInterfaceTypeCadenceFunctionTypeCadenceEntitlementSetAccessCadenceEntitlementMapAccessCadenceReferenceTypeCadenceIntersectionTypeCadenceCapabilityTypeCadenceEnumTypeRawStringAddressLocationBytesVariableCompositeTypeInfoCompositeFieldInvocationStorageMapStorageKeyTypeTokenErrorTokenSpaceTokenProgramIdentifierArgumentBlockFunctionBlockParameterParameterListTypeParameterTypeParameterListTransferMembersTypeAnnotationDictionaryEntryFunctionDeclarationCompositeDeclarationAttachmentDeclarationInterfaceDeclarationEntitlementDeclarationEntitlementMappingElementEntitlementMappingDeclarationEnumCaseDeclarationFieldDeclarationTransactionDeclarationImportDeclarationVariableDeclarationSpecialFunctionDeclarationPragmaDeclarationAssignmentStatementBreakStatementContinueStatementEmitStatementExpressionStatementForStatementIfStatementReturnStatementSwapStatementSwitchStatementWhileStatementRemoveStatementBooleanExpressionVoidExpressionNilExpressionStringExpressionIntegerExpressionFixedPointExpressionArrayExpressionDictionaryExpressionIdentifierExpressionInvocationExpressionMemberExpressionIndexExpressionConditionalExpressionUnaryExpressionBinaryExpressionFunctionExpressionCastingExpressionCreateExpressionDestroyExpressionReferenceExpressionForceExpressionPathExpressionAttachExpressionConstantSizedTypeDictionaryTypeFunctionTypeInstantiationTypeNominalTypeOptionalTypeReferenceTypeIntersectionTypeVariableSizedTypePositionRangeElaborationActivationActivationEntriesVariableSizedSemaTypeConstantSizedSemaTypeDictionarySemaTypeOptionalSemaTypeIntersectionSemaTypeReferenceSemaTypeEntitlementSemaTypeEntitlementMapSemaTypeEntitlementRelationSemaTypeCapabilitySemaTypeInclusiveRangeSemaTypeOrderedMapOrderedMapEntryListOrderedMapEntryLast" +const _MemoryKind_name = "UnknownAddressValueStringValueCharacterValueNumberValueArrayValueBaseDictionaryValueBaseCompositeValueBaseSimpleCompositeValueBaseOptionalValueTypeValuePathValueCapabilityValueStorageReferenceValueEphemeralReferenceValueInterpretedFunctionValueHostFunctionValueBoundFunctionValueBigIntSimpleCompositeValuePublishedValueStorageCapabilityControllerValueAccountCapabilityControllerValueAtreeArrayDataSlabAtreeArrayMetaDataSlabAtreeArrayElementOverheadAtreeMapDataSlabAtreeMapMetaDataSlabAtreeMapElementOverheadAtreeMapPreAllocatedElementAtreeEncodedSlabPrimitiveStaticTypeCompositeStaticTypeInterfaceStaticTypeVariableSizedStaticTypeConstantSizedStaticTypeDictionaryStaticTypeInclusiveRangeStaticTypeOptionalStaticTypeIntersectionStaticTypeEntitlementSetStaticAccessEntitlementMapStaticAccessReferenceStaticTypeCapabilityStaticTypeFunctionStaticTypeCadenceVoidValueCadenceOptionalValueCadenceBoolValueCadenceStringValueCadenceCharacterValueCadenceAddressValueCadenceIntValueCadenceNumberValueCadenceArrayValueBaseCadenceArrayValueLengthCadenceDictionaryValueCadenceInclusiveRangeValueCadenceKeyValuePairCadenceStructValueBaseCadenceStructValueSizeCadenceResourceValueBaseCadenceAttachmentValueBaseCadenceResourceValueSizeCadenceAttachmentValueSizeCadenceEventValueBaseCadenceEventValueSizeCadenceContractValueBaseCadenceContractValueSizeCadenceEnumValueBaseCadenceEnumValueSizeCadencePathValueCadenceTypeValueCadenceCapabilityValueCadenceDeprecatedPathCapabilityTypeCadenceFunctionValueCadenceOptionalTypeCadenceDeprecatedRestrictedTypeCadenceVariableSizedArrayTypeCadenceConstantSizedArrayTypeCadenceDictionaryTypeCadenceInclusiveRangeTypeCadenceFieldCadenceParameterCadenceTypeParameterCadenceStructTypeCadenceResourceTypeCadenceAttachmentTypeCadenceEventTypeCadenceContractTypeCadenceStructInterfaceTypeCadenceResourceInterfaceTypeCadenceContractInterfaceTypeCadenceFunctionTypeCadenceEntitlementSetAccessCadenceEntitlementMapAccessCadenceReferenceTypeCadenceIntersectionTypeCadenceCapabilityTypeCadenceEnumTypeRawStringAddressLocationBytesVariableCompositeTypeInfoCompositeFieldInvocationStorageMapStorageKeyTypeTokenErrorTokenSpaceTokenProgramIdentifierArgumentBlockFunctionBlockParameterParameterListTypeParameterTypeParameterListTransferMembersTypeAnnotationDictionaryEntryFunctionDeclarationCompositeDeclarationAttachmentDeclarationInterfaceDeclarationEntitlementDeclarationEntitlementMappingElementEntitlementMappingDeclarationEnumCaseDeclarationFieldDeclarationTransactionDeclarationImportDeclarationVariableDeclarationSpecialFunctionDeclarationPragmaDeclarationAssignmentStatementBreakStatementContinueStatementEmitStatementExpressionStatementForStatementIfStatementReturnStatementSwapStatementSwitchStatementWhileStatementRemoveStatementBooleanExpressionVoidExpressionNilExpressionStringExpressionIntegerExpressionFixedPointExpressionArrayExpressionStringTemplateExpressionDictionaryExpressionIdentifierExpressionInvocationExpressionMemberExpressionIndexExpressionConditionalExpressionUnaryExpressionBinaryExpressionFunctionExpressionCastingExpressionCreateExpressionDestroyExpressionReferenceExpressionForceExpressionPathExpressionAttachExpressionConstantSizedTypeDictionaryTypeFunctionTypeInstantiationTypeNominalTypeOptionalTypeReferenceTypeIntersectionTypeVariableSizedTypePositionRangeElaborationActivationActivationEntriesVariableSizedSemaTypeConstantSizedSemaTypeDictionarySemaTypeOptionalSemaTypeIntersectionSemaTypeReferenceSemaTypeEntitlementSemaTypeEntitlementMapSemaTypeEntitlementRelationSemaTypeCapabilitySemaTypeInclusiveRangeSemaTypeOrderedMapOrderedMapEntryListOrderedMapEntryLast" -var _MemoryKind_index = [...]uint16{0, 7, 19, 30, 44, 55, 69, 88, 106, 130, 143, 152, 161, 176, 197, 220, 244, 261, 279, 285, 305, 319, 351, 383, 401, 423, 448, 464, 484, 507, 534, 550, 569, 588, 607, 630, 653, 673, 697, 715, 737, 763, 789, 808, 828, 846, 862, 882, 898, 916, 937, 956, 971, 989, 1010, 1033, 1055, 1081, 1100, 1122, 1144, 1168, 1194, 1218, 1244, 1265, 1286, 1310, 1334, 1354, 1374, 1390, 1406, 1428, 1463, 1483, 1502, 1533, 1562, 1591, 1612, 1637, 1649, 1665, 1685, 1702, 1721, 1742, 1758, 1777, 1803, 1831, 1859, 1878, 1905, 1932, 1952, 1975, 1996, 2011, 2020, 2035, 2040, 2048, 2065, 2079, 2089, 2099, 2109, 2118, 2128, 2138, 2145, 2155, 2163, 2168, 2181, 2190, 2203, 2216, 2233, 2241, 2248, 2262, 2277, 2296, 2316, 2337, 2357, 2379, 2404, 2433, 2452, 2468, 2490, 2507, 2526, 2552, 2569, 2588, 2602, 2619, 2632, 2651, 2663, 2674, 2689, 2702, 2717, 2731, 2746, 2763, 2777, 2790, 2806, 2823, 2843, 2858, 2878, 2898, 2918, 2934, 2949, 2970, 2985, 3001, 3019, 3036, 3052, 3069, 3088, 3103, 3117, 3133, 3150, 3164, 3176, 3193, 3204, 3216, 3229, 3245, 3262, 3270, 3275, 3286, 3296, 3313, 3334, 3355, 3373, 3389, 3409, 3426, 3445, 3467, 3494, 3512, 3534, 3544, 3563, 3578, 3582} +var _MemoryKind_index = [...]uint16{0, 7, 19, 30, 44, 55, 69, 88, 106, 130, 143, 152, 161, 176, 197, 220, 244, 261, 279, 285, 305, 319, 351, 383, 401, 423, 448, 464, 484, 507, 534, 550, 569, 588, 607, 630, 653, 673, 697, 715, 737, 763, 789, 808, 828, 846, 862, 882, 898, 916, 937, 956, 971, 989, 1010, 1033, 1055, 1081, 1100, 1122, 1144, 1168, 1194, 1218, 1244, 1265, 1286, 1310, 1334, 1354, 1374, 1390, 1406, 1428, 1463, 1483, 1502, 1533, 1562, 1591, 1612, 1637, 1649, 1665, 1685, 1702, 1721, 1742, 1758, 1777, 1803, 1831, 1859, 1878, 1905, 1932, 1952, 1975, 1996, 2011, 2020, 2035, 2040, 2048, 2065, 2079, 2089, 2099, 2109, 2118, 2128, 2138, 2145, 2155, 2163, 2168, 2181, 2190, 2203, 2216, 2233, 2241, 2248, 2262, 2277, 2296, 2316, 2337, 2357, 2379, 2404, 2433, 2452, 2468, 2490, 2507, 2526, 2552, 2569, 2588, 2602, 2619, 2632, 2651, 2663, 2674, 2689, 2702, 2717, 2731, 2746, 2763, 2777, 2790, 2806, 2823, 2843, 2858, 2882, 2902, 2922, 2942, 2958, 2973, 2994, 3009, 3025, 3043, 3060, 3076, 3093, 3112, 3127, 3141, 3157, 3174, 3188, 3200, 3217, 3228, 3240, 3253, 3269, 3286, 3294, 3299, 3310, 3320, 3337, 3358, 3379, 3397, 3413, 3433, 3450, 3469, 3491, 3518, 3536, 3558, 3568, 3587, 3602, 3606} func (i MemoryKind) String() string { if i >= MemoryKind(len(_MemoryKind_index)-1) { diff --git a/runtime/common/metering.go b/runtime/common/metering.go index a4b4afc2dd..e6e6249571 100644 --- a/runtime/common/metering.go +++ b/runtime/common/metering.go @@ -795,6 +795,13 @@ func NewArrayExpressionMemoryUsage(length int) MemoryUsage { } } +func NewStringTemplateExpressionMemoryUsage(length int) MemoryUsage { + return MemoryUsage{ + Kind: MemoryKindStringTemplateExpression, + Amount: uint64(length), + } +} + func NewDictionaryExpressionMemoryUsage(length int) MemoryUsage { return MemoryUsage{ Kind: MemoryKindDictionaryExpression, diff --git a/runtime/interpreter/interpreter_expression.go b/runtime/interpreter/interpreter_expression.go index 05272cb606..13e2f192e3 100644 --- a/runtime/interpreter/interpreter_expression.go +++ b/runtime/interpreter/interpreter_expression.go @@ -961,22 +961,18 @@ func (interpreter *Interpreter) VisitStringExpression(expression *ast.StringExpr 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) { - // this is equivalent to toString() for supported types - s := values[i].String() - switch argumentTypes[i] { - case sema.StringType: - // remove quotations - s = s[1 : len(s)-1] - builder.WriteString(s) + // 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(s) + builder.WriteString(value.String()) } } } diff --git a/runtime/parser/expression_test.go b/runtime/parser/expression_test.go index 3804c13ce6..8f68a7b468 100644 --- a/runtime/parser/expression_test.go +++ b/runtime/parser/expression_test.go @@ -6317,6 +6317,60 @@ func TestParseStringTemplate(t *testing.T) { errs, ) }) + + t.Run("unbalanced paren", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "\(add" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected token ')'", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + }, + errs, + ) + }) + + t.Run("nested templates", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(` + "outer string \( "\(inner template)" )" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.Error(t, err) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected token ')'", + Pos: ast.Position{Offset: 30, Line: 2, Column: 29}, + }, + }, + errs, + ) + }) } func TestParseNilCoalescing(t *testing.T) { diff --git a/runtime/parser/lexer/lexer.go b/runtime/parser/lexer/lexer.go index 6b9c0714b8..25e7635594 100644 --- a/runtime/parser/lexer/lexer.go +++ b/runtime/parser/lexer/lexer.go @@ -49,11 +49,11 @@ type position struct { column int } -type LexerMode int +type lexerMode uint8 const ( - NORMAL = iota - STR_IDENTIFIER + NORMAL lexerMode = iota + STR_INTERPOLATION ) type lexer struct { @@ -82,7 +82,7 @@ type lexer struct { // canBackup indicates whether stepping back is allowed canBackup bool // lexer mode is used for string templates - mode LexerMode + mode lexerMode // counts the number of unclosed brackets for string templates \((())) openBrackets int } @@ -427,7 +427,7 @@ func (l *lexer) scanString(quote rune) { switch r { case '(': // string template, stop and set mode - l.mode = STR_IDENTIFIER + l.mode = STR_INTERPOLATION // no need to update prev values because these next tokens will not backup l.endOffset = tmpBackupOffset l.current = tmpBackup diff --git a/runtime/parser/lexer/state.go b/runtime/parser/lexer/state.go index f2775b81dc..b536111c86 100644 --- a/runtime/parser/lexer/state.go +++ b/runtime/parser/lexer/state.go @@ -56,14 +56,14 @@ func rootState(l *lexer) stateFn { case '%': l.emitType(TokenPercent) case '(': - if l.mode == STR_IDENTIFIER { + if l.mode == STR_INTERPOLATION { // 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 == STR_IDENTIFIER { + if l.mode == STR_INTERPOLATION { l.openBrackets-- if l.openBrackets == 0 { l.mode = NORMAL @@ -130,7 +130,7 @@ func rootState(l *lexer) stateFn { case '"': return stringState case '\\': - if l.mode == STR_IDENTIFIER { + if l.mode == STR_INTERPOLATION { r = l.next() switch r { case '(': diff --git a/runtime/sema/check_string_template_expression.go b/runtime/sema/check_string_template_expression.go index 7a7eac8b3d..ddb0d6ed0d 100644 --- a/runtime/sema/check_string_template_expression.go +++ b/runtime/sema/check_string_template_expression.go @@ -20,6 +20,12 @@ package sema import "github.com/onflow/cadence/runtime/ast" +// All number types, addresses, path types, bool, strings and characters are supported in string template +func isValidStringTemplateValue(valueType Type) bool { + return valueType == TheAddressType || valueType == StringType || valueType == BoolType || valueType == CharacterType || + IsSubType(valueType, NumberType) || IsSubType(valueType, PathType) +} + func (checker *Checker) VisitStringTemplateExpression(stringTemplateExpression *ast.StringTemplateExpression) Type { // visit all elements @@ -37,11 +43,7 @@ func (checker *Checker) VisitStringTemplateExpression(stringTemplateExpression * argumentTypes[i] = valueType - // All number types, addresses, path types, bool and strings are supported in string template - if IsSubType(valueType, NumberType) || IsSubType(valueType, TheAddressType) || - IsSubType(valueType, PathType) || IsSubType(valueType, StringType) || IsSubType(valueType, BoolType) { - checker.checkResourceMoveOperation(element, valueType) - } else { + if !isValidStringTemplateValue(valueType) { checker.report( &TypeMismatchWithDescriptionError{ ActualType: valueType, diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index 855888eea0..bf1f3a5c30 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -12394,4 +12394,22 @@ func TestInterpretStringTemplates(t *testing.T) { 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), + ) + }) } From 5f163333eb1290df11bd7e1443b08ea03eb8a3c0 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 2 Oct 2024 15:18:47 -0400 Subject: [PATCH 21/38] Refactor stringer, improve generator, update simple types. --- runtime/sema/bool_type.go | 3 + runtime/sema/character.cdc | 2 +- runtime/sema/character.gen.go | 1 + runtime/sema/gen/main.go | 263 +++++++++++++++++- runtime/sema/path_type.go | 3 + runtime/sema/simple_type.go | 21 ++ runtime/sema/string_type.go | 3 + runtime/sema/struct_stringer.cdc | 7 + .../{stringer.go => struct_stringer.gen.go} | 49 ++-- runtime/sema/struct_stringer.go | 21 ++ runtime/sema/type.go | 116 ++++---- 11 files changed, 412 insertions(+), 77 deletions(-) create mode 100644 runtime/sema/struct_stringer.cdc rename runtime/sema/{stringer.go => struct_stringer.gen.go} (53%) create mode 100644 runtime/sema/struct_stringer.go diff --git a/runtime/sema/bool_type.go b/runtime/sema/bool_type.go index 36f1db3e70..1c19660bd8 100644 --- a/runtime/sema/bool_type.go +++ b/runtime/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/runtime/sema/character.cdc b/runtime/sema/character.cdc index 7b54afdf5d..3532e4713a 100644 --- a/runtime/sema/character.cdc +++ b/runtime/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/runtime/sema/character.gen.go b/runtime/sema/character.gen.go index 3938f01bb8..57bcc0f431 100644 --- a/runtime/sema/character.gen.go +++ b/runtime/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/runtime/sema/gen/main.go b/runtime/sema/gen/main.go index d06e081257..9871ab963f 100644 --- a/runtime/sema/gen/main.go +++ b/runtime/sema/gen/main.go @@ -164,6 +164,7 @@ type typeDecl struct { memberDeclarations []ast.Declaration nestedTypes []*typeDecl hasConstructor bool + structStringer bool } type generator struct { @@ -572,6 +573,8 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ )) } typeDecl.memberAccessible = true + case "StructStringer": + typeDecl.structStringer = true } } @@ -736,8 +739,152 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ return } -func (*generator) VisitInterfaceDeclaration(_ *ast.InterfaceDeclaration) struct{} { - panic("interface declarations are not supported") +func (g *generator) VisitInterfaceDeclaration(decl *ast.InterfaceDeclaration) (_ struct{}) { + compositeKind := decl.CompositeKind + switch compositeKind { + case common.CompositeKindStructure, + common.CompositeKindResource, + common.CompositeKindContract: + break + default: + 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, + ) + } + + g.typeStack = append( + g.typeStack, + typeDecl, + ) + defer func() { + // Pop + lastIndex := len(g.typeStack) - 1 + g.typeStack[lastIndex] = nil + g.typeStack = g.typeStack[:lastIndex] + }() + + for _, memberDeclaration := range decl.Members.Declarations() { + generateDeclaration(g, memberDeclaration) + + // Visiting unsupported declarations panics, + // so only supported member declarations are added + typeDecl.memberDeclarations = append( + typeDecl.memberDeclarations, + memberDeclaration, + ) + } + + var typeVarDecl = interfaceTypeExpr(typeDecl) + + fullTypeName := typeDecl.fullTypeName + + tyVarName := typeVarName(fullTypeName) + + g.addDecls( + goConstDecl( + typeNameVarName(fullTypeName), + goStringLit(typeName), + ), + goVarDecl( + tyVarName, + typeVarDecl, + ), + ) + + memberDeclarations := typeDecl.memberDeclarations + + if len(memberDeclarations) > 0 { + + // func init() { + // members := []*Member{...} + // t.Members = MembersAsMap(members) + // t.Fields = MembersFieldNames(members) + // t.ConstructorParameters = ... + // } + + members := membersExpr( + fullTypeName, + tyVarName, + memberDeclarations, + ) + + const membersVariableIdentifier = "members" + + stmts := []dst.Stmt{ + &dst.DeclStmt{ + Decl: goVarDecl( + membersVariableIdentifier, + members, + ), + }, + &dst.AssignStmt{ + Lhs: []dst.Expr{ + &dst.SelectorExpr{ + X: dst.NewIdent(tyVarName), + Sel: dst.NewIdent("Members"), + }, + }, + Tok: token.ASSIGN, + Rhs: []dst.Expr{ + &dst.CallExpr{ + Fun: &dst.Ident{ + Name: "MembersAsMap", + Path: semaPath, + }, + Args: []dst.Expr{ + dst.NewIdent(membersVariableIdentifier), + }, + }, + }, + }, + &dst.AssignStmt{ + Lhs: []dst.Expr{ + &dst.SelectorExpr{ + X: dst.NewIdent(tyVarName), + Sel: dst.NewIdent("Fields"), + }, + }, + Tok: token.ASSIGN, + Rhs: []dst.Expr{ + &dst.CallExpr{ + Fun: &dst.Ident{ + Name: "MembersFieldNames", + Path: semaPath, + }, + Args: []dst.Expr{ + dst.NewIdent(membersVariableIdentifier), + }, + }, + }, + }, + } + + g.addDecls( + &dst.FuncDecl{ + Name: dst.NewIdent("init"), + Type: &dst.FuncType{}, + Body: &dst.BlockStmt{ + List: stmts, + }, + }, + ) + } + + return } func (*generator) VisitAttachmentDeclaration(_ *ast.AttachmentDeclaration) struct{} { @@ -1591,6 +1738,9 @@ func simpleTypeLiteral(ty *typeDecl) dst.Expr { // Comparable: false, // Exportable: false, // Importable: false, + // comformances: []*InterfaceType { + // StructStringer, + // } //} isResource := ty.compositeKind == common.CompositeKindResource @@ -1607,6 +1757,21 @@ func simpleTypeLiteral(ty *typeDecl) dst.Expr { goKeyValue("Exportable", goBoolLit(ty.exportable)), goKeyValue("Importable", goBoolLit(ty.importable)), goKeyValue("ContainFields", goBoolLit(ty.memberAccessible)), + goKeyValue("conformances", &dst.CompositeLit{ + Type: &dst.ArrayType{ + Elt: &dst.StarExpr{ + X: &dst.Ident{ + Name: "InterfaceType", + }, + }, + }, + Elts: []dst.Expr{ + &dst.Ident{ + Name: "StructStringerType", + Path: semaPath, + }, + }, + }), } return &dst.UnaryExpr{ @@ -2069,6 +2234,100 @@ func compositeTypeLiteral(ty *typeDecl) dst.Expr { } } +func interfaceTypeExpr(ty *typeDecl) dst.Expr { + + // 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, + interfaceTypeLiteral(ty), + ), + }, + } + + for _, nestedType := range ty.nestedTypes { + statements = append( + statements, + &dst.ExprStmt{ + X: &dst.CallExpr{ + Fun: &dst.SelectorExpr{ + X: dst.NewIdent(typeVarName), + Sel: dst.NewIdent("SetNestedType"), + }, + Args: []dst.Expr{ + typeNameVarIdent(nestedType.fullTypeName), + typeVarIdent(nestedType.fullTypeName), + }, + }, + }, + ) + } + + statements = append( + statements, + &dst.ReturnStmt{ + Results: []dst.Expr{ + dst.NewIdent(typeVarName), + }, + }, + ) + + return &dst.CallExpr{ + Fun: &dst.FuncLit{ + Type: &dst.FuncType{ + Func: true, + Results: &dst.FieldList{ + List: []*dst.Field{ + { + Type: &dst.StarExpr{ + X: &dst.Ident{ + Name: "InterfaceType", + Path: semaPath, + }, + }, + }, + }, + }, + }, + Body: &dst.BlockStmt{ + List: statements, + }, + }, + } +} + +func interfaceTypeLiteral(ty *typeDecl) dst.Expr { + kind := compositeKindExpr(ty.compositeKind) + + elements := []dst.Expr{ + goKeyValue("Identifier", typeNameVarIdent(ty.fullTypeName)), + goKeyValue("CompositeKind", kind), + } + + return &dst.UnaryExpr{ + Op: token.AND, + X: &dst.CompositeLit{ + Type: &dst.Ident{ + Name: "InterfaceType", + Path: semaPath, + }, + Elts: elements, + }, + } +} + func typeAnnotationCallExpr(ty dst.Expr) *dst.CallExpr { return &dst.CallExpr{ Fun: &dst.Ident{ diff --git a/runtime/sema/path_type.go b/runtime/sema/path_type.go index 4334054f60..018a189aec 100644 --- a/runtime/sema/path_type.go +++ b/runtime/sema/path_type.go @@ -31,6 +31,9 @@ var PathType = &SimpleType{ Comparable: false, Exportable: true, Importable: true, + conformances: []*InterfaceType{ + StructStringerType, + }, } var PathTypeAnnotation = NewTypeAnnotation(PathType) diff --git a/runtime/sema/simple_type.go b/runtime/sema/simple_type.go index 7f2d7e362a..eea74d1060 100644 --- a/runtime/sema/simple_type.go +++ b/runtime/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/runtime/sema/string_type.go b/runtime/sema/string_type.go index 200c599f88..9257c4b5bb 100644 --- a/runtime/sema/string_type.go +++ b/runtime/sema/string_type.go @@ -143,6 +143,9 @@ var StringType = &SimpleType{ }, IndexingType: IntegerType, }, + conformances: []*InterfaceType{ + StructStringerType, + }, } var StringTypeAnnotation = NewTypeAnnotation(StringType) diff --git a/runtime/sema/struct_stringer.cdc b/runtime/sema/struct_stringer.cdc new file mode 100644 index 0000000000..224765fd90 --- /dev/null +++ b/runtime/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/runtime/sema/stringer.go b/runtime/sema/struct_stringer.gen.go similarity index 53% rename from runtime/sema/stringer.go rename to runtime/sema/struct_stringer.gen.go index 1331fdc3eb..fd583822cd 100644 --- a/runtime/sema/stringer.go +++ b/runtime/sema/struct_stringer.gen.go @@ -1,3 +1,4 @@ +// Code generated from struct_stringer.cdc. DO NOT EDIT. /* * Cadence - The resource-oriented smart contract programming language * @@ -23,37 +24,41 @@ import ( "github.com/onflow/cadence/runtime/common" ) -const StringerTypeName = "Stringer" +const StructStringerTypeToStringFunctionName = "toString" -var StringerType = func() *InterfaceType { +var StructStringerTypeToStringFunctionType = &FunctionType{ + Purity: FunctionPurityView, + ReturnTypeAnnotation: NewTypeAnnotation( + StringType, + ), +} - stringerType := &InterfaceType{ - Identifier: StringerTypeName, - CompositeKind: common.CompositeKindStructure, - Members: &StringMemberOrderedMap{}, - } +const StructStringerTypeToStringFunctionDocString = ` +Returns the string representation of this object. +` - const StringerTypeToStringFunctionDocString = `Returns this object as a String.` +const StructStringerTypeName = "StructStringer" - const StringerTypeToStringFunctionName = "toString" - - var StringerTypeToStringFunctionType = &FunctionType{ - Purity: FunctionPurityView, - ReturnTypeAnnotation: NewTypeAnnotation( - StringType, - ), +var StructStringerType = func() *InterfaceType { + var t = &InterfaceType{ + Identifier: StructStringerTypeName, + CompositeKind: common.CompositeKindStructure, } + return t +}() + +func init() { var members = []*Member{ NewUnmeteredFunctionMember( - stringerType, + StructStringerType, PrimitiveAccess(ast.AccessAll), - StringerTypeToStringFunctionName, - StringerTypeToStringFunctionType, - StringerTypeToStringFunctionDocString, + StructStringerTypeToStringFunctionName, + StructStringerTypeToStringFunctionType, + StructStringerTypeToStringFunctionDocString, ), } - stringerType.Members = MembersAsMap(members) - return stringerType -}() + StructStringerType.Members = MembersAsMap(members) + StructStringerType.Fields = MembersFieldNames(members) +} diff --git a/runtime/sema/struct_stringer.go b/runtime/sema/struct_stringer.go new file mode 100644 index 0000000000..a91c8796c7 --- /dev/null +++ b/runtime/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/runtime/sema/type.go b/runtime/sema/type.go index c9ab715c7f..221b6ce67b 100644 --- a/runtime/sema/type.go +++ b/runtime/sema/type.go @@ -4198,7 +4198,7 @@ func init() { DeploymentResultType, HashableStructType, &InclusiveRangeType{}, - StringerType, + StructStringerType, }, ) @@ -4900,7 +4900,7 @@ func IsHashableStructType(t Type) bool { } } -// which simple types conform to stringer interface (except Bool?) +// which simple types conform to stringer interface func IsStringerType(t Type) bool { switch t { case BoolType, CharacterType, StringType: @@ -7723,6 +7723,14 @@ func checkSubTypeWithoutEquality(subType Type, superType Type) bool { case *IntersectionType: + switch typedSubType := subType.(type) { + case *SimpleType: + // An simple type `T` is a subtype of an intersection type `{Vs}` / `{Vs}` / `{Vs}`: + // when `T` conforms to `Vs`. + return typedSuperType.EffectiveIntersectionSet(). + IsSubsetOf(typedSubType.EffectiveInterfaceConformanceSet()) + } + // TODO: replace with // //switch typedSubType := subType.(type) { @@ -7837,9 +7845,8 @@ func checkSubTypeWithoutEquality(subType Type, superType Type) bool { IsSubsetOf(typedSubType.EffectiveInterfaceConformanceSet()) } - // STRINGERTODO: other options? how to make existing simple types - // conform to an intersection type? - if typedSuperType.Types[0].Identifier == StringerTypeName { + // deal with non-simple types such as NumberType and AddressType + if typedSuperType.EffectiveIntersectionSet().Contains(StructStringerType) { return IsStringerType(subType) } @@ -9517,10 +9524,57 @@ 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) { + nestedCompositeType, ok := nestedType.(*CompositeType) + if !ok { + return + } + + types = append(types, nestedCompositeType) + }) + case *InterfaceType: + NativeInterfaceTypes[actualType.QualifiedIdentifier()] = actualType + + nestedTypes := actualType.NestedTypes + if nestedTypes == nil { + continue + } + + nestedTypes.Foreach(func(_ string, nestedType Type) { + nestedInterfaceType, ok := nestedType.(*InterfaceType) + if !ok { + return + } + + types = append(types, nestedInterfaceType) + }) + } + + } +} + var NativeCompositeTypes = map[string]*CompositeType{} func init() { - compositeTypes := []*CompositeType{ + compositeTypes := []Type{ AccountKeyType, PublicKeyType, HashAlgorithmType, @@ -9529,57 +9583,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 - - nestedTypes := compositeType.NestedTypes - if nestedTypes == nil { - continue - } - - nestedTypes.Foreach(func(_ string, nestedType Type) { - nestedCompositeType, ok := nestedType.(*CompositeType) - if !ok { - return - } - - compositeTypes = append(compositeTypes, nestedCompositeType) - }) - } + extractNativeTypes(compositeTypes) } var NativeInterfaceTypes = map[string]*InterfaceType{} func init() { - interfaceTypes := []*InterfaceType{ - StringerType, + interfaceTypes := []Type{ + StructStringerType, } - for len(interfaceTypes) > 0 { - lastIndex := len(interfaceTypes) - 1 - interfaceType := interfaceTypes[lastIndex] - interfaceTypes[lastIndex] = nil - interfaceTypes = interfaceTypes[:lastIndex] - - NativeInterfaceTypes[interfaceType.QualifiedIdentifier()] = interfaceType - - nestedTypes := interfaceType.NestedTypes - if nestedTypes == nil { - continue - } - - nestedTypes.Foreach(func(_ string, nestedType Type) { - nestedInterfaceType, ok := nestedType.(*InterfaceType) - if !ok { - return - } - - interfaceTypes = append(interfaceTypes, nestedInterfaceType) - }) - } + extractNativeTypes(interfaceTypes) } From ab157ad8e4b0fb15147493aca0beab5d08be0203 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 2 Oct 2024 15:19:42 -0400 Subject: [PATCH 22/38] Update tests. --- runtime/tests/checker/stringer_test.go | 18 ++++++------ runtime/tests/interpreter/stringer_test.go | 32 +++++++++++++++++++--- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/runtime/tests/checker/stringer_test.go b/runtime/tests/checker/stringer_test.go index e9b953f4e3..eb6897e232 100644 --- a/runtime/tests/checker/stringer_test.go +++ b/runtime/tests/checker/stringer_test.go @@ -31,16 +31,16 @@ func TestCheckStringer(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` - let a: {Stringer} = 1 - let b: {Stringer} = false - let c: {Stringer} = "hey" + let a: {StructStringer} = 1 + let b: {StructStringer} = false + let c: {StructStringer} = "hey" access(all) - struct Foo: Stringer { + struct Foo: StructStringer { view fun toString():String { return "foo" } } - let d: {Stringer} = Foo() + let d: {StructStringer} = Foo() `) assert.NoError(t, err) @@ -53,10 +53,10 @@ func TestCheckInvalidStringer(t *testing.T) { _, err := ParseAndCheck(t, ` resource R {} - let a: {Stringer} = <-create R() - let b: {Stringer} = [<-create R()] - let c: {Stringer} = {1: true} - struct Foo: Stringer {} + let a: {StructStringer} = <-create R() + let b: {StructStringer} = [<-create R()] + let c: {StructStringer} = {1: true} + struct Foo: StructStringer {} `) errs := RequireCheckerErrors(t, err, 4) diff --git a/runtime/tests/interpreter/stringer_test.go b/runtime/tests/interpreter/stringer_test.go index fbd6da1388..eca78e7b5c 100644 --- a/runtime/tests/interpreter/stringer_test.go +++ b/runtime/tests/interpreter/stringer_test.go @@ -33,12 +33,12 @@ func TestStringerBasic(t *testing.T) { inter := parseCheckAndInterpret(t, ` access(all) - struct Example: Stringer { - view fun toString():String { + struct Example: StructStringer { + view fun toString(): String { return "example" } } - fun test() :String { + fun test(): String { return Example().toString() } `) @@ -60,7 +60,7 @@ func TestStringerBuiltIn(t *testing.T) { inter := parseCheckAndInterpret(t, ` access(all) - fun test() :String { + fun test(): String { let v = 1 return v.toString() } @@ -76,3 +76,27 @@ func TestStringerBuiltIn(t *testing.T) { result, ) } + +func TestStringerAsValue(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, + ) +} From 72ff98ca1832b518b26f8d1e438e17cf709b62c2 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 2 Oct 2024 15:29:44 -0400 Subject: [PATCH 23/38] Fix generator conditional conformances check. --- runtime/sema/gen/main.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/runtime/sema/gen/main.go b/runtime/sema/gen/main.go index 9871ab963f..7588b742ec 100644 --- a/runtime/sema/gen/main.go +++ b/runtime/sema/gen/main.go @@ -1757,7 +1757,10 @@ func simpleTypeLiteral(ty *typeDecl) dst.Expr { goKeyValue("Exportable", goBoolLit(ty.exportable)), goKeyValue("Importable", goBoolLit(ty.importable)), goKeyValue("ContainFields", goBoolLit(ty.memberAccessible)), - goKeyValue("conformances", &dst.CompositeLit{ + } + + if ty.structStringer { + elements = append(elements, goKeyValue("conformances", &dst.CompositeLit{ Type: &dst.ArrayType{ Elt: &dst.StarExpr{ X: &dst.Ident{ @@ -1771,7 +1774,7 @@ func simpleTypeLiteral(ty *typeDecl) dst.Expr { Path: semaPath, }, }, - }), + })) } return &dst.UnaryExpr{ From 060945d93dd7152538f39201156e32e842930014 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 2 Oct 2024 16:41:10 -0400 Subject: [PATCH 24/38] Code cleanup. --- runtime/parser/expression.go | 3 +-- runtime/parser/lexer/lexer.go | 8 ++++---- runtime/parser/lexer/state.go | 8 ++++---- .../sema/check_string_template_expression.go | 19 ++++++++++--------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/runtime/parser/expression.go b/runtime/parser/expression.go index 78a53c77ad..4d2dbbeef1 100644 --- a/runtime/parser/expression.go +++ b/runtime/parser/expression.go @@ -1165,14 +1165,13 @@ func defineStringExpression() { for curToken.Is(lexer.TokenString) { literal = p.tokenSource(curToken) - length = len(literal) // remove quotation marks if they exist if curToken == startToken { literal = literal[1:] - length = len(literal) } + length = len(literal) if length >= 1 && literal[length-1] == '"' { literal = literal[:length-1] missingEnd = false diff --git a/runtime/parser/lexer/lexer.go b/runtime/parser/lexer/lexer.go index 25e7635594..4bd89aabf6 100644 --- a/runtime/parser/lexer/lexer.go +++ b/runtime/parser/lexer/lexer.go @@ -52,8 +52,8 @@ type position struct { type lexerMode uint8 const ( - NORMAL lexerMode = iota - STR_INTERPOLATION + normal lexerMode = iota + stringInterpolation ) type lexer struct { @@ -141,7 +141,7 @@ func (l *lexer) clear() { l.cursor = 0 l.tokens = l.tokens[:0] l.tokenCount = 0 - l.mode = NORMAL + l.mode = normal l.openBrackets = 0 } @@ -427,7 +427,7 @@ func (l *lexer) scanString(quote rune) { switch r { case '(': // string template, stop and set mode - l.mode = STR_INTERPOLATION + l.mode = stringInterpolation // no need to update prev values because these next tokens will not backup l.endOffset = tmpBackupOffset l.current = tmpBackup diff --git a/runtime/parser/lexer/state.go b/runtime/parser/lexer/state.go index b536111c86..0a436760af 100644 --- a/runtime/parser/lexer/state.go +++ b/runtime/parser/lexer/state.go @@ -56,17 +56,17 @@ func rootState(l *lexer) stateFn { case '%': l.emitType(TokenPercent) case '(': - if l.mode == STR_INTERPOLATION { + if l.mode == stringInterpolation { // 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 == STR_INTERPOLATION { + if l.mode == stringInterpolation { l.openBrackets-- if l.openBrackets == 0 { - l.mode = NORMAL + l.mode = normal return stringState } } @@ -130,7 +130,7 @@ func rootState(l *lexer) stateFn { case '"': return stringState case '\\': - if l.mode == STR_INTERPOLATION { + if l.mode == stringInterpolation { r = l.next() switch r { case '(': diff --git a/runtime/sema/check_string_template_expression.go b/runtime/sema/check_string_template_expression.go index ddb0d6ed0d..5baf38cd83 100644 --- a/runtime/sema/check_string_template_expression.go +++ b/runtime/sema/check_string_template_expression.go @@ -22,8 +22,16 @@ import "github.com/onflow/cadence/runtime/ast" // All number types, addresses, path types, bool, strings and characters are supported in string template func isValidStringTemplateValue(valueType Type) bool { - return valueType == TheAddressType || valueType == StringType || valueType == BoolType || valueType == CharacterType || - IsSubType(valueType, NumberType) || IsSubType(valueType, PathType) + 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 { @@ -55,12 +63,5 @@ func (checker *Checker) VisitStringTemplateExpression(stringTemplateExpression * } } - checker.Elaboration.SetStringTemplateExpressionTypes( - stringTemplateExpression, - StringTemplateExpressionTypes{ - ArgumentTypes: argumentTypes, - }, - ) - return StringType } From ad94aebd34a09163b10822b2404362f5b0c0302e Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Thu, 3 Oct 2024 10:35:33 -0400 Subject: [PATCH 25/38] Add ConformingType interface. --- runtime/sema/gen/main.go | 22 ++++--- runtime/sema/path_type.go | 12 ++++ runtime/sema/type.go | 71 ++++++++++++++++++---- runtime/tests/checker/stringer_test.go | 1 + runtime/tests/interpreter/stringer_test.go | 25 +++++++- 5 files changed, 108 insertions(+), 23 deletions(-) diff --git a/runtime/sema/gen/main.go b/runtime/sema/gen/main.go index 7588b742ec..ae16966da5 100644 --- a/runtime/sema/gen/main.go +++ b/runtime/sema/gen/main.go @@ -164,7 +164,9 @@ type typeDecl struct { memberDeclarations []ast.Declaration nestedTypes []*typeDecl hasConstructor bool - structStringer bool + + // used in simpleType generation + conformances []string } type generator struct { @@ -574,7 +576,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ } typeDecl.memberAccessible = true case "StructStringer": - typeDecl.structStringer = true + typeDecl.conformances = append(typeDecl.conformances, "StructStringerType") } } @@ -1759,7 +1761,14 @@ func simpleTypeLiteral(ty *typeDecl) dst.Expr { goKeyValue("ContainFields", goBoolLit(ty.memberAccessible)), } - if ty.structStringer { + if len(ty.conformances) > 0 { + var elts = []dst.Expr{} + for _, conformance := range ty.conformances { + elts = append(elts, &dst.Ident{ + Name: conformance, + Path: semaPath, + }) + } elements = append(elements, goKeyValue("conformances", &dst.CompositeLit{ Type: &dst.ArrayType{ Elt: &dst.StarExpr{ @@ -1768,12 +1777,7 @@ func simpleTypeLiteral(ty *typeDecl) dst.Expr { }, }, }, - Elts: []dst.Expr{ - &dst.Ident{ - Name: "StructStringerType", - Path: semaPath, - }, - }, + Elts: elts, })) } diff --git a/runtime/sema/path_type.go b/runtime/sema/path_type.go index 018a189aec..042be61502 100644 --- a/runtime/sema/path_type.go +++ b/runtime/sema/path_type.go @@ -51,6 +51,9 @@ var StoragePathType = &SimpleType{ Comparable: false, Exportable: true, Importable: true, + conformances: []*InterfaceType{ + StructStringerType, + }, } var StoragePathTypeAnnotation = NewTypeAnnotation(StoragePathType) @@ -68,6 +71,9 @@ var CapabilityPathType = &SimpleType{ Comparable: false, Exportable: true, Importable: true, + conformances: []*InterfaceType{ + StructStringerType, + }, } var CapabilityPathTypeAnnotation = NewTypeAnnotation(CapabilityPathType) @@ -85,6 +91,9 @@ var PublicPathType = &SimpleType{ Comparable: false, Exportable: true, Importable: true, + conformances: []*InterfaceType{ + StructStringerType, + }, } var PublicPathTypeAnnotation = NewTypeAnnotation(PublicPathType) @@ -102,6 +111,9 @@ var PrivatePathType = &SimpleType{ Comparable: false, Exportable: true, Importable: true, + conformances: []*InterfaceType{ + StructStringerType, + }, } var PrivatePathTypeAnnotation = NewTypeAnnotation(PrivatePathType) diff --git a/runtime/sema/type.go b/runtime/sema/type.go index 221b6ce67b..3687c385b1 100644 --- a/runtime/sema/type.go +++ b/runtime/sema/type.go @@ -312,6 +312,12 @@ func TypeActivationNestedType(typeActivation *VariableActivation, qualifiedIdent return ty } +// allow all types to specify interface conformances +type ConformingType interface { + Type + EffectiveInterfaceConformanceSet() *InterfaceSet +} + // CompositeKindedType is a type which has a composite kind type CompositeKindedType interface { Type @@ -1190,6 +1196,11 @@ type NumericType struct { memberResolversOnce sync.Once saturatingArithmetic SaturatingArithmeticSupport isSuperType bool + + // allow numeric types to conform to interfaces + conformances []*InterfaceType + effectiveInterfaceConformanceSet *InterfaceSet + effectiveInterfaceConformanceSetOnce sync.Once } var _ Type = &NumericType{} @@ -1197,7 +1208,12 @@ var _ IntegerRangedType = &NumericType{} var _ SaturatingArithmeticType = &NumericType{} func NewNumericType(typeName string) *NumericType { - return &NumericType{name: typeName} + return &NumericType{ + name: typeName, + conformances: []*InterfaceType{ + StructStringerType, + }, + } } func (t *NumericType) Tag() TypeTag { @@ -1375,6 +1391,21 @@ func (*NumericType) CheckInstantiated(_ ast.HasPosition, _ common.MemoryGauge, _ // NO-OP } +func (t *NumericType) EffectiveInterfaceConformanceSet() *InterfaceSet { + t.initializeEffectiveInterfaceConformanceSet() + return t.effectiveInterfaceConformanceSet +} + +func (t *NumericType) initializeEffectiveInterfaceConformanceSet() { + t.effectiveInterfaceConformanceSetOnce.Do(func() { + t.effectiveInterfaceConformanceSet = NewInterfaceSet() + + for _, conformance := range t.conformances { + t.effectiveInterfaceConformanceSet.Add(conformance) + } + }) +} + // FixedPointNumericType represents all the types in the fixed-point range. type FixedPointNumericType struct { maxFractional *big.Int @@ -7256,11 +7287,18 @@ const AddressTypeName = "Address" // AddressType represents the address type type AddressType struct { - memberResolvers map[string]MemberResolver - memberResolversOnce sync.Once + memberResolvers map[string]MemberResolver + memberResolversOnce sync.Once + conformances []*InterfaceType + effectiveInterfaceConformanceSet *InterfaceSet + effectiveInterfaceConformanceSetOnce sync.Once } -var TheAddressType = &AddressType{} +var TheAddressType = &AddressType{ + conformances: []*InterfaceType{ + StructStringerType, + }, +} var AddressTypeAnnotation = NewTypeAnnotation(TheAddressType) var _ Type = &AddressType{} @@ -7405,6 +7443,21 @@ func (t *AddressType) initializeMemberResolvers() { }) } +func (t *AddressType) EffectiveInterfaceConformanceSet() *InterfaceSet { + t.initializeEffectiveInterfaceConformanceSet() + return t.effectiveInterfaceConformanceSet +} + +func (t *AddressType) initializeEffectiveInterfaceConformanceSet() { + t.effectiveInterfaceConformanceSetOnce.Do(func() { + t.effectiveInterfaceConformanceSet = NewInterfaceSet() + + for _, conformance := range t.conformances { + t.effectiveInterfaceConformanceSet.Add(conformance) + } + }) +} + func IsPrimitiveOrContainerOfPrimitive(referencedType Type) bool { switch ty := referencedType.(type) { case *VariableSizedType: @@ -7723,14 +7776,6 @@ func checkSubTypeWithoutEquality(subType Type, superType Type) bool { case *IntersectionType: - switch typedSubType := subType.(type) { - case *SimpleType: - // An simple type `T` is a subtype of an intersection type `{Vs}` / `{Vs}` / `{Vs}`: - // when `T` conforms to `Vs`. - return typedSuperType.EffectiveIntersectionSet(). - IsSubsetOf(typedSubType.EffectiveInterfaceConformanceSet()) - } - // TODO: replace with // //switch typedSubType := subType.(type) { @@ -7829,7 +7874,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, diff --git a/runtime/tests/checker/stringer_test.go b/runtime/tests/checker/stringer_test.go index eb6897e232..5bfc2c7fcb 100644 --- a/runtime/tests/checker/stringer_test.go +++ b/runtime/tests/checker/stringer_test.go @@ -41,6 +41,7 @@ func TestCheckStringer(t *testing.T) { } } let d: {StructStringer} = Foo() + let e: {StructStringer} = /public/foo `) assert.NoError(t, err) diff --git a/runtime/tests/interpreter/stringer_test.go b/runtime/tests/interpreter/stringer_test.go index eca78e7b5c..eafe44652f 100644 --- a/runtime/tests/interpreter/stringer_test.go +++ b/runtime/tests/interpreter/stringer_test.go @@ -77,7 +77,7 @@ func TestStringerBuiltIn(t *testing.T) { ) } -func TestStringerAsValue(t *testing.T) { +func TestStringerCast(t *testing.T) { t.Parallel() @@ -100,3 +100,26 @@ func TestStringerAsValue(t *testing.T) { 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, + ) +} From 26428cb0f2fe7993080a8e87dbd43ae903388120 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Thu, 3 Oct 2024 10:39:45 -0400 Subject: [PATCH 26/38] Clean up unnecessary code. --- runtime/sema/type.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/runtime/sema/type.go b/runtime/sema/type.go index 3687c385b1..b22f23f9d0 100644 --- a/runtime/sema/type.go +++ b/runtime/sema/type.go @@ -4931,17 +4931,6 @@ func IsHashableStructType(t Type) bool { } } -// which simple types conform to stringer interface -func IsStringerType(t Type) bool { - switch t { - case BoolType, CharacterType, StringType: - return true - default: - return IsSubType(t, NumberType) || - IsSubType(t, PathType) || IsSubType(t, TheAddressType) - } -} - func (t *CompositeType) GetBaseType() Type { return t.baseType } @@ -7890,11 +7879,6 @@ func checkSubTypeWithoutEquality(subType Type, superType Type) bool { IsSubsetOf(typedSubType.EffectiveInterfaceConformanceSet()) } - // deal with non-simple types such as NumberType and AddressType - if typedSuperType.EffectiveIntersectionSet().Contains(StructStringerType) { - return IsStringerType(subType) - } - default: // Supertype (intersection) has a non-Any* legacy type From 447f05565ac9c6d4092edd117822407b5c02e069 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Thu, 3 Oct 2024 16:02:36 -0400 Subject: [PATCH 27/38] Remove unused code. --- runtime/sema/elaboration.go | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/runtime/sema/elaboration.go b/runtime/sema/elaboration.go index d2ef948a78..b6b025eef0 100644 --- a/runtime/sema/elaboration.go +++ b/runtime/sema/elaboration.go @@ -79,10 +79,6 @@ type ArrayExpressionTypes struct { ArgumentTypes []Type } -type StringTemplateExpressionTypes struct { - ArgumentTypes []Type -} - type DictionaryExpressionTypes struct { DictionaryType *DictionaryType EntryTypes []DictionaryEntryType @@ -144,7 +140,6 @@ 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 @@ -485,21 +480,6 @@ 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 From c81e69c17bf32f9deb9368fba58d9fb5077cf049 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Thu, 10 Oct 2024 15:33:59 -0700 Subject: [PATCH 28/38] Add simple tests, fix formatting. --- runtime/sema/gen/main.go | 3 +-- runtime/tests/checker/stringer_test.go | 10 ++++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/runtime/sema/gen/main.go b/runtime/sema/gen/main.go index ae16966da5..1b07af1fe6 100644 --- a/runtime/sema/gen/main.go +++ b/runtime/sema/gen/main.go @@ -814,8 +814,7 @@ func (g *generator) VisitInterfaceDeclaration(decl *ast.InterfaceDeclaration) (_ // func init() { // members := []*Member{...} // t.Members = MembersAsMap(members) - // t.Fields = MembersFieldNames(members) - // t.ConstructorParameters = ... + // t.Fields = MembersFieldNames(members)= // } members := membersExpr( diff --git a/runtime/tests/checker/stringer_test.go b/runtime/tests/checker/stringer_test.go index 5bfc2c7fcb..c0026904ab 100644 --- a/runtime/tests/checker/stringer_test.go +++ b/runtime/tests/checker/stringer_test.go @@ -36,7 +36,7 @@ func TestCheckStringer(t *testing.T) { let c: {StructStringer} = "hey" access(all) struct Foo: StructStringer { - view fun toString():String { + view fun toString(): String { return "foo" } } @@ -58,12 +58,18 @@ func TestCheckInvalidStringer(t *testing.T) { 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, 4) + 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]) } From 57805ee7d985eeddd7590e295ff123292df4914e Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 16 Oct 2024 10:45:53 -0400 Subject: [PATCH 29/38] Update code after review. --- runtime/ast/expression.go | 7 +- runtime/ast/string_template_test.go | 48 ++++++ runtime/parser/expression_test.go | 142 +++++++++++++++++- runtime/parser/lexer/lexer.go | 8 +- runtime/parser/lexer/state.go | 8 +- .../sema/check_string_template_expression.go | 3 +- 6 files changed, 203 insertions(+), 13 deletions(-) create mode 100644 runtime/ast/string_template_test.go diff --git a/runtime/ast/expression.go b/runtime/ast/expression.go index b8bf93e86d..8cf54fdce5 100644 --- a/runtime/ast/expression.go +++ b/runtime/ast/expression.go @@ -264,7 +264,12 @@ func (e *StringTemplateExpression) String() string { } func (e *StringTemplateExpression) Doc() prettier.Doc { - return prettier.Text(QuoteString("String template")) + if len(e.Expressions) == 0 { + return prettier.Text(e.Values[0]) + } + + // TODO: must reproduce expressions as literals + panic("not implemented") } func (e *StringTemplateExpression) MarshalJSON() ([]byte, error) { diff --git a/runtime/ast/string_template_test.go b/runtime/ast/string_template_test.go new file mode 100644 index 0000000000..d6aecb0299 --- /dev/null +++ b/runtime/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_test + +import ( + "testing" + + "github.com/onflow/cadence/runtime/ast" + "github.com/stretchr/testify/assert" + "github.com/turbolent/prettier" +) + +func TestStringTemplate_Doc(t *testing.T) { + + t.Parallel() + + stmt := &ast.StringTemplateExpression{ + Values: []string{ + "abc", + }, + Expressions: []ast.Expression{}, + Range: ast.Range{ + StartPos: ast.Position{Offset: 4, Line: 2, Column: 3}, + EndPos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + } + + assert.Equal(t, + prettier.Text("abc"), + stmt.Doc(), + ) +} diff --git a/runtime/parser/expression_test.go b/runtime/parser/expression_test.go index 8f68a7b468..e2377cf934 100644 --- a/runtime/parser/expression_test.go +++ b/runtime/parser/expression_test.go @@ -6318,7 +6318,7 @@ func TestParseStringTemplate(t *testing.T) { ) }) - t.Run("unbalanced paren", func(t *testing.T) { + t.Run("invalid, unbalanced paren", func(t *testing.T) { t.Parallel() @@ -6345,7 +6345,7 @@ func TestParseStringTemplate(t *testing.T) { ) }) - t.Run("nested templates", func(t *testing.T) { + t.Run("invalid, nested templates", func(t *testing.T) { t.Parallel() @@ -6371,6 +6371,144 @@ func TestParseStringTemplate(t *testing.T) { errs, ) }) + + t.Run("valid, alternating", func(t *testing.T) { + + t.Parallel() + + actual, errs := testParseExpression(` + "a\(b)c" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + + expected := &ast.StringTemplateExpression{ + Values: []string{ + "a", + "c", + }, + Expressions: []ast.Expression{ + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 4, Line: 2, Column: 3}, + EndPos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) + }) + + t.Run("valid, surrounded", func(t *testing.T) { + + t.Parallel() + + actual, errs := testParseExpression(` + "\(a)b\(c)" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + + expected := &ast.StringTemplateExpression{ + Values: []string{ + "", + "b", + "", + }, + Expressions: []ast.Expression{ + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "c", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 4, Line: 2, Column: 3}, + EndPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) + }) + + t.Run("valid, adjacent", func(t *testing.T) { + + t.Parallel() + + actual, errs := testParseExpression(` + "\(a)\(b)\(c)" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + + expected := &ast.StringTemplateExpression{ + Values: []string{ + "", + "", + "", + "", + }, + Expressions: []ast.Expression{ + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 11, Line: 2, Column: 11}, + }, + }, + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "c", + Pos: ast.Position{Offset: 15, Line: 2, Column: 16}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 4, Line: 2, Column: 3}, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 18}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) + }) } func TestParseNilCoalescing(t *testing.T) { diff --git a/runtime/parser/lexer/lexer.go b/runtime/parser/lexer/lexer.go index 4bd89aabf6..08f689be0e 100644 --- a/runtime/parser/lexer/lexer.go +++ b/runtime/parser/lexer/lexer.go @@ -52,8 +52,8 @@ type position struct { type lexerMode uint8 const ( - normal lexerMode = iota - stringInterpolation + lexerModeNormal lexerMode = iota + lexerModeStringInterpolation ) type lexer struct { @@ -141,7 +141,7 @@ func (l *lexer) clear() { l.cursor = 0 l.tokens = l.tokens[:0] l.tokenCount = 0 - l.mode = normal + l.mode = lexerModeNormal l.openBrackets = 0 } @@ -427,7 +427,7 @@ func (l *lexer) scanString(quote rune) { switch r { case '(': // string template, stop and set mode - l.mode = stringInterpolation + l.mode = lexerModeStringInterpolation // no need to update prev values because these next tokens will not backup l.endOffset = tmpBackupOffset l.current = tmpBackup diff --git a/runtime/parser/lexer/state.go b/runtime/parser/lexer/state.go index 0a436760af..08558c6b78 100644 --- a/runtime/parser/lexer/state.go +++ b/runtime/parser/lexer/state.go @@ -56,17 +56,17 @@ func rootState(l *lexer) stateFn { case '%': l.emitType(TokenPercent) case '(': - if l.mode == stringInterpolation { + 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 == stringInterpolation { + if l.mode == lexerModeStringInterpolation { l.openBrackets-- if l.openBrackets == 0 { - l.mode = normal + l.mode = lexerModeNormal return stringState } } @@ -130,7 +130,7 @@ func rootState(l *lexer) stateFn { case '"': return stringState case '\\': - if l.mode == stringInterpolation { + if l.mode == lexerModeStringInterpolation { r = l.next() switch r { case '(': diff --git a/runtime/sema/check_string_template_expression.go b/runtime/sema/check_string_template_expression.go index 5baf38cd83..8b4be45b48 100644 --- a/runtime/sema/check_string_template_expression.go +++ b/runtime/sema/check_string_template_expression.go @@ -42,9 +42,8 @@ func (checker *Checker) VisitStringTemplateExpression(stringTemplateExpression * elementCount := len(stringTemplateExpression.Expressions) - var argumentTypes []Type if elementCount > 0 { - argumentTypes = make([]Type, elementCount) + argumentTypes := make([]Type, elementCount) for i, element := range stringTemplateExpression.Expressions { valueType := checker.VisitExpression(element, stringTemplateExpression, elementType) From cd83793f8d65f9a5dcf67c09370891c536c496f8 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 16 Oct 2024 10:53:56 -0400 Subject: [PATCH 30/38] Fix linting. --- runtime/ast/string_template_test.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/runtime/ast/string_template_test.go b/runtime/ast/string_template_test.go index d6aecb0299..a19a399bb9 100644 --- a/runtime/ast/string_template_test.go +++ b/runtime/ast/string_template_test.go @@ -16,12 +16,11 @@ * limitations under the License. */ -package ast_test +package ast import ( "testing" - "github.com/onflow/cadence/runtime/ast" "github.com/stretchr/testify/assert" "github.com/turbolent/prettier" ) @@ -30,14 +29,14 @@ func TestStringTemplate_Doc(t *testing.T) { t.Parallel() - stmt := &ast.StringTemplateExpression{ + stmt := &StringTemplateExpression{ Values: []string{ "abc", }, - Expressions: []ast.Expression{}, - Range: ast.Range{ - StartPos: ast.Position{Offset: 4, Line: 2, Column: 3}, - EndPos: ast.Position{Offset: 11, Line: 2, Column: 10}, + Expressions: []Expression{}, + Range: Range{ + StartPos: Position{Offset: 4, Line: 2, Column: 3}, + EndPos: Position{Offset: 11, Line: 2, Column: 10}, }, } From fab5f6c196f07d1ed822c931ddc2febafedafd19 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 16 Oct 2024 14:54:46 -0400 Subject: [PATCH 31/38] Abstract common functionality, clean up. --- runtime/sema/gen/main.go | 361 +++++------------- .../gen/testdata/simple_interface/test.cdc | 1 + .../testdata/simple_interface/test.golden.go | 36 ++ runtime/sema/type.go | 76 ++-- 4 files changed, 151 insertions(+), 323 deletions(-) create mode 100644 runtime/sema/gen/testdata/simple_interface/test.cdc create mode 100644 runtime/sema/gen/testdata/simple_interface/test.golden.go diff --git a/runtime/sema/gen/main.go b/runtime/sema/gen/main.go index 1b07af1fe6..a56f0efa14 100644 --- a/runtime/sema/gen/main.go +++ b/runtime/sema/gen/main.go @@ -166,7 +166,7 @@ type typeDecl struct { hasConstructor bool // used in simpleType generation - conformances []string + conformances []*sema.InterfaceType } type generator struct { @@ -432,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 isCompositeType 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 + isCompositeType = true + case *ast.InterfaceDeclaration: + compositeKind = actualDecl.Kind() + typeName = actualDecl.Identifier.Identifier + typeDec = &typeDecl{ + typeName: typeName, + fullTypeName: g.newFullTypeName(typeName), + compositeKind: compositeKind, + } + members = actualDecl.Members.Declarations() + isCompositeType = false + default: + panic("Expected composite or interface declaration") + } - compositeKind := decl.CompositeKind switch compositeKind { case common.CompositeKindStructure, common.CompositeKindResource, @@ -444,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 @@ -476,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 !isCompositeType { + generateSimpleType = false } else { // If not, decide what to generate depending on the type. @@ -495,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, ) @@ -517,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 { @@ -526,7 +551,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ g.currentTypeID(), )) } - typeDecl.storable = true + typeDec.storable = true case "Primitive": if !generateSimpleType { @@ -535,7 +560,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ g.currentTypeID(), )) } - typeDecl.primitive = true + typeDec.primitive = true case "Equatable": if !generateSimpleType { @@ -544,7 +569,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ g.currentTypeID(), )) } - typeDecl.equatable = true + typeDec.equatable = true case "Comparable": if !generateSimpleType { @@ -553,7 +578,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ g.currentTypeID(), )) } - typeDecl.comparable = true + typeDec.comparable = true case "Exportable": if !generateSimpleType { @@ -562,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 { @@ -574,20 +599,20 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ g.currentTypeID(), )) } - typeDecl.memberAccessible = true + typeDec.memberAccessible = true case "StructStringer": - typeDecl.conformances = append(typeDecl.conformances, "StructStringerType") + 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, isCompositeType) } - fullTypeName := typeDecl.fullTypeName + fullTypeName := typeDec.fullTypeName tyVarName := typeVarName(fullTypeName) @@ -602,7 +627,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ ), ) - memberDeclarations := typeDecl.memberDeclarations + memberDeclarations := typeDec.memberDeclarations if len(memberDeclarations) > 0 { @@ -705,7 +730,7 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ }, } - if typeDecl.hasConstructor { + if typeDec.hasConstructor { stmts = append( stmts, &dst.AssignStmt{ @@ -741,151 +766,12 @@ func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ return } -func (g *generator) VisitInterfaceDeclaration(decl *ast.InterfaceDeclaration) (_ struct{}) { - compositeKind := decl.CompositeKind - switch compositeKind { - case common.CompositeKindStructure, - common.CompositeKindResource, - common.CompositeKindContract: - break - default: - 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, - ) - } - - g.typeStack = append( - g.typeStack, - typeDecl, - ) - defer func() { - // Pop - lastIndex := len(g.typeStack) - 1 - g.typeStack[lastIndex] = nil - g.typeStack = g.typeStack[:lastIndex] - }() - - for _, memberDeclaration := range decl.Members.Declarations() { - generateDeclaration(g, memberDeclaration) - - // Visiting unsupported declarations panics, - // so only supported member declarations are added - typeDecl.memberDeclarations = append( - typeDecl.memberDeclarations, - memberDeclaration, - ) - } - - var typeVarDecl = interfaceTypeExpr(typeDecl) - - fullTypeName := typeDecl.fullTypeName - - tyVarName := typeVarName(fullTypeName) - - g.addDecls( - goConstDecl( - typeNameVarName(fullTypeName), - goStringLit(typeName), - ), - goVarDecl( - tyVarName, - typeVarDecl, - ), - ) - - memberDeclarations := typeDecl.memberDeclarations - - if len(memberDeclarations) > 0 { - - // func init() { - // members := []*Member{...} - // t.Members = MembersAsMap(members) - // t.Fields = MembersFieldNames(members)= - // } - - members := membersExpr( - fullTypeName, - tyVarName, - memberDeclarations, - ) - - const membersVariableIdentifier = "members" - - stmts := []dst.Stmt{ - &dst.DeclStmt{ - Decl: goVarDecl( - membersVariableIdentifier, - members, - ), - }, - &dst.AssignStmt{ - Lhs: []dst.Expr{ - &dst.SelectorExpr{ - X: dst.NewIdent(tyVarName), - Sel: dst.NewIdent("Members"), - }, - }, - Tok: token.ASSIGN, - Rhs: []dst.Expr{ - &dst.CallExpr{ - Fun: &dst.Ident{ - Name: "MembersAsMap", - Path: semaPath, - }, - Args: []dst.Expr{ - dst.NewIdent(membersVariableIdentifier), - }, - }, - }, - }, - &dst.AssignStmt{ - Lhs: []dst.Expr{ - &dst.SelectorExpr{ - X: dst.NewIdent(tyVarName), - Sel: dst.NewIdent("Fields"), - }, - }, - Tok: token.ASSIGN, - Rhs: []dst.Expr{ - &dst.CallExpr{ - Fun: &dst.Ident{ - Name: "MembersFieldNames", - Path: semaPath, - }, - Args: []dst.Expr{ - dst.NewIdent(membersVariableIdentifier), - }, - }, - }, - }, - } - - g.addDecls( - &dst.FuncDecl{ - Name: dst.NewIdent("init"), - Type: &dst.FuncType{}, - Body: &dst.BlockStmt{ - List: stmts, - }, - }, - ) - } +func (g *generator) VisitCompositeDeclaration(decl *ast.CompositeDeclaration) (_ struct{}) { + return g.VisitCompositeOrInterfaceDeclaration(decl) +} - return +func (g *generator) VisitInterfaceDeclaration(decl *ast.InterfaceDeclaration) (_ struct{}) { + return g.VisitCompositeOrInterfaceDeclaration(decl) } func (*generator) VisitAttachmentDeclaration(_ *ast.AttachmentDeclaration) struct{} { @@ -1740,7 +1626,7 @@ func simpleTypeLiteral(ty *typeDecl) dst.Expr { // Exportable: false, // Importable: false, // comformances: []*InterfaceType { - // StructStringer, + // StructStringerType, // } //} @@ -1763,8 +1649,15 @@ func simpleTypeLiteral(ty *typeDecl) dst.Expr { 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: conformance, + Name: name, Path: semaPath, }) } @@ -2142,10 +2035,10 @@ func stringMemberResolverMapType() *dst.MapType { } } -func compositeTypeExpr(ty *typeDecl) dst.Expr { +func compositeOrInterfaceTypeExpr(ty *typeDecl, isCompositeType bool) dst.Expr { // func() *CompositeType { - // var t = &CompositeType{ + // var t = &CompositeType { // Identifier: FooTypeName, // Kind: common.CompositeKindStructure, // ImportableBuiltin: false, @@ -2156,92 +2049,6 @@ func compositeTypeExpr(ty *typeDecl) dst.Expr { // return t // }() - const typeVarName = "t" - - statements := []dst.Stmt{ - &dst.DeclStmt{ - Decl: goVarDecl( - typeVarName, - compositeTypeLiteral(ty), - ), - }, - } - - for _, nestedType := range ty.nestedTypes { - statements = append( - statements, - &dst.ExprStmt{ - X: &dst.CallExpr{ - Fun: &dst.SelectorExpr{ - X: dst.NewIdent(typeVarName), - Sel: dst.NewIdent("SetNestedType"), - }, - Args: []dst.Expr{ - typeNameVarIdent(nestedType.fullTypeName), - typeVarIdent(nestedType.fullTypeName), - }, - }, - }, - ) - } - - statements = append( - statements, - &dst.ReturnStmt{ - Results: []dst.Expr{ - dst.NewIdent(typeVarName), - }, - }, - ) - - return &dst.CallExpr{ - Fun: &dst.FuncLit{ - Type: &dst.FuncType{ - Func: true, - Results: &dst.FieldList{ - List: []*dst.Field{ - { - Type: &dst.StarExpr{ - X: &dst.Ident{ - Name: "CompositeType", - Path: semaPath, - }, - }, - }, - }, - }, - }, - Body: &dst.BlockStmt{ - List: statements, - }, - }, - } -} - -func compositeTypeLiteral(ty *typeDecl) 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)), - } - - return &dst.UnaryExpr{ - Op: token.AND, - X: &dst.CompositeLit{ - Type: &dst.Ident{ - Name: "CompositeType", - Path: semaPath, - }, - Elts: elements, - }, - } -} - -func interfaceTypeExpr(ty *typeDecl) dst.Expr { - // func() *InterfaceType { // var t = &InterfaceType{ // Identifier: FooTypeName, @@ -2258,7 +2065,7 @@ func interfaceTypeExpr(ty *typeDecl) dst.Expr { &dst.DeclStmt{ Decl: goVarDecl( typeVarName, - interfaceTypeLiteral(ty), + compositeOrInterfaceTypeLiteral(ty, isCompositeType), ), }, } @@ -2290,6 +2097,11 @@ func interfaceTypeExpr(ty *typeDecl) dst.Expr { }, ) + name := "InterfaceType" + if isCompositeType { + name = "CompositeType" + } + return &dst.CallExpr{ Fun: &dst.FuncLit{ Type: &dst.FuncType{ @@ -2299,7 +2111,7 @@ func interfaceTypeExpr(ty *typeDecl) dst.Expr { { Type: &dst.StarExpr{ X: &dst.Ident{ - Name: "InterfaceType", + Name: name, Path: semaPath, }, }, @@ -2314,19 +2126,30 @@ func interfaceTypeExpr(ty *typeDecl) dst.Expr { } } -func interfaceTypeLiteral(ty *typeDecl) dst.Expr { +func compositeOrInterfaceTypeLiteral(ty *typeDecl, isCompositeType bool) dst.Expr { kind := compositeKindExpr(ty.compositeKind) elements := []dst.Expr{ goKeyValue("Identifier", typeNameVarIdent(ty.fullTypeName)), - goKeyValue("CompositeKind", kind), + } + + name := "InterfaceType" + if isCompositeType { + name = "CompositeType" + elements = append(elements, + goKeyValue("Kind", kind), + goKeyValue("ImportableBuiltin", goBoolLit(ty.importable)), + goKeyValue("HasComputedMembers", goBoolLit(true))) + } else { + elements = append(elements, + goKeyValue("CompositeKind", kind)) } return &dst.UnaryExpr{ Op: token.AND, X: &dst.CompositeLit{ Type: &dst.Ident{ - Name: "InterfaceType", + Name: name, Path: semaPath, }, Elts: elements, diff --git a/runtime/sema/gen/testdata/simple_interface/test.cdc b/runtime/sema/gen/testdata/simple_interface/test.cdc new file mode 100644 index 0000000000..388ef59051 --- /dev/null +++ b/runtime/sema/gen/testdata/simple_interface/test.cdc @@ -0,0 +1 @@ +access(all) struct interface Test {} diff --git a/runtime/sema/gen/testdata/simple_interface/test.golden.go b/runtime/sema/gen/testdata/simple_interface/test.golden.go new file mode 100644 index 0000000000..ae15975a61 --- /dev/null +++ b/runtime/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/runtime/common" + "github.com/onflow/cadence/runtime/sema" +) + +const TestTypeName = "Test" + +var TestType = func() *sema.InterfaceType { + var t = &sema.InterfaceType{ + Identifier: TestTypeName, + CompositeKind: common.CompositeKindStructure, + } + + return t +}() diff --git a/runtime/sema/type.go b/runtime/sema/type.go index b22f23f9d0..0955934c6e 100644 --- a/runtime/sema/type.go +++ b/runtime/sema/type.go @@ -312,7 +312,7 @@ func TypeActivationNestedType(typeActivation *VariableActivation, qualifiedIdent return ty } -// allow all types to specify interface conformances +// ConformingType is a type that can conform to interfaces type ConformingType interface { Type EffectiveInterfaceConformanceSet() *InterfaceSet @@ -1196,11 +1196,6 @@ type NumericType struct { memberResolversOnce sync.Once saturatingArithmetic SaturatingArithmeticSupport isSuperType bool - - // allow numeric types to conform to interfaces - conformances []*InterfaceType - effectiveInterfaceConformanceSet *InterfaceSet - effectiveInterfaceConformanceSetOnce sync.Once } var _ Type = &NumericType{} @@ -1210,9 +1205,6 @@ var _ SaturatingArithmeticType = &NumericType{} func NewNumericType(typeName string) *NumericType { return &NumericType{ name: typeName, - conformances: []*InterfaceType{ - StructStringerType, - }, } } @@ -1391,19 +1383,15 @@ func (*NumericType) CheckInstantiated(_ ast.HasPosition, _ common.MemoryGauge, _ // NO-OP } -func (t *NumericType) EffectiveInterfaceConformanceSet() *InterfaceSet { - t.initializeEffectiveInterfaceConformanceSet() - return t.effectiveInterfaceConformanceSet -} +var numericTypeEffectiveInterfaceConformanceSet *InterfaceSet -func (t *NumericType) initializeEffectiveInterfaceConformanceSet() { - t.effectiveInterfaceConformanceSetOnce.Do(func() { - t.effectiveInterfaceConformanceSet = NewInterfaceSet() +func init() { + numericTypeEffectiveInterfaceConformanceSet = NewInterfaceSet() + numericTypeEffectiveInterfaceConformanceSet.Add(StructStringerType) +} - for _, conformance := range t.conformances { - t.effectiveInterfaceConformanceSet.Add(conformance) - } - }) +func (t *NumericType) EffectiveInterfaceConformanceSet() *InterfaceSet { + return numericTypeEffectiveInterfaceConformanceSet } // FixedPointNumericType represents all the types in the fixed-point range. @@ -7276,18 +7264,11 @@ const AddressTypeName = "Address" // AddressType represents the address type type AddressType struct { - memberResolvers map[string]MemberResolver - memberResolversOnce sync.Once - conformances []*InterfaceType - effectiveInterfaceConformanceSet *InterfaceSet - effectiveInterfaceConformanceSetOnce sync.Once + memberResolvers map[string]MemberResolver + memberResolversOnce sync.Once } -var TheAddressType = &AddressType{ - conformances: []*InterfaceType{ - StructStringerType, - }, -} +var TheAddressType = &AddressType{} var AddressTypeAnnotation = NewTypeAnnotation(TheAddressType) var _ Type = &AddressType{} @@ -7432,19 +7413,14 @@ func (t *AddressType) initializeMemberResolvers() { }) } -func (t *AddressType) EffectiveInterfaceConformanceSet() *InterfaceSet { - t.initializeEffectiveInterfaceConformanceSet() - return t.effectiveInterfaceConformanceSet -} - -func (t *AddressType) initializeEffectiveInterfaceConformanceSet() { - t.effectiveInterfaceConformanceSetOnce.Do(func() { - t.effectiveInterfaceConformanceSet = NewInterfaceSet() +var addressTypeEffectiveInterfaceConformanceSet *InterfaceSet - for _, conformance := range t.conformances { - t.effectiveInterfaceConformanceSet.Add(conformance) - } - }) +func init() { + addressTypeEffectiveInterfaceConformanceSet = NewInterfaceSet() + addressTypeEffectiveInterfaceConformanceSet.Add(StructStringerType) +} +func (t *AddressType) AddressInterfaceConformanceSet() *InterfaceSet { + return numericTypeEffectiveInterfaceConformanceSet } func IsPrimitiveOrContainerOfPrimitive(referencedType Type) bool { @@ -9572,12 +9548,7 @@ func extractNativeTypes( } nestedTypes.Foreach(func(_ string, nestedType Type) { - nestedCompositeType, ok := nestedType.(*CompositeType) - if !ok { - return - } - - types = append(types, nestedCompositeType) + types = append(types, nestedType) }) case *InterfaceType: NativeInterfaceTypes[actualType.QualifiedIdentifier()] = actualType @@ -9588,13 +9559,10 @@ func extractNativeTypes( } nestedTypes.Foreach(func(_ string, nestedType Type) { - nestedInterfaceType, ok := nestedType.(*InterfaceType) - if !ok { - return - } - - types = append(types, nestedInterfaceType) + types = append(types, nestedType) }) + default: + panic("Expected only composite or interface type") } } From 698328897d5e97ae72d98825bab583e9f329b290 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Wed, 16 Oct 2024 15:22:20 -0400 Subject: [PATCH 32/38] Code cleanup. --- runtime/ast/expression.go | 14 +++++++++++--- runtime/ast/string_template_test.go | 2 +- runtime/sema/check_string_template_expression.go | 6 +----- runtime/tests/checker/string_test.go | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/runtime/ast/expression.go b/runtime/ast/expression.go index 8cf54fdce5..7f6fe4b502 100644 --- a/runtime/ast/expression.go +++ b/runtime/ast/expression.go @@ -223,6 +223,10 @@ func (*StringExpression) precedence() precedence { // 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 @@ -237,6 +241,10 @@ func NewStringTemplateExpression( 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, @@ -255,8 +263,8 @@ func (*StringTemplateExpression) isExpression() {} func (*StringTemplateExpression) isIfStatementTest() {} -func (*StringTemplateExpression) Walk(_ func(Element)) { - // NO-OP +func (e *StringTemplateExpression) Walk(walkChild func(Element)) { + walkExpressions(walkChild, e.Expressions) } func (e *StringTemplateExpression) String() string { @@ -265,7 +273,7 @@ func (e *StringTemplateExpression) String() string { func (e *StringTemplateExpression) Doc() prettier.Doc { if len(e.Expressions) == 0 { - return prettier.Text(e.Values[0]) + return prettier.Text(QuoteString(e.Values[0])) } // TODO: must reproduce expressions as literals diff --git a/runtime/ast/string_template_test.go b/runtime/ast/string_template_test.go index a19a399bb9..c6167aaffb 100644 --- a/runtime/ast/string_template_test.go +++ b/runtime/ast/string_template_test.go @@ -41,7 +41,7 @@ func TestStringTemplate_Doc(t *testing.T) { } assert.Equal(t, - prettier.Text("abc"), + prettier.Text("\"abc\""), stmt.Doc(), ) } diff --git a/runtime/sema/check_string_template_expression.go b/runtime/sema/check_string_template_expression.go index 8b4be45b48..6cbd8d80ec 100644 --- a/runtime/sema/check_string_template_expression.go +++ b/runtime/sema/check_string_template_expression.go @@ -43,13 +43,9 @@ func (checker *Checker) VisitStringTemplateExpression(stringTemplateExpression * elementCount := len(stringTemplateExpression.Expressions) if elementCount > 0 { - argumentTypes := make([]Type, elementCount) - - for i, element := range stringTemplateExpression.Expressions { + for _, element := range stringTemplateExpression.Expressions { valueType := checker.VisitExpression(element, stringTemplateExpression, elementType) - argumentTypes[i] = valueType - if !isValidStringTemplateValue(valueType) { checker.report( &TypeMismatchWithDescriptionError{ diff --git a/runtime/tests/checker/string_test.go b/runtime/tests/checker/string_test.go index 4bea07a7dd..84e305d3b3 100644 --- a/runtime/tests/checker/string_test.go +++ b/runtime/tests/checker/string_test.go @@ -746,7 +746,7 @@ func TestCheckStringTemplate(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` - let x :[AnyStruct] = ["tmp", 1] + let x: [AnyStruct] = ["tmp", 1] let y = "\(x)" `) From b9243e369e7d2e25f0545ef8fd71b15e1ed34197 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Mon, 21 Oct 2024 10:13:46 -0400 Subject: [PATCH 33/38] Fix imports. --- sema/gen/testdata/simple_interface/test.golden.go | 4 ++-- sema/struct_stringer.gen.go | 4 ++-- tests/checker/stringer_test.go | 2 +- tests/interpreter/stringer_test.go | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sema/gen/testdata/simple_interface/test.golden.go b/sema/gen/testdata/simple_interface/test.golden.go index ae15975a61..c976eafded 100644 --- a/sema/gen/testdata/simple_interface/test.golden.go +++ b/sema/gen/testdata/simple_interface/test.golden.go @@ -20,8 +20,8 @@ package simple_interface import ( - "github.com/onflow/cadence/runtime/common" - "github.com/onflow/cadence/runtime/sema" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/sema" ) const TestTypeName = "Test" diff --git a/sema/struct_stringer.gen.go b/sema/struct_stringer.gen.go index fd583822cd..7f23bd22df 100644 --- a/sema/struct_stringer.gen.go +++ b/sema/struct_stringer.gen.go @@ -20,8 +20,8 @@ package sema import ( - "github.com/onflow/cadence/runtime/ast" - "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/ast" + "github.com/onflow/cadence/common" ) const StructStringerTypeToStringFunctionName = "toString" diff --git a/tests/checker/stringer_test.go b/tests/checker/stringer_test.go index c0026904ab..315cb1d4da 100644 --- a/tests/checker/stringer_test.go +++ b/tests/checker/stringer_test.go @@ -23,7 +23,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/onflow/cadence/runtime/sema" + "github.com/onflow/cadence/sema" ) func TestCheckStringer(t *testing.T) { diff --git a/tests/interpreter/stringer_test.go b/tests/interpreter/stringer_test.go index eafe44652f..d01acd5b43 100644 --- a/tests/interpreter/stringer_test.go +++ b/tests/interpreter/stringer_test.go @@ -23,8 +23,8 @@ import ( "github.com/stretchr/testify/require" - "github.com/onflow/cadence/runtime/interpreter" - . "github.com/onflow/cadence/runtime/tests/utils" + "github.com/onflow/cadence/interpreter" + . "github.com/onflow/cadence/tests/utils" ) func TestStringerBasic(t *testing.T) { From f30b70cb372fde5684ede6bfef2392f2dbb907eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 24 Oct 2024 18:15:17 -0700 Subject: [PATCH 34/38] Update tests/checker/stringer_test.go --- tests/checker/stringer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/checker/stringer_test.go b/tests/checker/stringer_test.go index 315cb1d4da..0939eb76fc 100644 --- a/tests/checker/stringer_test.go +++ b/tests/checker/stringer_test.go @@ -1,7 +1,7 @@ /* * Cadence - The resource-oriented smart contract programming language * - * Copyright Dapper Labs, Inc. + * 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. From bd4c77de6645f58cf0ee3a7ce96311c17cce15b0 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Mon, 28 Oct 2024 10:01:33 -0400 Subject: [PATCH 35/38] Invert flag for readability. --- sema/gen/main.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/sema/gen/main.go b/sema/gen/main.go index bf97d23819..c203adf180 100644 --- a/sema/gen/main.go +++ b/sema/gen/main.go @@ -438,7 +438,7 @@ func (g *generator) VisitCompositeOrInterfaceDeclaration(decl ast.ConformingDecl var typeDec *typeDecl var members []ast.Declaration var conformances []*ast.NominalType - var isCompositeType bool + var isInterfaceType bool switch actualDecl := decl.(type) { case *ast.CompositeDeclaration: @@ -451,7 +451,7 @@ func (g *generator) VisitCompositeOrInterfaceDeclaration(decl ast.ConformingDecl } members = actualDecl.Members.Declarations() conformances = actualDecl.Conformances - isCompositeType = true + isInterfaceType = false case *ast.InterfaceDeclaration: compositeKind = actualDecl.Kind() typeName = actualDecl.Identifier.Identifier @@ -461,7 +461,7 @@ func (g *generator) VisitCompositeOrInterfaceDeclaration(decl ast.ConformingDecl compositeKind: compositeKind, } members = actualDecl.Members.Declarations() - isCompositeType = false + isInterfaceType = true default: panic("Expected composite or interface declaration") } @@ -499,7 +499,7 @@ func (g *generator) VisitCompositeOrInterfaceDeclaration(decl ast.ConformingDecl // Check if the declaration is explicitly marked to be generated as a composite type. if _, ok := g.leadingPragma["compositeType"]; ok { generateSimpleType = false - } else if !isCompositeType { + } else if isInterfaceType { generateSimpleType = false } else { // If not, decide what to generate depending on the type. @@ -2035,7 +2035,7 @@ func stringMemberResolverMapType() *dst.MapType { } } -func compositeOrInterfaceTypeExpr(ty *typeDecl, isCompositeType bool) dst.Expr { +func compositeOrInterfaceTypeExpr(ty *typeDecl, isInterfaceType bool) dst.Expr { // func() *CompositeType { // var t = &CompositeType { @@ -2097,9 +2097,9 @@ func compositeOrInterfaceTypeExpr(ty *typeDecl, isCompositeType bool) dst.Expr { }, ) - name := "InterfaceType" - if isCompositeType { - name = "CompositeType" + name := "CompositeType" + if isInterfaceType { + name = "InterfaceType" } return &dst.CallExpr{ @@ -2126,7 +2126,7 @@ func compositeOrInterfaceTypeExpr(ty *typeDecl, isCompositeType bool) dst.Expr { } } -func compositeOrInterfaceTypeLiteral(ty *typeDecl, isCompositeType bool) dst.Expr { +func compositeOrInterfaceTypeLiteral(ty *typeDecl, isInterfaceType bool) dst.Expr { kind := compositeKindExpr(ty.compositeKind) elements := []dst.Expr{ @@ -2134,15 +2134,15 @@ func compositeOrInterfaceTypeLiteral(ty *typeDecl, isCompositeType bool) dst.Exp } name := "InterfaceType" - if isCompositeType { + 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))) - } else { - elements = append(elements, - goKeyValue("CompositeKind", kind)) } return &dst.UnaryExpr{ From 58d7d66dbbb8b1430273e4c5c41dedec450dc6e2 Mon Sep 17 00:00:00 2001 From: Raymond Zhang Date: Mon, 28 Oct 2024 10:06:48 -0400 Subject: [PATCH 36/38] Small fix. --- sema/gen/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sema/gen/main.go b/sema/gen/main.go index c203adf180..49fdaf24b6 100644 --- a/sema/gen/main.go +++ b/sema/gen/main.go @@ -609,7 +609,7 @@ func (g *generator) VisitCompositeOrInterfaceDeclaration(decl ast.ConformingDecl if generateSimpleType { typeVarDecl = simpleTypeLiteral(typeDec) } else { - typeVarDecl = compositeOrInterfaceTypeExpr(typeDec, isCompositeType) + typeVarDecl = compositeOrInterfaceTypeExpr(typeDec, isInterfaceType) } fullTypeName := typeDec.fullTypeName @@ -2065,7 +2065,7 @@ func compositeOrInterfaceTypeExpr(ty *typeDecl, isInterfaceType bool) dst.Expr { &dst.DeclStmt{ Decl: goVarDecl( typeVarName, - compositeOrInterfaceTypeLiteral(ty, isCompositeType), + compositeOrInterfaceTypeLiteral(ty, isInterfaceType), ), }, } From f642b8e24632eea3ac6cc94815cc1c4ce4122221 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Wed, 30 Oct 2024 12:03:54 -0700 Subject: [PATCH 37/38] Fix tests --- parser/expression_test.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/parser/expression_test.go b/parser/expression_test.go index 92006a1914..172884de18 100644 --- a/parser/expression_test.go +++ b/parser/expression_test.go @@ -6082,7 +6082,7 @@ func TestParseStringTemplate(t *testing.T) { }, } - utils.AssertEqualWithDiff(t, expected, actual) + AssertEqualWithDiff(t, expected, actual) }) t.Run("multi", func(t *testing.T) { @@ -6128,7 +6128,7 @@ func TestParseStringTemplate(t *testing.T) { }, } - utils.AssertEqualWithDiff(t, expected, actual) + AssertEqualWithDiff(t, expected, actual) }) t.Run("missing end", func(t *testing.T) { @@ -6147,7 +6147,7 @@ func TestParseStringTemplate(t *testing.T) { } require.Error(t, err) - utils.AssertEqualWithDiff(t, + AssertEqualWithDiff(t, []error{ &SyntaxError{ Message: "invalid end of string literal: missing '\"'", @@ -6174,7 +6174,7 @@ func TestParseStringTemplate(t *testing.T) { } require.Error(t, err) - utils.AssertEqualWithDiff(t, + AssertEqualWithDiff(t, []error{ &SyntaxError{ Message: "unexpected token in expression: '.'", @@ -6201,7 +6201,7 @@ func TestParseStringTemplate(t *testing.T) { } require.Error(t, err) - utils.AssertEqualWithDiff(t, + AssertEqualWithDiff(t, []error{ &SyntaxError{ Message: "expected identifier got: 2 + 2", @@ -6248,7 +6248,7 @@ func TestParseStringTemplate(t *testing.T) { }, } - utils.AssertEqualWithDiff(t, expected, actual) + AssertEqualWithDiff(t, expected, actual) }) t.Run("invalid, empty", func(t *testing.T) { @@ -6267,7 +6267,7 @@ func TestParseStringTemplate(t *testing.T) { } require.Error(t, err) - utils.AssertEqualWithDiff(t, + AssertEqualWithDiff(t, []error{ &SyntaxError{ Message: "unexpected token in expression: ')'", @@ -6294,7 +6294,7 @@ func TestParseStringTemplate(t *testing.T) { } require.Error(t, err) - utils.AssertEqualWithDiff(t, + AssertEqualWithDiff(t, []error{ &SyntaxError{ Message: "expected identifier got: add()", @@ -6321,7 +6321,7 @@ func TestParseStringTemplate(t *testing.T) { } require.Error(t, err) - utils.AssertEqualWithDiff(t, + AssertEqualWithDiff(t, []error{ &SyntaxError{ Message: "expected token ')'", @@ -6348,7 +6348,7 @@ func TestParseStringTemplate(t *testing.T) { } require.Error(t, err) - utils.AssertEqualWithDiff(t, + AssertEqualWithDiff(t, []error{ &SyntaxError{ Message: "expected token ')'", @@ -6395,7 +6395,7 @@ func TestParseStringTemplate(t *testing.T) { }, } - utils.AssertEqualWithDiff(t, expected, actual) + AssertEqualWithDiff(t, expected, actual) }) t.Run("valid, surrounded", func(t *testing.T) { @@ -6441,7 +6441,7 @@ func TestParseStringTemplate(t *testing.T) { }, } - utils.AssertEqualWithDiff(t, expected, actual) + AssertEqualWithDiff(t, expected, actual) }) t.Run("valid, adjacent", func(t *testing.T) { @@ -6494,7 +6494,7 @@ func TestParseStringTemplate(t *testing.T) { }, } - utils.AssertEqualWithDiff(t, expected, actual) + AssertEqualWithDiff(t, expected, actual) }) } From 247a13d3eabdbe98af126ef762fbf527caeca4a7 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Wed, 30 Oct 2024 12:45:58 -0700 Subject: [PATCH 38/38] Refactor and fix struct string tests --- .../stringer_test.go => interpreter/struct_stringer_test.go | 2 +- tests/checker/stringer_test.go => sema/struct_stringer_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) rename tests/interpreter/stringer_test.go => interpreter/struct_stringer_test.go (97%) rename tests/checker/stringer_test.go => sema/struct_stringer_test.go (96%) diff --git a/tests/interpreter/stringer_test.go b/interpreter/struct_stringer_test.go similarity index 97% rename from tests/interpreter/stringer_test.go rename to interpreter/struct_stringer_test.go index d01acd5b43..d81ea4e7a4 100644 --- a/tests/interpreter/stringer_test.go +++ b/interpreter/struct_stringer_test.go @@ -24,7 +24,7 @@ import ( "github.com/stretchr/testify/require" "github.com/onflow/cadence/interpreter" - . "github.com/onflow/cadence/tests/utils" + . "github.com/onflow/cadence/test_utils/interpreter_utils" ) func TestStringerBasic(t *testing.T) { diff --git a/tests/checker/stringer_test.go b/sema/struct_stringer_test.go similarity index 96% rename from tests/checker/stringer_test.go rename to sema/struct_stringer_test.go index 0939eb76fc..119f25471d 100644 --- a/tests/checker/stringer_test.go +++ b/sema/struct_stringer_test.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package checker +package sema_test import ( "testing" @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/onflow/cadence/sema" + . "github.com/onflow/cadence/test_utils/sema_utils" ) func TestCheckStringer(t *testing.T) {