Skip to content

Commit

Permalink
Merge pull request #15810 from dwijnand/fix-Show-infra
Browse files Browse the repository at this point in the history
Fix aspects of the Show setup
  • Loading branch information
dwijnand authored Aug 28, 2022
2 parents f0be00d + 63068a9 commit 8a7c84c
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 66 deletions.
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/core/Decorators.scala
Original file line number Diff line number Diff line change
Expand Up @@ -293,13 +293,13 @@ object Decorators {
* error messages after the first one if some of their arguments are "non-sensical".
*/
def em(args: Shown*)(using Context): String =
new ErrorMessageFormatter(sc).assemble(args)
forErrorMessages(new StringFormatter(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: Shown*)(using Context): String =
explained(em(args: _*))
explained(new StringFormatter(sc).assemble(args))

extension [T <: AnyRef](arr: Array[T])
def binarySearch(x: T | Null): Int = java.util.Arrays.binarySearch(arr.asInstanceOf[Array[Object | Null]], x)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ object OrderingConstraint {
empty
else
val result = new OrderingConstraint(boundsMap, lowerMap, upperMap)
ctx.run.nn.recordConstraintSize(result, result.boundsMap.size)
if ctx.run != null then ctx.run.nn.recordConstraintSize(result, result.boundsMap.size)
result

/** A lens for updating a single entry array in one of the three constraint maps */
Expand Down
69 changes: 42 additions & 27 deletions compiler/src/dotty/tools/dotc/printing/Formatting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ 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. */
* It may also be a CtxShow, which allows the Show instance to finish showing the value with the string
* interpolator's correct context, that is with non-sensical tagging, message limiting, explanations, etc. */
opaque type Shown = Any
object Shown:
given [A: Show]: Conversion[A, Shown] = Show[A].show(_)
Expand All @@ -29,6 +28,14 @@ object Formatting {
/** Show a value T by returning a "shown" result. */
def show(x: T): Shown

trait CtxShow:
def run(using Context): Shown

extension (s: Shown)
def ctxShow(using Context): Shown = s match
case cs: CtxShow => cs.run
case _ => s

/** 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
Expand All @@ -37,11 +44,7 @@ object Formatting {
given Show[Product] = ShowAny

class ShowImplicits2 extends ShowImplicits3:
given Show[ParamInfo] with
def show(x: ParamInfo) = x match
case x: Symbol => Show[x.type].show(x)
case x: LambdaParam => Show[x.type].show(x)
case _ => ShowAny
given Show[ParamInfo] = ShowAny

class ShowImplicits1 extends ShowImplicits2:
given Show[ImplicitRef] = ShowAny
Expand All @@ -52,10 +55,12 @@ object Formatting {
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)
def show(x: Seq[X]) = new CtxShow:
def run(using Context) = x.map(show1)

given [A: Show, B: Show]: Show[(A, B)] with
def show(x: (A, B)) = (Show[A].show(x._1), Show[B].show(x._2))
def show(x: (A, B)) = new CtxShow:
def run(using Context) = (show1(x._1), show1(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)
Expand All @@ -71,6 +76,7 @@ object Formatting {
given Show[Int] = ShowAny
given Show[Char] = ShowAny
given Show[Boolean] = ShowAny
given Show[Integer] = ShowAny
given Show[String] = ShowAny
given Show[Class[?]] = ShowAny
given Show[Throwable] = ShowAny
Expand All @@ -84,6 +90,11 @@ object Formatting {
given Show[util.SourceFile] = ShowAny
given Show[util.Spans.Span] = ShowAny
given Show[tasty.TreeUnpickler#OwnerTree] = ShowAny

private def show1[A: Show](x: A)(using Context) = show2(Show[A].show(x).ctxShow)
private def show2(x: Shown)(using Context): String = x match
case seq: Seq[?] => seq.map(show2).mkString("[", ", ", "]")
case res => res.tryToShow
end Show
end ShownDef
export ShownDef.{ Show, Shown }
Expand All @@ -100,15 +111,14 @@ object Formatting {
class StringFormatter(protected val sc: StringContext) {
protected def showArg(arg: Any)(using Context): String = arg.tryToShow

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)
if (rest.nonEmpty) (arg.map(showArg).mkString(sep), rest.tail)
else (arg, suffix)
private def treatArg(arg: Shown, suffix: String)(using Context): (String, String) = arg.ctxShow match {
case arg: Seq[?] if suffix.indexOf('%') == 0 && suffix.indexOf('%', 1) != -1 =>
val end = suffix.indexOf('%', 1)
val sep = StringContext.processEscapes(suffix.substring(1, end))
(arg.mkString(sep), suffix.substring(end + 1))
case arg: Seq[?] =>
(arg.map(showArg).mkString("[", ", ", "]"), suffix)
case _ =>
case arg =>
(showArg(arg), suffix)
}

Expand All @@ -134,11 +144,13 @@ object Formatting {
* like concatenation, stripMargin etc on the values returned by em"...", and in the current error
* message composition methods, this is crucial.
*/
class ErrorMessageFormatter(sc: StringContext) extends StringFormatter(sc):
override protected def showArg(arg: Any)(using Context): String =
wrapNonSensical(arg, super.showArg(arg)(using errorMessageCtx))
def forErrorMessages(op: Context ?=> String)(using Context): String = op(using errorMessageCtx)

private class ErrorMessagePrinter(_ctx: Context) extends RefinedPrinter(_ctx):
override def toText(tp: Type): Text = wrapNonSensical(tp, super.toText(tp))
override def toText(sym: Symbol): Text = wrapNonSensical(sym, super.toText(sym))

private def wrapNonSensical(arg: Any, str: String)(using Context): String = {
private def wrapNonSensical(arg: Any, text: Text)(using Context): Text = {
import Message._
def isSensical(arg: Any): Boolean = arg match {
case tpe: Type =>
Expand All @@ -151,8 +163,8 @@ object Formatting {
case _ => true
}

if (isSensical(arg)) str
else nonSensicalStartTag + str + nonSensicalEndTag
if (isSensical(arg)) text
else nonSensicalStartTag ~ text ~ nonSensicalEndTag
}

private type Recorded = Symbol | ParamRef | SkolemType
Expand Down Expand Up @@ -203,7 +215,7 @@ object Formatting {
}
}

private class ExplainingPrinter(seen: Seen)(_ctx: Context) extends RefinedPrinter(_ctx) {
private class ExplainingPrinter(seen: Seen)(_ctx: Context) extends ErrorMessagePrinter(_ctx) {

/** True if printer should a source module instead of its module class */
private def useSourceModule(sym: Symbol): Boolean =
Expand Down Expand Up @@ -307,9 +319,12 @@ object Formatting {
}

private def errorMessageCtx(using Context): Context =
ctx.property(MessageLimiter) match
val ctx1 = ctx.property(MessageLimiter) match
case Some(_: ErrorMessageLimiter) => ctx
case _ => ctx.fresh.setProperty(MessageLimiter, ErrorMessageLimiter())
ctx1.printer match
case _: ErrorMessagePrinter => ctx1
case _ => ctx1.fresh.setPrinterFn(ctx => ErrorMessagePrinter(ctx))

/** Context with correct printer set for explanations */
private def explainCtx(seen: Seen)(using Context): Context =
Expand Down Expand Up @@ -364,8 +379,8 @@ object Formatting {
* highlight the difference
*/
def typeDiff(found: Type, expected: Type)(using Context): (String, String) = {
val fnd = wrapNonSensical(found, found.show)
val exp = wrapNonSensical(expected, expected.show)
val fnd = wrapNonSensical(found, found.toText(ctx.printer)).show
val exp = wrapNonSensical(expected, expected.toText(ctx.printer)).show

DiffUtil.mkColoredTypeDiff(fnd, exp) match {
case _ if ctx.settings.color.value == "never" => (fnd, exp)
Expand Down
6 changes: 6 additions & 0 deletions compiler/src/dotty/tools/dotc/printing/MessageLimiter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ class ErrorMessageLimiter extends MessageLimiter:

override def recurseLimit =
val freeFraction: Double = ((sizeLimit - textLength) max 0).toDouble / sizeLimit
// 10'000 - 0 / 10'0000 = 100% free
// 10'000 - 200 / 10'0000 = 98% free * 50 = 49
// 10'000 - 1'000 / 10'0000 = 90% free * 50 = 45
// 10'000 - 2'000 / 10'0000 = 80% free * 50 = 40
// every 200 characters consumes a "recurseCount"
// which, additionally, is lowered from 100 to 50 here
(initialRecurseLimit * freeFraction).toInt


96 changes: 96 additions & 0 deletions compiler/test/dotty/tools/dotc/StringFormatterTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package dotty.tools
package dotc

import core.*, Contexts.*, Decorators.*, Denotations.*, Flags.*, Names.*, StdNames.*, SymDenotations.*, Symbols.*, Types.*
import config.Printers.*
import printing.Formatting.Show

import org.junit.Test
import org.junit.Assert.*

class StringFormatterTest extends AbstractStringFormatterTest:
@Test def string = check("foo", i"${"foo"}")
@Test def integer = check("1", i"${Int.box(1)}")
@Test def type1 = check("Any", i"${defn.AnyType}")
@Test def symbol = check("class Any", i"${defn.AnyClass}")
@Test def paramInfo = check("class Any", i"${defn.AnyClass: ParamInfo}")
@Test def seq = check("[Any, String]", i"${Seq(defn.AnyType, defn.StringType)}")
@Test def seqSep = check("Any; String", i"${Seq(defn.AnyType, defn.StringType)}%; %")
@Test def tuple = check("(1,Any)", i"${(1, defn.AnyType)}")
@Test def seqOfTup = check("(1,Any), (2,String)", i"${Seq(1 -> defn.AnyType, 2 -> defn.StringType)}%, %")
@Test def flags1 = check("final", i"$Final")
@Test def flagsSeq = check("<static>, final", i"${Seq(JavaStatic, Final)}%, %")
@Test def flagsTup = check("(<static>,final)", i"${(JavaStatic, Final)}")
@Test def seqOfTup2 = check("(final,given), (private,lazy)", i"${Seq((Final, Given), (Private, Lazy))}%, %")

class StorePrinter extends 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)
end StringFormatterTest

class EmStringFormatterTest extends AbstractStringFormatterTest:
@Test def seq = check("[Any, String]", em"${Seq(defn.AnyType, defn.StringType)}")
@Test def seqSeq = check("Any; String", em"${Seq(defn.AnyType, defn.StringType)}%; %")
@Test def ellipsis = assert(em"$Big".contains("..."))
@Test def err = check("<nonsensical>type Err</nonsensical>", em"$Err")
@Test def ambig = check("Foo vs Foo", em"$Foo vs $Foo")
@Test def cstrd = check("Foo; Bar", em"$mkCstrd%; %")
@Test def seqErr = check("[class Any, <nonsensical>type Err</nonsensical>]", em"${Seq(defn.AnyClass, Err)}")
@Test def seqSeqErr = check("class Any; <nonsensical>type Err</nonsensical>", em"${Seq(defn.AnyClass, Err)}%; %")
@Test def tupleErr = check("(1,<nonsensical>type Err</nonsensical>)", em"${(1, Err)}")
@Test def tupleAmb = check("(Foo,Foo)", em"${(Foo, Foo)}")
@Test def tupleFlags = check("(Foo,abstract)", em"${(Foo, Abstract)}")
@Test def seqOfTupleFlags = check("[(Foo,abstract)]", em"${Seq((Foo, Abstract))}")
end EmStringFormatterTest

class ExStringFormatterTest extends AbstractStringFormatterTest:
@Test def seq = check("[Any, String]", ex"${Seq(defn.AnyType, defn.StringType)}")
@Test def seqSeq = check("Any; String", ex"${Seq(defn.AnyType, defn.StringType)}%; %")
@Test def ellipsis = assert(ex"$Big".contains("..."))
@Test def err = check("<nonsensical>type Err</nonsensical>", ex"$Err")
@Test def ambig = check("""Foo vs Foo²
|
|where: Foo is a type
| Foo² is a type
|""".stripMargin, ex"$Foo vs $Foo")
@Test def cstrd = check("""Foo; Bar
|
|where: Bar is a type variable with constraint <: String
| Foo is a type variable with constraint <: Int
|""".stripMargin, ex"$mkCstrd%; %")
@Test def seqErr = check("[class Any, <nonsensical>type Err</nonsensical>]", ex"${Seq(defn.AnyClass, Err)}")
@Test def seqSeqErr = check("class Any; <nonsensical>type Err</nonsensical>", ex"${Seq(defn.AnyClass, Err)}%; %")
@Test def tupleErr = check("(1,<nonsensical>type Err</nonsensical>)", ex"${(1, Err)}")
@Test def tupleAmb = check("""(Foo,Foo²)
|
|where: Foo is a type
| Foo² is a type
|""".stripMargin, ex"${(Foo, Foo)}")
end ExStringFormatterTest

abstract class AbstractStringFormatterTest extends DottyTest:
override def initializeCtx(fc: FreshContext) = super.initializeCtx(fc.setSetting(fc.settings.color, "never"))

def Foo = newSymbol(defn.RootClass, typeName("Foo"), EmptyFlags, TypeBounds.empty).typeRef
def Err = newErrorSymbol(defn.RootClass, typeName("Err"), "")
def Big = (1 to 120).foldLeft(defn.StringType)((tp, i) => RefinedType(tp, typeName("A" * 69 + i), TypeAlias(defn.IntType)))

def mkCstrd =
val names = List(typeName("Foo"), typeName("Bar"))
val infos = List(TypeBounds.upper(defn.IntType), TypeBounds.upper(defn.StringType))
val tl = PolyType(names)(_ => infos, _ => defn.AnyType)
TypeComparer.addToConstraint(tl, Nil)
tl.paramRefs

def ckSub(obtained: String, snippet: String) = assert(obtained.contains(snippet))
def check(expected: String, obtained: String) = assertEquals(expected, obtained)
41 changes: 5 additions & 36 deletions compiler/test/dotty/tools/dotc/printing/PrinterTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,14 @@ package dotty.tools
package dotc
package printing

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

import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.Assert.*

class PrinterTests extends DottyTest {

private def newContext = {
initialCtx.setSetting(ctx.settings.color, "never")
}
ctx = newContext

import tpd._
override def initializeCtx(fc: FreshContext) = super.initializeCtx(fc.setSetting(fc.settings.color, "never"))

@Test
def packageObject: Unit = {
Expand Down Expand Up @@ -47,30 +38,8 @@ class PrinterTests extends DottyTest {

checkCompile("typer", source) { (tree, context) =>
given Context = context
val bar @ Trees.DefDef(_, _, _, _) = tree.find(tree => tree.symbol.name == termName("bar2")).get: @unchecked
val bar @ DefDef(_, _, _, _) = tree.find(tree => tree.symbol.name == termName("bar2")).get: @unchecked
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 8a7c84c

Please sign in to comment.