@@ -8,24 +8,13 @@ import dotty.tools.dotc.util.SourceFile
8
8
9
9
import dotty .tools .dotc .core .Flags .Synthetic
10
10
11
- import dotty .tools .repl .{ReplDriver , State }
12
-
13
11
import org .eclipse .lsp4j .jsonrpc .CancelChecker
14
12
15
13
import java .io .{File , InputStream , InputStreamReader , PrintStream }
16
14
import java .util .concurrent .CancellationException
17
15
18
16
object Worksheet {
19
17
20
- private val javaExec : Option [String ] = {
21
- val isWindows = sys.props(" os.name" ).toLowerCase().indexOf(" win" ) >= 0
22
- val bin = new File (sys.props(" java.home" ), " bin" )
23
- val java = new File (bin, if (isWindows) " java.exe" else " java" )
24
-
25
- if (java.exists()) Some (java.getAbsolutePath())
26
- else None
27
- }
28
-
29
18
/**
30
19
* Evaluate `tree` as a worksheet using the REPL.
31
20
*
@@ -36,132 +25,220 @@ object Worksheet {
36
25
def evaluate (tree : SourceTree ,
37
26
sendMessage : String => Unit ,
38
27
cancelChecker : CancelChecker )(
39
- implicit ctx : Context ): Unit = {
40
-
41
- val replMain = dotty.tools.repl.WorksheetMain .getClass.getName.init
42
- val classpath = sys.props(" java.class.path" )
43
- val options = Array (javaExec.get, " -classpath" , classpath, replMain) ++ replOptions
44
- val replProcess = new ProcessBuilder (options : _* ).redirectErrorStream(true ).start()
45
-
46
- // The stream that we use to send commands to the REPL
47
- val replIn = new PrintStream (replProcess.getOutputStream())
48
-
49
- // Messages coming out of the REPL
50
- val replOut = new ReplReader (replProcess.getInputStream())
51
- replOut.start()
52
-
53
- // The thread that monitors cancellation
54
- val cancellationThread = new CancellationThread (replOut, replProcess, cancelChecker)
55
- cancellationThread.start()
56
-
57
- // Wait for the REPL to be ready
58
- replOut.next()
59
-
60
- tree.tree match {
61
- case td @ TypeDef (_, template : Template ) =>
62
- val executed = collection.mutable.Set .empty[(Int , Int )]
63
-
64
- template.body.foreach {
65
- case statement : DefTree if statement.symbol.is(Synthetic ) =>
66
- ()
67
-
68
- case statement if executed.add(bounds(statement.pos)) =>
69
- val line = execute(replIn, statement, tree.source)
70
- val result = replOut.next().trim
71
- if (result.nonEmpty) sendMessage(encode(result, line))
72
-
73
- case _ =>
74
- ()
28
+ implicit ctx : Context ): Unit = synchronized {
29
+
30
+ Evaluator .get(cancelChecker) match {
31
+ case None =>
32
+ sendMessage(encode(" Couldn't start JVM." , 1 ))
33
+ case Some (evaluator) =>
34
+ tree.tree match {
35
+ case td @ TypeDef (_, template : Template ) =>
36
+ val executed = collection.mutable.Set .empty[(Int , Int )]
37
+
38
+ template.body.foreach {
39
+ case statement : DefTree if statement.symbol.is(Synthetic ) =>
40
+ ()
41
+
42
+ case statement if executed.add(bounds(statement.pos)) =>
43
+ try {
44
+ cancelChecker.checkCanceled()
45
+ val (line, result) = execute(evaluator, statement, tree.source)
46
+ if (result.nonEmpty) sendMessage(encode(result, line))
47
+ } catch { case _ : CancellationException => () }
48
+
49
+ case _ =>
50
+ ()
51
+ }
75
52
}
76
53
}
77
54
}
78
55
79
56
/**
80
57
* Extract `tree` from the source and evaluate it in the REPL.
81
58
*
82
- * @param replIn A stream to send commands to the REPL.
59
+ * @param evaluator The JVM that runs the REPL.
83
60
* @param tree The compiled tree to evaluate.
84
61
* @param sourcefile The sourcefile of the worksheet.
85
- * @return The line in the sourcefile that corresponds to `tree`.
62
+ * @return The line in the sourcefile that corresponds to `tree`, and the result .
86
63
*/
87
- private def execute (replIn : PrintStream , tree : Tree , sourcefile : SourceFile ): Int = {
64
+ private def execute (evaluator : Evaluator , tree : Tree , sourcefile : SourceFile ): ( Int , String ) = {
88
65
val source = sourcefile.content.slice(tree.pos.start, tree.pos.end).mkString
89
66
val line = sourcefile.offsetToLine(tree.pos.end)
90
- replIn.println(source)
91
- replIn.flush()
92
- line
67
+ (line, evaluator.eval(source))
93
68
}
94
69
95
- private def replOptions (implicit ctx : Context ): Array [String ] =
96
- Array (" -color:never" , " -classpath" , ctx.settings.classpath.value)
97
-
98
70
private def encode (message : String , line : Int ): String =
99
71
line + " :" + message
100
72
101
73
private def bounds (pos : Position ): (Int , Int ) = (pos.start, pos.end)
102
74
75
+ }
76
+
77
+ private object Evaluator {
78
+
79
+ private val javaExec : Option [String ] = {
80
+ val isWindows = sys.props(" os.name" ).toLowerCase().indexOf(" win" ) >= 0
81
+ val bin = new File (sys.props(" java.home" ), " bin" )
82
+ val java = new File (bin, if (isWindows) " java.exe" else " java" )
83
+
84
+ if (java.exists()) Some (java.getAbsolutePath())
85
+ else None
86
+ }
87
+
88
+ /**
89
+ * The most recent Evaluator that was used. It can be reused if the user classpath hasn't changed
90
+ * between two calls.
91
+ */
92
+ private [this ] var previousEvaluator : Option [(String , Evaluator )] = None
93
+
103
94
/**
104
- * Regularly check whether execution has been cancelled, kill REPL if it is .
95
+ * Get a (possibly reused) Evaluator and set cancel checker .
105
96
*
106
- * @param replReader The ReplReader that reads the output of the REPL
107
- * @param process The forked JVM that runs the REPL.
108
- * @param cancelChecker The token that reports cancellation.
97
+ * @param cancelChecker The token that indicates whether evaluation has been cancelled.
98
+ * @return A JVM running the REPL.
109
99
*/
110
- private class CancellationThread (replReader : ReplReader , process : Process , cancelChecker : CancelChecker ) extends Thread {
111
- private final val checkCancelledDelayMs = 50
112
-
113
- override def run (): Unit = {
114
- try {
115
- while (! Thread .interrupted()) {
116
- cancelChecker.checkCanceled()
117
- Thread .sleep(checkCancelledDelayMs)
118
- }
119
- } catch {
120
- case _ : CancellationException =>
121
- replReader.interrupt()
122
- process.destroyForcibly()
123
- }
100
+ def get (cancelChecker : CancelChecker )(implicit ctx : Context ): Option [Evaluator ] = {
101
+ val classpath = ctx.settings.classpath.value
102
+ previousEvaluator match {
103
+ case Some (cp, evaluator) if cp == classpath =>
104
+ evaluator.reset(cancelChecker)
105
+ Some (evaluator)
106
+ case _ =>
107
+ previousEvaluator.foreach(_._2.exit())
108
+ val newEvaluator = javaExec.map(new Evaluator (_, ctx.settings.classpath.value, cancelChecker))
109
+ previousEvaluator = newEvaluator.map(jvm => (classpath, jvm))
110
+ newEvaluator
124
111
}
125
112
}
113
+ }
114
+
115
+ /**
116
+ * Represents a JVM running the REPL, ready for evaluation.
117
+ *
118
+ * @param javaExec The path to the `java` executable.
119
+ * @param userClasspath The REPL classpath
120
+ * @param cancelChecker The token that indicates whether evaluation has been cancelled.
121
+ */
122
+ private class Evaluator private (javaExec : String ,
123
+ userClasspath : String ,
124
+ cancelChecker : CancelChecker ) {
125
+ private val process =
126
+ new ProcessBuilder (
127
+ javaExec,
128
+ " -classpath" , sys.props(" java.class.path" ),
129
+ dotty.tools.repl.WorksheetMain .getClass.getName.init,
130
+ " -classpath" , userClasspath,
131
+ " -color:never" )
132
+ .redirectErrorStream(true )
133
+ .start()
134
+
135
+ // The stream that we use to send commands to the REPL
136
+ private val processInput = new PrintStream (process.getOutputStream())
137
+
138
+ // Messages coming out of the REPL
139
+ private val processOutput = new ReplReader (process.getInputStream())
140
+ processOutput.start()
141
+
142
+ // The thread that monitors cancellation
143
+ private val cancellationThread = new CancellationThread (cancelChecker, this )
144
+ cancellationThread.start()
145
+
146
+ // Wait for the REPL to be ready
147
+ processOutput.next()
126
148
127
149
/**
128
- * Reads the output from the REPL and makes it available via `next()` .
150
+ * Submit `command` to the REPL, wait for the result .
129
151
*
130
- * @param stream The stream of messages coming out of the REPL.
152
+ * @param command The command to evaluate.
153
+ * @return The result from the REPL.
131
154
*/
132
- private class ReplReader (stream : InputStream ) extends Thread {
133
- private val in = new InputStreamReader (stream)
134
-
135
- private [this ] var output : Option [String ] = None
136
-
137
- override def run (): Unit = synchronized {
138
- val prompt = " scala> "
139
- val buffer = new StringBuilder
140
- val chars = new Array [Char ](256 )
141
- var read = 0
142
-
143
- while (! Thread .interrupted() && { read = in.read(chars); read >= 0 }) {
144
- buffer.appendAll(chars, 0 , read)
145
- if (buffer.endsWith(prompt)) {
146
- output = Some (buffer.toString.stripSuffix(prompt))
147
- buffer.clear()
148
- notify()
149
- wait()
150
- }
155
+ def eval (command : String ): String = {
156
+ processInput.println(command)
157
+ processInput.flush()
158
+ processOutput.next().trim
159
+ }
160
+
161
+ /**
162
+ * Reset the REPL to its initial state, update the cancel checker.
163
+ */
164
+ def reset (cancelChecker : CancelChecker ): Unit = {
165
+ cancellationThread.setCancelChecker(cancelChecker)
166
+ eval(" :reset" )
167
+ }
168
+
169
+ /** Terminate this JVM. */
170
+ def exit (): Unit = {
171
+ processOutput.interrupt()
172
+ process.destroyForcibly()
173
+ Evaluator .previousEvaluator = None
174
+ cancellationThread.interrupt()
175
+ }
176
+
177
+ }
178
+
179
+ /**
180
+ * Regularly check whether execution has been cancelled, kill REPL if it is.
181
+ */
182
+ private class CancellationThread (private [this ] var cancelChecker : CancelChecker ,
183
+ evaluator : Evaluator ) extends Thread {
184
+ private final val checkCancelledDelayMs = 50
185
+
186
+ override def run (): Unit = {
187
+ try {
188
+ while (! Thread .interrupted()) {
189
+ cancelChecker.checkCanceled()
190
+ Thread .sleep(checkCancelledDelayMs)
151
191
}
192
+ } catch {
193
+ case _ : CancellationException => evaluator.exit()
194
+ case _ : InterruptedException => evaluator.exit()
152
195
}
196
+ }
197
+
198
+ def setCancelChecker (cancelChecker : CancelChecker ): Unit = {
199
+ this .cancelChecker = cancelChecker
200
+ }
201
+ }
153
202
154
- /** Block until the next message is ready. */
155
- def next (): String = synchronized {
156
- while (output.isEmpty) {
203
+ /**
204
+ * Reads the output from the REPL and makes it available via `next()`.
205
+ *
206
+ * @param stream The stream of messages coming out of the REPL.
207
+ */
208
+ private class ReplReader (stream : InputStream ) extends Thread {
209
+ private val in = new InputStreamReader (stream)
210
+
211
+ private [this ] var output : Option [String ] = None
212
+
213
+ override def run (): Unit = synchronized {
214
+ val prompt = " scala> "
215
+ val buffer = new StringBuilder
216
+ val chars = new Array [Char ](256 )
217
+ var read = 0
218
+
219
+ while (! Thread .interrupted() && { read = in.read(chars); read >= 0 }) {
220
+ buffer.appendAll(chars, 0 , read)
221
+ if (buffer.endsWith(prompt)) {
222
+ output = Some (buffer.toString.stripSuffix(prompt))
223
+ buffer.clear()
224
+ notify()
157
225
wait()
158
226
}
159
-
160
- val result = output.get
161
- notify()
162
- output = None
163
- result
164
227
}
228
+ output = Some (" " )
229
+ notify()
165
230
}
166
231
232
+ /** Block until the next message is ready. */
233
+ def next (): String = synchronized {
234
+
235
+ while (output.isEmpty) {
236
+ wait()
237
+ }
238
+
239
+ val result = output.get
240
+ notify()
241
+ output = None
242
+ result
243
+ }
167
244
}
0 commit comments