Skip to content

Commit ceb5223

Browse files
lihaoyibracevac
andauthored
Re-implement Ammonite's Ctrl-C interruption for Scala REPL via bytecode instrumentation (#24194)
Since `Thread.stop` was removed (https://stackoverflow.com/questions/4426592/why-thread-stop-doesnt-work), the only way to re-implement such functionality is via bytecode instrumentation. This PR implements such functionality in the Scala REPL, such that we can now `Ctrl-C` to interrupt runaway code that doesn't check for `Thread.isInterrupted()` (which is what the current `Thread.interrupt` does) without needing to kill the entire JVM These snippets are now interruptable where they weren't before: ```scala scala> scala.collection.Iterator.continually(1).foreach(x => scala.Predef.identity(x)) ^C Interrupting running thread java.lang.ThreadDeath at dotty.tools.repl.ReplCancel.stopCheck(ReplCancel.scala:22) at scala.collection.IterableOnceOps.foreach(IterableOnce.scala:619) at scala.collection.IterableOnceOps.foreach$(IterableOnce.scala:617) at scala.collection.AbstractIterator.foreach(Iterator.scala:1306) ... 32 elided scala> scala> var x = 1; while(true) x += 1 ^C Interrupting running thread java.lang.ThreadDeath at dotty.tools.repl.ReplCancel.stopCheck(ReplCancel.scala:22) ... 32 elided scala> scala> def fib(n: Int): Int = if (n <= 0) 1 else fib(n-1) + fib(n-2); fib(99) ^C Interrupting running thread java.lang.ThreadDeath at dotty.tools.repl.ReplCancel.throwIfReplStopped(ReplCancel.scala:16) at rs$line$2$.fib(rs$line$2) at rs$line$2$.fib(rs$line$2:1) at rs$line$2$.fib(rs$line$2:1) ... scala> ``` The way this works is that we instrument all bytecode that gets loaded into the REPL classloader using `scala.tools.asm` and add checks at every backwards branch and start of each method body. These checks call a classloader-scoped `ReplCancel.stopCheck` method, and the `Ctrl-C` handler is wired up to flip a var and make the `stopCheck()` calls fail. ## Configuration This feature is controlled by the `-Xrepl-interrupt-instrumentation` that can take three settings - `true` (default): all non-JDK classfiles loaded into the REPL are instrumented. This allows interruption of both local code and third-party libraries, but results in the REPL classes being incompatible with parent classloader classes and cannot be shared across the classloader boundary since the REPL uses its own instrumented copies of those classes. - `local`: only REPL classes are instrumented. Only local code can be interrupted, but long-running library code such as `Iterator.range(0, Int.MaxValue).max` cannot. But REPL-defined classes can be shared with the parent classloaded - `false`: all instrumentation is disabled, interruption is not supported in REPL-define classes or library classes || true (default) | local | false | |-----|-------|------|------| | REPL code interruptable (e.g. `while(true) ()`) | Y | Y | N | | library code interruptable (e.g. `Iterator.range(0, Int.MaxValue).max`) | Y | N | N | | REPL classes can interop with parent classloaded | N | Y | Y | | Performance Overhead | More | Less | None | The `true` default above also appears to be what JShell does, which also supports interruption of library classes, e.g. code such as `scala.collection.Iterator.range(0, Integer.MAX_VALUE).max(scala.math.Ordering.Int$.MODULE$);` can be interrupted in JShell even though the hot loop is in the library and not in REPL code ## Performance Impact This adds some incremental performance hit to code running in the Scala REPL, but the result of no longer needing to trash your entire REPL process and session history due to a single runaway command is probably worth it. There may be other ways to instrument the code to minimize the performance hit. Some rough benchmarks: ```scala var start = System.nanoTime(); var x = 1L; while(true) { x += 1; if (x % 100000000 == 0){ val next = System.nanoTime(); println(next - start); start = next}} ``` - `if (boolean) throw` (the current implementation): ~2ns per loop - `1/int`, which throws when `int == 0`: ~2ns per loop - `if (Thread.interrupted())`: ~2ns per loop - No instrumentation: ~1ns per loop An exponential-but-technically-not-infinite recursion benchmark below shows a minor slowdown from the start-of-method-body instrumentation (~6%): ```scala def fib(n: Int): Int = if (n <= 0) 1 else fib(n-1) + fib(n-2); val now = System.nanoTime(); fib(40); val duration = System.nanoTime() - now ``` - With instrumentation: 753,994,875ns - No instrumentation: 712,178,417ns This 50% slowdown is the worst case slowdown that instrumentation adds; anything more complex than a `while(true) x += 1` loop will have a longer time taken, and the % slowdown from instrumentation would be smaller. Probably can expect a 10-20% slowdown on more typical code This instrumentation is on by default on the assumption that most REPL work isn't performance sensitive, but I added a flag to switch it off and fall back to the prior un-instrumented behavior which would require terminating the process to stop runaway code. One consequence of this is that REPL-loaded classes will be different from non-REPL-loaded classes, due to the bytecode instrumentation and class re-definition. So use cases embedding the REPL into an existing program to interact with it "live" would need to pass `-Xrepl-bytecode-instrumentation:false` or `-Xrepl-bytecode-instrumentation:local` to allow classes and instances to be shared between them The `jshell` REPL also allows interruption of these snippets, and likely uses a similar approach though I haven't checked --------- Co-authored-by: Oliver Bračevac <[email protected]>
1 parent b14afef commit ceb5223

File tree

10 files changed

+195
-28
lines changed

10 files changed

+195
-28
lines changed

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,13 @@ private sealed trait XSettings:
325325
val XprintSuspension: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xprint-suspension", "Show when code is suspended until macros are compiled.")
326326
val Xprompt: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xprompt", "Display a prompt after each error (debugging option).")
327327
val XreplDisableDisplay: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xrepl-disable-display", "Do not display definitions in REPL.")
328+
val XreplInterruptInstrumentation: Setting[String] = StringSetting(
329+
AdvancedSetting,
330+
"Xrepl-interrupt-instrumentation",
331+
"true|false|local",
332+
"pass `false` to disable bytecode instrumentation for interrupt handling in REPL, or `local` to limit interrupt support to only REPL-defined classes",
333+
"true"
334+
)
328335
val XverifySignatures: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xverify-signatures", "Verify generic signatures in generated bytecode.")
329336
val XignoreScala2Macros: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xignore-scala2-macros", "Ignore errors when compiling code that calls Scala2 macros, these will fail at runtime.")
330337
val XimportSuggestionTimeout: Setting[Int] = IntSetting(AdvancedSetting, "Ximport-suggestion-timeout", "Timeout (in ms) for searching for import suggestions when errors are reported.", 8000)

compiler/src/dotty/tools/dotc/quoted/Interpreter.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class Interpreter(pos: SrcPos, classLoader0: ClassLoader)(using Context):
3535

3636
val classLoader =
3737
if ctx.owner.topLevelClass.name.startsWith(str.REPL_SESSION_LINE) then
38-
new AbstractFileClassLoader(ctx.settings.outputDir.value, classLoader0)
38+
new AbstractFileClassLoader(ctx.settings.outputDir.value, classLoader0, "false")
3939
else classLoader0
4040

4141
/** Local variable environment */
@@ -204,7 +204,11 @@ class Interpreter(pos: SrcPos, classLoader0: ClassLoader)(using Context):
204204
}
205205

