Repository: expr-lang/expr (Go)

Issue:
Feature request: allow escaping backticks in string literals
### Description

Allow simple escaping of backticks in string literals using backticks. Example expression:
```
let myConst = `
  This string contains a single backtick: ``
`;
someVar == myConst
```

### Why

- Improve ergonomics
- Very simple to implement
- Backward compatible: it doesn't break anything
- Known pattern for escaping. Used e.g. in CSV (e.g. `"field 1","field "" 2"`), SQL (`'It''s fine'`), YAML (`'It''s fine'`), etc.
- Code generation use case:
   - Currently, every other literal can be used in code generation except for strings. With this approach, one can generate code for an expression and use strings.ReplaceAll(src, "`", "``") to interpolate a string constant. This would make generated code very performant and efficient.
   - Especially useful if: (1) the environment is a fixed struct with very few variables; (2) an expression needs to be generated and will be executed many times in a tight loop; (3) there can be one or many constant string values and adding them to the environment can be impractical or incur in performance costs.

Adding a reference PR with the implementation and tests: https://github.com/expr-lang/expr/pull/820


Discussion:
Lets add it. I know no such things in Go. But we can add in Expr those. 



Patch:
diff --git a/parser/lexer/lexer.go b/parser/lexer/lexer.go
index f417f02b6..a8cf6a5be 100644
--- a/parser/lexer/lexer.go
+++ b/parser/lexer/lexer.go
@@ -242,15 +242,47 @@ func (l *Lexer) scanString(quote rune) (n int) {
 }
 
 func (l *Lexer) scanRawString(quote rune) (n int) {
-	ch := l.next() // read character after back tick
-	for ch != quote {
-		if ch == eof {
+	var escapedQuotes int
+loop:
+	for {
+		ch := l.next()
+		for ch == quote && l.peek() == quote {
+			// skip current and next char which are the quote escape sequence
+			l.next()
+			ch = l.next()
+			escapedQuotes++
+		}
+		switch ch {
+		case quote:
+			break loop
+		case eof:
 			l.error("literal not terminated")
 			return
 		}
-		ch = l.next()
 		n++
 	}
-	l.emitValue(String, l.source.String()[l.start.byte+1:l.end.byte-1])
+	str := l.source.String()[l.start.byte+1 : l.end.byte-1]
+
+	// handle simple case where no quoted backtick was found, then no allocation
+	// is needed for the new string
+	if escapedQuotes == 0 {
+		l.emitValue(String, str)
+		return
+	}
+
+	var b strings.Builder
+	var skipped bool
+	b.Grow(len(str) - escapedQuotes)
+	for _, r := range str {
+		if r == quote {
+			if !skipped {
+				skipped = true
+				continue
+			}
+			skipped = false
+		}
+		b.WriteRune(r)
+	}
+	l.emitValue(String, b.String())
 	return
 }


Test patch:
diff --git a/parser/lexer/lexer_test.go b/parser/lexer/lexer_test.go
index 5171f4255..baa5aabb1 100644
--- a/parser/lexer/lexer_test.go
+++ b/parser/lexer/lexer_test.go
@@ -68,6 +68,22 @@ func TestLex(t *testing.T) {
 				{Kind: EOF},
 			},
 		},
+		{
+			"`escaped backticks` `` `a``b` ```` `a``` ```b` ```a````b``` ```````` ```a````` `````b```",
+			[]Token{
+				{Kind: String, Value: "escaped backticks"},
+				{Kind: String, Value: ""},
+				{Kind: String, Value: "a`b"},
+				{Kind: String, Value: "`"},
+				{Kind: String, Value: "a`"},
+				{Kind: String, Value: "`b"},
+				{Kind: String, Value: "`a``b`"},
+				{Kind: String, Value: "```"},
+				{Kind: String, Value: "`a``"},
+				{Kind: String, Value: "``b`"},
+				{Kind: EOF},
+			},
+		},
 		{
 			"a and orb().val #.",
 			[]Token{
@@ -332,6 +348,26 @@ literal not terminated (1:10)
  | id "hello
  | .........^
 
+id ` + "`" + `hello
+literal not terminated (1:10)
+ | id ` + "`" + `hello
+ | .........^
+
+id ` + "`" + `hello` + "``" + `
+literal not terminated (1:12)
+ | id ` + "`" + `hello` + "``" + `
+ | ...........^
+
+id ` + "```" + `hello
+literal not terminated (1:12)
+ | id ` + "```" + `hello
+ | ...........^
+
+id ` + "`" + `hello` + "``" + ` world
+literal not terminated (1:18)
+ | id ` + "`" + `hello` + "``" + ` world
+ | .................^
+
 früh ♥︎
 unrecognized character: U+2665 '♥' (1:6)
  | früh ♥︎