Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit cacd117

Browse files
authored
Use PPrint to handle printing of REPL output values (#23849)
This PR demonstrates using the https://github.com/com-lihaoyi/PPrint library to handle pretty-printing of values in the REPL. Visible improvements: - Data structures like sequences and case classes are now nicely formatted and indented, including deeply nested data structures, to make best use of the vertical and horizontal space available - Strings are consistently quoted in collections and case classes, rather than sometimes quoted and sometimes not. - The rendering of `Seq("")` and `Seq()` is no longer identical (sometimes!) - Unusual characters within strings are now properly quoted, rather than being butchered during the rendering - (not shown) Character literals like `'X'` are properly pretty-printed with quotes - Adjustments to the syntax highlighting colour scheme, making it subjectively easier to read and converging it with the scheme used by `pprint`'s own internal highlighter: - Unified `StringColor` and `LiteralColor` as green rather than red. This should help avoid the red of literals being visually confused with the red of error messages when pretty printing code during compilation errors, which is something I have had problems with in the past - Highlighted capitalized identifies like `Foo` or `Seq` or `List`, since the vast majority of these identifiers are likely to be the companion object of types, and highlighting them helps greatly in visually finding your way around pretty-printed data structures Before: <img width="976" height="659" alt="Screenshot 2025年09月01日 at 9 42 52 AM" src="https://github.com/user-attachments/assets/1f082d42-7ac3-4785-b957-029e96603819" /> After: <img width="976" height="835" alt="Screenshot 2025年09月01日 at 1 41 05 PM" src="https://github.com/user-attachments/assets/847e6210-eb3d-40f9-a96c-5fc658cb1de4" /> Notes: * This PR only uses PPrint for formatting and not coloring, relying on the existing REPL code that deals with syntax highlighting (with tweaks). Using PPrint's highlighter directly would require a larger refactor that can come in a follow up iff we decide to do so * We build pprint/fansi/sourcecode from source using `sourceGenerators`. This requires a bit of patching to work around `-Xexplicit-nulls` and `-Xfatal-warnings`, but otherwise is straightforward and means for all intents and purposes it's just part of the Dotty codebase. We mangle the package paths to make them `dotty.shaded.*` packages to avoid conflict with user code * The verbosity of `PPrint` can be configured, e.g. we can decide whether we want to print field names or not. By default it prints field names for any `case class` with more than `1` field I set the default pprint dimensions to width=100 height=50, and added a `import dotty.shaded.pprint.pprintln` to the predef of every REPL so users have `pprintln` available in scope. Users who want to print more than 50 lines can call `pprintln` which prints up to 100x500 by default, and can take a custom `height=9999` if they want to print more. The numbers 100x50 and 100x500 are heuristics: - 100x50 as the default for echo-ed values is a heuristic optimizing for terminal use, where width=100 approximates the common maximum width people tend to format their code to (typically 80-120), and 50 reflects about 0.5 to 1 vertical screenful of text so it doesn't kick previous terminal output off the top of your terminal - 100x500 as the default for `pprintln` is a heuristic optimizing for non-terminal use: it's about 5-10 vertical screenfuls of text, and about the limit of what we expect people to usefully be able to skim through. Typically, in most cases when the output is larger than this, you'd want to cut it down by selecting a subset of the output programmatically. - If the user really wants to print everything, they can run `pprintln(foo, height=99999)` or similar The heuristics can be tweaked, but they should provide a decent baseline for printing a useful amount of output to screen without flooding the user's terminal. TODO/Future-Work: * Automatically select max height/width based on terminal size, and provide a helper (similar to Ammonite's `show(...)`) to bypass the max height. For now, it's fixed at the default width of 100 columns * We can use the same approach to make use of `os-lib` and other libraries within `scala3-compiler` by building them from source * Make use of `fansi` elsewhere in the dotty codebase. e.g. the highlighting of stack traces via the code syntax highlighter is super ugly and could be cleaned up: <img width="675" height="259" alt="Screenshot 2025年09月01日 at 1 09 22 PM" src="https://github.com/user-attachments/assets/47515039-b2e1-4299-86fe-22fd2c199958" />
1 parent c8d77df commit cacd117

File tree

24 files changed

+172
-101
lines changed

24 files changed

+172
-101
lines changed

‎compiler/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ object SyntaxHighlighting {
2727
val CommentColor: String = Console.BLUE
2828
val KeywordColor: String = Console.YELLOW
2929
val ValDefColor: String = Console.CYAN
30-
val LiteralColor: String = Console.RED
30+
val LiteralColor: String = Console.GREEN
3131
val StringColor: String = Console.GREEN
3232
val TypeColor: String = Console.MAGENTA
3333
val AnnotationColor: String = Console.MAGENTA
@@ -80,6 +80,9 @@ object SyntaxHighlighting {
8080
case IDENTIFIER if name == nme.??? =>
8181
highlightRange(start, end, Console.RED_B)
8282

83+
case IDENTIFIER if name.head.isUpper && name.exists(!_.isUpper) =>
84+
highlightRange(start, end, KeywordColor)
85+
8386
case _ =>
8487
}
8588
}

‎compiler/src/dotty/tools/repl/Main.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ package dotty.tools.repl
44
// To test, run bin/scala
55
object Main {
66
def main(args: Array[String]): Unit =
7-
new ReplDriver(args).tryRunning
7+
new ReplDriver(args, extraPredef =ReplDriver.pprintImport).tryRunning
88
}

‎compiler/src/dotty/tools/repl/Rendering.scala

Lines changed: 4 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):
2626