206206
private def loadReplLineClass(moduleClass: Symbol): Class[?] = {
207-
val lineClassloader = new AbstractFileClassLoader(ctx.settings.outputDir.value, classLoader)
207+
val lineClassloader = new AbstractFileClassLoader(
208+
ctx.settings.outputDir.value,
209+
classLoader,
210+
"false"
211+
)
208212
lineClassloader.loadClass(moduleClass.name.firstPart.toString)
209213
}
210214

compiler/src/dotty/tools/repl/AbstractFileClassLoader.scala

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ package repl
1616
import scala.language.unsafeNulls
1717

1818
import io.AbstractFile
19+
import dotty.tools.repl.ReplBytecodeInstrumentation
1920

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

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

2627
// on JDK 20 the URL constructor we're using is deprecated,
@@ -45,17 +46,61 @@ class AbstractFileClassLoader(val root: AbstractFile, parent: ClassLoader) exten
4546
val pathParts = name.split("[./]").toList
4647
for (dirPart <- pathParts.init) {
4748
file = file.lookupName(dirPart, true)
48-
if (file == null) {
49-
throw new ClassNotFoundException(name)
50-
}
49+
if (file == null) throw new ClassNotFoundException(name)
5150
}
5251
file = file.lookupName(pathParts.last+".class", false)
53-
if (file == null) {
54-
throw new ClassNotFoundException(name)
55-
}
52+
if (file == null) throw new ClassNotFoundException(name)
53+
5654
val bytes = file.toByteArray
57-
defineClass(name, bytes, 0, bytes.length)
55+
56+
if interruptInstrumentation != "false" then defineClassInstrumented(name, bytes)
57+
else defineClass(name, bytes, 0, bytes.length)
5858
}
5959

