From d942c7b21f12654084fd196ec812835f3e3a8b20 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Tue, 8 Apr 2025 20:15:10 -0700 Subject: [PATCH 1/4] Improve printing of strings --- .../tools/dotc/printing/PlainPrinter.scala | 76 +++++++++++++++---- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index fce460d5acb3..f40903bd520b 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -17,6 +17,8 @@ import scala.annotation.switch import config.{Config, Feature} import cc.{CapturingType, EventuallyCapturingType, CaptureSet, isBoxed} +import java.lang.StringBuilder + class PlainPrinter(_ctx: Context) extends Printer { /** The context of all public methods in Printer and subclasses. @@ -579,22 +581,18 @@ 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).nn - } + private def escapedChar(ch: Char): String = + if requiresFormat(ch) then + val b = StringBuilder().append('\'') + escapedChar(b, ch) + b.append('\'').toString + else + "'" + ch + "'" def toText(const: Constant): Text = const.tag match { - case StringTag => stringText("\"" + escapedString(const.value.toString) + "\"") + case StringTag => stringText(escapedString(const.value.toString, quoted = true)) case ClazzTag => "classOf[" ~ toText(const.typeValue) ~ "]" - case CharTag => literalText(s"'${escapedChar(const.charValue)}'") + case CharTag => literalText(escapedChar(const.charValue)) case LongTag => literalText(const.longValue.toString + "L") case DoubleTag => literalText(const.doubleValue.toString + "d") case FloatTag => literalText(const.floatValue.toString + "f") @@ -612,7 +610,57 @@ 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 = escapedString(str, quoted = false) + + private def requiresFormat(c: Char): Boolean = (c: @switch) match + case '\b' | '\t' | '\n' | '\f' | '\r' | '"' | '\'' | '\\' => true + case c => c.isControl + + private def escapedString(text: String, quoted: Boolean): String = + def mustBuild: Boolean = + var i = 0 + while i < text.length do + if requiresFormat(text.charAt(i)) then return true + i += 1 + false + if mustBuild then + 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 + else if quoted then "\"" + text + "\"" + else text + + private def escapedChar(b: StringBuilder, c: Char): Unit = + def quadNibble(b: StringBuilder, x: Int, i: Int): Unit = + if i < 4 then + quadNibble(b, 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 c.isControl then + b.append("\\u") + quadNibble(b, c.toInt, 0) + else + b.append(c) + return + b.append(replace) def dclsText(syms: List[Symbol], sep: String): Text = Text(syms map dclText, sep) From 877794a71b994e433fe7c4b07e9112cb5280b16a Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Wed, 9 Apr 2025 03:29:50 -0700 Subject: [PATCH 2/4] Improve SourceCode string printing --- .../runtime/impl/printers/SourceCode.scala | 84 ++++++++++++++----- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala b/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala index 017ee7eecb7e..46bae90e9bd6 100644 --- a/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala +++ b/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala @@ -1,8 +1,12 @@ package scala.quoted package runtime.impl.printers +import dotty.tools.dotc.util.Chars + import scala.annotation.switch +import java.lang.StringBuilder + /** Printer for fully elaborated representation of the source code */ object SourceCode { @@ -97,7 +101,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 +442,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,8 +961,8 @@ object SourceCode { } - inline private val qc = '\'' - inline private val qSc = '"' + inline private val qc = "\'" + inline private val qSc = "\"" def printConstant(const: Constant): this.type = const match { case UnitConstant() => this += highlightLiteral("()") @@ -970,8 +974,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(escapedChar(v)) + case StringConstant(v) => this += highlightString(escapedString(v)) case ClassOfConstant(v) => this += "classOf" inSquare(printType(v)) @@ -1444,19 +1448,61 @@ 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).nn - } - - private def escapedString(str: String): String = str flatMap escapedChar + private def escapedChar(ch: Char): String = + if requiresFormat(ch) then + val b = StringBuilder().append(qc) + escapedChar(b, ch) + b.append(qc).toString + else + qc + ch + qc + + private def escapedChar(b: StringBuilder, c: Char): Unit = + def quadNibble(b: StringBuilder, x: Int, i: Int): Unit = + if i < 4 then + quadNibble(b, 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 c.isControl then + b.append("\\u") + quadNibble(b, c.toInt, 0) + else + b.append(c) + return + b.append(replace) + + private def requiresFormat(c: Char): Boolean = (c: @switch) match + case '\b' | '\t' | '\n' | '\f' | '\r' | '"' | '\'' | '\\' => true + case c => c.isControl + + private def escapedString(text: String): String = + def mustBuild: Boolean = + var i = 0 + while i < text.length do + if requiresFormat(text.charAt(i)) then return true + i += 1 + false + if mustBuild then + val b = StringBuilder(text.length + 16) + b.append(qSc) + var i = 0 + while i < text.length do + escapedChar(b, text.charAt(i)) + i += 1 + b.append(qSc) + b.toString + else + qSc + text + qSc private[this] val names = collection.mutable.Map.empty[Symbol, String] private[this] val namesIndex = collection.mutable.Map.empty[String, Int] From 4a738ec5be90eea12b5d4b60906722d8dbd9df1d Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Tue, 2 Sep 2025 14:08:28 -0700 Subject: [PATCH 3/4] Improve test of string printing --- .../scala/quoted/runtime/impl/printers/SourceCode.scala | 5 +++-- .../test/dotty/tools/dotc/printing/PrintingTest.scala | 1 + tests/printing/untyped/strings.check | 8 ++++++++ tests/printing/untyped/strings.scala | 4 ++++ 4 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 tests/printing/untyped/strings.check create mode 100644 tests/printing/untyped/strings.scala diff --git a/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala b/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala index 46bae90e9bd6..aba620828661 100644 --- a/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala +++ b/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala @@ -4,6 +4,7 @@ package runtime.impl.printers import dotty.tools.dotc.util.Chars import scala.annotation.switch +import scala.collection.mutable import java.lang.StringBuilder @@ -1504,8 +1505,8 @@ object SourceCode { else qSc + text + qSc - private[this] val names = collection.mutable.Map.empty[Symbol, String] - private[this] val namesIndex = collection.mutable.Map.empty[String, Int] + private val names = collection.mutable.Map.empty[Symbol, String] + private val namesIndex = collection.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 0d235bace899..b237af6a131d 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" From b3d6db8ee8d513f37adf4ef3426d02aee40a3506 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Wed, 15 Oct 2025 11:39:16 -0700 Subject: [PATCH 4/4] Relocate to Chars and share --- .../tools/dotc/printing/PlainPrinter.scala | 68 ++----------------- .../src/dotty/tools/dotc/util/Chars.scala | 59 +++++++++++++++- .../runtime/impl/printers/SourceCode.scala | 63 +---------------- 3 files changed, 64 insertions(+), 126 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index f40903bd520b..5efc822231c3 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -11,14 +11,12 @@ 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} import cc.{CapturingType, EventuallyCapturingType, CaptureSet, isBoxed} -import java.lang.StringBuilder - class PlainPrinter(_ctx: Context) extends Printer { /** The context of all public methods in Printer and subclasses. @@ -581,18 +579,10 @@ class PlainPrinter(_ctx: Context) extends Printer { def toText(denot: Denotation): Text = toText(denot.symbol) ~ "/D" - private def escapedChar(ch: Char): String = - if requiresFormat(ch) then - val b = StringBuilder().append('\'') - escapedChar(b, ch) - b.append('\'').toString - else - "'" + ch + "'" - def toText(const: Constant): Text = const.tag match { - case StringTag => stringText(escapedString(const.value.toString, quoted = true)) + case StringTag => stringText(Chars.escapedString(const.value.toString, quoted = true)) case ClazzTag => "classOf[" ~ toText(const.typeValue) ~ "]" - case CharTag => literalText(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") @@ -610,57 +600,7 @@ class PlainPrinter(_ctx: Context) extends Printer { ~ (if param.isTypeParam then "" else ": ") ~ toText(param.paramInfo) - protected final def escapedString(str: String): String = escapedString(str, quoted = false) - - private def requiresFormat(c: Char): Boolean = (c: @switch) match - case '\b' | '\t' | '\n' | '\f' | '\r' | '"' | '\'' | '\\' => true - case c => c.isControl - - private def escapedString(text: String, quoted: Boolean): String = - def mustBuild: Boolean = - var i = 0 - while i < text.length do - if requiresFormat(text.charAt(i)) then return true - i += 1 - false - if mustBuild then - 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 - else if quoted then "\"" + text + "\"" - else text - - private def escapedChar(b: StringBuilder, c: Char): Unit = - def quadNibble(b: StringBuilder, x: Int, i: Int): Unit = - if i < 4 then - quadNibble(b, 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 c.isControl then - b.append("\\u") - quadNibble(b, c.toInt, 0) - else - b.append(c) - return - b.append(replace) + 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 4bdf6e464cd1..7595e4e29715 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 aba620828661..dbcf8a3a9bdf 100644 --- a/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala +++ b/compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala @@ -962,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") @@ -975,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(escapedChar(v)) - case StringConstant(v) => this += highlightString(escapedString(v)) + 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)) @@ -1449,62 +1446,6 @@ 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 = - if requiresFormat(ch) then - val b = StringBuilder().append(qc) - escapedChar(b, ch) - b.append(qc).toString - else - qc + ch + qc - - private def escapedChar(b: StringBuilder, c: Char): Unit = - def quadNibble(b: StringBuilder, x: Int, i: Int): Unit = - if i < 4 then - quadNibble(b, 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 c.isControl then - b.append("\\u") - quadNibble(b, c.toInt, 0) - else - b.append(c) - return - b.append(replace) - - private def requiresFormat(c: Char): Boolean = (c: @switch) match - case '\b' | '\t' | '\n' | '\f' | '\r' | '"' | '\'' | '\\' => true - case c => c.isControl - - private def escapedString(text: String): String = - def mustBuild: Boolean = - var i = 0 - while i < text.length do - if requiresFormat(text.charAt(i)) then return true - i += 1 - false - if mustBuild then - val b = StringBuilder(text.length + 16) - b.append(qSc) - var i = 0 - while i < text.length do - escapedChar(b, text.charAt(i)) - i += 1 - b.append(qSc) - b.toString - else - qSc + text + qSc - private val names = collection.mutable.Map.empty[Symbol, String] private val namesIndex = collection.mutable.Map.empty[String, Int]