diff options
Diffstat (limited to 'src/compiler/scala/tools/nsc/interpreter/ILoop.scala')
-rw-r--r-- | src/compiler/scala/tools/nsc/interpreter/ILoop.scala | 651 |
1 files changed, 651 insertions, 0 deletions
diff --git a/src/compiler/scala/tools/nsc/interpreter/ILoop.scala b/src/compiler/scala/tools/nsc/interpreter/ILoop.scala new file mode 100644 index 0000000000..413f08dfb1 --- /dev/null +++ b/src/compiler/scala/tools/nsc/interpreter/ILoop.scala @@ -0,0 +1,651 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2011 LAMP/EPFL + * @author Alexander Spoon + */ + +package scala.tools.nsc +package interpreter + +import Predef.{ println => _, _ } +import java.io.{ BufferedReader, FileReader, PrintWriter } +import java.io.IOException + +import scala.sys.process.Process +import scala.tools.nsc.interpreter.{ Results => IR } +import scala.tools.util.SignalManager +import scala.annotation.tailrec +import scala.util.control.Exception.{ ignoring } +import scala.collection.mutable.ListBuffer +import scala.concurrent.ops +import util.{ ClassPath } +import interpreter._ +import io.File + +/** The Scala interactive shell. It provides a read-eval-print loop + * around the Interpreter class. + * After instantiation, clients should call the main() method. + * + * If no in0 is specified, then input will come from the console, and + * the class will attempt to provide input editing feature such as + * input history. + * + * @author Moez A. Abdel-Gawad + * @author Lex Spoon + * @version 1.2 + */ +class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) + extends AnyRef + with LoopCommands +{ + def this(in0: BufferedReader, out: PrintWriter) = this(Some(in0), out) + def this() = this(None, new PrintWriter(Console.out)) + + var in: InteractiveReader = _ // the input stream from which commands come + var settings: Settings = _ + var intp: IMain = _ + var power: Power = _ + + @deprecated("Use `intp` instead.") + def interpreter = intp + + /** The context class loader at the time this object was created */ + protected val originalClassLoader = Thread.currentThread.getContextClassLoader + + // classpath entries added via :cp + var addedClasspath: String = "" + + /** A reverse list of commands to replay if the user requests a :replay */ + var replayCommandStack: List[String] = Nil + + /** A list of commands to replay if the user requests a :replay */ + def replayCommands = replayCommandStack.reverse + + /** Record a command for replay should the user request a :replay */ + def addReplay(cmd: String) = replayCommandStack ::= cmd + + /** Try to install sigint handler: ignore failure. Signal handler + * will interrupt current line execution if any is in progress. + * + * Attempting to protect the repl from accidental exit, we only honor + * a single ctrl-C if the current buffer is empty: otherwise we look + * for a second one within a short time. + */ + private def installSigIntHandler() { + def onExit() { + Console.println("") // avoiding "shell prompt in middle of line" syndrome + sys.exit(1) + } + ignoring(classOf[Exception]) { + SignalManager("INT") = { + if (intp == null) + onExit() + else if (intp.lineManager.running) + intp.lineManager.cancel() + else if (in.currentLine != "") { + // non-empty buffer, so make them hit ctrl-C a second time + SignalManager("INT") = onExit() + io.timer(5)(installSigIntHandler()) // and restore original handler if they don't + } + else onExit() + } + } + } + + /** Close the interpreter and set the var to null. */ + def closeInterpreter() { + if (intp ne null) { + intp.close + intp = null + power = null + Thread.currentThread.setContextClassLoader(originalClassLoader) + } + } + + /** Create a new interpreter. */ + def createInterpreter() { + if (addedClasspath != "") + settings.classpath append addedClasspath + + intp = new IMain(settings, out) { + override lazy val formatting = new Formatting { + def prompt = ILoop.this.prompt + } + override protected def createLineManager() = new Line.Manager { + override def onRunaway(line: Line[_]): Unit = { + val template = """ + |// She's gone rogue, captain! Have to take her out! + |// Calling Thread.stop on runaway %s with offending code: + |// scala> %s""".stripMargin + + println(template.format(line.thread, line.code)) + // XXX no way to suppress the deprecation warning + line.thread.stop() + in.redrawLine() + } + } + override protected def parentClassLoader = + settings.explicitParentLoader.getOrElse( classOf[ILoop].getClassLoader ) + } + intp.setContextClassLoader() + installSigIntHandler() + // intp.quietBind("settings", "scala.tools.nsc.InterpreterSettings", intp.isettings) + } + + /** print a friendly help message */ + def printHelp() = { + out println "All commands can be abbreviated - for example :he instead of :help.\n" + val cmds = commands map (x => (x.usage, x.help)) + val width: Int = cmds map { case (x, _) => x.length } max + val formatStr = "%-" + width + "s %s" + cmds foreach { case (usage, help) => out println formatStr.format(usage, help) } + } + + /** Print a welcome message */ + def printWelcome() { + import Properties._ + val welcomeMsg = + """|Welcome to Scala %s (%s, Java %s). + |Type in expressions to have them evaluated. + |Type :help for more information.""" . + stripMargin.format(versionString, javaVmName, javaVersion) + + plushln(welcomeMsg) + } + + /** Show the history */ + def printHistory(xs: List[String]): Result = { + if (in.history eq History.Empty) + return "No history available." + + val defaultLines = 20 + val current = in.history.index + val count = try xs.head.toInt catch { case _: Exception => defaultLines } + val lines = in.history.asStrings takeRight count + val offset = current - lines.size + 1 + + for ((line, index) <- lines.zipWithIndex) + println("%3d %s".format(index + offset, line)) + } + + /** Some print conveniences */ + def println(x: Any) = out println x + def plush(x: Any) = { out print x ; out.flush() } + def plushln(x: Any) = { out println x ; out.flush() } + + /** Search the history */ + def searchHistory(_cmdline: String) { + val cmdline = _cmdline.toLowerCase + val offset = in.history.index - in.history.size + 1 + + for ((line, index) <- in.history.asStrings.zipWithIndex ; if line.toLowerCase contains cmdline) + println("%d %s".format(index + offset, line)) + } + + private var currentPrompt = Properties.shellPromptString + def setPrompt(prompt: String) = currentPrompt = prompt + /** Prompt to print when awaiting input */ + def prompt = currentPrompt + + /** Standard commands **/ + val standardCommands: List[LoopCommand] = { + List( + LineArg("cp", "add an entry (jar or directory) to the classpath", addClasspath), + NoArgs("help", "print this help message", printHelp), + VarArgs("history", "show the history (optional arg: lines to show)", printHistory), + LineArg("h?", "search the history", searchHistory), + OneArg("load", "load and interpret a Scala file", load), + NoArgs("power", "enable power user mode", powerCmd), + NoArgs("quit", "exit the interpreter", () => Result(false, None)), + NoArgs("replay", "reset execution and replay all previous commands", replay), + LineArg("sh", "fork a shell and run a command", runShellCmd), + NoArgs("silent", "disable/enable automatic printing of results", verbosity) + ) + } + + /** Power user commands */ + val powerCommands: List[LoopCommand] = { + List( + NoArgs("dump", "displays a view of the interpreter's internal state", power.toString _), + LineArg("phase", "set the implicit phase for power commands", phaseCommand), + LineArg("symfilter", "change the filter for symbol printing", symfilterCmd) + ) + } + private def symfilterCmd(line: String): Result = { + if (line == "") { + power.vars.symfilter set "_ => true" + "Remove symbol filter." + } + else { + power.vars.symfilter set line + "Set symbol filter to '" + line + "'." + } + } + private def phaseCommand(_name: String): Result = { + val name = _name.toLowerCase + // This line crashes us in TreeGen: + // + // if (intp.power.phased set name) "..." + // + // Exception in thread "main" java.lang.AssertionError: assertion failed: ._7.type + // at scala.Predef$.assert(Predef.scala:99) + // at scala.tools.nsc.ast.TreeGen.mkAttributedQualifier(TreeGen.scala:69) + // at scala.tools.nsc.ast.TreeGen.mkAttributedQualifier(TreeGen.scala:44) + // at scala.tools.nsc.ast.TreeGen.mkAttributedRef(TreeGen.scala:101) + // at scala.tools.nsc.ast.TreeGen.mkAttributedStableRef(TreeGen.scala:143) + // + // But it works like so, type annotated. + val x: Phased = power.phased + if (name == "") "Active phase is '" + x.get + "'" + else if (x set name) "Active phase is now '" + name + "'" + else "'" + name + "' does not appear to be a valid phase." + } + + /** Available commands */ + def commands: List[LoopCommand] = standardCommands ++ ( + if (power == null) Nil + else powerCommands + ) + + /** The main read-eval-print loop for the repl. It calls + * command() for each line of input, and stops when + * command() returns false. + */ + def loop() { + def readOneLine() = { + out.flush + in readLine prompt + } + // return false if repl should exit + def processLine(line: String): Boolean = + if (line eq null) false // assume null means EOF + else command(line) match { + case Result(false, _) => false + case Result(_, Some(finalLine)) => addReplay(finalLine) ; true + case _ => true + } + + while (processLine(readOneLine)) { } + } + + /** interpret all lines from a specified file */ + def interpretAllFrom(file: File) { + val oldIn = in + val oldReplay = replayCommandStack + + try file applyReader { reader => + in = new SimpleReader(reader, out, false) + plushln("Loading " + file + "...") + loop() + } + finally { + in = oldIn + replayCommandStack = oldReplay + } + } + + /** create a new interpreter and replay all commands so far */ + def replay() { + closeInterpreter() + createInterpreter() + for (cmd <- replayCommands) { + plushln("Replaying: " + cmd) // flush because maybe cmd will have its own output + command(cmd) + out.println + } + } + + /** fork a shell and run a command */ + def runShellCmd(cmd: String) { + intp.beQuietDuring { intp.interpret("import _root_.scala.sys.process._") } + val xs = Process(cmd).lines + if (xs.nonEmpty) + intp.bind("stdout", "scala.Stream[String]", xs) + } + + def withFile(filename: String)(action: File => Unit) { + val f = File(filename) + + if (f.exists) action(f) + else out.println("That file does not exist") + } + + def load(arg: String) = { + var shouldReplay: Option[String] = None + withFile(arg)(f => { + interpretAllFrom(f) + shouldReplay = Some(":load " + arg) + }) + Result(true, shouldReplay) + } + + def addClasspath(arg: String): Unit = { + val f = File(arg).normalize + if (f.exists) { + addedClasspath = ClassPath.join(addedClasspath, f.path) + val totalClasspath = ClassPath.join(settings.classpath.value, addedClasspath) + println("Added '%s'. Your new classpath is:\n\"%s\"".format(f.path, totalClasspath)) + replay() + } + else out.println("The path '" + f + "' doesn't seem to exist.") + } + + def powerCmd(): Result = { + if (power != null) + return "Already in power mode." + + power = new Power(this) + power.unleash() + power.banner + } + + def verbosity() = { + val old = intp.printResults + intp.printResults = !old + out.println("Switched " + (if (old) "off" else "on") + " result printing.") + } + + /** Run one command submitted by the user. Two values are returned: + * (1) whether to keep running, (2) the line to record for replay, + * if any. */ + def command(line: String): Result = { + def withError(msg: String) = { + out println msg + Result(true, None) + } + def ambiguous(cmds: List[LoopCommand]) = "Ambiguous: did you mean " + cmds.map(":" + _.name).mkString(" or ") + "?" + + // not a command + if (!line.startsWith(":")) { + // Notice failure to create compiler + if (intp.global == null) return Result(false, None) + else return Result(true, interpretStartingWith(line)) + } + + val tokens = (line drop 1 split """\s+""").toList + if (tokens.isEmpty) + return withError(ambiguous(commands)) + + val (cmd :: args) = tokens + + // this lets us add commands willy-nilly and only requires enough command to disambiguate + commands.filter(_.name startsWith cmd) match { + case List(x) => x(args) + case Nil => withError("Unknown command. Type :help for help.") + case xs => withError(ambiguous(xs)) + } + } + + private val CONTINUATION_STRING = " | " + private val PROMPT_STRING = "scala> " + + /** If it looks like they're pasting in a scala interpreter + * transcript, remove all the formatting we inserted so we + * can make some sense of it. + */ + private var pasteStamp: Long = 0 + + /** Returns true if it's long enough to quit. */ + def updatePasteStamp(): Boolean = { + /* Enough milliseconds between readLines to call it a day. */ + val PASTE_FINISH = 1000 + + val prevStamp = pasteStamp + pasteStamp = System.currentTimeMillis + + (pasteStamp - prevStamp > PASTE_FINISH) + + } + /** TODO - we could look for the usage of resXX variables in the transcript. + * Right now backreferences to auto-named variables will break. + */ + + /** The trailing lines complication was an attempt to work around the introduction + * of newlines in e.g. email messages of repl sessions. It doesn't work because + * an unlucky newline can always leave you with a syntactically valid first line, + * which is executed before the next line is considered. So this doesn't actually + * accomplish anything, but I'm leaving it in case I decide to try harder. + */ + case class PasteCommand(cmd: String, trailing: ListBuffer[String] = ListBuffer[String]()) + + /** Commands start on lines beginning with "scala>" and each successive + * line which begins with the continuation string is appended to that command. + * Everything else is discarded. When the end of the transcript is spotted, + * all the commands are replayed. + */ + @tailrec private def cleanTranscript(lines: List[String], acc: List[PasteCommand]): List[PasteCommand] = lines match { + case Nil => acc.reverse + case x :: xs if x startsWith PROMPT_STRING => + val first = x stripPrefix PROMPT_STRING + val (xs1, xs2) = xs span (_ startsWith CONTINUATION_STRING) + val rest = xs1 map (_ stripPrefix CONTINUATION_STRING) + val result = (first :: rest).mkString("", "\n", "\n") + + cleanTranscript(xs2, PasteCommand(result) :: acc) + + case ln :: lns => + val newacc = acc match { + case Nil => Nil + case PasteCommand(cmd, trailing) :: accrest => + PasteCommand(cmd, trailing :+ ln) :: accrest + } + cleanTranscript(lns, newacc) + } + + /** The timestamp is for safety so it doesn't hang looking for the end + * of a transcript. Ad hoc parsing can't be too demanding. You can + * also use ctrl-D to start it parsing. + */ + @tailrec private def interpretAsPastedTranscript(lines: List[String]) { + val line = in.readLine("") + val finished = updatePasteStamp() + + if (line == null || finished || line.trim == PROMPT_STRING.trim) { + val xs = cleanTranscript(lines.reverse, Nil) + println("Replaying %d commands from interpreter transcript." format xs.size) + for (PasteCommand(cmd, trailing) <- xs) { + out.flush() + def runCode(code: String, extraLines: List[String]) { + (intp interpret code) match { + case IR.Incomplete if extraLines.nonEmpty => + runCode(code + "\n" + extraLines.head, extraLines.tail) + case _ => () + } + } + runCode(cmd, trailing.toList) + } + } + else + interpretAsPastedTranscript(line :: lines) + } + + /** Interpret expressions starting with the first line. + * Read lines until a complete compilation unit is available + * or until a syntax error has been seen. If a full unit is + * read, go ahead and interpret it. Return the full string + * to be recorded for replay, if any. + */ + def interpretStartingWith(code: String): Option[String] = { + // signal completion non-completion input has been received + in.completion.resetVerbosity() + + def reallyInterpret = intp.interpret(code) match { + case IR.Error => None + case IR.Success => Some(code) + case IR.Incomplete => + if (in.interactive && code.endsWith("\n\n")) { + out.println("You typed two blank lines. Starting a new command.") + None + } + else in.readLine(CONTINUATION_STRING) match { + case null => + // we know compilation is going to fail since we're at EOF and the + // parser thinks the input is still incomplete, but since this is + // a file being read non-interactively we want to fail. So we send + // it straight to the compiler for the nice error message. + intp.compileString(code) + None + + case line => interpretStartingWith(code + "\n" + line) + } + } + + /** Here we place ourselves between the user and the interpreter and examine + * the input they are ostensibly submitting. We intervene in several cases: + * + * 1) If the line starts with "scala> " it is assumed to be an interpreter paste. + * 2) If the line starts with "." (but not ".." or "./") it is treated as an invocation + * on the previous result. + * 3) If the Completion object's execute returns Some(_), we inject that value + * and avoid the interpreter, as it's likely not valid scala code. + */ + if (code == "") None + else if (code startsWith PROMPT_STRING) { + updatePasteStamp() + interpretAsPastedTranscript(List(code)) + None + } + else if (Completion.looksLikeInvocation(code) && intp.mostRecentVar != "") { + interpretStartingWith(intp.mostRecentVar + code) + } + else { + if (intp.isParseable(code)) reallyInterpret + else (in.completion execute code) match { + // completion took responsibility, so do not parse + // but do directly inject the result + case Some(res) => injectAndName(res) ; None + case _ => reallyInterpret // we know it will fail, this is to show the error + } + } + } + + // 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) + out.println() + } + case _ => + } + + def process(settings: Settings): Boolean = { + this.settings = settings + createInterpreter() + + // sets in to some kind of reader depending on environmental cues + in = in0 match { + case Some(in0) => new SimpleReader(in0, out, true) + case None => + // the interpreter is passed as an argument to expose tab completion info + if (settings.Xnojline.value || Properties.isEmacsShell) new SimpleReader + else JLineReader( + if (settings.noCompletion.value) Completion.Empty + else new JLineCompletion(intp) + ) + } + + loadFiles(settings) + try { + // it is broken on startup; go ahead and exit + if (intp.reporter.hasErrors) return false + + printWelcome() + + // this is about the illusion of snappiness. We call initialize() + // which spins off a separate thread, then print the prompt and try + // our best to look ready. Ideally the user will spend a + // couple seconds saying "wow, it starts so fast!" and by the time + // they type a command the compiler is ready to roll. + intp.initialize() + if (sys.props contains PowerProperty) { + plushln("Starting in power mode, one moment...\n") + powerCmd() + } + loop() + } + finally closeInterpreter() + true + } + + private def objClass(x: Any) = x.asInstanceOf[AnyRef].getClass + private def objName(x: Any) = { + val clazz = objClass(x) + clazz.getName + tpString(clazz) + } + + def tpString[T: Manifest] : String = + tpString(manifest[T].erasure) + + def tpString(clazz: Class[_]): String = { + clazz.getTypeParameters.size match { + case 0 => "" + case x => List.fill(x)("_").mkString("[", ", ", "]") + } + } + + def inject[T: Manifest](name: String, value: T): (String, String) = { + intp.bind[T](name, value) + (name, objName(value)) + } + def injectAndName(obj: Any): (String, String) = { + val name = intp.getVarName + val className = objName(obj) + intp.bind(name, className, obj) + (name, className) + } + + /** process command-line arguments and do as they request */ + def process(args: Array[String]): Boolean = { + def error1(msg: String) = out println ("scala: " + msg) + val command = new CommandLine(args.toList, error1) + def neededHelp(): String = + (if (command.settings.help.value) command.usageMsg + "\n" else "") + + (if (command.settings.Xhelp.value) command.xusageMsg + "\n" else "") + + // if they asked for no help and command is valid, we call the real main + neededHelp() match { + case "" => if (command.ok) main(command.settings) // else nothing + case help => plush(help) + } + true + } + + @deprecated("Use `process` instead") + def main(args: Array[String]): Unit = process(args) + @deprecated("Use `process` instead") + def main(settings: Settings): Unit = process(settings) +} + +object ILoop { + implicit def loopToInterpreter(repl: ILoop): IMain = repl.intp + + // provide the enclosing type T + // in order to set up the interpreter's classpath and parent class loader properly + def breakIf[T: Manifest](assertion: => Boolean, args: NamedParam*): Unit = + if (assertion) break[T](args.toList) + + // start a repl, binding supplied args + def break[T: Manifest](args: List[NamedParam]): Unit = { + val msg = if (args.isEmpty) "" else " Binding " + args.size + " value%s.".format( + if (args.size == 1) "" else "s" + ) + Console.println("Debug repl starting." + msg) + val repl = new ILoop { + override def prompt = "\ndebug> " + } + repl.settings = new Settings(Console println _) + repl.settings.embeddedDefaults[T] + repl.createInterpreter() + repl.in = JLineReader(repl) + + // rebind exit so people don't accidentally call sys.exit by way of predef + repl.quietRun("""def exit = println("Type :quit to resume program execution.")""") + args foreach (p => repl.bind(p.name, p.tpe, p.value)) + repl.loop() + + Console.println("\nDebug repl exiting.") + repl.closeInterpreter() + } +}
\ No newline at end of file |