Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
176 changes: 173 additions & 3 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -1107,9 +1110,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
Expand Down Expand Up @@ -1177,6 +1177,176 @@ object Build {
(Compile / run).toTask(" -usejavacp").value
},
bspEnabled := false,
(Compile / sourceGenerators) += Def.task {
val s = streams.value
val cacheDir = s.cacheDirectory
val dest = (Compile / sourceManaged).value / "downloaded"
val lm = dependencyResolution.value

val dependencies = Seq(
("com.lihaoyi", "pprint_3", "0.9.5"),
("com.lihaoyi", "fansi_3", "0.5.1"),
("com.lihaoyi", "sourcecode_3", "0.4.4"),
)

// 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)
}

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

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",
"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
for((search, replacement) <- patches 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-embedded` = project.in(file("repl-embedded"))
.dependsOn(`scala-library-bootstrapped`)
.enablePlugins(sbtassembly.AssemblyPlugin)
.settings(publishSettings)
.settings(
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",
),
Compile / unmanagedSourceDirectories := Seq(baseDirectory.value / "src"),
// Assembly configuration for shading
assembly / assemblyJarName := s"scala3-repl-embedded-${version.value}.jar",
// Add scala3-repl to assembly classpath without making it a published dependency
assembly / fullClasspath := {
(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)
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
// This is the entrypoint to the embedded Scala REPL so don't shade it
name.contains("jline")
}
},

assembly := {
val originalJar = assembly.value
val log = streams.value.log

log.info(s"Post-processing assembly to relocate files into shaded subfolder...")

val tmpDir = IO.createTemporaryDirectory
try {
IO.unzip(originalJar, tmpDir)
val shadedDir = tmpDir / "dotty" / "isolated"
IO.createDirectory(shadedDir)

for(file <- (tmpDir ** "*").get if file.isFile) {
val relativePath = file.relativeTo(tmpDir).get.getPath

val shouldKeepInPlace =
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/")

if (!shouldKeepInPlace) {
val newPath = shadedDir / relativePath
IO.createDirectory(newPath.getParentFile)
IO.move(file, newPath)
}
}

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")
} finally IO.delete(tmpDir)

originalJar
},
// Use the shaded assembly jar as our packageBin for publishing
Compile / packageBin := (Compile / assembly).value,
publish / skip := false,
)

// ==============================================================================================
Expand Down
2 changes: 2 additions & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
80 changes: 80 additions & 0 deletions repl-embedded/src/dotty/embedded/EmbeddedReplMain.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package dotty.embedded

import java.net.{URL, URLClassLoader}
import java.io.InputStream

/**
* A classloader that remaps shaded classes back to their original package names.
*/
class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) {

// 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

// 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{
case Some(is) =>
try {
val bytes = is.readAllBytes()
val clazz = defineClass(name, bytes, 0, bytes.length)
if (resolve) resolveClass(clazz)
clazz
} finally is.close()
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 = {
super.getResourceAsStream(ISOLATED_PREFIX.replace('.', '/') + name)
}
}

/**
* 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 = {
val argsWithClasspath =
if (args.exists(arg => arg == "-classpath" || arg == "-cp")) args
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*/ pprintImport
)

replDriverClass.getMethod("tryRunning").invoke(replDriver)
}
}
33 changes: 14 additions & 19 deletions repl/src/dotty/tools/repl/Rendering.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -31,21 +31,16 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):
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
// 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("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 Color = pprintCls.getField("MODULE$").get(null)
val Color_apply = pprintCls.getMethod("apply",
classOf[Any], // value
Expand All @@ -56,12 +51,12 @@ 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
)
FansiStr_render.invoke(fansiStr).asInstanceOf[String]

val fansiStr = Color_apply.invoke(Color, value, width, height, 2, initialOffset, false, true)
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()
}
Expand Down
2 changes: 1 addition & 1 deletion repl/src/dotty/tools/repl/ReplDriver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -706,4 +706,4 @@ class ReplDriver(settings: Array[String],

end ReplDriver
object ReplDriver:
def pprintImport = "import pprint.pprintln\n"
def pprintImport = "import dotty.shaded.pprint.pprintln\n"
1 change: 1 addition & 0 deletions repl/src/dotty/tools/repl/StackTraceOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Loading
Loading