Skip to content

Commit a15b2b1

Browse files
committed
Fix classloader confusion in the REPL
The REPL constructed a classloader containing the user classpath but having the classloader used to run the REPL as a parent, meaning that the scala-library we actually used came from the JVM classpath, so of course everything exploded when using a repl compiled with the 2.12 stdlib and running with the 2.13 stdlib. Fixed by having Rendering#classpath be constructed without any parent, just based on the user classpath. This required some additional changes: - We called ScalaRuntime#replStringOf using the ScalaRunTime class from the scala-library used by the REPL, switch to calling it reflectively through - ReplDriver#withRedirectedOutput used `Console.withOut/Err` but that only affects the Console object in the current classloader, not the one in the user classloader, use `System.setOut/Err` instead (the class `System` comes from the system classloader so there's only one of it). - The macro tests now need to be run with the compiler on the user classpath, this required some refactoring (This would be simpler to handle if ReplTest didn't extend ReplDriver, but I'll let someone else do that refactoring)
1 parent 6170bce commit a15b2b1

File tree

5 files changed

+127
-102
lines changed

5 files changed

+127
-102
lines changed

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

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import java.io.{ StringWriter, PrintWriter }
55
import java.lang.{ ClassLoader, ExceptionInInitializerError }
66
import java.lang.reflect.InvocationTargetException
77

