Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
56 changes: 32 additions & 24 deletions repl/src/dotty/tools/repl/AbstractFileClassLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,38 @@ package repl

import scala.language.unsafeNulls

import dotty.tools.dotc.config.ScalaSettings

import io.AbstractFile

import java.net.{URL, URLConnection, URLStreamHandler}
import java.util.Collections

class AbstractFileClassLoader(val root: AbstractFile, parent: ClassLoader, interruptInstrumentation: String) extends ClassLoader(parent):
private def findAbstractFile(name: String) = root.lookupPath(name.split('/').toIndexedSeq, directory = false)

// on JDK 20 the URL constructor we're using is deprecated,
// but the recommended replacement, URL.of, doesn't exist on JDK 8
@annotation.nowarn("cat=deprecation")
override protected def findResource(name: String): URL | Null =
findAbstractFile(name) match
case null => null
case file => new URL(null, s"memory:${file.path}", new URLStreamHandler {
override def openConnection(url: URL): URLConnection = new URLConnection(url) {
override def connect() = ()
override def getInputStream = file.input
}
})
override protected def findResources(name: String): java.util.Enumeration[URL] =
findResource(name) match
case null => Collections.enumeration(Collections.emptyList[URL]) //Collections.emptyEnumeration[URL]
case url => Collections.enumeration(Collections.singleton(url))
import AbstractFileClassLoader.InterruptInstrumentation


object AbstractFileClassLoader:
enum InterruptInstrumentation(val stringValue: String):
case Disabled extends InterruptInstrumentation("false")
case Enabled extends InterruptInstrumentation("true")
case Local extends InterruptInstrumentation("local")

def is(value: InterruptInstrumentation): Boolean = this == value
def isOneOf(others: InterruptInstrumentation*): Boolean = others.contains(this)

object InterruptInstrumentation:
def fromString(string: String): InterruptInstrumentation = string match {
case "false" => Disabled
case "true" => Enabled
case "local" => Local
case _ => throw new IllegalArgumentException(s"Invalid interrupt instrumentation value: $string")
}

class AbstractFileClassLoader(
root: AbstractFile,
parent: ClassLoader,
interruptInstrumentation: InterruptInstrumentation = InterruptInstrumentation.fromString(ScalaSettings.XreplInterruptInstrumentation.default)
) extends io.AbstractFileClassLoader(root, parent):
Copy link
Contributor

Choose a reason for hiding this comment

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

I think for binary compatibility we would need a separate def this(...) constructor if I am not mistaken

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Member

Choose a reason for hiding this comment

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

There's no binary compatibility to uphold. We shouldn't worsen our code to "preserve" binary compatibility.

Even preserving source compat is a benevolent favor we make. We can do it if it helps other projects, but not to expense of our own code quality.

Copy link
Contributor

Choose a reason for hiding this comment

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

This has been stable for a while and we use it in Mdoc, which then is used by worksheets in Metals. We don't fully cross publish for different Scala versions, we just check if each version works. I don't really want to add another library that we would need to cross publish.

The other solution would be to have a worksheet API, but that is something no one had the time for.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Thanks @sjrd for the clarifications. I completely agree with this. T repl and the compiler too are not libraries but rather applications, there is no guarantee to have or to follow.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, we should think of having an interface for worksheets, but if we decide not to include this change, worksheets will stop working altogether and it will require a lot of my work to fix it. At a cost of a single added custom constructor.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, I think we already accepted much worse code that is being fixed by Wojtek in this PR, so the change is anyway a net positive. Not sure why we would be blocking this change (which will save me loads of time) and anyway accept much worse PRs. :/

Copy link
Member

Choose a reason for hiding this comment

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

I'm not blocking anything. But the expectation for this code should be that it will break. There is no guarantee here.

Copy link
Contributor

Choose a reason for hiding this comment

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

I fully understand this. I will try to vibe code some solution in the background modeled on the presentation compiler interfaces.


