From 17e82e6c49ad0366d2d5e1553c463cc1d1558086 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 3 Oct 2025 08:19:35 -0600 Subject: [PATCH 01/22] initial demo works --- .../src/dotty/tools/repl/ReplDriver.scala | 31 +++++++++++++++++-- project/Build.scala | 1 + 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index befb3de9a941..39b933b1b9ad 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -301,8 +301,35 @@ class ReplDriver(settings: Array[String], protected def interpret(res: ParseResult)(using state: State): State = { res match { - case parsed: Parsed if parsed.trees.nonEmpty => - compile(parsed, state) + case parsed: Parsed => + // Check for magic comments specifying dependencies + val sourceCode = parsed.source.content().mkString + val depStrings = DependencyResolver.extractDependencies(sourceCode) + + 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") + + // Only compile if there are actual trees to compile + if parsed.trees.nonEmpty then + compile(parsed, state) + else + state case SyntaxErrors(_, errs, _) => displayErrors(errs) diff --git a/project/Build.scala b/project/Build.scala index 8c233af8b2b3..65e034de1785 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -717,6 +717,7 @@ object Build { "org.jline" % "jline-terminal" % "3.29.0", "org.jline" % "jline-terminal-jni" % "3.29.0", // needed for Windows ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13), + "io.get-coursier" % "interface" % "1.0.19", // used by the REPL for dependency resolution ), (Compile / sourceGenerators) += ShadedSourceGenerator.task.taskValue, From 44abbbfe1ddfcba037b927d5a62fd3cadbc4aa9d Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 3 Oct 2025 08:37:25 -0600 Subject: [PATCH 02/22] . --- .../dotty/tools/repl/DependencyResolver.scala | 121 ++++++++++++++++++ project/Build.scala | 1 + 2 files changed, 122 insertions(+) create mode 100644 compiler/src/dotty/tools/repl/DependencyResolver.scala diff --git a/compiler/src/dotty/tools/repl/DependencyResolver.scala b/compiler/src/dotty/tools/repl/DependencyResolver.scala new file mode 100644 index 000000000000..e01590435d00 --- /dev/null +++ b/compiler/src/dotty/tools/repl/DependencyResolver.scala @@ -0,0 +1,121 @@ +package dotty.tools.repl + +import scala.language.unsafeNulls + +import java.io.File +import java.net.{URL, URLClassLoader} +import scala.jdk.CollectionConverters.* +import scala.util.control.NonFatal + +import coursierapi.{Repository, Dependency, MavenRepository} +import com.virtuslab.using_directives.UsingDirectivesProcessor +import com.virtuslab.using_directives.custom.model.{Path, StringValue, Value} + +/** Handles dependency resolution using Coursier for the REPL */ +object DependencyResolver: + + /** Parse a dependency string of the form `org::artifact:version` or `org:artifact:version` + * and return the (organization, artifact, version) triple if successful. + * + * Supports both Maven-style (single colon) and Scala-style (double colon) notation: + * - Maven: `com.lihaoyi:scalatags_3:0.13.1` + * - Scala: `com.lihaoyi::scalatags:0.13.1` (automatically appends _3) + */ + def parseDependency(dep: String): Option[(String, String, String)] = + // Match either org:artifact:version or org::artifact:version + val pattern = """([^:]+)::?([^:]+):([^:]+)""".r + + dep match + case pattern(org, artifact, version) => + val isScalaStyle = dep.contains("::") + val fullArtifact = if isScalaStyle then s"${artifact}_3" else artifact + Some((org, fullArtifact, version)) + case _ => None + + /** Extract all dependencies from using directives in source code */ + def extractDependencies(sourceCode: String): List[String] = + try + val processor = new UsingDirectivesProcessor() + val directives = processor.extract(sourceCode.toCharArray) + + val deps = scala.collection.mutable.ListBuffer[String]() + + directives.asScala.foreach { directive => + val flatMap = directive.getFlattenedMap + flatMap.asScala.foreach { case (path, values) => + // Check if this is a "dep" directive (path segments: ["dep"]) + if path.getPath.asScala.toList == List("dep") then + values.asScala.foreach { value => + value match + case strValue: StringValue => + deps += strValue.get() + case _ => + } + } + } + + deps.toList + catch + case NonFatal(e) => + // If parsing fails, fall back to empty list + Nil + + /** Resolve dependencies using Coursier Interface and return the classpath as a list of File objects */ + def resolveDependencies(dependencies: List[(String, String, String)]): Either[String, List[File]] = + if dependencies.isEmpty then Right(Nil) + else + try + // Add Maven Central and Sonatype repositories + val repos = Array( + MavenRepository.of("https://repo1.maven.org/maven2"), + MavenRepository.of("https://oss.sonatype.org/content/repositories/releases") + ) + + // Create dependency objects + val deps = dependencies.map { case (org, artifact, version) => + Dependency.of(org, artifact, version) + }.toArray + + val fetch = coursierapi.Fetch.create() + .withRepositories(repos*) + .withDependencies(deps*) + + val files = fetch.fetch().asScala.toList + Right(files) + + catch + case NonFatal(e) => + Left(s"Failed to resolve dependencies: ${e.getMessage}") + + /** Add resolved dependencies to the compiler classpath and classloader. + * Returns the new classloader. + * + * This follows the same pattern as the `:jar` command. + */ + def addToCompilerClasspath( + files: List[File], + prevClassLoader: ClassLoader, + prevOutputDir: dotty.tools.io.AbstractFile + )(using ctx: dotty.tools.dotc.core.Contexts.Context): AbstractFileClassLoader = + import dotty.tools.dotc.classpath.ClassPathFactory + import dotty.tools.dotc.core.SymbolLoaders + import dotty.tools.dotc.core.Symbols.defn + import dotty.tools.io.* + import dotty.tools.runner.ScalaClassLoader.fromURLsParallelCapable + + // Create a classloader with all the resolved JAR files + val urls = files.map(_.toURI.toURL).toArray + val depsClassLoader = new URLClassLoader(urls, prevClassLoader) + + // Add each JAR to the compiler's classpath + for file <- files do + val jarFile = AbstractFile.getDirectory(file.getAbsolutePath) + if jarFile != null then + val jarClassPath = ClassPathFactory.newClassPath(jarFile) + ctx.platform.addToClassPath(jarClassPath) + SymbolLoaders.mergeNewEntries(defn.RootClass, ClassPath.RootPackage, jarClassPath, ctx.platform.classPath) + + // Create new classloader with previous output dir and resolved dependencies + new AbstractFileClassLoader(prevOutputDir, depsClassLoader) + +end DependencyResolver diff --git a/project/Build.scala b/project/Build.scala index 65e034de1785..6c43815cf587 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -718,6 +718,7 @@ object Build { "org.jline" % "jline-terminal-jni" % "3.29.0", // needed for Windows ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13), "io.get-coursier" % "interface" % "1.0.19", // used by the REPL for dependency resolution + "org.virtuslab" % "using_directives" % "1.1.4", // used by the REPL for parsing magic comments ), (Compile / sourceGenerators) += ShadedSourceGenerator.task.taskValue, From 34a8fb7c77b0440a67552e925946e05d955109ae Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 3 Oct 2025 08:51:03 -0600 Subject: [PATCH 03/22] . --- .../dotty/tools/repl/DependencyResolver.scala | 58 ++++++++----------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/compiler/src/dotty/tools/repl/DependencyResolver.scala b/compiler/src/dotty/tools/repl/DependencyResolver.scala index e01590435d00..96eb4d11cac0 100644 --- a/compiler/src/dotty/tools/repl/DependencyResolver.scala +++ b/compiler/src/dotty/tools/repl/DependencyResolver.scala @@ -22,43 +22,34 @@ object DependencyResolver: * - Scala: `com.lihaoyi::scalatags:0.13.1` (automatically appends _3) */ def parseDependency(dep: String): Option[(String, String, String)] = - // Match either org:artifact:version or org::artifact:version - val pattern = """([^:]+)::?([^:]+):([^:]+)""".r - dep match - case pattern(org, artifact, version) => - val isScalaStyle = dep.contains("::") - val fullArtifact = if isScalaStyle then s"${artifact}_3" else artifact - Some((org, fullArtifact, version)) - case _ => None + case s"$org::$artifact:$version" => Some((org, s"${artifact}_3", version)) + case s"$org:$artifact:$version" => Some((org, artifact, version)) + case _ => + System.err.println("Unable to parse dependency \"" + dep + "\"") + None /** Extract all dependencies from using directives in source code */ def extractDependencies(sourceCode: String): List[String] = try - val processor = new UsingDirectivesProcessor() - val directives = processor.extract(sourceCode.toCharArray) - - val deps = scala.collection.mutable.ListBuffer[String]() - - directives.asScala.foreach { directive => - val flatMap = directive.getFlattenedMap - flatMap.asScala.foreach { case (path, values) => - // Check if this is a "dep" directive (path segments: ["dep"]) - if path.getPath.asScala.toList == List("dep") then - values.asScala.foreach { value => - value match - case strValue: StringValue => - deps += strValue.get() - case _ => - } - } - } + val directives = new UsingDirectivesProcessor().extract(sourceCode.toCharArray) + val deps = scala.collection.mutable.Buffer[String]() + + for + directive <- directives.asScala + (path, values) <- directive.getFlattenedMap.asScala + do + if path.getPath.asScala.toList == List("dep") then + values.asScala.foreach { + case strValue: StringValue => deps += strValue.get() + case value => System.err.println("Unrecognized directive value " + value) + } + else + System.err.println("Unrecognized directive " + path.getPath) deps.toList catch - case NonFatal(e) => - // If parsing fails, fall back to empty list - Nil + case NonFatal(e) => Nil // If parsing fails, fall back to empty list /** Resolve dependencies using Coursier Interface and return the classpath as a list of File objects */ def resolveDependencies(dependencies: List[(String, String, String)]): Either[String, List[File]] = @@ -72,16 +63,15 @@ object DependencyResolver: ) // Create dependency objects - val deps = dependencies.map { case (org, artifact, version) => - Dependency.of(org, artifact, version) - }.toArray + val deps = dependencies + .map { case (org, artifact, version) => Dependency.of(org, artifact, version) } + .toArray val fetch = coursierapi.Fetch.create() .withRepositories(repos*) .withDependencies(deps*) - val files = fetch.fetch().asScala.toList - Right(files) + Right(fetch.fetch().asScala.toList) catch case NonFatal(e) => From 976e7c1575bbcdc6eb21cd3d9cac96066f9ba765 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 3 Oct 2025 09:12:20 -0600 Subject: [PATCH 04/22] . --- project/Build.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project/Build.scala b/project/Build.scala index 6c43815cf587..1f82f362fd5c 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2139,6 +2139,8 @@ object Build { "org.jline" % "jline-terminal" % "3.29.0", "org.jline" % "jline-terminal-jni" % "3.29.0", ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13), + "io.get-coursier" % "interface" % "1.0.19", // used by the REPL for dependency resolution + "org.virtuslab" % "using_directives" % "1.1.4", // used by the REPL for parsing magic comments ), // NOTE: The only difference here is that we drop `-Werror` and semanticDB for now Compile / scalacOptions := Seq("-deprecation", "-feature", "-unchecked", "-encoding", "UTF8", "-language:implicitConversions"), From cd2a13b6bdea1518768d5a0e4c4592d70a929888 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 3 Oct 2025 09:50:53 -0600 Subject: [PATCH 05/22] . --- project/Build.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project/Build.scala b/project/Build.scala index 1f82f362fd5c..1e043b2b5143 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2313,6 +2313,8 @@ object Build { "org.jline" % "jline-terminal-jni" % "3.29.0", "com.github.sbt" % "junit-interface" % "0.13.3" % Test, ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13), + "io.get-coursier" % "interface" % "1.0.19", // used by the REPL for dependency resolution + "org.virtuslab" % "using_directives" % "1.1.4", // used by the REPL for parsing magic comments ), // NOTE: The only difference here is that we drop `-Werror` and semanticDB for now Compile / scalacOptions := Seq("-deprecation", "-feature", "-unchecked", "-encoding", "UTF8", "-language:implicitConversions"), From c69759f3032aef18dd9c6432547a2fe482cb3986 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 11:29:59 -0600 Subject: [PATCH 06/22] . --- project/Build.scala | 8 ++------ repl/src/dotty/tools/repl/DependencyResolver.scala | 6 +++++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index e9d980edeb8f..f384bc892a6f 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -754,8 +754,6 @@ object Build { libraryDependencies ++= Seq( "org.scala-lang.modules" % "scala-asm" % "9.8.0-scala-1", // used by the backend Dependencies.compilerInterface, - ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13), - "io.get-coursier" % "interface" % "1.0.19", // used by the REPL for dependency resolution "org.virtuslab" % "using_directives" % "1.1.4", // used by the REPL for parsing magic comments ), @@ -1837,6 +1835,8 @@ object Build { "com.lihaoyi" %% "fansi" % "0.5.1", "com.lihaoyi" %% "sourcecode" % "0.4.4", "com.github.sbt" % "junit-interface" % "0.13.3" % Test, + ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13), + "io.get-coursier" % "interface" % "1.0.19", // used by the REPL for dependency resolution ), // Configure to use the non-bootstrapped compiler scalaInstance := { @@ -2456,8 +2456,6 @@ object Build { "com.github.sbt" % "junit-interface" % "0.13.3" % Test, "org.scala-lang.modules" % "scala-asm" % "9.8.0-scala-1", Dependencies.compilerInterface, - ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13), - "io.get-coursier" % "interface" % "1.0.19", // used by the REPL for dependency resolution "org.virtuslab" % "using_directives" % "1.1.4", // used by the REPL for parsing magic comments ), // NOTE: The only difference here is that we drop `-Werror` and semanticDB for now @@ -2608,8 +2606,6 @@ object Build { "org.scala-lang.modules" % "scala-asm" % "9.8.0-scala-1", Dependencies.compilerInterface, "com.github.sbt" % "junit-interface" % "0.13.3" % Test, - ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13), - "io.get-coursier" % "interface" % "1.0.19", // used by the REPL for dependency resolution "org.virtuslab" % "using_directives" % "1.1.4", // used by the REPL for parsing magic comments ), // NOTE: The only difference here is that we drop `-Werror` and semanticDB for now diff --git a/repl/src/dotty/tools/repl/DependencyResolver.scala b/repl/src/dotty/tools/repl/DependencyResolver.scala index 96eb4d11cac0..732b69e1721b 100644 --- a/repl/src/dotty/tools/repl/DependencyResolver.scala +++ b/repl/src/dotty/tools/repl/DependencyResolver.scala @@ -106,6 +106,10 @@ object DependencyResolver: SymbolLoaders.mergeNewEntries(defn.RootClass, ClassPath.RootPackage, jarClassPath, ctx.platform.classPath) // Create new classloader with previous output dir and resolved dependencies - new AbstractFileClassLoader(prevOutputDir, depsClassLoader) + new dotty.tools.repl.AbstractFileClassLoader( + prevOutputDir, + depsClassLoader, + ctx.settings.XreplInterruptInstrumentation.value + ) end DependencyResolver From fe4bf794adc8011a34b2d471da99910d438cc842 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 11:49:22 -0600 Subject: [PATCH 07/22] . --- project/Build.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index f384bc892a6f..176f1cef0fa1 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1835,8 +1835,8 @@ object Build { "com.lihaoyi" %% "fansi" % "0.5.1", "com.lihaoyi" %% "sourcecode" % "0.4.4", "com.github.sbt" % "junit-interface" % "0.13.3" % Test, - ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13), - "io.get-coursier" % "interface" % "1.0.19", // used by the REPL for dependency resolution + ("io.get-coursier" %% "coursier" % "2.0.24" % Test).cross(CrossVersion.for3Use2_13), + "io.get-coursier" % "interface" % "1.0.28", // used by the REPL for dependency resolution ), // Configure to use the non-bootstrapped compiler scalaInstance := { From 523232b9083c22f9c70c64895b5df0c29a070412 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 11:59:15 -0600 Subject: [PATCH 08/22] . --- project/Build.scala | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 176f1cef0fa1..91b04fe5a9e5 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -753,8 +753,7 @@ object Build { // get libraries onboard libraryDependencies ++= Seq( "org.scala-lang.modules" % "scala-asm" % "9.8.0-scala-1", // used by the backend - Dependencies.compilerInterface, - "org.virtuslab" % "using_directives" % "1.1.4", // used by the REPL for parsing magic comments + Dependencies.compilerInterface ), // For convenience, change the baseDirectory when running the compiler @@ -1836,7 +1835,7 @@ object Build { "com.lihaoyi" %% "sourcecode" % "0.4.4", "com.github.sbt" % "junit-interface" % "0.13.3" % Test, ("io.get-coursier" %% "coursier" % "2.0.24" % Test).cross(CrossVersion.for3Use2_13), - "io.get-coursier" % "interface" % "1.0.28", // used by the REPL for dependency resolution + "io.get-coursier" % "interface" % "1.0.29", // used by the REPL for dependency resolution ), // Configure to use the non-bootstrapped compiler scalaInstance := { @@ -2456,7 +2455,6 @@ object Build { "com.github.sbt" % "junit-interface" % "0.13.3" % Test, "org.scala-lang.modules" % "scala-asm" % "9.8.0-scala-1", Dependencies.compilerInterface, - "org.virtuslab" % "using_directives" % "1.1.4", // used by the REPL for parsing magic comments ), // NOTE: The only difference here is that we drop `-Werror` and semanticDB for now Compile / scalacOptions := Seq("-deprecation", "-feature", "-unchecked", "-encoding", "UTF8", "-language:implicitConversions"), @@ -2606,7 +2604,6 @@ object Build { "org.scala-lang.modules" % "scala-asm" % "9.8.0-scala-1", Dependencies.compilerInterface, "com.github.sbt" % "junit-interface" % "0.13.3" % Test, - "org.virtuslab" % "using_directives" % "1.1.4", // used by the REPL for parsing magic comments ), // NOTE: The only difference here is that we drop `-Werror` and semanticDB for now Compile / scalacOptions := Seq("-deprecation", "-feature", "-unchecked", "-encoding", "UTF8", "-language:implicitConversions"), From 7041530b1b9fb297928a43ee3f7975e5b5421850 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 11:59:51 -0600 Subject: [PATCH 09/22] . --- project/Build.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project/Build.scala b/project/Build.scala index 91b04fe5a9e5..2794c5c89250 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -753,7 +753,8 @@ object Build { // get libraries onboard libraryDependencies ++= Seq( "org.scala-lang.modules" % "scala-asm" % "9.8.0-scala-1", // used by the backend - Dependencies.compilerInterface + Dependencies.compilerInterface, + ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13), ), // For convenience, change the baseDirectory when running the compiler From a2498b6fa9fa13cb63eb9c795352a555b62f71bd Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 12:00:17 -0600 Subject: [PATCH 10/22] . --- project/Build.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project/Build.scala b/project/Build.scala index 2794c5c89250..472c7b25783b 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2456,6 +2456,7 @@ object Build { "com.github.sbt" % "junit-interface" % "0.13.3" % Test, "org.scala-lang.modules" % "scala-asm" % "9.8.0-scala-1", Dependencies.compilerInterface, + ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13), ), // NOTE: The only difference here is that we drop `-Werror` and semanticDB for now Compile / scalacOptions := Seq("-deprecation", "-feature", "-unchecked", "-encoding", "UTF8", "-language:implicitConversions"), @@ -2605,6 +2606,7 @@ object Build { "org.scala-lang.modules" % "scala-asm" % "9.8.0-scala-1", Dependencies.compilerInterface, "com.github.sbt" % "junit-interface" % "0.13.3" % Test, + ("io.get-coursier" %% "coursier" % "2.0.16" % Test).cross(CrossVersion.for3Use2_13), ), // NOTE: The only difference here is that we drop `-Werror` and semanticDB for now Compile / scalacOptions := Seq("-deprecation", "-feature", "-unchecked", "-encoding", "UTF8", "-language:implicitConversions"), From cf5e818169ee65c0527c6eaa1d1467c405a202ee Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 12:15:02 -0600 Subject: [PATCH 11/22] . --- project/Build.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 472c7b25783b..4d55f282523a 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1835,8 +1835,8 @@ object Build { "com.lihaoyi" %% "fansi" % "0.5.1", "com.lihaoyi" %% "sourcecode" % "0.4.4", "com.github.sbt" % "junit-interface" % "0.13.3" % Test, - ("io.get-coursier" %% "coursier" % "2.0.24" % Test).cross(CrossVersion.for3Use2_13), - "io.get-coursier" % "interface" % "1.0.29", // used by the REPL for dependency resolution + "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 ), // Configure to use the non-bootstrapped compiler scalaInstance := { From 49a1038c530534d3045156053001776bf0003dbb Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 12:32:54 -0600 Subject: [PATCH 12/22] . --- repl/src/dotty/tools/repl/AbstractFileClassLoader.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala b/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala index c23a6bcd0268..255855062793 100644 --- a/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala +++ b/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala @@ -73,6 +73,9 @@ class AbstractFileClassLoader(val root: AbstractFile, parent: ClassLoader, inter case s"javax.$_" => super.loadClass(name) case s"sun.$_" => super.loadClass(name) case s"jdk.$_" => super.loadClass(name) + case s"org.xml.sax.$_" => super.loadClass(name) // XML SAX API (part of java.xml module) + case s"org.w3c.dom.$_" => super.loadClass(name) // W3C DOM API (part of java.xml module) + case s"com.sun.org.apache.$_" => super.loadClass(name) // Internal Xerces implementation case "dotty.tools.repl.StopRepl" => // Load StopRepl bytecode from parent but ensure each classloader gets its own copy val classFileName = name.replace('.', '/') + ".class" From 9c54fed8ad70b00538cc2659fa190c2a35dcde89 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 15:39:40 -0600 Subject: [PATCH 13/22] . --- repl/src/dotty/tools/repl/ReplDriver.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repl/src/dotty/tools/repl/ReplDriver.scala b/repl/src/dotty/tools/repl/ReplDriver.scala index 79fa25a0cbaa..9bd926557aa8 100644 --- a/repl/src/dotty/tools/repl/ReplDriver.scala +++ b/repl/src/dotty/tools/repl/ReplDriver.scala @@ -339,7 +339,7 @@ class ReplDriver(settings: Array[String], protected def interpret(res: ParseResult)(using state: State): State = { res match { - case parsed: Parsed => + case parsed: Parsed if parsed.source.content().mkString.startsWith("//>") => // Check for magic comments specifying dependencies val sourceCode = parsed.source.content().mkString val depStrings = DependencyResolver.extractDependencies(sourceCode) From 34c51d8a74722a69208dd96a715527c23cce0f89 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 15:54:22 -0600 Subject: [PATCH 14/22] . --- repl/src/dotty/tools/repl/ReplDriver.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/repl/src/dotty/tools/repl/ReplDriver.scala b/repl/src/dotty/tools/repl/ReplDriver.scala index 9bd926557aa8..7fa9f05f5cf2 100644 --- a/repl/src/dotty/tools/repl/ReplDriver.scala +++ b/repl/src/dotty/tools/repl/ReplDriver.scala @@ -362,12 +362,10 @@ class ReplDriver(settings: Array[String], out.println(s"Resolved ${deps.size} dependencies (${files.size} JARs)") case Left(error) => out.println(s"Error resolving dependencies: $error") + state - // Only compile if there are actual trees to compile - if parsed.trees.nonEmpty then + case parsed: Parsed if parsed.trees.nonEmpty => compile(parsed, state) - else - state case SyntaxErrors(_, errs, _) => displayErrors(errs) From 7225ec9a6915472cbf87568e6f99da3a778e8dbb Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 17:26:13 -0600 Subject: [PATCH 15/22] . --- repl/src/dotty/tools/repl/ReplDriver.scala | 45 +++++++++++++--------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/repl/src/dotty/tools/repl/ReplDriver.scala b/repl/src/dotty/tools/repl/ReplDriver.scala index 7fa9f05f5cf2..f42f3bc14857 100644 --- a/repl/src/dotty/tools/repl/ReplDriver.scala +++ b/repl/src/dotty/tools/repl/ReplDriver.scala @@ -342,27 +342,34 @@ class ReplDriver(settings: Array[String], case parsed: Parsed if parsed.source.content().mkString.startsWith("//>") => // Check for magic comments specifying dependencies val sourceCode = parsed.source.content().mkString - val depStrings = DependencyResolver.extractDependencies(sourceCode) + println("Please using `:dep com.example::artifact:version` to add dependencies in the REPL") + state + case parsed: Parsed if parsed.source.content().mkString.startsWith(":dep ") => + // Check for magic command specifying dependencies + val sourceCode = parsed.source.content().mkString + val depStrings = Seq(parsed.source.content().mkString.drop(":dep")) + resolveDepStrings(depStrings) 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 + 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 parsed: Parsed if parsed.trees.nonEmpty => compile(parsed, state) From ba1a94138724d901330d2849bb63fb911f6e2f83 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 17:26:26 -0600 Subject: [PATCH 16/22] . --- repl/src/dotty/tools/repl/ReplDriver.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repl/src/dotty/tools/repl/ReplDriver.scala b/repl/src/dotty/tools/repl/ReplDriver.scala index f42f3bc14857..f7db5c50de5b 100644 --- a/repl/src/dotty/tools/repl/ReplDriver.scala +++ b/repl/src/dotty/tools/repl/ReplDriver.scala @@ -348,7 +348,7 @@ class ReplDriver(settings: Array[String], case parsed: Parsed if parsed.source.content().mkString.startsWith(":dep ") => // Check for magic command specifying dependencies val sourceCode = parsed.source.content().mkString - val depStrings = Seq(parsed.source.content().mkString.drop(":dep")) + val depStrings = Seq(parsed.source.content().mkString.drop(":dep ")) resolveDepStrings(depStrings) if depStrings.nonEmpty then val deps = depStrings.flatMap(DependencyResolver.parseDependency) From 8675bcfdf650c17f88777696f638fc52f953f03e Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 17:30:35 -0600 Subject: [PATCH 17/22] . --- repl/src/dotty/tools/repl/ReplDriver.scala | 40 ++++++++++------------ 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/repl/src/dotty/tools/repl/ReplDriver.scala b/repl/src/dotty/tools/repl/ReplDriver.scala index f7db5c50de5b..dd4a8179b40c 100644 --- a/repl/src/dotty/tools/repl/ReplDriver.scala +++ b/repl/src/dotty/tools/repl/ReplDriver.scala @@ -348,28 +348,26 @@ class ReplDriver(settings: Array[String], case parsed: Parsed if parsed.source.content().mkString.startsWith(":dep ") => // Check for magic command specifying dependencies val sourceCode = parsed.source.content().mkString - val depStrings = Seq(parsed.source.content().mkString.drop(":dep ")) - resolveDepStrings(depStrings) + val depStrings = List(parsed.source.content().mkString.stripPrefix(":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 - + 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 parsed: Parsed if parsed.trees.nonEmpty => compile(parsed, state) From c66ef3aa0f2e996a95daecb8cb71677e39a38e66 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 17:35:54 -0600 Subject: [PATCH 18/22] . --- repl/src/dotty/tools/repl/ParseResult.scala | 6 +++ repl/src/dotty/tools/repl/ReplDriver.scala | 45 ++++++++++----------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/repl/src/dotty/tools/repl/ParseResult.scala b/repl/src/dotty/tools/repl/ParseResult.scala index 24aa07c707a7..7b55e1c4968f 100644 --- a/repl/src/dotty/tools/repl/ParseResult.scala +++ b/repl/src/dotty/tools/repl/ParseResult.scala @@ -41,6 +41,10 @@ sealed trait Command extends ParseResult /** An unknown command that will not be handled by the REPL */ case class UnknownCommand(cmd: String) extends Command +case class Dep(dep: String) extends Command +object Dep { + val command: String = ":dep" +} /** An ambiguous prefix that matches multiple commands */ case class AmbiguousCommand(cmd: String, matchingCommands: List[String]) extends Command @@ -145,6 +149,7 @@ case object Help extends Command { |:reset [options] reset the repl to its initial state, forgetting all session entries |:settings update compiler options, if possible |:silent disable/enable automatic printing of results + |:dep ::: Resolve a dependency and make it available in the REPL """.stripMargin } @@ -169,6 +174,7 @@ object ParseResult { KindOf.command -> (arg => KindOf(arg)), Load.command -> (arg => Load(arg)), Require.command -> (arg => Require(arg)), + Dep.command -> (arg => Dep(arg)), TypeOf.command -> (arg => TypeOf(arg)), DocOf.command -> (arg => DocOf(arg)), Settings.command -> (arg => Settings(arg)), diff --git a/repl/src/dotty/tools/repl/ReplDriver.scala b/repl/src/dotty/tools/repl/ReplDriver.scala index dd4a8179b40c..3c1e04b83bb8 100644 --- a/repl/src/dotty/tools/repl/ReplDriver.scala +++ b/repl/src/dotty/tools/repl/ReplDriver.scala @@ -345,30 +345,6 @@ class ReplDriver(settings: Array[String], println("Please using `:dep com.example::artifact:version` to add dependencies in the REPL") state - case parsed: Parsed if parsed.source.content().mkString.startsWith(":dep ") => - // Check for magic command specifying dependencies - val sourceCode = parsed.source.content().mkString - val depStrings = List(parsed.source.content().mkString.stripPrefix(":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 parsed: Parsed if parsed.trees.nonEmpty => compile(parsed, state) @@ -684,6 +660,27 @@ class ReplDriver(settings: Array[String], 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! From bbb5779e132a30aa9aeaa51cbefb4901f8084e3a Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 11 Nov 2025 20:55:37 -0600 Subject: [PATCH 19/22] . --- repl/test/dotty/tools/repl/TabcompleteTests.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/repl/test/dotty/tools/repl/TabcompleteTests.scala b/repl/test/dotty/tools/repl/TabcompleteTests.scala index d3284402f58b..0541e25b2992 100644 --- a/repl/test/dotty/tools/repl/TabcompleteTests.scala +++ b/repl/test/dotty/tools/repl/TabcompleteTests.scala @@ -210,6 +210,7 @@ class TabcompleteTests extends ReplTest { @Test def commands = initially { assertEquals( List( + ":dep", ":doc", ":exit", ":help", @@ -232,7 +233,7 @@ class TabcompleteTests extends ReplTest { @Test def commandPreface = initially { // This looks odd, but if we return :doc here it will result in ::doc in the REPL assertEquals( - List(":doc"), + List(":dep", ":doc"), tabComplete(":d") ) } From 84aee989dce33105cd98130ac5b4d28a014031c6 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 12 Nov 2025 04:02:51 -0600 Subject: [PATCH 20/22] Update repl/src/dotty/tools/repl/ReplDriver.scala Co-authored-by: Jamie Thompson --- repl/src/dotty/tools/repl/ReplDriver.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/repl/src/dotty/tools/repl/ReplDriver.scala b/repl/src/dotty/tools/repl/ReplDriver.scala index 3c1e04b83bb8..83b54d28dfe7 100644 --- a/repl/src/dotty/tools/repl/ReplDriver.scala +++ b/repl/src/dotty/tools/repl/ReplDriver.scala @@ -341,7 +341,6 @@ class ReplDriver(settings: Array[String], res match { case parsed: Parsed if parsed.source.content().mkString.startsWith("//>") => // Check for magic comments specifying dependencies - val sourceCode = parsed.source.content().mkString println("Please using `:dep com.example::artifact:version` to add dependencies in the REPL") state From c65bf6916cdfa625bd3edd9f23ae6811b85cf616 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 12 Nov 2025 04:03:04 -0600 Subject: [PATCH 21/22] Update repl/src/dotty/tools/repl/ReplDriver.scala Co-authored-by: Jamie Thompson --- repl/src/dotty/tools/repl/ReplDriver.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repl/src/dotty/tools/repl/ReplDriver.scala b/repl/src/dotty/tools/repl/ReplDriver.scala index 83b54d28dfe7..075f2555c592 100644 --- a/repl/src/dotty/tools/repl/ReplDriver.scala +++ b/repl/src/dotty/tools/repl/ReplDriver.scala @@ -341,7 +341,7 @@ class ReplDriver(settings: Array[String], res match { case parsed: Parsed if parsed.source.content().mkString.startsWith("//>") => // Check for magic comments specifying dependencies - println("Please using `:dep com.example::artifact:version` to add dependencies in the REPL") + println("Please use `:dep com.example::artifact:version` to add dependencies in the REPL") state case parsed: Parsed if parsed.trees.nonEmpty => From a1386648c0d43e655588e4717fa77104cdd02ea4 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 13 Nov 2025 04:13:40 -0600 Subject: [PATCH 22/22] Update AbstractFileClassLoader.scala --- repl/src/dotty/tools/repl/AbstractFileClassLoader.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala b/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala index 255855062793..1bde97621897 100644 --- a/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala +++ b/repl/src/dotty/tools/repl/AbstractFileClassLoader.scala @@ -68,7 +68,10 @@ class AbstractFileClassLoader(val root: AbstractFile, parent: ClassLoader, inter val loaded = findLoadedClass(name) // Check if already loaded if loaded != null then return loaded - name match { // Don't instrument JDK classes or StopRepl + name match { + // Don't instrument JDK classes or StopRepl. These are often restricted to load from a single classloader + // due to the JDK module system, and so instrumenting them and loading the modified copy of the class + // results in runtime exceptions case s"java.$_" => super.loadClass(name) case s"javax.$_" => super.loadClass(name) case s"sun.$_" => super.loadClass(name) @@ -76,6 +79,7 @@ class AbstractFileClassLoader(val root: AbstractFile, parent: ClassLoader, inter case s"org.xml.sax.$_" => super.loadClass(name) // XML SAX API (part of java.xml module) case s"org.w3c.dom.$_" => super.loadClass(name) // W3C DOM API (part of java.xml module) case s"com.sun.org.apache.$_" => super.loadClass(name) // Internal Xerces implementation + // Don't instrument StopRepl, which would otherwise cause infinite recursion case "dotty.tools.repl.StopRepl" => // Load StopRepl bytecode from parent but ensure each classloader gets its own copy val classFileName = name.replace('.', '/') + ".class"