60-
override def loadClass(name: String): Class[?] = try findClass(name) catch case _: ClassNotFoundException => super.loadClass(name)
60+
def defineClassInstrumented(name: String, originalBytes: Array[Byte]) = {
61+
val instrumentedBytes = ReplBytecodeInstrumentation.instrument(originalBytes)
62+
defineClass(name, instrumentedBytes, 0, instrumentedBytes.length)
63+
}
64+
65+
override def loadClass(name: String): Class[?] =
66+
if interruptInstrumentation == "false" || interruptInstrumentation == "local"
67+
then return super.loadClass(name)
68+
69+
val loaded = findLoadedClass(name) // Check if already loaded
70+
if loaded != null then return loaded
71+
72+
name match { // Don't instrument JDK classes or StopRepl
73+
case s"java.$_" => super.loadClass(name)
74+
case s"javax.$_" => super.loadClass(name)
75+
case s"sun.$_" => super.loadClass(name)
76+
case s"jdk.$_" => super.loadClass(name)
77+
case "dotty.tools.repl.StopRepl" =>
78+
// Load StopRepl bytecode from parent but ensure each classloader gets its own copy
79+
val classFileName = name.replace('.', '/') + ".class"
80+
val is = Option(getParent.getResourceAsStream(classFileName))
81+
// Can't get as resource, use the classloader that loaded this AbstractFileClassLoader
82+
// class itself, which must have access to StopRepl
83+
.getOrElse(classOf[AbstractFileClassLoader].getClassLoader.getResourceAsStream(classFileName))
84+
85+
try
86+
val bytes = is.readAllBytes()
87+
defineClass(name, bytes, 0, bytes.length)
88+
finally is.close()
89+
90+
case _ =>
91+
try findClass(name)
92+
catch case _: ClassNotFoundException =>
93+
// Not in REPL output, try to load from parent and instrument it
94+
try
95+
val resourceName = name.replace('.', '/') + ".class"
96+
getParent.getResourceAsStream(resourceName) match {
97+
case null => super.loadClass(resourceName)
98+
case is =>
99+
try defineClassInstrumented(name, is.readAllBytes())
100+
finally is.close()
101+
}
102+
catch
103+
case ex: Exception => super.loadClass(name)
104+
}
105+
61106
end AbstractFileClassLoader

compiler/src/dotty/tools/repl/Rendering.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None):
7272
new java.net.URLClassLoader(compilerClasspath.toArray, baseClassLoader)
7373
}
7474

