/* NSC -- new Scala compiler * Copyright 2005-2009 LAMP/EPFL * @author Alexander Spoon */ // $Id: InterpreterLoop.scala 18548 2009-08-22 21:27:49Z extempore $ package scala.tools.nsc import java.io.{BufferedReader, File, FileReader, PrintWriter} import java.io.IOException import scala.tools.nsc.{InterpreterResults => IR} import scala.tools.nsc.interpreter._ // Classes to wrap up interpreter commands and their results // You can add new commands by adding entries to val commands // inside InterpreterLoop. object InterpreterControl { // the default result means "keep running, and don't record that line" val defaultResult = Result(true, None) // a single interpreter command sealed abstract class Command extends Function1[List[String], Result] { val name: String val help: String def error(msg: String) = { println(":" + name + " " + msg + ".") Result(true, None) } def getHelp(): String = ":" + name + " " + help + "." } case class NoArgs(name: String, help: String, f: () => Result) extends Command { def apply(args: List[String]) = if (args.isEmpty) f() else error("accepts no arguments") } case class LineArg(name: String, help: String, f: (String) => Result) extends Command { def apply(args: List[String]) = if (args.size == 1) f(args.head) else error("requires a line of input") } case class OneArg(name: String, help: String, f: (String) => Result) extends Command { def apply(args: List[String]) = if (args.size == 1) f(args.head) else error("requires exactly one argument") } case class VarArgs(name: String, help: String, f: (List[String]) => Result) extends Command { def apply(args: List[String]) = f(args) } // the result of a single command case class Result(keepRunning: Boolean, lineToRecord: Option[String]) } import InterpreterControl._ // import scala.concurrent.ops.defaultRunner /** The * <a href="http://scala-lang.org/" target="_top">Scala</a> * interactive shell. It provides a read-eval-print loop around * the Interpreter class. * After instantiation, clients should call the <code>main()</code> method. * * <p>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 InterpreterLoop(in0: Option[BufferedReader], out: PrintWriter) { def this(in0: BufferedReader, out: PrintWriter) = this(Some(in0), out) def this() = this(None, new PrintWriter(Console.out)) /** The input stream from which commands come, set by main() */ var in: InteractiveReader = _ /** The context class loader at the time this object was created */ protected val originalClassLoader = Thread.currentThread.getContextClassLoader var settings: Settings = _ // set by main() var interpreter: Interpreter = _ // set by createInterpreter() def isettings = interpreter.isettings // XXX var addedClasspath: List[String] = Nil /** A reverse list of commands to replay if the user requests a :replay */ var replayCommandsRev: List[String] = Nil /** A list of commands to replay if the user requests a :replay */ def replayCommands = replayCommandsRev.reverse /** Record a command for replay should the user request a :replay */ def addReplay(cmd: String) = replayCommandsRev = cmd :: replayCommandsRev /** Close the interpreter and set the var to <code>null</code>. */ def closeInterpreter() { if (interpreter ne null) { interpreter.close interpreter = null Thread.currentThread.setContextClassLoader(originalClassLoader) } } /** Create a new interpreter. */ def createInterpreter() { if (!addedClasspath.isEmpty) settings.classpath.value += addedClasspath.map(File.pathSeparator + _).mkString interpreter = new Interpreter(settings, out) { override protected def parentClassLoader = classOf[InterpreterLoop].getClassLoader } interpreter.setContextClassLoader() } /** Bind the settings so that evaluated code can modify them */ def bindSettings() { interpreter.beQuietDuring { interpreter.compileString(InterpreterSettings.sourceCodeForClass) interpreter.bind("settings", "scala.tools.nsc.InterpreterSettings", isettings) } } /** print a friendly help message */ def printHelp() = { out println "All commands can be abbreviated - for example :h or :he instead of :help.\n" commands foreach { c => out println c.getHelp } } /** 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) out println welcomeMsg out.flush } /** Prompt to print when awaiting input */ val prompt = Properties.shellPromptString // most commands do not want to micromanage the Result, but they might want // to print something to the console, so we accomodate Unit and String returns. object CommandImplicits { implicit def u2ir(x: Unit): Result = defaultResult implicit def s2ir(s: String): Result = { out println s defaultResult } } /** Standard commands **/ val standardCommands: List[Command] = { import CommandImplicits._ List( NoArgs("help", "prints this help message", printHelp), OneArg("jar", "add a jar to the classpath", addJar), OneArg("load", "followed by a filename loads a Scala file", load), NoArgs("power", "enable power user mode", power), NoArgs("quit", "exits the interpreter", () => Result(false, None)), NoArgs("replay", "resets execution and replays all previous commands", replay), NoArgs("silent", "disable/enable automatic printing of results", verbosity) ) } /** Power user commands */ // XXX - why does a third argument like "interpreter dumpState(_)" throw an NPE // while the version below works? var powerUserOn = false val powerCommands: List[Command] = { import CommandImplicits._ List( VarArgs("dump", "displays a view of the interpreter's internal state", (xs: List[String]) => interpreter dumpState xs), VarArgs("tree", "displays ASTs for specified identifiers", (xs: List[String]) => interpreter dumpTrees xs) // LineArg("meta", "given code which produces scala code, executes the results", // (xs: List[String]) => ) ) } /** Available commands */ def commands: List[Command] = standardCommands ::: (if (powerUserOn) powerCommands else Nil) /** The main read-eval-print loop for the interpreter. It calls * <code>command()</code> for each line of input, and stops when * <code>command()</code> returns <code>false</code>. */ def repl() { 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 } /* For some reason, the first interpreted command always takes * a second or two. So, wait until the welcome message * has been printed before calling bindSettings. That way, * the user can read the welcome message while this * command executes. */ val futLine = scala.concurrent.ops.future(readOneLine) bindSettings() if (!processLine(futLine())) return // loops until false, then returns while (processLine(readOneLine)) { } } /** interpret all lines from a specified file */ def interpretAllFrom(filename: String) { val fileIn = try { new FileReader(filename) } catch { case _:IOException => return out.println("Error opening file: " + filename) } val oldIn = in val oldReplay = replayCommandsRev try { val inFile = new BufferedReader(fileIn) in = new SimpleReader(inFile, out, false) out.println("Loading " + filename + "...") out.flush repl } finally { in = oldIn replayCommandsRev = oldReplay fileIn.close } } /** create a new interpreter and replay all commands so far */ def replay() { closeInterpreter() createInterpreter() for (cmd <- replayCommands) { out.println("Replaying: " + cmd) out.flush() // because maybe cmd will have its own output command(cmd) out.println } } def withFile(filename: String)(action: String => Unit) { if (! new File(filename).exists) out.println("That file does not exist") else action(filename) } def load(arg: String) = { var shouldReplay: Option[String] = None withFile(arg)(f => { interpretAllFrom(f) shouldReplay = Some(":load " + arg) }) Result(true, shouldReplay) } def addJar(arg: String): Unit = { val f = new java.io.File(arg) if (!f.exists) { out.println("The file '" + f + "' doesn't seem to exist.") return } addedClasspath = addedClasspath ::: List(f.getCanonicalPath) println("Added " + f.getCanonicalPath + " to your classpath.") replay() } def power() = { powerUserOn = true interpreter.powerUser() } def verbosity() = { val old = interpreter.printResults interpreter.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[Command]) = "Ambiguous: did you mean " + cmds.map(":" + _.name).mkString(" or ") + "?" // not a command if (!line.startsWith(":")) return Result(true, interpretStartingWith(line)) val tokens = line.substring(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)) } } /** 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] = interpreter.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(" | ") match { case null => None // end of file case line => interpretStartingWith(code + "\n" + line) } } // 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) replayCommandsRev = cmd :: replayCommandsRev out.println() } case _ => } def main(settings: Settings) { 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 => val emacsShell = System.getProperty("env.emacs", "") != "" // the interpeter is passed as an argument to expose tab completion info if (settings.Xnojline.value || emacsShell) new SimpleReader else if (settings.noCompletion.value) InteractiveReader.createDefault() else InteractiveReader.createDefault(interpreter) } loadFiles(settings) try { // it is broken on startup; go ahead and exit if (interpreter.reporter.hasErrors) return printWelcome() repl() } finally { closeInterpreter() } } // injects one value into the repl; returns pair of name and class def injectOne(name: String, obj: Any): Tuple2[String, String] = { val className = obj.asInstanceOf[AnyRef].getClass.getName interpreter.bind(name, className, obj) (name, className) } // injects list of values into the repl; returns summary string def inject(args: List[Any]): String = { val strs = for ((arg, i) <- args.zipWithIndex) yield { val varName = "p" + (i + 1) val (vname, vtype) = injectOne(varName, arg) vname + ": " + vtype } if (strs.size == 0) "Set no variables." else "Variables set:\n" + strs.mkString("\n") } /** process command-line arguments and do as they request */ def main(args: Array[String]) { def error1(msg: String) = out println ("scala: " + msg) val command = new InterpreterCommand(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 => out print help ; out flush } } }