-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Re-implement Ammonite's Ctrl-C interruption for Scala REPL via bytecode instrumentation #24194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
6a15587
bdd8be0
4ca79ec
65b75b6
5c89528
9f6b053
b546d5e
c789bf4
b413546
5308000
fb03b2b
68542fc
9561f4e
9c68d96
56dfcda
c1a65db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package dotty.tools | ||
| package repl | ||
|
|
||
| import scala.language.unsafeNulls | ||
|
|
||
| import scala.tools.asm.* | ||
| import scala.tools.asm.Opcodes.* | ||
| import scala.tools.asm.tree.* | ||
| import scala.collection.JavaConverters.* | ||
| import java.util.concurrent.atomic.AtomicBoolean | ||
|
|
||
| object ReplBytecodeInstrumentation: | ||
| /** Instrument bytecode to add checks to throw an exception if the REPL command is cancelled | ||
| */ | ||
| def instrument(originalBytes: Array[Byte]): Array[Byte] = | ||
| try | ||
| val cr = new ClassReader(originalBytes) | ||
| val cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES) | ||
| val instrumenter = new InstrumentClassVisitor(cw) | ||
| cr.accept(instrumenter, ClassReader.EXPAND_FRAMES) | ||
| cw.toByteArray | ||
| catch | ||
| case ex: Exception => originalBytes | ||
|
|
||
| def setStopFlag(classLoader: ClassLoader, b: Boolean): Unit = | ||
| val cancelClassOpt = | ||
| try Some(classLoader.loadClass(classOf[dotty.tools.repl.StopRepl].getName)) | ||
| catch{ | ||
lihaoyi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| case _: java.lang.ClassNotFoundException => None | ||
| } | ||
| for(cancelClass <- cancelClassOpt){ | ||
lihaoyi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| val setAllStopMethod = cancelClass.getDeclaredMethod("setStop", classOf[Boolean]) | ||
| setAllStopMethod.invoke(null, b.asInstanceOf[AnyRef]) | ||
| } | ||
|
|
||
| private class InstrumentClassVisitor(cv: ClassVisitor) extends ClassVisitor(ASM9, cv): | ||
|
|
||
| override def visitMethod( | ||
| access: Int, | ||
| name: String, | ||
| descriptor: String, | ||
| signature: String, | ||
| exceptions: Array[String] | ||
| ): MethodVisitor = | ||
| new InstrumentMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions)) | ||
|
|
||
| /** MethodVisitor that inserts stop checks at backward branches */ | ||
| private class InstrumentMethodVisitor(mv: MethodVisitor) extends MethodVisitor(ASM9, mv): | ||
| // Track labels we've seen to identify backward branches | ||
| private val seenLabels = scala.collection.mutable.Set[Label]() | ||
|
|
||
| def addStopCheck() = mv.visitMethodInsn( | ||
| INVOKESTATIC, | ||
| classOf[dotty.tools.repl.StopRepl].getName.replace('.', '/'), | ||
| "throwIfReplStopped", | ||
| "()V", | ||
| false | ||
| ) | ||
|
|
||
| override def visitCode(): Unit = | ||
| super.visitCode() | ||
| // Insert throwIfReplStopped() call at the start of the method | ||
| // to allow breaking out of deeply recursive methods like fib(99) | ||
| addStopCheck() | ||
|
|
||
| override def visitLabel(label: Label): Unit = | ||
| seenLabels.add(label) | ||
| super.visitLabel(label) | ||
|
|
||
| override def visitJumpInsn(opcode: Int, label: Label): Unit = | ||
| // Add throwIfReplStopped if this is a backward branch (jumping to a label we've already seen) | ||
| if seenLabels.contains(label) then addStopCheck() | ||
| super.visitJumpInsn(opcode, label) | ||
|
|
||
| end ReplBytecodeInstrumentation | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,7 +24,8 @@ class ScriptEngine extends AbstractScriptEngine { | |
| "-classpath", "", // Avoid the default "." | ||
| "-usejavacp", | ||
| "-color:never", | ||
| "-Xrepl-disable-display" | ||
| "-Xrepl-disable-display", | ||
| "-Xrepl-disable-bytecode-instrumentation" | ||
|
||
| ), Console.out, None) | ||
| private val rendering = new Rendering(Some(getClass.getClassLoader)) | ||
| private var state: State = driver.initialState | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package dotty.tools.repl | ||
|
|
||
| import scala.annotation.static | ||
|
|
||
| class StopRepl | ||
|
|
||
| object StopRepl { | ||
| // Needs to be volatile, otherwise changes to this may not get seen by other threads | ||
| // for arbitrarily long periods of time (minutes!) | ||
| @static @volatile private var stop: Boolean = false | ||
|
|
||
| @static def setStop(n: Boolean): Unit = { stop = n } | ||
|
|
||
| /** Check if execution should stop, and throw ThreadDeath if so */ | ||
| @static def throwIfReplStopped(): Unit = { | ||
| if (stop) throw new ThreadDeath() | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.