75-
myClassLoader = new AbstractFileClassLoader(ctx.settings.outputDir.value, parent)
75+
myClassLoader = new AbstractFileClassLoader(
76+
ctx.settings.outputDir.value,
77+
parent,
78+
ctx.settings.XreplInterruptInstrumentation.value
79+
)
7680
myClassLoader
7781
}
7882

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package dotty.tools
2+
package repl
3+
4+
import scala.language.unsafeNulls
5+
6+
import scala.tools.asm.*
7+
import scala.tools.asm.Opcodes.*
8+
import scala.tools.asm.tree.*
9+
import scala.collection.JavaConverters.*
10+
import java.util.concurrent.atomic.AtomicBoolean
11+
12+
object ReplBytecodeInstrumentation:
13+
/** Instrument bytecode to add checks to throw an exception if the REPL command is cancelled
14+
*/
15+
def instrument(originalBytes: Array[Byte]): Array[Byte] =
16+
try
17+
val cr = new ClassReader(originalBytes)
18+
val cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES)
19+
val instrumenter = new InstrumentClassVisitor(cw)
20+
cr.accept(instrumenter, ClassReader.EXPAND_FRAMES)
21+
cw.toByteArray
22+
catch
23+
case ex: Exception => originalBytes
24+
25+
def setStopFlag(classLoader: ClassLoader, b: Boolean): Unit =
26+
val cancelClassOpt =
27+
try Some(classLoader.loadClass(classOf[dotty.tools.repl.StopRepl].getName))
28+
catch {
29+
case _: java.lang.ClassNotFoundException => None
30+
}
31+
for(cancelClass <- cancelClassOpt) {
32+
val setAllStopMethod = cancelClass.getDeclaredMethod("setStop", classOf[Boolean])
33+
setAllStopMethod.invoke(null, b.asInstanceOf[AnyRef])
34+
}
35+
36+
private class InstrumentClassVisitor(cv: ClassVisitor) extends ClassVisitor(ASM9, cv):
37+
38+
override def visitMethod(
39+
access: Int,
40+
name: String,
41+
descriptor: String,
42+
signature: String,
43+
exceptions: Array[String]
44+
): MethodVisitor =
45+
new InstrumentMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions))
46+
47+
/** MethodVisitor that inserts stop checks at backward branches */
48+
private class InstrumentMethodVisitor(mv: MethodVisitor) extends MethodVisitor(ASM9, mv):
49+
// Track labels we've seen to identify backward branches
50+
private val seenLabels = scala.collection.mutable.Set[Label]()
51+
52+
def addStopCheck() = mv.visitMethodInsn(
53+
INVOKESTATIC,
54+
classOf[dotty.tools.repl.StopRepl].getName.replace('.', '/'),
55+
"throwIfReplStopped",
56+
"()V",
57+
false
58+
)
59+
60+
override def visitCode(): Unit =
61+
super.visitCode()
62+
// Insert throwIfReplStopped() call at the start of the method
63+
// to allow breaking out of deeply recursive methods like fib(99)
64+
addStopCheck()
65+
66+
override def visitLabel(label: Label): Unit =
67+
seenLabels.add(label)
68+
super.visitLabel(label)
69+
70+
override def visitJumpInsn(opcode: Int, label: Label): Unit =
71+
// Add throwIfReplStopped if this is a backward branch (jumping to a label we've already seen)
72+
if seenLabels.contains(label) then addStopCheck()
73+
super.visitJumpInsn(opcode, label)
74+
75+
end ReplBytecodeInstrumentation

compiler/src/dotty/tools/repl/ReplDriver.scala

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import dotty.tools.dotc.{CompilationUnit, Driver}
3434
import dotty.tools.dotc.config.CompilerCommand
3535
import dotty.tools.io.*
3636
import dotty.tools.repl.Rendering.showUser
37+
import dotty.tools.repl.ReplBytecodeInstrumentation
3738
import dotty.tools.runner.ScalaClassLoader.*
3839
import org.jline.reader.*
3940

