diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index f6584a22f5bc..90dfb72ef010 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -11,7 +11,7 @@ import ast.Trees.* import typer.Implicits.* import typer.ImportInfo import Variances.varianceSign -import util.SourcePosition +import util.{Chars, SourcePosition} import scala.util.control.NonFatal import scala.annotation.switch import config.{Config, Feature} @@ -704,22 +704,10 @@ class PlainPrinter(_ctx: Context) extends Printer { def toText(denot: Denotation): Text = toText(denot.symbol) ~ "/D" - private def escapedChar(ch: Char): String = (ch: @switch) match { - case '\b' => "\\b" - case '\t' => "\\t" - case '\n' => "\\n" - case '\f' => "\\f" - case '\r' => "\\r" - case '"' => "\\\"" - case '\'' => "\\\'" - case '\\' => "\\\\" - case _ => if ch.isControl then f"${"\\"}u${ch.toInt}%04x" else String.valueOf(ch) - } - def toText(const: Constant): Text = const.tag match { - case StringTag => stringText("\"" + escapedString(const.value.toString) + "\"") + case StringTag => stringText(Chars.escapedString(const.value.toString, quoted = true)) case ClazzTag => "classOf[" ~ toText(const.typeValue) ~ "]" - case CharTag => literalText(s"'${escapedChar(const.charValue)}'") + case CharTag => literalText(Chars.escapedChar(const.charValue)) case LongTag => literalText(const.longValue.toString + "L") case DoubleTag => literalText(const.doubleValue.toString + "d") case FloatTag => literalText(const.floatValue.toString + "f") @@ -741,7 +729,7 @@ class PlainPrinter(_ctx: Context) extends Printer { ~ (if param.isTypeParam then "" else ": ") ~ toText(param.paramInfo) - protected def escapedString(str: String): String = str flatMap escapedChar + protected final def escapedString(str: String): String = Chars.escapedString(str, quoted = false) def dclsText(syms: List[Symbol], sep: String): Text = Text(syms map dclText, sep) diff --git a/compiler/src/dotty/tools/dotc/util/Chars.scala b/compiler/src/dotty/tools/dotc/util/Chars.scala index e68c48903a63..359d6bd12ce8 100644 --- a/compiler/src/dotty/tools/dotc/util/Chars.scala +++ b/compiler/src/dotty/tools/dotc/util/Chars.scala @@ -3,7 +3,8 @@ package dotty.tools.dotc.util import scala.annotation.switch import Character.{LETTER_NUMBER, LOWERCASE_LETTER, OTHER_LETTER, TITLECASE_LETTER, UPPERCASE_LETTER} import Character.{MATH_SYMBOL, OTHER_SYMBOL} -import Character.{isJavaIdentifierPart, isUnicodeIdentifierStart, isUnicodeIdentifierPart} +import Character.{isISOControl as isControl, isJavaIdentifierPart, isUnicodeIdentifierStart, isUnicodeIdentifierPart} +import java.lang.StringBuilder /** Contains constants and classifier methods for characters */ object Chars: @@ -110,3 +111,59 @@ object Chars: /** Would the character be encoded by `NameTransformer.encode`? */ def willBeEncoded(c: Char): Boolean = !isJavaIdentifierPart(c) + + private inline def requiresFormat(c: Char): Boolean = (c: @switch) match + case '\b' | '\t' | '\n' | '\f' | '\r' | '"' | '\'' | '\\' => true + case c => isControl(c) + + def escapedString(text: String, quoted: Boolean): String = + inline def doBuild: String = + val b = StringBuilder(text.length + 16) + if quoted then + b.append('"') + var i = 0 + while i < text.length do + escapedChar(b, text.charAt(i)) + i += 1 + if quoted then + b.append('"') + b.toString + var i = 0 + while i < text.length do + if requiresFormat(text.charAt(i)) then return doBuild + i += 1 + if quoted then "\"" + text + "\"" + else text + + def escapedChar(ch: Char): String = + if requiresFormat(ch) then + val b = StringBuilder().append('\'') + escapedChar(b, ch) + b.append('\'').toString + else + "'" + ch + "'" + + private def escapedChar(b: StringBuilder, c: Char): Unit = + inline def quadNibble(x: Int, i: Int): Unit = + if i < 4 then + quadNibble(x >> 4, i + 1) + val n = x & 0xF + val c = if (n < 10) '0' + n else 'a' + (n - 10) + b.append(c.toChar) + val replace = (c: @switch) match + case '\b' => "\\b" + case '\t' => "\\t" + case '\n' => "\\n" + case '\f' => "\\f" + case '\r' => "\\r" + case '"' => "\\\"" + case '\'' => "\\\'" + case '\\' => "\\\\" + case c => + if isControl(c) then + b.append("\\u") + quadNibble(c.toInt, 0) + else + b.append(c) + return + b.append(replace) diff --git a/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala b/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala index 6e0d65bc044e..35d57f4a05c0 100644 --- a/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala +++ b/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala @@ -1,7 +1,12 @@ package scala.quoted package runtime.impl.printers +import dotty.tools.dotc.util.Chars + import scala.annotation.switch +import scala.collection.mutable + +import java.lang.StringBuilder /** Printer for fully elaborated representation of the source code */ object SourceCode { @@ -97,7 +102,7 @@ object SourceCode { this += lineBreak() += "}" } - def result(): String = sb.result() + def result(): String = sb.toString private def lineBreak(): String = "\n" + (" " * indent) private def doubleLineBreak(): String = "\n\n" + (" " * indent) @@ -438,7 +443,7 @@ object SourceCode { case _ => inParens { printTree(term) - this += (if (dotty.tools.dotc.util.Chars.isOperatorPart(sb.last)) " : " else ": ") + this += (if Chars.isOperatorPart(sb.charAt(sb.length - 1)) then " : " else ": ") def printTypeOrAnnots(tpe: TypeRepr): Unit = tpe match { case AnnotatedType(tp, annot) if tp == term.tpe => printAnnotation(annot) @@ -957,9 +962,6 @@ object SourceCode { } - inline private val qc = '\'' - inline private val qSc = '"' - def printConstant(const: Constant): this.type = const match { case UnitConstant() => this += highlightLiteral("()") case NullConstant() => this += highlightLiteral("null") @@ -970,8 +972,8 @@ object SourceCode { case LongConstant(v) => this += highlightLiteral(v.toString + "L") case FloatConstant(v) => this += highlightLiteral(v.toString + "f") case DoubleConstant(v) => this += highlightLiteral(v.toString) - case CharConstant(v) => this += highlightString(s"${qc}${escapedChar(v)}${qc}") - case StringConstant(v) => this += highlightString(s"${qSc}${escapedString(v)}${qSc}") + case CharConstant(v) => this += highlightString(Chars.escapedChar(v)) + case StringConstant(v) => this += highlightString(Chars.escapedString(v, quoted = true)) case ClassOfConstant(v) => this += "classOf" inSquare(printType(v)) @@ -1445,22 +1447,8 @@ object SourceCode { private def +=(x: Char): this.type = { sb.append(x); this } private def +=(x: String): this.type = { sb.append(x); this } - private def escapedChar(ch: Char): String = (ch: @switch) match { - case '\b' => "\\b" - case '\t' => "\\t" - case '\n' => "\\n" - case '\f' => "\\f" - case '\r' => "\\r" - case '"' => "\\\"" - case '\'' => "\\\'" - case '\\' => "\\\\" - case _ => if ch.isControl then f"${"\\"}u${ch.toInt}%04x" else String.valueOf(ch) - } - - private def escapedString(str: String): String = str flatMap escapedChar - - private val names = collection.mutable.Map.empty[Symbol, String] - private val namesIndex = collection.mutable.Map.empty[String, Int] + private val names = mutable.Map.empty[Symbol, String] + private val namesIndex = mutable.Map.empty[String, Int] private def splicedName(sym: Symbol): Option[String] = { if sym.owner.isClassDef then None diff --git a/compiler/test/dotty/tools/dotc/printing/PrintingTest.scala b/compiler/test/dotty/tools/dotc/printing/PrintingTest.scala index 1a8e55b2fdcb..de7e9ed2cea2 100644 --- a/compiler/test/dotty/tools/dotc/printing/PrintingTest.scala +++ b/compiler/test/dotty/tools/dotc/printing/PrintingTest.scala @@ -1,6 +1,7 @@ package dotty package tools package dotc +package printing import scala.language.unsafeNulls diff --git a/tests/printing/untyped/strings.check b/tests/printing/untyped/strings.check new file mode 100644 index 000000000000..1d797c4654bd --- /dev/null +++ b/tests/printing/untyped/strings.check @@ -0,0 +1,8 @@ +[[syntax trees at end of parser]] // tests/printing/untyped/strings.scala +package { + class C { + def chars = "\b\t\n\f\r\"\'\\a\u0003" + def greeting = "hello, world" + } +} + diff --git a/tests/printing/untyped/strings.scala b/tests/printing/untyped/strings.scala new file mode 100644 index 000000000000..8f7183333df6 --- /dev/null +++ b/tests/printing/untyped/strings.scala @@ -0,0 +1,4 @@ + +class C: + def chars = "\b\t\n\f\r\"\'\\\u0061\u0003" + def greeting = "hello, world"