From 22d77f160bc0d0f15f967f163b85fd08e2df1602 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 26 Oct 2025 10:40:07 +0800 Subject: [PATCH 01/42] . --- .../src/dotty/tools/repl/ReplDriver.scala | 14 +++- compiler/src/dotty/tools/repl/ReplMain.scala | 60 +++++++++++++++ .../test/dotty/tools/repl/ReplMainTest.scala | 73 +++++++++++++++++++ sbt-bridge/src/xsbt/ConsoleInterface.java | 2 +- 4 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 compiler/src/dotty/tools/repl/ReplMain.scala create mode 100644 compiler/test/dotty/tools/repl/ReplMainTest.scala diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index ffa1b648446d..441ece19262e 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -168,9 +168,12 @@ class ReplDriver(settings: Array[String], * observable outside of the CLI, for this reason, most helper methods are * `protected final` to facilitate testing. */ - def runUntilQuit(using initialState: State = initialState)(): State = { + def runUntilQuit(using initialState: State = initialState)(hardcodedInput: java.io.InputStream = null): State = { val terminal = new JLineTerminal + val hardcodedInputLines = + if (hardcodedInput == null) null + else new java.io.BufferedReader(new java.io.InputStreamReader(hardcodedInput)) out.println( s"""Welcome to Scala $simpleVersionString ($javaVersion, Java $javaVmName). |Type in expressions for evaluation. Or try :help.""".stripMargin) @@ -208,8 +211,13 @@ class ReplDriver(settings: Array[String], } try { - val line = terminal.readLine(completer) - ParseResult(line) + println("hardcodedInputLines " + hardcodedInputLines) + val line = + if (hardcodedInputLines != null) hardcodedInputLines.readLine() + else terminal.readLine(completer) + println("line " + line) + if (line == null) Quit + else ParseResult(line) } catch { case _: EndOfFileException => // Ctrl+D Quit diff --git a/compiler/src/dotty/tools/repl/ReplMain.scala b/compiler/src/dotty/tools/repl/ReplMain.scala new file mode 100644 index 000000000000..3cf514502392 --- /dev/null +++ b/compiler/src/dotty/tools/repl/ReplMain.scala @@ -0,0 +1,60 @@ +package dotty.tools.repl + +import java.io.PrintStream + +class ReplMain( + settings: Array[String] = Array.empty, + out: PrintStream = Console.out, + classLoader: Option[ClassLoader] = Some(getClass.getClassLoader), + predefCode: String = "", + testCode: String = "" +): + def run(bindings: ReplMain.Bind[_]*): Any = + try + ReplMain.currentBindings.set(bindings.map{bind => bind.name -> bind.value}.toMap) + + val bindingsPredef = bindings + .map { case bind => + s"def ${bind.name}: ${bind.typeName.value} = dotty.tools.repl.ReplMain.currentBinding[${bind.typeName.value}](\"${bind.name}\")" + } + .mkString("\n") + + val fullPredef = + ReplDriver.pprintImport + + (if bindingsPredef.nonEmpty then s"\n$bindingsPredef\n" else "") + + (if predefCode.nonEmpty then s"\n$predefCode\n" else "") + + val driver = new ReplDriver(settings, out, classLoader, fullPredef) + + if (testCode == "") driver.tryRunning + else { + driver.runUntilQuit(using driver.initialState)(new java.io.ByteArrayInputStream(testCode.getBytes())) + } + () + finally + ReplMain.currentBindings.set(null) + + +object ReplMain: + final case class TypeName[A](value: String) + object TypeName extends TypeNamePlatform + + import scala.quoted._ + + trait TypeNamePlatform: + inline given [A]: TypeName[A] = ${TypeNamePlatform.impl[A]} + + object TypeNamePlatform: + def impl[A](using t: Type[A], ctx: Quotes): Expr[TypeName[A]] = + '{TypeName[A](${Expr(Type.show[A])})} + + + case class Bind[T](name: String, value: T)(implicit val typeName: TypeName[T]) + object Bind: + implicit def ammoniteReplArrowBinder[T](t: (String, T))(implicit typeName: TypeName[T]): Bind[T] = { + Bind(t._1, t._2)(typeName) + } + + def currentBinding[T](s: String): T = currentBindings.get().apply(s).asInstanceOf[T] + + private val currentBindings = new ThreadLocal[Map[String, Any]]() diff --git a/compiler/test/dotty/tools/repl/ReplMainTest.scala b/compiler/test/dotty/tools/repl/ReplMainTest.scala new file mode 100644 index 000000000000..495d688f035d --- /dev/null +++ b/compiler/test/dotty/tools/repl/ReplMainTest.scala @@ -0,0 +1,73 @@ +package dotty.tools +package repl + +import scala.language.unsafeNulls + +import java.io.{ByteArrayOutputStream, PrintStream} +import java.nio.charset.StandardCharsets + +import vulpix.TestConfiguration +import org.junit.Test +import org.junit.Assert._ + +/** Tests for the programmatic REPL API (ReplMain) */ +class ReplMainTest: + + private val defaultOptions = Array("-classpath", TestConfiguration.withCompilerClasspath) + + private def captureOutput(body: PrintStream => Unit): String = + val out = new ByteArrayOutputStream() + val ps = new PrintStream(out, true, StandardCharsets.UTF_8.name) + body(ps) + dotty.shaded.fansi.Str(out.toString(StandardCharsets.UTF_8.name)).plainText + + @Test def basicBinding(): Unit = + val output = captureOutput { out => + val replMain = new ReplMain( + settings = defaultOptions, + out = out, + testCode = "test" + ) + + replMain.run("test" -> 42) + } + + assertTrue(output.contains("val res0: Int = 42")) + + @Test def multipleBindings(): Unit = + val output = captureOutput { out => + val replMain = new ReplMain( + settings = defaultOptions, + out = out, + testCode = "x\ny\nz" + ) + + replMain.run( + "x" -> 1, + "y" -> "hello", + "z" -> true + ) + } + + assertTrue(output.contains("val res0: Int = 1")) + assertTrue(output.contains("val res1: String = \"hello\"")) + assertTrue(output.contains("val res2: Boolean = true")) + + @Test def bindingTypes(): Unit = + val output = captureOutput { out => + val replMain = new ReplMain( + settings = defaultOptions ++ Array("-repl-quit-after-init"), + out = out, + testCode = "list\nmap" + ) + + replMain.run( + "list" -> List(1, 2, 3), + "map" -> Map(1 -> "hello") + ) + } + + assertTrue(output.contains("val res0: List[Int] = List(1, 2, 3)")) + assertTrue(output.contains("val res1: Map[Int, String] = Map(1 -> \"hello\")")) + +end ReplMainTest diff --git a/sbt-bridge/src/xsbt/ConsoleInterface.java b/sbt-bridge/src/xsbt/ConsoleInterface.java index 3ba4e011c8e3..2f9ac33098d5 100644 --- a/sbt-bridge/src/xsbt/ConsoleInterface.java +++ b/sbt-bridge/src/xsbt/ConsoleInterface.java @@ -49,7 +49,7 @@ public void run( state = driver.run(initialCommands, state); // TODO handle failure during initialisation - state = driver.runUntilQuit(state); + state = driver.runUntilQuit(state, null); driver.run(cleanupCommands, state); } } From 8ec198ffa324691b81a598749acd3288dccf4ff9 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 26 Oct 2025 10:44:31 +0800 Subject: [PATCH 02/42] wip --- compiler/src/dotty/tools/repl/ReplDriver.scala | 3 +-- compiler/src/dotty/tools/repl/ReplMain.scala | 14 +++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 441ece19262e..dd67b43ee0a9 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -211,11 +211,10 @@ class ReplDriver(settings: Array[String], } try { - println("hardcodedInputLines " + hardcodedInputLines) val line = if (hardcodedInputLines != null) hardcodedInputLines.readLine() else terminal.readLine(completer) - println("line " + line) + if (line == null) Quit else ParseResult(line) } catch { diff --git a/compiler/src/dotty/tools/repl/ReplMain.scala b/compiler/src/dotty/tools/repl/ReplMain.scala index 3cf514502392..9d93ee9a6be4 100644 --- a/compiler/src/dotty/tools/repl/ReplMain.scala +++ b/compiler/src/dotty/tools/repl/ReplMain.scala @@ -27,9 +27,9 @@ class ReplMain( val driver = new ReplDriver(settings, out, classLoader, fullPredef) if (testCode == "") driver.tryRunning - else { - driver.runUntilQuit(using driver.initialState)(new java.io.ByteArrayInputStream(testCode.getBytes())) - } + else driver.runUntilQuit(using driver.initialState)( + new java.io.ByteArrayInputStream(testCode.getBytes()) + ) () finally ReplMain.currentBindings.set(null) @@ -49,12 +49,12 @@ object ReplMain: '{TypeName[A](${Expr(Type.show[A])})} - case class Bind[T](name: String, value: T)(implicit val typeName: TypeName[T]) + case class Bind[T](name: String, value: () => T)(implicit val typeName: TypeName[T]) object Bind: implicit def ammoniteReplArrowBinder[T](t: (String, T))(implicit typeName: TypeName[T]): Bind[T] = { - Bind(t._1, t._2)(typeName) + Bind(t._1, () => t._2)(typeName) } - def currentBinding[T](s: String): T = currentBindings.get().apply(s).asInstanceOf[T] + def currentBinding[T](s: String): T = currentBindings.get().apply(s).apply().asInstanceOf[T] - private val currentBindings = new ThreadLocal[Map[String, Any]]() + private val currentBindings = new ThreadLocal[Map[String, () => Any]]() From 8b17db9b142ca288e92e1c193b2e86503a98b1c1 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 10:17:12 -0800 Subject: [PATCH 03/42] wip --- build.sbt | 1 + project/Build.scala | 43 ++++++ project/plugins.sbt | 2 + .../scala/tools/repl/EmbeddedReplMain.scala | 139 ++++++++++++++++++ 4 files changed, 185 insertions(+) create mode 100644 repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala diff --git a/build.sbt b/build.sbt index aedb04e93ec9..10920336da89 100644 --- a/build.sbt +++ b/build.sbt @@ -9,6 +9,7 @@ val `scala3-compiler-nonbootstrapped` = Build.`scala3-compiler-nonbootstrapped` val `scala3-compiler-bootstrapped-new` = Build.`scala3-compiler-bootstrapped-new` val `scala3-repl` = Build.`scala3-repl` +val `scala3-repl-embedded` = Build.`scala3-repl-embedded` // The Standard Library val `scala-library-nonbootstrapped` = Build.`scala-library-nonbootstrapped` diff --git a/project/Build.scala b/project/Build.scala index abcbc2e2ebcc..b79584b75d55 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -26,6 +26,9 @@ import dotty.tools.sbtplugin.ScalaLibraryPlugin import dotty.tools.sbtplugin.ScalaLibraryPlugin.autoImport._ import dotty.tools.sbtplugin.DottyJSPlugin import dotty.tools.sbtplugin.DottyJSPlugin.autoImport._ +import sbtassembly.AssemblyPlugin.autoImport._ +import sbtassembly.{MergeStrategy, PathList} +import com.eed3si9n.jarjarabrams.ShadeRule import sbt.plugins.SbtPlugin import sbt.ScriptedPlugin.autoImport._ @@ -1175,6 +1178,46 @@ object Build { }, ) + lazy val `scala3-repl-embedded` = project.in(file("repl-embedded")) + .dependsOn(`scala3-repl`) + .settings( + name := "scala3-repl-embedded", + moduleName := "scala3-repl-embedded", + version := dottyVersion, + versionScheme := Some("semver-spec"), + scalaVersion := referenceVersion, + crossPaths := true, + autoScalaLibrary := false, + // Source directories + Compile / unmanagedSourceDirectories := Seq(baseDirectory.value / "src"), + // Assembly configuration for shading + assembly / assemblyJarName := s"scala3-repl-embedded-${version.value}.jar", + assembly / mainClass := Some("scala.tools.repl.EmbeddedReplMain"), + // Shading rules: relocate specific packages to dotty.tools.repl.shaded, except scala.*, java.*, javax.* + assembly / assemblyShadeRules := Seq( + ShadeRule.rename("dotty.**" -> "dotty.tools.repl.shaded.dotty.@1").inAll, + ShadeRule.rename("org.**" -> "dotty.tools.repl.shaded.org.@1").inAll, + ShadeRule.rename("com.**" -> "dotty.tools.repl.shaded.com.@1").inAll, + ShadeRule.rename("io.**" -> "dotty.tools.repl.shaded.io.@1").inAll, + ShadeRule.rename("coursier.**" -> "dotty.tools.repl.shaded.coursier.@1").inAll, + ShadeRule.rename("dependency.**" -> "dotty.tools.repl.shaded.dependency.@1").inAll, + ShadeRule.rename("pprint.**" -> "dotty.tools.repl.shaded.pprint.@1").inAll, + ShadeRule.rename("fansi.**" -> "dotty.tools.repl.shaded.fansi.@1").inAll, + ShadeRule.rename("sourcecode.**" -> "dotty.tools.repl.shaded.sourcecode.@1").inAll, + ), + // Merge strategy for assembly + assembly / assemblyMergeStrategy := { + case PathList("META-INF", xs @ _*) => xs match { + case "MANIFEST.MF" :: Nil => MergeStrategy.discard + case _ => MergeStrategy.discard + } + case x if x.endsWith(".proto") => MergeStrategy.first + case x => MergeStrategy.first + }, + // Don't run tests for assembly + assembly / test := {}, + ) + // ============================================================================================== // =================================== SCALA STANDARD LIBRARY =================================== // ============================================================================================== diff --git a/project/plugins.sbt b/project/plugins.sbt index 241dda64f69b..277c5017a061 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -25,3 +25,5 @@ addSbtPlugin("com.gradle" % "sbt-develocity" % "1.3.1") addSbtPlugin("com.gradle" % "sbt-develocity-common-custom-user-data" % "1.1") addSbtPlugin("com.github.sbt" % "sbt-jdi-tools" % "1.2.0") + +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.5") diff --git a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala new file mode 100644 index 000000000000..95f662b39407 --- /dev/null +++ b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -0,0 +1,139 @@ +package scala.tools.repl + +import java.net.{URL, URLClassLoader} +import java.io.InputStream + +/** + * A classloader that remaps shaded classes back to their original package names. + * + * This classloader intercepts class loading requests and remaps them from + * dotty.tools.repl.shaded.* back to their original package names, allowing the + * shaded classes to be loaded as if they were in their original packages. + * + * The scala.* packages are not shaded, so they pass through normally. + */ +class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { + + private val SHADED_PREFIX = "dotty.tools.repl.shaded." + + // Packages that were shaded + private val SHADED_PACKAGES = Seq("dotty.", "org.", "com.", "io.", "coursier.", "dependency.", "pprint.", "fansi.", "sourcecode.") + + override def loadClass(name: String, resolve: Boolean): Class[?] = { + // Check if this is a class from a package we shaded (and not already in the shaded package) + val shouldUnshade = SHADED_PACKAGES.exists(pkg => name.startsWith(pkg)) && + !name.startsWith(SHADED_PREFIX) + + if (shouldUnshade) { + // Try to find the shaded version + val shadedName = SHADED_PREFIX + name + + // First check if we've already loaded this class + val loaded = findLoadedClass(name) + if (loaded != null) return loaded + + try { + // Load the shaded class bytes from parent + val resourceName = shadedName.replace('.', '/') + ".class" + val is = getParent.getResourceAsStream(resourceName) + + if (is != null) { + try { + // Read the class bytes + val bytes = readAllBytes(is) + + // Define the class with the unshaded name + val clazz = defineClass(name, bytes, 0, bytes.length) + if (resolve) resolveClass(clazz) + return clazz + } finally { + is.close() + } + } + } catch { + case _: Exception => // Fall through to parent + } + } + + // For everything else (scala.* and already shaded classes), delegate to parent + super.loadClass(name, resolve) + } + + private def readAllBytes(is: InputStream): Array[Byte] = { + val buffer = new Array[Byte](8192) + var bytesRead = 0 + val baos = new java.io.ByteArrayOutputStream() + + while ({bytesRead = is.read(buffer); bytesRead != -1}) { + baos.write(buffer, 0, bytesRead) + } + + baos.toByteArray() + } +} + +/** + * Main entry point for the embedded shaded REPL. + * + * This creates an isolated classloader that loads the shaded REPL classes + * as if they were unshaded, instantiates a ReplDriver, and runs it. + */ +object EmbeddedReplMain { + def main(args: Array[String]): Unit = { + // Get the location of the current jar to use as classpath + val codeSource = getClass.getProtectionDomain.getCodeSource + val jarPath = if (codeSource != null) { + val location = codeSource.getLocation + if (location.getProtocol == "file") { + new java.io.File(location.toURI).getAbsolutePath + } else { + location.toString + } + } else { + // Fallback: try to extract from classpath + System.getProperty("java.class.path") + } + + // Add -classpath argument pointing to the shaded jar itself + // This allows the ReplDriver's compiler to find scala.* classes + val argsWithClasspath = if (args.exists(arg => arg == "-classpath" || arg == "-cp")) { + args // Already has classpath + } else { + Array("-classpath", jarPath) ++ args + } + + // Create the unshading classloader with the current classloader as parent + // This ensures it has access to all dependencies in the shaded jar + val unshadingClassLoader = new UnshadingClassLoader( + getClass.getClassLoader // Use current classloader to access all dependencies + ) + + // Load the ReplDriver class through the unshading classloader + val replDriverClass = unshadingClassLoader.loadClass("dotty.tools.repl.ReplDriver") + + // Get the constructor: ReplDriver(Array[String], PrintStream, Option[ClassLoader], String) + val constructor = replDriverClass.getConstructors()(0) + + // Create an Option[ClassLoader] containing the external classloader + val scalaOptionClass = unshadingClassLoader.loadClass("scala.Option") + val scalaOptionModule = unshadingClassLoader.loadClass("scala.Option$") + val someMethod = scalaOptionModule.getField("MODULE$").get(null) + .asInstanceOf[Object].getClass.getMethod("apply", classOf[Object]) + val classLoaderOption = someMethod.invoke( + scalaOptionModule.getField("MODULE$").get(null), + getClass.getClassLoader // Pass the external classloader + ) + + // Create the ReplDriver instance with classpath argument + val replDriver = constructor.newInstance( + argsWithClasspath, // settings: Array[String] (now includes -classpath) + System.out, // out: PrintStream + classLoaderOption, // classLoader: Option[ClassLoader] + "" // extraPredef: String + ) + + // Call tryRunning on the ReplDriver + val tryRunningMethod = replDriverClass.getMethod("tryRunning") + tryRunningMethod.invoke(replDriver) + } +} From 817e279ce11cadffdf12d9f2c6e5ee34beb90305 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 10:26:43 -0800 Subject: [PATCH 04/42] wip --- project/Build.scala | 17 ++++++++++++++++- .../src/scala/tools/repl/EmbeddedReplMain.scala | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index b79584b75d55..a94a05b229d2 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1177,9 +1177,10 @@ object Build { (Compile / run).toTask(" -usejavacp").value }, ) - + lazy val `scala3-repl-embedded` = project.in(file("repl-embedded")) .dependsOn(`scala3-repl`) + .settings(publishSettings) .settings( name := "scala3-repl-embedded", moduleName := "scala3-repl-embedded", @@ -1200,10 +1201,12 @@ object Build { ShadeRule.rename("com.**" -> "dotty.tools.repl.shaded.com.@1").inAll, ShadeRule.rename("io.**" -> "dotty.tools.repl.shaded.io.@1").inAll, ShadeRule.rename("coursier.**" -> "dotty.tools.repl.shaded.coursier.@1").inAll, + ShadeRule.rename("coursierapi.**" -> "dotty.tools.repl.shaded.coursierapi.@1").inAll, ShadeRule.rename("dependency.**" -> "dotty.tools.repl.shaded.dependency.@1").inAll, ShadeRule.rename("pprint.**" -> "dotty.tools.repl.shaded.pprint.@1").inAll, ShadeRule.rename("fansi.**" -> "dotty.tools.repl.shaded.fansi.@1").inAll, ShadeRule.rename("sourcecode.**" -> "dotty.tools.repl.shaded.sourcecode.@1").inAll, + ShadeRule.rename("xsbti.**" -> "dotty.tools.repl.shaded.xsbti.@1").inAll, ), // Merge strategy for assembly assembly / assemblyMergeStrategy := { @@ -1216,6 +1219,18 @@ object Build { }, // Don't run tests for assembly assembly / test := {}, + // Publishing configuration: publish the assembly jar instead of regular jar + Compile / packageBin := assembly.value, + Compile / packageBin / artifact := { + val art = (Compile / packageBin / artifact).value + art.withClassifier(None) + }, + Compile / packageDoc / publishArtifact := false, + Compile / packageSrc / publishArtifact := true, + Test / publishArtifact := false, + publish / skip := false, + // Make assembly jar depend on packageBin in Compile scope + assembly := (assembly dependsOn (Compile / compile)).value, ) // ============================================================================================== diff --git a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala index 95f662b39407..6426e1b97efa 100644 --- a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -17,7 +17,7 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { private val SHADED_PREFIX = "dotty.tools.repl.shaded." // Packages that were shaded - private val SHADED_PACKAGES = Seq("dotty.", "org.", "com.", "io.", "coursier.", "dependency.", "pprint.", "fansi.", "sourcecode.") + private val SHADED_PACKAGES = Seq("dotty.", "org.", "com.", "io.", "coursier.", "coursierapi.", "dependency.", "pprint.", "fansi.", "sourcecode.", "xsbti.") override def loadClass(name: String, resolve: Boolean): Class[?] = { // Check if this is a class from a package we shaded (and not already in the shaded package) From a04c07a0948adc409721b393a89a1b9bd83e52b3 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 10:40:55 -0800 Subject: [PATCH 05/42] cleanup --- .../scala/tools/repl/EmbeddedReplMain.scala | 64 ++++--------------- 1 file changed, 12 insertions(+), 52 deletions(-) diff --git a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala index 6426e1b97efa..7695366463c7 100644 --- a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -25,30 +25,21 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { !name.startsWith(SHADED_PREFIX) if (shouldUnshade) { - // Try to find the shaded version - val shadedName = SHADED_PREFIX + name - - // First check if we've already loaded this class val loaded = findLoadedClass(name) if (loaded != null) return loaded try { // Load the shaded class bytes from parent - val resourceName = shadedName.replace('.', '/') + ".class" - val is = getParent.getResourceAsStream(resourceName) + val is = getParent.getResourceAsStream((SHADED_PREFIX + name).replace('.', '/') + ".class") if (is != null) { try { - // Read the class bytes - val bytes = readAllBytes(is) - + val bytes = is.readAllBytes() // Define the class with the unshaded name val clazz = defineClass(name, bytes, 0, bytes.length) if (resolve) resolveClass(clazz) return clazz - } finally { - is.close() - } + } finally is.close() } } catch { case _: Exception => // Fall through to parent @@ -58,18 +49,6 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { // For everything else (scala.* and already shaded classes), delegate to parent super.loadClass(name, resolve) } - - private def readAllBytes(is: InputStream): Array[Byte] = { - val buffer = new Array[Byte](8192) - var bytesRead = 0 - val baos = new java.io.ByteArrayOutputStream() - - while ({bytesRead = is.read(buffer); bytesRead != -1}) { - baos.write(buffer, 0, bytesRead) - } - - baos.toByteArray() - } } /** @@ -82,17 +61,13 @@ object EmbeddedReplMain { def main(args: Array[String]): Unit = { // Get the location of the current jar to use as classpath val codeSource = getClass.getProtectionDomain.getCodeSource - val jarPath = if (codeSource != null) { - val location = codeSource.getLocation - if (location.getProtocol == "file") { - new java.io.File(location.toURI).getAbsolutePath - } else { - location.toString + val jarPath = + if (codeSource == null) System.getProperty("java.class.path") // Fallback: try to extract from classpath + else { + val location = codeSource.getLocation + if (location.getProtocol == "file") new java.io.File(location.toURI).getAbsolutePath + else location.toString } - } else { - // Fallback: try to extract from classpath - System.getProperty("java.class.path") - } // Add -classpath argument pointing to the shaded jar itself // This allows the ReplDriver's compiler to find scala.* classes @@ -104,31 +79,16 @@ object EmbeddedReplMain { // Create the unshading classloader with the current classloader as parent // This ensures it has access to all dependencies in the shaded jar - val unshadingClassLoader = new UnshadingClassLoader( - getClass.getClassLoader // Use current classloader to access all dependencies - ) + val unshadingClassLoader = new UnshadingClassLoader(getClass.getClassLoader) - // Load the ReplDriver class through the unshading classloader val replDriverClass = unshadingClassLoader.loadClass("dotty.tools.repl.ReplDriver") - - // Get the constructor: ReplDriver(Array[String], PrintStream, Option[ClassLoader], String) - val constructor = replDriverClass.getConstructors()(0) - - // Create an Option[ClassLoader] containing the external classloader - val scalaOptionClass = unshadingClassLoader.loadClass("scala.Option") - val scalaOptionModule = unshadingClassLoader.loadClass("scala.Option$") - val someMethod = scalaOptionModule.getField("MODULE$").get(null) - .asInstanceOf[Object].getClass.getMethod("apply", classOf[Object]) - val classLoaderOption = someMethod.invoke( - scalaOptionModule.getField("MODULE$").get(null), - getClass.getClassLoader // Pass the external classloader - ) + val constructor = replDriverClass.getConstructors().head // Create the ReplDriver instance with classpath argument val replDriver = constructor.newInstance( argsWithClasspath, // settings: Array[String] (now includes -classpath) System.out, // out: PrintStream - classLoaderOption, // classLoader: Option[ClassLoader] + Option(getClass.getClassLoader), // classLoader: Option[ClassLoader] "" // extraPredef: String ) From 04a8d16e98d5c4c668b95c80713476400723d5ee Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 12:46:03 -0800 Subject: [PATCH 06/42] cleanup --- build.sbt | 1 + project/Build.scala | 70 ++++++++++++++++--- .../scala/tools/repl/EmbeddedReplMain.scala | 15 ++-- 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/build.sbt b/build.sbt index 10920336da89..191229ea9971 100644 --- a/build.sbt +++ b/build.sbt @@ -9,6 +9,7 @@ val `scala3-compiler-nonbootstrapped` = Build.`scala3-compiler-nonbootstrapped` val `scala3-compiler-bootstrapped-new` = Build.`scala3-compiler-bootstrapped-new` val `scala3-repl` = Build.`scala3-repl` +val `scala3-repl-shaded` = Build.`scala3-repl-shaded` val `scala3-repl-embedded` = Build.`scala3-repl-embedded` // The Standard Library diff --git a/project/Build.scala b/project/Build.scala index a94a05b229d2..1cdf733fafed 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1178,24 +1178,45 @@ object Build { }, ) - lazy val `scala3-repl-embedded` = project.in(file("repl-embedded")) + lazy val `scala3-repl-shaded` = project.in(file("repl-embedded")) .dependsOn(`scala3-repl`) - .settings(publishSettings) + .enablePlugins(sbtassembly.AssemblyPlugin) .settings( - name := "scala3-repl-embedded", - moduleName := "scala3-repl-embedded", + name := "scala3-repl-shaded", + moduleName := "scala3-repl-shaded", version := dottyVersion, versionScheme := Some("semver-spec"), scalaVersion := referenceVersion, crossPaths := true, autoScalaLibrary := false, + // Expose assembly as packageBin for cross-project reference + Compile / packageBin := assembly.value, // Source directories Compile / unmanagedSourceDirectories := Seq(baseDirectory.value / "src"), + // Configure scalaInstance to use the bootstrapped compiler + scalaInstance := { + val scalaLibrary = (`scala-library-bootstrapped` / Compile / packageBin).value + val tastyCore = (`tasty-core-bootstrapped-new` / Compile / packageBin).value + val scala3Interfaces = (`scala3-interfaces` / Compile / packageBin).value + val scala3Compiler = (`scala3-compiler-bootstrapped-new` / Compile / packageBin).value + val externalCompilerDeps = (`scala3-compiler-bootstrapped-new` / Compile / externalDependencyClasspath).value.map(_.data).toSet + + Defaults.makeScalaInstance( + dottyVersion, + libraryJars = Array(scalaLibrary), + allCompilerJars = Seq(tastyCore, scala3Interfaces, scala3Compiler) ++ externalCompilerDeps, + allDocJars = Seq.empty, + state.value, + scalaInstanceTopLoader.value + ) + }, // Assembly configuration for shading - assembly / assemblyJarName := s"scala3-repl-embedded-${version.value}.jar", + assembly / assemblyJarName := s"scala3-repl-shaded-${version.value}.jar", assembly / mainClass := Some("scala.tools.repl.EmbeddedReplMain"), // Shading rules: relocate specific packages to dotty.tools.repl.shaded, except scala.*, java.*, javax.* + // Keep org.jline unshaded (needs to access native terminal libraries) assembly / assemblyShadeRules := Seq( + ShadeRule.rename("org.jline.**" -> "org.jline.@1").inAll, ShadeRule.rename("dotty.**" -> "dotty.tools.repl.shaded.dotty.@1").inAll, ShadeRule.rename("org.**" -> "dotty.tools.repl.shaded.org.@1").inAll, ShadeRule.rename("com.**" -> "dotty.tools.repl.shaded.com.@1").inAll, @@ -1219,18 +1240,49 @@ object Build { }, // Don't run tests for assembly assembly / test := {}, - // Publishing configuration: publish the assembly jar instead of regular jar - Compile / packageBin := assembly.value, + // Exclude scala-library and jline from assembly (users provide them on classpath) + assembly / assemblyExcludedJars := { + val cp = (assembly / fullClasspath).value + cp.filter { jar => + val name = jar.data.getName + name.startsWith("scala-library") || name.startsWith("scala3-library") || name.startsWith("jline") + } + }, + // Don't publish scala3-repl-shaded - it's an internal build artifact + publish / skip := true, + publishLocal / skip := true, + // Make assembly jar depend on compile + assembly := (assembly dependsOn (Compile / compile)).value, + ) + + lazy val `scala3-repl-embedded` = project.in(file("repl-embedded-publish")) + .dependsOn(`scala-library-bootstrapped`) + .settings(publishSettings) + .settings( + name := "scala3-repl-embedded", + moduleName := "scala3-repl-embedded", + version := dottyVersion, + versionScheme := Some("semver-spec"), + scalaVersion := referenceVersion, + crossPaths := true, + libraryDependencies ++= Seq( + "org.jline" % "jline-reader" % "3.29.0", + "org.jline" % "jline-terminal" % "3.29.0", + "org.jline" % "jline-terminal-jni" % "3.29.0", + ), + // No source files in this project - just publishes the shaded jar + Compile / unmanagedSourceDirectories := Seq.empty, + // Use the shaded assembly jar as our packageBin + Compile / packageBin := (`scala3-repl-shaded` / Compile / packageBin).value, Compile / packageBin / artifact := { val art = (Compile / packageBin / artifact).value art.withClassifier(None) }, + // Publish sources from scala3-repl-shaded Compile / packageDoc / publishArtifact := false, Compile / packageSrc / publishArtifact := true, Test / publishArtifact := false, publish / skip := false, - // Make assembly jar depend on packageBin in Compile scope - assembly := (assembly dependsOn (Compile / compile)).value, ) // ============================================================================================== diff --git a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala index 7695366463c7..91a1cccbe946 100644 --- a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -19,10 +19,15 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { // Packages that were shaded private val SHADED_PACKAGES = Seq("dotty.", "org.", "com.", "io.", "coursier.", "coursierapi.", "dependency.", "pprint.", "fansi.", "sourcecode.", "xsbti.") + // Packages that are NOT shaded (even though they match SHADED_PACKAGES patterns) + private val UNSHADED_PACKAGES = Seq("scala.", "scala.tools.repl.", "org.jline.") + override def loadClass(name: String, resolve: Boolean): Class[?] = { // Check if this is a class from a package we shaded (and not already in the shaded package) + // Also exclude packages that are explicitly not shaded val shouldUnshade = SHADED_PACKAGES.exists(pkg => name.startsWith(pkg)) && - !name.startsWith(SHADED_PREFIX) + !name.startsWith(SHADED_PREFIX) && + !UNSHADED_PACKAGES.exists(pkg => name.startsWith(pkg)) if (shouldUnshade) { val loaded = findLoadedClass(name) @@ -61,13 +66,7 @@ object EmbeddedReplMain { def main(args: Array[String]): Unit = { // Get the location of the current jar to use as classpath val codeSource = getClass.getProtectionDomain.getCodeSource - val jarPath = - if (codeSource == null) System.getProperty("java.class.path") // Fallback: try to extract from classpath - else { - val location = codeSource.getLocation - if (location.getProtocol == "file") new java.io.File(location.toURI).getAbsolutePath - else location.toString - } + val jarPath = System.getProperty("java.class.path") // Add -classpath argument pointing to the shaded jar itself // This allows the ReplDriver's compiler to find scala.* classes From 1276bd00dc8a6ce7de5996c82981523135cac180 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 13:31:35 -0800 Subject: [PATCH 07/42] cleanup --- repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala index 91a1cccbe946..4448e6d30717 100644 --- a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -66,14 +66,13 @@ object EmbeddedReplMain { def main(args: Array[String]): Unit = { // Get the location of the current jar to use as classpath val codeSource = getClass.getProtectionDomain.getCodeSource - val jarPath = System.getProperty("java.class.path") // Add -classpath argument pointing to the shaded jar itself // This allows the ReplDriver's compiler to find scala.* classes val argsWithClasspath = if (args.exists(arg => arg == "-classpath" || arg == "-cp")) { args // Already has classpath } else { - Array("-classpath", jarPath) ++ args + Array("-classpath", System.getProperty("java.class.path")) ++ args } // Create the unshading classloader with the current classloader as parent @@ -91,8 +90,6 @@ object EmbeddedReplMain { "" // extraPredef: String ) - // Call tryRunning on the ReplDriver - val tryRunningMethod = replDriverClass.getMethod("tryRunning") - tryRunningMethod.invoke(replDriver) + replDriverClass.getMethod("tryRunning").invoke(replDriver) } } From b7f7a504fe6da8490087c28bdcce4723165195f4 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 13:31:44 -0800 Subject: [PATCH 08/42] cleanup --- repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala | 5 ----- 1 file changed, 5 deletions(-) diff --git a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala index 4448e6d30717..3bf6a2326df5 100644 --- a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -64,11 +64,6 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { */ object EmbeddedReplMain { def main(args: Array[String]): Unit = { - // Get the location of the current jar to use as classpath - val codeSource = getClass.getProtectionDomain.getCodeSource - - // Add -classpath argument pointing to the shaded jar itself - // This allows the ReplDriver's compiler to find scala.* classes val argsWithClasspath = if (args.exists(arg => arg == "-classpath" || arg == "-cp")) { args // Already has classpath } else { From 0c3e845fae690e89f3e1e1655170e6f5a53eda9e Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 13:33:35 -0800 Subject: [PATCH 09/42] . --- .../scala/tools/repl/EmbeddedReplMain.scala | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala index 3bf6a2326df5..bc81f214181f 100644 --- a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -34,13 +34,11 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { if (loaded != null) return loaded try { - // Load the shaded class bytes from parent val is = getParent.getResourceAsStream((SHADED_PREFIX + name).replace('.', '/') + ".class") if (is != null) { try { val bytes = is.readAllBytes() - // Define the class with the unshaded name val clazz = defineClass(name, bytes, 0, bytes.length) if (resolve) resolveClass(clazz) return clazz @@ -64,27 +62,25 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { */ object EmbeddedReplMain { def main(args: Array[String]): Unit = { - val argsWithClasspath = if (args.exists(arg => arg == "-classpath" || arg == "-cp")) { - args // Already has classpath - } else { - Array("-classpath", System.getProperty("java.class.path")) ++ args - } + val argsWithClasspath = + if (args.exists(arg => arg == "-classpath" || arg == "-cp")) args + else Array("-classpath", System.getProperty("java.class.path")) ++ args - // Create the unshading classloader with the current classloader as parent - // This ensures it has access to all dependencies in the shaded jar val unshadingClassLoader = new UnshadingClassLoader(getClass.getClassLoader) + try { - val replDriverClass = unshadingClassLoader.loadClass("dotty.tools.repl.ReplDriver") - val constructor = replDriverClass.getConstructors().head + val replDriverClass = unshadingClassLoader.loadClass("dotty.tools.repl.ReplDriver") + val constructor = replDriverClass.getConstructors().head - // Create the ReplDriver instance with classpath argument - val replDriver = constructor.newInstance( - argsWithClasspath, // settings: Array[String] (now includes -classpath) - System.out, // out: PrintStream - Option(getClass.getClassLoader), // classLoader: Option[ClassLoader] - "" // extraPredef: String - ) + // Create the ReplDriver instance with classpath argument + val replDriver = constructor.newInstance( + argsWithClasspath, // settings: Array[String] (now includes -classpath) + System.out, // out: PrintStream + Option(getClass.getClassLoader), // classLoader: Option[ClassLoader] + "" // extraPredef: String + ) - replDriverClass.getMethod("tryRunning").invoke(replDriver) + replDriverClass.getMethod("tryRunning").invoke(replDriver) + }finally unshadingClassLoader.close } } From 1d3b2f7c8460223b12a2f39384b3d1d95681ba8a Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 13:50:02 -0800 Subject: [PATCH 10/42] wip --- project/Build.scala | 58 ++++++++++++------ .../scala/tools/repl/EmbeddedReplMain.scala | 59 +++++++++++-------- .../tools/repl/AbstractFileClassLoader.scala | 2 +- 3 files changed, 77 insertions(+), 42 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 1cdf733fafed..c4e0f50da974 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1212,23 +1212,6 @@ object Build { }, // Assembly configuration for shading assembly / assemblyJarName := s"scala3-repl-shaded-${version.value}.jar", - assembly / mainClass := Some("scala.tools.repl.EmbeddedReplMain"), - // Shading rules: relocate specific packages to dotty.tools.repl.shaded, except scala.*, java.*, javax.* - // Keep org.jline unshaded (needs to access native terminal libraries) - assembly / assemblyShadeRules := Seq( - ShadeRule.rename("org.jline.**" -> "org.jline.@1").inAll, - ShadeRule.rename("dotty.**" -> "dotty.tools.repl.shaded.dotty.@1").inAll, - ShadeRule.rename("org.**" -> "dotty.tools.repl.shaded.org.@1").inAll, - ShadeRule.rename("com.**" -> "dotty.tools.repl.shaded.com.@1").inAll, - ShadeRule.rename("io.**" -> "dotty.tools.repl.shaded.io.@1").inAll, - ShadeRule.rename("coursier.**" -> "dotty.tools.repl.shaded.coursier.@1").inAll, - ShadeRule.rename("coursierapi.**" -> "dotty.tools.repl.shaded.coursierapi.@1").inAll, - ShadeRule.rename("dependency.**" -> "dotty.tools.repl.shaded.dependency.@1").inAll, - ShadeRule.rename("pprint.**" -> "dotty.tools.repl.shaded.pprint.@1").inAll, - ShadeRule.rename("fansi.**" -> "dotty.tools.repl.shaded.fansi.@1").inAll, - ShadeRule.rename("sourcecode.**" -> "dotty.tools.repl.shaded.sourcecode.@1").inAll, - ShadeRule.rename("xsbti.**" -> "dotty.tools.repl.shaded.xsbti.@1").inAll, - ), // Merge strategy for assembly assembly / assemblyMergeStrategy := { case PathList("META-INF", xs @ _*) => xs match { @@ -1248,6 +1231,47 @@ object Build { name.startsWith("scala-library") || name.startsWith("scala3-library") || name.startsWith("jline") } }, + // Post-process assembly to physically move files into dotty/tools/repl/shaded/ subfolder + assembly := { + val originalJar = assembly.value + val log = streams.value.log + + log.info(s"Post-processing assembly to relocate files into shaded subfolder...") + + // Create a temporary directory for processing + val tmpDir = IO.createTemporaryDirectory + try { + IO.unzip(originalJar, tmpDir) + val shadedDir = tmpDir / "dotty" / "tools" / "repl" / "shaded" + IO.createDirectory(shadedDir) + + (tmpDir ** "*").get.foreach { file => + if (file.isFile) { + val relativePath = file.relativeTo(tmpDir).get.getPath + + // Skip META-INF and scala/tools/repl files + val shouldMove = !relativePath.startsWith("META-INF") && + !relativePath.startsWith("scala/tools/repl/") && + !relativePath.startsWith("dotty/tools/repl/shaded/") + + if (shouldMove) { + val newPath = shadedDir / relativePath + IO.createDirectory(newPath.getParentFile) + IO.move(file, newPath) + } + } + } + + val filesToZip = (tmpDir ** "*").get.filter(_.isFile).map { f => + (f, f.relativeTo(tmpDir).get.getPath) + } + IO.zip(filesToZip, originalJar, None) + + log.info(s"Assembly post-processing complete") + } finally IO.delete(tmpDir) + + originalJar + }, // Don't publish scala3-repl-shaded - it's an internal build artifact publish / skip := true, publishLocal / skip := true, diff --git a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala index bc81f214181f..d12304136ae6 100644 --- a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -16,25 +16,26 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { private val SHADED_PREFIX = "dotty.tools.repl.shaded." - // Packages that were shaded private val SHADED_PACKAGES = Seq("dotty.", "org.", "com.", "io.", "coursier.", "coursierapi.", "dependency.", "pprint.", "fansi.", "sourcecode.", "xsbti.") - // Packages that are NOT shaded (even though they match SHADED_PACKAGES patterns) private val UNSHADED_PACKAGES = Seq("scala.", "scala.tools.repl.", "org.jline.") - override def loadClass(name: String, resolve: Boolean): Class[?] = { - // Check if this is a class from a package we shaded (and not already in the shaded package) - // Also exclude packages that are explicitly not shaded - val shouldUnshade = SHADED_PACKAGES.exists(pkg => name.startsWith(pkg)) && - !name.startsWith(SHADED_PREFIX) && - !UNSHADED_PACKAGES.exists(pkg => name.startsWith(pkg)) + /** Check if a class/resource name should be loaded from the shaded location */ + private def shouldUnshade(name: String): Boolean = { + SHADED_PACKAGES.exists(pkg => name.startsWith(pkg)) && + !name.startsWith(SHADED_PREFIX) && + !UNSHADED_PACKAGES.exists(pkg => name.startsWith(pkg)) || + name.startsWith("scala.tools.asm") + } - if (shouldUnshade) { + override def loadClass(name: String, resolve: Boolean): Class[?] = { + if (shouldUnshade(name)) { val loaded = findLoadedClass(name) if (loaded != null) return loaded try { - val is = getParent.getResourceAsStream((SHADED_PREFIX + name).replace('.', '/') + ".class") + val shadedPath = (SHADED_PREFIX + name).replace('.', '/') + ".class" + val is = getParent.getResourceAsStream(shadedPath) if (is != null) { try { @@ -49,9 +50,20 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { } } - // For everything else (scala.* and already shaded classes), delegate to parent super.loadClass(name, resolve) } + + override def getResourceAsStream(name: String): InputStream | Null = { + val nameAsDots = name.replace('/', '.') + + if (shouldUnshade(nameAsDots)) { + val shadedPath = SHADED_PREFIX.replace('.', '/') + name + val shadedStream = super.getResourceAsStream(shadedPath) + if (shadedStream != null) return shadedStream + } + + super.getResourceAsStream(name) + } } /** @@ -64,23 +76,22 @@ object EmbeddedReplMain { def main(args: Array[String]): Unit = { val argsWithClasspath = if (args.exists(arg => arg == "-classpath" || arg == "-cp")) args - else Array("-classpath", System.getProperty("java.class.path")) ++ args + else Array("-classpath", System.getProperty("java.class.path")) ++ args val unshadingClassLoader = new UnshadingClassLoader(getClass.getClassLoader) - try { - val replDriverClass = unshadingClassLoader.loadClass("dotty.tools.repl.ReplDriver") - val constructor = replDriverClass.getConstructors().head + val replDriverClass = unshadingClassLoader.loadClass("dotty.tools.repl.ReplDriver") + val constructor = replDriverClass.getConstructors().head + + // Create the ReplDriver instance with classpath argument + val replDriver = constructor.newInstance( + argsWithClasspath, // settings: Array[String] (now includes -classpath) + System.out, // out: PrintStream + Option(getClass.getClassLoader), // classLoader: Option[ClassLoader] + "" // extraPredef: String + ) - // Create the ReplDriver instance with classpath argument - val replDriver = constructor.newInstance( - argsWithClasspath, // settings: Array[String] (now includes -classpath) - System.out, // out: PrintStream - Option(getClass.getClassLoader), // classLoader: Option[ClassLoader] - "" // extraPredef: String - ) + replDriverClass.getMethod("tryRunning").invoke(replDriver) - replDriverClass.getMethod("tryRunning").invoke(replDriver) - }finally unshadingClassLoader.close } } diff --git a/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala b/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala index 1bde97621897..80a0367279fd 100644 --- a/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala +++ b/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala @@ -91,7 +91,7 @@ class AbstractFileClassLoader(val root: AbstractFile, parent: ClassLoader, inter try val bytes = is.readAllBytes() defineClass(name, bytes, 0, bytes.length) - finally is.close() + finally Option(is).foreach(_.close()) case _ => try findClass(name) From 0a499d5cac58cf7e75bc6a56c6f87f2a5c664359 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 14:07:35 -0800 Subject: [PATCH 11/42] wip --- repl/src/dotty/tools/repl/AbstractFileClassLoader.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala b/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala index 80a0367279fd..1bde97621897 100644 --- a/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala +++ b/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala @@ -91,7 +91,7 @@ class AbstractFileClassLoader(val root: AbstractFile, parent: ClassLoader, inter try val bytes = is.readAllBytes() defineClass(name, bytes, 0, bytes.length) - finally Option(is).foreach(_.close()) + finally is.close() case _ => try findClass(name) From 410d67998fcd834c27ff42159399ba1170423ad9 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 14:57:20 -0800 Subject: [PATCH 12/42] wip --- project/Build.scala | 15 +++-- .../scala/tools/repl/EmbeddedReplMain.scala | 59 +++++++------------ 2 files changed, 29 insertions(+), 45 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index c4e0f50da974..45cfe9d1b9b6 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1228,7 +1228,7 @@ object Build { val cp = (assembly / fullClasspath).value cp.filter { jar => val name = jar.data.getName - name.startsWith("scala-library") || name.startsWith("scala3-library") || name.startsWith("jline") + name.contains("scala-library") || name.contains("scala3-library") || name.contains("jline") } }, // Post-process assembly to physically move files into dotty/tools/repl/shaded/ subfolder @@ -1249,12 +1249,15 @@ object Build { if (file.isFile) { val relativePath = file.relativeTo(tmpDir).get.getPath - // Skip META-INF and scala/tools/repl files - val shouldMove = !relativePath.startsWith("META-INF") && - !relativePath.startsWith("scala/tools/repl/") && - !relativePath.startsWith("dotty/tools/repl/shaded/") + val shouldDelete = + relativePath.startsWith("scala/") && !relativePath.startsWith("scala/tools/") || + relativePath.startsWith("org/jline/") - if (shouldMove) { + val shouldKeepInPlace = relativePath.startsWith("scala/tools/repl/") + + if (shouldDelete) IO.delete(file) + else if (!shouldKeepInPlace) { + // Move everything else to the shaded directory val newPath = shadedDir / relativePath IO.createDirectory(newPath.getParentFile) IO.move(file, newPath) diff --git a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala index d12304136ae6..e687aff5c130 100644 --- a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -16,53 +16,34 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { private val SHADED_PREFIX = "dotty.tools.repl.shaded." - private val SHADED_PACKAGES = Seq("dotty.", "org.", "com.", "io.", "coursier.", "coursierapi.", "dependency.", "pprint.", "fansi.", "sourcecode.", "xsbti.") - - private val UNSHADED_PACKAGES = Seq("scala.", "scala.tools.repl.", "org.jline.") - - /** Check if a class/resource name should be loaded from the shaded location */ - private def shouldUnshade(name: String): Boolean = { - SHADED_PACKAGES.exists(pkg => name.startsWith(pkg)) && - !name.startsWith(SHADED_PREFIX) && - !UNSHADED_PACKAGES.exists(pkg => name.startsWith(pkg)) || - name.startsWith("scala.tools.asm") - } - override def loadClass(name: String, resolve: Boolean): Class[?] = { - if (shouldUnshade(name)) { - val loaded = findLoadedClass(name) - if (loaded != null) return loaded - - try { - val shadedPath = (SHADED_PREFIX + name).replace('.', '/') + ".class" - val is = getParent.getResourceAsStream(shadedPath) - - if (is != null) { - try { - val bytes = is.readAllBytes() - val clazz = defineClass(name, bytes, 0, bytes.length) - if (resolve) resolveClass(clazz) - return clazz - } finally is.close() - } - } catch { - case _: Exception => // Fall through to parent + val loaded = findLoadedClass(name) + if (loaded != null) return loaded + + try { + val shadedPath = (SHADED_PREFIX + name).replace('.', '/') + ".class" + val is = getParent.getResourceAsStream(shadedPath) + + if (is != null) { + try { + val bytes = is.readAllBytes() + val clazz = defineClass(name, bytes, 0, bytes.length) + if (resolve) resolveClass(clazz) + return clazz + } finally is.close() } + } catch { + case _: Exception => // Fall through to parent } super.loadClass(name, resolve) } override def getResourceAsStream(name: String): InputStream | Null = { - val nameAsDots = name.replace('/', '.') - - if (shouldUnshade(nameAsDots)) { - val shadedPath = SHADED_PREFIX.replace('.', '/') + name - val shadedStream = super.getResourceAsStream(shadedPath) - if (shadedStream != null) return shadedStream - } - - super.getResourceAsStream(name) + val shadedPath = SHADED_PREFIX.replace('.', '/') + name + val shadedStream = super.getResourceAsStream(shadedPath) + if (shadedStream != null) return shadedStream + else super.getResourceAsStream(name) } } From 9b686ca0601ddcba8548dcec31bdcb0bd29b76f9 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 14:58:35 -0800 Subject: [PATCH 13/42] wip --- project/Build.scala | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 45cfe9d1b9b6..966ad988def8 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1193,23 +1193,6 @@ object Build { Compile / packageBin := assembly.value, // Source directories Compile / unmanagedSourceDirectories := Seq(baseDirectory.value / "src"), - // Configure scalaInstance to use the bootstrapped compiler - scalaInstance := { - val scalaLibrary = (`scala-library-bootstrapped` / Compile / packageBin).value - val tastyCore = (`tasty-core-bootstrapped-new` / Compile / packageBin).value - val scala3Interfaces = (`scala3-interfaces` / Compile / packageBin).value - val scala3Compiler = (`scala3-compiler-bootstrapped-new` / Compile / packageBin).value - val externalCompilerDeps = (`scala3-compiler-bootstrapped-new` / Compile / externalDependencyClasspath).value.map(_.data).toSet - - Defaults.makeScalaInstance( - dottyVersion, - libraryJars = Array(scalaLibrary), - allCompilerJars = Seq(tastyCore, scala3Interfaces, scala3Compiler) ++ externalCompilerDeps, - allDocJars = Seq.empty, - state.value, - scalaInstanceTopLoader.value - ) - }, // Assembly configuration for shading assembly / assemblyJarName := s"scala3-repl-shaded-${version.value}.jar", // Merge strategy for assembly From 21539f8f20b4e5e4941f076eb6e86124fc46d684 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 15:04:46 -0800 Subject: [PATCH 14/42] wip --- project/Build.scala | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 966ad988def8..642186b3c91e 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1189,8 +1189,6 @@ object Build { scalaVersion := referenceVersion, crossPaths := true, autoScalaLibrary := false, - // Expose assembly as packageBin for cross-project reference - Compile / packageBin := assembly.value, // Source directories Compile / unmanagedSourceDirectories := Seq(baseDirectory.value / "src"), // Assembly configuration for shading @@ -1283,11 +1281,7 @@ object Build { // No source files in this project - just publishes the shaded jar Compile / unmanagedSourceDirectories := Seq.empty, // Use the shaded assembly jar as our packageBin - Compile / packageBin := (`scala3-repl-shaded` / Compile / packageBin).value, - Compile / packageBin / artifact := { - val art = (Compile / packageBin / artifact).value - art.withClassifier(None) - }, + Compile / packageBin := (`scala3-repl-shaded` / Compile / assembly).value, // Publish sources from scala3-repl-shaded Compile / packageDoc / publishArtifact := false, Compile / packageSrc / publishArtifact := true, From a5c0f83568a8c9c2a508e5597c3d311f65ed58ec Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 15:06:09 -0800 Subject: [PATCH 15/42] wip --- project/Build.scala | 9 --------- 1 file changed, 9 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 642186b3c91e..50fedb7aa295 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1193,15 +1193,6 @@ object Build { Compile / unmanagedSourceDirectories := Seq(baseDirectory.value / "src"), // Assembly configuration for shading assembly / assemblyJarName := s"scala3-repl-shaded-${version.value}.jar", - // Merge strategy for assembly - assembly / assemblyMergeStrategy := { - case PathList("META-INF", xs @ _*) => xs match { - case "MANIFEST.MF" :: Nil => MergeStrategy.discard - case _ => MergeStrategy.discard - } - case x if x.endsWith(".proto") => MergeStrategy.first - case x => MergeStrategy.first - }, // Don't run tests for assembly assembly / test := {}, // Exclude scala-library and jline from assembly (users provide them on classpath) From 94f34c1b061ce87840617fe3d61305c9f4d47445 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 15:44:05 -0800 Subject: [PATCH 16/42] wip --- project/Build.scala | 9 ++------- .../src/scala/tools/repl/EmbeddedReplMain.scala | 0 2 files changed, 2 insertions(+), 7 deletions(-) rename {repl-embedded => repl-shaded}/src/scala/tools/repl/EmbeddedReplMain.scala (100%) diff --git a/project/Build.scala b/project/Build.scala index 50fedb7aa295..c0e8dc9ffd0b 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1178,7 +1178,7 @@ object Build { }, ) - lazy val `scala3-repl-shaded` = project.in(file("repl-embedded")) + lazy val `scala3-repl-shaded` = project.in(file("repl-shaded")) .dependsOn(`scala3-repl`) .enablePlugins(sbtassembly.AssemblyPlugin) .settings( @@ -1250,11 +1250,9 @@ object Build { // Don't publish scala3-repl-shaded - it's an internal build artifact publish / skip := true, publishLocal / skip := true, - // Make assembly jar depend on compile - assembly := (assembly dependsOn (Compile / compile)).value, ) - lazy val `scala3-repl-embedded` = project.in(file("repl-embedded-publish")) + lazy val `scala3-repl-embedded` = project.in(file("repl-embedded")) .dependsOn(`scala-library-bootstrapped`) .settings(publishSettings) .settings( @@ -1273,9 +1271,6 @@ object Build { Compile / unmanagedSourceDirectories := Seq.empty, // Use the shaded assembly jar as our packageBin Compile / packageBin := (`scala3-repl-shaded` / Compile / assembly).value, - // Publish sources from scala3-repl-shaded - Compile / packageDoc / publishArtifact := false, - Compile / packageSrc / publishArtifact := true, Test / publishArtifact := false, publish / skip := false, ) diff --git a/repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala similarity index 100% rename from repl-embedded/src/scala/tools/repl/EmbeddedReplMain.scala rename to repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala From 0d913362d72399fbb79094b9bf92454456e03c36 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 16:05:37 -0800 Subject: [PATCH 17/42] more-isolation --- project/Build.scala | 2 +- .../scala/tools/repl/EmbeddedReplMain.scala | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index c0e8dc9ffd0b..51bc08f37ba2 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1222,7 +1222,7 @@ object Build { val relativePath = file.relativeTo(tmpDir).get.getPath val shouldDelete = - relativePath.startsWith("scala/") && !relativePath.startsWith("scala/tools/") || + /*relativePath.startsWith("scala/") && !relativePath.startsWith("scala/tools/") ||*/ relativePath.startsWith("org/jline/") val shouldKeepInPlace = relativePath.startsWith("scala/tools/repl/") diff --git a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala index e687aff5c130..46f475562bc1 100644 --- a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -20,23 +20,23 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { val loaded = findLoadedClass(name) if (loaded != null) return loaded - try { - val shadedPath = (SHADED_PREFIX + name).replace('.', '/') + ".class" - val is = getParent.getResourceAsStream(shadedPath) + val shadedPath = (SHADED_PREFIX + name).replace('.', '/') + ".class" + val is0 = try { + Option(super.getResourceAsStream(shadedPath)) + }catch{ + case _: Exception => None + } - if (is != null) { + is0 match{ + case Some(is) => try { val bytes = is.readAllBytes() val clazz = defineClass(name, bytes, 0, bytes.length) if (resolve) resolveClass(clazz) return clazz } finally is.close() - } - } catch { - case _: Exception => // Fall through to parent + case None => super.loadClass(name, resolve) } - - super.loadClass(name, resolve) } override def getResourceAsStream(name: String): InputStream | Null = { @@ -62,13 +62,13 @@ object EmbeddedReplMain { val unshadingClassLoader = new UnshadingClassLoader(getClass.getClassLoader) val replDriverClass = unshadingClassLoader.loadClass("dotty.tools.repl.ReplDriver") - val constructor = replDriverClass.getConstructors().head + val someCls = unshadingClassLoader.loadClass("scala.Some") // Create the ReplDriver instance with classpath argument - val replDriver = constructor.newInstance( + val replDriver = replDriverClass.getConstructors().head.newInstance( argsWithClasspath, // settings: Array[String] (now includes -classpath) System.out, // out: PrintStream - Option(getClass.getClassLoader), // classLoader: Option[ClassLoader] + someCls.getConstructors().head.newInstance(getClass.getClassLoader), "" // extraPredef: String ) From 312a5d2640664f53c7ab095febded56d4dfb2e30 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 16:06:30 -0800 Subject: [PATCH 18/42] . --- repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala index 46f475562bc1..29d6b9f913ce 100644 --- a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -66,10 +66,10 @@ object EmbeddedReplMain { val someCls = unshadingClassLoader.loadClass("scala.Some") // Create the ReplDriver instance with classpath argument val replDriver = replDriverClass.getConstructors().head.newInstance( - argsWithClasspath, // settings: Array[String] (now includes -classpath) - System.out, // out: PrintStream - someCls.getConstructors().head.newInstance(getClass.getClassLoader), - "" // extraPredef: String + /*settings*/ argsWithClasspath, + /*out*/ System.out, + /*classLoader*/ someCls.getConstructors().head.newInstance(getClass.getClassLoader), + /*extraPredef*/ "" ) replDriverClass.getMethod("tryRunning").invoke(replDriver) From 2d1adcda2dd2a3872ceac112af1e48fcd0f11a76 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 16:07:08 -0800 Subject: [PATCH 19/42] . --- repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala index 29d6b9f913ce..1d4a94fad7a8 100644 --- a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -64,7 +64,7 @@ object EmbeddedReplMain { val replDriverClass = unshadingClassLoader.loadClass("dotty.tools.repl.ReplDriver") val someCls = unshadingClassLoader.loadClass("scala.Some") - // Create the ReplDriver instance with classpath argument + val replDriver = replDriverClass.getConstructors().head.newInstance( /*settings*/ argsWithClasspath, /*out*/ System.out, From 7965f9e34fdeb900bd71bbc6abde986b3eb68c5e Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 16:12:59 -0800 Subject: [PATCH 20/42] . --- project/Build.scala | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 51bc08f37ba2..6a52fdcd69fd 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1193,8 +1193,8 @@ object Build { Compile / unmanagedSourceDirectories := Seq(baseDirectory.value / "src"), // Assembly configuration for shading assembly / assemblyJarName := s"scala3-repl-shaded-${version.value}.jar", - // Don't run tests for assembly - assembly / test := {}, + + assembly / test := {}, // Don't run tests for assembly // Exclude scala-library and jline from assembly (users provide them on classpath) assembly / assemblyExcludedJars := { val cp = (assembly / fullClasspath).value @@ -1210,7 +1210,6 @@ object Build { log.info(s"Post-processing assembly to relocate files into shaded subfolder...") - // Create a temporary directory for processing val tmpDir = IO.createTemporaryDirectory try { IO.unzip(originalJar, tmpDir) @@ -1221,15 +1220,14 @@ object Build { if (file.isFile) { val relativePath = file.relativeTo(tmpDir).get.getPath - val shouldDelete = - /*relativePath.startsWith("scala/") && !relativePath.startsWith("scala/tools/") ||*/ - relativePath.startsWith("org/jline/") - + // Avoid shading JLine because shading it causes problems with + // its service discovery and JNI-related logic + val shouldDelete = relativePath.startsWith("org/jline/") + // This is the entrypoint to the embedded Scala REPL so don't shade it val shouldKeepInPlace = relativePath.startsWith("scala/tools/repl/") if (shouldDelete) IO.delete(file) else if (!shouldKeepInPlace) { - // Move everything else to the shaded directory val newPath = shadedDir / relativePath IO.createDirectory(newPath.getParentFile) IO.move(file, newPath) From 0a2e8e9cda5fa55c9f22ea8d35b29fc1bd684f17 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 16:31:29 -0800 Subject: [PATCH 21/42] . --- project/Build.scala | 6 +++++- repl/src/dotty/tools/repl/Rendering.scala | 11 ++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 6a52fdcd69fd..09debd54ae7d 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1222,7 +1222,11 @@ object Build { // Avoid shading JLine because shading it causes problems with // its service discovery and JNI-related logic - val shouldDelete = relativePath.startsWith("org/jline/") + val shouldDelete = + relativePath.startsWith("scala/") && + !relativePath.startsWith("scala/tools/")&& + !relativePath.startsWith("scala/collection/internal/pprint/") || + relativePath.startsWith("org/jline/") // This is the entrypoint to the embedded Scala REPL so don't shade it val shouldKeepInPlace = relativePath.startsWith("scala/tools/repl/") diff --git a/repl/src/dotty/tools/repl/Rendering.scala b/repl/src/dotty/tools/repl/Rendering.scala index fde300bcf39d..016b09faefeb 100644 --- a/repl/src/dotty/tools/repl/Rendering.scala +++ b/repl/src/dotty/tools/repl/Rendering.scala @@ -28,10 +28,12 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): var myClassLoader: AbstractFileClassLoader = uninitialized private def pprintRender(value: Any, width: Int, height: Int, initialOffset: Int)(using Context): String = { - def fallback() = + def fallback() = { pprint.PPrinter.Color .apply(value, width = width, height = height, initialOffset = initialOffset) - .plainText + .render + } + try // normally, if we used vanilla JDK and layered classloaders, we wouldnt need reflection. // however PPrint works by runtime type testing to deconstruct values. This is @@ -56,10 +58,9 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): classOf[Boolean], // escape Unicode classOf[Boolean], // show field names ) + val FansiStr_render = fansiStrCls.getMethod("render") - val fansiStr = Color_apply.invoke( - Color, value, width, height, 2, initialOffset, false, true - ) + val fansiStr = Color_apply.invoke(Color, value, width, height, 2, initialOffset, false, true) FansiStr_render.invoke(fansiStr).asInstanceOf[String] catch case ex: ClassNotFoundException => fallback() From a6be2bfbc5d599abed51c44ba58e834a701471ce Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 16:44:53 -0800 Subject: [PATCH 22/42] . --- build.sbt | 1 + project/Build.scala | 40 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index 191229ea9971..51f067f82832 100644 --- a/build.sbt +++ b/build.sbt @@ -9,6 +9,7 @@ val `scala3-compiler-nonbootstrapped` = Build.`scala3-compiler-nonbootstrapped` val `scala3-compiler-bootstrapped-new` = Build.`scala3-compiler-bootstrapped-new` val `scala3-repl` = Build.`scala3-repl` +val `scala3-repl-pprint` = Build.`scala3-repl-pprint` val `scala3-repl-shaded` = Build.`scala3-repl-shaded` val `scala3-repl-embedded` = Build.`scala3-repl-embedded` diff --git a/project/Build.scala b/project/Build.scala index 09debd54ae7d..2da1c7612f3c 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1177,7 +1177,42 @@ object Build { (Compile / run).toTask(" -usejavacp").value }, ) - + + lazy val `scala3-repl-pprint` = project.in(file("repl-pprint")) + .enablePlugins(sbtassembly.AssemblyPlugin) + .settings(publishSettings) + .settings( + name := "scala3-repl-pprint", + moduleName := "scala3-repl-pprint", + version := dottyVersion, + versionScheme := Some("semver-spec"), + scalaVersion := referenceVersion, + crossPaths := true, + autoScalaLibrary := false, + + libraryDependencies ++= Seq( // Dependencies to shade + "com.lihaoyi" %% "pprint" % "0.9.0", + "com.lihaoyi" %% "fansi" % "0.5.0", + "com.lihaoyi" %% "sourcecode" % "0.4.2" + ), + assembly / assemblyJarName := s"scala3-repl-pprint-${version.value}.jar", + assembly / assemblyShadeRules := Seq( + ShadeRule.rename("pprint.**" -> "dotty.shaded.pprint.@1").inAll, + ShadeRule.rename("fansi.**" -> "dotty.shaded.fansi.@1").inAll, + ShadeRule.rename("sourcecode.**" -> "dotty.shaded.sourcecode.@1").inAll, + ), + // Exclude scala-library from assembly + assembly / assemblyExcludedJars := { + val cp = (assembly / fullClasspath).value + cp.filter { jar => + val name = jar.data.getName + name.startsWith("scala-library") || name.startsWith("scala3-library") + } + }, + Compile / packageBin := assembly.value, + publish / skip := false, + ) + lazy val `scala3-repl-shaded` = project.in(file("repl-shaded")) .dependsOn(`scala3-repl`) .enablePlugins(sbtassembly.AssemblyPlugin) @@ -1255,7 +1290,7 @@ object Build { ) lazy val `scala3-repl-embedded` = project.in(file("repl-embedded")) - .dependsOn(`scala-library-bootstrapped`) + .dependsOn(`scala-library-bootstrapped`, `scala3-repl-pprint`) .settings(publishSettings) .settings( name := "scala3-repl-embedded", @@ -1273,7 +1308,6 @@ object Build { Compile / unmanagedSourceDirectories := Seq.empty, // Use the shaded assembly jar as our packageBin Compile / packageBin := (`scala3-repl-shaded` / Compile / assembly).value, - Test / publishArtifact := false, publish / skip := false, ) From 4930d185a25a6b5d78ada04882ad95c7fe93d5ef Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 17:25:43 -0800 Subject: [PATCH 23/42] . --- build.sbt | 1 - project/Build.scala | 136 ++++++++++++------ .../scala/tools/repl/EmbeddedReplMain.scala | 3 +- repl/src/dotty/tools/repl/Rendering.scala | 14 +- repl/src/dotty/tools/repl/StackTraceOps.scala | 1 + 5 files changed, 108 insertions(+), 47 deletions(-) diff --git a/build.sbt b/build.sbt index 51f067f82832..191229ea9971 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,6 @@ val `scala3-compiler-nonbootstrapped` = Build.`scala3-compiler-nonbootstrapped` val `scala3-compiler-bootstrapped-new` = Build.`scala3-compiler-bootstrapped-new` val `scala3-repl` = Build.`scala3-repl` -val `scala3-repl-pprint` = Build.`scala3-repl-pprint` val `scala3-repl-shaded` = Build.`scala3-repl-shaded` val `scala3-repl-embedded` = Build.`scala3-repl-embedded` diff --git a/project/Build.scala b/project/Build.scala index 2da1c7612f3c..972e597539f4 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1108,9 +1108,6 @@ object Build { "org.jline" % "jline-reader" % "3.29.0", "org.jline" % "jline-terminal" % "3.29.0", "org.jline" % "jline-terminal-jni" % "3.29.0", - "com.lihaoyi" %% "pprint" % "0.9.3", - "com.lihaoyi" %% "fansi" % "0.5.1", - "com.lihaoyi" %% "sourcecode" % "0.4.4", "com.github.sbt" % "junit-interface" % "0.13.3" % Test, "io.get-coursier" % "interface" % "1.0.28", // used by the REPL for dependency resolution "org.virtuslab" % "using_directives" % "1.1.4", // used by the REPL for parsing magic comments @@ -1176,41 +1173,97 @@ object Build { // with it as a parameter. THIS IS NOT A LEGIT USE CASE OF THE `-usejavacp` FLAG. (Compile / run).toTask(" -usejavacp").value }, - ) + (Compile / sourceGenerators) += Def.task { + val s = streams.value + val cacheDir = s.cacheDirectory + val dest = (Compile / sourceManaged).value / "downloaded" + val lm = dependencyResolution.value - lazy val `scala3-repl-pprint` = project.in(file("repl-pprint")) - .enablePlugins(sbtassembly.AssemblyPlugin) - .settings(publishSettings) - .settings( - name := "scala3-repl-pprint", - moduleName := "scala3-repl-pprint", - version := dottyVersion, - versionScheme := Some("semver-spec"), - scalaVersion := referenceVersion, - crossPaths := true, - autoScalaLibrary := false, + val dependencies = Seq( + ("com.lihaoyi", "pprint_3", "0.9.3"), + ("com.lihaoyi", "fansi_3", "0.5.1"), + ("com.lihaoyi", "sourcecode_3", "0.4.4"), + ) - libraryDependencies ++= Seq( // Dependencies to shade - "com.lihaoyi" %% "pprint" % "0.9.0", - "com.lihaoyi" %% "fansi" % "0.5.0", - "com.lihaoyi" %% "sourcecode" % "0.4.2" - ), - assembly / assemblyJarName := s"scala3-repl-pprint-${version.value}.jar", - assembly / assemblyShadeRules := Seq( - ShadeRule.rename("pprint.**" -> "dotty.shaded.pprint.@1").inAll, - ShadeRule.rename("fansi.**" -> "dotty.shaded.fansi.@1").inAll, - ShadeRule.rename("sourcecode.**" -> "dotty.shaded.sourcecode.@1").inAll, - ), - // Exclude scala-library from assembly - assembly / assemblyExcludedJars := { - val cp = (assembly / fullClasspath).value - cp.filter { jar => - val name = jar.data.getName - name.startsWith("scala-library") || name.startsWith("scala3-library") + // Create a marker file that tracks the dependencies for cache invalidation + val markerFile = cacheDir / "shaded-sources-marker" + val markerContent = dependencies.map { case (org, name, version) => s"$org:$name:$version:sources" }.mkString("\n") + if (!markerFile.exists || IO.read(markerFile) != markerContent) { + IO.write(markerFile, markerContent) } - }, - Compile / packageBin := assembly.value, - publish / skip := false, + + FileFunction.cached(cacheDir / "fetchShadedSources", + FilesInfo.lastModified, FilesInfo.exists) { _ => + s.log.info(s"Downloading and processing shaded sources to $dest...") + + if (dest.exists) { + IO.delete(dest) + } + IO.createDirectory(dest) + + for((org, name, version) <- dependencies) { + import sbt.librarymanagement._ + + val moduleId = ModuleID(org, name, version).sources() + val retrieveDir = cacheDir / "retrieved" / s"$org-$name-$version-sources" + + s.log.info(s"Retrieving $org:$name:$version:sources...") + val retrieved = lm.retrieve(moduleId, scalaModuleInfo = None, retrieveDir, s.log) + val jarFiles = retrieved.fold( + w => throw w.resolveException, + files => files.filter(_.getName.contains("-sources.jar")) + ) + + jarFiles.foreach { jarFile => + s.log.info(s"Extracting ${jarFile.getName}...") + IO.unzip(jarFile, dest) + } + } + + val scalaFiles = (dest ** "*.scala").get + + // Define patches as a map from search text to replacement text + val patches = Map( + "import scala" -> "import _root_.scala", + " scala.collection." -> " _root_.scala.collection.", + "def apply(c: Char): Trie[T]" -> "def apply(c: Char): Trie[T] | Null", + "var head: Iterator[T] = null" -> "var head: Iterator[T] | Null = null", + "if (head != null && head.hasNext) true" -> "if (head != null && head.nn.hasNext) true", + "head.next()" -> "head.nn.next()", + "abstract class Walker" -> "@scala.annotation.nowarn abstract class Walker", + "object TPrintLowPri" -> "@scala.annotation.nowarn object TPrintLowPri", + "x.toString match{" -> "scala.runtime.ScalaRunTime.stringOf(x) match{" + ) + + val patchUsageCounter = scala.collection.mutable.Map(patches.keys.map(_ -> 0).toSeq: _*) + + scalaFiles.foreach { file => + val text = IO.read(file) + if (!file.getName.equals("CollectionName.scala")) { + var processedText = "package dotty.shaded\n" + text + + // Apply patches and count usage + patches.foreach { case (search, replacement) => + if (processedText.contains(search)) { + processedText = processedText.replace(search, replacement) + patchUsageCounter(search) += 1 + } + } + + IO.write(file, processedText) + } + } + + // Assert that all patches were applied at least once + val unappliedPatches = patchUsageCounter.filter(_._2 == 0).keys + if (unappliedPatches.nonEmpty) { + throw new RuntimeException(s"Patches were not applied: ${unappliedPatches.mkString(", ")}") + } + + scalaFiles.toSet + } (Set(markerFile)).toSeq + + } ) lazy val `scala3-repl-shaded` = project.in(file("repl-shaded")) @@ -1223,7 +1276,7 @@ object Build { versionScheme := Some("semver-spec"), scalaVersion := referenceVersion, crossPaths := true, - autoScalaLibrary := false, + autoScalaLibrary := true, // Source directories Compile / unmanagedSourceDirectories := Seq(baseDirectory.value / "src"), // Assembly configuration for shading @@ -1248,7 +1301,7 @@ object Build { val tmpDir = IO.createTemporaryDirectory try { IO.unzip(originalJar, tmpDir) - val shadedDir = tmpDir / "dotty" / "tools" / "repl" / "shaded" + val shadedDir = tmpDir / "dotty" / "shaded" IO.createDirectory(shadedDir) (tmpDir ** "*").get.foreach { file => @@ -1259,11 +1312,13 @@ object Build { // its service discovery and JNI-related logic val shouldDelete = relativePath.startsWith("scala/") && - !relativePath.startsWith("scala/tools/")&& + !relativePath.startsWith("scala/tools/") && !relativePath.startsWith("scala/collection/internal/pprint/") || relativePath.startsWith("org/jline/") // This is the entrypoint to the embedded Scala REPL so don't shade it - val shouldKeepInPlace = relativePath.startsWith("scala/tools/repl/") + val shouldKeepInPlace = relativePath.startsWith("scala/tools/repl/")|| + relativePath.startsWith("dotty/shaded/") || + relativePath.startsWith("scala/collection/internal/pprint/") if (shouldDelete) IO.delete(file) else if (!shouldKeepInPlace) { @@ -1287,10 +1342,11 @@ object Build { // Don't publish scala3-repl-shaded - it's an internal build artifact publish / skip := true, publishLocal / skip := true, + ) lazy val `scala3-repl-embedded` = project.in(file("repl-embedded")) - .dependsOn(`scala-library-bootstrapped`, `scala3-repl-pprint`) + .dependsOn(`scala-library-bootstrapped`) .settings(publishSettings) .settings( name := "scala3-repl-embedded", diff --git a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala index 1d4a94fad7a8..7809be4a5e97 100644 --- a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -14,7 +14,7 @@ import java.io.InputStream */ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { - private val SHADED_PREFIX = "dotty.tools.repl.shaded." + private val SHADED_PREFIX = "dotty.shaded." override def loadClass(name: String, resolve: Boolean): Class[?] = { val loaded = findLoadedClass(name) @@ -55,6 +55,7 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { */ object EmbeddedReplMain { def main(args: Array[String]): Unit = { + dotty.shaded.pprint.log(dotty.shaded.fansi.Color.Green("Helloo").toString) val argsWithClasspath = if (args.exists(arg => arg == "-classpath" || arg == "-cp")) args else Array("-classpath", System.getProperty("java.class.path")) ++ args diff --git a/repl/src/dotty/tools/repl/Rendering.scala b/repl/src/dotty/tools/repl/Rendering.scala index 016b09faefeb..bfa206995fdc 100644 --- a/repl/src/dotty/tools/repl/Rendering.scala +++ b/repl/src/dotty/tools/repl/Rendering.scala @@ -9,7 +9,7 @@ import printing.ReplPrinter import printing.SyntaxHighlighting import reporting.Diagnostic import StackTraceOps.* - +import dotty.shaded.* import scala.compiletime.uninitialized import scala.util.control.NonFatal @@ -46,8 +46,8 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): // Due the possible interruption instrumentation, it is unlikely that we can get // rid of reflection here. val cl = classLoader() - val pprintCls = Class.forName("pprint.PPrinter$Color$", false, cl) - val fansiStrCls = Class.forName("fansi.Str", false, cl) + val pprintCls = Class.forName("dotty.shaded.pprint.PPrinter$Color$", false, cl) + val fansiStrCls = Class.forName("dotty.shaded.fansi.Str", false, cl) val Color = pprintCls.getField("MODULE$").get(null) val Color_apply = pprintCls.getMethod("apply", classOf[Any], // value @@ -63,8 +63,12 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): val fansiStr = Color_apply.invoke(Color, value, width, height, 2, initialOffset, false, true) FansiStr_render.invoke(fansiStr).asInstanceOf[String] catch - case ex: ClassNotFoundException => fallback() - case ex: NoSuchMethodException => fallback() + case ex: ClassNotFoundException => + println("FALLBACK 1") + fallback() + case ex: NoSuchMethodException => + println("FALLBACK 2") + fallback() } diff --git a/repl/src/dotty/tools/repl/StackTraceOps.scala b/repl/src/dotty/tools/repl/StackTraceOps.scala index 618bdd979688..c81d8c435dd7 100644 --- a/repl/src/dotty/tools/repl/StackTraceOps.scala +++ b/repl/src/dotty/tools/repl/StackTraceOps.scala @@ -16,6 +16,7 @@ import scala.language.unsafeNulls import collection.mutable, mutable.ListBuffer import dotty.tools.dotc.util.chaining.* import java.lang.System.lineSeparator +import dotty.shaded.* object StackTraceOps: From 66a1641c0a800d1d95468135cd8e500ddc01ac42 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 17:25:50 -0800 Subject: [PATCH 24/42] . --- repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala index 7809be4a5e97..3365533a8dd3 100644 --- a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -55,7 +55,6 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { */ object EmbeddedReplMain { def main(args: Array[String]): Unit = { - dotty.shaded.pprint.log(dotty.shaded.fansi.Color.Green("Helloo").toString) val argsWithClasspath = if (args.exists(arg => arg == "-classpath" || arg == "-cp")) args else Array("-classpath", System.getProperty("java.class.path")) ++ args From 0e292bcb625e4d5cb4410924cd59e92be7f0736a Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 18:13:26 -0800 Subject: [PATCH 25/42] . --- repl/src/dotty/tools/repl/Rendering.scala | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/repl/src/dotty/tools/repl/Rendering.scala b/repl/src/dotty/tools/repl/Rendering.scala index bfa206995fdc..36ca963bd7ed 100644 --- a/repl/src/dotty/tools/repl/Rendering.scala +++ b/repl/src/dotty/tools/repl/Rendering.scala @@ -63,12 +63,8 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): val fansiStr = Color_apply.invoke(Color, value, width, height, 2, initialOffset, false, true) FansiStr_render.invoke(fansiStr).asInstanceOf[String] catch - case ex: ClassNotFoundException => - println("FALLBACK 1") - fallback() - case ex: NoSuchMethodException => - println("FALLBACK 2") - fallback() + case ex: ClassNotFoundException => fallback() + case ex: NoSuchMethodException => fallback() } From 52a6891017b8a2463647702fb5d42e0152916d1b Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 20:29:49 -0800 Subject: [PATCH 26/42] . --- build.sbt | 1 - project/Build.scala | 53 ++++++++----------- .../scala/tools/repl/EmbeddedReplMain.scala | 2 +- 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/build.sbt b/build.sbt index 191229ea9971..10920336da89 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,6 @@ val `scala3-compiler-nonbootstrapped` = Build.`scala3-compiler-nonbootstrapped` val `scala3-compiler-bootstrapped-new` = Build.`scala3-compiler-bootstrapped-new` val `scala3-repl` = Build.`scala3-repl` -val `scala3-repl-shaded` = Build.`scala3-repl-shaded` val `scala3-repl-embedded` = Build.`scala3-repl-embedded` // The Standard Library diff --git a/project/Build.scala b/project/Build.scala index 103d7a925e11..dd5d2b1c2cb6 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1270,22 +1270,33 @@ object Build { } ) - lazy val `scala3-repl-shaded` = project.in(file("repl-shaded")) - .dependsOn(`scala3-repl`) + lazy val `scala3-repl-embedded` = project.in(file("repl-shaded")) + .dependsOn(`scala-library-bootstrapped`) .enablePlugins(sbtassembly.AssemblyPlugin) + .settings(publishSettings) .settings( - name := "scala3-repl-shaded", - moduleName := "scala3-repl-shaded", + name := "scala3-repl-embedded", + moduleName := "scala3-repl-embedded", version := dottyVersion, versionScheme := Some("semver-spec"), scalaVersion := referenceVersion, crossPaths := true, autoScalaLibrary := true, + libraryDependencies ++= Seq( + "org.jline" % "jline-reader" % "3.29.0", + "org.jline" % "jline-terminal" % "3.29.0", + "org.jline" % "jline-terminal-jni" % "3.29.0", + ), // Source directories Compile / unmanagedSourceDirectories := Seq(baseDirectory.value / "src"), // Assembly configuration for shading - assembly / assemblyJarName := s"scala3-repl-shaded-${version.value}.jar", - + assembly / assemblyJarName := s"scala3-repl-embedded-${version.value}.jar", + // Add scala3-repl to assembly classpath without making it a published dependency + assembly / fullClasspath := { + val replJar = (`scala3-repl` / Compile / packageBin).value + val cp = (assembly / fullClasspath).value + cp :+ Attributed.blank(replJar) + }, assembly / test := {}, // Don't run tests for assembly // Exclude scala-library and jline from assembly (users provide them on classpath) assembly / assemblyExcludedJars := { @@ -1305,7 +1316,7 @@ object Build { val tmpDir = IO.createTemporaryDirectory try { IO.unzip(originalJar, tmpDir) - val shadedDir = tmpDir / "dotty" / "shaded" + val shadedDir = tmpDir / "dotty" / "isolated" IO.createDirectory(shadedDir) (tmpDir ** "*").get.foreach { file => @@ -1343,32 +1354,10 @@ object Build { originalJar }, - // Don't publish scala3-repl-shaded - it's an internal build artifact - publish / skip := true, - publishLocal / skip := true, - - ) - - lazy val `scala3-repl-embedded` = project.in(file("repl-embedded")) - .dependsOn(`scala-library-bootstrapped`) - .settings(publishSettings) - .settings( - name := "scala3-repl-embedded", - moduleName := "scala3-repl-embedded", - version := dottyVersion, - versionScheme := Some("semver-spec"), - scalaVersion := referenceVersion, - crossPaths := true, - libraryDependencies ++= Seq( - "org.jline" % "jline-reader" % "3.29.0", - "org.jline" % "jline-terminal" % "3.29.0", - "org.jline" % "jline-terminal-jni" % "3.29.0", - ), - // No source files in this project - just publishes the shaded jar - Compile / unmanagedSourceDirectories := Seq.empty, - // Use the shaded assembly jar as our packageBin - Compile / packageBin := (`scala3-repl-shaded` / Compile / assembly).value, + // Use the shaded assembly jar as our packageBin for publishing + Compile / packageBin := (Compile / assembly).value, publish / skip := false, + ) // ============================================================================================== diff --git a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala index 3365533a8dd3..47a414a4000f 100644 --- a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -14,7 +14,7 @@ import java.io.InputStream */ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { - private val SHADED_PREFIX = "dotty.shaded." + private val SHADED_PREFIX = "dotty.isolated." override def loadClass(name: String, resolve: Boolean): Class[?] = { val loaded = findLoadedClass(name) From d50f5b18007a54c488ba4d0789efb79820674e7f Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 20:31:24 -0800 Subject: [PATCH 27/42] . --- repl/src/dotty/tools/repl/Rendering.scala | 3 +-- sbt-bridge/src-bootstrapped/ConsoleInterface.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/repl/src/dotty/tools/repl/Rendering.scala b/repl/src/dotty/tools/repl/Rendering.scala index 36ca963bd7ed..366a5c0479d0 100644 --- a/repl/src/dotty/tools/repl/Rendering.scala +++ b/repl/src/dotty/tools/repl/Rendering.scala @@ -28,11 +28,10 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): var myClassLoader: AbstractFileClassLoader = uninitialized private def pprintRender(value: Any, width: Int, height: Int, initialOffset: Int)(using Context): String = { - def fallback() = { + def fallback() = pprint.PPrinter.Color .apply(value, width = width, height = height, initialOffset = initialOffset) .render - } try // normally, if we used vanilla JDK and layered classloaders, we wouldnt need reflection. diff --git a/sbt-bridge/src-bootstrapped/ConsoleInterface.java b/sbt-bridge/src-bootstrapped/ConsoleInterface.java index 2f9ac33098d5..3841457bcbe8 100644 --- a/sbt-bridge/src-bootstrapped/ConsoleInterface.java +++ b/sbt-bridge/src-bootstrapped/ConsoleInterface.java @@ -10,7 +10,7 @@ import xsbti.Logger; import dotty.tools.dotc.core.Contexts.Context; -import dotty.tools.repl.ReplDriver; +import dotty.tools.repl.; import dotty.tools.repl.State; public class ConsoleInterface { From 4ad3f796da26af1c9dbd97b8004046000f5ec7c6 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 20:32:19 -0800 Subject: [PATCH 28/42] . --- repl/src/dotty/tools/repl/ReplDriver.scala | 716 ------------------ repl/src/dotty/tools/repl/ReplMain.scala | 60 -- repl/test/dotty/tools/repl/ReplMainTest.scala | 73 -- 3 files changed, 849 deletions(-) delete mode 100644 repl/src/dotty/tools/repl/ReplDriver.scala delete mode 100644 repl/src/dotty/tools/repl/ReplMain.scala delete mode 100644 repl/test/dotty/tools/repl/ReplMainTest.scala diff --git a/repl/src/dotty/tools/repl/ReplDriver.scala b/repl/src/dotty/tools/repl/ReplDriver.scala deleted file mode 100644 index 013daae339d2..000000000000 --- a/repl/src/dotty/tools/repl/ReplDriver.scala +++ /dev/null @@ -1,716 +0,0 @@ -package dotty.tools -package repl - -import scala.language.unsafeNulls - -import java.io.{File => JFile, PrintStream} -import java.nio.charset.StandardCharsets - -import dotc.ast.Trees.* -import dotc.ast.{tpd, untpd} -import dotc.classpath.ClassPathFactory -import dotc.config.CommandLineParser.tokenize -import dotc.config.Properties.{javaVersion, javaVmName, simpleVersionString} -import dotc.core.Contexts.* -import dotc.core.Decorators.* -import dotc.core.Phases.{unfusedPhases, typerPhase} -import dotc.core.Denotations.Denotation -import dotc.core.Flags.* -import dotc.core.Mode -import dotc.core.NameKinds.SimpleNameKind -import dotc.core.NameKinds.DefaultGetterName -import dotc.core.NameOps.* -import dotc.core.Names.Name -import dotc.core.StdNames.* -import dotc.core.Symbols.{Symbol, defn} -import dotc.core.SymbolLoaders -import dotc.interfaces -import dotc.interactive.Completion -import dotc.printing.SyntaxHighlighting -import dotc.reporting.{ConsoleReporter, StoreReporter} -import dotc.reporting.Diagnostic -import dotc.util.Spans.Span -import dotc.util.{SourceFile, SourcePosition} -import dotc.{CompilationUnit, Driver} -import dotc.config.CompilerCommand -import dotty.tools.io.{AbstractFileClassLoader => _, *} -import dotty.tools.runner.ScalaClassLoader.* - -import org.jline.reader.* - -import Rendering.showUser - -import scala.annotation.tailrec -import scala.collection.mutable -import scala.compiletime.uninitialized -import scala.jdk.CollectionConverters.* -import scala.tools.asm.ClassReader -import scala.util.control.NonFatal -import scala.util.Using - -/** The state of the REPL contains necessary bindings instead of having to have - * mutation - * - * The compiler in the REPL needs to do some wrapping in order to compile - * valid code. This wrapping occurs when a single `MemberDef` that cannot be - * top-level needs to be compiled. In order to do this, we need some unique - * identifier for each of these wrappers. That identifier is `objectIndex`. - * - * Free expressions such as `1 + 1` needs to have an assignment in order to be - * of use. These expressions are therefore given a identifier on the format - * `resX` where `X` starts at 0 and each new expression that needs an - * identifier is given the increment of the old identifier. This identifier is - * `valIndex`. - * - * @param objectIndex the index of the next wrapper - * @param valIndex the index of next value binding for free expressions - * @param imports a map from object index to the list of user defined imports - * @param invalidObjectIndexes the set of object indexes that failed to initialize - * @param quiet whether we print evaluation results - * @param context the latest compiler context - */ -case class State(objectIndex: Int, - valIndex: Int, - imports: Map[Int, List[tpd.Import]], - invalidObjectIndexes: Set[Int], - quiet: Boolean, - context: Context): - def validObjectIndexes = (1 to objectIndex).filterNot(invalidObjectIndexes.contains(_)) - -/** Main REPL instance, orchestrating input, compilation and presentation */ -class ReplDriver(settings: Array[String], - out: PrintStream = Console.out, - classLoader: Option[ClassLoader] = None, - extraPredef: String = "") extends Driver: - - /** Overridden to `false` in order to not have to give sources on the - * commandline - */ - override def sourcesRequired: Boolean = false - - /** Create a fresh and initialized context with IDE mode enabled */ - private def initialCtx(settings: List[String]) = { - val rootCtx = initCtx.fresh.addMode(Mode.ReadPositions | Mode.Interactive) - rootCtx.setSetting(rootCtx.settings.XcookComments, true) - rootCtx.setSetting(rootCtx.settings.XreadComments, true) - setupRootCtx(this.settings ++ settings, rootCtx) - } - - private val incompatibleOptions: Seq[String] = Seq( - initCtx.settings.YbestEffort.name, - initCtx.settings.YwithBestEffortTasty.name - ) - - private def setupRootCtx(settings: Array[String], rootCtx: Context) = { - val incompatible = settings.intersect(incompatibleOptions) - val filteredSettings = - if !incompatible.isEmpty then - inContext(rootCtx) { - out.println(i"Options incompatible with repl will be ignored: ${incompatible.mkString(", ")}") - } - settings.filter(!incompatible.contains(_)) - else settings - setup(filteredSettings, rootCtx) match - case Some((files, ictx)) => inContext(ictx) { - shouldStart = true - if files.nonEmpty then out.println(i"Ignoring spurious arguments: $files%, %") - ictx.base.initialize() - ictx - } - case None => - shouldStart = false - rootCtx - } - - /** the initial, empty state of the REPL session */ - final def initialState: State = - val emptyState = State(0, 0, Map.empty, Set.empty, false, rootCtx) - val initScript = rootCtx.settings.replInitScript.value(using rootCtx) - val combinedScript = initScript.trim() match - case "" => extraPredef - case script => s"$extraPredef\n$script" - run(combinedScript)(using emptyState) - - /** Reset state of repl to the initial state - * - * This method is responsible for performing an all encompassing reset. As - * such, when the user enters `:reset` this method should be called to reset - * everything properly - */ - protected def resetToInitial(settings: List[String] = Nil): Unit = { - rootCtx = initialCtx(settings) - if (rootCtx.settings.outputDir.isDefault(using rootCtx)) - rootCtx = rootCtx.fresh - .setSetting(rootCtx.settings.outputDir, new VirtualDirectory("")) - compiler = new ReplCompiler - rendering = new Rendering(classLoader) - } - - private var rootCtx: Context = uninitialized - private var shouldStart: Boolean = uninitialized - private var compiler: ReplCompiler = uninitialized - protected var rendering: Rendering = uninitialized - - // initialize the REPL session as part of the constructor so that once `run` - // is called, we're in business - resetToInitial() - - override protected def command: CompilerCommand = ReplCommand - - /** Try to run REPL if there is nothing that prevents us doing so. - * - * Possible reason for unsuccessful run are raised flags in CLI like --help or --version - */ - final def tryRunning = if shouldStart then - if rootCtx.settings.replQuitAfterInit.value(using rootCtx) then initialState - else runUntilQuit() - - /** Run REPL with `state` until `:quit` command found - * - * This method is the main entry point into the REPL. Its effects are not - * observable outside of the CLI, for this reason, most helper methods are - * `protected final` to facilitate testing. - */ - def runUntilQuit(using initialState: State = initialState)(hardcodedInput: java.io.InputStream = null): State = { - val terminal = new JLineTerminal - - val hardcodedInputLines = - if (hardcodedInput == null) null - else new java.io.BufferedReader(new java.io.InputStreamReader(hardcodedInput)) - out.println( - s"""Welcome to Scala $simpleVersionString ($javaVersion, Java $javaVmName). - |Type in expressions for evaluation. Or try :help.""".stripMargin) - - /** Blockingly read a line, getting back a parse result */ - def readLine()(using state: State): ParseResult = { - given Context = state.context - val completer: Completer = { (lineReader, line, candidates) => - def makeCandidate(label: String) = { - new Candidate( - /* value = */ label, - /* displ = */ stripBackTicks(label), // displayed value - /* group = */ null, // can be used to group completions together - /* descr = */ null, // TODO use for documentation? - /* suffix = */ null, - /* key = */ null, - /* complete = */ false // if true adds space when completing - ) - } - val comps = completions(line.cursor, line.line, state) - candidates.addAll(comps.map(_.label).distinct.map(makeCandidate).asJava) - val lineWord = line.word() - comps.filter(c => c.label == lineWord && c.symbols.nonEmpty) match - case Nil => - case exachMatches => - val terminal = lineReader.nn.getTerminal - lineReader.callWidget(LineReader.CLEAR) - terminal.writer.println() - exachMatches.foreach: exact => - exact.symbols.foreach: sym => - terminal.writer.println(SyntaxHighlighting.highlight(sym.showUser)) - lineReader.callWidget(LineReader.REDRAW_LINE) - lineReader.callWidget(LineReader.REDISPLAY) - terminal.flush() - } - - try { - val line = - if (hardcodedInputLines != null) hardcodedInputLines.readLine() - else terminal.readLine(completer) - - if (line == null) Quit - else ParseResult(line) - } catch { - case _: EndOfFileException => // Ctrl+D - Quit - case _: UserInterruptException => // Ctrl+C at prompt - clear and continue - SigKill - } - } - - @tailrec def loop(using state: State)(): State = { - - val res = readLine() - if (res == Quit) state - // Ctrl-C pressed at prompt - just continue with same state (line is cleared by JLine) - else if (res == SigKill) loop(using state)() - else { - // Set up interrupt handler for command execution - var firstCtrlCEntered = false - val thread = Thread.currentThread() - - // Clear the stop flag before executing new code - ReplBytecodeInstrumentation.setStopFlag(rendering.classLoader()(using state.context), false) - - val previousSignalHandler = terminal.handle( - org.jline.terminal.Terminal.Signal.INT, - (sig: org.jline.terminal.Terminal.Signal) => { - if (!firstCtrlCEntered) { - firstCtrlCEntered = true - // Set the stop flag to trigger throwIfReplStopped() in instrumented code - ReplBytecodeInstrumentation.setStopFlag(rendering.classLoader()(using state.context), true) - // Also interrupt the thread as a fallback for non-instrumented code - thread.interrupt() - out.println("\nAttempting to interrupt running thread with `Thread.interrupt`") - } else { - out.println("\nTerminating REPL Process...") - System.exit(130) // Standard exit code for SIGINT - } - } - ) - - val newState = - try interpret(res) - // Restore previous handler - finally terminal.handle(org.jline.terminal.Terminal.Signal.INT, previousSignalHandler) - - loop(using newState)() - } - } - - try runBody { loop() } - finally terminal.close() - } - - final def run(input: String)(using state: State): State = runBody { - interpret(ParseResult.complete(input)) - } - - protected def runBody(body: => State): State = rendering.classLoader()(using rootCtx).asContext(withRedirectedOutput(body)) - - // TODO: i5069 - final def bind(name: String, value: Any)(using state: State): State = state - - /** - * Controls whether the `System.out` and `System.err` streams are set to the provided constructor parameter instance - * of [[java.io.PrintStream]] during the execution of the repl. On by default. - * - * Disabling this can be beneficial when executing a repl instance inside a concurrent environment, for example a - * thread pool (such as the Scala compile server in the Scala Plugin for IntelliJ IDEA). - * - * In such environments, indepently executing `System.setOut` and `System.setErr` without any synchronization can - * lead to unpredictable results when restoring the original streams (dependent on the order of execution), leaving - * the Java process in an inconsistent state. - */ - protected def redirectOutput: Boolean = true - - // redirecting the output allows us to test `println` in scripted tests - private def withRedirectedOutput(op: => State): State = { - if redirectOutput then - val savedOut = System.out - val savedErr = System.err - try { - System.setOut(out) - System.setErr(out) - op - } - finally { - System.setOut(savedOut) - System.setErr(savedErr) - } - else op - } - - private def newRun(state: State, reporter: StoreReporter = newStoreReporter) = { - val run = compiler.newRun(rootCtx.fresh.setReporter(reporter), state) - state.copy(context = run.runContext) - } - - private def stripBackTicks(label: String) = - if label.startsWith("`") && label.endsWith("`") then - label.drop(1).dropRight(1) - else - label - - /** Extract possible completions at the index of `cursor` in `expr` */ - protected final def completions(cursor: Int, expr: String, state0: State): List[Completion] = - if expr.startsWith(":") then - ParseResult.commands.collect { - case command if command._1.startsWith(expr) => Completion(command._1, "", List()) - } - else - given state: State = newRun(state0) - compiler - .typeCheck(expr, errorsAllowed = true) - .map { (untpdTree, tpdTree) => - val file = SourceFile.virtual("", expr, maybeIncomplete = true) - val unit = CompilationUnit(file)(using state.context) - unit.untpdTree = untpdTree - unit.tpdTree = tpdTree - given Context = state.context.fresh.setCompilationUnit(unit) - val srcPos = SourcePosition(file, Span(cursor)) - try Completion.completions(srcPos)._2 catch case NonFatal(_) => Nil - } - .getOrElse(Nil) - end completions - - protected def interpret(res: ParseResult)(using state: State): State = { - res match { - case parsed: Parsed if parsed.source.content().mkString.startsWith("//>") => - // Check for magic comments specifying dependencies - println("Please use `:dep com.example::artifact:version` to add dependencies in the REPL") - state - - case parsed: Parsed if parsed.trees.nonEmpty => - compile(parsed, state) - - case SyntaxErrors(_, errs, _) => - displayErrors(errs) - state - - case cmd: Command => - interpretCommand(cmd) - - case SigKill => // TODO - state - - case _ => // new line, empty tree - state - } - } - - /** Compile `parsed` trees and evolve `state` in accordance */ - private def compile(parsed: Parsed, istate: State): State = { - def extractNewestWrapper(tree: untpd.Tree): Name = tree match { - case PackageDef(_, (obj: untpd.ModuleDef) :: Nil) => obj.name.moduleClassName - case _ => nme.NO_NAME - } - - def extractTopLevelImports(ctx: Context): List[tpd.Import] = - unfusedPhases(using ctx).collectFirst { case phase: CollectTopLevelImports => phase.imports }.get - - def contextWithNewImports(ctx: Context, imports: List[tpd.Import]): Context = - if imports.isEmpty then ctx - else - imports.foldLeft(ctx.fresh.setNewScope)((ctx, imp) => - ctx.importContext(imp, imp.symbol(using ctx))) - - given State = { - val state0 = newRun(istate, parsed.reporter) - state0.copy(context = state0.context.withSource(parsed.source)) - } - compiler - .compile(parsed) - .fold( - displayErrors, - { - case (unit: CompilationUnit, newState: State) => - val newestWrapper = extractNewestWrapper(unit.untpdTree) - val newImports = extractTopLevelImports(newState.context) - var allImports = newState.imports - if (newImports.nonEmpty) - allImports += (newState.objectIndex -> newImports) - val newStateWithImports = newState.copy( - imports = allImports, - context = contextWithNewImports(newState.context, newImports) - ) - - val warnings = newState.context.reporter - .removeBufferedMessages(using newState.context) - - inContext(newState.context) { - val (updatedState, definitions) = - if (!ctx.settings.XreplDisableDisplay.value) - renderDefinitions(unit.tpdTree, newestWrapper)(using newStateWithImports) - else - (newStateWithImports, Seq.empty) - - // output is printed in the order it was put in. warnings should be - // shown before infos (eg. typedefs) for the same line. column - // ordering is mostly to make tests deterministic - given Ordering[Diagnostic] = - Ordering[(Int, Int, Int)].on(d => (d.pos.line, -d.level, d.pos.column)) - - (if istate.quiet then warnings else definitions ++ warnings) - .sorted - .foreach(printDiagnostic) - - updatedState - } - } - ) - } - - private def renderDefinitions(tree: tpd.Tree, newestWrapper: Name)(using state: State): (State, Seq[Diagnostic]) = { - given Context = state.context - - def resAndUnit(denot: Denotation) = { - import scala.util.{Success, Try} - val sym = denot.symbol - val name = sym.name.show - val hasValidNumber = Try(name.drop(3).toInt) match { - case Success(num) => num < state.valIndex - case _ => false - } - name.startsWith(str.REPL_RES_PREFIX) && hasValidNumber && sym.info == defn.UnitType - } - - def extractAndFormatMembers(symbol: Symbol): (State, Seq[Diagnostic]) = if (tree.symbol.info.exists) { - val info = symbol.info - val defs = - info.bounds.hi.finalResultType - .membersBasedOnFlags(required = Method, excluded = Accessor | ParamAccessor | Synthetic | Private) - .filterNot { denot => - defn.topClasses.contains(denot.symbol.owner) || denot.symbol.isConstructor - || denot.symbol.name.is(DefaultGetterName) - } - - val vals = - info.fields - .filterNot(_.symbol.isOneOf(ParamAccessor | Private | Synthetic | Artifact | Module)) - .filter(_.symbol.name.is(SimpleNameKind)) - - val typeAliases = - info.bounds.hi.typeMembers.filter(_.symbol.info.isTypeAlias) - - // The wrapper object may fail to initialize if the rhs of a ValDef throws. - // In that case, don't attempt to render any subsequent vals, and mark this - // wrapper object index as invalid. - var failedInit = false - val renderedVals = - val buf = mutable.ListBuffer[Diagnostic]() - for d <- vals do if !failedInit then rendering.renderVal(d) match - case Right(Some(v)) => - buf += v - case Left(e) => - buf += rendering.renderError(e, d) - failedInit = true - case _ => - buf.toList - - if failedInit then - // We limit the returned diagnostics here to `renderedVals`, which will contain the rendered error - // for the val which failed to initialize. Since any other defs, aliases, imports, etc. from this - // input line will be inaccessible, we avoid rendering those so as not to confuse the user. - (state.copy(invalidObjectIndexes = state.invalidObjectIndexes + state.objectIndex), renderedVals) - else - val formattedMembers = - typeAliases.map(rendering.renderTypeAlias) - ++ defs.map(rendering.renderMethod) - ++ renderedVals - val diagnostics = if formattedMembers.isEmpty then rendering.forceModule(symbol) else formattedMembers - (state.copy(valIndex = state.valIndex - vals.count(resAndUnit)), diagnostics) - } - else (state, Seq.empty) - - def isSyntheticCompanion(sym: Symbol) = - sym.is(Module) && sym.is(Synthetic) - - def typeDefs(sym: Symbol): Seq[Diagnostic] = sym.info.memberClasses - .collect { - case x if !isSyntheticCompanion(x.symbol) && !x.symbol.name.isReplWrapperName => - rendering.renderTypeDef(x) - } - - atPhase(typerPhase.next) { - // Display members of wrapped module: - tree.symbol.info.memberClasses - .find(_.symbol.name == newestWrapper.moduleClassName) - .map { wrapperModule => - val (newState, formattedMembers) = extractAndFormatMembers(wrapperModule.symbol) - val formattedTypeDefs = // don't render type defs if wrapper initialization failed - if newState.invalidObjectIndexes.contains(state.objectIndex) then Seq.empty - else typeDefs(wrapperModule.symbol) - (newState, formattedTypeDefs ++ formattedMembers) - } - .getOrElse { - // user defined a trait/class/object, so no module needed - (state, Seq.empty) - } - } - } - - /** Interpret `cmd` to action and propagate potentially new `state` */ - private def interpretCommand(cmd: Command)(using state: State): State = cmd match { - case UnknownCommand(cmd) => - out.println(s"""Unknown command: "$cmd", run ":help" for a list of commands""") - state - - case AmbiguousCommand(cmd, matching) => - out.println(s""""$cmd" matches ${matching.mkString(", ")}. Try typing a few more characters. Run ":help" for a list of commands""") - state - - case Help => - out.println(Help.text) - state - - case Reset(arg) => - val tokens = tokenize(arg) - - if tokens.nonEmpty then - out.println(s"""|Resetting REPL state with the following settings: - | ${tokens.mkString("\n ")} - |""".stripMargin) - else - out.println("Resetting REPL state.") - - resetToInitial(tokens) - initialState - - case Imports => - for { - objectIndex <- state.validObjectIndexes - imp <- state.imports.getOrElse(objectIndex, Nil) - } out.println(imp.show(using state.context)) - state - - case Load(path) => - val file = new JFile(path) - if (file.exists) { - val contents = Using(scala.io.Source.fromFile(file, StandardCharsets.UTF_8.name))(_.mkString).get - run(contents) - } - else { - out.println(s"""Couldn't find file "${file.getCanonicalPath}"""") - state - } - - case Require(path) => - out.println(":require is no longer supported, but has been replaced with :jar. Please use :jar") - state - - case JarCmd(path) => - val jarFile = AbstractFile.getDirectory(path) - if (jarFile == null) - out.println(s"""Cannot add "$path" to classpath.""") - state - else - def flatten(f: AbstractFile): Iterator[AbstractFile] = - if (f.isClassContainer) f.iterator.flatMap(flatten) - else Iterator(f) - - def tryClassLoad(classFile: AbstractFile): Option[String] = { - val input = classFile.input - try { - val reader = new ClassReader(input) - val clsName = reader.getClassName.replace('/', '.') - rendering.myClassLoader.loadClass(clsName) - Some(clsName) - } catch - case _: ClassNotFoundException => None - finally { - input.close() - } - } - - try { - val entries = flatten(jarFile) - - val existingClass = entries.filter(_.ext.isClass).find(tryClassLoad(_).isDefined) - if (existingClass.nonEmpty) - out.println(s"The path '$path' cannot be loaded, it contains a classfile that already exists on the classpath: ${existingClass.get}") - else inContext(state.context): - val jarClassPath = ClassPathFactory.newClassPath(jarFile) - val prevOutputDir = ctx.settings.outputDir.value - - // add to compiler class path - ctx.platform.addToClassPath(jarClassPath) - SymbolLoaders.mergeNewEntries(defn.RootClass, ClassPath.RootPackage, jarClassPath, ctx.platform.classPath) - - // new class loader with previous output dir and specified jar - val prevClassLoader = rendering.classLoader() - val jarClassLoader = fromURLsParallelCapable( - jarClassPath.asURLs, prevClassLoader) - rendering.myClassLoader = new AbstractFileClassLoader( - prevOutputDir, - jarClassLoader, - ctx.settings.XreplInterruptInstrumentation.value - ) - - out.println(s"Added '$path' to classpath.") - } catch { - case e: Throwable => - out.println(s"Failed to load '$path' to classpath: ${e.getMessage}") - } - state - - case KindOf(expr) => - out.println(s"""The :kind command is not currently supported.""") - state - case TypeOf(expr) => - expr match { - case "" => out.println(s":type ") - case _ => - compiler.typeOf(expr)(using newRun(state)).fold( - displayErrors, - res => out.println(res) // result has some highlights - ) - } - state - - case DocOf(expr) => - expr match { - case "" => out.println(s":doc ") - case _ => - compiler.docOf(expr)(using newRun(state)).fold( - displayErrors, - res => out.println(res) - ) - } - state - - case Sh(expr) => - out.println(s"""The :sh command is deprecated. Use `import scala.sys.process._` and `"command".!` instead.""") - state - - case Settings(arg) => arg match - case "" => - given ctx: Context = state.context - for (s <- ctx.settings.userSetSettings(ctx.settingsState).sortBy(_.name)) - out.println(s"${s.name} = ${if s.value == "" then "\"\"" else s.value}") - state - case _ => - rootCtx = setupRootCtx(tokenize(arg).toArray, rootCtx) - state.copy(context = rootCtx) - - case Silent => state.copy(quiet = !state.quiet) - case Dep(dep) => - val depStrings = List(dep) - if depStrings.nonEmpty then - val deps = depStrings.flatMap(DependencyResolver.parseDependency) - if deps.nonEmpty then - DependencyResolver.resolveDependencies(deps) match - case Right(files) => - if files.nonEmpty then - inContext(state.context): - // Update both compiler classpath and classloader - val prevOutputDir = ctx.settings.outputDir.value - val prevClassLoader = rendering.classLoader() - rendering.myClassLoader = DependencyResolver.addToCompilerClasspath( - files, - prevClassLoader, - prevOutputDir - ) - out.println(s"Resolved ${deps.size} dependencies (${files.size} JARs)") - case Left(error) => - out.println(s"Error resolving dependencies: $error") - state - - case Quit => - // end of the world! - state - } - - /** shows all errors nicely formatted */ - private def displayErrors(errs: Seq[Diagnostic])(using state: State): State = { - errs.foreach(printDiagnostic) - state - } - - /** Like ConsoleReporter, but without file paths, -Xprompt displaying, - * and using a PrintStream rather than a PrintWriter so messages aren't re-encoded. */ - private object ReplConsoleReporter extends ConsoleReporter.AbstractConsoleReporter { - override def posFileStr(pos: SourcePosition) = "" // omit file paths - override def printMessage(msg: String): Unit = out.println(msg) - override def echoMessage(msg: String): Unit = printMessage(msg) - override def flush()(using Context): Unit = out.flush() - } - - /** Print warnings & errors using ReplConsoleReporter, and info straight to out */ - private def printDiagnostic(dia: Diagnostic)(using state: State) = dia.level match - case interfaces.Diagnostic.INFO => out.println(dia.msg) // print REPL's special info diagnostics directly to out - case _ => ReplConsoleReporter.doReport(dia)(using state.context) - -end ReplDriver -object ReplDriver: - def pprintImport = "import pprint.pprintln\n" \ No newline at end of file diff --git a/repl/src/dotty/tools/repl/ReplMain.scala b/repl/src/dotty/tools/repl/ReplMain.scala deleted file mode 100644 index 9d93ee9a6be4..000000000000 --- a/repl/src/dotty/tools/repl/ReplMain.scala +++ /dev/null @@ -1,60 +0,0 @@ -package dotty.tools.repl - -import java.io.PrintStream - -class ReplMain( - settings: Array[String] = Array.empty, - out: PrintStream = Console.out, - classLoader: Option[ClassLoader] = Some(getClass.getClassLoader), - predefCode: String = "", - testCode: String = "" -): - def run(bindings: ReplMain.Bind[_]*): Any = - try - ReplMain.currentBindings.set(bindings.map{bind => bind.name -> bind.value}.toMap) - - val bindingsPredef = bindings - .map { case bind => - s"def ${bind.name}: ${bind.typeName.value} = dotty.tools.repl.ReplMain.currentBinding[${bind.typeName.value}](\"${bind.name}\")" - } - .mkString("\n") - - val fullPredef = - ReplDriver.pprintImport + - (if bindingsPredef.nonEmpty then s"\n$bindingsPredef\n" else "") + - (if predefCode.nonEmpty then s"\n$predefCode\n" else "") - - val driver = new ReplDriver(settings, out, classLoader, fullPredef) - - if (testCode == "") driver.tryRunning - else driver.runUntilQuit(using driver.initialState)( - new java.io.ByteArrayInputStream(testCode.getBytes()) - ) - () - finally - ReplMain.currentBindings.set(null) - - -object ReplMain: - final case class TypeName[A](value: String) - object TypeName extends TypeNamePlatform - - import scala.quoted._ - - trait TypeNamePlatform: - inline given [A]: TypeName[A] = ${TypeNamePlatform.impl[A]} - - object TypeNamePlatform: - def impl[A](using t: Type[A], ctx: Quotes): Expr[TypeName[A]] = - '{TypeName[A](${Expr(Type.show[A])})} - - - case class Bind[T](name: String, value: () => T)(implicit val typeName: TypeName[T]) - object Bind: - implicit def ammoniteReplArrowBinder[T](t: (String, T))(implicit typeName: TypeName[T]): Bind[T] = { - Bind(t._1, () => t._2)(typeName) - } - - def currentBinding[T](s: String): T = currentBindings.get().apply(s).apply().asInstanceOf[T] - - private val currentBindings = new ThreadLocal[Map[String, () => Any]]() diff --git a/repl/test/dotty/tools/repl/ReplMainTest.scala b/repl/test/dotty/tools/repl/ReplMainTest.scala deleted file mode 100644 index 495d688f035d..000000000000 --- a/repl/test/dotty/tools/repl/ReplMainTest.scala +++ /dev/null @@ -1,73 +0,0 @@ -package dotty.tools -package repl - -import scala.language.unsafeNulls - -import java.io.{ByteArrayOutputStream, PrintStream} -import java.nio.charset.StandardCharsets - -import vulpix.TestConfiguration -import org.junit.Test -import org.junit.Assert._ - -/** Tests for the programmatic REPL API (ReplMain) */ -class ReplMainTest: - - private val defaultOptions = Array("-classpath", TestConfiguration.withCompilerClasspath) - - private def captureOutput(body: PrintStream => Unit): String = - val out = new ByteArrayOutputStream() - val ps = new PrintStream(out, true, StandardCharsets.UTF_8.name) - body(ps) - dotty.shaded.fansi.Str(out.toString(StandardCharsets.UTF_8.name)).plainText - - @Test def basicBinding(): Unit = - val output = captureOutput { out => - val replMain = new ReplMain( - settings = defaultOptions, - out = out, - testCode = "test" - ) - - replMain.run("test" -> 42) - } - - assertTrue(output.contains("val res0: Int = 42")) - - @Test def multipleBindings(): Unit = - val output = captureOutput { out => - val replMain = new ReplMain( - settings = defaultOptions, - out = out, - testCode = "x\ny\nz" - ) - - replMain.run( - "x" -> 1, - "y" -> "hello", - "z" -> true - ) - } - - assertTrue(output.contains("val res0: Int = 1")) - assertTrue(output.contains("val res1: String = \"hello\"")) - assertTrue(output.contains("val res2: Boolean = true")) - - @Test def bindingTypes(): Unit = - val output = captureOutput { out => - val replMain = new ReplMain( - settings = defaultOptions ++ Array("-repl-quit-after-init"), - out = out, - testCode = "list\nmap" - ) - - replMain.run( - "list" -> List(1, 2, 3), - "map" -> Map(1 -> "hello") - ) - } - - assertTrue(output.contains("val res0: List[Int] = List(1, 2, 3)")) - assertTrue(output.contains("val res1: Map[Int, String] = Map(1 -> \"hello\")")) - -end ReplMainTest From 4ea6918891047cf230f18f4ed031f43a91ebd3f6 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 20:32:38 -0800 Subject: [PATCH 29/42] . --- repl/src/dotty/tools/repl/ReplDriver.scala | 709 ++++++++++++++++++ .../src-bootstrapped/ConsoleInterface.java | 4 +- 2 files changed, 711 insertions(+), 2 deletions(-) create mode 100644 repl/src/dotty/tools/repl/ReplDriver.scala diff --git a/repl/src/dotty/tools/repl/ReplDriver.scala b/repl/src/dotty/tools/repl/ReplDriver.scala new file mode 100644 index 000000000000..298c44487f2e --- /dev/null +++ b/repl/src/dotty/tools/repl/ReplDriver.scala @@ -0,0 +1,709 @@ +package dotty.tools +package repl + +import scala.language.unsafeNulls + +import java.io.{File => JFile, PrintStream} +import java.nio.charset.StandardCharsets + +import dotc.ast.Trees.* +import dotc.ast.{tpd, untpd} +import dotc.classpath.ClassPathFactory +import dotc.config.CommandLineParser.tokenize +import dotc.config.Properties.{javaVersion, javaVmName, simpleVersionString} +import dotc.core.Contexts.* +import dotc.core.Decorators.* +import dotc.core.Phases.{unfusedPhases, typerPhase} +import dotc.core.Denotations.Denotation +import dotc.core.Flags.* +import dotc.core.Mode +import dotc.core.NameKinds.SimpleNameKind +import dotc.core.NameKinds.DefaultGetterName +import dotc.core.NameOps.* +import dotc.core.Names.Name +import dotc.core.StdNames.* +import dotc.core.Symbols.{Symbol, defn} +import dotc.core.SymbolLoaders +import dotc.interfaces +import dotc.interactive.Completion +import dotc.printing.SyntaxHighlighting +import dotc.reporting.{ConsoleReporter, StoreReporter} +import dotc.reporting.Diagnostic +import dotc.util.Spans.Span +import dotc.util.{SourceFile, SourcePosition} +import dotc.{CompilationUnit, Driver} +import dotc.config.CompilerCommand +import dotty.tools.io.{AbstractFileClassLoader => _, *} +import dotty.tools.runner.ScalaClassLoader.* + +import org.jline.reader.* + +import Rendering.showUser + +import scala.annotation.tailrec +import scala.collection.mutable +import scala.compiletime.uninitialized +import scala.jdk.CollectionConverters.* +import scala.tools.asm.ClassReader +import scala.util.control.NonFatal +import scala.util.Using + +/** The state of the REPL contains necessary bindings instead of having to have + * mutation + * + * The compiler in the REPL needs to do some wrapping in order to compile + * valid code. This wrapping occurs when a single `MemberDef` that cannot be + * top-level needs to be compiled. In order to do this, we need some unique + * identifier for each of these wrappers. That identifier is `objectIndex`. + * + * Free expressions such as `1 + 1` needs to have an assignment in order to be + * of use. These expressions are therefore given a identifier on the format + * `resX` where `X` starts at 0 and each new expression that needs an + * identifier is given the increment of the old identifier. This identifier is + * `valIndex`. + * + * @param objectIndex the index of the next wrapper + * @param valIndex the index of next value binding for free expressions + * @param imports a map from object index to the list of user defined imports + * @param invalidObjectIndexes the set of object indexes that failed to initialize + * @param quiet whether we print evaluation results + * @param context the latest compiler context + */ +case class State(objectIndex: Int, + valIndex: Int, + imports: Map[Int, List[tpd.Import]], + invalidObjectIndexes: Set[Int], + quiet: Boolean, + context: Context): + def validObjectIndexes = (1 to objectIndex).filterNot(invalidObjectIndexes.contains(_)) + +/** Main REPL instance, orchestrating input, compilation and presentation */ +class ReplDriver(settings: Array[String], + out: PrintStream = Console.out, + classLoader: Option[ClassLoader] = None, + extraPredef: String = "") extends Driver: + + /** Overridden to `false` in order to not have to give sources on the + * commandline + */ + override def sourcesRequired: Boolean = false + + /** Create a fresh and initialized context with IDE mode enabled */ + private def initialCtx(settings: List[String]) = { + val rootCtx = initCtx.fresh.addMode(Mode.ReadPositions | Mode.Interactive) + rootCtx.setSetting(rootCtx.settings.XcookComments, true) + rootCtx.setSetting(rootCtx.settings.XreadComments, true) + setupRootCtx(this.settings ++ settings, rootCtx) + } + + private val incompatibleOptions: Seq[String] = Seq( + initCtx.settings.YbestEffort.name, + initCtx.settings.YwithBestEffortTasty.name + ) + + private def setupRootCtx(settings: Array[String], rootCtx: Context) = { + val incompatible = settings.intersect(incompatibleOptions) + val filteredSettings = + if !incompatible.isEmpty then + inContext(rootCtx) { + out.println(i"Options incompatible with repl will be ignored: ${incompatible.mkString(", ")}") + } + settings.filter(!incompatible.contains(_)) + else settings + setup(filteredSettings, rootCtx) match + case Some((files, ictx)) => inContext(ictx) { + shouldStart = true + if files.nonEmpty then out.println(i"Ignoring spurious arguments: $files%, %") + ictx.base.initialize() + ictx + } + case None => + shouldStart = false + rootCtx + } + + /** the initial, empty state of the REPL session */ + final def initialState: State = + val emptyState = State(0, 0, Map.empty, Set.empty, false, rootCtx) + val initScript = rootCtx.settings.replInitScript.value(using rootCtx) + val combinedScript = initScript.trim() match + case "" => extraPredef + case script => s"$extraPredef\n$script" + run(combinedScript)(using emptyState) + + /** Reset state of repl to the initial state + * + * This method is responsible for performing an all encompassing reset. As + * such, when the user enters `:reset` this method should be called to reset + * everything properly + */ + protected def resetToInitial(settings: List[String] = Nil): Unit = { + rootCtx = initialCtx(settings) + if (rootCtx.settings.outputDir.isDefault(using rootCtx)) + rootCtx = rootCtx.fresh + .setSetting(rootCtx.settings.outputDir, new VirtualDirectory("")) + compiler = new ReplCompiler + rendering = new Rendering(classLoader) + } + + private var rootCtx: Context = uninitialized + private var shouldStart: Boolean = uninitialized + private var compiler: ReplCompiler = uninitialized + protected var rendering: Rendering = uninitialized + + // initialize the REPL session as part of the constructor so that once `run` + // is called, we're in business + resetToInitial() + + override protected def command: CompilerCommand = ReplCommand + + /** Try to run REPL if there is nothing that prevents us doing so. + * + * Possible reason for unsuccessful run are raised flags in CLI like --help or --version + */ + final def tryRunning = if shouldStart then + if rootCtx.settings.replQuitAfterInit.value(using rootCtx) then initialState + else runUntilQuit() + + /** Run REPL with `state` until `:quit` command found + * + * This method is the main entry point into the REPL. Its effects are not + * observable outside of the CLI, for this reason, most helper methods are + * `protected final` to facilitate testing. + */ + def runUntilQuit(using initialState: State = initialState)(): State = { + val terminal = new JLineTerminal + + out.println( + s"""Welcome to Scala $simpleVersionString ($javaVersion, Java $javaVmName). + |Type in expressions for evaluation. Or try :help.""".stripMargin) + + /** Blockingly read a line, getting back a parse result */ + def readLine()(using state: State): ParseResult = { + given Context = state.context + val completer: Completer = { (lineReader, line, candidates) => + def makeCandidate(label: String) = { + new Candidate( + /* value = */ label, + /* displ = */ stripBackTicks(label), // displayed value + /* group = */ null, // can be used to group completions together + /* descr = */ null, // TODO use for documentation? + /* suffix = */ null, + /* key = */ null, + /* complete = */ false // if true adds space when completing + ) + } + val comps = completions(line.cursor, line.line, state) + candidates.addAll(comps.map(_.label).distinct.map(makeCandidate).asJava) + val lineWord = line.word() + comps.filter(c => c.label == lineWord && c.symbols.nonEmpty) match + case Nil => + case exachMatches => + val terminal = lineReader.nn.getTerminal + lineReader.callWidget(LineReader.CLEAR) + terminal.writer.println() + exachMatches.foreach: exact => + exact.symbols.foreach: sym => + terminal.writer.println(SyntaxHighlighting.highlight(sym.showUser)) + lineReader.callWidget(LineReader.REDRAW_LINE) + lineReader.callWidget(LineReader.REDISPLAY) + terminal.flush() + } + + try { + val line = terminal.readLine(completer) + ParseResult(line) + } catch { + case _: EndOfFileException => // Ctrl+D + Quit + case _: UserInterruptException => // Ctrl+C at prompt - clear and continue + SigKill + } + } + + @tailrec def loop(using state: State)(): State = { + + val res = readLine() + if (res == Quit) state + // Ctrl-C pressed at prompt - just continue with same state (line is cleared by JLine) + else if (res == SigKill) loop(using state)() + else { + // Set up interrupt handler for command execution + var firstCtrlCEntered = false + val thread = Thread.currentThread() + + // Clear the stop flag before executing new code + ReplBytecodeInstrumentation.setStopFlag(rendering.classLoader()(using state.context), false) + + val previousSignalHandler = terminal.handle( + org.jline.terminal.Terminal.Signal.INT, + (sig: org.jline.terminal.Terminal.Signal) => { + if (!firstCtrlCEntered) { + firstCtrlCEntered = true + // Set the stop flag to trigger throwIfReplStopped() in instrumented code + ReplBytecodeInstrumentation.setStopFlag(rendering.classLoader()(using state.context), true) + // Also interrupt the thread as a fallback for non-instrumented code + thread.interrupt() + out.println("\nAttempting to interrupt running thread with `Thread.interrupt`") + } else { + out.println("\nTerminating REPL Process...") + System.exit(130) // Standard exit code for SIGINT + } + } + ) + + val newState = + try interpret(res) + // Restore previous handler + finally terminal.handle(org.jline.terminal.Terminal.Signal.INT, previousSignalHandler) + + loop(using newState)() + } + } + + try runBody { loop() } + finally terminal.close() + } + + final def run(input: String)(using state: State): State = runBody { + interpret(ParseResult.complete(input)) + } + + protected def runBody(body: => State): State = rendering.classLoader()(using rootCtx).asContext(withRedirectedOutput(body)) + + // TODO: i5069 + final def bind(name: String, value: Any)(using state: State): State = state + + /** + * Controls whether the `System.out` and `System.err` streams are set to the provided constructor parameter instance + * of [[java.io.PrintStream]] during the execution of the repl. On by default. + * + * Disabling this can be beneficial when executing a repl instance inside a concurrent environment, for example a + * thread pool (such as the Scala compile server in the Scala Plugin for IntelliJ IDEA). + * + * In such environments, indepently executing `System.setOut` and `System.setErr` without any synchronization can + * lead to unpredictable results when restoring the original streams (dependent on the order of execution), leaving + * the Java process in an inconsistent state. + */ + protected def redirectOutput: Boolean = true + + // redirecting the output allows us to test `println` in scripted tests + private def withRedirectedOutput(op: => State): State = { + if redirectOutput then + val savedOut = System.out + val savedErr = System.err + try { + System.setOut(out) + System.setErr(out) + op + } + finally { + System.setOut(savedOut) + System.setErr(savedErr) + } + else op + } + + private def newRun(state: State, reporter: StoreReporter = newStoreReporter) = { + val run = compiler.newRun(rootCtx.fresh.setReporter(reporter), state) + state.copy(context = run.runContext) + } + + private def stripBackTicks(label: String) = + if label.startsWith("`") && label.endsWith("`") then + label.drop(1).dropRight(1) + else + label + + /** Extract possible completions at the index of `cursor` in `expr` */ + protected final def completions(cursor: Int, expr: String, state0: State): List[Completion] = + if expr.startsWith(":") then + ParseResult.commands.collect { + case command if command._1.startsWith(expr) => Completion(command._1, "", List()) + } + else + given state: State = newRun(state0) + compiler + .typeCheck(expr, errorsAllowed = true) + .map { (untpdTree, tpdTree) => + val file = SourceFile.virtual("", expr, maybeIncomplete = true) + val unit = CompilationUnit(file)(using state.context) + unit.untpdTree = untpdTree + unit.tpdTree = tpdTree + given Context = state.context.fresh.setCompilationUnit(unit) + val srcPos = SourcePosition(file, Span(cursor)) + try Completion.completions(srcPos)._2 catch case NonFatal(_) => Nil + } + .getOrElse(Nil) + end completions + + protected def interpret(res: ParseResult)(using state: State): State = { + res match { + case parsed: Parsed if parsed.source.content().mkString.startsWith("//>") => + // Check for magic comments specifying dependencies + println("Please use `:dep com.example::artifact:version` to add dependencies in the REPL") + state + + case parsed: Parsed if parsed.trees.nonEmpty => + compile(parsed, state) + + case SyntaxErrors(_, errs, _) => + displayErrors(errs) + state + + case cmd: Command => + interpretCommand(cmd) + + case SigKill => // TODO + state + + case _ => // new line, empty tree + state + } + } + + /** Compile `parsed` trees and evolve `state` in accordance */ + private def compile(parsed: Parsed, istate: State): State = { + def extractNewestWrapper(tree: untpd.Tree): Name = tree match { + case PackageDef(_, (obj: untpd.ModuleDef) :: Nil) => obj.name.moduleClassName + case _ => nme.NO_NAME + } + + def extractTopLevelImports(ctx: Context): List[tpd.Import] = + unfusedPhases(using ctx).collectFirst { case phase: CollectTopLevelImports => phase.imports }.get + + def contextWithNewImports(ctx: Context, imports: List[tpd.Import]): Context = + if imports.isEmpty then ctx + else + imports.foldLeft(ctx.fresh.setNewScope)((ctx, imp) => + ctx.importContext(imp, imp.symbol(using ctx))) + + given State = { + val state0 = newRun(istate, parsed.reporter) + state0.copy(context = state0.context.withSource(parsed.source)) + } + compiler + .compile(parsed) + .fold( + displayErrors, + { + case (unit: CompilationUnit, newState: State) => + val newestWrapper = extractNewestWrapper(unit.untpdTree) + val newImports = extractTopLevelImports(newState.context) + var allImports = newState.imports + if (newImports.nonEmpty) + allImports += (newState.objectIndex -> newImports) + val newStateWithImports = newState.copy( + imports = allImports, + context = contextWithNewImports(newState.context, newImports) + ) + + val warnings = newState.context.reporter + .removeBufferedMessages(using newState.context) + + inContext(newState.context) { + val (updatedState, definitions) = + if (!ctx.settings.XreplDisableDisplay.value) + renderDefinitions(unit.tpdTree, newestWrapper)(using newStateWithImports) + else + (newStateWithImports, Seq.empty) + + // output is printed in the order it was put in. warnings should be + // shown before infos (eg. typedefs) for the same line. column + // ordering is mostly to make tests deterministic + given Ordering[Diagnostic] = + Ordering[(Int, Int, Int)].on(d => (d.pos.line, -d.level, d.pos.column)) + + (if istate.quiet then warnings else definitions ++ warnings) + .sorted + .foreach(printDiagnostic) + + updatedState + } + } + ) + } + + private def renderDefinitions(tree: tpd.Tree, newestWrapper: Name)(using state: State): (State, Seq[Diagnostic]) = { + given Context = state.context + + def resAndUnit(denot: Denotation) = { + import scala.util.{Success, Try} + val sym = denot.symbol + val name = sym.name.show + val hasValidNumber = Try(name.drop(3).toInt) match { + case Success(num) => num < state.valIndex + case _ => false + } + name.startsWith(str.REPL_RES_PREFIX) && hasValidNumber && sym.info == defn.UnitType + } + + def extractAndFormatMembers(symbol: Symbol): (State, Seq[Diagnostic]) = if (tree.symbol.info.exists) { + val info = symbol.info + val defs = + info.bounds.hi.finalResultType + .membersBasedOnFlags(required = Method, excluded = Accessor | ParamAccessor | Synthetic | Private) + .filterNot { denot => + defn.topClasses.contains(denot.symbol.owner) || denot.symbol.isConstructor + || denot.symbol.name.is(DefaultGetterName) + } + + val vals = + info.fields + .filterNot(_.symbol.isOneOf(ParamAccessor | Private | Synthetic | Artifact | Module)) + .filter(_.symbol.name.is(SimpleNameKind)) + + val typeAliases = + info.bounds.hi.typeMembers.filter(_.symbol.info.isTypeAlias) + + // The wrapper object may fail to initialize if the rhs of a ValDef throws. + // In that case, don't attempt to render any subsequent vals, and mark this + // wrapper object index as invalid. + var failedInit = false + val renderedVals = + val buf = mutable.ListBuffer[Diagnostic]() + for d <- vals do if !failedInit then rendering.renderVal(d) match + case Right(Some(v)) => + buf += v + case Left(e) => + buf += rendering.renderError(e, d) + failedInit = true + case _ => + buf.toList + + if failedInit then + // We limit the returned diagnostics here to `renderedVals`, which will contain the rendered error + // for the val which failed to initialize. Since any other defs, aliases, imports, etc. from this + // input line will be inaccessible, we avoid rendering those so as not to confuse the user. + (state.copy(invalidObjectIndexes = state.invalidObjectIndexes + state.objectIndex), renderedVals) + else + val formattedMembers = + typeAliases.map(rendering.renderTypeAlias) + ++ defs.map(rendering.renderMethod) + ++ renderedVals + val diagnostics = if formattedMembers.isEmpty then rendering.forceModule(symbol) else formattedMembers + (state.copy(valIndex = state.valIndex - vals.count(resAndUnit)), diagnostics) + } + else (state, Seq.empty) + + def isSyntheticCompanion(sym: Symbol) = + sym.is(Module) && sym.is(Synthetic) + + def typeDefs(sym: Symbol): Seq[Diagnostic] = sym.info.memberClasses + .collect { + case x if !isSyntheticCompanion(x.symbol) && !x.symbol.name.isReplWrapperName => + rendering.renderTypeDef(x) + } + + atPhase(typerPhase.next) { + // Display members of wrapped module: + tree.symbol.info.memberClasses + .find(_.symbol.name == newestWrapper.moduleClassName) + .map { wrapperModule => + val (newState, formattedMembers) = extractAndFormatMembers(wrapperModule.symbol) + val formattedTypeDefs = // don't render type defs if wrapper initialization failed + if newState.invalidObjectIndexes.contains(state.objectIndex) then Seq.empty + else typeDefs(wrapperModule.symbol) + (newState, formattedTypeDefs ++ formattedMembers) + } + .getOrElse { + // user defined a trait/class/object, so no module needed + (state, Seq.empty) + } + } + } + + /** Interpret `cmd` to action and propagate potentially new `state` */ + private def interpretCommand(cmd: Command)(using state: State): State = cmd match { + case UnknownCommand(cmd) => + out.println(s"""Unknown command: "$cmd", run ":help" for a list of commands""") + state + + case AmbiguousCommand(cmd, matching) => + out.println(s""""$cmd" matches ${matching.mkString(", ")}. Try typing a few more characters. Run ":help" for a list of commands""") + state + + case Help => + out.println(Help.text) + state + + case Reset(arg) => + val tokens = tokenize(arg) + + if tokens.nonEmpty then + out.println(s"""|Resetting REPL state with the following settings: + | ${tokens.mkString("\n ")} + |""".stripMargin) + else + out.println("Resetting REPL state.") + + resetToInitial(tokens) + initialState + + case Imports => + for { + objectIndex <- state.validObjectIndexes + imp <- state.imports.getOrElse(objectIndex, Nil) + } out.println(imp.show(using state.context)) + state + + case Load(path) => + val file = new JFile(path) + if (file.exists) { + val contents = Using(scala.io.Source.fromFile(file, StandardCharsets.UTF_8.name))(_.mkString).get + run(contents) + } + else { + out.println(s"""Couldn't find file "${file.getCanonicalPath}"""") + state + } + + case Require(path) => + out.println(":require is no longer supported, but has been replaced with :jar. Please use :jar") + state + + case JarCmd(path) => + val jarFile = AbstractFile.getDirectory(path) + if (jarFile == null) + out.println(s"""Cannot add "$path" to classpath.""") + state + else + def flatten(f: AbstractFile): Iterator[AbstractFile] = + if (f.isClassContainer) f.iterator.flatMap(flatten) + else Iterator(f) + + def tryClassLoad(classFile: AbstractFile): Option[String] = { + val input = classFile.input + try { + val reader = new ClassReader(input) + val clsName = reader.getClassName.replace('/', '.') + rendering.myClassLoader.loadClass(clsName) + Some(clsName) + } catch + case _: ClassNotFoundException => None + finally { + input.close() + } + } + + try { + val entries = flatten(jarFile) + + val existingClass = entries.filter(_.ext.isClass).find(tryClassLoad(_).isDefined) + if (existingClass.nonEmpty) + out.println(s"The path '$path' cannot be loaded, it contains a classfile that already exists on the classpath: ${existingClass.get}") + else inContext(state.context): + val jarClassPath = ClassPathFactory.newClassPath(jarFile) + val prevOutputDir = ctx.settings.outputDir.value + + // add to compiler class path + ctx.platform.addToClassPath(jarClassPath) + SymbolLoaders.mergeNewEntries(defn.RootClass, ClassPath.RootPackage, jarClassPath, ctx.platform.classPath) + + // new class loader with previous output dir and specified jar + val prevClassLoader = rendering.classLoader() + val jarClassLoader = fromURLsParallelCapable( + jarClassPath.asURLs, prevClassLoader) + rendering.myClassLoader = new AbstractFileClassLoader( + prevOutputDir, + jarClassLoader, + ctx.settings.XreplInterruptInstrumentation.value + ) + + out.println(s"Added '$path' to classpath.") + } catch { + case e: Throwable => + out.println(s"Failed to load '$path' to classpath: ${e.getMessage}") + } + state + + case KindOf(expr) => + out.println(s"""The :kind command is not currently supported.""") + state + case TypeOf(expr) => + expr match { + case "" => out.println(s":type ") + case _ => + compiler.typeOf(expr)(using newRun(state)).fold( + displayErrors, + res => out.println(res) // result has some highlights + ) + } + state + + case DocOf(expr) => + expr match { + case "" => out.println(s":doc ") + case _ => + compiler.docOf(expr)(using newRun(state)).fold( + displayErrors, + res => out.println(res) + ) + } + state + + case Sh(expr) => + out.println(s"""The :sh command is deprecated. Use `import scala.sys.process._` and `"command".!` instead.""") + state + + case Settings(arg) => arg match + case "" => + given ctx: Context = state.context + for (s <- ctx.settings.userSetSettings(ctx.settingsState).sortBy(_.name)) + out.println(s"${s.name} = ${if s.value == "" then "\"\"" else s.value}") + state + case _ => + rootCtx = setupRootCtx(tokenize(arg).toArray, rootCtx) + state.copy(context = rootCtx) + + case Silent => state.copy(quiet = !state.quiet) + case Dep(dep) => + val depStrings = List(dep) + if depStrings.nonEmpty then + val deps = depStrings.flatMap(DependencyResolver.parseDependency) + if deps.nonEmpty then + DependencyResolver.resolveDependencies(deps) match + case Right(files) => + if files.nonEmpty then + inContext(state.context): + // Update both compiler classpath and classloader + val prevOutputDir = ctx.settings.outputDir.value + val prevClassLoader = rendering.classLoader() + rendering.myClassLoader = DependencyResolver.addToCompilerClasspath( + files, + prevClassLoader, + prevOutputDir + ) + out.println(s"Resolved ${deps.size} dependencies (${files.size} JARs)") + case Left(error) => + out.println(s"Error resolving dependencies: $error") + state + + case Quit => + // end of the world! + state + } + + /** shows all errors nicely formatted */ + private def displayErrors(errs: Seq[Diagnostic])(using state: State): State = { + errs.foreach(printDiagnostic) + state + } + + /** Like ConsoleReporter, but without file paths, -Xprompt displaying, + * and using a PrintStream rather than a PrintWriter so messages aren't re-encoded. */ + private object ReplConsoleReporter extends ConsoleReporter.AbstractConsoleReporter { + override def posFileStr(pos: SourcePosition) = "" // omit file paths + override def printMessage(msg: String): Unit = out.println(msg) + override def echoMessage(msg: String): Unit = printMessage(msg) + override def flush()(using Context): Unit = out.flush() + } + + /** Print warnings & errors using ReplConsoleReporter, and info straight to out */ + private def printDiagnostic(dia: Diagnostic)(using state: State) = dia.level match + case interfaces.Diagnostic.INFO => out.println(dia.msg) // print REPL's special info diagnostics directly to out + case _ => ReplConsoleReporter.doReport(dia)(using state.context) + +end ReplDriver +object ReplDriver: + def pprintImport = "import pprint.pprintln\n" \ No newline at end of file diff --git a/sbt-bridge/src-bootstrapped/ConsoleInterface.java b/sbt-bridge/src-bootstrapped/ConsoleInterface.java index 3841457bcbe8..3ba4e011c8e3 100644 --- a/sbt-bridge/src-bootstrapped/ConsoleInterface.java +++ b/sbt-bridge/src-bootstrapped/ConsoleInterface.java @@ -10,7 +10,7 @@ import xsbti.Logger; import dotty.tools.dotc.core.Contexts.Context; -import dotty.tools.repl.; +import dotty.tools.repl.ReplDriver; import dotty.tools.repl.State; public class ConsoleInterface { @@ -49,7 +49,7 @@ public void run( state = driver.run(initialCommands, state); // TODO handle failure during initialisation - state = driver.runUntilQuit(state, null); + state = driver.runUntilQuit(state); driver.run(cleanupCommands, state); } } From 772185a8609b31277c3cdadfadc529fccfe2ff9b Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 20:47:52 -0800 Subject: [PATCH 30/42] . --- project/Build.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index dd5d2b1c2cb6..79136e201232 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1293,9 +1293,7 @@ object Build { assembly / assemblyJarName := s"scala3-repl-embedded-${version.value}.jar", // Add scala3-repl to assembly classpath without making it a published dependency assembly / fullClasspath := { - val replJar = (`scala3-repl` / Compile / packageBin).value - val cp = (assembly / fullClasspath).value - cp :+ Attributed.blank(replJar) + (Compile / fullClasspath).value ++ (`scala3-repl` / assembly / fullClasspath).value }, assembly / test := {}, // Don't run tests for assembly // Exclude scala-library and jline from assembly (users provide them on classpath) From a9ac383edf73e75ec5aee02da3d7cbb0746b5e1b Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 20:48:05 -0800 Subject: [PATCH 31/42] . --- project/Build.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 79136e201232..ee5d51f2d338 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1298,8 +1298,7 @@ object Build { assembly / test := {}, // Don't run tests for assembly // Exclude scala-library and jline from assembly (users provide them on classpath) assembly / assemblyExcludedJars := { - val cp = (assembly / fullClasspath).value - cp.filter { jar => + (assembly / fullClasspath).value.filter { jar => val name = jar.data.getName name.contains("scala-library") || name.contains("scala3-library") || name.contains("jline") } From a01ae3e921ea48d8999dbc3d050379b3eb9c9eed Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 20:50:23 -0800 Subject: [PATCH 32/42] . --- project/Build.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project/Build.scala b/project/Build.scala index ee5d51f2d338..3650914ced4f 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1329,7 +1329,9 @@ object Build { relativePath.startsWith("org/jline/") // This is the entrypoint to the embedded Scala REPL so don't shade it val shouldKeepInPlace = relativePath.startsWith("scala/tools/repl/")|| + // These are manually shaded so leave them alone relativePath.startsWith("dotty/shaded/") || + // This needs to be inside scala/collection so cannot be moved relativePath.startsWith("scala/collection/internal/pprint/") if (shouldDelete) IO.delete(file) From c9467f2fc5b8ea727866e4b189217fd8718d95b8 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 20:55:46 -0800 Subject: [PATCH 33/42] . --- project/Build.scala | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 3650914ced4f..86aa5eb63494 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1200,9 +1200,7 @@ object Build { FilesInfo.lastModified, FilesInfo.exists) { _ => s.log.info(s"Downloading and processing shaded sources to $dest...") - if (dest.exists) { - IO.delete(dest) - } + if (dest.exists) IO.delete(dest) IO.createDirectory(dest) for((org, name, version) <- dependencies) { @@ -1287,7 +1285,6 @@ object Build { "org.jline" % "jline-terminal" % "3.29.0", "org.jline" % "jline-terminal-jni" % "3.29.0", ), - // Source directories Compile / unmanagedSourceDirectories := Seq(baseDirectory.value / "src"), // Assembly configuration for shading assembly / assemblyJarName := s"scala3-repl-embedded-${version.value}.jar", @@ -1300,10 +1297,15 @@ object Build { assembly / assemblyExcludedJars := { (assembly / fullClasspath).value.filter { jar => val name = jar.data.getName - name.contains("scala-library") || name.contains("scala3-library") || name.contains("jline") + + name.contains("scala-library") || + // Avoid shading JLine because shading it causes problems with + // its service discovery and JNI-related logic + // This is the entrypoint to the embedded Scala REPL so don't shade it + name.contains("jline") } }, - // Post-process assembly to physically move files into dotty/tools/repl/shaded/ subfolder + assembly := { val originalJar = assembly.value val log = streams.value.log @@ -1320,22 +1322,14 @@ object Build { if (file.isFile) { val relativePath = file.relativeTo(tmpDir).get.getPath - // Avoid shading JLine because shading it causes problems with - // its service discovery and JNI-related logic - val shouldDelete = - relativePath.startsWith("scala/") && - !relativePath.startsWith("scala/tools/") && - !relativePath.startsWith("scala/collection/internal/pprint/") || - relativePath.startsWith("org/jline/") - // This is the entrypoint to the embedded Scala REPL so don't shade it - val shouldKeepInPlace = relativePath.startsWith("scala/tools/repl/")|| + val shouldKeepInPlace = + relativePath.startsWith("scala/tools/repl/")|| // These are manually shaded so leave them alone relativePath.startsWith("dotty/shaded/") || // This needs to be inside scala/collection so cannot be moved relativePath.startsWith("scala/collection/internal/pprint/") - if (shouldDelete) IO.delete(file) - else if (!shouldKeepInPlace) { + if (!shouldKeepInPlace) { val newPath = shadedDir / relativePath IO.createDirectory(newPath.getParentFile) IO.move(file, newPath) @@ -1356,7 +1350,6 @@ object Build { // Use the shaded assembly jar as our packageBin for publishing Compile / packageBin := (Compile / assembly).value, publish / skip := false, - ) // ============================================================================================== From 8f60a5ccd6b7fe979881a83275958a597a1d09be Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 20:59:36 -0800 Subject: [PATCH 34/42] . --- project/Build.scala | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 86aa5eb63494..e3ba3269c970 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1297,7 +1297,8 @@ object Build { assembly / assemblyExcludedJars := { (assembly / fullClasspath).value.filter { jar => val name = jar.data.getName - + // Filter out the `scala-library` here otherwise it conflicts with the + // `scala-library` pulled in via `assembly / fullClasspath` name.contains("scala-library") || // Avoid shading JLine because shading it causes problems with // its service discovery and JNI-related logic @@ -1318,28 +1319,27 @@ object Build { val shadedDir = tmpDir / "dotty" / "isolated" IO.createDirectory(shadedDir) - (tmpDir ** "*").get.foreach { file => - if (file.isFile) { - val relativePath = file.relativeTo(tmpDir).get.getPath - - val shouldKeepInPlace = - relativePath.startsWith("scala/tools/repl/")|| - // These are manually shaded so leave them alone - relativePath.startsWith("dotty/shaded/") || - // This needs to be inside scala/collection so cannot be moved - relativePath.startsWith("scala/collection/internal/pprint/") - - if (!shouldKeepInPlace) { - val newPath = shadedDir / relativePath - IO.createDirectory(newPath.getParentFile) - IO.move(file, newPath) - } + for(file <- (tmpDir ** "*").get if file.isFile) { + val relativePath = file.relativeTo(tmpDir).get.getPath + + val shouldKeepInPlace = + relativePath.startsWith("scala/tools/repl/")|| + // These are manually shaded so leave them alone + relativePath.startsWith("dotty/shaded/") || + // This needs to be inside scala/collection so cannot be moved + relativePath.startsWith("scala/collection/internal/pprint/") + + if (!shouldKeepInPlace) { + val newPath = shadedDir / relativePath + IO.createDirectory(newPath.getParentFile) + IO.move(file, newPath) } } - val filesToZip = (tmpDir ** "*").get.filter(_.isFile).map { f => - (f, f.relativeTo(tmpDir).get.getPath) - } + val filesToZip = + for(f <- (tmpDir ** "*").get if f.isFile) + yield (f, f.relativeTo(tmpDir).get.getPath) + IO.zip(filesToZip, originalJar, None) log.info(s"Assembly post-processing complete") From b6b098c07b44255a8155569187e92809b838183d Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 21:06:21 -0800 Subject: [PATCH 35/42] . --- .../src/scala/tools/repl/EmbeddedReplMain.scala | 12 +++--------- repl/src/dotty/tools/repl/ReplDriver.scala | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala index 47a414a4000f..0d38d2304651 100644 --- a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala @@ -21,11 +21,7 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { if (loaded != null) return loaded val shadedPath = (SHADED_PREFIX + name).replace('.', '/') + ".class" - val is0 = try { - Option(super.getResourceAsStream(shadedPath)) - }catch{ - case _: Exception => None - } + val is0 = scala.util.Try(Option(super.getResourceAsStream(shadedPath))).toOption.flatten is0 match{ case Some(is) => @@ -60,19 +56,17 @@ object EmbeddedReplMain { else Array("-classpath", System.getProperty("java.class.path")) ++ args val unshadingClassLoader = new UnshadingClassLoader(getClass.getClassLoader) - val replDriverClass = unshadingClassLoader.loadClass("dotty.tools.repl.ReplDriver") - val someCls = unshadingClassLoader.loadClass("scala.Some") + val pprintImport = replDriverClass.getMethod("pprintImport").invoke(null) val replDriver = replDriverClass.getConstructors().head.newInstance( /*settings*/ argsWithClasspath, /*out*/ System.out, /*classLoader*/ someCls.getConstructors().head.newInstance(getClass.getClassLoader), - /*extraPredef*/ "" + /*extraPredef*/ pprintImport ) replDriverClass.getMethod("tryRunning").invoke(replDriver) - } } diff --git a/repl/src/dotty/tools/repl/ReplDriver.scala b/repl/src/dotty/tools/repl/ReplDriver.scala index 298c44487f2e..4c54b2959c32 100644 --- a/repl/src/dotty/tools/repl/ReplDriver.scala +++ b/repl/src/dotty/tools/repl/ReplDriver.scala @@ -706,4 +706,4 @@ class ReplDriver(settings: Array[String], end ReplDriver object ReplDriver: - def pprintImport = "import pprint.pprintln\n" \ No newline at end of file + def pprintImport = "import dotty.shaded.pprint.pprintln\n" \ No newline at end of file From e25b15a792478e0d98adbf2a81fde36f2f4cbf75 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 21:06:56 -0800 Subject: [PATCH 36/42] . --- project/Build.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Build.scala b/project/Build.scala index e3ba3269c970..dc9179f3cfd0 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1184,7 +1184,7 @@ object Build { val lm = dependencyResolution.value val dependencies = Seq( - ("com.lihaoyi", "pprint_3", "0.9.3"), + ("com.lihaoyi", "pprint_3", "0.9.5"), ("com.lihaoyi", "fansi_3", "0.5.1"), ("com.lihaoyi", "sourcecode_3", "0.4.4"), ) From d139dfbabb8633ec73521e36c7a4ca3757e9a42c Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 21:10:30 -0800 Subject: [PATCH 37/42] . --- project/Build.scala | 11 ++++------- repl/test/dotty/tools/repl/ReplCompilerTests.scala | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index dc9179f3cfd0..19da2fea9ecd 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1224,8 +1224,7 @@ object Build { val scalaFiles = (dest ** "*.scala").get - // Define patches as a map from search text to replacement text - val patches = Map( + val patches = Map( // Define patches as a map from search text to replacement text "import scala" -> "import _root_.scala", " scala.collection." -> " _root_.scala.collection.", "def apply(c: Char): Trie[T]" -> "def apply(c: Char): Trie[T] | Null", @@ -1245,11 +1244,9 @@ object Build { var processedText = "package dotty.shaded\n" + text // Apply patches and count usage - patches.foreach { case (search, replacement) => - if (processedText.contains(search)) { - processedText = processedText.replace(search, replacement) - patchUsageCounter(search) += 1 - } + for((search, replacement) <- patches if processedText.contains(search)){ + processedText = processedText.replace(search, replacement) + patchUsageCounter(search) += 1 } IO.write(file, processedText) diff --git a/repl/test/dotty/tools/repl/ReplCompilerTests.scala b/repl/test/dotty/tools/repl/ReplCompilerTests.scala index c148be0d27ca..08917b8d02da 100644 --- a/repl/test/dotty/tools/repl/ReplCompilerTests.scala +++ b/repl/test/dotty/tools/repl/ReplCompilerTests.scala @@ -78,7 +78,7 @@ class ReplCompilerTests extends ReplTest: assertEquals(1, summon[State].imports.size) run("""mutable.Map("one" -> 1)""") assertEquals( - "val res0: scala.collection.mutable.Map[String, Int] = HashMap(one -> 1)", + "val res0: scala.collection.mutable.Map[String, Int] = HashMap(\"one\" -> 1)", storedOutput().trim ) } From ee4ed2b89080f3d21401c3699ea883812e383920 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 20 Nov 2025 22:16:19 -0800 Subject: [PATCH 38/42] . --- repl/test-resources/repl/i6474 | 8 ++++---- repl/test-resources/type-printer/vals | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/repl/test-resources/repl/i6474 b/repl/test-resources/repl/i6474 index 1390dce7042f..4885ea62254c 100644 --- a/repl/test-resources/repl/i6474 +++ b/repl/test-resources/repl/i6474 @@ -5,9 +5,9 @@ scala> object Foo2 { type T[+A] = [B] =>> (A, B) } scala> object Foo3 { type T[+A] = [B] =>> [C] =>> (A, B) } // defined object Foo3 scala> ((1, 2): Foo1.T[Int]): Foo1.T[Any] -val res0: Foo1.T[Any] = (1,2) +val res0: Foo1.T[Any] = (1, 2) scala> ((1, 2): Foo2.T[Int][Int]): Foo2.T[Any][Int] -val res1: Foo2.T[Any][Int] = (1,2) +val res1: Foo2.T[Any][Int] = (1, 2) scala> (1, 2): Foo3.T[Int][Int] -- [E056] Syntax Error: -------------------------------------------------------- 1 | (1, 2): Foo3.T[Int][Int] @@ -19,6 +19,6 @@ val res2: Foo3.T[Any][Int][Int] = (1,2) scala> object Foo3 { type T[A] = [B] =>> [C] =>> (A, B) } // defined object Foo3 scala> ((1, 2): Foo3.T[Int][Int][Int]) -val res3: Foo3.T[Int][Int][Int] = (1,2) +val res3: Foo3.T[Int][Int][Int] = (1, 2) scala> ((1, 2): Foo3.T[Int][Int][Int]) -val res4: Foo3.T[Int][Int][Int] = (1,2) \ No newline at end of file +val res4: Foo3.T[Int][Int][Int] = (1, 2) \ No newline at end of file diff --git a/repl/test-resources/type-printer/vals b/repl/test-resources/type-printer/vals index 21cd311cf284..cfffdf183c99 100644 --- a/repl/test-resources/type-printer/vals +++ b/repl/test-resources/type-printer/vals @@ -7,4 +7,4 @@ val xs: List[Int] = List(1) scala> scala.util.Try(1) val res0: scala.util.Try[Int] = Success(1) scala> Map(1 -> "one") -val res1: Map[Int, String] = Map(1 -> one) +val res1: Map[Int, String] = Map(1 -> "one") From 919f02cbe200870f2f30823cb8683a653cb76900 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 21 Nov 2025 01:29:15 -0800 Subject: [PATCH 39/42] . --- project/Build.scala | 6 +-- .../dotty/embedded}/EmbeddedReplMain.scala | 38 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) rename {repl-shaded/src/scala/tools/repl => repl-embedded/src/dotty/embedded}/EmbeddedReplMain.scala (57%) diff --git a/project/Build.scala b/project/Build.scala index 19da2fea9ecd..3e21224e4288 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1265,7 +1265,7 @@ object Build { } ) - lazy val `scala3-repl-embedded` = project.in(file("repl-shaded")) + lazy val `scala3-repl-embedded` = project.in(file("repl-embedded")) .dependsOn(`scala-library-bootstrapped`) .enablePlugins(sbtassembly.AssemblyPlugin) .settings(publishSettings) @@ -1320,8 +1320,8 @@ object Build { val relativePath = file.relativeTo(tmpDir).get.getPath val shouldKeepInPlace = - relativePath.startsWith("scala/tools/repl/")|| - // These are manually shaded so leave them alone + relativePath.startsWith("dotty/embedded/")|| + // These are manually shaded when vendored/patched so leave them alone relativePath.startsWith("dotty/shaded/") || // This needs to be inside scala/collection so cannot be moved relativePath.startsWith("scala/collection/internal/pprint/") diff --git a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala b/repl-embedded/src/dotty/embedded/EmbeddedReplMain.scala similarity index 57% rename from repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala rename to repl-embedded/src/dotty/embedded/EmbeddedReplMain.scala index 0d38d2304651..92b19640ea91 100644 --- a/repl-shaded/src/scala/tools/repl/EmbeddedReplMain.scala +++ b/repl-embedded/src/dotty/embedded/EmbeddedReplMain.scala @@ -1,26 +1,29 @@ -package scala.tools.repl +package dotty.embedded import java.net.{URL, URLClassLoader} import java.io.InputStream /** * A classloader that remaps shaded classes back to their original package names. - * - * This classloader intercepts class loading requests and remaps them from - * dotty.tools.repl.shaded.* back to their original package names, allowing the - * shaded classes to be loaded as if they were in their original packages. - * - * The scala.* packages are not shaded, so they pass through normally. */ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { - private val SHADED_PREFIX = "dotty.isolated." + // dotty.isolated classes are loaded only within the REPL impl classloader. + // They exist in the enclosing classpath relocated within the dotty.isolated + // package, but are relocated to their proper package when the REPL impl + // classloader loads them + private val ISOLATED_PREFIX = "dotty.isolated." override def loadClass(name: String, resolve: Boolean): Class[?] = { val loaded = findLoadedClass(name) if (loaded != null) return loaded - val shadedPath = (SHADED_PREFIX + name).replace('.', '/') + ".class" + // dotty.shaded classes are loaded separately between the REPL line classloader + // and the REPL impl classloader, but at the same path because the REPL line + // classloader doesn't tolerate relocating classfiles + val shadedPath = (if (name.startsWith("dotty.shaded.")) name else ISOLATED_PREFIX + name) + .replace('.', '/') + ".class" + val is0 = scala.util.Try(Option(super.getResourceAsStream(shadedPath))).toOption.flatten is0 match{ @@ -29,17 +32,22 @@ class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { val bytes = is.readAllBytes() val clazz = defineClass(name, bytes, 0, bytes.length) if (resolve) resolveClass(clazz) - return clazz + clazz } finally is.close() - case None => super.loadClass(name, resolve) + case None => + // These classes are loaded shared between all classloaders, because + // they misbehave if loaded multiple times in separate classloaders + if (name.startsWith("java.") || name.startsWith("org.jline.")) parent.loadClass(name) + // Other classes loaded by the `UnshadingClassLoader` *must* be found in the + // `dotty.isolated` package. If they're not there, throw an error rather than + // trying to look for them at their normal package path, to ensure we're not + // accidentally pulling stuff in from the enclosing classloader + else throw new ClassNotFoundException(name) } } override def getResourceAsStream(name: String): InputStream | Null = { - val shadedPath = SHADED_PREFIX.replace('.', '/') + name - val shadedStream = super.getResourceAsStream(shadedPath) - if (shadedStream != null) return shadedStream - else super.getResourceAsStream(name) + super.getResourceAsStream(ISOLATED_PREFIX.replace('.', '/') + name) } } From bcae8fbf0d8d56487ae6418e16b9e401afe2609c Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 21 Nov 2025 01:34:38 -0800 Subject: [PATCH 40/42] . --- repl/src/dotty/tools/repl/Rendering.scala | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/repl/src/dotty/tools/repl/Rendering.scala b/repl/src/dotty/tools/repl/Rendering.scala index 366a5c0479d0..7b71d16ed5d1 100644 --- a/repl/src/dotty/tools/repl/Rendering.scala +++ b/repl/src/dotty/tools/repl/Rendering.scala @@ -34,19 +34,13 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): .render try - // normally, if we used vanilla JDK and layered classloaders, we wouldnt need reflection. - // however PPrint works by runtime type testing to deconstruct values. This is - // sensitive to which classloader instantiates the object under test, i.e. - // `value` is constructed inside the repl classloader. Testing for - // `value.isInstanceOf[scala.Product]` in this classloader fails (JDK AppClassLoader), - // because repl classloader has two layers where it can redefine `scala.Product`: - // - `new URLClassLoader` constructed with contents of the `-classpath` setting - // - `AbstractFileClassLoader` also might instrument the library code to support interrupt. - // Due the possible interruption instrumentation, it is unlikely that we can get - // rid of reflection here. + // PPrint needs to do type-tests against scala-library classes, but the `classLoader()` + // used in the REPL typically has a its own copy of such classes to support + // `-XreplInterruptInstrumentation`. Thus we need to use the copy of PPrint from the + // REPL-line `classLoader()` rather than our own REPL-impl classloader in order for it + // to work val cl = classLoader() val pprintCls = Class.forName("dotty.shaded.pprint.PPrinter$Color$", false, cl) - val fansiStrCls = Class.forName("dotty.shaded.fansi.Str", false, cl) val Color = pprintCls.getField("MODULE$").get(null) val Color_apply = pprintCls.getMethod("apply", classOf[Any], // value @@ -58,10 +52,11 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): classOf[Boolean], // show field names ) - val FansiStr_render = fansiStrCls.getMethod("render") val fansiStr = Color_apply.invoke(Color, value, width, height, 2, initialOffset, false, true) - FansiStr_render.invoke(fansiStr).asInstanceOf[String] + fansiStr.toString catch + // If classloading fails for whatever reason, try to fallback to our own version + // of PPrint. Won't be as good, but better than blowing up with an exception case ex: ClassNotFoundException => fallback() case ex: NoSuchMethodException => fallback() } From 1fbb3a5d9af5e784e3e66b9215031df56569c43c Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 22 Nov 2025 16:09:38 +0800 Subject: [PATCH 41/42] . --- repl/test-resources/repl/i6474 | 2 +- repl/test-resources/type-printer/prefixless | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/repl/test-resources/repl/i6474 b/repl/test-resources/repl/i6474 index 4885ea62254c..79e8cd83a28d 100644 --- a/repl/test-resources/repl/i6474 +++ b/repl/test-resources/repl/i6474 @@ -15,7 +15,7 @@ scala> (1, 2): Foo3.T[Int][Int] | Missing type parameter for Foo3.T[Int][Int] 1 error found scala> ((1, 2): Foo3.T[Int][Int][Int]): Foo3.T[Any][Int][Int] -val res2: Foo3.T[Any][Int][Int] = (1,2) +val res2: Foo3.T[Any][Int][Int] = (1, 2) scala> object Foo3 { type T[A] = [B] =>> [C] =>> (A, B) } // defined object Foo3 scala> ((1, 2): Foo3.T[Int][Int][Int]) diff --git a/repl/test-resources/type-printer/prefixless b/repl/test-resources/type-printer/prefixless index 0ade08eb186b..df02b46988a2 100644 --- a/repl/test-resources/type-printer/prefixless +++ b/repl/test-resources/type-printer/prefixless @@ -1,10 +1,10 @@ scala> List(1,2,3) val res0: List[Int] = List(1, 2, 3) scala> Map("foo" -> 1) -val res1: Map[String, Int] = Map(foo -> 1) +val res1: Map[String, Int] = Map("foo" -> 1) scala> Seq('a','b') -val res2: Seq[Char] = List(a, b) +val res2: Seq[Char] = List('a', 'b') scala> Set(4, 5) val res3: Set[Int] = Set(4, 5) scala> Iterator(1) -val res4: Iterator[Int] = +val res4: Iterator[Int] = non-empty iterator From 6748da06099cde27c6866071fd57c7b3077f92f5 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 22 Nov 2025 19:22:10 +0800 Subject: [PATCH 42/42] . --- repl/test-resources/repl/i1374 | 38 ++++++- repl/test-resources/repl/i4852 | 2 +- repl/test-resources/repl/i5218 | 6 +- .../repl/settings-repl-max-print-elements | 102 +++++++++++++++++- 4 files changed, 139 insertions(+), 9 deletions(-) diff --git a/repl/test-resources/repl/i1374 b/repl/test-resources/repl/i1374 index 2e0b5be900af..4346c92bb9b7 100644 --- a/repl/test-resources/repl/i1374 +++ b/repl/test-resources/repl/i1374 @@ -2,8 +2,40 @@ scala> implicit class Padder(val sb: StringBuilder) extends AnyVal { infix def p // defined class Padder def Padder(sb: StringBuilder): Padder scala> val greeting = new StringBuilder("Hello, kitteh!") -val greeting: StringBuilder = Hello, kitteh! +val greeting: StringBuilder = IndexedSeq( + 'H', + 'e', + 'l', + 'l', + 'o', + ',', + ' ', + 'k', + 'i', + 't', + 't', + 'e', + 'h', + '!' +) + scala> val a = greeting pad2 20 -val a: StringBuilder = Hello, kitteh!* +val a: StringBuilder = IndexedSeq( + 'H', + 'e', + 'l', + 'l', + 'o', + ',', + ' ', + 'k', + 'i', + 't', + 't', + 'e', + 'h', + '!', + '*' +) scala> val farewell = new StringBuilder("U go now.") // I hatez long bye-bye. -val farewell: StringBuilder = U go now. +val farewell: StringBuilder = IndexedSeq('U', ' ', 'g', 'o', ' ', 'n', 'o', 'w', '.') diff --git a/repl/test-resources/repl/i4852 b/repl/test-resources/repl/i4852 index 32c126578259..32ba96d053e1 100644 --- a/repl/test-resources/repl/i4852 +++ b/repl/test-resources/repl/i4852 @@ -1,6 +1,6 @@ scala> inline def foo[T](t : T*) : Any = t def foo[T](t: T*): Any scala> foo(1, "hi", false) -val res0: Any = ArraySeq(1, hi, false) +val res0: Any = ArraySeq(1, "hi", false) scala> foo() val res1: Any = ArraySeq() diff --git a/repl/test-resources/repl/i5218 b/repl/test-resources/repl/i5218 index abe63009ef74..9160df2fb7fb 100644 --- a/repl/test-resources/repl/i5218 +++ b/repl/test-resources/repl/i5218 @@ -1,6 +1,6 @@ scala> val tuple = (1, "2", 3L) -val tuple: (Int, String, Long) = (1,2,3) +val tuple: (Int, String, Long) = (1, "2", 3L) scala> 0.0 *: tuple -val res0: (Double, Int, String, Long) = (0.0,1,2,3) +val res0: (Double, Int, String, Long) = (0.0, 1, "2", 3L) scala> tuple ++ tuple -val res1: Int *: String *: Long *: tuple.type = (1,2,3,1,2,3) +val res1: Int *: String *: Long *: tuple.type = (1, "2", 3L, 1, "2", 3L) diff --git a/repl/test-resources/repl/settings-repl-max-print-elements b/repl/test-resources/repl/settings-repl-max-print-elements index 8afb244e1cf9..edb42d1f60a3 100644 --- a/repl/test-resources/repl/settings-repl-max-print-elements +++ b/repl/test-resources/repl/settings-repl-max-print-elements @@ -1,8 +1,106 @@ scala> 1.to(200).toList -val res0: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200) +val res0: List[Int] = List( + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, +... scala>:settings -Vrepl-max-print-elements:20 scala> 1.to(300).toList -val res1: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300) +val res1: List[Int] = List( + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, +...