@@ -228,13 +229,20 @@ class ReplDriver(settings: Array[String],
228229
// Set up interrupt handler for command execution
229230
var firstCtrlCEntered = false
230231
val thread = Thread.currentThread()
232+
233+
// Clear the stop flag before executing new code
234+
ReplBytecodeInstrumentation.setStopFlag(rendering.classLoader()(using state.context), false)
235+
231236
val previousSignalHandler = terminal.handle(
232237
org.jline.terminal.Terminal.Signal.INT,
233238
(sig: org.jline.terminal.Terminal.Signal) => {
234239
if (!firstCtrlCEntered) {
235240
firstCtrlCEntered = true
241+
// Set the stop flag to trigger throwIfReplStopped() in instrumented code
242+
ReplBytecodeInstrumentation.setStopFlag(rendering.classLoader()(using state.context), true)
243+
// Also interrupt the thread as a fallback for non-instrumented code
236244
thread.interrupt()
237-
out.println("\nInterrupting running thread, Ctrl-C again to terminate the REPL Process")
245+
out.println("\nAttempting to interrupt running thread with `Thread.interrupt`")
238246
} else {
239247
out.println("\nTerminating REPL Process...")
240248
System.exit(130) // Standard exit code for SIGINT
@@ -592,7 +600,10 @@ class ReplDriver(settings: Array[String],
592600
val jarClassLoader = fromURLsParallelCapable(
593601
jarClassPath.asURLs, prevClassLoader)
594602
rendering.myClassLoader = new AbstractFileClassLoader(
595-
prevOutputDir, jarClassLoader)
603+
prevOutputDir,
604+
jarClassLoader,
605+
ctx.settings.XreplInterruptInstrumentation.value
606+
)
596607

597608
out.println(s"Added '$path' to classpath.")
598609
} catch {

compiler/src/dotty/tools/repl/ScriptEngine.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ class ScriptEngine extends AbstractScriptEngine {
2424
"-classpath", "", // Avoid the default "."
2525
"-usejavacp",
2626
"-color:never",
27-
"-Xrepl-disable-display"
27+
"-Xrepl-disable-display",
28+
"-Xrepl-interrupt-instrumentation",
29+
"false"
2830
), Console.out, None)
31+
2932
private val rendering = new Rendering(Some(getClass.getClassLoader))
3033
private var state: State = driver.initialState
3134

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package dotty.tools.repl
2+
3+
import scala.annotation.static
4+
5+
class StopRepl
6+
7+
object StopRepl {
8+
// Needs to be volatile, otherwise changes to this may not get seen by other threads
9+
// for arbitrarily long periods of time (minutes!)
10+
@static @volatile private var stop: Boolean = false
11+
12+
@static def setStop(n: Boolean): Unit = { stop = n }
13+
14+
/** Check if execution should stop, and throw ThreadDeath if so */
15+
@static def throwIfReplStopped(): Unit = {
16+
if (stop) throw new ThreadDeath()
17+
}
18+
}

compiler/test/dotty/tools/repl/AbstractFileClassLoaderTest.scala

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,13 @@ class AbstractFileClassLoaderTest:
5050
@Test def afclGetsParent(): Unit =
5151
val p = new URLClassLoader(Array.empty[URL])
5252
val d = new VirtualDirectory("vd", None)
53-
val x = new AbstractFileClassLoader(d, p)
53+
val x = new AbstractFileClassLoader(d, p, "false")
5454
assertSame(p, x.getParent)
5555

5656
@Test def afclGetsResource(): Unit =
5757
val (fuzz, booz) = fuzzBuzzBooz
5858
booz.writeContent("hello, world")
59-
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader)
59+
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
6060
val res = sut.getResource("buzz/booz.class")
6161
assertNotNull("Find buzz/booz.class", res)
6262
assertEquals("hello, world", slurp(res))
@@ -66,8 +66,8 @@ class AbstractFileClassLoaderTest:
6666
val (fuzz_, booz_) = fuzzBuzzBooz
6767
booz.writeContent("hello, world")
6868
booz_.writeContent("hello, world_")
69-
val p = new AbstractFileClassLoader(fuzz, NoClassLoader)
70-
val sut = new AbstractFileClassLoader(fuzz_, p)
69+
val p = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
70+
val sut = new AbstractFileClassLoader(fuzz_, p, "false")
7171
val res = sut.getResource("buzz/booz.class")
7272
assertNotNull("Find buzz/booz.class", res)
7373
assertEquals("hello, world", slurp(res))
@@ -78,7 +78,7 @@ class AbstractFileClassLoaderTest:
7878
val bass = fuzz.fileNamed("bass")
7979
booz.writeContent("hello, world")
8080
bass.writeContent("lo tone")
81-
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader)
81+
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
8282
val res = sut.getResource("booz.class")
8383
assertNotNull(res)
8484
assertEquals("hello, world", slurp(res))
@@ -88,7 +88,7 @@ class AbstractFileClassLoaderTest:
8888
@Test def afclGetsResources(): Unit =
8989
val (fuzz, booz) = fuzzBuzzBooz
9090
booz.writeContent("hello, world")
91-
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader)
91+
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
9292
val e = sut.getResources("buzz/booz.class")
9393
assertTrue("At least one buzz/booz.class", e.hasMoreElements)
9494
assertEquals("hello, world", slurp(e.nextElement))
@@ -99,8 +99,8 @@ class AbstractFileClassLoaderTest:
9999
val (fuzz_, booz_) = fuzzBuzzBooz
100100
booz.writeContent("hello, world")
101101
booz_.writeContent("hello, world_")
102-
val p = new AbstractFileClassLoader(fuzz, NoClassLoader)
103-
val x = new AbstractFileClassLoader(fuzz_, p)
102+
val p = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
103+
val x = new AbstractFileClassLoader(fuzz_, p, "false")
104104
val e = x.getResources("buzz/booz.class")
105105
assertTrue(e.hasMoreElements)
106106
assertEquals("hello, world", slurp(e.nextElement))
@@ -111,15 +111,15 @@ class AbstractFileClassLoaderTest:
111111
@Test def afclGetsResourceAsStream(): Unit =
112112
val (fuzz, booz) = fuzzBuzzBooz
113113
booz.writeContent("hello, world")
114-
val x = new AbstractFileClassLoader(fuzz, NoClassLoader)
114+
val x = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
115115
val r = x.getResourceAsStream("buzz/booz.class")
116116
assertNotNull(r)
117117
assertEquals("hello, world", closing(r)(is => Source.fromInputStream(is).mkString))
118118

