diff --git a/compiler/src/dotty/tools/dotc/parsing/CharArrayReader.scala b/compiler/src/dotty/tools/dotc/parsing/CharArrayReader.scala index a6e57db81db7..5c838728dd44 100644 --- a/compiler/src/dotty/tools/dotc/parsing/CharArrayReader.scala +++ b/compiler/src/dotty/tools/dotc/parsing/CharArrayReader.scala @@ -10,7 +10,7 @@ abstract class CharArrayReader { self => protected def startFrom: Int = 0 /** Switch whether unicode should be decoded */ - protected def decodeUni: Boolean = true + protected def decodeUni: Boolean = false /** An error routine to call on bad unicode escapes \\uxxxx. */ protected def error(msg: String, offset: Int): Unit diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala b/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala index 68007d147085..7816af83474d 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala @@ -14,6 +14,8 @@ object JavaScanners { class JavaScanner(source: SourceFile, override val startFrom: Offset = 0)(implicit ctx: Context) extends ScannerCommon(source)(ctx) { + override def decodeUni: Boolean = true + def toToken(name: SimpleName): Token = { val idx = name.start if (idx >= 0 && idx <= lastKeywordStart) kwArray(idx) else IDENTIFIER diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 34484ba62218..a2abd22c132c 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -1137,6 +1137,26 @@ object Scanners { * and advance to next character. */ protected def getLitChar(): Unit = + def invalidUnicodeEscape() = { + error("invalid character in unicode escape sequence", charOffset - 1) + putChar(ch) + } + def putUnicode(): Unit = { + while ch == 'u' || ch == 'U' do nextChar() + var i = 0 + var cp = 0 + while (i < 4) { + val shift = (3 - i) * 4 + val d = digit2int(ch, 16) + if(d < 0) { + return invalidUnicodeEscape() + } + cp += (d << shift) + nextChar() + i += 1 + } + putChar(cp.asInstanceOf[Char]) + } if (ch == '\\') { nextChar() if ('0' <= ch && ch <= '7') { @@ -1153,6 +1173,9 @@ object Scanners { } putChar(oct.toChar) } + else if (ch == 'u' || ch == 'U') { + putUnicode() + } else { ch match { case 'b' => putChar('\b') diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala index 738b01883d5f..19df8cc00353 100644 --- a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala +++ b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala @@ -63,6 +63,20 @@ class StringInterpolatorOpt extends MiniPhase { } } + //Extract the position from InvalidUnicodeEscapeException + //which due to bincompat reasons is unaccessible. + //TODO: remove once there is less restrictive bincompat + private object InvalidEscapePosition { + def unapply(t: Throwable): Option[Int] = t match { + case iee: StringContext.InvalidEscapeException => Some(iee.index) + case il: IllegalArgumentException => il.getMessage() match { + case s"""invalid unicode escape at index $index of $_""" => index.toIntOption + case _ => None + } + case _ => None + } + } + /** * Match trees that resemble s and raw string interpolations. In the case of the s * interpolator, escapes the string constants. Exposes the string constants as well as @@ -74,14 +88,22 @@ class StringInterpolatorOpt extends MiniPhase { case SOrRawInterpolator(strs, elems) => if (tree.symbol == defn.StringContext_raw) Some(strs, elems) else { // tree.symbol == defn.StringContextS + import dotty.tools.dotc.util.SourcePosition + var stringPosition: SourcePosition = null try { - val escapedStrs = strs.map { str => - val escapedValue = StringContext.processEscapes(str.const.stringValue) - cpy.Literal(str)(Constant(escapedValue)) - } + val escapedStrs = strs.map(str => { + stringPosition = str.sourcePos + val escaped = StringContext.processEscapes(str.const.stringValue) + cpy.Literal(str)(Constant(escaped)) + }) Some(escapedStrs, elems) } catch { - case _: StringContext.InvalidEscapeException => None + case t @ InvalidEscapePosition(p) => { + val errorSpan = stringPosition.span.startPos.shift(p) + val errorPosition = stringPosition.withSpan(errorSpan) + ctx.error(t.getMessage() + "\n", errorPosition) + None + } } } case _ => None diff --git a/compiler/test/dotty/tools/dotc/parsing/StringInterpolationPositionTest.scala b/compiler/test/dotty/tools/dotc/parsing/StringInterpolationPositionTest.scala new file mode 100644 index 000000000000..01fe97ec6815 --- /dev/null +++ b/compiler/test/dotty/tools/dotc/parsing/StringInterpolationPositionTest.scala @@ -0,0 +1,43 @@ +package dotty.tools +package dotc +package parsing + +import ast.untpd._ +import org.junit.Test + +class StringInterpolationPositionTest extends ParserTest { + + val tq = "\"\"\"" + val program = s""" + |class A { + | val expr = 42 + | val s0 = s"string1" + | val s1 = s"string1$${expr}string2" + | val s2 = s"string1$${expr}string2$${expr}string3" + | val s0m = s${tq}string1${tq} + | val s1m = s${tq}string1$${expr}string2${tq} + | val s2m = s${tq}string1$${expr}string2$${expr}string3${tq} + |}""".stripMargin + + @Test + def interpolationLiteralPosition: Unit = { + val t = parseText(program) + t match { + case PackageDef(_, List(TypeDef(_, Template(_, _, _, statements: List[Tree])))) => { + val interpolations = statements.collect{ case ValDef(_, _, InterpolatedString(_, int)) => int } + val lits = interpolations.flatten.flatMap { + case l @ Literal(_) => List(l) + case Thicket(trees) => trees.collect { case l @ Literal(_) => l } + } + for { + lit <- lits + Literal(c) = lit + str <- List(c.value).collect { case str: String => str} + } { + val fromPos = program.substring(lit.span.start, lit.span.end) + assert(fromPos == str, s"$fromPos == $str") + } + } + } + } +} \ No newline at end of file diff --git a/tests/neg/firstError.scala b/tests/neg/firstError.scala index 5395aa00c374..a73a76e80c72 100644 --- a/tests/neg/firstError.scala +++ b/tests/neg/firstError.scala @@ -1,4 +1 @@ -. // error: expected class or object definition - -\u890u3084eu // error: error in unicode escape // error: illegal character '\uffff' - +. // error: expected class or object definition \ No newline at end of file diff --git a/tests/neg/unicodeEscapes-interpolations.check b/tests/neg/unicodeEscapes-interpolations.check new file mode 100644 index 000000000000..39f90f32df15 --- /dev/null +++ b/tests/neg/unicodeEscapes-interpolations.check @@ -0,0 +1,48 @@ +-- Error: tests/neg/unicodeEscapes-interpolations.scala:2:27 ----------------------------------------------------------- +2 | val badInters1 = s"foo \unope that's wrong" // error + | ^ + | invalid unicode escape at index 6 of foo \unope that's wrong +-- Error: tests/neg/unicodeEscapes-interpolations.scala:3:32 ----------------------------------------------------------- +3 | val badIntersEnd1 = s"foo \u12" // error + | ^ + | invalid unicode escape at index 8 of foo \u12 +-- Error: tests/neg/unicodeEscapes-interpolations.scala:4:29 ----------------------------------------------------------- +4 | val badInters3 = s"""foo \unope that's wrong""" // error + | ^ + | invalid unicode escape at index 6 of foo \unope that's wrong +-- Error: tests/neg/unicodeEscapes-interpolations.scala:5:28 ----------------------------------------------------------- +5 | val caretPos1 = s"foo \u12x3 pos @ x" // error + | ^ + | invalid unicode escape at index 8 of foo \u12x3 pos @ x +-- Error: tests/neg/unicodeEscapes-interpolations.scala:6:34 ----------------------------------------------------------- +6 | val caretPos2 = s"foo \uuuuuuu12x3 pos @ x" // error + | ^ + | invalid unicode escape at index 14 of foo \uuuuuuu12x3 pos @ x +-- Error: tests/neg/unicodeEscapes-interpolations.scala:7:30 ----------------------------------------------------------- +7 | val caretPos3 = s"""foo \u12x3 pos @ x""" // error + | ^ + | invalid unicode escape at index 8 of foo \u12x3 pos @ x +-- Error: tests/neg/unicodeEscapes-interpolations.scala:8:36 ----------------------------------------------------------- +8 | val caretPos4 = s"""foo \uuuuuuu12x3 pos @ x""" // error + | ^ + | invalid unicode escape at index 14 of foo \uuuuuuu12x3 pos @ x +-- Error: tests/neg/unicodeEscapes-interpolations.scala:10:53 ---------------------------------------------------------- +10 | val badIntersmultiAfter = s"foo $placeholder bar \unope that's wrong" // error + | ^ + | invalid unicode escape at index 7 of bar \unope that's wrong +-- Error: tests/neg/unicodeEscapes-interpolations.scala:11:37 ---------------------------------------------------------- +11 | val badIntersmultiBefore = s"foo \unope $placeholder that's wrong" // error + | ^ + | invalid unicode escape at index 6 of foo \unope +-- Error: tests/neg/unicodeEscapes-interpolations.scala:12:56 ---------------------------------------------------------- +12 | val badInterstmultiAfter = s"""foo $placeholder bar \unope that's wrong""" // error + | ^ + | invalid unicode escape at index 7 of bar \unope that's wrong +-- Error: tests/neg/unicodeEscapes-interpolations.scala:13:40 ---------------------------------------------------------- +13 | val badInterstmultiBefore = s"""foo \unope $placeholder that's wrong""" // error + | ^ + | invalid unicode escape at index 6 of foo \unope +-- Error: tests/neg/unicodeEscapes-interpolations.scala:14:29 ---------------------------------------------------------- +14 | val badInterother = s"this \p ain't legal either" // error + | ^ + |invalid escape '\p' not one of [\b, \t, \n, \f, \r, \\, \", \', \uxxxx] at index 5 in "this \p ain't legal either". Use \\ for literal \. diff --git a/tests/neg/unicodeEscapes-interpolations.scala b/tests/neg/unicodeEscapes-interpolations.scala new file mode 100644 index 000000000000..605f02c333a3 --- /dev/null +++ b/tests/neg/unicodeEscapes-interpolations.scala @@ -0,0 +1,15 @@ +object Example { + val badInters1 = s"foo \unope that's wrong" // error + val badIntersEnd1 = s"foo \u12" // error + val badInters3 = s"""foo \unope that's wrong""" // error + val caretPos1 = s"foo \u12x3 pos @ x" // error + val caretPos2 = s"foo \uuuuuuu12x3 pos @ x" // error + val caretPos3 = s"""foo \u12x3 pos @ x""" // error + val caretPos4 = s"""foo \uuuuuuu12x3 pos @ x""" // error + val placeholder = "place" + val badIntersmultiAfter = s"foo $placeholder bar \unope that's wrong" // error + val badIntersmultiBefore = s"foo \unope $placeholder that's wrong" // error + val badInterstmultiAfter = s"""foo $placeholder bar \unope that's wrong""" // error + val badInterstmultiBefore = s"""foo \unope $placeholder that's wrong""" // error + val badInterother = s"this \p ain't legal either" // error +} \ No newline at end of file diff --git a/tests/neg/unicodeEscapes.check b/tests/neg/unicodeEscapes.check new file mode 100644 index 000000000000..7bbad2041328 --- /dev/null +++ b/tests/neg/unicodeEscapes.check @@ -0,0 +1,28 @@ +-- Error: tests/neg/unicodeEscapes.scala:3:25 -------------------------------------------------------------------------- +3 | val badsingle = "foo \unope that's wrong" // error + | ^ + | invalid character in unicode escape sequence +-- Error: tests/neg/unicodeEscapes.scala:4:26 -------------------------------------------------------------------------- +4 | val caretPos = "foo \u12x3 pos @ x" // error + | ^ + | invalid character in unicode escape sequence +-- Error: tests/neg/unicodeEscapes.scala:5:33 -------------------------------------------------------------------------- +5 | val caretPos2 = "foo \uuuuuuu12x3 pos @ x" // error + | ^ + | invalid character in unicode escape sequence +-- Error: tests/neg/unicodeEscapes.scala:6:29 -------------------------------------------------------------------------- +6 | val carPosTerm = "foo \u123" // error + | ^ + | invalid character in unicode escape sequence +-- Error: tests/neg/unicodeEscapes.scala:7:30 -------------------------------------------------------------------------- +7 | val halfAnEscape = "foo \u12" // error + | ^ + | invalid character in unicode escape sequence +-- Error: tests/neg/unicodeEscapes.scala:8:30 -------------------------------------------------------------------------- +8 | val halfAnEscapeChar = '\u45' // error + | ^ + | invalid character in unicode escape sequence +-- Error: tests/neg/unicodeEscapes.scala:9:29 -------------------------------------------------------------------------- +9 | val `half An Identifier\u45` = "nope" // error + | ^ + | invalid character in unicode escape sequence diff --git a/tests/neg/unicodeEscapes.scala b/tests/neg/unicodeEscapes.scala new file mode 100644 index 000000000000..6387f8a5b9d1 --- /dev/null +++ b/tests/neg/unicodeEscapes.scala @@ -0,0 +1,10 @@ + +object Example { + val badsingle = "foo \unope that's wrong" // error + val caretPos = "foo \u12x3 pos @ x" // error + val caretPos2 = "foo \uuuuuuu12x3 pos @ x" // error + val carPosTerm = "foo \u123" // error + val halfAnEscape = "foo \u12" // error + val halfAnEscapeChar = '\u45' // error + val `half An Identifier\u45` = "nope" // error +} \ No newline at end of file diff --git a/tests/run/literals.scala b/tests/run/literals.scala index 929eabd6988f..57e1eb7da04f 100644 --- a/tests/run/literals.scala +++ b/tests/run/literals.scala @@ -1,32 +1,38 @@ +// scalac: -deprecation +// //############################################################################ // Literals //############################################################################ -import scala.util.{Failure, Success, Try} +//############################################################################ object Test { - /* I add a couple of Unicode identifier tests here "temporarily" */ - - def \u03b1\u03c1\u03b5\u03c4\u03b7 = "alpha rho epsilon tau eta" - - case class GGG(i: Int) { - def \u03b1\u03b1(that: GGG) = i + that.i + def check_success[A](name: String, closure: => A, expected: A): Unit = { + val res: Option[String] = + try { + val actual: A = closure + if (actual == expected) None //print(" was successful") + else Some(s" failed: expected $expected, found $actual") + } catch { + case exception: Throwable => Some(s" raised exception $exception") + } + for (e <- res) println(s"test $name $e") } - def check_success[A](name: String, closure: => A, expected: A): Unit = - Try(closure) match { - case Success(actual) => assert(actual == expected, s"test $name failed: expected $expected, found $actual") - case Failure(error) => throw new AssertionError(s"test $name raised exception $error") - } - def main(args: Array[String]): Unit = { // char + + //unicode escapes escape in char literals check_success("'\\u0024' == '$'", '\u0024', '$') check_success("'\\u005f' == '_'", '\u005f', '_') + + //unicode escapes escape in interpolations + check_success("\"\\u0024\" == \"$\"", s"\u0024", "$") + check_success("\"\"\"\\u0024\"\"\" == \"$\"", s"""\u0024""", "$") + + //Int#asInstanceOf[Char] gets the char at the codepont check_success("65.asInstanceOf[Char] == 'A'", 65.asInstanceOf[Char], 'A') - check_success("\"\\141\\142\" == \"ab\"", "\141\142", "ab") - check_success("\"\\0x61\\0x62\".trim() == \"x61\\0x62\"", "\0x61\0x62".substring(1), "x61\0x62") // boolean check_success("(65 : Byte) == 'A'", (65: Byte) == 'A', true) // contrib #176 @@ -77,7 +83,6 @@ object Test { check_success("01.23f == 1.23f", 01.23f, 1.23f) check_success("3.14f == 3.14f", 3.14f, 3.14f) check_success("6.022e23f == 6.022e23f", 6.022e23f, 6.022e23f) - check_success("9f == 9.0f", 9f, 9.0f) check_success("09f == 9.0f", 09f, 9.0f) check_success("1.00000017881393421514957253748434595763683319091796875001f == 1.0000001f", 1.00000017881393421514957253748434595763683319091796875001f, @@ -107,11 +112,7 @@ object Test { check_success("1L.asInstanceOf[Double] == 1.0", 1L.asInstanceOf[Double], 1.0) check_success("\"\".length()", "\u001a".length(), 1) - - val ggg = GGG(1) \u03b1\u03b1 GGG(2) - check_success("ggg == 3", ggg, 3) - } } -//############################################################################ +//############################################################################ \ No newline at end of file diff --git a/tests/run/t3220-3.check b/tests/run/t3220-3.check new file mode 100644 index 000000000000..b6bd6d73790c --- /dev/null +++ b/tests/run/t3220-3.check @@ -0,0 +1,9 @@ +processed...OK +unprocessed...OK +after backslashes +List(\, \, u, 0, 0, 4, 0) +List(\, u, 0, 0, 4, 0) +List(\, \, u, 0, 0, 4, 0) +List(\, u, 0, 0, 4, 0) +List(", (, [, ^, ", \, x, 0, 0, -, \, x, 1, F, \, x, 7, F, \, \, ], |, \, \, [, \, \, ', ", b, f, n, r, t, ], |, \, \, u, [, a, -, f, A, -, F, 0, -, 9, ], {, 4, }, ), *, ") +List(b, a, d, \) \ No newline at end of file diff --git a/tests/run/t3220-3.scala b/tests/run/t3220-3.scala new file mode 100644 index 000000000000..6b28b5f5e908 --- /dev/null +++ b/tests/run/t3220-3.scala @@ -0,0 +1,85 @@ +object Literals { + //unicode escapes don't get expanded in comments + def comment = "comment" //\u000A is the bomb + //unicode escapes work in string + def inString = "\u000A" + def inTripleQuoted = """\u000A""" + def inRawInterpolation = raw"\u000A" + def inRawTripleQuoted = raw"""\u000A""" + def inChar = '\u000A' + def `in backtick quoted\u0020identifier` = "bueno" + //unicode escapes preceded by an odd number of backslash characters + //are not processed iff the backslashes themselves are escaped + def after2slashestriple = """\\u0040""" + def after2slashesplain = "\\u0040" + def after2slashesraw = raw"\\u0040" + def after2slashess = s"\\u0040" + def firstFailure = ("\""+"""([^"\x00-\x1F\x7F\\]|\\[\\'"bfnrt]|\\u[a-fA-F0-9]{4})*"""+"\"") + def badString = """bad\""" + def escapedQuotesInInterpolation = s"\u0022_\u0022" + def escapedQuotesInSingleQuotedString = "\u0022" + def escapedQuotesInCharLit = '\u0027' + + + def processed = List( + "literal tab in single quoted string" -> "tab tab", + "tab escape char in single quoted string" -> "tab\ttab", + "tab unicode escape in single quoted string" -> "tab\u0009tab", + "literal tab in triple quoted string" -> """tab tab""", + "literal tab in triple quoted raw interpolator" -> raw"""tab tab""", + "literal tab in single quoted raw interpolator" -> raw"tab tab", + "literal tab in triple quoted s interpolator" -> s"""tab tab""", + "literal tab in single quoted s interpolator" -> s"tab tab", + "tab escape char in triple quoted s interpolator" -> s"""tab\ttab""", + "tab escape char in single quoted s interpolator" -> s"""tab\ttab""", + "tab unicode escape in triple quoted s interpolator" -> s"""tab\u0009tab""", + "tab unicode escape in single quoted s interpolator" -> s"tab\u0009tab" + ) +} + +object Test { + def main(args: Array[String]): Unit = { + val bueono = Literals.`in backtick quoted identifier` + + def printways(ways: List[(String, String)]) = + ways.map(_._1).sorted.mkString(", ") + + def printSegment(l: List[(String, String)]) = + l.groupBy(_._2).toList.foreach{ + case (result, ways) => { + println(s"literals that result in $result:") + ways.foreach{case (x, _) => println(x)} + println() + } + } + + print("processed...") + + for { + case (description, format) <- Literals.processed + } { + assert(format == "tab\ttab", description) + } + println("OK") + + print("unprocessed...") + assert("""t\tt""".toList == List('t', '\\', 't', 't'), "tab escape char in triple quoted string") + assert("""tab\ttab""" == raw"tab\ttab", "tab escape char in raw interpolator") + assert("""tab\ttab""" == raw"""tab\ttab""", "tab escape char in raw triple quoted interpolator") + println("OK") + + println("after backslashes") + println(Literals.after2slashestriple.toList) + println(Literals.after2slashesplain.toList) + println(Literals.after2slashesraw.toList) + println(Literals.after2slashess.toList) + println(Literals.firstFailure.toList) + println(Literals.badString.toList) + + val asList = List('\\', 'u', '0', '0', '0', 'A') + assert(asList == Literals.inTripleQuoted.toList) + assert(asList == Literals.inRawInterpolation.toList) + assert(asList == Literals.inRawTripleQuoted.toList) + + } +} \ No newline at end of file diff --git a/tests/run/t3835.scala b/tests/run/t3835.scala index 766b6ddc2e4d..9be42b142be3 100644 --- a/tests/run/t3835.scala +++ b/tests/run/t3835.scala @@ -2,8 +2,8 @@ object Test extends App { // work around optimizer bug SI-5672 -- generates wrong bytecode for switches in arguments // virtpatmat happily emits a switch for a one-case switch // this is not the focus of this test, hence the temporary workaround - def a = (1, 2, 3) match { case (r, \u03b8, \u03c6) => r + \u03b8 + \u03c6 } + def a = (1, 2, 3) match { case (r, θ, φ) => r + θ + φ } println(a) - def b = (1 match { case \u00e9 => \u00e9 }) + def b = (1 match { case é => é }) println(b) } diff --git a/tests/run/t8015-ffc.scala b/tests/run/t8015-ffc.scala index 449faa5bb0f0..4e64d0d8c9be 100644 --- a/tests/run/t8015-ffc.scala +++ b/tests/run/t8015-ffc.scala @@ -1,6 +1,6 @@ object Test extends App { - val ms = """This is a long multiline string + val ms = s"""This is a long multiline interpolation with \u000d\u000a CRLF embedded.""" assert(ms.linesIterator.size == 3, s"lines.size ${ms.linesIterator.size}") assert(ms contains "\r\n CRLF", "no CRLF") diff --git a/tests/run/unicodeEscapes.check b/tests/run/unicodeEscapes.check new file mode 100644 index 000000000000..79fd28bde344 --- /dev/null +++ b/tests/run/unicodeEscapes.check @@ -0,0 +1,10 @@ +chars...OK +char delim...OK +string...OK +string delim...OK +back-quoted identifier...OK +triple quoted string...OK +single quoted s interpolation...OK +triple quoted s interpolation...OK +single quoted raw interpolation...OK +triple quoted raw interpolation...OK diff --git a/tests/run/unicodeEscapes.scala b/tests/run/unicodeEscapes.scala new file mode 100644 index 000000000000..2c76cfc22b20 --- /dev/null +++ b/tests/run/unicodeEscapes.scala @@ -0,0 +1,25 @@ +object Test { + def test(prelude: String, assertion: => Boolean): Unit = { + print(s"$prelude...") + assert(assertion) + print("OK") + print("\n") + } + def main(args: Array[String]): Unit = { + test("chars", 'a' == '\u0061') + test("char delim", '\'' == '\u0027') + test("string", "abcd" == "\u0061b\u0063d") + test("string delim", "\"" == "\u0022") + val `id\u0061` = 17 + test("back-quoted identifier", ida == 17) + val abcescape = List('\\', 'u', '0', '0', '6', '1', 'b', 'c') + test("""triple quoted string""", """\u0061bc""".toList == abcescape) + val b = 'b' + test("single quoted s interpolation", "abc" == s"\u0061${b}c") + test("triple quoted s interpolation", "abc" == s"""\u0061${b}c""") + //test("f interpolation", "abc" == f"\u0061${b}c") + //raw is *NOT* processed (as it shouldn't be) + test("single quoted raw interpolation", raw"\u0061${b}c".toList == abcescape) + test("triple quoted raw interpolation", raw"""\u0061${b}c""".toList == abcescape) + } +}