2727
var myClassLoader: AbstractFileClassLoader = uninitialized
2828

29-
/** (value, maxElements, maxCharacters) => String */
30-
var myReplStringOf: (Object, Int, Int) => String = uninitialized
3129

3230
/** Class loader used to load compiled code */
3331
private[repl] def classLoader()(using Context) =
@@ -46,45 +44,6 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):
4644
}
4745

4846
myClassLoader = new AbstractFileClassLoader(ctx.settings.outputDir.value, parent)
49-
myReplStringOf = {
50-
// We need to use the ScalaRunTime class coming from the scala-library
51-
// on the user classpath, and not the one available in the current
52-
// classloader, so we use reflection instead of simply calling
53-
// `ScalaRunTime.stringOf`. Also probe for new stringOf that does string quoting, etc.
54-
val scalaRuntime = Class.forName("scala.runtime.ScalaRunTime", true, myClassLoader)
55-
val renderer = "stringOf"
56-
val stringOfInvoker: (Object, Int) => String =
57-
def richStringOf: (Object, Int) => String =
58-
val method = scalaRuntime.getMethod(renderer, classOf[Object], classOf[Int], classOf[Boolean])
59-
val richly = java.lang.Boolean.TRUE // add a repl option for enriched output
60-
(value, maxElements) => method.invoke(null, value, maxElements, richly).asInstanceOf[String]
61-
def poorStringOf: (Object, Int) => String =
62-
try
63-
val method = scalaRuntime.getMethod(renderer, classOf[Object], classOf[Int])
64-
(value, maxElements) => method.invoke(null, value, maxElements).asInstanceOf[String]
65-
catch case _: NoSuchMethodException => (value, maxElements) => String.valueOf(value).take(maxElements)
66-
try richStringOf
67-
catch case _: NoSuchMethodException => poorStringOf
68-
def stringOfMaybeTruncated(value: Object, maxElements: Int): String = stringOfInvoker(value, maxElements)
69-
70-
// require value != null
71-
// `ScalaRuntime.stringOf` returns null iff value.toString == null, let caller handle that.
72-
// `ScalaRuntime.stringOf` may truncate the output, in which case we want to indicate that fact to the user
73-
// In order to figure out if it did get truncated, we invoke it twice - once with the `maxElements` that we
74-
// want to print, and once without a limit. If the first is shorter, truncation did occur.
75-
// Note that `stringOf` has new API in flight to handle truncation, see stringOfMaybeTruncated.
76-
(value: Object, maxElements: Int, maxCharacters: Int) =>
77-
stringOfMaybeTruncated(value, Int.MaxValue) match
78-
case null => null
79-
case notTruncated =>
80-
val maybeTruncated =
81-
val maybeTruncatedByElementCount = stringOfMaybeTruncated(value, maxElements)
82-
truncate(maybeTruncatedByElementCount, maxCharacters)
83-
// our string representation may have been truncated by element and/or character count
84-
// if so, append an info string - but only once
85-
if notTruncated.length == maybeTruncated.length then maybeTruncated
86-
else s"$maybeTruncated ... large output truncated, print value to show all"
87-
}
8847
myClassLoader
8948
}
9049

@@ -94,18 +53,10 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):
9453
else str.substring(0, str.offsetByCodePoints(0, maxPrintCharacters - 1))
9554