119119
@Test def afclGetsClassBytes(): Unit =
120120
val (fuzz, booz) = fuzzBuzzBooz
121121
booz.writeContent("hello, world")
122-
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader)
122+
val sut = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
123123
val b = sut.classBytes("buzz/booz.class")
124124
assertEquals("hello, world", new String(b, UTF8.charSet))
125125

@@ -129,8 +129,8 @@ class AbstractFileClassLoaderTest:
129129
booz.writeContent("hello, world")
130130
booz_.writeContent("hello, world_")
131131

132-
val p = new AbstractFileClassLoader(fuzz, NoClassLoader)
133-
val sut = new AbstractFileClassLoader(fuzz_, p)
132+
val p = new AbstractFileClassLoader(fuzz, NoClassLoader, "false")
133+
val sut = new AbstractFileClassLoader(fuzz_, p, "false")
134134
val b = sut.classBytes("buzz/booz.class")
135135
assertEquals("hello, world", new String(b, UTF8.charSet))
136136
end AbstractFileClassLoaderTest

staging/src/scala/quoted/staging/QuoteDriver.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ private class QuoteDriver(appClassloader: ClassLoader) extends Driver:
6161
case Left(classname) =>
6262
assert(!ctx.reporter.hasErrors)
6363

64-
val classLoader = new AbstractFileClassLoader(outDir, appClassloader)
64+
val classLoader = new AbstractFileClassLoader(outDir, appClassloader, "false")
6565

6666
val clazz = classLoader.loadClass(classname)
6767
val method = clazz.getMethod("apply")

0 commit comments

Comments
 (0)