diff options
author | Martin Odersky <odersky@gmail.com> | 2016-02-13 22:35:57 +0100 |
---|---|---|
committer | Martin Odersky <odersky@gmail.com> | 2016-02-17 18:38:52 +0100 |
commit | 6ecdc8a69db1a808269b1c288284a4a430ce865e (patch) | |
tree | c641eac92c93ca5d59f1c9c531633ae7eaffc72e /src | |
parent | 5f5eca9ee9367c57da8138f2618759dfc86ffb71 (diff) | |
download | dotty-6ecdc8a69db1a808269b1c288284a4a430ce865e.tar.gz dotty-6ecdc8a69db1a808269b1c288284a4a430ce865e.tar.bz2 dotty-6ecdc8a69db1a808269b1c288284a4a430ce865e.zip |
First PoC of REPL
Adaptation of REPL by Spoon from ca 2007. Compiles OK, but
not yet tested.
Diffstat (limited to 'src')
-rw-r--r-- | src/dotty/tools/dotc/REPL.scala | 47 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/AbstractFileClassLoader.scala | 31 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/ConsoleWriter.scala | 21 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/InteractiveReader.scala | 28 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/Interpreter.scala | 920 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/InterpreterLoop.scala | 248 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/InterpreterResults.scala | 19 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/InterpreterSettings.scala | 69 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/NewLinePrintWriter.scala | 11 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/SimpleReader.scala | 23 |
10 files changed, 1417 insertions, 0 deletions
diff --git a/src/dotty/tools/dotc/REPL.scala b/src/dotty/tools/dotc/REPL.scala new file mode 100644 index 000000000..a0255efa6 --- /dev/null +++ b/src/dotty/tools/dotc/REPL.scala @@ -0,0 +1,47 @@ +package dotty.tools +package dotc + +import core.Phases +import core.Contexts.Context +import reporting.Reporter +import java.io.EOFException +import scala.annotation.tailrec +import io.VirtualDirectory +import java.io.{BufferedReader, File, FileReader, PrintWriter} +import repl._ + +/** A compiler which stays resident between runs. + * Usage: + * + * > scala dotty.tools.dotc.Resident <options> <initial files> + * + * dotc> "more options and files to compile" + * + * ... + * + * dotc> :reset // reset all options to the ones passed on the command line + * + * ... + * + * dotc> :q // quit + */ +class REPL extends Driver { + + def input(implicit ctx: Context): InteractiveReader = { + val emacsShell = System.getProperty("env.emacs", "") != "" + //println("emacsShell="+emacsShell) //debug + if (ctx.settings.Xnojline.value || emacsShell) new SimpleReader() + else InteractiveReader.createDefault() + } + + def output: PrintWriter = new NewLinePrintWriter(new ConsoleWriter, true) + + override def newCompiler(): Compiler = new repl.Interpreter(output) + + override def sourcesRequired = false + + override def doCompile(compiler: Compiler, fileNames: List[String])(implicit ctx: Context): Reporter = { + new InterpreterLoop(compiler, input, output).run() + ctx.reporter + } +} diff --git a/src/dotty/tools/dotc/repl/AbstractFileClassLoader.scala b/src/dotty/tools/dotc/repl/AbstractFileClassLoader.scala new file mode 100644 index 000000000..a3a463717 --- /dev/null +++ b/src/dotty/tools/dotc/repl/AbstractFileClassLoader.scala @@ -0,0 +1,31 @@ +package dotty.tools +package dotc +package repl + +import io.AbstractFile + +/** + * A class loader that loads files from a {@link scala.tools.nsc.io.AbstractFile}. + * + * @author Lex Spoon + */ +class AbstractFileClassLoader(root: AbstractFile, parent: ClassLoader) +extends ClassLoader(parent) +{ + override def findClass(name: String): Class[_] = { + var file: AbstractFile = root + val pathParts = name.split("[./]").toList + for (dirPart <- pathParts.init) { + file = file.lookupName(dirPart, true) + if (file == null) { + throw new ClassNotFoundException(name) + } + } + file = file.lookupName(pathParts.last+".class", false) + if (file == null) { + throw new ClassNotFoundException(name) + } + val bytes = file.toByteArray + defineClass(name, bytes, 0, bytes.length) + } +} diff --git a/src/dotty/tools/dotc/repl/ConsoleWriter.scala b/src/dotty/tools/dotc/repl/ConsoleWriter.scala new file mode 100644 index 000000000..9387f366a --- /dev/null +++ b/src/dotty/tools/dotc/repl/ConsoleWriter.scala @@ -0,0 +1,21 @@ +package dotty.tools +package dotc +package repl +import java.io.Writer + +/** A Writer that writes onto the Scala Console. + * + * @author Lex Spoon + * @version 1.0 + */ +class ConsoleWriter extends Writer { + def close = flush + + def flush = Console.flush + + def write(cbuf: Array[Char], off: Int, len: Int): Unit = + if (len > 0) + write(new String(cbuf, off, len)) + + override def write(str: String): Unit = Console.print(str) +} diff --git a/src/dotty/tools/dotc/repl/InteractiveReader.scala b/src/dotty/tools/dotc/repl/InteractiveReader.scala new file mode 100644 index 000000000..55f7de0b2 --- /dev/null +++ b/src/dotty/tools/dotc/repl/InteractiveReader.scala @@ -0,0 +1,28 @@ +package dotty.tools +package dotc +package repl + +/** Reads lines from an input stream */ +trait InteractiveReader { + def readLine(prompt: String): String + val interactive: Boolean +} + +object InteractiveReader { + /** Create an interactive reader. Uses JLine if the + * library is available, but otherwise uses a + * SimpleReader. */ + def createDefault(): InteractiveReader = new SimpleReader() + /* + { + try { + new JLineReader + } catch { + case e => + //out.println("jline is not available: " + e) //debug + new SimpleReader() + } + } +*/ + +} diff --git a/src/dotty/tools/dotc/repl/Interpreter.scala b/src/dotty/tools/dotc/repl/Interpreter.scala new file mode 100644 index 000000000..b9853e18b --- /dev/null +++ b/src/dotty/tools/dotc/repl/Interpreter.scala @@ -0,0 +1,920 @@ +package dotty.tools +package dotc +package repl + +import java.io.{File, PrintWriter, StringWriter, Writer} +import java.lang.{Class, ClassLoader} +import java.net.{URL, URLClassLoader} + +import scala.collection.immutable.ListSet +import scala.collection.mutable +import scala.collection.mutable.{ListBuffer, HashSet, ArrayBuffer} + +//import ast.parser.SyntaxAnalyzer +import io.{PlainFile, VirtualDirectory} +import reporting.{ConsoleReporter, Reporter} +import core.Flags +import util.{SourceFile, NameTransformer} +import io.ClassPath +import repl.{InterpreterResults=>IR} +import ast.Trees._ +import parsing.Parsers._ +import core._ +import dotty.tools.backend.jvm.GenBCode +import Symbols._, Types._, Contexts._, StdNames._, Names._, NameOps._ +import Decorators._ +import Interpreter._ + +/** <p> + * An interpreter for Scala code. + * </p> + * <p> + * The main public entry points are <code>compile()</code> and + * <code>interpret()</code>. The <code>compile()</code> method loads a + * complete Scala file. The <code>interpret()</code> method executes one + * line of Scala code at the request of the user. + * </p> + * <p> + * The overall approach is based on compiling the requested code and then + * using a Java classloader and Java reflection to run the code + * and access its results. + * </p> + * <p> + * In more detail, a single compiler instance is used + * to accumulate all successfully compiled or interpreted Scala code. To + * "interpret" a line of code, the compiler generates a fresh object that + * includes the line of code and which has public member(s) to export + * all variables defined by that code. To extract the result of an + * interpreted line to show the user, a second "result object" is created + * which imports the variables exported by the above object and then + * exports a single member named "result". To accomodate user expressions + * that read from variables or methods defined in previous statements, "import" + * statements are used. + * </p> + * <p> + * This interpreter shares the strengths and weaknesses of using the + * full compiler-to-Java. The main strength is that interpreted code + * behaves exactly as does compiled code, including running at full speed. + * The main weakness is that redefining classes and methods is not handled + * properly, because rebinding at the Java level is technically difficult. + * </p> + * + * @author Moez A. Abdel-Gawad + * @author Lex Spoon + */ +class Interpreter(out: PrintWriter) extends Compiler { + + import ast.untpd._ + import Interpreter._ + + /** directory to save .class files to */ + val virtualDirectory = new VirtualDirectory("(memory)", None) + + class REPLGenBCode extends GenBCode { + override def outputDir(implicit ctx: Context) = virtualDirectory + } + + override def phases = Phases.replace( + classOf[GenBCode], _ => new REPLGenBCode :: Nil, super.phases) + + /** whether to print out result lines */ + private var printResults: Boolean = true + + /** Be quiet. Do not print out the results of each + * submitted command unless an exception is thrown. */ + def beQuiet = { printResults = false } + + /** Temporarily be quiet */ + def beQuietDuring[T](operation: => T): T = { + val wasPrinting = printResults + try { + printResults = false + operation + } finally { + printResults = wasPrinting + } + } + + /** interpreter settings */ + val isettings = new InterpreterSettings + + def newReporter = new ConsoleReporter(Console.in, writer = out) { + //override def printMessage(msg: String) { out.println(clean(msg)) } + override def printMessage(msg: String) = { + out.print(clean(msg) + "\n") + out.flush() + } + } + + /** the previous requests this interpreter has processed */ + private val prevRequests = new ArrayBuffer[Request]() + + /** the compiler's classpath, as URL's */ + var compilerClasspath: List[URL] = _ + + /* A single class loader is used for all commands interpreted by this Interpreter. + It would also be possible to create a new class loader for each command + to interpret. The advantages of the current approach are: + + - Expressions are only evaluated one time. This is especially + significant for I/O, e.g. "val x = Console.readLine" + + The main disadvantage is: + + - Objects, classes, and methods cannot be rebound. Instead, definitions + shadow the old ones, and old code objects refer to the old + definitions. + */ + /** class loader used to load compiled code */ + var classLoader: ClassLoader = _ + + protected def parentClassLoader: ClassLoader = classOf[Interpreter].getClassLoader + + def init()(implicit ctx: Context) = { + compilerClasspath = ctx.platform.classPath.asURLs + classLoader = { + val parent = new URLClassLoader(compilerClasspath.toArray, parentClassLoader) + new AbstractFileClassLoader(virtualDirectory, parent) + } + } + + /** Set the current Java "context" class loader to this + * interpreter's class loader + */ + def setContextClassLoader(): Unit = { + Thread.currentThread.setContextClassLoader(classLoader) + } + + /** Parse a line into a sequence of trees. Returns None if the input + * is incomplete. + */ + private def parse(line: String)(implicit ctx: Context): Option[List[Tree]] = { + var justNeedsMore = false + val reporter = newReporter + reporter.withIncompleteHandler { _ => _ => justNeedsMore = true } { + // simple parse: just parse it, nothing else + def simpleParse(code: String): List[Tree] = { + val source = new SourceFile("<console>", code.toCharArray()) + val parser = new Parser(source) + val (selfDef, stats) = parser.templateStatSeq + stats + } + val trees = simpleParse(line) + if (reporter.hasErrors) { + Some(Nil) // the result did not parse, so stop + } else if (justNeedsMore) { + None + } else { + Some(trees) + } + } + } + + /** Compile a SourceFile. Returns true if there are + * no compilation errors, or false othrewise. + */ + def compileSources(sources: List[SourceFile])(implicit ctx: Context): Boolean = { + val reporter = newReporter + val run = new Run(this)(ctx.fresh.setReporter(reporter)) + run.compileSources(sources) + !reporter.hasErrors + } + + /** Compile a string. Returns true if there are no + * compilation errors, or false otherwise. + */ + def compileString(code: String)(implicit ctx: Context): Boolean = + compileSources(List(new SourceFile("<script>", code.toCharArray))) + + /** <p> + * Interpret one line of input. All feedback, including parse errors + * and evaluation results, are printed via the supplied compiler's + * reporter. Values defined are available for future interpreted + * strings. + * </p> + * <p> + * The return value is whether the line was interpreter successfully, + * e.g. that there were no parse errors. + * </p> + * + * @param line ... + * @return ... + */ + def interpret(line: String)(implicit ctx: Context): IR.Result = { + if (prevRequests.isEmpty) + new Run(this) // initialize the compiler + + // parse + val trees = parse(indentCode(line)) match { + case None => return IR.Incomplete + case Some(Nil) => return IR.Error // parse error or empty input + case Some(trees) => trees + } + + trees match { + case List(_: Assign) => () + + case List(_: TermTree) | List(_: Ident) | List(_: Select) => + // Treat a single expression specially. + // This is necessary due to it being hard to modify + // code at a textual level, and it being hard to + // submit an AST to the compiler. + return interpret("val " + newVarName() + " = \n" + line) + + case _ => () + } + + val lineName = newLineName + + // figure out what kind of request + val req = buildRequest(trees, line, lineName) + if (req eq null) return IR.Error // a disallowed statement type + + if (!req.compile) + return IR.Error // an error happened during compilation, e.g. a type error + + val (interpreterResultString, succeeded) = req.loadAndRun + + if (printResults || !succeeded) { + // print the result + out.print(clean(interpreterResultString)) + } + + // book-keeping + if (succeeded) + prevRequests += req + + if (succeeded) IR.Success else IR.Error + } + + /** A counter used for numbering objects created by <code>bind()</code>. */ + private var binderNum = 0 + + /** Bind a specified name to a specified value. The name may + * later be used by expressions passed to interpret. + * + * @param name the variable name to bind + * @param boundType the type of the variable, as a string + * @param value the object value to bind to it + * @return an indication of whether the binding succeeded + */ + def bind(name: String, boundType: String, value: Any)(implicit ctx: Context): IR.Result = { + val binderName = "binder" + binderNum + binderNum += 1 + + compileString( + "object " + binderName + + "{ var value: " + boundType + " = _; " + + " def set(x: Any) = value=x.asInstanceOf[" + boundType + "]; }") + + val binderObject = + Class.forName(binderName, true, classLoader) + val setterMethod = + (binderObject + .getDeclaredMethods + .toList + .find(meth => meth.getName == "set") + .get) + var argsHolder: Array[Any] = null // this roundabout approach is to try and + // make sure the value is boxed + argsHolder = List(value).toArray + setterMethod.invoke(null, argsHolder.asInstanceOf[Array[AnyRef]]) + + interpret("val " + name + " = " + binderName + ".value") + } + + /** Class to handle one member among all the members included + * in a single interpreter request. + */ + private trait Handler { + def member: Tree + def usedNames: List[Name] + val boundNames: List[Name] + def valAndVarNames: List[Name] + def defNames: List[Name] + val importsWildcard: Boolean + val importedNames: Seq[Name] + val definesImplicit: Boolean + def extraCodeToEvaluate(req: Request, code: PrintWriter): Unit + def resultExtractionCode(req: Request, code: PrintWriter): Unit + } + + /** Build a request from the user. <code>trees</code> is <code>line</code> + * after being parsed. + */ + private def buildRequest(trees: List[Tree], line: String, lineName: String)(implicit ctx: Context): Request = + new Request(line, lineName) + + /** One line of code submitted by the user for interpretation */ + private class Request(val line: String, val lineName: String)(implicit ctx: Context) { + val trees = parse(line) match { + case Some(ts) => ts + case None => Nil + } + + /** name to use for the object that will compute "line" */ + def objectName = lineName + INTERPRETER_WRAPPER_SUFFIX + + /** name of the object that retrieves the result from the above object */ + def resultObjectName = "RequestResult$" + objectName + + private val handlers: List[MemberHandler] = trees.flatMap(chooseHandler(_)) + + /** all (public) names defined by these statements */ + val boundNames = (ListSet() ++ handlers.flatMap(_.boundNames)).toList + + /** list of names used by this expression */ + val usedNames: List[Name] = handlers.flatMap(_.usedNames) + + def myImportsCode = importsCode(Set.empty ++ usedNames) + + /** Code to append to objectName to access anything that + * the request binds. */ + val accessPath = myImportsCode._3 + + + /** Code to access a variable with the specified name */ + def fullPath(vname: String): String = + objectName + accessPath + ".`" + vname + "`" + + /** Code to access a variable with the specified name */ + def fullPath(vname: Name): String = fullPath(vname.toString) + + /** the line of code to compute */ + def toCompute = line + + /** generate the source code for the object that computes this request */ + def objectSourceCode: String = + stringFrom { code => + // header for the wrapper object + code.println("object " + objectName + " {") + + val (importsPreamble, importsTrailer, _) = myImportsCode + + code.print(importsPreamble) + + code.println(indentCode(toCompute)) + + handlers.foreach(_.extraCodeToEvaluate(this,code)) + + code.println(importsTrailer) + + //end the wrapper object + code.println(";}") + } + + /** Types of variables defined by this request. They are computed + after compilation of the main object */ + var typeOf: Map[Name, String] = _ + + /** generate source code for the object that retrieves the result + from objectSourceCode */ + def resultObjectSourceCode: String = + stringFrom(code => { + code.println("object " + resultObjectName) + code.println("{ val result: String = {") + code.println(objectName + accessPath + ";") // evaluate the object, to make sure its constructor is run + code.print("(\"\"") // print an initial empty string, so later code can + // uniformly be: + morestuff + handlers.foreach(_.resultExtractionCode(this, code)) + code.println("\n)}") + code.println(";}") + }) + + + /** Compile the object file. Returns whether the compilation succeeded. + * If all goes well, the "types" map is computed. */ + def compile(): Boolean = + compileSources( + List(new SourceFile("<console>", objectSourceCode.toCharArray))) && { + // extract and remember types + this.typeOf = findTypes() + compileSources( + List(new SourceFile("<console>", resultObjectSourceCode.toCharArray))) + } + + /** Dig the types of all bound variables out of the compiler run. + * + * @param objRun ... + * @return ... + */ + def findTypes(): Map[Name, String] = { + def valAndVarNames = handlers.flatMap(_.valAndVarNames) + def defNames = handlers.flatMap(_.defNames) + + def getTypes(names: List[Name], nameMap: Name=>Name): Map[Name, String] = { + /** the outermost wrapper object */ + val outerResObjSym: Symbol = + defn.EmptyPackageClass.info.decl(objectName.toTermName).symbol + + /** the innermost object inside the wrapper, found by + * following accessPath into the outer one. */ + val resObjSym = + (accessPath.split("\\.")).foldLeft(outerResObjSym) { (sym,str) => + if (str.isEmpty) sym + else + ctx.atPhase(ctx.typerPhase.next) { implicit ctx => + sym.info.member(str.toTermName).symbol + } + } + + names.foldLeft(Map.empty[Name,String]) { (map, name) => + val rawType = + ctx.atPhase(ctx.typerPhase.next) { implicit ctx => + resObjSym.info.member(name).info + } + + // the types are all =>T; remove the => + val cleanedType = rawType.widenExpr + + map + (name -> + ctx.atPhase(ctx.typerPhase.next) { implicit ctx => + cleanedType.show + }) + } + } + + val names1 = getTypes(valAndVarNames, n => n.toTermName.fieldName) + val names2 = getTypes(defNames, identity) + names1 ++ names2 + } + + /** load and run the code using reflection */ + def loadAndRun: (String, Boolean) = { + val interpreterResultObject: Class[_] = + Class.forName(resultObjectName, true, classLoader) + val resultValMethod: java.lang.reflect.Method = + interpreterResultObject.getMethod("result", null) + try { + (resultValMethod.invoke(interpreterResultObject, null).toString(), + true) + } catch { + case e => + def caus(e: Throwable): Throwable = + if (e.getCause eq null) e else caus(e.getCause) + val orig = caus(e) + (stringFrom(str => orig.printStackTrace(str)), false) + } + } + + /** Compute imports that allow definitions from previous + * requests to be visible in a new request. Returns + * three pieces of related code: + * + * 1. An initial code fragment that should go before + * the code of the new request. + * + * 2. A code fragment that should go after the code + * of the new request. + * + * 3. An access path which can be traversed to access + * any bindings inside code wrapped by #1 and #2 . + * + * The argument is a set of Names that need to be imported. + * + * Limitations: This method is not as precise as it could be. + * (1) It does not process wildcard imports to see what exactly + * they import. + * (2) If it imports any names from a request, it imports all + * of them, which is not really necessary. + * (3) It imports multiple same-named implicits, but only the + * last one imported is actually usable. + */ + private def importsCode(wanted: Set[Name]): (String, String, String) = { + /** Narrow down the list of requests from which imports + * should be taken. Removes requests which cannot contribute + * useful imports for the specified set of wanted names. + */ + def reqsToUse: List[(Request, Handler)] = { + /** Loop through a list of MemberHandlers and select + * which ones to keep. 'wanted' is the set of + * names that need to be imported, and + * 'shadowed' is the list of names useless to import + * because a later request will re-import it anyway. + */ + def select(reqs: List[(Request, Handler)], wanted: Set[Name]): List[(Request, Handler)] = { + reqs match { + case Nil => Nil + + case (req, handler) :: rest => + val keepit = + (handler.definesImplicit || + handler.importsWildcard || + handler.importedNames.exists(wanted.contains(_)) || + handler.boundNames.exists(wanted.contains(_))) + + val newWanted = + if (keepit) { + (wanted + ++ handler.usedNames + -- handler.boundNames + -- handler.importedNames) + } else { + wanted + } + + val restToKeep = select(rest, newWanted) + + if (keepit) + (req, handler) :: restToKeep + else + restToKeep + } + } + + val rhpairs = for { + req <- prevRequests.toList.reverse + handler <- req.handlers + } yield (req, handler) + + select(rhpairs, wanted).reverse + } + + val code = new StringBuffer + val trailingBraces = new StringBuffer + val accessPath = new StringBuffer + val impname = INTERPRETER_IMPORT_WRAPPER + val currentImps = mutable.Set.empty[Name] + + // add code for a new object to hold some imports + def addWrapper(): Unit = { + code.append("object " + impname + "{\n") + trailingBraces.append("}\n") + accessPath.append("." + impname) + currentImps.clear + } + + addWrapper() + + // loop through previous requests, adding imports + // for each one + for ((req, handler) <- reqsToUse) { + // If the user entered an import, then just use it + + // add an import wrapping level if the import might + // conflict with some other import + if (handler.importsWildcard || + currentImps.exists(handler.importedNames.contains)) + if (!currentImps.isEmpty) + addWrapper() + + if (handler.member.isInstanceOf[Import]) + code.append(handler.member.toString + ";\n") + + // give wildcard imports a import wrapper all to their own + if (handler.importsWildcard) + addWrapper() + else + currentImps ++= handler.importedNames + + // For other requests, import each bound variable. + // import them explicitly instead of with _, so that + // ambiguity errors will not be generated. Also, quote + // the name of the variable, so that we don't need to + // handle quoting keywords separately. + for (imv <- handler.boundNames) { + if (currentImps.contains(imv)) + addWrapper() + code.append("import ") + code.append(req.objectName + req.accessPath + ".`" + imv + "`;\n") + currentImps += imv + } + } + + addWrapper() // Add one extra wrapper, to prevent warnings + // in the frequent case of redefining + // the value bound in the last interpreter + // request. + + (code.toString, trailingBraces.toString, accessPath.toString) + } + + // ------ Handlers ------------------------------------------ + + private def chooseHandler(member: Tree): Option[MemberHandler] = + member match { + case member: DefDef => + Some(new DefHandler(member)) + case member: ValDef => + Some(new ValHandler(member)) + case member @ Assign(Ident(_), _) => Some(new AssignHandler(member)) + case member: ModuleDef => Some(new ModuleHandler(member)) + case member: TypeDef => + Some(if (member.isClassDef) new ClassHandler(member) + else new TypeAliasHandler(member)) + case member: Import => Some(new ImportHandler(member)) + // case DocDef(_, documented) => chooseHandler(documented) + case member => Some(new GenericHandler(member)) + } + + /** A traverser that finds all mentioned identifiers, i.e. things + * that need to be imported. + * It might return extra names. + */ + private class ImportVarsTraverser(definedVars: List[Name]) extends TreeTraverser { + val importVars = new HashSet[Name]() + + override def traverse(ast: Tree)(implicit ctx: Context): Unit = { + ast match { + case Ident(name) => importVars += name + case _ => traverseChildren(ast) + } + } + } + + /** Class to handle one member among all the members included + * in a single interpreter request. + */ + private sealed abstract class MemberHandler(val member: Tree) extends Handler { + val usedNames: List[Name] = { + val ivt = new ImportVarsTraverser(boundNames) + ivt.traverse(member) + ivt.importVars.toList + } + val boundNames: List[Name] = Nil + def valAndVarNames: List[Name] = Nil + def defNames: List[Name] = Nil + val importsWildcard = false + val importedNames: Seq[Name] = Nil + val definesImplicit = + member match { + case tree: MemberDef => tree.mods.is(Flags.Implicit) + case _ => false + } + + def extraCodeToEvaluate(req: Request, code: PrintWriter) = {} + def resultExtractionCode(req: Request, code: PrintWriter) = {} + } + + private class GenericHandler(member: Tree) extends MemberHandler(member) + + private class ValHandler(member: ValDef) extends MemberHandler(member) { + override val boundNames = List(member.name) + override def valAndVarNames = boundNames + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + val vname = member.name + if (!member.mods.is(Flags.AccessFlags) && + !(isGeneratedVarName(vname.toString) && + req.typeOf(vname.encode) == "Unit")) { + val prettyName = vname.decode + code.print(" + \"" + prettyName + ": " + + string2code(req.typeOf(vname)) + + " = \" + " + + " (if(" + + req.fullPath(vname) + + ".asInstanceOf[AnyRef] != null) " + + " ((if(" + + req.fullPath(vname) + + ".toString().contains('\\n')) " + + " \"\\n\" else \"\") + " + + req.fullPath(vname) + ".toString() + \"\\n\") else \"null\\n\") ") + } + } + } + + private class DefHandler(defDef: DefDef) extends MemberHandler(defDef) { + override val boundNames = List(defDef.name) + override def defNames = boundNames + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + if (!defDef.mods.is(Flags.AccessFlags)) + code.print("+\"" + string2code(defDef.name.toString) + ": " + + string2code(req.typeOf(defDef.name)) + "\\n\"") + } + } + + private class AssignHandler(member: Assign) extends MemberHandler(member) { + val lhs = member.lhs.asInstanceOf[Ident] // an unfortunate limitation + + val helperName = newInternalVarName().toTermName + override val valAndVarNames = List(helperName) + + override def extraCodeToEvaluate(req: Request, code: PrintWriter): Unit = { + code.println("val " + helperName + " = " + member.lhs + ";") + } + + /** Print out lhs instead of the generated varName */ + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.print(" + \"" + lhs + ": " + + string2code(req.typeOf(helperName.encode)) + + " = \" + " + + string2code(req.fullPath(helperName)) + + " + \"\\n\"") + } + } + + private class ModuleHandler(module: ModuleDef) extends MemberHandler(module) { + override val boundNames = List(module.name) + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.println(" + \"defined module " + + string2code(module.name.toString) + + "\\n\"") + } + } + + private class ClassHandler(classdef: TypeDef) + extends MemberHandler(classdef) { + override val boundNames = + List(classdef.name) ::: + (if (classdef.mods.is(Flags.Case)) + List(classdef.name.toTermName) + else + Nil) + + // TODO: MemberDef.keyword does not include "trait"; + // otherwise it could be used here + def keyword: String = + if (classdef.mods.is(Flags.Trait)) "trait" else "class" + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.print( + " + \"defined " + + keyword + + " " + + string2code(classdef.name.toString) + + "\\n\"") + } + } + + private class TypeAliasHandler(typeDef: TypeDef) + extends MemberHandler(typeDef) { + override val boundNames = + if (!typeDef.mods.is(Flags.AccessFlags) && !typeDef.rhs.isInstanceOf[TypeBoundsTree]) + List(typeDef.name) + else + Nil + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.println(" + \"defined type alias " + + string2code(typeDef.name.toString) + "\\n\"") + } + } + + private class ImportHandler(imp: Import) extends MemberHandler(imp) { + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.println("+ \"" + imp.toString + "\\n\"") + } + + def isWildcardSelector(tree: Tree) = tree match { + case Ident(nme.USCOREkw) => true + case _ => false + } + + /** Whether this import includes a wildcard import */ + override val importsWildcard = imp.selectors.exists(isWildcardSelector) + + /** The individual names imported by this statement */ + override val importedNames: Seq[Name] = + imp.selectors.filterNot(isWildcardSelector).flatMap { + case sel: RefTree => List(sel.name.toTypeName, sel.name.toTermName) + case _ => Nil + } + } + + } // end Request + + // ------- String handling ---------------------------------- + + /** next line number to use */ + private var nextLineNo = 0 + + /** allocate a fresh line name */ + private def newLineName = { + val num = nextLineNo + nextLineNo += 1 + INTERPRETER_LINE_PREFIX + num + } + + /** next result variable number to use */ + private var nextVarNameNo = 0 + + /** allocate a fresh variable name */ + private def newVarName() = { + val num = nextVarNameNo + nextVarNameNo += 1 + INTERPRETER_VAR_PREFIX + num + } + + /** next internal variable number to use */ + private var nextInternalVarNo = 0 + + /** allocate a fresh internal variable name */ + private def newInternalVarName() = { + val num = nextVarNameNo + nextVarNameNo += 1 + INTERPRETER_SYNTHVAR_PREFIX + num + } + + /** Check if a name looks like it was generated by newVarName */ + private def isGeneratedVarName(name: String): Boolean = + name.startsWith(INTERPRETER_VAR_PREFIX) && { + val suffix = name.drop(INTERPRETER_VAR_PREFIX.length) + suffix.forall(_.isDigit) + } + + /** generate a string using a routine that wants to write on a stream */ + private def stringFrom(writer: PrintWriter => Unit): String = { + val stringWriter = new StringWriter() + val stream = new NewLinePrintWriter(stringWriter) + writer(stream) + stream.close + stringWriter.toString + } + + /** Truncate a string if it is longer than settings.maxPrintString */ + private def truncPrintString(str: String): String = { + val maxpr = isettings.maxPrintString + + if (maxpr <= 0) + return str + + if (str.length <= maxpr) + return str + + val trailer = "..." + if (maxpr >= trailer.length+1) + return str.substring(0, maxpr-3) + trailer + + str.substring(0, maxpr) + } + + /** Clean up a string for output */ + private def clean(str: String) = + truncPrintString(Interpreter.stripWrapperGunk(str)) + + /** Indent some code by the width of the scala> prompt. + * This way, compiler error messages read better. + */ + def indentCode(code: String) = { + val spaces = " " + + stringFrom(str => + for (line <- code.lines) { + str.print(spaces) + str.print(line + "\n") + str.flush() + }) + } +} + +/** Utility methods for the Interpreter. */ +object Interpreter { + val INTERPRETER_WRAPPER_SUFFIX = "$object" + val INTERPRETER_LINE_PREFIX = "line" + val INTERPRETER_VAR_PREFIX = "res" + val INTERPRETER_IMPORT_WRAPPER = "$iw" + val INTERPRETER_SYNTHVAR_PREFIX = "synthvar$" + + /** Delete a directory tree recursively. Use with care! + */ + private[repl] def deleteRecursively(path: File): Unit = { + path match { + case _ if !path.exists => + () + case _ if path.isDirectory => + for (p <- path.listFiles) + deleteRecursively(p) + path.delete + case _ => + path.delete + } + } + + /** Heuristically strip interpreter wrapper prefixes + * from an interpreter output string. + */ + def stripWrapperGunk(str: String): String = { + val wrapregex = "(line[0-9]+\\$object[$.])?(\\$iw[$.])*" + str.replaceAll(wrapregex, "") + } + + /** Convert a string into code that can recreate the string. + * This requires replacing all special characters by escape + * codes. It does not add the surrounding " marks. */ + def string2code(str: String): String = { + /** Convert a character to a backslash-u escape */ + def char2uescape(c: Char): String = { + var rest = c.toInt + val buf = new StringBuilder + for (i <- 1 to 4) { + buf ++= (rest % 16).toHexString + rest = rest / 16 + } + "\\" + "u" + buf.toString.reverse + } + val res = new StringBuilder + for (c <- str) { + if ("'\"\\" contains c) { + res += '\\' + res += c + } else if (!c.isControl) { + res += c + } else { + res ++= char2uescape(c) + } + } + res.toString + } +} diff --git a/src/dotty/tools/dotc/repl/InterpreterLoop.scala b/src/dotty/tools/dotc/repl/InterpreterLoop.scala new file mode 100644 index 000000000..b97ef3599 --- /dev/null +++ b/src/dotty/tools/dotc/repl/InterpreterLoop.scala @@ -0,0 +1,248 @@ +package dotty.tools +package dotc +package repl + +import java.io.{BufferedReader, File, FileReader, PrintWriter} +import java.io.IOException +import java.lang.{ClassLoader, System} +import scala.concurrent.{Future, Await} +import scala.concurrent.duration.Duration +import reporting.Reporter +import core._ +import Contexts._ +import annotation.tailrec +import scala.concurrent.ExecutionContext.Implicits.global + +import repl.{InterpreterResults => IR} + +/** 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( + compiler: Compiler, + private var in: InteractiveReader, + out: PrintWriter)(implicit ctx: Context) { + + val interpreter = compiler.asInstanceOf[Interpreter] + interpreter.init() + + /** The context class loader at the time this object was created */ + protected val originalClassLoader = + Thread.currentThread.getContextClassLoader + + /** 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 requset a :replay */ + def addReplay(cmd: String) = + replayCommandsRev = cmd :: replayCommandsRev + + /** Close the interpreter */ + def closeInterpreter()(implicit ctx: Context): Unit = { + ctx.reporter.flush() + Thread.currentThread.setContextClassLoader(originalClassLoader) + } + + /** Bind the settings so that evaluated code can modify them */ + def bindSettings(): Unit = { + interpreter.beQuietDuring { + interpreter.compileString(InterpreterSettings.sourceCodeForClass) + + interpreter.bind( + "settings", + "scala.tools.nsc.InterpreterSettings", + interpreter.isettings) + } + } + + + /** print a friendly help message */ + def printHelp(): Unit = { + printWelcome() + out.println("Type :load followed by a filename to load a Scala file.") + out.println("Type :replay to reset execution and replay all previous commands.") + out.println("Type :quit to exit the interpreter.") + } + + /** Print a welcome message */ + def printWelcome(): Unit = { + out.println("Welcome to Scala.next " + " (" + + System.getProperty("java.vm.name") + ", Java " + System.getProperty("java.version") + ")." ) + out.println("Type in expressions to have them evaluated.") + out.println("Type :help for more information.") + out.flush() + } + + /** Prompt to print when awaiting input */ + val prompt = "scala> " + + /** The first interpreted command always takes a couple of seconds + * due to classloading. To bridge the gap, wait until the welcome message + * has been printed before calling bindSettings. That way, + * the user can read the welcome message while this + * command executes. + */ + def firstLine(): String = { + val futLine = Future(in.readLine(prompt)) + bindSettings() + Await.result(futLine, Duration.Inf) + } + + /** 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>. + */ + @tailrec final def repl(line: String): Unit = + if (line != null) { + val (keepGoing, finalLineOpt) = command(line) + if (keepGoing) { + finalLineOpt.foreach(addReplay) + out.flush() + repl(in.readLine(prompt)) + } + } + + /** interpret all lines from a specified file */ + def interpretAllFrom(filename: String): Unit = { + val fileIn = try { + new FileReader(filename) + } catch { + case _:IOException => + out.println("Error opening file: " + filename) + return + } + 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(in.readLine(prompt)) + } finally { + in = oldIn + replayCommandsRev = oldReplay + fileIn.close + } + } + + /** create a new interpreter and replay all commands so far */ + def replay(): Unit = { + for (cmd <- replayCommands) { + out.println("Replaying: " + cmd) + out.flush() // because maybe cmd will have its own output + command(cmd) + out.println + } + } + + /** Run one command submitted by the user. Three values are returned: + * (1) whether to keep running, (2) the line to record for replay, + * if any. */ + def command(line: String): (Boolean, Option[String]) = { + def withFile(command: String)(action: String => Unit): Unit = { + val spaceIdx = command.indexOf(' ') + if (spaceIdx <= 0) { + out.println("That command requires a filename to be specified.") + return () + } + val filename = command.substring(spaceIdx).trim + if (! new File(filename).exists) { + out.println("That file does not exist") + return () + } + action(filename) + } + + val helpRegexp = ":h(e(l(p)?)?)?" + val quitRegexp = ":q(u(i(t)?)?)?" + val loadRegexp = ":l(o(a(d)?)?)?.*" + val replayRegexp = ":r(e(p(l(a(y)?)?)?)?)?.*" + + var shouldReplay: Option[String] = None + + if (line.matches(helpRegexp)) + printHelp() + else if (line.matches(quitRegexp)) + return (false, None) + else if (line.matches(loadRegexp)) { + withFile(line)(f => { + interpretAllFrom(f) + shouldReplay = Some(line) + }) + } + else if (line matches replayRegexp) + replay + else if (line startsWith ":") + out.println("Unknown command. Type :help for help.") + else + shouldReplay = interpretStartingWith(line) + + (true, shouldReplay) + } + + /** 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.Success => Some(code) + case IR.Error => None + case IR.Incomplete => + if (in.interactive && code.endsWith("\n\n")) { + out.println("You typed two blank lines. Starting a new command.") + None + } else { + val nextLine = in.readLine(" | ") + if (nextLine == null) + None // end of file + else + interpretStartingWith(code + "\n" + nextLine) + } + } + } +/* + 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 run(): Reporter = { + // loadFiles(settings) + try { + if (!ctx.reporter.hasErrors) { + printWelcome() + repl(firstLine()) + } + } finally { + closeInterpreter() + } + ctx.reporter + } +} diff --git a/src/dotty/tools/dotc/repl/InterpreterResults.scala b/src/dotty/tools/dotc/repl/InterpreterResults.scala new file mode 100644 index 000000000..e5f8affa8 --- /dev/null +++ b/src/dotty/tools/dotc/repl/InterpreterResults.scala @@ -0,0 +1,19 @@ +package dotty.tools.dotc.repl + +object InterpreterResults { + + /** A result from interpreting one line of input. */ + abstract sealed class Result + + /** The line was interpreted successfully. */ + case object Success extends Result + + /** The line was erroneous in some way. */ + case object Error extends Result + + /** The input was incomplete. The caller should request more + * input. + */ + case object Incomplete extends Result + +} diff --git a/src/dotty/tools/dotc/repl/InterpreterSettings.scala b/src/dotty/tools/dotc/repl/InterpreterSettings.scala new file mode 100644 index 000000000..ee2f9335e --- /dev/null +++ b/src/dotty/tools/dotc/repl/InterpreterSettings.scala @@ -0,0 +1,69 @@ +package dotty.tools +package dotc +package repl + +/** Settings for the interpreter + * + * @version 1.0 + * @author Lex Spoon, 2007/3/24 + **/ +class InterpreterSettings { + /** A list of paths where :load should look */ + var loadPath = List(".") + + /** The maximum length of toString to use when printing the result + * of an evaluation. 0 means no maximum. If a printout requires + * more than this number of characters, then the printout is + * truncated. + */ + var maxPrintString = 390 + + override def toString = + "InterpreterSettings {\n" + +// " loadPath = " + loadPath + "\n" + + " maxPrintString = " + maxPrintString + "\n" + + "}" +} + + + +/* Utilities for the InterpreterSettings class + * + * @version 1.0 + * @author Lex Spoon, 2007/5/24 + */ +object InterpreterSettings { + /** Source code for the InterpreterSettings class. This is + * used so that the interpreter is sure to have the code + * available. + */ + val sourceCodeForClass = +""" +package scala.tools.nsc + +/** Settings for the interpreter + * + * @version 1.0 + * @author Lex Spoon, 2007/3/24 + **/ +class InterpreterSettings { + /** A list of paths where :load should look */ + var loadPath = List(".") + + /** The maximum length of toString to use when printing the result + * of an evaluation. 0 means no maximum. If a printout requires + * more than this number of characters, then the printout is + * truncated. + */ + var maxPrintString = 390 + + override def toString = + "InterpreterSettings {\n" + +// " loadPath = " + loadPath + "\n" + + " maxPrintString = " + maxPrintString + "\n" + + "}" +} + +""" + +} diff --git a/src/dotty/tools/dotc/repl/NewLinePrintWriter.scala b/src/dotty/tools/dotc/repl/NewLinePrintWriter.scala new file mode 100644 index 000000000..8e36a0ae4 --- /dev/null +++ b/src/dotty/tools/dotc/repl/NewLinePrintWriter.scala @@ -0,0 +1,11 @@ +package dotty.tools +package dotc +package repl +import java.io.{Writer, PrintWriter} + +class NewLinePrintWriter(out: Writer, autoFlush: Boolean) +extends PrintWriter(out, autoFlush) { + def this(out: Writer) = this(out, false) + override def println(): Unit = { print("\n"); flush() } +} + diff --git a/src/dotty/tools/dotc/repl/SimpleReader.scala b/src/dotty/tools/dotc/repl/SimpleReader.scala new file mode 100644 index 000000000..9fd563382 --- /dev/null +++ b/src/dotty/tools/dotc/repl/SimpleReader.scala @@ -0,0 +1,23 @@ +package dotty.tools +package dotc +package repl + +import java.io.{BufferedReader, PrintWriter} + + +/** Reads using standard JDK API */ +class SimpleReader( + in: BufferedReader, + out: PrintWriter, + val interactive: Boolean) +extends InteractiveReader { + def this() = this(Console.in, new PrintWriter(Console.out), true) + + def readLine(prompt: String) = { + if (interactive) { + out.print(prompt) + out.flush() + } + in.readLine() + } +} |