9655
/** Return a String representation of a value we got from `classLoader()`. */
97-
private[repl] def replStringOf(sym: Symbol, value: Object)(using Context): String =
98-
assert(myReplStringOf != null,
99-
"replStringOf should only be called on values creating using `classLoader()`, but `classLoader()` has not been called so far")
100-
val maxPrintElements = ctx.settings.VreplMaxPrintElements.valueIn(ctx.settingsState)
101-
val maxPrintCharacters = ctx.settings.VreplMaxPrintCharacters.valueIn(ctx.settingsState)
102-
// stringOf returns null if value.toString returns null. Show some text as a fallback.
103-
def fallback = s"""null // result of "${sym.name}.toString" is null"""
104-
if value == null then "null" else
105-
myReplStringOf(value, maxPrintElements, maxPrintCharacters) match
106-
case null => fallback
107-
case res => res
108-
end if
56+
private[repl] def replStringOf(sym: Symbol, value: Object)(using Context): String = {
57+
// pretty-print things with 100 cols 50 rows by default,
58+
dotty.shaded.pprint.PPrinter.BlackWhite.apply(value, width = 100, height = 50).plainText
59+
}
10960

11061
/** Load the value of the symbol using reflection.
11162
*

‎compiler/src/dotty/tools/repl/ReplDriver.scala

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ case class State(objectIndex: Int,
7777
/** Main REPL instance, orchestrating input, compilation and presentation */
7878
class ReplDriver(settings: Array[String],
7979
out: PrintStream = Console.out,
80-
classLoader: Option[ClassLoader] = None) extends Driver:
80+
classLoader: Option[ClassLoader] = None,
81+
extraPredef: String = "") extends Driver:
8182