8-
import scala.runtime.ScalaRunTime
9-
108
import dotc.core.Contexts.Context
119
import dotc.core.Denotations.Denotation
1210
import dotc.core.Flags
@@ -23,25 +21,42 @@ import dotc.core.StdNames.str
2321
*/
2422
private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None) {
2523

24+
private[this] val MaxStringElements: Int = 1000 // no need to mkString billions of elements
25+
2626
private[this] var myClassLoader: ClassLoader = _
2727

28+
private[this] var myReplStringOf: Object => String = _
29+
30+
2831
/** Class loader used to load compiled code */
2932
private[repl] def classLoader()(implicit ctx: Context) =
3033
if (myClassLoader != null) myClassLoader
3134
else {
3235
val parent = parentClassLoader.getOrElse {
33-
// the compiler's classpath, as URL's
3436
val compilerClasspath = ctx.platform.classPath(ctx).asURLs
35-
new java.net.URLClassLoader(compilerClasspath.toArray, classOf[ReplDriver].getClassLoader)
37+
new java.net.URLClassLoader(compilerClasspath.toArray, null)
3638
}
3739

3840
myClassLoader = new AbstractFileClassLoader(ctx.settings.outputDir.value, parent)
39-
// Set the current Java "context" class loader to this rendering class loader
40-
Thread.currentThread.setContextClassLoader(myClassLoader)
41+
myReplStringOf = {
42+
// We need to use the ScalaRunTime class coming from the scala-library
43+
// on the user classpath, and not the one avilable in the current
44+
// classloader, so we use reflection instead of simply calling
45+
// `ScalaRunTime.replStringOf`.
46+
val scalaRuntime = Class.forName("scala.runtime.ScalaRunTime", true, myClassLoader)
47+
val meth = scalaRuntime.getMethod("replStringOf", classOf[Object], classOf[Int])
48+
49+
(value: Object) => meth.invoke(null, value, Integer.valueOf(MaxStringElements)).asInstanceOf[String]
50+
}
4151
myClassLoader
4252
}
4353

44-
private[this] def MaxStringElements = 1000 // no need to mkString billions of elements
54+
/** Return a String representation of a value we got from `classLoader()`. */
55+
private[repl] def replStringOf(value: Object)(implicit ctx: Context): String = {
56+
assert(myReplStringOf != null,
57+
"replStringOf should only be called on values creating using `classLoader()`, but `classLoader()` has not been called so far")
58+
myReplStringOf(value)
59+
}
4560

4661
/** Load the value of the symbol using reflection.
4762
*
@@ -55,7 +70,7 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None) {
5570
resObj
5671
.getDeclaredMethods.find(_.getName == sym.name.encode.toString)
5772
.map(_.invoke(null))
58-
val string = value.map(ScalaRunTime.replStringOf(_, MaxStringElements).trim)
73+
val string = value.map(replStringOf(_).trim)
5974
if (!sym.is(Flags.Method) && sym.info == defn.UnitType)
6075
None
6176
else

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,19 @@ class ReplDriver(settings: Array[String],
139139
// TODO: i5069
140140
final def bind(name: String, value: Any)(implicit state: State): State = state
141141

142-
private def withRedirectedOutput(op: => State): State =
143-
Console.withOut(out) { Console.withErr(out) { op } }
142+
private def withRedirectedOutput(op: => State): State = {
143+
val savedOut = System.out
144+
val savedErr = System.err
145+
try {
146+
System.setOut(out)
147+
System.setErr(out)
148+
op
149+
}
150+
finally {
151+
System.setOut(savedOut)
152+
System.setErr(savedErr)
153+
}
154+
}
144155

145156
private def newRun(state: State) = {
146157
val run = compiler.newRun(rootCtx.fresh.setReporter(newStoreReporter), state)

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

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,27 @@ package repl
33

44
import vulpix.TestConfiguration
55

6-
import java.io.{ByteArrayOutputStream, PrintStream}
6+
import java.lang.System.{lineSeparator => EOL}
7+
import java.io.{ByteArrayOutputStream, File => JFile, PrintStream}
8+
import scala.io.Source
9+
10+
import scala.collection.mutable.ArrayBuffer
711

812
import dotty.tools.dotc.reporting.MessageRendering
913
import org.junit.{After, Before}
14+
import org.junit.Assert._
1015

1116

12-
class ReplTest private (out: ByteArrayOutputStream) extends ReplDriver(
13-
Array("-classpath", TestConfiguration.basicClasspath, "-color:never"),
17+
class ReplTest(withCompiler: Boolean = false, out: ByteArrayOutputStream = new ByteArrayOutputStream) extends ReplDriver(
18+
Array(
19+
"-classpath",
20+
if (withCompiler)
21+
TestConfiguration.withCompilerClasspath
22+
else
23+
TestConfiguration.basicClasspath,
24+
"-color:never"),
1425
new PrintStream(out)
1526
) with MessageRendering {
16-
17-
def this() = this(new ByteArrayOutputStream)
18-
1927
/** Get the stored output from `out`, resetting the buffer */
2028
def storedOutput(): String = {
2129
val output = stripColor(out.toString)
@@ -37,4 +45,77 @@ class ReplTest private (out: ByteArrayOutputStream) extends ReplDriver(
3745
implicit class TestingState(state: State) {
3846
def andThen[A](op: State => A): A = op(state)
3947
}
48+
49+
def scripts(path: String): Array[JFile] = {
50+
val dir = new JFile(getClass.getResource(path).getPath)
51+
assert(dir.exists && dir.isDirectory, "Couldn't load scripts dir")
52+
dir.listFiles
53+
}
54+
55+
def testFile(f: JFile): Unit = {
56+
val prompt = "scala>"
57+
val lines = Source.fromFile(f, "UTF-8").getLines().buffered
58+
59+
assert(lines.head.startsWith(prompt),
60+
s"""Each file has to start with the prompt: "$prompt"""")
61+
62+
def extractInputs(prompt: String): List[String] = {
63+
val input = lines.next()
64+
65+
if (!input.startsWith(prompt)) extractInputs(prompt)
66+
else if (lines.hasNext) {
67+
// read lines and strip trailing whitespace:
68+
while (lines.hasNext && !lines.head.startsWith(prompt))
69+
lines.next()
70+
71+
input :: { if (lines.hasNext) extractInputs(prompt) else Nil }
72+
}
73+
else Nil
74+
}
75+
76+
def evaluate(state: State, input: String, prompt: String) =
77+
try {
78+
val nstate = run(input.drop(prompt.length))(state)
79+
val out = input + EOL + storedOutput()
80+
(out, nstate)
81+
}
82+
catch {
83+
case ex: Throwable =>
84+
System.err.println(s"failed while running script: $f, on:\n$input")
85+
throw ex
86+
}
87+
88+
def filterEmpties(line: String): List[String] =
89+
line.replaceAll("""(?m)\s+$""", "") match {
90+
case "" => Nil
91+
case nonEmptyLine => nonEmptyLine :: Nil
92+
}
93+
94+
val expectedOutput =
95+
Source.fromFile(f, "UTF-8").getLines().flatMap(filterEmpties).mkString(EOL)
96+
val actualOutput = {
97+
resetToInitial()
98+
val inputRes = extractInputs(prompt)
99+
val buf = new ArrayBuffer[String]
100+
inputRes.foldLeft(initialState) { (state, input) =>
101+
val (out, nstate) = evaluate(state, input, prompt)
102+
buf.append(out)
103+
104+
assert(out.endsWith("\n"),
105+
s"Expected output of $input to end with newline")
106+
107+
nstate
108+
}
109+
buf.flatMap(filterEmpties).mkString(EOL)
110+
}
111+
112+
if (expectedOutput != actualOutput) {
113+
println("expected =========>")
114+
println(expectedOutput)
115+
println("actual ===========>")
116+
println(actualOutput)
117+
118+
fail(s"Error in file $f, expected output did not match actual")
119+
}
120+
}
40121
}

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

Lines changed: 4 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2,98 +2,16 @@ package dotty
22
package tools
33
package repl
44

5-
import java.io.{File => JFile}
6-
import java.lang.System.{lineSeparator => EOL}
7-
8-
import org.junit.Assert._
95
import org.junit.Test
10-
import org.junit.experimental.categories.Category
11-
12-
import scala.collection.mutable.ArrayBuffer
13-
import scala.io.Source
14-
15-
import dotc.reporting.MessageRendering
166

177
/** Runs all tests contained in `compiler/test-resources/repl/` */
18-
class ScriptedTests extends ReplTest with MessageRendering {
19-
20-
private def scripts(path: String): Array[JFile] = {
21-
val dir = new JFile(getClass.getResource(path).getPath)
22-
assert(dir.exists && dir.isDirectory, "Couldn't load scripts dir")
23-
dir.listFiles
24-
}
25-
26-
private def testFile(f: JFile): Unit = {
27-
val prompt = "scala>"
28-
val lines = Source.fromFile(f, "UTF-8").getLines().buffered
29-
30-
assert(lines.head.startsWith(prompt),
31-
s"""Each file has to start with the prompt: "$prompt"""")
32-
33-
def extractInputs(prompt: String): List[String] = {
34-
val input = lines.next()
35-
36-
if (!input.startsWith(prompt)) extractInputs(prompt)
37-
else if (lines.hasNext) {
38-
// read lines and strip trailing whitespace:
39-
while (lines.hasNext && !lines.head.startsWith(prompt))
40-
lines.next()
41-
42-
input :: { if (lines.hasNext) extractInputs(prompt) else Nil }
43-
}
44-
else Nil
45-
}
46-
47-
def evaluate(state: State, input: String, prompt: String) =
48-
try {
49-
val nstate = run(input.drop(prompt.length))(state)
50-
val out = input + EOL + storedOutput()
51-
(out, nstate)
52-
}
53-
catch {
54-
case ex: Throwable =>
55-
System.err.println(s"failed while running script: $f, on:\n$input")
56-
throw ex
57-
}
58-
59-
def filterEmpties(line: String): List[String] =
60-
line.replaceAll("""(?m)\s+$""", "") match {
61-
case "" => Nil
62-
case nonEmptyLine => nonEmptyLine :: Nil
63-
}
64-
65-
val expectedOutput =
66-
Source.fromFile(f, "UTF-8").getLines().flatMap(filterEmpties).mkString(EOL)
67-
val actualOutput = {
68-
resetToInitial()
69-
val inputRes = extractInputs(prompt)
70-
val buf = new ArrayBuffer[String]
71-
inputRes.foldLeft(initialState) { (state, input) =>
72-
val (out, nstate) = evaluate(state, input, prompt)
73-
buf.append(out)
74-
75-
assert(out.endsWith("\n"),
76-
s"Expected output of $input to end with newline")
77-
78-
nstate
79-
}
80-
buf.flatMap(filterEmpties).mkString(EOL)
81-
}
82-
83-
if (expectedOutput != actualOutput) {
84-
println("expected =========>")
85-
println(expectedOutput)
86-
println("actual ===========>")
87-
println(actualOutput)
88-
89-
fail(s"Error in file $f, expected output did not match actual")
90-
}
91-
}
8+
class ScriptedTests extends ReplTest {
9+
import ScriptedTests._
9210

9311
@Test def replTests = scripts("/repl").foreach(testFile)
9412

9513
@Test def typePrinterTests = scripts("/type-printer").foreach(testFile)
14+
}
9615

97-
@Category(Array(classOf[BootstrappedOnlyTests]))
98-
@Test def replMacrosTests = scripts("/repl-macros").foreach(testFile)
16+
object ScriptedTests {
9917
}

0 commit comments

Comments
 (0)