Skip to content

Commit

Permalink
Merge pull request scala#14455 from dwijnand/shown
Browse files Browse the repository at this point in the history
Switch our string interpolators to use Show/Shown
  • Loading branch information
nicolasstucki authored Apr 14, 2022
2 parents c084b46 + e5ed401 commit eb6aaa0
Show file tree
Hide file tree
Showing 13 changed files with 158 additions and 74 deletions.
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 @@ -2730,15 +2730,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)
}

0 comments on commit eb6aaa0

Please sign in to comment.