8283
/** Overridden to `false` in order to not have to give sources on the
8384
* commandline
@@ -122,9 +123,10 @@ class ReplDriver(settings: Array[String],
122123
final def initialState: State =
123124
val emptyState = State(0, 0, Map.empty, Set.empty, false, rootCtx)
124125
val initScript = rootCtx.settings.replInitScript.value(using rootCtx)
125-
initScript.trim() match
126-
case "" => emptyState
127-
case script => run(script)(using emptyState)
126+
val combinedScript = initScript.trim() match
127+
case "" => extraPredef
128+
case script => s"$extraPredef\n$script"
129+
run(combinedScript)(using emptyState)
128130

129131
/** Reset state of repl to the initial state
130132
*
@@ -638,3 +640,5 @@ class ReplDriver(settings: Array[String],
638640
case _ => ReplConsoleReporter.doReport(dia)(using state.context)
639641

640642
end ReplDriver
643+
object ReplDriver:
644+
def pprintImport = "import dotty.shaded.pprint.pprintln\n"

‎compiler/test-resources/repl/19184

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
scala> def o(s: String) = "o"; def oo(s: String) = "oo"; val o = "o"; val oo = "oo"
22
def o(s: String): String
33
def oo(s: String): String
4-
val o: String = o
5-
val oo: String = oo
4+
val o: String = "o"
5+
val oo: String = "oo"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
scala> val d: Long = (new java.sql.Date(100L)).getTime
2-
val d: Long = 100
2+
val d: Long = 100L

‎compiler/test-resources/repl/i13181

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
scala> scala.compiletime.codeOf(1+2)
2-
val res0: String = 1 + 2
2+
val res0: String = "1 + 2"

‎compiler/test-resources/repl/i1369

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
scala> print("foo")
22
foo
33
scala> "Hello"
4-
val res0: String = Hello
4+
val res0: String = "Hello"

‎compiler/test-resources/repl/i15493

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ scala> NInt(23)
55
val res0: NInt = NInt@17
66

77
scala> res0.toString
8-
val res1: String = NInt@17
8+
val res1: String = "rs$line1ドル$NInt@17"
99

1010
scala> 23
1111
val res2: Int = 23
@@ -17,7 +17,7 @@ scala> NBoolean(true)
1717
val res3: NBoolean = NBoolean@4cf
1818

1919
scala> res3.toString
20-
val res4: String = NBoolean@4cf
20+
val res4: String = "rs$line5ドル$NBoolean@4cf"
2121

2222
scala> true
2323
val res5: Boolean = true
@@ -29,7 +29,7 @@ scala> NByte(1)
2929
val res6: NByte = NByte@1
3030

3131
scala> res6.toString
32-
val res7: String = NByte@1
32+
val res7: String = "rs$line9ドル$NByte@1"
3333

3434
scala> val res8: Byte = 1
3535
val res8: Byte = 1
@@ -41,7 +41,7 @@ scala> NShort(1)
4141
val res9: NShort = NShort@1
4242

4343
scala> res9.toString
44-
val res10: String = NShort@1
44+
val res10: String = "rs$line13ドル$NShort@1"
4545

4646
scala> val res11: Short = 1
4747
val res11: Short = 1
@@ -53,10 +53,10 @@ scala> NLong(1)
5353
val res12: NLong = NLong@1
5454

5555
scala> res12.toString
56-
val res13: String = NLong@1
56+
val res13: String = "rs$line17ドル$NLong@1"
5757

5858
scala> 1L
59-
val res14: Long = 1
59+
val res14: Long = 1L
6060

6161
scala> class NFloat(val x: Float) extends AnyVal
6262
// defined class NFloat
@@ -65,10 +65,10 @@ scala> NFloat(1L)
6565
val res15: NFloat = NFloat@3f800000
6666

6767
scala> res15.toString
68-
val res16: String = NFloat@3f800000
68+
val res16: String = "rs$line21ドル$NFloat@3f800000"
6969

7070
scala> 1.0F
71-
val res17: Float = 1.0
71+
val res17: Float = 1.0F
7272

7373
scala> class NDouble(val x: Double) extends AnyVal
7474
// defined class NDouble
@@ -77,7 +77,7 @@ scala> NDouble(1D)
7777
val res18: NDouble = NDouble@3ff00000
7878

7979
scala> res18.toString
80-
val res19: String = NDouble@3ff00000
80+
val res19: String = "rs$line25ドル$NDouble@3ff00000"
8181

8282
scala> 1.0D
8383
val res20: Double = 1.0
@@ -89,10 +89,10 @@ scala> NChar('a')
8989
val res21: NChar = NChar@61
9090

9191
scala> res21.toString
92-
val res22: String = NChar@61
92+
val res22: String = "rs$line29ドル$NChar@61"
9393

9494
scala> 'a'
95-
val res23: Char = a
95+
val res23: Char = 'a'
9696

9797
scala> class NString(val x: String) extends AnyVal
9898
// defined class NString
@@ -101,10 +101,10 @@ scala> NString("test")
101101
val res24: NString = NString@364492
102102

103103
scala> res24.toString
104-
val res25: String = NString@364492
104+
val res25: String = "rs$line33ドル$NString@364492"
105105

106106
scala> "test"
107-
val res26: String = test
107+
val res26: String = "test"
108108

109109
scala> class CustomToString(val x: Int) extends AnyVal { override def toString(): String = s"Test$x" }
110110
// defined class CustomToString
@@ -113,7 +113,7 @@ scala> CustomToString(23)
113113
val res27: CustomToString = Test23
114114

115115
scala> res27.toString
116-
val res28: String = Test23
116+
val res28: String = "Test23"
117117

118118
scala> class `<>`(x: Int) extends AnyVal
119119
// defined class <>
@@ -122,7 +122,7 @@ scala> `<>`(23)
122122
val res29: <> = less$greater@17
123123

124124
scala> res29.toString
125-
val res30: String = less$greater@17
125+
val res30: String = "rs$line40ドル$$less$greater@17"
126126

127127
scala> class `🤪`(x: Int) extends AnyVal
128128
// defined class 🤪
@@ -131,7 +131,7 @@ scala> `🤪`(23)
131131
val res31: 🤪 = uD83E$uDD2A@17
132132

133133
scala> res31.toString
134-
val res32: String = uD83E$uDD2A@17
134+
val res32: String = "rs$line43ドル$$uD83E$uDD2A@17"
135135

136136
scala> object Outer { class Foo(x: Int) extends AnyVal }
137137
// defined object Outer
@@ -140,7 +140,7 @@ scala> Outer.Foo(23)
140140
val res33: Outer.Foo = Outer$Foo@17
141141

142142
scala> res33.toString
143-
val res34: String = Outer$Foo@17
143+
val res34: String = "rs$line46ドル$Outer$Foo@17"
144144

145145
scala> Vector.unapplySeq(Vector(2))
146146
val res35: scala.collection.SeqFactory.UnapplySeqWrapper[Int] = scala.collection.SeqFactory$UnapplySeqWrapper@df507bfd

‎compiler/test-resources/repl/i18383

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ scala> class Foo { import scala.util.*; println("foo") }
1111
// defined class Foo
1212

1313
scala> { import scala.util.*; "foo" }
14-
val res0: String = foo
14+
val res0: String = "foo"

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /