Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ object SyntaxHighlighting {
val CommentColor: String = Console.BLUE
val KeywordColor: String = Console.YELLOW
val ValDefColor: String = Console.CYAN
val LiteralColor: String = Console.RED
val LiteralColor: String = Console.GREEN
val StringColor: String = Console.GREEN
val TypeColor: String = Console.MAGENTA
val AnnotationColor: String = Console.MAGENTA
Expand Down Expand Up @@ -80,6 +80,9 @@ object SyntaxHighlighting {
case IDENTIFIER if name == nme.??? =>
highlightRange(start, end, Console.RED_B)

case IDENTIFIER if name.head.isUpper && name.exists(!_.isUpper) =>
highlightRange(start, end, KeywordColor)

case _ =>
}
}
Expand Down
53 changes: 1 addition & 52 deletions compiler/src/dotty/tools/repl/Rendering.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):

var myClassLoader: AbstractFileClassLoader = uninitialized

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

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

myClassLoader = new AbstractFileClassLoader(ctx.settings.outputDir.value, parent)
myReplStringOf = {
// We need to use the ScalaRunTime class coming from the scala-library
// on the user classpath, and not the one available in the current
// classloader, so we use reflection instead of simply calling
// `ScalaRunTime.stringOf`. Also probe for new stringOf that does string quoting, etc.
val scalaRuntime = Class.forName("scala.runtime.ScalaRunTime", true, myClassLoader)
val renderer = "stringOf"
val stringOfInvoker: (Object, Int) => String =
def richStringOf: (Object, Int) => String =
val method = scalaRuntime.getMethod(renderer, classOf[Object], classOf[Int], classOf[Boolean])
val richly = java.lang.Boolean.TRUE // add a repl option for enriched output
(value, maxElements) => method.invoke(null, value, maxElements, richly).asInstanceOf[String]
def poorStringOf: (Object, Int) => String =
try
val method = scalaRuntime.getMethod(renderer, classOf[Object], classOf[Int])
(value, maxElements) => method.invoke(null, value, maxElements).asInstanceOf[String]
catch case _: NoSuchMethodException => (value, maxElements) => String.valueOf(value).take(maxElements)
try richStringOf
catch case _: NoSuchMethodException => poorStringOf
def stringOfMaybeTruncated(value: Object, maxElements: Int): String = stringOfInvoker(value, maxElements)

// require value != null
// `ScalaRuntime.stringOf` returns null iff value.toString == null, let caller handle that.
// `ScalaRuntime.stringOf` may truncate the output, in which case we want to indicate that fact to the user
// In order to figure out if it did get truncated, we invoke it twice - once with the `maxElements` that we
// want to print, and once without a limit. If the first is shorter, truncation did occur.
// Note that `stringOf` has new API in flight to handle truncation, see stringOfMaybeTruncated.
(value: Object, maxElements: Int, maxCharacters: Int) =>
stringOfMaybeTruncated(value, Int.MaxValue) match
case null => null
case notTruncated =>
val maybeTruncated =
val maybeTruncatedByElementCount = stringOfMaybeTruncated(value, maxElements)
truncate(maybeTruncatedByElementCount, maxCharacters)
// our string representation may have been truncated by element and/or character count
// if so, append an info string - but only once
if notTruncated.length == maybeTruncated.length then maybeTruncated
else s"$maybeTruncated ... large output truncated, print value to show all"
}
myClassLoader
}

Expand All @@ -95,17 +54,7 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):

/** Return a String representation of a value we got from `classLoader()`. */
private[repl] def replStringOf(sym: Symbol, value: Object)(using Context): String =
assert(myReplStringOf != null,
"replStringOf should only be called on values creating using `classLoader()`, but `classLoader()` has not been called so far")
val maxPrintElements = ctx.settings.VreplMaxPrintElements.valueIn(ctx.settingsState)
val maxPrintCharacters = ctx.settings.VreplMaxPrintCharacters.valueIn(ctx.settingsState)
// stringOf returns null if value.toString returns null. Show some text as a fallback.
def fallback = s"""null // result of "${sym.name}.toString" is null"""
if value == null then "null" else
myReplStringOf(value, maxPrintElements, maxPrintCharacters) match
case null => fallback
case res => res
end if
dotty.shaded.pprint.PPrinter.BlackWhite.apply(value).plainText

