/* NSC -- new Scala compiler * Copyright 2005-2009 LAMP/EPFL * @author Martin Odersky */ // $Id$ package scala.tools.nsc import java.io.{ File, PrintWriter, StringWriter, Writer } import java.lang.{ Class, ClassLoader } import java.net.{ MalformedURLException, URL, URLClassLoader } import java.lang.reflect import reflect.InvocationTargetException import scala.collection.immutable.ListSet import scala.collection.mutable import scala.collection.mutable.{ ListBuffer, HashSet, ArrayBuffer } import io.{ PlainFile, VirtualDirectory } import reporters.{ ConsoleReporter, Reporter } import symtab.{ Flags, Names } import util.{ SourceFile, BatchSourceFile, ClassPath, NameTransformer } import nsc.{ InterpreterResults => IR } import nsc.interpreter._ import Interpreter._ /**

* An interpreter for Scala code. *

*

* The main public entry points are compile(), * interpret(), and bind(). * The compile() method loads a * complete Scala file. The interpret() method executes one * line of Scala code at the request of the user. The bind() * method binds an object to a variable that can then be used by later * interpreted code. *

*

* 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 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. *

*

* 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 */ class Interpreter(val settings: Settings, out: PrintWriter) { /* If running on pre-1.5 JVM, force target setting to 1.4 */ private val major = System.getProperty("java.class.version").split("\\.")(0) if (major.toInt < 49) this.settings.target.value = "jvm-1.4" /** directory to save .class files to */ val virtualDirectory = new VirtualDirectory("(memory)", None) /** the compiler to compile expressions with */ val compiler: nsc.Global = newCompiler(settings, reporter) import compiler.{ Traverser, CompilationUnit, Symbol, Name, Type } import compiler.{ Tree, TermTree, ValOrDefDef, ValDef, DefDef, Assign, ClassDef, ModuleDef, Ident, Select, TypeDef, Import, MemberDef, DocDef } import compiler.{ nme, newTermName } import nme.{ INTERPRETER_VAR_PREFIX, INTERPRETER_SYNTHVAR_PREFIX, INTERPRETER_LINE_PREFIX, INTERPRETER_IMPORT_WRAPPER, INTERPRETER_WRAPPER_SUFFIX, USCOREkw } /** construct an interpreter that reports to Console */ def this(settings: Settings) = this(settings, new NewLinePrintWriter(new ConsoleWriter, true)) /** 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 */ lazy val isettings = new InterpreterSettings object reporter extends ConsoleReporter(settings, null, out) { override def printMessage(msg: String) { out.print(clean(msg) + "\n"); out.flush() } } /** Instantiate a compiler. Subclasses can override this to * change the compiler class used by this interpreter. */ protected def newCompiler(settings: Settings, reporter: Reporter) = { val comp = new nsc.Global(settings, reporter) comp.genJVM.outputDir = virtualDirectory comp } /** the compiler's classpath, as URL's */ val compilerClasspath: List[URL] = { val classpathPart = ClassPath.expandPath(compiler.settings.classpath.value).map(s => new File(s).toURL) def parseURL(s: String): Option[URL] = try { Some(new URL(s)) } catch { case _: MalformedURLException => None } val codebasePart = (compiler.settings.Xcodebase.value.split(" ")).toList flatMap parseURL classpathPart ::: codebasePart } /* 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. */ private var classLoader = makeClassLoader() private def makeClassLoader(): ClassLoader = { val cp = compilerClasspath.toArray val parent = if (parentClassLoader == null) new URLClassLoader(cp) else new URLClassLoader(cp, parentClassLoader) new AbstractFileClassLoader(virtualDirectory, parent) } private def loadByName(s: String): Class[_] = Class.forName(s, true, classLoader) // XXX how does this differ from getMethod("set") ? private def methodByName(c: Class[_], name: String): Option[reflect.Method] = c.getDeclaredMethods.toList.find(_.getName == name) // Set the current Java "context" class loader to this interpreter's class loader def setContextClassLoader() = Thread.currentThread.setContextClassLoader(classLoader) protected def parentClassLoader: ClassLoader = this.getClass.getClassLoader() /** the previous requests this interpreter has processed */ private val prevRequests = new ArrayBuffer[Request]() private def allUsedNames = prevRequests.toList.flatMap(_.usedNames).removeDuplicates private def allBoundNames = prevRequests.toList.flatMap(_.boundNames).removeDuplicates /** counter creator */ def mkNameCreator(s: String) = new Function0[String] with Function1[String,String] { private var x = -1 def apply(): String = { x += 1 ; s + x.toString } // second apply method temp for newInternalVarName's bug compatibility def apply(pre: String) = { x += 1 ; pre + x.toString } def reset(): Unit = x = -1 } /** allocate a fresh line name */ private val newLineName = mkNameCreator(INTERPRETER_LINE_PREFIX) /** allocate a fresh var name */ private val newVarName = mkNameCreator(INTERPRETER_VAR_PREFIX) /** allocate a fresh internal variable name */ /** XXX temporarily shares newVarName's creator to be bug-compatible with * test case interpreter.scala */ private def newInternalVarName = () => newVarName(INTERPRETER_SYNTHVAR_PREFIX) // private val newInternalVarName = mkNameCreator(INTERPRETER_SYNTHVAR_PREFIX) private def isGenerated(pre: String, name: String) = (name startsWith pre) && (name drop pre.length).forall(_.isDigit) /** Check if a name looks like it was generated by newVarName */ private def isGeneratedVarName(name: String): Boolean = isGenerated(INTERPRETER_VAR_PREFIX, name) private def isSynthVarName(name: String): Boolean = isGenerated(INTERPRETER_SYNTHVAR_PREFIX, name) /** 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 val trailer = "..." if (maxpr <= 0 || str.length <= maxpr) str else str.substring(0, maxpr-3) + trailer } /** Clean up a string for output */ private def clean(str: String) = truncPrintString(stripWrapperGunk(str)) /** Indent some code by the width of the scala> prompt. * This way, compiler error messages read better. */ private final val spaces = List.make(7, " ").mkString def indentCode(code: String) = stringFrom(str => for (line <- code.lines) { str.print(spaces) str.print(line + "\n") str.flush() }) implicit def name2string(name: Name) = name.toString /** 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 traverested 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 case class ComputedImports(prepend: String, append: String, access: String) private def importsCode(wanted: Set[Name]): ComputedImports = { /** 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. */ case class ReqAndHandler(req: Request, handler: MemberHandler) def reqsToUse: List[ReqAndHandler] = { /** Loop through a list of MemberHandlers and select which ones to keep. * 'wanted' is the set of names that need to be imported. */ def select(reqs: List[ReqAndHandler], wanted: Set[Name]): List[ReqAndHandler] = { val isWanted = wanted contains _ def keepHandler(handler: MemberHandler): Boolean = { import handler._ definesImplicit || importsWildcard || (importedNames ++ boundNames).exists(isWanted) } reqs match { case Nil => Nil case rh :: rest if !keepHandler(rh.handler) => select(rest, wanted) case rh :: rest => import rh.handler._ val newWanted = wanted ++ usedNames -- boundNames -- importedNames rh :: select(rest, newWanted) } } val rhpairs = for { req <- prevRequests.toList.reverse handler <- req.handlers } yield ReqAndHandler(req, handler) select(rhpairs, wanted).reverse } val code, trailingBraces, accessPath = new StringBuffer val currentImps = mutable.Set.empty[Name] // add code for a new object to hold some imports def addWrapper() { val impname = INTERPRETER_IMPORT_WRAPPER code append "object %s {\n".format(impname) trailingBraces append "}\n" accessPath append ("." + impname) currentImps.clear } addWrapper() // loop through previous requests, adding imports for each one for (ReqAndHandler(req, handler) <- reqsToUse) { import handler._ // 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 (importsWildcard || currentImps.exists(importedNames.contains)) addWrapper() if (member.isInstanceOf[Import]) code append (member.toString + "\n") // give wildcard imports a import wrapper all to their own if (importsWildcard) addWrapper() else currentImps ++= 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 <- boundNames) { if (currentImps contains imv) addWrapper() code append ("import " + req.fullPath(imv)) currentImps += imv } } // add one extra wrapper, to prevent warnings in the common case of // redefining the value bound in the last interpreter request. addWrapper() ComputedImports(code.toString, trailingBraces.toString, accessPath.toString) } /** Parse a line into a sequence of trees. Returns None if the input is incomplete. */ private def parse(line: String): Option[List[Tree]] = { var justNeedsMore = false reporter.withIncompleteHandler((pos,msg) => {justNeedsMore = true}) { // simple parse: just parse it, nothing else def simpleParse(code: String): List[Tree] = { reporter.reset val unit = new CompilationUnit(new BatchSourceFile("", code)) val scanner = new compiler.syntaxAnalyzer.UnitParser(unit) scanner.templateStatSeq(false)._2 } val trees = simpleParse(line) if (reporter.hasErrors) Some(Nil) // the result did not parse, so stop else if (justNeedsMore) None else Some(trees) } } /** Compile an nsc SourceFile. Returns true if there are * no compilation errors, or false othrewise. */ def compileSources(sources: SourceFile*): Boolean = { reporter.reset new compiler.Run() compileSources sources.toList !reporter.hasErrors } /** Compile a string. Returns true if there are no * compilation errors, or false otherwise. */ def compileString(code: String): Boolean = compileSources(new BatchSourceFile("