Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions repl/test-resources/repl/i6474
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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)
val res4: Foo3.T[Int][Int][Int] = (1, 2)
2 changes: 1 addition & 1 deletion repl/test-resources/type-printer/vals
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Loading
Loading