Skip to content

Switch our string interpolators to use Show/Shown #14455

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

Merged
merged 6 commits into from
Apr 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/backend/sjs/JSExportsGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ final class JSExportsGen(jsCodeGen: JSCodeGen)(using Context) {
if (kind != overallKind) {
bad = true
report.error(
em"export overload conflicts with export of $firstSym: they are of different types ($kind / $overallKind)",
em"export overload conflicts with export of $firstSym: they are of different types (${kind.tryToShow} / ${overallKind.tryToShow})",
info.pos)
}
}
Expand Down
43 changes: 30 additions & 13 deletions compiler/src/dotty/tools/dotc/core/Decorators.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package dotty.tools
package dotc
package core

import annotation.tailrec
import Symbols._
import Contexts._, Names._, Phases._, printing.Texts._
import collection.mutable.ListBuffer
import dotty.tools.dotc.transform.MegaPhase
import printing.Formatting._
import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer
import scala.util.control.NonFatal

import Contexts._, Names._, Phases._, Symbols._
import printing.{ Printer, Showable }, printing.Formatting._, printing.Texts._
import transform.MegaPhase

/** This object provides useful implicit decorators for types defined elsewhere */
object Decorators {
Expand Down Expand Up @@ -246,13 +247,29 @@ object Decorators {
}

extension [T](x: T)
def showing(
op: WrappedResult[T] ?=> String,
printer: config.Printers.Printer = config.Printers.default): T = {
printer.println(op(using WrappedResult(x)))
def showing[U](
op: WrappedResult[U] ?=> String,
printer: config.Printers.Printer = config.Printers.default)(using c: Conversion[T, U] | Null = null): T = {
// either the use of `$result` was driven by the expected type of `Shown`
// which led to the summoning of `Conversion[T, Shown]` (which we'll invoke)
// or no such conversion was found so we'll consume the result as it is instead
val obj = if c == null then x.asInstanceOf[U] else c(x)
printer.println(op(using WrappedResult(obj)))
x
}

/** Instead of `toString` call `show` on `Showable` values, falling back to `toString` if an exception is raised. */
def tryToShow(using Context): String = x match
case x: Showable =>
try x.show
catch
case ex: CyclicReference => "... (caught cyclic reference) ..."
case NonFatal(ex)
if !ctx.mode.is(Mode.PrintShowExceptions) && !ctx.settings.YshowPrintErrors.value =>
val msg = ex match { case te: TypeError => te.toMessage case _ => ex.getMessage }
s"[cannot display due to $msg, raw string = $x]"
case _ => String.valueOf(x).nn

extension [T](x: T)
def assertingErrorsReported(using Context): T = {
assert(ctx.reporter.errorsReported)
Expand All @@ -269,19 +286,19 @@ object Decorators {

extension (sc: StringContext)
/** General purpose string formatting */
def i(args: Any*)(using Context): String =
def i(args: Shown*)(using Context): String =
new StringFormatter(sc).assemble(args)

/** Formatting for error messages: Like `i` but suppress follow-on
* error messages after the first one if some of their arguments are "non-sensical".
*/
def em(args: Any*)(using Context): String =
def em(args: Shown*)(using Context): String =
new ErrorMessageFormatter(sc).assemble(args)

/** Formatting with added explanations: Like `em`, but add explanations to
* give more info about type variables and to disambiguate where needed.
*/
def ex(args: Any*)(using Context): String =
def ex(args: Shown*)(using Context): String =
explained(em(args: _*))

extension [T <: AnyRef](arr: Array[T])
Expand Down
19 changes: 10 additions & 9 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2713,15 +2713,16 @@ object TypeComparer {
*/
val Fresh: Repr = 4

extension (approx: Repr)
def low: Boolean = (approx & LoApprox) != 0
def high: Boolean = (approx & HiApprox) != 0
def addLow: Repr = approx | LoApprox
def addHigh: Repr = approx | HiApprox
def show: String =
val lo = if low then " (left is approximated)" else ""
val hi = if high then " (right is approximated)" else ""
lo ++ hi
object Repr:
extension (approx: Repr)
def low: Boolean = (approx & LoApprox) != 0
def high: Boolean = (approx & HiApprox) != 0
def addLow: Repr = approx | LoApprox
def addHigh: Repr = approx | HiApprox
def show: String =
val lo = if low then " (left is approximated)" else ""
val hi = if high then " (right is approximated)" else ""
lo ++ hi
end ApproxState
type ApproxState = ApproxState.Repr

Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/interactive/Completion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ object Completion {
| prefix = ${completer.prefix},
| term = ${completer.mode.is(Mode.Term)},
| type = ${completer.mode.is(Mode.Type)}
| results = $backtickCompletions%, %""")
| results = $backtickedCompletions%, %""")
(offset, backtickedCompletions)
}

Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ object Parsers {
if startIndentWidth <= nextIndentWidth then
i"""Line is indented too far to the right, or a `{` is missing before:
|
|$t"""
|${t.tryToShow}"""
else
in.spaceTabMismatchMsg(startIndentWidth, nextIndentWidth),
in.next.offset
Expand Down
112 changes: 76 additions & 36 deletions compiler/src/dotty/tools/dotc/printing/Formatting.scala
Original file line number Diff line number Diff line change
@@ -1,47 +1,99 @@
package dotty.tools.dotc
package dotty.tools
package dotc
package printing

import scala.language.unsafeNulls

import scala.collection.mutable

import core._
import Texts._, Types._, Flags._, Symbols._, Contexts._
import collection.mutable
import Decorators._
import scala.util.control.NonFatal
import reporting.Message
import util.DiffUtil
import Highlighting._

object Formatting {

object ShownDef:
/** Represents a value that has been "shown" and can be consumed by StringFormatter.
* Not just a string because it may be a Seq that StringFormatter will intersperse with the trailing separator.
* Also, it's not a `String | Seq[String]` because then we'd need a Context to call `Showable#show`. We could
* make Context a requirement for a Show instance but then we'd have lots of instances instead of just one ShowAny
* instance. We could also try to make `Show#show` require the Context, but then that breaks the Conversion. */
opaque type Shown = Any
object Shown:
given [A: Show]: Conversion[A, Shown] = Show[A].show(_)

sealed abstract class Show[-T]:
/** Show a value T by returning a "shown" result. */
def show(x: T): Shown

/** The base implementation, passing the argument to StringFormatter which will try to `.show` it. */
object ShowAny extends Show[Any]:
def show(x: Any): Shown = x

class ShowImplicits2:
given Show[Product] = ShowAny

class ShowImplicits1 extends ShowImplicits2:
given Show[ImplicitRef] = ShowAny
given Show[Names.Designator] = ShowAny
given Show[util.SrcPos] = ShowAny

object Show extends ShowImplicits1:
inline def apply[A](using inline z: Show[A]): Show[A] = z

given [X: Show]: Show[Seq[X]] with
def show(x: Seq[X]) = x.map(Show[X].show)

given [A: Show, B: Show]: Show[(A, B)] with
def show(x: (A, B)) = (Show[A].show(x._1), Show[B].show(x._2))

given [X: Show]: Show[X | Null] with
def show(x: X | Null) = if x == null then "null" else Show[X].show(x.nn)

given Show[FlagSet] with
def show(x: FlagSet) = x.flagsString

given Show[TypeComparer.ApproxState] with
def show(x: TypeComparer.ApproxState) = TypeComparer.ApproxState.Repr.show(x)

given Show[Showable] = ShowAny
given Show[Shown] = ShowAny
given Show[Int] = ShowAny
given Show[Char] = ShowAny
given Show[Boolean] = ShowAny
given Show[String] = ShowAny
given Show[Class[?]] = ShowAny
given Show[Exception] = ShowAny
given Show[StringBuffer] = ShowAny
given Show[CompilationUnit] = ShowAny
given Show[Phases.Phase] = ShowAny
given Show[TyperState] = ShowAny
given Show[config.ScalaVersion] = ShowAny
given Show[io.AbstractFile] = ShowAny
given Show[parsing.Scanners.Scanner] = ShowAny
given Show[util.SourceFile] = ShowAny
given Show[util.Spans.Span] = ShowAny
given Show[tasty.TreeUnpickler#OwnerTree] = ShowAny
end Show
end ShownDef
export ShownDef.{ Show, Shown }

/** General purpose string formatter, with the following features:
*
* 1) On all Showables, `show` is called instead of `toString`
* 2) Exceptions raised by a `show` are handled by falling back to `toString`.
* 3) Sequences can be formatted using the desired separator between two `%` signs,
* 1. Invokes the `show` extension method on the interpolated arguments.
* 2. Sequences can be formatted using the desired separator between two `%` signs,
* eg `i"myList = (${myList}%, %)"`
* 4) Safe handling of multi-line margins. Left margins are skipped om the parts
* 3. Safe handling of multi-line margins. Left margins are stripped on the parts
* of the string context *before* inserting the arguments. That way, we guard
* against accidentally treating an interpolated value as a margin.
*/
class StringFormatter(protected val sc: StringContext) {
protected def showArg(arg: Any)(using Context): String = arg match {
case arg: Showable =>
try arg.show
catch {
case ex: CyclicReference => "... (caught cyclic reference) ..."
case NonFatal(ex)
if !ctx.mode.is(Mode.PrintShowExceptions) &&
!ctx.settings.YshowPrintErrors.value =>
val msg = ex match
case te: TypeError => te.toMessage
case _ => ex.getMessage
s"[cannot display due to $msg, raw string = ${arg.toString}]"
}
case _ => String.valueOf(arg)
}
protected def showArg(arg: Any)(using Context): String = arg.tryToShow

private def treatArg(arg: Any, suffix: String)(using Context): (Any, String) = arg match {
private def treatArg(arg: Shown, suffix: String)(using Context): (Any, String) = arg match {
case arg: Seq[?] if suffix.nonEmpty && suffix.head == '%' =>
val (rawsep, rest) = suffix.tail.span(_ != '%')
val sep = StringContext.processEscapes(rawsep)
Expand All @@ -51,7 +103,7 @@ object Formatting {
(showArg(arg), suffix)
}

def assemble(args: Seq[Any])(using Context): String = {
def assemble(args: Seq[Shown])(using Context): String = {
def isLineBreak(c: Char) = c == '\n' || c == '\f' // compatible with StringLike#isLineBreak
def stripTrailingPart(s: String) = {
val (pre, post) = s.span(c => !isLineBreak(c))
Expand All @@ -77,18 +129,6 @@ object Formatting {
override protected def showArg(arg: Any)(using Context): String =
wrapNonSensical(arg, super.showArg(arg)(using errorMessageCtx))

class SyntaxFormatter(sc: StringContext) extends StringFormatter(sc) {
override protected def showArg(arg: Any)(using Context): String =
arg match {
case hl: Highlight =>
hl.show
case hb: HighlightBuffer =>
hb.toString
case _ =>
SyntaxHighlighting.highlight(super.showArg(arg))
}
}

private def wrapNonSensical(arg: Any, str: String)(using Context): String = {
import Message._
def isSensical(arg: Any): Boolean = arg match {
Expand Down
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1759,7 +1759,9 @@ import transform.SymUtils._

class ClassAndCompanionNameClash(cls: Symbol, other: Symbol)(using Context)
extends NamingMsg(ClassAndCompanionNameClashID) {
def msg = em"Name clash: both ${cls.owner} and its companion object defines ${cls.name.stripModuleClassSuffix}"
def msg =
val name = cls.name.stripModuleClassSuffix
em"Name clash: both ${cls.owner} and its companion object defines $name"
def explain =
em"""|A ${cls.kindString} and its companion object cannot both define a ${hl("class")}, ${hl("trait")} or ${hl("object")} with the same name:
| - ${cls.owner} defines ${cls}
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,7 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
case n: Name =>
h = nameHash(n, h)
case elem =>
cannotHash(what = i"`$elem` of unknown class ${elem.getClass}", elem, tree)
cannotHash(what = i"`${elem.tryToShow}` of unknown class ${elem.getClass}", elem, tree)
h
end iteratorHash

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ abstract class AccessProxies {

/** Add all needed accessors to the `body` of class `cls` */
def addAccessorDefs(cls: Symbol, body: List[Tree])(using Context): List[Tree] = {
val accDefs = accessorDefs(cls)
val accDefs = accessorDefs(cls).toList
transforms.println(i"add accessors for $cls: $accDefs%, %")
if (accDefs.isEmpty) body else body ++ accDefs
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class DropOuterAccessors extends MiniPhase with IdentityDenotTransformer:
cpy.Block(rhs)(inits.filterNot(dropOuterInit), expr)
})
assert(droppedParamAccessors.isEmpty,
i"""Failed to eliminate: $droppedParamAccessors
i"""Failed to eliminate: ${droppedParamAccessors.toList}
when dropping outer accessors for ${ctx.owner} with
$impl""")
cpy.Template(impl)(constr = constr1, body = body1)
Expand Down
6 changes: 3 additions & 3 deletions compiler/src/dotty/tools/dotc/transform/Erasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ object Erasure {
def constant(tree: Tree, const: Tree)(using Context): Tree =
(if (isPureExpr(tree)) const else Block(tree :: Nil, const)).withSpan(tree.span)

final def box(tree: Tree, target: => String = "")(using Context): Tree = trace(i"boxing ${tree.showSummary}: ${tree.tpe} into $target") {
final def box(tree: Tree, target: => String = "")(using Context): Tree = trace(i"boxing ${tree.showSummary()}: ${tree.tpe} into $target") {
tree.tpe.widen match {
case ErasedValueType(tycon, _) =>
New(tycon, cast(tree, underlyingOfValueClass(tycon.symbol.asClass)) :: Nil) // todo: use adaptToType?
Expand All @@ -286,7 +286,7 @@ object Erasure {
}
}

def unbox(tree: Tree, pt: Type)(using Context): Tree = trace(i"unboxing ${tree.showSummary}: ${tree.tpe} as a $pt") {
def unbox(tree: Tree, pt: Type)(using Context): Tree = trace(i"unboxing ${tree.showSummary()}: ${tree.tpe} as a $pt") {
pt match {
case ErasedValueType(tycon, underlying) =>
def unboxedTree(t: Tree) =
Expand Down Expand Up @@ -1031,7 +1031,7 @@ object Erasure {
}

override def adapt(tree: Tree, pt: Type, locked: TypeVars, tryGadtHealing: Boolean)(using Context): Tree =
trace(i"adapting ${tree.showSummary}: ${tree.tpe} to $pt", show = true) {
trace(i"adapting ${tree.showSummary()}: ${tree.tpe} to $pt", show = true) {
if ctx.phase != erasurePhase && ctx.phase != erasurePhase.next then
// this can happen when reading annotations loaded during erasure,
// since these are loaded at phase typer.
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ trait Checking {
case _: TypeTree =>
case _ =>
if tree.tpe.typeParams.nonEmpty then
val what = if tree.symbol.exists then tree.symbol else i"type $tree"
val what = if tree.symbol.exists then tree.symbol.show else i"type $tree"
report.error(em"$what takes type parameters", tree.srcPos)

/** Check that we are in an inline context (inside an inline method or in inline code) */
Expand Down
34 changes: 29 additions & 5 deletions compiler/test/dotty/tools/dotc/printing/PrinterTests.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package dotty.tools.dotc.printing
package dotty.tools
package dotc
package printing

import dotty.tools.DottyTest
import dotty.tools.dotc.ast.{Trees,tpd}
import dotty.tools.dotc.core.Names._
import dotty.tools.dotc.core.Symbols._
import ast.{ Trees, tpd }
import core.Names._
import core.Symbols._
import core.Decorators._
import dotty.tools.dotc.core.Contexts.Context

import org.junit.Assert.assertEquals
Expand Down Expand Up @@ -49,4 +51,26 @@ class PrinterTests extends DottyTest {
assertEquals("Int & (Boolean | String)", bar.tpt.show)
}
}

@Test def string: Unit = assertEquals("foo", i"${"foo"}")

import core.Flags._
@Test def flagsSingle: Unit = assertEquals("final", i"$Final")
@Test def flagsSeq: Unit = assertEquals("<static>, final", i"${Seq(JavaStatic, Final)}%, %")
@Test def flagsTuple: Unit = assertEquals("(<static>,final)", i"${(JavaStatic, Final)}")
@Test def flagsSeqOfTuple: Unit = assertEquals("(final,given), (private,lazy)", i"${Seq((Final, Given), (Private, Lazy))}%, %")

class StorePrinter extends config.Printers.Printer:
var string: String = "<never set>"
override def println(msg: => String) = string = msg

@Test def testShowing: Unit =
val store = StorePrinter()
(JavaStatic | Final).showing(i"flags=$result", store)
assertEquals("flags=final <static>", store.string)

@Test def TestShowingWithOriginalType: Unit =
val store = StorePrinter()
(JavaStatic | Final).showing(i"flags=${if result.is(Private) then result &~ Private else result | Private}", store)
assertEquals("flags=private final <static>", store.string)
}