diff options
author | Lukas Rytz <lukas.rytz@typesafe.com> | 2016-06-01 09:51:33 +0200 |
---|---|---|
committer | Lukas Rytz <lukas.rytz@typesafe.com> | 2016-06-01 09:51:33 +0200 |
commit | 90215ce2b9cb99b5dea7c0ef474eea50755c0a40 (patch) | |
tree | 291b6c18bce82614411a14be77bda80844c7350c /src | |
parent | 971f5abc37538e1c4e7f0eef6b175f7951634e86 (diff) | |
parent | b462e5a97b499bc91222014e45ec2439f56b46b7 (diff) | |
download | scala-90215ce2b9cb99b5dea7c0ef474eea50755c0a40.tar.gz scala-90215ce2b9cb99b5dea7c0ef474eea50755c0a40.tar.bz2 scala-90215ce2b9cb99b5dea7c0ef474eea50755c0a40.zip |
Merge pull request #4998 from som-snytt/issue/7898-i
SI-7898 Read user input during REPL warmup
Diffstat (limited to 'src')
5 files changed, 250 insertions, 74 deletions
diff --git a/src/compiler/scala/tools/nsc/GenericRunnerCommand.scala b/src/compiler/scala/tools/nsc/GenericRunnerCommand.scala index 24496fa013..bab612bad5 100644 --- a/src/compiler/scala/tools/nsc/GenericRunnerCommand.scala +++ b/src/compiler/scala/tools/nsc/GenericRunnerCommand.scala @@ -79,6 +79,7 @@ Other startup options: -howtorun what to run <script|object|jar|guess> (default: guess) -i <file> preload <file> before starting the repl + -I <file> preload <file>, enforcing line-by-line interpretation -e <string> execute <string> as if entered in the repl -save save the compiled script in a jar for future use -nc no compilation daemon: do not use the fsc offline compiler diff --git a/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala b/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala index 1289d55c37..d1f8db048b 100644 --- a/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala +++ b/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala @@ -21,9 +21,15 @@ class GenericRunnerSettings(error: String => Unit) extends Settings(error) { val loadfiles = MultiStringSetting( + "-I", + "file", + "load a file line-by-line") + + val pastefiles = + MultiStringSetting( "-i", "file", - "load a file (assumes the code is given interactively)") + "paste a file") val execute = StringSetting( diff --git a/src/repl/scala/tools/nsc/interpreter/ILoop.scala b/src/repl/scala/tools/nsc/interpreter/ILoop.scala index adac438b37..7dab371caf 100644 --- a/src/repl/scala/tools/nsc/interpreter/ILoop.scala +++ b/src/repl/scala/tools/nsc/interpreter/ILoop.scala @@ -1,5 +1,5 @@ /* NSC -- new Scala compiler - * Copyright 2005-2015 LAMP/EPFL + * Copyright 2005-2016 LAMP/EPFL * @author Alexander Spoon */ package scala @@ -15,7 +15,7 @@ import scala.tools.asm.ClassReader import scala.util.Properties.{ jdkHome, javaVersion, versionString, javaVmName } import scala.tools.nsc.util.{ ClassPath, Exceptional, stringFromWriter, stringFromStream } import scala.reflect.classTag -import scala.reflect.internal.util.{ BatchSourceFile, ScalaClassLoader } +import scala.reflect.internal.util.{ BatchSourceFile, ScalaClassLoader, NoPosition } import ScalaClassLoader._ import scala.reflect.io.{ File, Directory } import scala.tools.util._ @@ -175,10 +175,19 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) echo("\n" + msg) in.redrawLine() } - protected def echo(msg: String) = { + protected var mum = false + protected def echo(msg: String) = if (!mum) { out println msg out.flush() } + // turn off intp reporter and our echo + def mumly[A](op: => A): A = + if (isReplDebug) op + else intp beQuietDuring { + val saved = mum + mum = true + try op finally mum = saved + } /** Search the history */ def searchHistory(_cmdline: String) { @@ -408,12 +417,13 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) * command() for each line of input, and stops when * command() returns false. */ - @tailrec final def loop(): LineResult = { + final def loop(): LineResult = loop(readOneLine()) + + @tailrec final def loop(line: String): LineResult = { import LineResults._ - readOneLine() match { - case null => EOF - case line => if (try processLine(line) catch crashRecovery) loop() else ERR - } + if (line == null) EOF + else if (try processLine(line) catch crashRecovery) loop(readOneLine()) + else ERR } /** interpret all lines from a specified file */ @@ -566,9 +576,9 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) } } - def withFile[A](filename: String)(action: File => A): Option[A] = { + def withFile[A](filename: String)(action: File => A): Option[A] = intp.withLabel(filename) { val res = Some(File(filename)) filter (_.exists) map action - if (res.isEmpty) echo("That file does not exist") // courtesy side-effect + if (res.isEmpty) intp.reporter.warning(NoPosition, s"File `$filename' does not exist.") // courtesy side-effect res } @@ -705,6 +715,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) */ def pasteCommand(arg: String): Result = { var shouldReplay: Option[String] = None + var label = "<pastie>" def result = Result(keepRunning = true, shouldReplay) val (raw, file, margin) = if (arg.isEmpty) (false, None, None) @@ -725,6 +736,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) } val code = (file, margin) match { case (Some(name), None) => + label = name withFile(name) { f => shouldReplay = Some(s":paste $arg") val s = f.slurp.trim @@ -747,17 +759,17 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) text } def interpretCode() = { - val res = intp interpret code + val res = intp.withLabel(label)(intp interpret code) // if input is incomplete, let the compiler try to say why if (res == IR.Incomplete) { echo("The pasted code is incomplete!\n") // Remembrance of Things Pasted in an object - val errless = intp compileSources new BatchSourceFile("<pastie>", s"object pastel {\n$code\n}") + val errless = intp compileSources new BatchSourceFile(label, s"object pastel {\n$code\n}") if (errless) echo("...but compilation found no error? Good luck with that.") } } def compileCode() = { - val errless = intp compileSources new BatchSourceFile("<pastie>", code) + val errless = intp compileSources new BatchSourceFile(label, code) if (!errless) echo("There were compilation errors!") } if (code.nonEmpty) { @@ -829,19 +841,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) } } - // runs :load `file` on any files passed via -i - def loadFiles(settings: Settings) = settings match { - case settings: GenericRunnerSettings => - for (filename <- settings.loadfiles.value) { - val cmd = ":load " + filename - command(cmd) - addReplay(cmd) - echo("") - } - case _ => - } - - /** Tries to create a JLineReader, falling back to SimpleReader, + /** Tries to create a jline.InteractiveReader, falling back to SimpleReader, * unless settings or properties are such that it should start with SimpleReader. * The constructor of the InteractiveReader must take a Completion strategy, * supplied as a `() => Completion`; the Completion object provides a concrete Completer. @@ -885,49 +885,116 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) } } - private def loopPostInit() { - // Bind intp somewhere out of the regular namespace where - // we can get at it in generated code. - intp.quietBind(NamedParam[IMain]("$intp", intp)(tagOfIMain, classTag[IMain])) - // Auto-run code via some setting. - ( replProps.replAutorunCode.option - flatMap (f => io.File(f).safeSlurp()) - foreach (intp quietRun _) - ) - // classloader and power mode setup - intp.setContextClassLoader() - if (isReplPower) { - replProps.power setValue true - unleashAndSetPhase() - asyncMessage(power.banner) - } - // SI-7418 Now, and only now, can we enable TAB completion. - in.postInit() - } - - // start an interpreter with the given settings + /** Start an interpreter with the given settings. + * @return true if successful + */ def process(settings: Settings): Boolean = savingContextLoader { - this.settings = settings - createInterpreter() - // sets in to some kind of reader depending on environmental cues - in = in0.fold(chooseReader(settings))(r => SimpleReader(r, out, interactive = true)) - globalFuture = future { - intp.initializeSynchronous() - loopPostInit() - !intp.reporter.hasErrors + def newReader = in0.fold(chooseReader(settings))(r => SimpleReader(r, out, interactive = true)) + + /** Reader to use before interpreter is online. */ + def preLoop = { + val sr = SplashReader(newReader) { r => + in = r + in.postInit() + } + in = sr + SplashLoop(sr, prompt) } - loadFiles(settings) - printWelcome() - try loop() match { - case LineResults.EOF => out print Properties.shellInterruptedString - case _ => + /* Actions to cram in parallel while collecting first user input at prompt. + * Run with output muted both from ILoop and from the intp reporter. + */ + def loopPostInit(): Unit = mumly { + // Bind intp somewhere out of the regular namespace where + // we can get at it in generated code. + intp.quietBind(NamedParam[IMain]("$intp", intp)(tagOfIMain, classTag[IMain])) + + // Auto-run code via some setting. + ( replProps.replAutorunCode.option + flatMap (f => File(f).safeSlurp()) + foreach (intp quietRun _) + ) + // power mode setup + if (isReplPower) { + replProps.power setValue true + unleashAndSetPhase() + asyncMessage(power.banner) + } + loadInitFiles() + // SI-7418 Now, and only now, can we enable TAB completion. + in.postInit() } - catch AbstractOrMissingHandler() - finally closeInterpreter() + def loadInitFiles(): Unit = settings match { + case settings: GenericRunnerSettings => + for (f <- settings.loadfiles.value) { + loadCommand(f) + addReplay(s":load $f") + } + for (f <- settings.pastefiles.value) { + pasteCommand(f) + addReplay(s":paste $f") + } + case _ => + } + // wait until after startup to enable noisy settings + def withSuppressedSettings[A](body: => A): A = { + val ss = this.settings + import ss._ + val noisy = List(Xprint, Ytyperdebug) + val noisesome = noisy.exists(!_.isDefault) + val current = (Xprint.value, Ytyperdebug.value) + if (isReplDebug || !noisesome) body + else { + this.settings.Xprint.value = List.empty + this.settings.Ytyperdebug.value = false + try body + finally { + Xprint.value = current._1 + Ytyperdebug.value = current._2 + intp.global.printTypings = current._2 + } + } + } + def startup(): String = withSuppressedSettings { + // starting + printWelcome() + + // let them start typing + val splash = preLoop + splash.start() - true + // while we go fire up the REPL + try { + createInterpreter() + intp.initializeSynchronous() + globalFuture = Future successful true + if (intp.reporter.hasErrors) { + echo("Interpreter encountered errors during initialization!") + null + } else { + loopPostInit() + val line = splash.line // what they typed in while they were waiting + if (line == null) { // they ^D + try out print Properties.shellInterruptedString + finally closeInterpreter() + } + line + } + } finally splash.stop() + } + this.settings = settings + startup() match { + case null => false + case line => + try loop(line) match { + case LineResults.EOF => out print Properties.shellInterruptedString + case _ => + } + catch AbstractOrMissingHandler() + finally closeInterpreter() + true + } } @deprecated("Use `process` instead", "2.9.0") diff --git a/src/repl/scala/tools/nsc/interpreter/IMain.scala b/src/repl/scala/tools/nsc/interpreter/IMain.scala index ef6ab4063a..dc8b6204c0 100644 --- a/src/repl/scala/tools/nsc/interpreter/IMain.scala +++ b/src/repl/scala/tools/nsc/interpreter/IMain.scala @@ -1,5 +1,5 @@ /* NSC -- new Scala compiler - * Copyright 2005-2013 LAMP/EPFL + * Copyright 2005-2016 LAMP/EPFL * @author Martin Odersky */ @@ -74,13 +74,14 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set lazy val isClassBased: Boolean = settings.Yreplclassbased.value - private[nsc] var printResults = true // whether to print result lines - private[nsc] var totalSilence = false // whether to print anything - private var _initializeComplete = false // compiler is initialized - private var _isInitialized: Future[Boolean] = null // set up initialization future - private var bindExceptions = true // whether to bind the lastException variable - private var _executionWrapper = "" // code to be wrapped around all lines - var partialInput: String = "" // code accumulated in multi-line REPL input + private[nsc] var printResults = true // whether to print result lines + private[nsc] var totalSilence = false // whether to print anything + private var _initializeComplete = false // compiler is initialized + private var _isInitialized: Future[Boolean] = null // set up initialization future + private var bindExceptions = true // whether to bind the lastException variable + private var _executionWrapper = "" // code to be wrapped around all lines + var partialInput: String = "" // code accumulated in multi-line REPL input + private var label = "<console>" // compilation unit name for reporting /** We're going to go to some trouble to initialize the compiler asynchronously. * It's critical that nothing call into it until it's been initialized or we will @@ -108,6 +109,12 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set try body finally if (!saved) settings.nowarn.value = false } + // Apply a temporary label for compilation (for example, script name) + def withLabel[A](temp: String)(body: => A): A = { + val saved = label + label = temp + try body finally label = saved + } /** construct an interpreter that reports to Console */ def this(settings: Settings, out: JPrintWriter) = this(null, settings, out) @@ -810,7 +817,7 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set case Right(result) => Right(result) } - def compile(source: String): Boolean = compileAndSaveRun("<console>", source) + def compile(source: String): Boolean = compileAndSaveRun(label, source) /** The innermost object inside the wrapper, found by * following accessPath into the outer one. @@ -1184,7 +1191,7 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set var isIncomplete = false def parse = { reporter.reset() - val trees = newUnitParser(line).parseStats() + val trees = newUnitParser(line, label).parseStats() if (reporter.hasErrors) Error(trees) else if (isIncomplete) Incomplete(trees) else Success(trees) diff --git a/src/repl/scala/tools/nsc/interpreter/InteractiveReader.scala b/src/repl/scala/tools/nsc/interpreter/InteractiveReader.scala index 71753a3e39..1f81d9965c 100644 --- a/src/repl/scala/tools/nsc/interpreter/InteractiveReader.scala +++ b/src/repl/scala/tools/nsc/interpreter/InteractiveReader.scala @@ -50,3 +50,98 @@ object InteractiveReader { def createDefault(): InteractiveReader = apply() // used by sbt } +/** Collect one line of user input from the supplied reader. + * Runs on a new thread while the REPL is initializing on the main thread. + * + * The user can enter text or a `:paste` command. + */ +class SplashLoop(reader: InteractiveReader, prompt: String) extends Runnable { + import java.util.concurrent.SynchronousQueue + import scala.compat.Platform.EOL + + private val result = new SynchronousQueue[Option[String]] + @volatile private var running: Boolean = _ + private var thread: Thread = _ + + /** Read one line of input which can be retrieved with `line`. */ + def run(): Unit = { + var line = "" + try + do { + line = reader.readLine(prompt) + if (line != null) { + line = process(line.trim) + } + } while (line != null && line.isEmpty && running) + finally { + result.put(Option(line)) + } + } + + /** Check for `:paste` command. */ + private def process(line: String): String = { + def isPrefix(s: String, p: String, n: Int) = ( + //s != null && p.inits.takeWhile(_.length >= n).exists(s == _) + s != null && s.length >= n && s.length <= p.length && s == p.take(s.length) + ) + if (isPrefix(line, ":paste", 3)) { + // while collecting lines, check running flag + var help = f"// Entering paste mode (ctrl-D to finish)%n%n" + def readWhile(cond: String => Boolean) = { + Iterator continually reader.readLine(help) takeWhile { x => + help = "" + x != null && cond(x) + } + } + val text = (readWhile(_ => running) mkString EOL).trim + val next = + if (text.isEmpty) "// Nothing pasted, nothing gained." + else "// Exiting paste mode, now interpreting." + Console println f"%n${next}%n" + text + } else { + line + } + } + + def start(): Unit = result.synchronized { + require(thread == null, "Already started") + thread = new Thread(this) + running = true + thread.start() + } + + def stop(): Unit = result.synchronized { + running = false + if (thread != null) thread.interrupt() + thread = null + } + + /** Block for the result line, or null on ctl-D. */ + def line: String = result.take getOrElse null +} +object SplashLoop { + def apply(reader: SplashReader, prompt: String): SplashLoop = new SplashLoop(reader, prompt) +} + +/** Reader during splash. Handles splash-completion with a stub, otherwise delegates. */ +class SplashReader(reader: InteractiveReader, postIniter: InteractiveReader => Unit) extends InteractiveReader { + /** Invoke the postInit action with the underlying reader. */ + override def postInit(): Unit = postIniter(reader) + + override val interactive: Boolean = reader.interactive + + override def reset(): Unit = reader.reset() + override def history: History = reader.history + override val completion: Completion = NoCompletion + override def redrawLine(): Unit = reader.redrawLine() + + override protected[interpreter] def readOneLine(prompt: String): String = ??? // unused + override protected[interpreter] def readOneKey(prompt: String): Int = ??? // unused + + override def readLine(prompt: String): String = reader.readLine(prompt) +} +object SplashReader { + def apply(reader: InteractiveReader)(postIniter: InteractiveReader => Unit) = + new SplashReader(reader, postIniter) +} |