diff options
author | Martin Odersky <odersky@gmail.com> | 2016-02-14 15:56:22 +0100 |
---|---|---|
committer | Martin Odersky <odersky@gmail.com> | 2016-02-17 18:39:35 +0100 |
commit | 4550b524771af0719846eb906795a5ba83106eb9 (patch) | |
tree | 39d008ec7e26d341beff0041d219c6f99fa35cda /src/dotty/tools | |
parent | 37b6df435f1595f8610ec6d5fbe16d079da2eff7 (diff) | |
download | dotty-4550b524771af0719846eb906795a5ba83106eb9.tar.gz dotty-4550b524771af0719846eb906795a5ba83106eb9.tar.bz2 dotty-4550b524771af0719846eb906795a5ba83106eb9.zip |
Revisions to REPL
Changes necessary to make basic REPL functionality work.
Major refactoing: Code of Interpreter is now in CompilingInterpreter.scala.
Interpreter.scala contains just the API.
Diffstat (limited to 'src/dotty/tools')
-rw-r--r-- | src/dotty/tools/dotc/repl/CompilingInterpreter.scala | 831 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/InteractiveReader.scala | 3 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/Interpreter.scala | 918 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/InterpreterLoop.scala | 54 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/InterpreterResults.scala | 19 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/Main.scala | 27 | ||||
-rw-r--r-- | src/dotty/tools/dotc/repl/REPL.scala (renamed from src/dotty/tools/dotc/REPL.scala) | 16 |
7 files changed, 923 insertions, 945 deletions
diff --git a/src/dotty/tools/dotc/repl/CompilingInterpreter.scala b/src/dotty/tools/dotc/repl/CompilingInterpreter.scala new file mode 100644 index 000000000..f60b3c000 --- /dev/null +++ b/src/dotty/tools/dotc/repl/CompilingInterpreter.scala @@ -0,0 +1,831 @@ +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 scala.reflect.io.{PlainDirectory, Directory} +import reporting.{ConsoleReporter, Reporter} +import core.Flags +import util.{SourceFile, NameTransformer} +import io.ClassPath +import ast.Trees._ +import parsing.Parsers._ +import core._ +import dotty.tools.backend.jvm.GenBCode +import Symbols._, Types._, Contexts._, StdNames._, Names._, NameOps._ +import Decorators._ +import scala.util.control.NonFatal + +/** An interpreter for Scala code which is based on the `dotc` compiler. + * + * 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. + * + * 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 definition(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 definition named "result". To accommodate user expressions + * that read from variables or methods defined in previous statements, "import" + * statements are used. + * + * 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. + * + * @author Moez A. Abdel-Gawad + * @author Lex Spoon + * @author Martin Odersky + * + * @param out The output to use for diagnostics + * @param ictx The context to use for initialization of the interpreter, + * needed to access the current classpath. + */ +class CompilingInterpreter(out: PrintWriter, ictx: Context) extends Compiler with Interpreter { + import ast.untpd._ + import CompilingInterpreter._ + + /** directory to save .class files to */ + val virtualDirectory = + if (ictx.settings.d.isDefault(ictx)) new VirtualDirectory("(memory)", None) + else new PlainDirectory(new Directory(new java.io.File(ictx.settings.d.value(ictx)))) // for now, to help debugging + + /** A GenBCode phase that uses `virtualDirectory` for its output */ + private class REPLGenBCode extends GenBCode { + override def outputDir(implicit ctx: Context) = virtualDirectory + } + + /** Phases of this compiler use `REPLGenBCode` instead of `GenBCode`. */ + override def phases = Phases.replace( + classOf[GenBCode], _ => new REPLGenBCode :: Nil, super.phases) + + /** whether to print out result lines */ + private var printResults: Boolean = true + + /** Temporarily be quiet */ + override def beQuietDuring[T](operation: => T): T = { + val wasPrinting = printResults + try { + printResults = false + operation + } finally { + printResults = wasPrinting + } + } + + /** interpreter settings */ + override val isettings = new InterpreterSettings + + private def newReporter = new ConsoleReporter(Console.in, out) { + 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 */ + val compilerClasspath: List[URL] = ictx.platform.classPath(ictx).asURLs + + protected def parentClassLoader: ClassLoader = classOf[Interpreter].getClassLoader + + /* 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 */ + val classLoader: 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 + 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)(implicit ctx: Context): List[Tree] = { + val source = new SourceFile("<console>", code.toCharArray()) + val parser = new Parser(source) + val (selfDef, stats) = parser.templateStatSeq + stats + } + val trees = simpleParse(line)(ctx.fresh.setReporter(reporter)) + if (reporter.hasErrors) { + Some(Nil) // the result did not parse, so stop + } else if (justNeedsMore) { + None + } else { + Some(trees) + } + } + } + + /** Compile a SourceFile. Returns the root context of the run that compiled the file. + */ + def compileSources(sources: List[SourceFile])(implicit ctx: Context): Context = { + val reporter = newReporter + val run = newRun(ctx.fresh.setReporter(reporter)) + run.compileSources(sources) + run.runContext + } + + /** Compile a string. Returns true if there are no + * compilation errors, or false otherwise. + */ + def compileString(code: String)(implicit ctx: Context): Boolean = { + val runCtx = compileSources(List(new SourceFile("<script>", code.toCharArray))) + !runCtx.reporter.hasErrors + } + + override def interpret(line: String)(implicit ctx: Context): Interpreter.Result = { + // if (prevRequests.isEmpty) + // new Run(this) // initialize the compiler // (not sure this is needed) + // parse + parse(indentCode(line)) match { + case None => Interpreter.Incomplete + case Some(Nil) => Interpreter.Error // parse error or empty input + case Some(tree :: Nil) if tree.isTerm && !tree.isInstanceOf[Assign] => + interpret(s"val $newVarName =\n$line") + case Some(trees) => + val req = new Request(line, newLineName) + if (!req.compile()) + Interpreter.Error // an error happened during compilation, e.g. a type error + else { + val (interpreterResultString, succeeded) = req.loadAndRun() + if (printResults || !succeeded) + out.print(clean(interpreterResultString)) + if (succeeded) { + prevRequests += req + Interpreter.Success + } + else Interpreter.Error + } + } + } + + /** A counter used for numbering objects created by <code>bind()</code>. */ + private var binderNum = 0 + + override def bind(name: String, boundType: String, value: Any)(implicit ctx: Context): Interpreter.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.find(_.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") + } + + /** Trait collecting info about one of the statements of an interpreter request */ + private trait StatementInfo { + /** The statement */ + def statement: Tree + + /** The names defined previously and referred to in the statement */ + def usedNames: List[Name] + + /** The names defined in the statement */ + val boundNames: List[Name] + + /** Statement is an import that contains a wildcard */ + val importsWildcard: Boolean + + /** The names imported by the statement (if it is an import clause) */ + val importedNames: Seq[Name] + + /** Statement defines an implicit calue or method */ + val definesImplicit: Boolean + } + + /** One line of code submitted by the user for interpretation */ + private class Request(val line: String, val lineName: String)(implicit ctx: Context) { + private val trees = parse(line) match { + case Some(ts) => ts + case None => Nil + } + + /** name to use for the object that will compute "line" */ + private def objectName = lineName + INTERPRETER_WRAPPER_SUFFIX + + /** name of the object that retrieves the result from the above object */ + private def resultObjectName = "RequestResult$" + objectName + + private def chooseHandler(stat: Tree): StatementHandler = stat match { + case stat: DefDef => new DefHandler(stat) + case stat: ValDef => new ValHandler(stat) + case stat @ Assign(Ident(_), _) => new AssignHandler(stat) + case stat: ModuleDef => new ModuleHandler(stat) + case stat: TypeDef if stat.isClassDef => new ClassHandler(stat) + case stat: TypeDef => new TypeAliasHandler(stat) + case stat: Import => new ImportHandler(stat) +// case DocDef(_, documented) => chooseHandler(documented) + case stat => new GenericHandler(stat) + } + + private val handlers: List[StatementHandler] = trees.map(chooseHandler) + + /** all (public) names defined by these statements */ + private val boundNames = ListSet(handlers.flatMap(_.boundNames): _*).toList + + /** list of names used by this expression */ + private val usedNames: List[Name] = handlers.flatMap(_.usedNames) + + private val (importsPreamble, importsTrailer, accessPath) = + importsCode(usedNames.toSet) + + /** Code to access a variable with the specified name */ + private def fullPath(vname: String): String = s"$objectName$accessPath.`$vname`" + + /** Code to access a variable with the specified name */ + private def fullPath(vname: Name): String = fullPath(vname.toString) + + /** the line of code to compute */ + private def toCompute = line + + /** generate the source code for the object that computes this request + * TODO Reformulate in a functional way + */ + private def objectSourceCode: String = + stringFrom { code => + // header for the wrapper object + code.println("object " + objectName + " {") + 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 */ + private var typeOf: Map[Name, String] = _ + + /** generate source code for the object that retrieves the result + from objectSourceCode */ + private 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 = { + val compileCtx = compileSources( + List(new SourceFile("<console>", objectSourceCode.toCharArray))) + !compileCtx.reporter.hasErrors && { + this.typeOf = findTypes(compileCtx) + val resultCtx = compileSources( + List(new SourceFile("<console>", resultObjectSourceCode.toCharArray))) + !resultCtx.reporter.hasErrors + } + } + + /** Dig the types of all bound variables out of the compiler run. + * TODO: Change the interface so that we typecheck, and then transform + * directly. Treating the compiler as less of a blackbox will require + * much less magic here. + */ + private def findTypes(implicit ctx: Context): 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. + * @return A pair consisting of the run's result as a string, and + * a boolean indicating whether the run succeeded without throwing + * an exception. + */ + def loadAndRun(): (String, Boolean) = { + val interpreterResultObject: Class[_] = + Class.forName(resultObjectName, true, classLoader) + val resultValMethod: java.lang.reflect.Method = + interpreterResultObject.getMethod("result") + try { + (resultValMethod.invoke(interpreterResultObject).toString, true) + } catch { + case NonFatal(ex) => + def cause(ex: Throwable): Throwable = + if (ex.getCause eq null) ex else cause(ex.getCause) + val orig = cause(ex) + (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 as strings: + * + * 1. A _preamble_: An initial code fragment that should go before + * the code of the new request. + * + * 2. A _trailer_: 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, StatementInfo)] = { + /** Loop through a list of StatementHandlers and select + * which ones to keep. 'wanted' is the set of + * names that need to be imported. + */ + def select(reqs: List[(Request, StatementInfo)], wanted: Set[Name]): List[(Request, StatementInfo)] = { + 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 preamble = new StringBuffer + val trailingBraces = new StringBuffer + val accessPath = new StringBuffer + val impname = INTERPRETER_IMPORT_WRAPPER + val currentImps = mutable.Set[Name]() + + // add code for a new object to hold some imports + def addWrapper(): Unit = { + preamble.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.statement.isInstanceOf[Import]) + preamble.append(handler.statement.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() + preamble.append("import ") + preamble.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. + + (preamble.toString, trailingBraces.toString, accessPath.toString) + } + + // ------ Handlers ------------------------------------------ + + /** Class to handle one statement among all the statements included + * in a single interpreter request. + */ + private sealed abstract class StatementHandler(val statement: Tree) extends StatementInfo { + val usedNames: List[Name] = { + val ivt = new UntypedTreeAccumulator[mutable.Set[Name]] { + override def apply(ns: mutable.Set[Name], tree: Tree)(implicit ctx: Context) = + tree match { + case Ident(name) => ns += name + case _ => foldOver(ns, tree) + } + } + ivt.foldOver(HashSet(), statement).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 = statement 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(statement: Tree) extends StatementHandler(statement) + + private class ValHandler(statement: ValDef) extends StatementHandler(statement) { + override val boundNames = List(statement.name) + override def valAndVarNames = boundNames + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + val vname = statement.name + if (!statement.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 StatementHandler(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(statement: Assign) extends StatementHandler(statement) { + val lhs = statement.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 + " = " + statement.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 StatementHandler(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 StatementHandler(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 StatementHandler(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 StatementHandler(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) = str + // truncPrintString(stripWrapperGunk(str)) // TODO: enable + + /** 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 CompilingInterpreter { + 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/InteractiveReader.scala b/src/dotty/tools/dotc/repl/InteractiveReader.scala index 55f7de0b2..96c55ebd0 100644 --- a/src/dotty/tools/dotc/repl/InteractiveReader.scala +++ b/src/dotty/tools/dotc/repl/InteractiveReader.scala @@ -8,6 +8,9 @@ trait InteractiveReader { val interactive: Boolean } +/** TODO Enable jline support. + * The current Scala REPL know how to do this flexibly. + */ object InteractiveReader { /** Create an interactive reader. Uses JLine if the * library is available, but otherwise uses a diff --git a/src/dotty/tools/dotc/repl/Interpreter.scala b/src/dotty/tools/dotc/repl/Interpreter.scala index bac912d33..9967aa347 100644 --- a/src/dotty/tools/dotc/repl/Interpreter.scala +++ b/src/dotty/tools/dotc/repl/Interpreter.scala @@ -2,914 +2,54 @@ 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 core.Contexts.Context -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, ictx: Context) 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 */ - val compilerClasspath: List[URL] = ictx.platform.classPath(ictx).asURLs - - /* 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: +/** This object defines the type of interpreter results */ +object Interpreter { - - 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 */ - val classLoader: ClassLoader = { - val parent = new URLClassLoader(compilerClasspath.toArray, parentClassLoader) - new AbstractFileClassLoader(virtualDirectory, parent) - } + /** A result from interpreting one line of input. */ + abstract sealed class Result - protected def parentClassLoader: ClassLoader = classOf[Interpreter].getClassLoader + /** The line was interpreted successfully. */ + case object Success extends Result - /** Set the current Java "context" class loader to this - * interpreter's class loader - */ - def setContextClassLoader(): Unit = { - Thread.currentThread.setContextClassLoader(classLoader) - } + /** The line was erroneous in some way. */ + case object Error extends Result - /** Parse a line into a sequence of trees. Returns None if the input - * is incomplete. + /** The input was incomplete. The caller should request more input. */ - 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) - } - } - } + case object Incomplete extends Result +} - /** 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 - } +/** The exported functionality of the interpreter */ +trait Interpreter { + import Interpreter._ - /** Compile a string. Returns true if there are no - * compilation errors, or false otherwise. + /** Interpret one line of input. All feedback, including parse errors + * and evaluation results, are printed via the context's reporter. + * reporter. Values defined are available for future interpreted strings. */ - def compileString(code: String)(implicit ctx: Context): Boolean = - compileSources(List(new SourceFile("<script>", code.toCharArray))) + def interpret(line: String)(implicit ctx: Context): Result - /** <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 ... + /** Compile a string without exectuting the result. + * Returns true if there are no compilation errors, false otherwise. */ - 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 _ => () - } + def compileString(code: String)(implicit ctx: Context): Boolean - val lineName = newLineName + /** Suppress output during evaluation of `operation`. */ + def beQuietDuring[T](operation: => T): T - // 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 + /** The interpreter settings */ + def isettings: InterpreterSettings /** Bind a specified name to a specified value. The name may - * later be used by expressions passed to interpret. + * later be used by expressions passed to interpret. Can be used to + * programmatically change intepreter settings. * * @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 - } + def bind(name: String, boundType: String, value: Any)(implicit ctx: Context): Result } diff --git a/src/dotty/tools/dotc/repl/InterpreterLoop.scala b/src/dotty/tools/dotc/repl/InterpreterLoop.scala index 1a7a6d115..0167c47c5 100644 --- a/src/dotty/tools/dotc/repl/InterpreterLoop.scala +++ b/src/dotty/tools/dotc/repl/InterpreterLoop.scala @@ -13,21 +13,13 @@ 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 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. + * After instantiation, clients should call the `run` method. * * @author Moez A. Abdel-Gawad * @author Lex Spoon - * @version 1.2 + * @author Martin Odersky */ class InterpreterLoop( compiler: Compiler, @@ -47,7 +39,7 @@ class InterpreterLoop( /** 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 */ + /** Record a command for replay should the user request a :replay */ def addReplay(cmd: String) = replayCommandsRev = cmd :: replayCommandsRev @@ -80,7 +72,7 @@ class InterpreterLoop( /** Print a welcome message */ def printWelcome(): Unit = { - out.println("Welcome to Scala.next " + " (" + + out.println(s"Welcome to Scala$version " + " (" + 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.") @@ -88,7 +80,10 @@ class InterpreterLoop( } /** Prompt to print when awaiting input */ - val prompt = "scala> " + val prompt = "scala> " + val continuationPrompt = " | " + + val version = ".next (pre-alpha)" /** The first interpreted command always takes a couple of seconds * due to classloading. To bridge the gap, wait until the welcome message @@ -98,21 +93,20 @@ class InterpreterLoop( */ def firstLine(): String = { val futLine = Future(in.readLine(prompt)) - bindSettings() + //bindSettings() // TODO enable 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>. + * `command()` for each line of input. */ - @tailrec final def repl(line: String): Unit = + @tailrec final def repl(line: String = in.readLine(prompt)): Unit = if (line != null) { val (keepGoing, finalLineOpt) = command(line) if (keepGoing) { finalLineOpt.foreach(addReplay) out.flush() - repl(in.readLine(prompt)) + repl() } } @@ -121,7 +115,7 @@ class InterpreterLoop( val fileIn = try { new FileReader(filename) } catch { - case _:IOException => + case _: IOException => out.println("Error opening file: " + filename) return } @@ -132,7 +126,7 @@ class InterpreterLoop( in = new SimpleReader(inFile, out, false) out.println("Loading " + filename + "...") out.flush - repl(in.readLine(prompt)) + repl() } finally { in = oldIn replayCommandsRev = oldReplay @@ -158,12 +152,12 @@ class InterpreterLoop( val spaceIdx = command.indexOf(' ') if (spaceIdx <= 0) { out.println("That command requires a filename to be specified.") - return () + return } val filename = command.substring(spaceIdx).trim - if (! new File(filename).exists) { + if (!new File(filename).exists) { out.println("That file does not exist") - return () + return } action(filename) } @@ -186,7 +180,7 @@ class InterpreterLoop( }) } else if (line matches replayRegexp) - replay + replay() else if (line startsWith ":") out.println("Unknown command. Type :help for help.") else @@ -203,14 +197,14 @@ class InterpreterLoop( */ def interpretStartingWith(code: String): Option[String] = { interpreter.interpret(code) match { - case IR.Success => Some(code) - case IR.Error => None - case IR.Incomplete => + case Interpreter.Success => Some(code) + case Interpreter.Error => None + case Interpreter.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(" | ") + val nextLine = in.readLine(continuationPrompt) if (nextLine == null) None // end of file else @@ -235,7 +229,7 @@ class InterpreterLoop( def run(): Reporter = { // loadFiles(settings) try { - if (!ctx.reporter.hasErrors) { + if (!ctx.reporter.hasErrors) { // if there are already errors, no sense to continue printWelcome() repl(firstLine()) } diff --git a/src/dotty/tools/dotc/repl/InterpreterResults.scala b/src/dotty/tools/dotc/repl/InterpreterResults.scala deleted file mode 100644 index e5f8affa8..000000000 --- a/src/dotty/tools/dotc/repl/InterpreterResults.scala +++ /dev/null @@ -1,19 +0,0 @@ -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/Main.scala b/src/dotty/tools/dotc/repl/Main.scala new file mode 100644 index 000000000..ef362ce94 --- /dev/null +++ b/src/dotty/tools/dotc/repl/Main.scala @@ -0,0 +1,27 @@ +package dotty.tools +package dotc +package repl + +/* This REPL was adapted from an old (2008-ish) version of the Scala + * REPL. The original version from which the adaptation was done is found in: + * + * https://github.com/odersky/legacy-svn-scala/tree/spoon + * + * The reason this version was picked instead of a more current one is that + * the older version is much smaller, therefore easier to port. It is also + * considerably less intertwined with nsc than later versions. + * + * There are a number of TODOs: + * + * - re-enable bindings (urgent, easy) + * - re-enable jline support (urgent, easy) + * - create or port REPL tests (urgent, intermediate) + * - copy improvements of current Scala REPL wrt to this version + * (somewhat urgent, intermediate) + * - make string generation more functional (not urgent, easy) + * - better handling of ^C (not urgent, intermediate) + * - syntax highlighting (not urgent, intermediate) + * - integrate with presentation compiler for command completion (not urgent, hard) + */ +/** The main entry point of the REPL */ +object Main extends REPL
\ No newline at end of file diff --git a/src/dotty/tools/dotc/REPL.scala b/src/dotty/tools/dotc/repl/REPL.scala index fdc8f690d..2d6a3c742 100644 --- a/src/dotty/tools/dotc/REPL.scala +++ b/src/dotty/tools/dotc/repl/REPL.scala @@ -1,14 +1,10 @@ package dotty.tools package dotc +package repl -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: @@ -27,6 +23,7 @@ import repl._ */ class REPL extends Driver { + /** The default input reader */ def input(implicit ctx: Context): InteractiveReader = { val emacsShell = System.getProperty("env.emacs", "") != "" //println("emacsShell="+emacsShell) //debug @@ -34,14 +31,19 @@ class REPL extends Driver { else InteractiveReader.createDefault() } + /** The defult output writer */ def output: PrintWriter = new NewLinePrintWriter(new ConsoleWriter, true) - override def newCompiler(implicit ctx: Context): Compiler = new repl.Interpreter(output, ctx) + override def newCompiler(implicit ctx: Context): Compiler = + new repl.CompilingInterpreter(output, ctx) override def sourcesRequired = false override def doCompile(compiler: Compiler, fileNames: List[String])(implicit ctx: Context): Reporter = { - new InterpreterLoop(compiler, input, output).run() + if (fileNames.isEmpty) + new InterpreterLoop(compiler, input, output).run() + else + ctx.error(s"don't now what to do with $fileNames%, %") ctx.reporter } } |