override def findClass(name: String): Class[?] = {
var file: AbstractFile | Null = root
Expand All @@ -52,23 +60,23 @@ class AbstractFileClassLoader(val root: AbstractFile, parent: ClassLoader, inter

val bytes = file.toByteArray

if interruptInstrumentation != "false" then defineClassInstrumented(name, bytes)
if !interruptInstrumentation.is(InterruptInstrumentation.Enabled) then defineClassInstrumented(name, bytes)
else defineClass(name, bytes, 0, bytes.length)
}

def defineClassInstrumented(name: String, originalBytes: Array[Byte]) = {
private def defineClassInstrumented(name: String, originalBytes: Array[Byte]) = {
val instrumentedBytes = ReplBytecodeInstrumentation.instrument(originalBytes)
defineClass(name, instrumentedBytes, 0, instrumentedBytes.length)
}

override def loadClass(name: String): Class[?] =
if interruptInstrumentation == "false" || interruptInstrumentation == "local"
then return super.loadClass(name)
if interruptInstrumentation.isOneOf(InterruptInstrumentation.Disabled, InterruptInstrumentation.Local) then
return super.loadClass(name)

val loaded = findLoadedClass(name) // Check if already loaded
if loaded != null then return loaded

name match {
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
Expand Down
8 changes: 5 additions & 3 deletions repl/src/dotty/tools/repl/DependencyResolver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import java.net.{URL, URLClassLoader}
import scala.jdk.CollectionConverters.*
import scala.util.control.NonFatal

import dotty.tools.repl.AbstractFileClassLoader

import coursierapi.{Repository, Dependency, MavenRepository}
import com.virtuslab.using_directives.UsingDirectivesProcessor
import com.virtuslab.using_directives.custom.model.{Path, StringValue, Value}
Expand Down Expand Up @@ -90,7 +92,7 @@ object DependencyResolver:
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.io.{AbstractFile, ClassPath}
import dotty.tools.repl.ScalaClassLoader.fromURLsParallelCapable

// Create a classloader with all the resolved JAR files
Expand All @@ -106,10 +108,10 @@ object DependencyResolver:
SymbolLoaders.mergeNewEntries(defn.RootClass, ClassPath.RootPackage, jarClassPath, ctx.platform.classPath)

// Create new classloader with previous output dir and resolved dependencies
new dotty.tools.repl.AbstractFileClassLoader(
new AbstractFileClassLoader(
prevOutputDir,
depsClassLoader,
ctx.settings.XreplInterruptInstrumentation.value
AbstractFileClassLoader.InterruptInstrumentation.fromString(ctx.settings.XreplInterruptInstrumentation.value)
)

end DependencyResolver
2 changes: 1 addition & 1 deletion repl/src/dotty/tools/repl/Rendering.scala
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):
myClassLoader = new AbstractFileClassLoader(
ctx.settings.outputDir.value,
parent,
ctx.settings.XreplInterruptInstrumentation.value
AbstractFileClassLoader.InterruptInstrumentation.fromString(ctx.settings.XreplInterruptInstrumentation.value)
)
myClassLoader
}
Expand Down
4 changes: 2 additions & 2 deletions repl/src/dotty/tools/repl/ReplBytecodeInstrumentation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import scala.language.unsafeNulls
import scala.tools.asm.*
import scala.tools.asm.Opcodes.*
import scala.tools.asm.tree.*
import scala.collection.JavaConverters.*
import scala.jdk.CollectionConverters.*
import java.util.concurrent.atomic.AtomicBoolean

object ReplBytecodeInstrumentation:
/** Instrument bytecode to add checks to throw an exception if the REPL command is cancelled
*/
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 @@ -607,7 +607,7 @@ class ReplDriver(settings: Array[String],
rendering.myClassLoader = new AbstractFileClassLoader(
prevOutputDir,
jarClassLoader,
ctx.settings.XreplInterruptInstrumentation.value
AbstractFileClassLoader.InterruptInstrumentation.fromString(ctx.settings.XreplInterruptInstrumentation.value)
)

out.println(s"Added '$path' to classpath.")
Expand Down
Loading