Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limited string templates: \(identifier) #3585

Merged
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Refactor code from review.
RZhang05 committed Oct 2, 2024
commit bda6b28fb29cff50e635a3e9d94b926290001b33
9 changes: 7 additions & 2 deletions runtime/ast/expression.go
Original file line number Diff line number Diff line change
@@ -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,
1 change: 1 addition & 0 deletions runtime/common/memorykind.go
Original file line number Diff line number Diff line change
@@ -204,6 +204,7 @@ const (
MemoryKindIntegerExpression
MemoryKindFixedPointExpression
MemoryKindArrayExpression
MemoryKindStringTemplateExpression
MemoryKindDictionaryExpression
MemoryKindIdentifierExpression
MemoryKindInvocationExpression
95 changes: 48 additions & 47 deletions runtime/common/memorykind_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions runtime/common/metering.go
Original file line number Diff line number Diff line change
@@ -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,
18 changes: 7 additions & 11 deletions runtime/interpreter/interpreter_expression.go
Original file line number Diff line number Diff line change
@@ -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())
}
}
}
54 changes: 54 additions & 0 deletions runtime/parser/expression_test.go
Original file line number Diff line number Diff line change
@@ -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,
)
})
}
SupunS marked this conversation as resolved.
Show resolved Hide resolved

func TestParseNilCoalescing(t *testing.T) {
10 changes: 5 additions & 5 deletions runtime/parser/lexer/lexer.go
Original file line number Diff line number Diff line change
@@ -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
SupunS marked this conversation as resolved.
Show resolved Hide resolved
)

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
6 changes: 3 additions & 3 deletions runtime/parser/lexer/state.go
Original file line number Diff line number Diff line change
@@ -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 '(':
12 changes: 7 additions & 5 deletions runtime/sema/check_string_template_expression.go
Original file line number Diff line number Diff line change
@@ -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)
SupunS marked this conversation as resolved.
Show resolved Hide resolved
}

func (checker *Checker) VisitStringTemplateExpression(stringTemplateExpression *ast.StringTemplateExpression) Type {

// visit all elements
@@ -37,11 +43,7 @@ func (checker *Checker) VisitStringTemplateExpression(stringTemplateExpression *

argumentTypes[i] = valueType
turbolent marked this conversation as resolved.
Show resolved Hide resolved

// 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,
18 changes: 18 additions & 0 deletions runtime/tests/interpreter/interpreter_test.go
Original file line number Diff line number Diff line change
@@ -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),
)
})
}
SupunS marked this conversation as resolved.
Show resolved Hide resolved