/** Load the value of the symbol using reflection.
*
Expand Down
2 changes: 1 addition & 1 deletion compiler/test-resources/repl/i18383
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ scala> class Foo { import scala.util.*; println("foo") }
// defined class Foo

scala> { import scala.util.*; "foo" }
val res0: String = foo
val res0: String = "foo"
2 changes: 1 addition & 1 deletion compiler/test-resources/repl/i3388
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
scala> val foo = "1"; foo.toInt
val foo: String = 1
val foo: String = "1"
val res0: Int = 1
10 changes: 5 additions & 5 deletions compiler/test-resources/repl/jar-multiple
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,26 @@ Added 'compiler/test-resources/jars/mylibrary.jar' to classpath.
scala> import mylibrary.Utils

scala> Utils.greet("Alice")
val res0: String = Hello, Alice!
val res0: String = "Hello, Alice!"

scala>:jar compiler/test-resources/jars/mylibrary2.jar
Added 'compiler/test-resources/jars/mylibrary2.jar' to classpath.

scala> import mylibrary2.Utils2

scala> Utils2.greet("Alice")
val res1: String = Greetings, Alice!
val res1: String = "Greetings, Alice!"

scala> Utils.greet("Alice")
val res2: String = Hello, Alice!
val res2: String = "Hello, Alice!"

scala> import mylibrary.Utils.greet

scala> greet("Tom")
val res3: String = Hello, Tom!
val res3: String = "Hello, Tom!"

scala> Utils.greet("Alice")
val res4: String = Hello, Alice!
val res4: String = "Hello, Alice!"

scala> z
val res5: Int = 1
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class SyntaxHighlightingTests extends DottyTest {
test("val foo = 123", "<K|val> <V|foo> = <L|123>")
test(
"val foo: List[List[Int]] = List(List(1))",
"<K|val> <V|foo>: <T|List>[<T|List>[<T|Int>]] = List(List(<L|1>))"
"<K|val> <V|foo>: <T|List>[<T|List>[<T|Int>]] = <T|List>(<T|List>(<L|1>))"
)

test("var", "<K|var>")
Expand Down
2 changes: 1 addition & 1 deletion compiler/test/dotty/tools/repl/LoadTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class LoadTests extends ReplTest {
|def helloWorld: String
|""".stripMargin,
runCode = "helloWorld",
output = """|val res0: String = Hello, World!
output = """|val res0: String = "Hello, World!"
|""".stripMargin
)

Expand Down
4 changes: 1 addition & 3 deletions compiler/test/dotty/tools/repl/ReplCompilerTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ class ReplCompilerTests extends ReplTest:

@Test def testSingletonPrint = initially {
run("""val a = "hello"; val x: a.type = a""")
assertMultiLineEquals("val a: String = hello\nval x: a.type = hello", storedOutput().trim)
assertMultiLineEquals("val a: String = \"hello\"\nval x: a.type = \"hello\"", storedOutput().trim)
}

@Test def i6574 = initially {
Expand Down Expand Up @@ -445,7 +445,6 @@ class ReplCompilerTests extends ReplTest:
.andThen:
val last = lines().last
assertTrue(last, last.startsWith("val tpolecat: Object = null"))
assertTrue(last, last.endsWith("""// result of "tpolecat.toString" is null"""))

@Test def `i17333 print toplevel object with null toString`: Unit =
initially:
Expand All @@ -454,7 +453,6 @@ class ReplCompilerTests extends ReplTest:
run("tpolecat")
val last = lines().last
assertTrue(last, last.startsWith("val res0: tpolecat.type = null"))
assertTrue(last, last.endsWith("""// result of "res0.toString" is null"""))

@Test def `i21431 filter out best effort options`: Unit =
initially:
Expand Down
2 changes: 1 addition & 1 deletion compiler/test/dotty/tools/repl/ShadowingTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class ShadowingTests extends ReplTest(options = ShadowingTests.options):
testScript(name = "<shadow-subdir-x>",
"""|scala> val (x, y) = (42, "foo")
|val x: Int = 42
|val y: String = foo
|val y: String = "foo"
|
|scala> if (true) x else y
|val res0: Int | String = 42
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class WorksheetTest {
case _ => "odd"
}${m2}"""
.run(m1,
((m1 to m2), "val res0: String = odd"))
((m1 to m2), "val res0: String = \"odd\""))
}

@Test def evaluationException: Unit = {
Expand Down
92 changes: 92 additions & 0 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,94 @@ object Build {
recur(lines)
}

val shadedSourceGenerator = (Compile / sourceGenerators) += Def.task {
val downloads = Seq(
"https://repo1.maven.org/maven2/com/lihaoyi/pprint_3/0.9.3/pprint_3-0.9.3-sources.jar",
"https://repo1.maven.org/maven2/com/lihaoyi/fansi_3/0.5.1/fansi_3-0.5.1-sources.jar",
"https://repo1.maven.org/maven2/com/lihaoyi/sourcecode_3/0.4.3-M5/sourcecode_3-0.4.3-M5-sources.jar",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...wait, is 0.4.3-M5 a stable version? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might want to bring it to stable before we depend on it in the compiler repo 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's almost a year old, so I guess so haha. I can tag a stable version if you would like, but the contents of the sourcejar will be unchanged

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's become stabilised, by all means. 👍
I'd rather avoid milestone versions here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @Gedochao. A stable version should be used here. Also, @lihaoyi what are the versioning scheme these 3 libraries follow? I'm not a fan of cloning the sources and change the package name. I prefer to just have a dependency and use the actual library (which we do for jline and will soon do for asm too).

Copy link
Contributor Author

@lihaoyi lihaoyi Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, others have raised concerns in the past about using Scala libraries in the compiler codebase affecting the bootstrapping process. By building from source, we treat it effectively as Dotty's own source files, removing any divergence in the code paths: they are treated identically to scala3's own sources. If scala3 can compile itself, it should be able to compile these sources without issue

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Why has it been downloaded every time?
  2. Seems no checkmd5?
  3. Extract the common version to fields?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should indeed compile these from sources. We can depend on binaries for Java libraries (hence jline and asm are fine), but not for Scala libraries.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should indeed compile these from sources. We can depend on binaries for Java libraries (hence jline and asm are fine), but not for Scala libraries.

Could you explain why? I thought Scala is maintaining backwards binary/tasty compatibility. Doesn't that mean we shohld always be able to depend on older scala 3 jars in the scala3 compiler regardless of how kuch bootstrapping we do?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because circular dependencies are evil. They're not too evil across time (Av2 -> Bv1 -> Av1), but they're still difficult to reason about.

And even though Scala 3 will forever be backward compat, an eventual Scala 4 wouldn't. We shouldn't paint our build into a corner. Scala 2 tried this several times over its lifetime, and rolled back every time. It's a massive pain every time it happens. There would need to be a huge upside to depending on a binary for that to be offset.

)
val dest = ((Compile / sourceManaged).value / "downloaded").toPath
if (Files.exists(dest)) {
Files.walk(dest)
.sorted(java.util.Comparator.reverseOrder()) // delete children before parents
.forEach(p => Files.delete(p));
}
Files.createDirectories(dest)

for(url <- downloads) {
import java.io._
import java.net.{HttpURLConnection, URL}
import java.nio.file._
import java.nio.file.attribute.FileTime
import java.util.zip.{ZipEntry, ZipInputStream}

val conn = new URL(url).openConnection().asInstanceOf[HttpURLConnection]
conn.setInstanceFollowRedirects(true)
conn.setConnectTimeout(15000)
conn.setReadTimeout(60000)
conn.setRequestMethod("GET")

var in: InputStream = null
var zis: ZipInputStream = null
try {
in = new BufferedInputStream(conn.getInputStream)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the using resource API here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If @hamzaremmal is bumping the required version to Java 17, I'm hoping to delete al of this and it'll collapse down to a single line

zis = new ZipInputStream(in)

var entry: ZipEntry = zis.getNextEntry
val buffer = new Array[Byte](8192)

while (entry != null) {
val target = dest.resolve(entry.getName).normalize()
if (entry.isDirectory) Files.createDirectories(target)
else {
Files.createDirectories(target.getParent)
var out: OutputStream = null
try {
out = new BufferedOutputStream(Files.newOutputStream(target, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))
var n = zis.read(buffer)
while (n != -1) {
out.write(buffer, 0, n)
n = zis.read(buffer)
}
} finally if (out != null) out.close()
}

zis.closeEntry()
entry = zis.getNextEntry
}
} finally {
if (zis != null) zis.close()
if (in != null) in.close()
conn.disconnect()
}
}

import collection.JavaConverters._
Files.walk(dest)
.filter(p => p.toString().endsWith(".scala"))
.map[java.io.File] { (file: java.nio.file.Path) =>
val text = new String(Files.readAllBytes(file.path), java.nio.charset.StandardCharsets.UTF_8)
if (!file.getFileName().toString().equals("CollectionName.scala")) Files.write(
file,
("package dotty.shaded\n" +
text
.replace("import scala", "import _root_.scala")
.replace(" scala.collection.", " _root_.scala.collection.")
.replace("_root_.pprint", "_root_.dotty.shaded.pprint")
.replace("_root_.fansi", "_root_.dotty.shaded.fansi")
.replace("def apply(c: Char): Trie[T]", "def apply(c: Char): Trie[T] | Null")
.replace("var head: Iterator[T] = null", "var head: Iterator[T] | Null = null")
.replace("if (head != null && head.hasNext) true", "if (head != null && head.nn.hasNext) true")
.replace("head.next()", "head.nn.next()")
.replace("abstract class Walker", "@scala.annotation.nowarn abstract class Walker")
.replace("object TPrintLowPri", "@scala.annotation.nowarn object TPrintLowPri")).getBytes
)
file.toFile

}
.collect(java.util.stream.Collectors.toList()).asScala.toSeq

}.taskValue
// Settings shared between scala3-compiler and scala3-compiler-bootstrapped
lazy val commonDottyCompilerSettings = Seq(
// Note: bench/profiles/projects.yml should be updated accordingly.
Expand Down Expand Up @@ -721,6 +809,8 @@ object Build {
("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13),
),

shadedSourceGenerator,

// For convenience, change the baseDirectory when running the compiler
Compile / forkOptions := (Compile / forkOptions).value.withWorkingDirectory((ThisBuild / baseDirectory).value),
Compile / run / forkOptions := (Compile / run / forkOptions).value.withWorkingDirectory((ThisBuild / baseDirectory).value),
Expand Down Expand Up @@ -2141,6 +2231,7 @@ object Build {

Seq(file)
}.taskValue,
shadedSourceGenerator,
// sbt adds all the projects to scala-tool config which breaks building the scalaInstance
// as a workaround, I build it manually by only adding the compiler
scalaInstance := {
Expand Down Expand Up @@ -2330,6 +2421,7 @@ object Build {
sjsSources
} (Set(scalaJSIRSourcesJar)).toSeq
}.taskValue,
shadedSourceGenerator
)

// ==============================================================================================
Expand Down
2 changes: 1 addition & 1 deletion staging/test-resources/repl-staging/i6007
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ def compiler: scala.quoted.staging.Compiler
scala> def v(using Quotes) = '{ (if true then Some(1) else None).map(v => v+1) }
def v(using x$1: scala.quoted.Quotes): scala.quoted.Expr[Option[Int]]
scala> scala.quoted.staging.withQuotes(v.show)
val res0: String = (if (true) scala.Some.apply[scala.Int](1) else scala.None).map[scala.Int](((v: scala.Int) => v.+(1)))
val res0: String = "(if (true) scala.Some.apply[scala.Int](1) else scala.None).map[scala.Int](((v: scala.Int) => v.+(1)))"
scala> scala.quoted.staging.run(v)
val res1: Option[Int] = Some(2)
Loading