diff options
Diffstat (limited to 'src/repl')
48 files changed, 5856 insertions, 0 deletions
diff --git a/src/repl/scala/tools/nsc/Interpreter.scala b/src/repl/scala/tools/nsc/Interpreter.scala new file mode 100644 index 0000000000..434f19f21b --- /dev/null +++ b/src/repl/scala/tools/nsc/Interpreter.scala @@ -0,0 +1,12 @@ +package scala.tools.nsc + +import interpreter._ +import java.io._ + +/** A compatibility stub. + */ +@deprecated("Use a class in the scala.tools.nsc.interpreter package.", "2.9.0") +class Interpreter(settings: Settings, out: PrintWriter) extends IMain(settings, out) { + def this(settings: Settings) = this(settings, new NewLinePrintWriter(new ConsoleWriter, true)) + def this() = this(new Settings()) +}
\ No newline at end of file diff --git a/src/repl/scala/tools/nsc/InterpreterLoop.scala b/src/repl/scala/tools/nsc/InterpreterLoop.scala new file mode 100644 index 0000000000..a0be3f4fdb --- /dev/null +++ b/src/repl/scala/tools/nsc/InterpreterLoop.scala @@ -0,0 +1,12 @@ +package scala.tools.nsc + +import interpreter._ +import java.io._ + +/** A compatibility stub. + */ +@deprecated("Use a class in the scala.tools.nsc.interpreter package.", "2.9.0") +class InterpreterLoop(in0: Option[BufferedReader], out: PrintWriter) extends ILoop(in0, out) { + def this(in0: BufferedReader, out: PrintWriter) = this(Some(in0), out) + def this() = this(None, new PrintWriter(scala.Console.out)) +} diff --git a/src/repl/scala/tools/nsc/MainGenericRunner.scala b/src/repl/scala/tools/nsc/MainGenericRunner.scala new file mode 100644 index 0000000000..7195424cf9 --- /dev/null +++ b/src/repl/scala/tools/nsc/MainGenericRunner.scala @@ -0,0 +1,105 @@ +/* NSC -- new Scala compiler + * Copyright 2006-2013 LAMP/EPFL + * @author Lex Spoon + */ + +package scala.tools.nsc + +import io.{ File } +import util.{ ClassPath, ScalaClassLoader } +import Properties.{ versionString, copyrightString } +import GenericRunnerCommand._ + +object JarRunner extends CommonRunner { + def runJar(settings: GenericRunnerSettings, jarPath: String, arguments: Seq[String]): Either[Throwable, Boolean] = { + val jar = new io.Jar(jarPath) + val mainClass = jar.mainClass getOrElse sys.error("Cannot find main class for jar: " + jarPath) + val jarURLs = ClassPath expandManifestPath jarPath + val urls = if (jarURLs.isEmpty) File(jarPath).toURL +: settings.classpathURLs else jarURLs + + if (settings.Ylogcp.value) { + Console.err.println("Running jar with these URLs as the classpath:") + urls foreach println + } + + runAndCatch(urls, mainClass, arguments) + } +} + +/** An object that runs Scala code. It has three possible + * sources for the code to run: pre-compiled code, a script file, + * or interactive entry. + */ +class MainGenericRunner { + def errorFn(ex: Throwable): Boolean = { + ex.printStackTrace() + false + } + def errorFn(str: String): Boolean = { + Console.err println str + false + } + + def process(args: Array[String]): Boolean = { + val command = new GenericRunnerCommand(args.toList, (x: String) => errorFn(x)) + import command.{ settings, howToRun, thingToRun } + def sampleCompiler = new Global(settings) // def so its not created unless needed + + if (!command.ok) return errorFn("\n" + command.shortUsageMsg) + else if (settings.version.value) return errorFn("Scala code runner %s -- %s".format(versionString, copyrightString)) + else if (command.shouldStopWithInfo) return errorFn(command getInfoMessage sampleCompiler) + + def isE = !settings.execute.isDefault + def dashe = settings.execute.value + + def isI = !settings.loadfiles.isDefault + def dashi = settings.loadfiles.value + + // Deadlocks on startup under -i unless we disable async. + if (isI) + settings.Yreplsync.value = true + + def combinedCode = { + val files = if (isI) dashi map (file => File(file).slurp()) else Nil + val str = if (isE) List(dashe) else Nil + + files ++ str mkString "\n\n" + } + + def runTarget(): Either[Throwable, Boolean] = howToRun match { + case AsObject => + ObjectRunner.runAndCatch(settings.classpathURLs, thingToRun, command.arguments) + case AsScript => + ScriptRunner.runScriptAndCatch(settings, thingToRun, command.arguments) + case AsJar => + JarRunner.runJar(settings, thingToRun, command.arguments) + case Error => + Right(false) + case _ => + // We start the repl when no arguments are given. + Right(new interpreter.ILoop process settings) + } + + /** If -e and -i were both given, we want to execute the -e code after the + * -i files have been included, so they are read into strings and prepended to + * the code given in -e. The -i option is documented to only make sense + * interactively so this is a pretty reasonable assumption. + * + * This all needs a rewrite though. + */ + if (isE) { + ScriptRunner.runCommand(settings, combinedCode, thingToRun +: command.arguments) + } + else runTarget() match { + case Left(ex) => errorFn(ex) + case Right(b) => b + } + } +} + +object MainGenericRunner extends MainGenericRunner { + def main(args: Array[String]) { + if (!process(args)) + sys.exit(1) + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/AbstractFileClassLoader.scala b/src/repl/scala/tools/nsc/interpreter/AbstractFileClassLoader.scala new file mode 100644 index 0000000000..712219533d --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/AbstractFileClassLoader.scala @@ -0,0 +1,7 @@ +package scala.tools.nsc +package interpreter + +import scala.reflect.io.AbstractFile + +@deprecated("Use `scala.tools.nsc.util.AbstractFileClassLoader`", "2.11.0") +class AbstractFileClassLoader(root: AbstractFile, parent: ClassLoader) extends util.AbstractFileClassLoader(root, parent) diff --git a/src/repl/scala/tools/nsc/interpreter/AbstractOrMissingHandler.scala b/src/repl/scala/tools/nsc/interpreter/AbstractOrMissingHandler.scala new file mode 100644 index 0000000000..e66e4eff29 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/AbstractOrMissingHandler.scala @@ -0,0 +1,41 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +class AbstractOrMissingHandler[T](onError: String => Unit, value: T) extends PartialFunction[Throwable, T] { + def isDefinedAt(t: Throwable) = t match { + case _: AbstractMethodError => true + case _: NoSuchMethodError => true + case _: MissingRequirementError => true + case _: NoClassDefFoundError => true + case _ => false + } + def apply(t: Throwable) = t match { + case x @ (_: AbstractMethodError | _: NoSuchMethodError | _: NoClassDefFoundError) => + onError(""" + |Failed to initialize compiler: %s. + |This is most often remedied by a full clean and recompile. + |Otherwise, your classpath may continue bytecode compiled by + |different and incompatible versions of scala. + |""".stripMargin.format(x.getClass.getName split '.' last) + ) + x.printStackTrace() + value + case x: MissingRequirementError => + onError(""" + |Failed to initialize compiler: %s not found. + |** Note that as of 2.8 scala does not assume use of the java classpath. + |** For the old behavior pass -usejavacp to scala, or if using a Settings + |** object programatically, settings.usejavacp.value = true.""".stripMargin.format(x.req) + ) + value + } +} + +object AbstractOrMissingHandler { + def apply[T]() = new AbstractOrMissingHandler[T](Console println _, null.asInstanceOf[T]) +} diff --git a/src/repl/scala/tools/nsc/interpreter/ByteCode.scala b/src/repl/scala/tools/nsc/interpreter/ByteCode.scala new file mode 100644 index 0000000000..e1e3678837 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/ByteCode.scala @@ -0,0 +1,32 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import java.lang.reflect +import util.ScalaClassLoader +import ScalaClassLoader.appLoader +import scala.reflect.NameTransformer._ + +object ByteCode { + /** Until I figure out why I can't get scalap onto the classpath such + * that the compiler will bootstrap, we have to use reflection. + */ + private lazy val DECODER: Option[AnyRef] = + for (clazz <- appLoader.tryToLoadClass[AnyRef]("scala.tools.scalap.Decode$")) yield + clazz.getField(MODULE_INSTANCE_NAME).get(null) + + private def decoderMethod(name: String, args: JClass*): Option[reflect.Method] = { + for (decoder <- DECODER ; m <- Option(decoder.getClass.getMethod(name, args: _*))) yield m + } + + private lazy val aliasMap = { + for (module <- DECODER ; method <- decoderMethod("typeAliases", classOf[String])) yield + method.invoke(module, _: String).asInstanceOf[Option[Map[String, String]]] + } + + def aliasesForPackage(pkg: String) = aliasMap flatMap (_(pkg)) +} diff --git a/src/repl/scala/tools/nsc/interpreter/CommandLine.scala b/src/repl/scala/tools/nsc/interpreter/CommandLine.scala new file mode 100644 index 0000000000..0ab92ab769 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/CommandLine.scala @@ -0,0 +1,13 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Lex Spoon + */ + +package scala.tools.nsc +package interpreter + +/** A command line for the interpreter. + */ +class CommandLine(arguments: List[String], error: String => Unit) extends CompilerCommand(arguments, error) { + override def cmdName = "scala" +} diff --git a/src/repl/scala/tools/nsc/interpreter/Completion.scala b/src/repl/scala/tools/nsc/interpreter/Completion.scala new file mode 100644 index 0000000000..84a5cb49ae --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/Completion.scala @@ -0,0 +1,49 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import Completion._ + +/** An implementation-agnostic completion interface which makes no + * reference to the jline classes. + */ +trait Completion { + type ExecResult + def resetVerbosity(): Unit + def completer(): ScalaCompleter +} +object NoCompletion extends Completion { + type ExecResult = Nothing + def resetVerbosity() = () + def completer() = NullCompleter +} + +object Completion { + case class Candidates(cursor: Int, candidates: List[String]) { } + val NoCandidates = Candidates(-1, Nil) + + object NullCompleter extends ScalaCompleter { + def complete(buffer: String, cursor: Int): Candidates = NoCandidates + } + trait ScalaCompleter { + def complete(buffer: String, cursor: Int): Candidates + } + + def looksLikeInvocation(code: String) = ( + (code != null) + && (code startsWith ".") + && !(code == ".") + && !(code startsWith "./") + && !(code startsWith "..") + ) + object Forwarder { + def apply(forwardTo: () => Option[CompletionAware]): CompletionAware = new CompletionAware { + def completions(verbosity: Int) = forwardTo() map (_ completions verbosity) getOrElse Nil + override def follow(s: String) = forwardTo() flatMap (_ follow s) + } + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/CompletionAware.scala b/src/repl/scala/tools/nsc/interpreter/CompletionAware.scala new file mode 100644 index 0000000000..3dd5d93390 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/CompletionAware.scala @@ -0,0 +1,53 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +/** An interface for objects which are aware of tab completion and + * will supply their own candidates and resolve their own paths. + */ +trait CompletionAware { + /** The complete list of unqualified Strings to which this + * object will complete. + */ + def completions(verbosity: Int): List[String] + + /** The next completor in the chain. + */ + def follow(id: String): Option[CompletionAware] = None + + /** A list of useful information regarding a specific uniquely + * identified completion. This is specifically written for the + * following situation, but should be useful elsewhere too: + * + * x.y.z.methodName<tab> + * + * If "methodName" is among z's completions, and verbosity > 0 + * indicating tab has been pressed twice consecutively, then we + * call alternativesFor and show a list of overloaded method + * signatures. + */ + def alternativesFor(id: String): List[String] = Nil + + /** Given string 'buf', return a list of all the strings + * to which it can complete. This may involve delegating + * to other CompletionAware objects. + */ + def completionsFor(parsed: Parsed): List[String] = { + import parsed.{ buffer, verbosity } + val comps = completions(verbosity) filter (_ startsWith buffer) + val exact = comps contains buffer + + val results = + if (parsed.isEmpty) comps + else if (parsed.isUnqualified && !parsed.isLastDelimiter) + if (verbosity > 0 && exact) alternativesFor(buffer) + else comps + else follow(parsed.bufferHead) map (_ completionsFor parsed.bufferTail) getOrElse Nil + + results.sorted + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/CompletionOutput.scala b/src/repl/scala/tools/nsc/interpreter/CompletionOutput.scala new file mode 100644 index 0000000000..d24ad60974 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/CompletionOutput.scala @@ -0,0 +1,85 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +/** This has a lot of duplication with other methods in Symbols and Types, + * but repl completion utility is very sensitive to precise output. Best + * thing would be to abstract an interface for how such things are printed, + * as is also in progress with error messages. + */ +trait CompletionOutput { + val global: Global + + import global._ + import definitions.{ isTupleType, isFunctionType, isRepeatedParamType } + + /** Reducing fully qualified noise for some common packages. + */ + val typeTransforms = List( + "java.lang." -> "", + "scala.collection.immutable." -> "immutable.", + "scala.collection.mutable." -> "mutable.", + "scala.collection.generic." -> "generic." + ) + + def quietString(tp: String): String = + typeTransforms.foldLeft(tp) { + case (str, (prefix, replacement)) => + if (str startsWith prefix) replacement + (str stripPrefix prefix) + else str + } + + class MethodSymbolOutput(method: Symbol) { + val pkg = method.ownerChain find (_.isPackageClass) map (_.fullName) getOrElse "" + + def relativize(str: String): String = quietString(str stripPrefix (pkg + ".")) + def relativize(tp: Type): String = relativize(tp.dealiasWiden.toString) + + def braceList(tparams: List[String]) = if (tparams.isEmpty) "" else (tparams map relativize).mkString("[", ", ", "]") + def parenList(params: List[Any]) = params.mkString("(", ", ", ")") + + def methodTypeToString(mt: MethodType) = + (mt.paramss map paramsString mkString "") + ": " + relativize(mt.finalResultType) + + def typeToString(tp: Type): String = relativize( + tp match { + case x if isFunctionType(x) => functionString(x) + case x if isTupleType(x) => tupleString(x) + case x if isRepeatedParamType(x) => typeToString(x.typeArgs.head) + "*" + case mt @ MethodType(_, _) => methodTypeToString(mt) + case x => x.toString + } + ) + + def tupleString(tp: Type) = parenList(tp.dealiasWiden.typeArgs map relativize) + def functionString(tp: Type) = tp.dealiasWiden.typeArgs match { + case List(t, r) => t + " => " + r + case xs => parenList(xs.init) + " => " + xs.last + } + + def tparamsString(tparams: List[Symbol]) = braceList(tparams map (_.defString)) + def paramsString(params: List[Symbol]) = { + def paramNameString(sym: Symbol) = if (sym.isSynthetic) "" else sym.nameString + ": " + def paramString(sym: Symbol) = paramNameString(sym) + typeToString(sym.info.dealiasWiden) + + val isImplicit = params.nonEmpty && params.head.isImplicit + val strs = (params map paramString) match { + case x :: xs if isImplicit => ("implicit " + x) :: xs + case xs => xs + } + parenList(strs) + } + + def methodString() = + method.keyString + " " + method.nameString + (method.info.dealiasWiden match { + case NullaryMethodType(resType) => ": " + typeToString(resType) + case PolyType(tparams, resType) => tparamsString(tparams) + typeToString(resType) + case mt @ MethodType(_, _) => methodTypeToString(mt) + case x => x.toString + }) + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala b/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala new file mode 100644 index 0000000000..48af261937 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala @@ -0,0 +1,63 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.tools.jline.console.{ ConsoleReader, CursorBuffer } + +trait ConsoleReaderHelper extends ConsoleReader { + def terminal = getTerminal() + def width = terminal.getWidth() + def height = terminal.getHeight() + + def readOneKey(prompt: String): Int + def eraseLine(): Unit + + private val marginSize = 3 + private def morePrompt = "--More--" + private def emulateMore(): Int = { + val key = readOneKey(morePrompt) + try key match { + case '\r' | '\n' => 1 + case 'q' => -1 + case _ => height - 1 + } + finally { + eraseLine() + // TODO: still not quite managing to erase --More-- and get + // back to a scala prompt without another keypress. + if (key == 'q') { + putString(getPrompt()) + redrawLine() + flush() + } + } + } + + override def printColumns(items: JCollection[_ <: CharSequence]): Unit = + printColumns(items: List[String]) + + def printColumns(items: List[String]): Unit = { + if (items forall (_ == "")) + return + + val longest = items map (_.length) max + var linesLeft = if (isPaginationEnabled()) height - 1 else Int.MaxValue + val columnSize = longest + marginSize + val padded = items map ("%-" + columnSize + "s" format _) + val groupSize = 1 max (width / columnSize) // make sure it doesn't divide to 0 + + padded grouped groupSize foreach { xs => + println(xs.mkString) + linesLeft -= 1 + if (linesLeft <= 0) { + linesLeft = emulateMore() + if (linesLeft < 0) + return + } + } + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/Delimited.scala b/src/repl/scala/tools/nsc/interpreter/Delimited.scala new file mode 100644 index 0000000000..e88a044931 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/Delimited.scala @@ -0,0 +1,41 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.tools.jline.console.completer.ArgumentCompleter.{ ArgumentDelimiter, ArgumentList } + +class JLineDelimiter extends ArgumentDelimiter { + def toJLine(args: List[String], cursor: Int) = args match { + case Nil => new ArgumentList(new Array[String](0), 0, 0, cursor) + case xs => new ArgumentList(xs.toArray, xs.size - 1, xs.last.length, cursor) + } + + def delimit(buffer: CharSequence, cursor: Int) = { + val p = Parsed(buffer.toString, cursor) + toJLine(p.args, cursor) + } + def isDelimiter(buffer: CharSequence, cursor: Int) = Parsed(buffer.toString, cursor).isDelimiter +} + +trait Delimited { + self: Parsed => + + def delimited: Char => Boolean + def escapeChars: List[Char] = List('\\') + + /** Break String into args based on delimiting function. + */ + protected def toArgs(s: String): List[String] = + if (s == "") Nil + else (s indexWhere isDelimiterChar) match { + case -1 => List(s) + case idx => (s take idx) :: toArgs(s drop (idx + 1)) + } + + def isDelimiterChar(ch: Char) = delimited(ch) + def isEscapeChar(ch: Char): Boolean = escapeChars contains ch +} diff --git a/src/repl/scala/tools/nsc/interpreter/ExprTyper.scala b/src/repl/scala/tools/nsc/interpreter/ExprTyper.scala new file mode 100644 index 0000000000..9edd54b939 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/ExprTyper.scala @@ -0,0 +1,99 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.tools.nsc.ast.parser.Tokens.EOF + +trait ExprTyper { + val repl: IMain + + import repl._ + import global.{ reporter => _, Import => _, _ } + import definitions._ + import syntaxAnalyzer.UnitParser + import naming.freshInternalVarName + + object codeParser { + val global: repl.global.type = repl.global + def applyRule[T](code: String, rule: UnitParser => T): T = { + reporter.reset() + val scanner = newUnitParser(code) + val result = rule(scanner) + + if (!reporter.hasErrors) + scanner.accept(EOF) + + result + } + def stmts(code: String) = applyRule(code, _.templateStats()) + } + + /** Parse a line into a sequence of trees. Returns None if the input is incomplete. */ + def parse(line: String): Option[List[Tree]] = debugging(s"""parse("$line")""") { + var isIncomplete = false + reporter.withIncompleteHandler((_, _) => isIncomplete = true) { + val trees = codeParser.stmts(line) + if (reporter.hasErrors) Some(Nil) + else if (isIncomplete) None + else Some(trees) + } + } + + def symbolOfLine(code: String): Symbol = { + def asExpr(): Symbol = { + val name = freshInternalVarName() + // Typing it with a lazy val would give us the right type, but runs + // into compiler bugs with things like existentials, so we compile it + // behind a def and strip the NullaryMethodType which wraps the expr. + val line = "def " + name + " = " + code + + interpretSynthetic(line) match { + case IR.Success => + val sym0 = symbolOfTerm(name) + // drop NullaryMethodType + sym0.cloneSymbol setInfo exitingTyper(sym0.info.finalResultType) + case _ => NoSymbol + } + } + def asDefn(): Symbol = { + val old = repl.definedSymbolList.toSet + + interpretSynthetic(code) match { + case IR.Success => + repl.definedSymbolList filterNot old match { + case Nil => NoSymbol + case sym :: Nil => sym + case syms => NoSymbol.newOverloaded(NoPrefix, syms) + } + case _ => NoSymbol + } + } + def asError(): Symbol = { + interpretSynthetic(code) + NoSymbol + } + beSilentDuring(asExpr()) orElse beSilentDuring(asDefn()) orElse asError() + } + + private var typeOfExpressionDepth = 0 + def typeOfExpression(expr: String, silent: Boolean = true): Type = { + if (typeOfExpressionDepth > 2) { + repldbg("Terminating typeOfExpression recursion for expression: " + expr) + return NoType + } + typeOfExpressionDepth += 1 + // Don't presently have a good way to suppress undesirable success output + // while letting errors through, so it is first trying it silently: if there + // is an error, and errors are desired, then it re-evaluates non-silently + // to induce the error message. + try beSilentDuring(symbolOfLine(expr).tpe) match { + case NoType if !silent => symbolOfLine(expr).tpe // generate error + case tpe => tpe + } + finally typeOfExpressionDepth -= 1 + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/Formatting.scala b/src/repl/scala/tools/nsc/interpreter/Formatting.scala new file mode 100644 index 0000000000..43e653edfd --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/Formatting.scala @@ -0,0 +1,35 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import util.stringFromWriter + +trait Formatting { + def prompt: String + + def spaces(code: String): String = { + /** Heuristic to avoid indenting and thereby corrupting """-strings and XML literals. */ + val tokens = List("\"\"\"", "</", "/>") + val noIndent = (code contains "\n") && (tokens exists code.contains) + + if (noIndent) "" + else prompt drop 1 map (_ => ' ') + } + /** Indent some code by the width of the scala> prompt. + * This way, compiler error messages read better. + */ + def indentCode(code: String) = { + val indent = spaces(code) + stringFromWriter(str => + for (line <- code.lines) { + str print indent + str print (line + "\n") + str.flush() + } + ) + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/ILoop.scala b/src/repl/scala/tools/nsc/interpreter/ILoop.scala new file mode 100644 index 0000000000..599a061984 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/ILoop.scala @@ -0,0 +1,748 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Alexander Spoon + */ + +package scala.tools.nsc +package interpreter + +import Predef.{ println => _, _ } +import java.io.{ BufferedReader, FileReader } +import session._ +import scala.annotation.tailrec +import scala.util.Properties.{ jdkHome, javaVersion, versionString, javaVmName } +import util.{ ClassPath, Exceptional, stringFromWriter, stringFromStream } +import io.{ File, Directory } +import util.ScalaClassLoader +import ScalaClassLoader._ +import scala.tools.util._ +import scala.language.{implicitConversions, existentials} +import scala.reflect.classTag +import StdReplTags._ +import scala.concurrent.{ ExecutionContext, Await, Future, future } +import ExecutionContext.Implicits._ + +/** The Scala interactive shell. It provides a read-eval-print loop + * around the Interpreter class. + * After instantiation, clients should call the main() method. + * + * If no in0 is specified, then input will come from the console, and + * the class will attempt to provide input editing feature such as + * input history. + * + * @author Moez A. Abdel-Gawad + * @author Lex Spoon + * @version 1.2 + */ +class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) + extends AnyRef + with LoopCommands +{ + def this(in0: BufferedReader, out: JPrintWriter) = this(Some(in0), out) + def this() = this(None, new JPrintWriter(Console.out, true)) + + @deprecated("Use `intp` instead.", "2.9.0") def interpreter = intp + @deprecated("Use `intp` instead.", "2.9.0") def interpreter_= (i: Interpreter): Unit = intp = i + + var in: InteractiveReader = _ // the input stream from which commands come + var settings: Settings = _ + var intp: IMain = _ + + private var globalFuture: Future[Boolean] = _ + + /** Print a welcome message */ + def printWelcome() { + echo(s""" + |Welcome to Scala $versionString ($javaVmName, Java $javaVersion). + |Type in expressions to have them evaluated. + |Type :help for more information.""".trim.stripMargin + ) + replinfo("[info] started at " + new java.util.Date) + } + + protected def asyncMessage(msg: String) { + if (isReplInfo || isReplPower) + echoAndRefresh(msg) + } + + override def echoCommandMessage(msg: String) { + intp.reporter printUntruncatedMessage msg + } + + lazy val power = new Power(intp, new StdReplVals(this))(tagOfStdReplVals, classTag[StdReplVals]) + def history = in.history + + // classpath entries added via :cp + var addedClasspath: String = "" + + /** A reverse list of commands to replay if the user requests a :replay */ + var replayCommandStack: List[String] = Nil + + /** A list of commands to replay if the user requests a :replay */ + def replayCommands = replayCommandStack.reverse + + /** Record a command for replay should the user request a :replay */ + def addReplay(cmd: String) = replayCommandStack ::= cmd + + def savingReplayStack[T](body: => T): T = { + val saved = replayCommandStack + try body + finally replayCommandStack = saved + } + def savingReader[T](body: => T): T = { + val saved = in + try body + finally in = saved + } + + /** Close the interpreter and set the var to null. */ + def closeInterpreter() { + if (intp ne null) { + intp.close() + intp = null + } + } + + class ILoopInterpreter extends IMain(settings, out) { + outer => + + override lazy val formatting = new Formatting { + def prompt = ILoop.this.prompt + } + override protected def parentClassLoader = + settings.explicitParentLoader.getOrElse( classOf[ILoop].getClassLoader ) + } + + /** Create a new interpreter. */ + def createInterpreter() { + if (addedClasspath != "") + settings.classpath append addedClasspath + + intp = new ILoopInterpreter + } + + /** print a friendly help message */ + def helpCommand(line: String): Result = { + if (line == "") helpSummary() + else uniqueCommand(line) match { + case Some(lc) => echo("\n" + lc.help) + case _ => ambiguousError(line) + } + } + private def helpSummary() = { + val usageWidth = commands map (_.usageMsg.length) max + val formatStr = "%-" + usageWidth + "s %s" + + echo("All commands can be abbreviated, e.g. :he instead of :help.") + + commands foreach { cmd => + echo(formatStr.format(cmd.usageMsg, cmd.help)) + } + } + private def ambiguousError(cmd: String): Result = { + matchingCommands(cmd) match { + case Nil => echo(cmd + ": no such command. Type :help for help.") + case xs => echo(cmd + " is ambiguous: did you mean " + xs.map(":" + _.name).mkString(" or ") + "?") + } + Result(keepRunning = true, None) + } + private def matchingCommands(cmd: String) = commands filter (_.name startsWith cmd) + private def uniqueCommand(cmd: String): Option[LoopCommand] = { + // this lets us add commands willy-nilly and only requires enough command to disambiguate + matchingCommands(cmd) match { + case List(x) => Some(x) + // exact match OK even if otherwise appears ambiguous + case xs => xs find (_.name == cmd) + } + } + + /** Show the history */ + lazy val historyCommand = new LoopCommand("history", "show the history (optional num is commands to show)") { + override def usage = "[num]" + def defaultLines = 20 + + def apply(line: String): Result = { + if (history eq NoHistory) + return "No history available." + + val xs = words(line) + val current = history.index + val count = try xs.head.toInt catch { case _: Exception => defaultLines } + val lines = history.asStrings takeRight count + val offset = current - lines.size + 1 + + for ((line, index) <- lines.zipWithIndex) + echo("%3d %s".format(index + offset, line)) + } + } + + // When you know you are most likely breaking into the middle + // of a line being typed. This softens the blow. + protected def echoAndRefresh(msg: String) = { + echo("\n" + msg) + in.redrawLine() + } + protected def echo(msg: String) = { + out println msg + out.flush() + } + + /** Search the history */ + def searchHistory(_cmdline: String) { + val cmdline = _cmdline.toLowerCase + val offset = history.index - history.size + 1 + + for ((line, index) <- history.asStrings.zipWithIndex ; if line.toLowerCase contains cmdline) + echo("%d %s".format(index + offset, line)) + } + + private val currentPrompt = Properties.shellPromptString + + /** Prompt to print when awaiting input */ + def prompt = currentPrompt + + import LoopCommand.{ cmd, nullary } + + /** Standard commands **/ + lazy val standardCommands = List( + cmd("cp", "<path>", "add a jar or directory to the classpath", addClasspath), + cmd("help", "[command]", "print this summary or command-specific help", helpCommand), + historyCommand, + cmd("h?", "<string>", "search the history", searchHistory), + cmd("imports", "[name name ...]", "show import history, identifying sources of names", importsCommand), + cmd("implicits", "[-v]", "show the implicits in scope", intp.implicitsCommand), + cmd("javap", "<path|class>", "disassemble a file or class name", javapCommand), + cmd("load", "<path>", "load and interpret a Scala file", loadCommand), + nullary("paste", "enter paste mode: all input up to ctrl-D compiled together", pasteCommand), + nullary("power", "enable power user mode", powerCmd), + nullary("quit", "exit the interpreter", () => Result(keepRunning = false, None)), + nullary("replay", "reset execution and replay all previous commands", replay), + nullary("reset", "reset the repl to its initial state, forgetting all session entries", resetCommand), + shCommand, + nullary("silent", "disable/enable automatic printing of results", verbosity), + cmd("type", "[-v] <expr>", "display the type of an expression without evaluating it", typeCommand), + nullary("warnings", "show the suppressed warnings from the most recent line which had any", warningsCommand) + ) + + /** Power user commands */ + lazy val powerCommands: List[LoopCommand] = List( + cmd("phase", "<phase>", "set the implicit phase for power commands", phaseCommand) + ) + + private def importsCommand(line: String): Result = { + val tokens = words(line) + val handlers = intp.languageWildcardHandlers ++ intp.importHandlers + + handlers.filterNot(_.importedSymbols.isEmpty).zipWithIndex foreach { + case (handler, idx) => + val (types, terms) = handler.importedSymbols partition (_.name.isTypeName) + val imps = handler.implicitSymbols + val found = tokens filter (handler importsSymbolNamed _) + val typeMsg = if (types.isEmpty) "" else types.size + " types" + val termMsg = if (terms.isEmpty) "" else terms.size + " terms" + val implicitMsg = if (imps.isEmpty) "" else imps.size + " are implicit" + val foundMsg = if (found.isEmpty) "" else found.mkString(" // imports: ", ", ", "") + val statsMsg = List(typeMsg, termMsg, implicitMsg) filterNot (_ == "") mkString ("(", ", ", ")") + + intp.reporter.printMessage("%2d) %-30s %s%s".format( + idx + 1, + handler.importString, + statsMsg, + foundMsg + )) + } + } + + private def findToolsJar() = { + val jdkPath = Directory(jdkHome) + val jar = jdkPath / "lib" / "tools.jar" toFile + + if (jar isFile) + Some(jar) + else if (jdkPath.isDirectory) + jdkPath.deepFiles find (_.name == "tools.jar") + else None + } + private def addToolsJarToLoader() = { + val cl = findToolsJar() match { + case Some(tools) => ScalaClassLoader.fromURLs(Seq(tools.toURL), intp.classLoader) + case _ => intp.classLoader + } + if (Javap.isAvailable(cl)) { + repldbg(":javap available.") + cl + } + else { + repldbg(":javap unavailable: no tools.jar at " + jdkHome) + intp.classLoader + } + } + + protected def newJavap() = + JavapClass(addToolsJarToLoader(), new IMain.ReplStrippingWriter(intp), Some(intp)) + + private lazy val javap = substituteAndLog[Javap]("javap", NoJavap)(newJavap()) + + // Still todo: modules. + private def typeCommand(line0: String): Result = { + line0.trim match { + case "" => ":type [-v] <expression>" + case s if s startsWith "-v " => intp.typeCommandInternal(s stripPrefix "-v " trim, verbose = true) + case s => intp.typeCommandInternal(s, verbose = false) + } + } + + private def warningsCommand(): Result = { + if (intp.lastWarnings.isEmpty) + "Can't find any cached warnings." + else + intp.lastWarnings foreach { case (pos, msg) => intp.reporter.warning(pos, msg) } + } + + private def javapCommand(line: String): Result = { + if (javap == null) + ":javap unavailable, no tools.jar at %s. Set JDK_HOME.".format(jdkHome) + else if (line == "") + ":javap [-lcsvp] [path1 path2 ...]" + else + javap(words(line)) foreach { res => + if (res.isError) return "Failed: " + res.value + else res.show() + } + } + + private def pathToPhaseWrapper = intp.originalPath("$r") + ".phased.atCurrent" + + private def phaseCommand(name: String): Result = { + val phased: Phased = power.phased + import phased.NoPhaseName + + if (name == "clear") { + phased.set(NoPhaseName) + intp.clearExecutionWrapper() + "Cleared active phase." + } + else if (name == "") phased.get match { + case NoPhaseName => "Usage: :phase <expr> (e.g. typer, erasure.next, erasure+3)" + case ph => "Active phase is '%s'. (To clear, :phase clear)".format(phased.get) + } + else { + val what = phased.parse(name) + if (what.isEmpty || !phased.set(what)) + "'" + name + "' does not appear to represent a valid phase." + else { + intp.setExecutionWrapper(pathToPhaseWrapper) + val activeMessage = + if (what.toString.length == name.length) "" + what + else "%s (%s)".format(what, name) + + "Active phase is now: " + activeMessage + } + } + } + + /** Available commands */ + def commands: List[LoopCommand] = standardCommands ++ ( + if (isReplPower) powerCommands else Nil + ) + + val replayQuestionMessage = + """|That entry seems to have slain the compiler. Shall I replay + |your session? I can re-run each line except the last one. + |[y/n] + """.trim.stripMargin + + private val crashRecovery: PartialFunction[Throwable, Boolean] = { + case ex: Throwable => + echo(intp.global.throwableAsString(ex)) + + ex match { + case _: NoSuchMethodError | _: NoClassDefFoundError => + echo("\nUnrecoverable error.") + throw ex + case _ => + def fn(): Boolean = + try in.readYesOrNo(replayQuestionMessage, { echo("\nYou must enter y or n.") ; fn() }) + catch { case _: RuntimeException => false } + + if (fn()) replay() + else echo("\nAbandoning crashed session.") + } + true + } + + // return false if repl should exit + def processLine(line: String): Boolean = { + import scala.concurrent.duration._ + Await.ready(globalFuture, 60.seconds) + + (line ne null) && (command(line) match { + case Result(false, _) => false + case Result(_, Some(line)) => addReplay(line) ; true + case _ => true + }) + } + + private def readOneLine() = { + out.flush() + in readLine prompt + } + + /** The main read-eval-print loop for the repl. It calls + * command() for each line of input, and stops when + * command() returns false. + */ + @tailrec final def loop() { + if ( try processLine(readOneLine()) catch crashRecovery ) + loop() + } + + /** interpret all lines from a specified file */ + def interpretAllFrom(file: File) { + savingReader { + savingReplayStack { + file applyReader { reader => + in = SimpleReader(reader, out, interactive = false) + echo("Loading " + file + "...") + loop() + } + } + } + } + + /** create a new interpreter and replay the given commands */ + def replay() { + reset() + if (replayCommandStack.isEmpty) + echo("Nothing to replay.") + else for (cmd <- replayCommands) { + echo("Replaying: " + cmd) // flush because maybe cmd will have its own output + command(cmd) + echo("") + } + } + def resetCommand() { + echo("Resetting interpreter state.") + if (replayCommandStack.nonEmpty) { + echo("Forgetting this session history:\n") + replayCommands foreach echo + echo("") + replayCommandStack = Nil + } + if (intp.namedDefinedTerms.nonEmpty) + echo("Forgetting all expression results and named terms: " + intp.namedDefinedTerms.mkString(", ")) + if (intp.definedTypes.nonEmpty) + echo("Forgetting defined types: " + intp.definedTypes.mkString(", ")) + + reset() + } + def reset() { + intp.reset() + unleashAndSetPhase() + } + + /** fork a shell and run a command */ + lazy val shCommand = new LoopCommand("sh", "run a shell command (result is implicitly => List[String])") { + override def usage = "<command line>" + def apply(line: String): Result = line match { + case "" => showUsage() + case _ => + val toRun = classOf[ProcessResult].getName + "(" + string2codeQuoted(line) + ")" + intp interpret toRun + () + } + } + + def withFile(filename: String)(action: File => Unit) { + val f = File(filename) + + if (f.exists) action(f) + else echo("That file does not exist") + } + + def loadCommand(arg: String) = { + var shouldReplay: Option[String] = None + withFile(arg)(f => { + interpretAllFrom(f) + shouldReplay = Some(":load " + arg) + }) + Result(keepRunning = true, shouldReplay) + } + + def addClasspath(arg: String): Unit = { + val f = File(arg).normalize + if (f.exists) { + addedClasspath = ClassPath.join(addedClasspath, f.path) + val totalClasspath = ClassPath.join(settings.classpath.value, addedClasspath) + echo("Added '%s'. Your new classpath is:\n\"%s\"".format(f.path, totalClasspath)) + replay() + } + else echo("The path '" + f + "' doesn't seem to exist.") + } + + def powerCmd(): Result = { + if (isReplPower) "Already in power mode." + else enablePowerMode(isDuringInit = false) + } + def enablePowerMode(isDuringInit: Boolean) = { + replProps.power setValue true + unleashAndSetPhase() + asyncEcho(isDuringInit, power.banner) + } + private def unleashAndSetPhase() { + if (isReplPower) { + power.unleash() + // Set the phase to "typer" + intp beSilentDuring phaseCommand("typer") + } + } + + def asyncEcho(async: Boolean, msg: => String) { + if (async) asyncMessage(msg) + else echo(msg) + } + + def verbosity() = { + val old = intp.printResults + intp.printResults = !old + echo("Switched " + (if (old) "off" else "on") + " result printing.") + } + + /** Run one command submitted by the user. Two values are returned: + * (1) whether to keep running, (2) the line to record for replay, + * if any. */ + def command(line: String): Result = { + if (line startsWith ":") { + val cmd = line.tail takeWhile (x => !x.isWhitespace) + uniqueCommand(cmd) match { + case Some(lc) => lc(line.tail stripPrefix cmd dropWhile (_.isWhitespace)) + case _ => ambiguousError(cmd) + } + } + else if (intp.global == null) Result(keepRunning = false, None) // Notice failure to create compiler + else Result(keepRunning = true, interpretStartingWith(line)) + } + + private def readWhile(cond: String => Boolean) = { + Iterator continually in.readLine("") takeWhile (x => x != null && cond(x)) + } + + def pasteCommand(): Result = { + echo("// Entering paste mode (ctrl-D to finish)\n") + val code = readWhile(_ => true) mkString "\n" + echo("\n// Exiting paste mode, now interpreting.\n") + intp interpret code + () + } + + private object paste extends Pasted { + val ContinueString = " | " + val PromptString = "scala> " + + def interpret(line: String): Unit = { + echo(line.trim) + intp interpret line + echo("") + } + + def transcript(start: String) = { + echo("\n// Detected repl transcript paste: ctrl-D to finish.\n") + apply(Iterator(start) ++ readWhile(_.trim != PromptString.trim)) + } + } + import paste.{ ContinueString, PromptString } + + /** Interpret expressions starting with the first line. + * Read lines until a complete compilation unit is available + * or until a syntax error has been seen. If a full unit is + * read, go ahead and interpret it. Return the full string + * to be recorded for replay, if any. + */ + def interpretStartingWith(code: String): Option[String] = { + // signal completion non-completion input has been received + in.completion.resetVerbosity() + + def reallyInterpret = { + val reallyResult = intp.interpret(code) + (reallyResult, reallyResult match { + case IR.Error => None + case IR.Success => Some(code) + case IR.Incomplete => + if (in.interactive && code.endsWith("\n\n")) { + echo("You typed two blank lines. Starting a new command.") + None + } + else in.readLine(ContinueString) match { + case null => + // we know compilation is going to fail since we're at EOF and the + // parser thinks the input is still incomplete, but since this is + // a file being read non-interactively we want to fail. So we send + // it straight to the compiler for the nice error message. + intp.compileString(code) + None + + case line => interpretStartingWith(code + "\n" + line) + } + }) + } + + /** Here we place ourselves between the user and the interpreter and examine + * the input they are ostensibly submitting. We intervene in several cases: + * + * 1) If the line starts with "scala> " it is assumed to be an interpreter paste. + * 2) If the line starts with "." (but not ".." or "./") it is treated as an invocation + * on the previous result. + * 3) If the Completion object's execute returns Some(_), we inject that value + * and avoid the interpreter, as it's likely not valid scala code. + */ + if (code == "") None + else if (!paste.running && code.trim.startsWith(PromptString)) { + paste.transcript(code) + None + } + else if (Completion.looksLikeInvocation(code) && intp.mostRecentVar != "") { + interpretStartingWith(intp.mostRecentVar + code) + } + else if (code.trim startsWith "//") { + // line comment, do nothing + None + } + else + reallyInterpret._2 + } + + // runs :load `file` on any files passed via -i + def loadFiles(settings: Settings) = settings match { + case settings: GenericRunnerSettings => + for (filename <- settings.loadfiles.value) { + val cmd = ":load " + filename + command(cmd) + addReplay(cmd) + echo("") + } + case _ => + } + + /** Tries to create a JLineReader, falling back to SimpleReader: + * unless settings or properties are such that it should start + * with SimpleReader. + */ + def chooseReader(settings: Settings): InteractiveReader = { + if (settings.Xnojline.value || Properties.isEmacsShell) + SimpleReader() + else try new JLineReader( + if (settings.noCompletion.value) NoCompletion + else new JLineCompletion(intp) + ) + catch { + case ex @ (_: Exception | _: NoClassDefFoundError) => + echo("Failed to created JLineReader: " + ex + "\nFalling back to SimpleReader.") + SimpleReader() + } + } + + private def loopPostInit() { + in match { + case x: JLineReader => x.consoleReader.postInit + case _ => + } + // Bind intp somewhere out of the regular namespace where + // we can get at it in generated code. + intp.quietBind(NamedParam[IMain]("$intp", intp)(tagOfIMain, classTag[IMain])) + // Auto-run code via some setting. + ( replProps.replAutorunCode.option + flatMap (f => io.File(f).safeSlurp()) + foreach (intp quietRun _) + ) + // classloader and power mode setup + intp.setContextClassLoader() + if (isReplPower) { + replProps.power setValue true + unleashAndSetPhase() + asyncMessage(power.banner) + } + } + def process(settings: Settings): Boolean = savingContextLoader { + this.settings = settings + createInterpreter() + + // sets in to some kind of reader depending on environmental cues + in = in0.fold(chooseReader(settings))(r => SimpleReader(r, out, interactive = true)) + globalFuture = future { + intp.initializeSynchronous() + loopPostInit() + loadFiles(settings) + !intp.reporter.hasErrors + } + printWelcome() + + try loop() + catch AbstractOrMissingHandler() + finally closeInterpreter() + + true + } + + @deprecated("Use `process` instead", "2.9.0") + def main(settings: Settings): Unit = process(settings) //used by sbt +} + +object ILoop { + implicit def loopToInterpreter(repl: ILoop): IMain = repl.intp + + // Designed primarily for use by test code: take a String with a + // bunch of code, and prints out a transcript of what it would look + // like if you'd just typed it into the repl. + def runForTranscript(code: String, settings: Settings): String = { + import java.io.{ BufferedReader, StringReader, OutputStreamWriter } + + stringFromStream { ostream => + Console.withOut(ostream) { + val output = new JPrintWriter(new OutputStreamWriter(ostream), true) { + override def write(str: String) = { + // completely skip continuation lines + if (str forall (ch => ch.isWhitespace || ch == '|')) () + // print a newline on empty scala prompts + else if ((str contains '\n') && (str.trim == "scala> ")) super.write("\n") + else super.write(str) + } + } + val input = new BufferedReader(new StringReader(code)) { + override def readLine(): String = { + val s = super.readLine() + // helping out by printing the line being interpreted. + if (s != null) + output.println(s) + s + } + } + val repl = new ILoop(input, output) + if (settings.classpath.isDefault) + settings.classpath.value = sys.props("java.class.path") + + repl process settings + } + } + } + + /** Creates an interpreter loop with default settings and feeds + * the given code to it as input. + */ + def run(code: String, sets: Settings = new Settings): String = { + import java.io.{ BufferedReader, StringReader, OutputStreamWriter } + + stringFromStream { ostream => + Console.withOut(ostream) { + val input = new BufferedReader(new StringReader(code)) + val output = new JPrintWriter(new OutputStreamWriter(ostream), true) + val repl = new ILoop(input, output) + + if (sets.classpath.isDefault) + sets.classpath.value = sys.props("java.class.path") + + repl process sets + } + } + } + def run(lines: List[String]): String = run(lines map (_ + "\n") mkString) +} diff --git a/src/repl/scala/tools/nsc/interpreter/IMain.scala b/src/repl/scala/tools/nsc/interpreter/IMain.scala new file mode 100644 index 0000000000..cb2e3c32b8 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/IMain.scala @@ -0,0 +1,1122 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package interpreter + +import Predef.{ println => _, _ } +import util.stringFromWriter +import scala.reflect.internal.util._ +import java.net.URL +import scala.sys.BooleanProp +import scala.tools.nsc.io.AbstractFile +import reporters._ +import scala.tools.util.PathResolver +import scala.tools.nsc.util.ScalaClassLoader +import scala.tools.nsc.typechecker.{ TypeStrings, StructuredTypeStrings } +import ScalaClassLoader.URLClassLoader +import scala.tools.nsc.util.Exceptional.unwrap +import scala.collection.{ mutable, immutable } +import IMain._ +import java.util.concurrent.Future +import scala.reflect.runtime.{ universe => ru } +import scala.reflect.{ ClassTag, classTag } +import StdReplTags._ + +/** 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 members called "$eval" and "$print". 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 IMain(initialSettings: Settings, protected val out: JPrintWriter) extends Imports { + imain => + + object replOutput extends ReplOutput(settings.Yreploutdir) { } + + @deprecated("Use replOutput.dir instead", "2.11.0") + def virtualDirectory = replOutput.dir + // Used in a test case. + def showDirectory() = replOutput.show(out) + + private[nsc] var printResults = true // whether to print result lines + private[nsc] var totalSilence = false // whether to print anything + private var _initializeComplete = false // compiler is initialized + private var _isInitialized: Future[Boolean] = null // set up initialization future + private var bindExceptions = true // whether to bind the lastException variable + private var _executionWrapper = "" // code to be wrapped around all lines + + /** We're going to go to some trouble to initialize the compiler asynchronously. + * It's critical that nothing call into it until it's been initialized or we will + * run into unrecoverable issues, but the perceived repl startup time goes + * through the roof if we wait for it. So we initialize it with a future and + * use a lazy val to ensure that any attempt to use the compiler object waits + * on the future. + */ + private var _classLoader: util.AbstractFileClassLoader = null // active classloader + private val _compiler: ReplGlobal = newCompiler(settings, reporter) // our private compiler + + def compilerClasspath: Seq[URL] = ( + if (isInitializeComplete) global.classPath.asURLs + else new PathResolver(settings).result.asURLs // the compiler's classpath + ) + def settings = initialSettings + // Run the code body with the given boolean settings flipped to true. + def withoutWarnings[T](body: => T): T = beQuietDuring { + val saved = settings.nowarn.value + if (!saved) + settings.nowarn.value = true + + try body + finally if (!saved) settings.nowarn.value = false + } + + /** construct an interpreter that reports to Console */ + def this(settings: Settings) = this(settings, new NewLinePrintWriter(new ConsoleWriter, true)) + def this() = this(new Settings()) + + lazy val formatting: Formatting = new Formatting { + val prompt = Properties.shellPromptString + } + lazy val reporter: ReplReporter = new ReplReporter(this) + + import formatting._ + import reporter.{ printMessage, withoutTruncating } + + // This exists mostly because using the reporter too early leads to deadlock. + private def echo(msg: String) { Console println msg } + private def _initSources = List(new BatchSourceFile("<init>", "class $repl_$init { }")) + private def _initialize() = { + try { + // todo. if this crashes, REPL will hang + new _compiler.Run() compileSources _initSources + _initializeComplete = true + true + } + catch AbstractOrMissingHandler() + } + private def tquoted(s: String) = "\"\"\"" + s + "\"\"\"" + private val logScope = scala.sys.props contains "scala.repl.scope" + private def scopelog(msg: String) = if (logScope) Console.err.println(msg) + + // argument is a thunk to execute after init is done + def initialize(postInitSignal: => Unit) { + synchronized { + if (_isInitialized == null) { + _isInitialized = io.spawn { + try _initialize() + finally postInitSignal + } + } + } + } + def initializeSynchronous(): Unit = { + if (!isInitializeComplete) { + _initialize() + assert(global != null, global) + } + } + def isInitializeComplete = _initializeComplete + + /** the public, go through the future compiler */ + lazy val global: Global = { + if (isInitializeComplete) _compiler + else { + // If init hasn't been called yet you're on your own. + if (_isInitialized == null) { + repldbg("Warning: compiler accessed before init set up. Assuming no postInit code.") + initialize(()) + } + // blocks until it is ; false means catastrophic failure + if (_isInitialized.get()) _compiler + else null + } + } + + import global._ + import definitions.{ ObjectClass, termMember, dropNullaryMethod} + + lazy val runtimeMirror = ru.runtimeMirror(classLoader) + + private def noFatal(body: => Symbol): Symbol = try body catch { case _: FatalError => NoSymbol } + + def getClassIfDefined(path: String) = ( + noFatal(runtimeMirror staticClass path) + orElse noFatal(rootMirror staticClass path) + ) + def getModuleIfDefined(path: String) = ( + noFatal(runtimeMirror staticModule path) + orElse noFatal(rootMirror staticModule path) + ) + + implicit class ReplTypeOps(tp: Type) { + def andAlso(fn: Type => Type): Type = if (tp eq NoType) tp else fn(tp) + } + + // TODO: If we try to make naming a lazy val, we run into big time + // scalac unhappiness with what look like cycles. It has not been easy to + // reduce, but name resolution clearly takes different paths. + object naming extends { + val global: imain.global.type = imain.global + } with Naming { + // make sure we don't overwrite their unwisely named res3 etc. + def freshUserTermName(): TermName = { + val name = newTermName(freshUserVarName()) + if (replScope containsName name) freshUserTermName() + else name + } + def isInternalTermName(name: Name) = isInternalVarName("" + name) + } + import naming._ + + object deconstruct extends { + val global: imain.global.type = imain.global + } with StructuredTypeStrings + + lazy val memberHandlers = new { + val intp: imain.type = imain + } with MemberHandlers + import memberHandlers._ + + /** Temporarily be quiet */ + def beQuietDuring[T](body: => T): T = { + val saved = printResults + printResults = false + try body + finally printResults = saved + } + def beSilentDuring[T](operation: => T): T = { + val saved = totalSilence + totalSilence = true + try operation + finally totalSilence = saved + } + + def quietRun[T](code: String) = beQuietDuring(interpret(code)) + + /** takes AnyRef because it may be binding a Throwable or an Exceptional */ + private def withLastExceptionLock[T](body: => T, alt: => T): T = { + assert(bindExceptions, "withLastExceptionLock called incorrectly.") + bindExceptions = false + + try beQuietDuring(body) + catch logAndDiscard("withLastExceptionLock", alt) + finally bindExceptions = true + } + + def executionWrapper = _executionWrapper + def setExecutionWrapper(code: String) = _executionWrapper = code + def clearExecutionWrapper() = _executionWrapper = "" + + /** interpreter settings */ + lazy val isettings = new ISettings(this) + + /** Instantiate a compiler. Overridable. */ + protected def newCompiler(settings: Settings, reporter: Reporter): ReplGlobal = { + settings.outputDirs setSingleOutput replOutput.dir + settings.exposeEmptyPackage.value = true + new Global(settings, reporter) with ReplGlobal { override def toString: String = "<global>" } + } + + /** Parent classloader. Overridable. */ + protected def parentClassLoader: ClassLoader = + settings.explicitParentLoader.getOrElse( this.getClass.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. + */ + def resetClassLoader() = { + repldbg("Setting new classloader: was " + _classLoader) + _classLoader = null + ensureClassLoader() + } + final def ensureClassLoader() { + if (_classLoader == null) + _classLoader = makeClassLoader() + } + def classLoader: util.AbstractFileClassLoader = { + ensureClassLoader() + _classLoader + } + + def backticked(s: String): String = ( + (s split '.').toList map { + case "_" => "_" + case s if nme.keywords(newTermName(s)) => s"`$s`" + case s => s + } mkString "." + ) + + abstract class PhaseDependentOps { + def shift[T](op: => T): T + + def path(name: => Name): String = shift(path(symbolOfName(name))) + def path(sym: Symbol): String = backticked(shift(sym.fullName)) + def sig(sym: Symbol): String = shift(sym.defString) + } + object typerOp extends PhaseDependentOps { + def shift[T](op: => T): T = exitingTyper(op) + } + object flatOp extends PhaseDependentOps { + def shift[T](op: => T): T = exitingFlatten(op) + } + + def originalPath(name: String): String = originalPath(name: TermName) + def originalPath(name: Name): String = typerOp path name + def originalPath(sym: Symbol): String = typerOp path sym + def flatPath(sym: Symbol): String = flatOp shift sym.javaClassName + def translatePath(path: String) = { + val sym = if (path endsWith "$") symbolOfTerm(path.init) else symbolOfIdent(path) + sym match { + case NoSymbol => None + case _ => Some(flatPath(sym)) + } + } + def translateEnclosingClass(n: String) = { + def enclosingClass(s: Symbol): Symbol = + if (s == NoSymbol || s.isClass) s else enclosingClass(s.owner) + enclosingClass(symbolOfTerm(n)) match { + case NoSymbol => None + case c => Some(flatPath(c)) + } + } + + private class TranslatingClassLoader(parent: ClassLoader) extends util.AbstractFileClassLoader(replOutput.dir, parent) { + /** Overridden here to try translating a simple name to the generated + * class name if the original attempt fails. This method is used by + * getResourceAsStream as well as findClass. + */ + override protected def findAbstractFile(name: String): AbstractFile = + super.findAbstractFile(name) match { + case null => translatePath(name) map (super.findAbstractFile(_)) orNull + case file => file + } + } + private def makeClassLoader(): util.AbstractFileClassLoader = + new TranslatingClassLoader(parentClassLoader match { + case null => ScalaClassLoader fromURLs compilerClasspath + case p => new URLClassLoader(compilerClasspath, p) + }) + + // Set the current Java "context" class loader to this interpreter's class loader + def setContextClassLoader() = classLoader.setAsContext() + + def allDefinedNames: List[Name] = exitingTyper(replScope.toList.map(_.name).sorted) + def unqualifiedIds: List[String] = allDefinedNames map (_.decode) sorted + + /** Most recent tree handled which wasn't wholly synthetic. */ + private def mostRecentlyHandledTree: Option[Tree] = { + prevRequests.reverse foreach { req => + req.handlers.reverse foreach { + case x: MemberDefHandler if x.definesValue && !isInternalTermName(x.name) => return Some(x.member) + case _ => () + } + } + None + } + + private def updateReplScope(sym: Symbol, isDefined: Boolean) { + def log(what: String) { + val mark = if (sym.isType) "t " else "v " + val name = exitingTyper(sym.nameString) + val info = cleanTypeAfterTyper(sym) + val defn = sym defStringSeenAs info + + scopelog(f"[$mark$what%6s] $name%-25s $defn%s") + } + if (ObjectClass isSubClass sym.owner) return + // unlink previous + replScope lookupAll sym.name foreach { sym => + log("unlink") + replScope unlink sym + } + val what = if (isDefined) "define" else "import" + log(what) + replScope enter sym + } + + def recordRequest(req: Request) { + if (req == null) + return + + prevRequests += req + + // warning about serially defining companions. It'd be easy + // enough to just redefine them together but that may not always + // be what people want so I'm waiting until I can do it better. + exitingTyper { + req.defines filterNot (s => req.defines contains s.companionSymbol) foreach { newSym => + val oldSym = replScope lookup newSym.name.companionName + if (Seq(oldSym, newSym).permutations exists { case Seq(s1, s2) => s1.isClass && s2.isModule }) { + replwarn(s"warning: previously defined $oldSym is not a companion to $newSym.") + replwarn("Companions must be defined together; you may wish to use :paste mode for this.") + } + } + } + exitingTyper { + req.imports foreach (sym => updateReplScope(sym, isDefined = false)) + req.defines foreach (sym => updateReplScope(sym, isDefined = true)) + } + } + + private[nsc] def replwarn(msg: => String) { + if (!settings.nowarnings.value) + printMessage(msg) + } + + def compileSourcesKeepingRun(sources: SourceFile*) = { + val run = new Run() + reporter.reset() + run compileSources sources.toList + (!reporter.hasErrors, run) + } + + /** Compile an nsc SourceFile. Returns true if there are + * no compilation errors, or false otherwise. + */ + def compileSources(sources: SourceFile*): Boolean = + compileSourcesKeepingRun(sources: _*)._1 + + /** Compile a string. Returns true if there are no + * compilation errors, or false otherwise. + */ + def compileString(code: String): Boolean = + compileSources(new BatchSourceFile("<script>", code)) + + /** Build a request from the user. `trees` is `line` after being parsed. + */ + private def buildRequest(line: String, trees: List[Tree]): Request = { + executingRequest = new Request(line, trees) + executingRequest + } + + private def safePos(t: Tree, alt: Int): Int = + try t.pos.startOrPoint + catch { case _: UnsupportedOperationException => alt } + + // Given an expression like 10 * 10 * 10 we receive the parent tree positioned + // at a '*'. So look at each subtree and find the earliest of all positions. + private def earliestPosition(tree: Tree): Int = { + var pos = Int.MaxValue + tree foreach { t => + pos = math.min(pos, safePos(t, Int.MaxValue)) + } + pos + } + + private def requestFromLine(line: String, synthetic: Boolean): Either[IR.Result, Request] = { + val content = indentCode(line) + val trees = parse(content) match { + case None => return Left(IR.Incomplete) + case Some(Nil) => return Left(IR.Error) // parse error or empty input + case Some(trees) => trees + } + repltrace( + trees map (t => { + // [Eugene to Paul] previously it just said `t map ...` + // because there was an implicit conversion from Tree to a list of Trees + // however Martin and I have removed the conversion + // (it was conflicting with the new reflection API), + // so I had to rewrite this a bit + val subs = t collect { case sub => sub } + subs map (t0 => + " " + safePos(t0, -1) + ": " + t0.shortClass + "\n" + ) mkString "" + }) mkString "\n" + ) + // If the last tree is a bare expression, pinpoint where it begins using the + // AST node position and snap the line off there. Rewrite the code embodied + // by the last tree as a ValDef instead, so we can access the value. + trees.last match { + case _:Assign => // we don't want to include assignments + case _:TermTree | _:Ident | _:Select => // ... but do want other unnamed terms. + val varName = if (synthetic) freshInternalVarName() else freshUserVarName() + val rewrittenLine = ( + // In theory this would come out the same without the 1-specific test, but + // it's a cushion against any more sneaky parse-tree position vs. code mismatches: + // this way such issues will only arise on multiple-statement repl input lines, + // which most people don't use. + if (trees.size == 1) "val " + varName + " =\n" + content + else { + // The position of the last tree + val lastpos0 = earliestPosition(trees.last) + // Oh boy, the parser throws away parens so "(2+2)" is mispositioned, + // with increasingly hard to decipher positions as we move on to "() => 5", + // (x: Int) => x + 1, and more. So I abandon attempts to finesse and just + // look for semicolons and newlines, which I'm sure is also buggy. + val (raw1, raw2) = content splitAt lastpos0 + repldbg("[raw] " + raw1 + " <---> " + raw2) + + val adjustment = (raw1.reverse takeWhile (ch => (ch != ';') && (ch != '\n'))).size + val lastpos = lastpos0 - adjustment + + // the source code split at the laboriously determined position. + val (l1, l2) = content splitAt lastpos + repldbg("[adj] " + l1 + " <---> " + l2) + + val prefix = if (l1.trim == "") "" else l1 + ";\n" + // Note to self: val source needs to have this precise structure so that + // error messages print the user-submitted part without the "val res0 = " part. + val combined = prefix + "val " + varName + " =\n" + l2 + + repldbg(List( + " line" -> line, + " content" -> content, + " was" -> l2, + "combined" -> combined) map { + case (label, s) => label + ": '" + s + "'" + } mkString "\n" + ) + combined + } + ) + // Rewriting "foo ; bar ; 123" + // to "foo ; bar ; val resXX = 123" + requestFromLine(rewrittenLine, synthetic) match { + case Right(req) => return Right(req withOriginalLine line) + case x => return x + } + case _ => + } + Right(buildRequest(line, trees)) + } + + // dealias non-public types so we don't see protected aliases like Self + def dealiasNonPublic(tp: Type) = tp match { + case TypeRef(_, sym, _) if sym.isAliasType && !sym.isPublic => tp.dealias + case _ => tp + } + + /** + * 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. + * + * The return value is whether the line was interpreter successfully, + * e.g. that there were no parse errors. + */ + def interpret(line: String): IR.Result = interpret(line, synthetic = false) + def interpretSynthetic(line: String): IR.Result = interpret(line, synthetic = true) + def interpret(line: String, synthetic: Boolean): IR.Result = { + def loadAndRunReq(req: Request) = { + classLoader.setAsContext() + val (result, succeeded) = req.loadAndRun + + /** To our displeasure, ConsoleReporter offers only printMessage, + * which tacks a newline on the end. Since that breaks all the + * output checking, we have to take one off to balance. + */ + if (succeeded) { + if (printResults && result != "") + printMessage(result stripSuffix "\n") + else if (isReplDebug) // show quiet-mode activity + printMessage(result.trim.lines map ("[quiet] " + _) mkString "\n") + + // Book-keeping. Have to record synthetic requests too, + // as they may have been issued for information, e.g. :type + recordRequest(req) + IR.Success + } + else { + // don't truncate stack traces + withoutTruncating(printMessage(result)) + IR.Error + } + } + + if (global == null) IR.Error + else requestFromLine(line, synthetic) match { + case Left(result) => result + case Right(req) => + // null indicates a disallowed statement type; otherwise compile and + // fail if false (implying e.g. a type error) + if (req == null || !req.compile) IR.Error + else loadAndRunReq(req) + } + } + + /** 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, modifiers: List[String] = Nil): IR.Result = { + val bindRep = new ReadEvalPrint() + bindRep.compile(""" + |object %s { + | var value: %s = _ + | def set(x: Any) = value = x.asInstanceOf[%s] + |} + """.stripMargin.format(bindRep.evalName, boundType, boundType) + ) + bindRep.callEither("set", value) match { + case Left(ex) => + repldbg("Set failed in bind(%s, %s, %s)".format(name, boundType, value)) + repldbg(util.stackTraceString(ex)) + IR.Error + + case Right(_) => + val line = "%sval %s = %s.value".format(modifiers map (_ + " ") mkString, name, bindRep.evalPath) + repldbg("Interpreting: " + line) + interpret(line) + } + } + def directBind(name: String, boundType: String, value: Any): IR.Result = { + val result = bind(name, boundType, value) + if (result == IR.Success) + directlyBoundNames += newTermName(name) + result + } + def directBind(p: NamedParam): IR.Result = directBind(p.name, p.tpe, p.value) + def directBind[T: ru.TypeTag : ClassTag](name: String, value: T): IR.Result = directBind((name, value)) + + def rebind(p: NamedParam): IR.Result = { + val name = p.name + val newType = p.tpe + val tempName = freshInternalVarName() + + quietRun("val %s = %s".format(tempName, name)) + quietRun("val %s = %s.asInstanceOf[%s]".format(name, tempName, newType)) + } + def quietBind(p: NamedParam): IR.Result = beQuietDuring(bind(p)) + def bind(p: NamedParam): IR.Result = bind(p.name, p.tpe, p.value) + def bind[T: ru.TypeTag : ClassTag](name: String, value: T): IR.Result = bind((name, value)) + + /** Reset this interpreter, forgetting all user-specified requests. */ + def reset() { + clearExecutionWrapper() + resetClassLoader() + resetAllCreators() + prevRequests.clear() + resetReplScope() + replOutput.dir.clear() + } + + /** This instance is no longer needed, so release any resources + * it is using. The reporter's output gets flushed. + */ + def close() { + reporter.flush() + } + + /** Here is where we: + * + * 1) Read some source code, and put it in the "read" object. + * 2) Evaluate the read object, and put the result in the "eval" object. + * 3) Create a String for human consumption, and put it in the "print" object. + * + * Read! Eval! Print! Some of that not yet centralized here. + */ + class ReadEvalPrint(lineId: Int) { + def this() = this(freshLineId()) + + val packageName = sessionNames.line + lineId + val readName = sessionNames.read + val evalName = sessionNames.eval + val printName = sessionNames.print + val resultName = sessionNames.result + + def bindError(t: Throwable) = { + if (!bindExceptions) // avoid looping if already binding + throw t + + val unwrapped = unwrap(t) + withLastExceptionLock[String]({ + directBind[Throwable]("lastException", unwrapped)(tagOfThrowable, classTag[Throwable]) + util.stackTraceString(unwrapped) + }, util.stackTraceString(unwrapped)) + } + + // TODO: split it out into a package object and a regular + // object and we can do that much less wrapping. + def packageDecl = "package " + packageName + + def pathTo(name: String) = packageName + "." + name + def packaged(code: String) = packageDecl + "\n\n" + code + + def readPath = pathTo(readName) + def evalPath = pathTo(evalName) + + def call(name: String, args: Any*): AnyRef = { + val m = evalMethod(name) + repldbg("Invoking: " + m) + if (args.nonEmpty) + repldbg(" with args: " + args.mkString(", ")) + + m.invoke(evalClass, args.map(_.asInstanceOf[AnyRef]): _*) + } + + def callEither(name: String, args: Any*): Either[Throwable, AnyRef] = + try Right(call(name, args: _*)) + catch { case ex: Throwable => Left(ex) } + + class EvalException(msg: String, cause: Throwable) extends RuntimeException(msg, cause) { } + + private def evalError(path: String, ex: Throwable) = + throw new EvalException("Failed to load '" + path + "': " + ex.getMessage, ex) + + private def load(path: String): Class[_] = { + try Class.forName(path, true, classLoader) + catch { case ex: Throwable => evalError(path, unwrap(ex)) } + } + + lazy val evalClass = load(evalPath) + + def compile(source: String): Boolean = compileAndSaveRun("<console>", source) + + /** The innermost object inside the wrapper, found by + * following accessPath into the outer one. + */ + def resolvePathToSymbol(accessPath: String): Symbol = { + val readRoot = getModuleIfDefined(readPath) // the outermost wrapper + (accessPath split '.').foldLeft(readRoot: Symbol) { + case (sym, "") => sym + case (sym, name) => exitingTyper(termMember(sym, name)) + } + } + /** We get a bunch of repeated warnings for reasons I haven't + * entirely figured out yet. For now, squash. + */ + private def updateRecentWarnings(run: Run) { + def loop(xs: List[(Position, String)]): List[(Position, String)] = xs match { + case Nil => Nil + case ((pos, msg)) :: rest => + val filtered = rest filter { case (pos0, msg0) => + (msg != msg0) || (pos.lineContent.trim != pos0.lineContent.trim) || { + // same messages and same line content after whitespace removal + // but we want to let through multiple warnings on the same line + // from the same run. The untrimmed line will be the same since + // there's no whitespace indenting blowing it. + (pos.lineContent == pos0.lineContent) + } + } + ((pos, msg)) :: loop(filtered) + } + val warnings = loop(run.allConditionalWarnings flatMap (_.warnings)) + if (warnings.nonEmpty) + mostRecentWarnings = warnings + } + private def evalMethod(name: String) = evalClass.getMethods filter (_.getName == name) match { + case Array(method) => method + case xs => sys.error("Internal error: eval object " + evalClass + ", " + xs.mkString("\n", "\n", "")) + } + private def compileAndSaveRun(label: String, code: String) = { + showCodeIfDebugging(code) + val (success, run) = compileSourcesKeepingRun(new BatchSourceFile(label, packaged(code))) + updateRecentWarnings(run) + success + } + } + + /** One line of code submitted by the user for interpretation */ + class Request(val line: String, val trees: List[Tree]) { + def defines = defHandlers flatMap (_.definedSymbols) + def imports = importedSymbols + def value = Some(handlers.last) filter (h => h.definesValue) map (h => definedSymbols(h.definesTerm.get)) getOrElse NoSymbol + + val lineRep = new ReadEvalPrint() + + private var _originalLine: String = null + def withOriginalLine(s: String): this.type = { _originalLine = s ; this } + def originalLine = if (_originalLine == null) line else _originalLine + + /** handlers for each tree in this request */ + val handlers: List[MemberHandler] = trees map (memberHandlers chooseHandler _) + def defHandlers = handlers collect { case x: MemberDefHandler => x } + + /** list of names used by this expression */ + val referencedNames: List[Name] = handlers flatMap (_.referencedNames) + + /** def and val names */ + def termNames = handlers flatMap (_.definesTerm) + def typeNames = handlers flatMap (_.definesType) + def importedSymbols = handlers flatMap { + case x: ImportHandler => x.importedSymbols + case _ => Nil + } + + /** Code to import bound names from previous lines - accessPath is code to + * append to objectName to access anything bound by request. + */ + val ComputedImports(importsPreamble, importsTrailer, accessPath) = + exitingTyper(importsCode(referencedNames.toSet)) + + /** the line of code to compute */ + def toCompute = line + + def fullPath(vname: String) = s"${lineRep.readPath}$accessPath.`$vname`" + + /** generate the source code for the object that computes this request */ + private object ObjectSourceCode extends CodeAssembler[MemberHandler] { + def path = originalPath("$intp") + def envLines = { + if (!isReplPower) Nil // power mode only for now + // $intp is not bound; punt, but include the line. + else if (path == "$intp") List( + "def $line = " + tquoted(originalLine), + "def $trees = Nil" + ) + else List( + "def $line = " + tquoted(originalLine), + "def $trees = Nil" + ) + } + + val preamble = """ + |object %s { + |%s%s%s + """.stripMargin.format(lineRep.readName, envLines.map(" " + _ + ";\n").mkString, importsPreamble, indentCode(toCompute)) + val postamble = importsTrailer + "\n}" + val generate = (m: MemberHandler) => m extraCodeToEvaluate Request.this + } + + private object ResultObjectSourceCode extends CodeAssembler[MemberHandler] { + /** We only want to generate this code when the result + * is a value which can be referred to as-is. + */ + val evalResult = Request.this.value match { + case NoSymbol => "" + case sym => "lazy val %s = %s".format(lineRep.resultName, originalPath(sym)) + } + // first line evaluates object to make sure constructor is run + // initial "" so later code can uniformly be: + etc + val preamble = """ + |object %s { + | %s + | val %s: String = %s { + | %s + | ("" + """.stripMargin.format( + lineRep.evalName, evalResult, lineRep.printName, + executionWrapper, lineRep.readName + accessPath + ) + + val postamble = """ + | ) + | } + |} + """.stripMargin + val generate = (m: MemberHandler) => m resultExtractionCode Request.this + } + + /** Compile the object file. Returns whether the compilation succeeded. + * If all goes well, the "types" map is computed. */ + lazy val compile: Boolean = { + // error counting is wrong, hence interpreter may overlook failure - so we reset + reporter.reset() + + // compile the object containing the user's code + lineRep.compile(ObjectSourceCode(handlers)) && { + // extract and remember types + typeOf + typesOfDefinedTerms + + // Assign symbols to the original trees + // TODO - just use the new trees. + defHandlers foreach { dh => + val name = dh.member.name + definedSymbols get name foreach { sym => + dh.member setSymbol sym + repldbg("Set symbol of " + name + " to " + symbolDefString(sym)) + } + } + + // compile the result-extraction object + withoutWarnings(lineRep compile ResultObjectSourceCode(handlers)) + } + } + + lazy val resultSymbol = lineRep.resolvePathToSymbol(accessPath) + def applyToResultMember[T](name: Name, f: Symbol => T) = exitingTyper(f(resultSymbol.info.nonPrivateDecl(name))) + + /* typeOf lookup with encoding */ + def lookupTypeOf(name: Name) = typeOf.getOrElse(name, typeOf(global.encode(name.toString))) + + private def typeMap[T](f: Type => T) = + mapFrom[Name, Name, T](termNames ++ typeNames)(x => f(cleanMemberDecl(resultSymbol, x))) + + /** Types of variables defined by this request. */ + lazy val compilerTypeOf = typeMap[Type](x => x) withDefaultValue NoType + /** String representations of same. */ + lazy val typeOf = typeMap[String](tp => exitingTyper(tp.toString)) + + lazy val definedSymbols = ( + termNames.map(x => x -> applyToResultMember(x, x => x)) ++ + typeNames.map(x => x -> compilerTypeOf(x).typeSymbolDirect) + ).toMap[Name, Symbol] withDefaultValue NoSymbol + + lazy val typesOfDefinedTerms = mapFrom[Name, Name, Type](termNames)(x => applyToResultMember(x, _.tpe)) + + /** load and run the code using reflection */ + def loadAndRun: (String, Boolean) = { + try { ("" + (lineRep call sessionNames.print), true) } + catch { case ex: Throwable => (lineRep.bindError(ex), false) } + } + + override def toString = "Request(line=%s, %s trees)".format(line, trees.size) + } + + /** Returns the name of the most recent interpreter result. + * Mostly this exists so you can conveniently invoke methods on + * the previous result. + */ + def mostRecentVar: String = + if (mostRecentlyHandledTree.isEmpty) "" + else "" + (mostRecentlyHandledTree.get match { + case x: ValOrDefDef => x.name + case Assign(Ident(name), _) => name + case ModuleDef(_, name, _) => name + case _ => naming.mostRecentVar + }) + + private var mostRecentWarnings: List[(global.Position, String)] = Nil + def lastWarnings = mostRecentWarnings + + private lazy val importToGlobal = global mkImporter ru + private lazy val importToRuntime = ru mkImporter global + private lazy val javaMirror = ru.rootMirror match { + case x: ru.JavaMirror => x + case _ => null + } + private implicit def importFromRu(sym: ru.Symbol): Symbol = importToGlobal importSymbol sym + private implicit def importToRu(sym: Symbol): ru.Symbol = importToRuntime importSymbol sym + + def classOfTerm(id: String): Option[JClass] = symbolOfTerm(id) match { + case NoSymbol => None + case sym => Some(javaMirror runtimeClass importToRu(sym).asClass) + } + + def typeOfTerm(id: String): Type = symbolOfTerm(id).tpe + + def valueOfTerm(id: String): Option[Any] = exitingTyper { + def value() = { + val sym0 = symbolOfTerm(id) + val sym = (importToRuntime importSymbol sym0).asTerm + val module = runtimeMirror.reflectModule(sym.owner.companionSymbol.asModule).instance + val module1 = runtimeMirror.reflect(module) + val invoker = module1.reflectField(sym) + + invoker.get + } + + try Some(value()) catch { case _: Exception => None } + } + + /** It's a bit of a shotgun approach, but for now we will gain in + * robustness. Try a symbol-producing operation at phase typer, and + * if that is NoSymbol, try again at phase flatten. I'll be able to + * lose this and run only from exitingTyper as soon as I figure out + * exactly where a flat name is sneaking in when calculating imports. + */ + def tryTwice(op: => Symbol): Symbol = exitingTyper(op) orElse exitingFlatten(op) + + def symbolOfIdent(id: String): Symbol = symbolOfType(id) orElse symbolOfTerm(id) + def symbolOfType(id: String): Symbol = tryTwice(replScope lookup (id: TypeName)) + def symbolOfTerm(id: String): Symbol = tryTwice(replScope lookup (id: TermName)) + def symbolOfName(id: Name): Symbol = replScope lookup id + + def runtimeClassAndTypeOfTerm(id: String): Option[(JClass, Type)] = { + classOfTerm(id) flatMap { clazz => + clazz.supers find (!_.isScalaAnonymous) map { nonAnon => + (nonAnon, runtimeTypeOfTerm(id)) + } + } + } + + def runtimeTypeOfTerm(id: String): Type = { + typeOfTerm(id) andAlso { tpe => + val clazz = classOfTerm(id) getOrElse { return NoType } + val staticSym = tpe.typeSymbol + val runtimeSym = getClassIfDefined(clazz.getName) + + if ((runtimeSym != NoSymbol) && (runtimeSym != staticSym) && (runtimeSym isSubClass staticSym)) + runtimeSym.info + else NoType + } + } + + def cleanTypeAfterTyper(sym: => Symbol): Type = { + exitingTyper( + dealiasNonPublic( + dropNullaryMethod( + sym.tpe_* + ) + ) + ) + } + def cleanMemberDecl(owner: Symbol, member: Name): Type = + cleanTypeAfterTyper(owner.info nonPrivateDecl member) + + object exprTyper extends { + val repl: IMain.this.type = imain + } with ExprTyper { } + + def parse(line: String): Option[List[Tree]] = exprTyper.parse(line) + + def symbolOfLine(code: String): Symbol = + exprTyper.symbolOfLine(code) + + def typeOfExpression(expr: String, silent: Boolean = true): Type = + exprTyper.typeOfExpression(expr, silent) + + protected def onlyTerms(xs: List[Name]): List[TermName] = xs collect { case x: TermName => x } + protected def onlyTypes(xs: List[Name]): List[TypeName] = xs collect { case x: TypeName => x } + + def definedTerms = onlyTerms(allDefinedNames) filterNot isInternalTermName + def definedTypes = onlyTypes(allDefinedNames) + def definedSymbolList = prevRequestList flatMap (_.defines) filterNot (s => isInternalTermName(s.name)) + + // Terms with user-given names (i.e. not res0 and not synthetic) + def namedDefinedTerms = definedTerms filterNot (x => isUserVarName("" + x) || directlyBoundNames(x)) + + private var _replScope: Scope = _ + private def resetReplScope() { + _replScope = newScope + } + def replScope = { + if (_replScope eq null) + _replScope = newScope + + _replScope + } + + private var executingRequest: Request = _ + private val prevRequests = mutable.ListBuffer[Request]() + private val directlyBoundNames = mutable.Set[Name]() + + def allHandlers = prevRequestList flatMap (_.handlers) + def lastRequest = if (prevRequests.isEmpty) null else prevRequests.last + def prevRequestList = prevRequests.toList + def importHandlers = allHandlers collect { case x: ImportHandler => x } + + def withoutUnwrapping(op: => Unit): Unit = { + val saved = isettings.unwrapStrings + isettings.unwrapStrings = false + try op + finally isettings.unwrapStrings = saved + } + + def symbolDefString(sym: Symbol) = { + TypeStrings.quieter( + exitingTyper(sym.defString), + sym.owner.name + ".this.", + sym.owner.fullName + "." + ) + } + + def showCodeIfDebugging(code: String) { + /** Secret bookcase entrance for repl debuggers: end the line + * with "// show" and see what's going on. + */ + def isShow = code.lines exists (_.trim endsWith "// show") + if (isReplDebug || isShow) { + beSilentDuring(parse(code)) foreach { ts => + ts foreach { t => + withoutUnwrapping(echo(asCompactString(t))) + } + } + } + } + + // debugging + def debugging[T](msg: String)(res: T) = { + repldbg(msg + " " + res) + res + } +} + +/** Utility methods for the Interpreter. */ +object IMain { + // The two name forms this is catching are the two sides of this assignment: + // + // $line3.$read.$iw.$iw.Bippy = + // $line3.$read$$iw$$iw$Bippy@4a6a00ca + private def removeLineWrapper(s: String) = s.replaceAll("""\$line\d+[./]\$(read|eval|print)[$.]""", "") + private def removeIWPackages(s: String) = s.replaceAll("""\$(iw|read|eval|print)[$.]""", "") + def stripString(s: String) = removeIWPackages(removeLineWrapper(s)) + + trait CodeAssembler[T] { + def preamble: String + def generate: T => String + def postamble: String + + def apply(contributors: List[T]): String = stringFromWriter { code => + code println preamble + contributors map generate foreach (code println _) + code println postamble + } + } + + trait StrippingWriter { + def isStripping: Boolean + def stripImpl(str: String): String + def strip(str: String): String = if (isStripping) stripImpl(str) else str + } + trait TruncatingWriter { + def maxStringLength: Int + def isTruncating: Boolean + def truncate(str: String): String = { + if (isTruncating && (maxStringLength != 0 && str.length > maxStringLength)) + (str take maxStringLength - 3) + "..." + else str + } + } + abstract class StrippingTruncatingWriter(out: JPrintWriter) + extends JPrintWriter(out) + with StrippingWriter + with TruncatingWriter { + self => + + def clean(str: String): String = truncate(strip(str)) + override def write(str: String) = super.write(clean(str)) + } + class ReplStrippingWriter(intp: IMain) extends StrippingTruncatingWriter(intp.out) { + import intp._ + def maxStringLength = isettings.maxPrintString + def isStripping = isettings.unwrapStrings + def isTruncating = reporter.truncationOK + + def stripImpl(str: String): String = naming.unmangle(str) + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/ISettings.scala b/src/repl/scala/tools/nsc/interpreter/ISettings.scala new file mode 100644 index 0000000000..9541d08db1 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/ISettings.scala @@ -0,0 +1,54 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Alexander Spoon + */ + +package scala.tools.nsc +package interpreter + +/** Settings for the interpreter + * + * @version 1.0 + * @author Lex Spoon, 2007/3/24 + **/ +class ISettings(intp: IMain) { + /** 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 = replProps.maxPrintString.option.getOrElse(800) + + /** The maximum number of completion candidates to print for tab + * completion without requiring confirmation. + */ + var maxAutoprintCompletion = 250 + + /** String unwrapping can be disabled if it is causing issues. + * Setting this to false means you will see Strings like "$iw.$iw.". + */ + var unwrapStrings = true + + def deprecation_=(x: Boolean) = { + val old = intp.settings.deprecation.value + intp.settings.deprecation.value = x + if (!old && x) println("Enabled -deprecation output.") + else if (old && !x) println("Disabled -deprecation output.") + } + def deprecation: Boolean = intp.settings.deprecation.value + + def allSettings = Map[String, Any]( + "maxPrintString" -> maxPrintString, + "maxAutoprintCompletion" -> maxAutoprintCompletion, + "unwrapStrings" -> unwrapStrings, + "deprecation" -> deprecation + ) + + private def allSettingsString = + allSettings.toList sortBy (_._1) map { case (k, v) => " " + k + " = " + v + "\n" } mkString + + override def toString = """ + | ISettings { + | %s + | }""".stripMargin.format(allSettingsString) +} diff --git a/src/repl/scala/tools/nsc/interpreter/Imports.scala b/src/repl/scala/tools/nsc/interpreter/Imports.scala new file mode 100644 index 0000000000..ff7bfd432c --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/Imports.scala @@ -0,0 +1,181 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.collection.{ mutable, immutable } + +trait Imports { + self: IMain => + + import global._ + import definitions.{ ObjectClass, ScalaPackage, JavaLangPackage, PredefModule } + import memberHandlers._ + + /** Synthetic import handlers for the language defined imports. */ + private def makeWildcardImportHandler(sym: Symbol): ImportHandler = { + val hd :: tl = sym.fullName.split('.').toList map newTermName + val tree = Import( + tl.foldLeft(Ident(hd): Tree)((x, y) => Select(x, y)), + ImportSelector.wildList + ) + tree setSymbol sym + new ImportHandler(tree) + } + + /** Symbols whose contents are language-defined to be imported. */ + def languageWildcardSyms: List[Symbol] = List(JavaLangPackage, ScalaPackage, PredefModule) + def languageWildcardHandlers = languageWildcardSyms map makeWildcardImportHandler + + def allImportedNames = importHandlers flatMap (_.importedNames) + + /** Types which have been wildcard imported, such as: + * val x = "abc" ; import x._ // type java.lang.String + * import java.lang.String._ // object java.lang.String + * + * Used by tab completion. + * + * XXX right now this gets import x._ and import java.lang.String._, + * but doesn't figure out import String._. There's a lot of ad hoc + * scope twiddling which should be swept away in favor of digging + * into the compiler scopes. + */ + def sessionWildcards: List[Type] = { + importHandlers filter (_.importsWildcard) map (_.targetType) distinct + } + + def languageSymbols = languageWildcardSyms flatMap membersAtPickler + def sessionImportedSymbols = importHandlers flatMap (_.importedSymbols) + def importedSymbols = languageSymbols ++ sessionImportedSymbols + def importedTermSymbols = importedSymbols collect { case x: TermSymbol => x } + + /** Tuples of (source, imported symbols) in the order they were imported. + */ + def importedSymbolsBySource: List[(Symbol, List[Symbol])] = { + val lang = languageWildcardSyms map (sym => (sym, membersAtPickler(sym))) + val session = importHandlers filter (_.targetType != NoType) map { mh => + (mh.targetType.typeSymbol, mh.importedSymbols) + } + + lang ++ session + } + def implicitSymbolsBySource: List[(Symbol, List[Symbol])] = { + importedSymbolsBySource map { + case (k, vs) => (k, vs filter (_.isImplicit)) + } filterNot (_._2.isEmpty) + } + + /** 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. + */ + case class ComputedImports(prepend: String, append: String, access: String) + protected 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] = { + // Single symbol imports might be implicits! See bug #1752. Rather than + // try to finesse this, we will mimic all imports for now. + def keepHandler(handler: MemberHandler) = handler match { + case _: ImportHandler => true + case x => x.definesImplicit || (x.definedNames exists wanted) + } + + reqs match { + case Nil => Nil + case rh :: rest if !keepHandler(rh.handler) => select(rest, wanted) + case rh :: rest => + import rh.handler._ + val newWanted = wanted ++ referencedNames -- definedNames -- importedNames + rh :: select(rest, newWanted) + } + } + + /** Flatten the handlers out and pair each with the original request */ + select(allReqAndHandlers reverseMap { case (r, h) => ReqAndHandler(r, h) }, wanted).reverse + } + + val code, trailingBraces, accessPath = new StringBuilder + val currentImps = mutable.HashSet[Name]() + + // add code for a new object to hold some imports + def addWrapper() { + val impname = nme.INTERPRETER_IMPORT_WRAPPER + code append "object %s {\n".format(impname) + trailingBraces append "}\n" + accessPath append ("." + impname) + currentImps.clear() + } + def maybeWrap(names: Name*) = if (names exists currentImps) addWrapper() + def wrapBeforeAndAfter[T](op: => T): T = { + addWrapper() + try op finally addWrapper() + } + + // loop through previous requests, adding imports for each one + wrapBeforeAndAfter { + for (ReqAndHandler(req, handler) <- reqsToUse) { + handler match { + // If the user entered an import, then just use it; add an import wrapping + // level if the import might conflict with some other import + case x: ImportHandler if x.importsWildcard => + wrapBeforeAndAfter(code append (x.member + "\n")) + case x: ImportHandler => + maybeWrap(x.importedNames: _*) + code append (x.member + "\n") + currentImps ++= x.importedNames + + // For other requests, import each defined name. + // 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. + case x => + for (sym <- x.definedSymbols) { + maybeWrap(sym.name) + code append s"import ${x.path}\n" + currentImps += sym.name + } + } + } + } + + ComputedImports(code.toString, trailingBraces.toString, accessPath.toString) + } + + private def allReqAndHandlers = + prevRequestList flatMap (req => req.handlers map (req -> _)) + + private def membersAtPickler(sym: Symbol): List[Symbol] = + enteringPickler(sym.info.nonPrivateMembers.toList) +} diff --git a/src/repl/scala/tools/nsc/interpreter/InteractiveReader.scala b/src/repl/scala/tools/nsc/interpreter/InteractiveReader.scala new file mode 100644 index 0000000000..28ddf2939c --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/InteractiveReader.scala @@ -0,0 +1,49 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Stepan Koltsov + */ + +package scala.tools.nsc +package interpreter + +import java.io.IOException +import session.History +import InteractiveReader._ +import Properties.isMac + +/** Reads lines from an input stream */ +trait InteractiveReader { + val interactive: Boolean + + def reset(): Unit + def history: History + def completion: Completion + def redrawLine(): Unit + + def readYesOrNo(prompt: String, alt: => Boolean): Boolean = readOneKey(prompt) match { + case 'y' => true + case 'n' => false + case _ => alt + } + + protected def readOneLine(prompt: String): String + protected def readOneKey(prompt: String): Int + + def readLine(prompt: String): String = + // hack necessary for OSX jvm suspension because read calls are not restarted after SIGTSTP + if (isMac) restartSysCalls(readOneLine(prompt), reset()) + else readOneLine(prompt) +} + +object InteractiveReader { + val msgEINTR = "Interrupted system call" + def restartSysCalls[R](body: => R, reset: => Unit): R = + try body catch { + case e: IOException if e.getMessage == msgEINTR => reset ; body + } + + def apply(): InteractiveReader = SimpleReader() + @deprecated("Use `apply` instead.", "2.9.0") + def createDefault(): InteractiveReader = apply() // used by sbt +} + diff --git a/src/repl/scala/tools/nsc/interpreter/JLineCompletion.scala b/src/repl/scala/tools/nsc/interpreter/JLineCompletion.scala new file mode 100644 index 0000000000..19fa562234 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/JLineCompletion.scala @@ -0,0 +1,352 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import Completion._ +import scala.collection.mutable.ListBuffer +import scala.reflect.internal.util.StringOps.longestCommonPrefix + +// REPL completor - queries supplied interpreter for valid +// completions based on current contents of buffer. +class JLineCompletion(val intp: IMain) extends Completion with CompletionOutput { + val global: intp.global.type = intp.global + import global._ + import definitions.{ PredefModule, AnyClass, AnyRefClass, ScalaPackage, JavaLangPackage } + import rootMirror.{ RootClass, getModuleIfDefined } + type ExecResult = Any + import intp.{ debugging } + + // verbosity goes up with consecutive tabs + private var verbosity: Int = 0 + def resetVerbosity() = verbosity = 0 + + def getSymbol(name: String, isModule: Boolean) = ( + if (isModule) getModuleIfDefined(name) + else getModuleIfDefined(name) + ) + + trait CompilerCompletion { + def tp: Type + def effectiveTp = tp match { + case MethodType(Nil, resType) => resType + case NullaryMethodType(resType) => resType + case _ => tp + } + + // for some reason any's members don't show up in subclasses, which + // we need so 5.<tab> offers asInstanceOf etc. + private def anyMembers = AnyClass.tpe.nonPrivateMembers + def anyRefMethodsToShow = Set("isInstanceOf", "asInstanceOf", "toString") + + def tos(sym: Symbol): String = sym.decodedName + def memberNamed(s: String) = exitingTyper(effectiveTp member newTermName(s)) + + // XXX we'd like to say "filterNot (_.isDeprecated)" but this causes the + // compiler to crash for reasons not yet known. + def members = exitingTyper((effectiveTp.nonPrivateMembers.toList ++ anyMembers) filter (_.isPublic)) + def methods = members.toList filter (_.isMethod) + def packages = members.toList filter (_.isPackage) + def aliases = members.toList filter (_.isAliasType) + + def memberNames = members map tos + def methodNames = methods map tos + def packageNames = packages map tos + def aliasNames = aliases map tos + } + + object NoTypeCompletion extends TypeMemberCompletion(NoType) { + override def memberNamed(s: String) = NoSymbol + override def members = Nil + override def follow(s: String) = None + override def alternativesFor(id: String) = Nil + } + + object TypeMemberCompletion { + def apply(tp: Type, runtimeType: Type, param: NamedParam): TypeMemberCompletion = { + new TypeMemberCompletion(tp) { + var upgraded = false + lazy val upgrade = { + intp rebind param + intp.reporter.printMessage("\nRebinding stable value %s from %s to %s".format(param.name, tp, param.tpe)) + upgraded = true + new TypeMemberCompletion(runtimeType) + } + override def completions(verbosity: Int) = { + super.completions(verbosity) ++ ( + if (verbosity == 0) Nil + else upgrade.completions(verbosity) + ) + } + override def follow(s: String) = super.follow(s) orElse { + if (upgraded) upgrade.follow(s) + else None + } + override def alternativesFor(id: String) = super.alternativesFor(id) ++ ( + if (upgraded) upgrade.alternativesFor(id) + else Nil + ) distinct + } + } + def apply(tp: Type): TypeMemberCompletion = { + if (tp eq NoType) NoTypeCompletion + else if (tp.typeSymbol.isPackageClass) new PackageCompletion(tp) + else new TypeMemberCompletion(tp) + } + def imported(tp: Type) = new ImportCompletion(tp) + } + + class TypeMemberCompletion(val tp: Type) extends CompletionAware + with CompilerCompletion { + def excludeEndsWith: List[String] = Nil + def excludeStartsWith: List[String] = List("<") // <byname>, <repeated>, etc. + def excludeNames: List[String] = (anyref.methodNames filterNot anyRefMethodsToShow) :+ "_root_" + + def methodSignatureString(sym: Symbol) = { + IMain stripString exitingTyper(new MethodSymbolOutput(sym).methodString()) + } + + def exclude(name: String): Boolean = ( + (name contains "$") || + (excludeNames contains name) || + (excludeEndsWith exists (name endsWith _)) || + (excludeStartsWith exists (name startsWith _)) + ) + def filtered(xs: List[String]) = xs filterNot exclude distinct + + def completions(verbosity: Int) = + debugging(tp + " completions ==> ")(filtered(memberNames)) + + override def follow(s: String): Option[CompletionAware] = + debugging(tp + " -> '" + s + "' ==> ")(Some(TypeMemberCompletion(memberNamed(s).tpe)) filterNot (_ eq NoTypeCompletion)) + + override def alternativesFor(id: String): List[String] = + debugging(id + " alternatives ==> ") { + val alts = members filter (x => x.isMethod && tos(x) == id) map methodSignatureString + + if (alts.nonEmpty) "" :: alts else Nil + } + + override def toString = "%s (%d members)".format(tp, members.size) + } + + class PackageCompletion(tp: Type) extends TypeMemberCompletion(tp) { + override def excludeNames = anyref.methodNames + } + + class LiteralCompletion(lit: Literal) extends TypeMemberCompletion(lit.value.tpe) { + override def completions(verbosity: Int) = verbosity match { + case 0 => filtered(memberNames) + case _ => memberNames + } + } + + class ImportCompletion(tp: Type) extends TypeMemberCompletion(tp) { + override def completions(verbosity: Int) = verbosity match { + case 0 => filtered(members filterNot (_.isSetter) map tos) + case _ => super.completions(verbosity) + } + } + + // not for completion but for excluding + object anyref extends TypeMemberCompletion(AnyRefClass.tpe) { } + + // the unqualified vals/defs/etc visible in the repl + object ids extends CompletionAware { + override def completions(verbosity: Int) = intp.unqualifiedIds ++ List("classOf") //, "_root_") + // now we use the compiler for everything. + override def follow(id: String): Option[CompletionAware] = { + if (!completions(0).contains(id)) + return None + + val tpe = intp typeOfExpression id + if (tpe == NoType) + return None + + def default = Some(TypeMemberCompletion(tpe)) + + // only rebinding vals in power mode for now. + if (!isReplPower) default + else intp runtimeClassAndTypeOfTerm id match { + case Some((clazz, runtimeType)) => + val sym = intp.symbolOfTerm(id) + if (sym.isStable) { + val param = new NamedParam.Untyped(id, intp valueOfTerm id getOrElse null) + Some(TypeMemberCompletion(tpe, runtimeType, param)) + } + else default + case _ => + default + } + } + override def toString = "<repl ids> (%s)".format(completions(0).size) + } + + // user-issued wildcard imports like "import global._" or "import String._" + private def imported = intp.sessionWildcards map TypeMemberCompletion.imported + + // literal Ints, Strings, etc. + object literals extends CompletionAware { + def simpleParse(code: String): Tree = newUnitParser(code).templateStats().last + def completions(verbosity: Int) = Nil + + override def follow(id: String) = simpleParse(id) match { + case x: Literal => Some(new LiteralCompletion(x)) + case _ => None + } + } + + // top level packages + object rootClass extends TypeMemberCompletion(RootClass.tpe) { + override def completions(verbosity: Int) = super.completions(verbosity) :+ "_root_" + override def follow(id: String) = id match { + case "_root_" => Some(this) + case _ => super.follow(id) + } + } + // members of Predef + object predef extends TypeMemberCompletion(PredefModule.tpe) { + override def excludeEndsWith = super.excludeEndsWith ++ List("Wrapper", "ArrayOps") + override def excludeStartsWith = super.excludeStartsWith ++ List("wrap") + override def excludeNames = anyref.methodNames + + override def exclude(name: String) = super.exclude(name) || ( + (name contains "2") + ) + + override def completions(verbosity: Int) = verbosity match { + case 0 => Nil + case _ => super.completions(verbosity) + } + } + // members of scala.* + object scalalang extends PackageCompletion(ScalaPackage.tpe) { + def arityClasses = List("Product", "Tuple", "Function") + def skipArity(name: String) = arityClasses exists (x => name != x && (name startsWith x)) + override def exclude(name: String) = super.exclude(name) || ( + skipArity(name) + ) + + override def completions(verbosity: Int) = verbosity match { + case 0 => filtered(packageNames ++ aliasNames) + case _ => super.completions(verbosity) + } + } + // members of java.lang.* + object javalang extends PackageCompletion(JavaLangPackage.tpe) { + override lazy val excludeEndsWith = super.excludeEndsWith ++ List("Exception", "Error") + override lazy val excludeStartsWith = super.excludeStartsWith ++ List("CharacterData") + + override def completions(verbosity: Int) = verbosity match { + case 0 => filtered(packageNames) + case _ => super.completions(verbosity) + } + } + + // the list of completion aware objects which should be consulted + // for top level unqualified, it's too noisy to let much in. + lazy val topLevelBase: List[CompletionAware] = List(ids, rootClass, predef, scalalang, javalang, literals) + def topLevel = topLevelBase ++ imported + def topLevelThreshold = 50 + + // the first tier of top level objects (doesn't include file completion) + def topLevelFor(parsed: Parsed): List[String] = { + val buf = new ListBuffer[String] + topLevel foreach { ca => + buf ++= (ca completionsFor parsed) + + if (buf.size > topLevelThreshold) + return buf.toList.sorted + } + buf.toList + } + + // the most recent result + def lastResult = Forwarder(() => ids follow intp.mostRecentVar) + + def lastResultFor(parsed: Parsed) = { + /** The logic is a little tortured right now because normally '.' is + * ignored as a delimiter, but on .<tab> it needs to be propagated. + */ + val xs = lastResult completionsFor parsed + if (parsed.isEmpty) xs map ("." + _) else xs + } + + def completer(): ScalaCompleter = new JLineTabCompletion + + /** This gets a little bit hairy. It's no small feat delegating everything + * and also keeping track of exactly where the cursor is and where it's supposed + * to end up. The alternatives mechanism is a little hacky: if there is an empty + * string in the list of completions, that means we are expanding a unique + * completion, so don't update the "last" buffer because it'll be wrong. + */ + class JLineTabCompletion extends ScalaCompleter { + // For recording the buffer on the last tab hit + private var lastBuf: String = "" + private var lastCursor: Int = -1 + + // Does this represent two consecutive tabs? + def isConsecutiveTabs(buf: String, cursor: Int) = + cursor == lastCursor && buf == lastBuf + + // This is jline's entry point for completion. + override def complete(buf: String, cursor: Int): Candidates = { + verbosity = if (isConsecutiveTabs(buf, cursor)) verbosity + 1 else 0 + repldbg("\ncomplete(%s, %d) last = (%s, %d), verbosity: %s".format(buf, cursor, lastBuf, lastCursor, verbosity)) + + // we don't try lower priority completions unless higher ones return no results. + def tryCompletion(p: Parsed, completionFunction: Parsed => List[String]): Option[Candidates] = { + val winners = completionFunction(p) + if (winners.isEmpty) + return None + val newCursor = + if (winners contains "") p.cursor + else { + val advance = longestCommonPrefix(winners) + lastCursor = p.position + advance.length + lastBuf = (buf take p.position) + advance + repldbg("tryCompletion(%s, _) lastBuf = %s, lastCursor = %s, p.position = %s".format( + p, lastBuf, lastCursor, p.position)) + p.position + } + + Some(Candidates(newCursor, winners)) + } + + def mkDotted = Parsed.dotted(buf, cursor) withVerbosity verbosity + + // a single dot is special cased to completion on the previous result + def lastResultCompletion = + if (!looksLikeInvocation(buf)) None + else tryCompletion(Parsed.dotted(buf drop 1, cursor), lastResultFor) + + def tryAll = ( + lastResultCompletion + orElse tryCompletion(mkDotted, topLevelFor) + getOrElse Candidates(cursor, Nil) + ) + + /** + * This is the kickoff point for all manner of theoretically + * possible compiler unhappiness. The fault may be here or + * elsewhere, but we don't want to crash the repl regardless. + * The compiler makes it impossible to avoid catching Throwable + * with its unfortunate tendency to throw java.lang.Errors and + * AssertionErrors as the hats drop. We take two swings at it + * because there are some spots which like to throw an assertion + * once, then work after that. Yeah, what can I say. + */ + try tryAll + catch { case ex: Throwable => + repldbg("Error: complete(%s, %s) provoked".format(buf, cursor) + ex) + Candidates(cursor, + if (isReplDebug) List("<error:" + ex + ">") + else Nil + ) + } + } + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/JLineReader.scala b/src/repl/scala/tools/nsc/interpreter/JLineReader.scala new file mode 100644 index 0000000000..5d41f1bbb4 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/JLineReader.scala @@ -0,0 +1,68 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Stepan Koltsov + */ + +package scala.tools.nsc +package interpreter + +import scala.tools.jline.console.ConsoleReader +import scala.tools.jline.console.completer._ +import session._ +import Completion._ + +/** + * Reads from the console using JLine. + */ +class JLineReader(_completion: => Completion) extends InteractiveReader { + val interactive = true + val consoleReader = new JLineConsoleReader() + + lazy val completion = _completion + lazy val history: JLineHistory = JLineHistory() + + private def term = consoleReader.getTerminal() + def reset() = term.reset() + + def scalaToJline(tc: ScalaCompleter): Completer = new Completer { + def complete(_buf: String, cursor: Int, candidates: JList[CharSequence]): Int = { + val buf = if (_buf == null) "" else _buf + val Candidates(newCursor, newCandidates) = tc.complete(buf, cursor) + newCandidates foreach (candidates add _) + newCursor + } + } + + class JLineConsoleReader extends ConsoleReader with ConsoleReaderHelper { + // working around protected/trait/java insufficiencies. + def goBack(num: Int): Unit = back(num) + if ((history: History) ne NoHistory) + this setHistory history + + def readOneKey(prompt: String) = { + this.print(prompt) + this.flush() + this.readVirtualKey() + } + def eraseLine() = consoleReader.resetPromptLine("", "", 0) + def redrawLineAndFlush(): Unit = { flush() ; drawLine() ; flush() } + + // A hook for running code after the repl is done initializing. + lazy val postInit: Unit = { + this setBellEnabled false + + if (completion ne NoCompletion) { + val argCompletor: ArgumentCompleter = + new ArgumentCompleter(new JLineDelimiter, scalaToJline(completion.completer())) + argCompletor setStrict false + + this addCompleter argCompletor + this setAutoprintThreshold 400 // max completion candidates without warning + } + } + } + + def redrawLine() = consoleReader.redrawLineAndFlush() + def readOneLine(prompt: String) = consoleReader readLine prompt + def readOneKey(prompt: String) = consoleReader readOneKey prompt +} diff --git a/src/repl/scala/tools/nsc/interpreter/JavapClass.scala b/src/repl/scala/tools/nsc/interpreter/JavapClass.scala new file mode 100644 index 0000000000..a895944c15 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/JavapClass.scala @@ -0,0 +1,693 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import java.lang.{ ClassLoader => JavaClassLoader, Iterable => JIterable } +import scala.tools.nsc.util.ScalaClassLoader +import java.io.{ ByteArrayInputStream, CharArrayWriter, FileNotFoundException, PrintWriter, Writer } +import java.util.{ Locale } +import java.util.regex.Pattern +import javax.tools.{ Diagnostic, DiagnosticCollector, DiagnosticListener, + ForwardingJavaFileManager, JavaFileManager, JavaFileObject, + SimpleJavaFileObject, StandardLocation } +import scala.reflect.io.{ AbstractFile, Directory, File, Path } +import scala.io.Source +import scala.util.{ Try, Success, Failure } +import scala.util.Properties.lineSeparator +import scala.collection.JavaConverters +import scala.collection.generic.Clearable +import java.net.URL +import scala.language.reflectiveCalls +import Javap._ + +class JavapClass( + val loader: ScalaClassLoader, + val printWriter: PrintWriter, + intp: Option[IMain] = None +) extends scala.tools.util.Javap { + import JavapTool.ToolArgs + import JavapClass._ + + lazy val tool = JavapTool() + + /** Run the tool. Option args start with "-". + * The default options are "-protected -verbose". + * Byte data for filename args is retrieved with findBytes. + */ + def apply(args: Seq[String]): List[JpResult] = { + val (options, claases) = args partition (s => (s startsWith "-") && s.length > 1) + val (flags, upgraded) = upgrade(options) + import flags.{ app, fun, help, raw } + val targets = if (fun && !help) FunFinder(loader, intp).funs(claases) else claases + if (help || claases.isEmpty) List(JpResult(JavapTool.helper(printWriter))) + else if (targets.isEmpty) List(JpResult("No anonfuns found.")) + else tool(raw, upgraded)(targets map (claas => claas -> bytesFor(claas, app))) + } + + /** Cull our tool options. */ + private def upgrade(options: Seq[String]): (ToolArgs, Seq[String]) = ToolArgs fromArgs options match { + case (t,s) if s.nonEmpty => (t,s) + case (t,s) => (t, JavapTool.DefaultOptions) + } + + /** Find bytes. Handle "-", "-app", "Foo#bar" (by ignoring member), "#bar" (by taking "bar"). */ + private def bytesFor(path: String, app: Boolean) = Try { + def last = intp.get.mostRecentVar // fail if no intp + def req = if (path == "-") last else { + val s = path.splitHashMember + if (s._1.nonEmpty) s._1 + else s._2 getOrElse "#" + } + def asAppBody(s: String) = { + val (cls, fix) = s.splitSuffix + s"${cls}$$delayedInit$$body${fix}" + } + def todo = if (app) asAppBody(req) else req + val bytes = findBytes(todo) + if (bytes.isEmpty) throw new FileNotFoundException(s"Could not find class bytes for '${path}'") + else bytes + } + + def findBytes(path: String): Array[Byte] = tryFile(path) getOrElse tryClass(path) + + /** Assume the string is a path and try to find the classfile + * it represents. + */ + def tryFile(path: String): Option[Array[Byte]] = + (Try (File(path.asClassResource)) filter (_.exists) map (_.toByteArray())).toOption + + /** Assume the string is a fully qualified class name and try to + * find the class object it represents. + * There are other symbols of interest, too: + * - a definition that is wrapped in an enclosing class + * - a synthetic that is not in scope but its associated class is + */ + def tryClass(path: String): Array[Byte] = { + def load(name: String) = loader classBytes name + def loadable(name: String) = loader resourceable name + // if path has an interior dollar, take it as a synthetic + // if the prefix up to the dollar is a symbol in scope, + // result is the translated prefix + suffix + def desynthesize(s: String) = { + val i = s indexOf '$' + if (0 until s.length - 1 contains i) { + val name = s substring (0, i) + val sufx = s substring i + val tran = intp flatMap (_ translatePath name) + def loadableOrNone(strip: Boolean) = { + def suffix(strip: Boolean)(x: String) = + (if (strip && (x endsWith "$")) x.init else x) + sufx + val res = tran map (suffix(strip) _) + if (res.isDefined && loadable(res.get)) res else None + } + // try loading translated+suffix + val res = loadableOrNone(strip = false) + // some synthetics lack a dollar, (e.g., suffix = delayedInit$body) + // so as a hack, if prefix$$suffix fails, also try prefix$suffix + if (res.isDefined) res else loadableOrNone(strip = true) + } else None + } + val p = path.asClassName // scrub any suffix + // if repl, translate the name to something replish + // (for translate, would be nicer to get the sym and ask .isClass, + // instead of translatePath and then asking did I get a class back) + val q = if (intp.isEmpty) p else ( + // only simple names get the scope treatment + Some(p) filter (_ contains '.') + // take path as a Name in scope + orElse (intp flatMap (_ translatePath p) filter loadable) + // take path as a Name in scope and find its enclosing class + orElse (intp flatMap (_ translateEnclosingClass p) filter loadable) + // take path as a synthetic derived from some Name in scope + orElse desynthesize(p) + // just try it plain + getOrElse p + ) + load(q) + } + + /** Base class for javap tool adapters for java 6 and 7. */ + abstract class JavapTool { + type ByteAry = Array[Byte] + type Input = Pair[String, Try[ByteAry]] + + /** Run the tool. */ + def apply(raw: Boolean, options: Seq[String])(inputs: Seq[Input]): List[JpResult] + + // Since the tool is loaded by reflection, check for catastrophic failure. + protected def failed: Boolean + implicit protected class Failer[A](a: =>A) { + def orFailed[B >: A](b: =>B) = if (failed) b else a + } + protected def noToolError = new JpError(s"No javap tool available: ${getClass.getName} failed to initialize.") + + // output filtering support + val writer = new CharArrayWriter + def written = { + writer.flush() + val w = writer.toString + writer.reset() + w + } + + /** Create a Showable with output massage. + * @param raw show ugly repl names + * @param target attempt to filter output to show region of interest + * @param preamble other messages to output + */ + def showWithPreamble(raw: Boolean, target: String, preamble: String = ""): Showable = new Showable { + // ReplStrippingWriter clips and scrubs on write(String) + // circumvent it by write(mw, 0, mw.length) or wrap it in withoutUnwrapping + def show() = + if (raw && intp.isDefined) intp.get withoutUnwrapping { writeLines() } + else writeLines() + private def writeLines() { + // take Foo# as Foo#apply for purposes of filtering. Useful for -fun Foo#; + // if apply is added here, it's for other than -fun: javap Foo#, perhaps m#? + val filterOn = target.splitHashMember._2 map { s => if (s.isEmpty) "apply" else s } + var filtering = false // true if in region matching filter + // true to output + def checkFilter(line: String) = if (filterOn.isEmpty) true else { + // cheap heuristic, todo maybe parse for the java sig. + // method sigs end in paren semi + def isAnyMethod = line.endsWith(");") + def isOurMethod = { + val lparen = line.lastIndexOf('(') + val blank = line.lastIndexOf(' ', lparen) + (blank >= 0 && line.substring(blank+1, lparen) == filterOn.get) + } + filtering = if (filtering) { + // next blank line terminates section + // for -public, next line is next method, more or less + line.trim.nonEmpty && !isAnyMethod + } else { + isAnyMethod && isOurMethod + } + filtering + } + for (line <- Source.fromString(preamble + written).getLines(); if checkFilter(line)) + printWriter write line+lineSeparator + printWriter.flush() + } + } + } + + class JavapTool6 extends JavapTool { + import JavapTool._ + val EnvClass = loader.tryToInitializeClass[FakeEnvironment](Env).orNull + val PrinterClass = loader.tryToInitializeClass[FakePrinter](Printer).orNull + override protected def failed = (EnvClass eq null) || (PrinterClass eq null) + + val PrinterCtr = PrinterClass.getConstructor(classOf[InputStream], classOf[PrintWriter], EnvClass) orFailed null + val printWrapper = new PrintWriter(writer) + def newPrinter(in: InputStream, env: FakeEnvironment): FakePrinter = + PrinterCtr.newInstance(in, printWrapper, env) orFailed null + def showable(raw: Boolean, target: String, fp: FakePrinter): Showable = { + fp.asInstanceOf[{ def print(): Unit }].print() // run tool and flush to buffer + printWrapper.flush() // just in case + showWithPreamble(raw, target) + } + + lazy val parser = new JpOptions + def newEnv(opts: Seq[String]): FakeEnvironment = { + def result = { + val env: FakeEnvironment = EnvClass.newInstance() + parser(opts) foreach { case (name, value) => + val field = EnvClass getDeclaredField name + field setAccessible true + field.set(env, value.asInstanceOf[AnyRef]) + } + env + } + result orFailed null + } + + override def apply(raw: Boolean, options: Seq[String])(inputs: Seq[Input]): List[JpResult] = + (inputs map { + case (claas, Success(ba)) => JpResult(showable(raw, claas, newPrinter(new ByteArrayInputStream(ba), newEnv(options)))) + case (_, Failure(e)) => JpResult(e.toString) + }).toList orFailed List(noToolError) + } + + class JavapTool7 extends JavapTool { + + import JavapTool._ + type Task = { + def call(): Boolean // true = ok + //def run(args: Array[String]): Int // all args + //def handleOptions(args: Array[String]): Unit // options, then run() or call() + } + // result of Task.run + //object TaskResult extends Enumeration { + // val Ok, Error, CmdErr, SysErr, Abnormal = Value + //} + val TaskClaas = loader.tryToInitializeClass[Task](JavapTool.Tool).orNull + override protected def failed = TaskClaas eq null + + val TaskCtor = TaskClaas.getConstructor( + classOf[Writer], + classOf[JavaFileManager], + classOf[DiagnosticListener[_]], + classOf[JIterable[String]], + classOf[JIterable[String]] + ) orFailed null + + class JavaReporter extends DiagnosticListener[JavaFileObject] with Clearable { + import scala.collection.mutable.{ ArrayBuffer, SynchronizedBuffer } + type D = Diagnostic[_ <: JavaFileObject] + val diagnostics = new ArrayBuffer[D] with SynchronizedBuffer[D] + override def report(d: Diagnostic[_ <: JavaFileObject]) { + diagnostics += d + } + override def clear() = diagnostics.clear() + /** All diagnostic messages. + * @param locale Locale for diagnostic messages, null by default. + */ + def messages(implicit locale: Locale = null) = (diagnostics map (_ getMessage locale)).toList + + def reportable(raw: Boolean): String = { + // don't filter this message if raw, since the names are likely to differ + val container = "Binary file .* contains .*".r + val m = if (raw) messages + else messages filter (_ match { case container() => false case _ => true }) + clear() + if (m.nonEmpty) m mkString ("", lineSeparator, lineSeparator) + else "" + } + } + val reporter = new JavaReporter + + // DisassemblerTool.getStandardFileManager(reporter,locale,charset) + val defaultFileManager: JavaFileManager = + (loader.tryToLoadClass[JavaFileManager]("com.sun.tools.javap.JavapFileManager").get getMethod ( + "create", + classOf[DiagnosticListener[_]], + classOf[PrintWriter] + ) invoke (null, reporter, new PrintWriter(System.err, true))).asInstanceOf[JavaFileManager] orFailed null + + // manages named arrays of bytes, which might have failed to load + class JavapFileManager(val managed: Seq[Input])(delegate: JavaFileManager = defaultFileManager) + extends ForwardingJavaFileManager[JavaFileManager](delegate) { + import JavaFileObject.Kind + import Kind._ + import StandardLocation._ + import JavaFileManager.Location + import java.net.URI + def uri(name: String): URI = new URI(name) // new URI("jfo:" + name) + + def inputNamed(name: String): Try[ByteAry] = (managed find (_._1 == name)).get._2 + def managedFile(name: String, kind: Kind) = kind match { + case CLASS => fileObjectForInput(name, inputNamed(name), kind) + case _ => null + } + // todo: just wrap it as scala abstractfile and adapt it uniformly + def fileObjectForInput(name: String, bytes: Try[ByteAry], kind: Kind): JavaFileObject = + new SimpleJavaFileObject(uri(name), kind) { + override def openInputStream(): InputStream = new ByteArrayInputStream(bytes.get) + // if non-null, ClassWriter wrongly requires scheme non-null + override def toUri: URI = null + override def getName: String = name + // suppress + override def getLastModified: Long = -1L + } + override def getJavaFileForInput(location: Location, className: String, kind: Kind): JavaFileObject = + location match { + case CLASS_PATH => managedFile(className, kind) + case _ => null + } + override def hasLocation(location: Location): Boolean = + location match { + case CLASS_PATH => true + case _ => false + } + } + def fileManager(inputs: Seq[Input]) = new JavapFileManager(inputs)() + + // show tool messages and tool output, with output massage + def showable(raw: Boolean, target: String): Showable = showWithPreamble(raw, target, reporter.reportable(raw)) + + // eventually, use the tool interface + def task(options: Seq[String], claases: Seq[String], inputs: Seq[Input]): Task = { + //ServiceLoader.load(classOf[javax.tools.DisassemblerTool]). + //getTask(writer, fileManager, reporter, options.asJava, claases.asJava) + import JavaConverters.asJavaIterableConverter + TaskCtor.newInstance(writer, fileManager(inputs), reporter, options.asJava, claases.asJava) + .orFailed (throw new IllegalStateException) + } + // a result per input + private def applyOne(raw: Boolean, options: Seq[String], claas: String, inputs: Seq[Input]): Try[JpResult] = + Try { + task(options, Seq(claas), inputs).call() + } map { + case true => JpResult(showable(raw, claas)) + case _ => JpResult(reporter.reportable(raw)) + } recoverWith { + case e: java.lang.reflect.InvocationTargetException => e.getCause match { + case t: IllegalArgumentException => Success(JpResult(t.getMessage)) // bad option + case x => Failure(x) + } + } lastly { + reporter.clear() + } + override def apply(raw: Boolean, options: Seq[String])(inputs: Seq[Input]): List[JpResult] = (inputs map { + case (claas, Success(_)) => applyOne(raw, options, claas, inputs).get + case (_, Failure(e)) => JpResult(e.toString) + }).toList orFailed List(noToolError) + } + + object JavapTool { + // >= 1.7 + val Tool = "com.sun.tools.javap.JavapTask" + + // < 1.7 + val Env = "sun.tools.javap.JavapEnvironment" + val Printer = "sun.tools.javap.JavapPrinter" + // "documentation" + type FakeEnvironment = AnyRef + type FakePrinter = AnyRef + + // support JavapEnvironment + class JpOptions { + private object Access { + final val PRIVATE = 0 + final val PROTECTED = 1 + final val PACKAGE = 2 + final val PUBLIC = 3 + } + private val envActionMap: Map[String, (String, Any)] = { + val map = Map( + "-l" -> (("showLineAndLocal", true)), + "-c" -> (("showDisassembled", true)), + "-s" -> (("showInternalSigs", true)), + "-verbose" -> (("showVerbose", true)), + "-private" -> (("showAccess", Access.PRIVATE)), + "-package" -> (("showAccess", Access.PACKAGE)), + "-protected" -> (("showAccess", Access.PROTECTED)), + "-public" -> (("showAccess", Access.PUBLIC)), + "-all" -> (("showallAttr", true)) + ) + map ++ List( + "-v" -> map("-verbose"), + "-p" -> map("-private") + ) + } + def apply(opts: Seq[String]): Seq[(String, Any)] = { + opts flatMap { opt => + envActionMap get opt match { + case Some(pair) => List(pair) + case _ => + val charOpts = opt.tail.toSeq map ("-" + _) + if (charOpts forall (envActionMap contains _)) + charOpts map envActionMap + else Nil + } + } + } + } + + case class ToolArgs(raw: Boolean = false, help: Boolean = false, app: Boolean = false, fun: Boolean = false) + + object ToolArgs { + def fromArgs(args: Seq[String]): (ToolArgs, Seq[String]) = ((ToolArgs(), Seq[String]()) /: (args flatMap massage)) { + case ((t,others), s) => s match { + case "-fun" => (t copy (fun=true), others) + case "-app" => (t copy (app=true), others) + case "-help" => (t copy (help=true), others) + case "-raw" => (t copy (raw=true), others) + case _ => (t, others :+ s) + } + } + } + + val helps = List( + "usage" -> ":javap [opts] [path or class or -]...", + "-help" -> "Prints this help message", + "-raw" -> "Don't unmangle REPL names", + "-app" -> "Show the DelayedInit body of Apps", + "-fun" -> "Show anonfuns for class or Class#method", + "-verbose/-v" -> "Stack size, number of locals, method args", + "-private/-p" -> "Private classes and members", + "-package" -> "Package-private classes and members", + "-protected" -> "Protected classes and members", + "-public" -> "Public classes and members", + "-l" -> "Line and local variable tables", + "-c" -> "Disassembled code", + "-s" -> "Internal type signatures", + "-sysinfo" -> "System info of class", + "-constants" -> "Static final constants" + ) + + // match prefixes and unpack opts, or -help on failure + def massage(arg: String): Seq[String] = { + require(arg startsWith "-") + // arg matches opt "-foo/-f" if prefix of -foo or exactly -f + val r = """(-[^/]*)(/(-.))?""".r + def maybe(opt: String, s: String): Option[String] = opt match { + // disambiguate by preferring short form + case r(lf,_,sf) if s == sf => Some(sf) + case r(lf,_,sf) if lf startsWith s => Some(lf) + case _ => None + } + def candidates(s: String) = (helps map (h => maybe(h._1, s))).flatten + // one candidate or one single-char candidate + def uniqueOf(maybes: Seq[String]) = { + def single(s: String) = s.length == 2 + if (maybes.length == 1) maybes + else if ((maybes count single) == 1) maybes filter single + else Nil + } + // each optchar must decode to exactly one option + def unpacked(s: String): Try[Seq[String]] = { + val ones = (s drop 1) map { c => + val maybes = uniqueOf(candidates(s"-$c")) + if (maybes.length == 1) Some(maybes.head) else None + } + Try(ones) filter (_ forall (_.isDefined)) map (_.flatten) + } + val res = uniqueOf(candidates(arg)) + if (res.nonEmpty) res + else (unpacked(arg) + getOrElse (Seq("-help"))) // or else someone needs help + } + + def helper(pw: PrintWriter) = new Showable { + def show() = helps foreach (p => pw write "%-12.12s%s%n".format(p._1,p._2)) + } + + val DefaultOptions = List("-protected", "-verbose") + + def isAvailable = Seq(Env, Tool) exists (cn => hasClass(loader, cn)) + + private def hasClass(cl: ScalaClassLoader, cn: String) = cl.tryToInitializeClass[AnyRef](cn).isDefined + + private def isTaskable(cl: ScalaClassLoader) = hasClass(cl, Tool) + + def apply() = if (isTaskable(loader)) new JavapTool7 else new JavapTool6 + } +} + +object JavapClass { + def apply( + loader: ScalaClassLoader = ScalaClassLoader.appLoader, + printWriter: PrintWriter = new PrintWriter(System.out, true), + intp: Option[IMain] = None + ) = new JavapClass(loader, printWriter, intp) + + // We enjoy flexibility in specifying either a fully-qualified class name com.acme.Widget + // or a resource path com/acme/Widget.class; but not widget.out + implicit class MaybeClassLike(val s: String) extends AnyVal { + /* private[this] final val suffix = ".class" */ + private def suffix = ".class" + def asClassName = (s stripSuffix suffix).replace('/', '.') + def asClassResource = if (s endsWith suffix) s else s.replace('.', '/') + suffix + def splitSuffix: (String, String) = if (s endsWith suffix) (s dropRight suffix.length, suffix) else (s, "") + def strippingSuffix(f: String => String): String = + if (s endsWith suffix) f(s dropRight suffix.length) else s + // e.g. Foo#bar. Foo# yields zero-length member part. + def splitHashMember: (String, Option[String]) = { + val i = s lastIndexOf '#' + if (i < 0) (s, None) + //else if (i >= s.length - 1) (s.init, None) + else (s take i, Some(s drop i+1)) + } + } + implicit class ClassLoaderOps(val cl: ClassLoader) extends AnyVal { + private def parentsOf(x: ClassLoader): List[ClassLoader] = if (x == null) Nil else x :: parentsOf(x.getParent) + def parents: List[ClassLoader] = parentsOf(cl) + /* all file locations */ + def locations = { + def alldirs = parents flatMap (_ match { + case ucl: ScalaClassLoader.URLClassLoader => ucl.classPathURLs + case jcl: java.net.URLClassLoader => jcl.getURLs + case _ => Nil + }) + val dirs = for (d <- alldirs; if d.getProtocol == "file") yield Path(new JFile(d.toURI)) + dirs + } + /* only the file location from which the given class is loaded */ + def locate(k: String): Option[Path] = { + Try { + val claas = try cl loadClass k catch { + case _: NoClassDefFoundError => null // let it snow + } + // cf ScalaClassLoader.originOfClass + claas.getProtectionDomain.getCodeSource.getLocation + } match { + case Success(null) => None + case Success(loc) if loc.isFile => Some(Path(new JFile(loc.toURI))) + case _ => None + } + } + /* would classBytes succeed with a nonempty array */ + def resourceable(className: String): Boolean = cl.getResource(className.asClassResource) != null + } + implicit class PathOps(val p: Path) extends AnyVal { + import scala.tools.nsc.io.Jar + def isJar = Jar isJarOrZip p + } + implicit class URLOps(val url: URL) extends AnyVal { + def isFile: Boolean = url.getProtocol == "file" + } + object FunFinder { + def apply(loader: ScalaClassLoader, intp: Option[IMain]) = new FunFinder(loader, intp) + } + class FunFinder(loader: ScalaClassLoader, intp: Option[IMain]) { + + // class k, candidate f without prefix + def isFunOfClass(k: String, f: String) = { + val p = (s"${Pattern quote k}\\$$+anonfun").r + (p findPrefixOf f).nonEmpty + } + // class k, candidate f without prefix, method m + def isFunOfMethod(k: String, m: String, f: String) = { + val p = (s"${Pattern quote k}\\$$+anonfun\\$$${Pattern quote m}\\$$").r + (p findPrefixOf f).nonEmpty + } + def isFunOfTarget(k: String, m: Option[String], f: String) = + if (m.isEmpty) isFunOfClass(k, f) + else isFunOfMethod(k, m.get, f) + def listFunsInAbsFile(k: String, m: Option[String], d: AbstractFile) = { + for (f <- d; if !f.isDirectory && isFunOfTarget(k, m, f.name)) yield f.name + } + // path prefix p, class k, dir d + def listFunsInDir(p: String, k: String, m: Option[String])(d: Directory) = { + val subdir = Path(p) + for (f <- (d / subdir).toDirectory.list; if f.isFile && isFunOfTarget(k, m, f.name)) + yield f.name + } + // path prefix p, class k, jar file f + def listFunsInJar(p: String, k: String, m: Option[String])(f: File) = { + import java.util.jar.JarEntry + import scala.tools.nsc.io.Jar + def maybe(e: JarEntry) = { + val (path, name) = { + val parts = e.getName split "/" + if (parts.length < 2) ("", e.getName) + else (parts.init mkString "/", parts.last) + } + if (path == p && isFunOfTarget(k, m, name)) Some(name) else None + } + (new Jar(f) map maybe).flatten + } + def loadable(name: String) = loader resourceable name + // translated class, optional member, opt member to filter on, whether it is repl output + def translate(s: String): (String, Option[String], Option[String], Boolean) = { + val (k0, m0) = s.splitHashMember + val k = k0.asClassName + val member = m0 filter (_.nonEmpty) // take Foo# as no member, not "" + val filter = m0 flatMap { case "" => Some("apply") case _ => None } // take Foo# as filter on apply + // class is either something replish or available to loader + // $line.$read$$etc$Foo#member + ((intp flatMap (_ translatePath k) filter (loadable) map ((_, member, filter, true))) + // s = "f" and $line.$read$$etc$#f is what we're after, + // ignoring any #member (except take # as filter on #apply) + orElse (intp flatMap (_ translateEnclosingClass k) map ((_, Some(k), filter, true))) + getOrElse (k, member, filter, false)) + } + /** Find the classnames of anonfuns associated with k, + * where k may be an available class or a symbol in scope. + */ + def funsOf(k0: String): Seq[String] = { + // class is either something replish or available to loader + val (k, member, filter, isReplish) = translate(k0) + val splat = k split "\\." + val name = splat.last + val prefix = if (splat.length > 1) splat.init mkString "/" else "" + val pkg = if (splat.length > 1) splat.init mkString "." else "" + // reconstitute an anonfun with a package + // if filtered, add the hash back, e.g. pkg.Foo#bar, pkg.Foo$anon$1#apply + def packaged(s: String) = { + val p = if (pkg.isEmpty) s else s"$pkg.$s" + val pm = filter map (p + "#" + _) + pm getOrElse p + } + // is this translated path in (usually virtual) repl outdir? or loadable from filesystem? + val fs = if (isReplish) { + def outed(d: AbstractFile, p: Seq[String]): Option[AbstractFile] = { + if (p.isEmpty) Option(d) + else Option(d.lookupName(p.head, directory = true)) flatMap (f => outed(f, p.tail)) + } + outed(intp.get.replOutput.dir, splat.init) map { d => + listFunsInAbsFile(name, member, d) map packaged + } + } else { + loader locate k map { w => + if (w.isDirectory) listFunsInDir(prefix, name, member)(w.toDirectory) map packaged + else if (w.isJar) listFunsInJar(prefix, name, member)(w.toFile) map packaged + else Nil + } + } + fs match { + case Some(xs) => xs.to[Seq] // maybe empty + case None => Seq() // nothing found, e.g., junk input + } + } + def funs(ks: Seq[String]) = ks flatMap funsOf _ + } +} + +object Javap { + def isAvailable(cl: ScalaClassLoader = ScalaClassLoader.appLoader) = JavapClass(cl).JavapTool.isAvailable + + def apply(path: String): Unit = apply(Seq(path)) + def apply(args: Seq[String]): Unit = JavapClass() apply args foreach (_.show()) + + trait Showable { + def show(): Unit + } + + sealed trait JpResult extends scala.tools.util.JpResult { + type ResultType + def isError: Boolean + def value: ResultType + def show(): Unit + // todo + // def header(): String + // def fields(): List[String] + // def methods(): List[String] + // def signatures(): List[String] + } + object JpResult { + def apply(msg: String) = new JpError(msg) + def apply(res: Showable) = new JpSuccess(res) + } + class JpError(msg: String) extends JpResult { + type ResultType = String + def isError = true + def value = msg + def show() = println(msg) // makes sense for :javap, less for -Ygen-javap + } + class JpSuccess(val value: Showable) extends JpResult { + type ResultType = AnyRef + def isError = false + def show() = value.show() // output to tool's PrintWriter + } + implicit class Lastly[A](val t: Try[A]) extends AnyVal { + private def effect[X](last: =>Unit)(a: X): Try[A] = { last; t } + def lastly(last: =>Unit): Try[A] = t transform (effect(last) _, effect(last) _) + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/Logger.scala b/src/repl/scala/tools/nsc/interpreter/Logger.scala new file mode 100644 index 0000000000..7407daf8d0 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/Logger.scala @@ -0,0 +1,14 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +trait Logger { + def isInfo: Boolean + def isDebug: Boolean + def isTrace: Boolean + def out: JPrintWriter +} diff --git a/src/repl/scala/tools/nsc/interpreter/LoopCommands.scala b/src/repl/scala/tools/nsc/interpreter/LoopCommands.scala new file mode 100644 index 0000000000..4bba27b714 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/LoopCommands.scala @@ -0,0 +1,86 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.collection.{ mutable, immutable } +import mutable.ListBuffer +import scala.language.implicitConversions + +class ProcessResult(val line: String) { + import scala.sys.process._ + private val buffer = new ListBuffer[String] + + val builder = Process(line) + val logger = ProcessLogger(buffer += _) + val exitCode = builder ! logger + def lines = buffer.toList + + override def toString = "`%s` (%d lines, exit %d)".format(line, buffer.size, exitCode) +} + +trait LoopCommands { + protected def out: JPrintWriter + + // So outputs can be suppressed. + def echoCommandMessage(msg: String): Unit = out println msg + + // a single interpreter command + abstract class LoopCommand(val name: String, val help: String) extends (String => Result) { + def usage: String = "" + def usageMsg: String = ":" + name + ( + if (usage == "") "" else " " + usage + ) + def apply(line: String): Result + + // called if no args are given + def showUsage(): Result = { + "usage is " + usageMsg + Result(keepRunning = true, None) + } + } + object LoopCommand { + def nullary(name: String, help: String, f: () => Result): LoopCommand = + new NullaryCmd(name, help, _ => f()) + + def cmd(name: String, usage: String, help: String, f: String => Result): LoopCommand = + if (usage == "") new NullaryCmd(name, help, f) + else new LineCmd(name, usage, help, f) + } + + class NullaryCmd(name: String, help: String, f: String => Result) extends LoopCommand(name, help) { + def apply(line: String): Result = f(line) + } + + class LineCmd(name: String, argWord: String, help: String, f: String => Result) extends LoopCommand(name, help) { + override def usage = argWord + def apply(line: String): Result = f(line) + } + + class VarArgsCmd(name: String, argWord: String, help: String, f: List[String] => Result) + extends LoopCommand(name, help) { + override def usage = argWord + def apply(line: String): Result = apply(words(line)) + def apply(args: List[String]) = f(args) + } + + // the result of a single command + case class Result(keepRunning: Boolean, lineToRecord: Option[String]) + + object Result { + // the default result means "keep running, and don't record that line" + val default = Result(keepRunning = true, None) + + // most commands do not want to micromanage the Result, but they might want + // to print something to the console, so we accomodate Unit and String returns. + implicit def resultFromUnit(x: Unit): Result = default + implicit def resultFromString(msg: String): Result = { + echoCommandMessage(msg) + default + } + } +} + diff --git a/src/repl/scala/tools/nsc/interpreter/MemberHandlers.scala b/src/repl/scala/tools/nsc/interpreter/MemberHandlers.scala new file mode 100644 index 0000000000..84a47311e2 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/MemberHandlers.scala @@ -0,0 +1,219 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package interpreter + +import scala.collection.{ mutable, immutable } +import scala.reflect.internal.Flags._ +import scala.language.implicitConversions + +trait MemberHandlers { + val intp: IMain + + import intp.{ Request, global, naming } + import global._ + import naming._ + + private def codegenln(leadingPlus: Boolean, xs: String*): String = codegen(leadingPlus, (xs ++ Array("\n")): _*) + private def codegenln(xs: String*): String = codegenln(true, xs: _*) + private def codegen(leadingPlus: Boolean, xs: String*): String = { + val front = if (leadingPlus) "+ " else "" + front + (xs map string2codeQuoted mkString " + ") + } + private implicit def name2string(name: Name) = name.toString + + /** A traverser that finds all mentioned identifiers, i.e. things + * that need to be imported. It might return extra names. + */ + private class ImportVarsTraverser extends Traverser { + val importVars = new mutable.HashSet[Name]() + + override def traverse(ast: Tree) = ast match { + case Ident(name) => + // XXX this is obviously inadequate but it's going to require some effort + // to get right. + if (name.toString startsWith "x$") () + else importVars += name + case _ => super.traverse(ast) + } + } + private object ImportVarsTraverser { + def apply(member: Tree) = { + val ivt = new ImportVarsTraverser() + ivt traverse member + ivt.importVars.toList + } + } + + private def isTermMacro(ddef: DefDef): Boolean = ddef.mods.isMacro + + def chooseHandler(member: Tree): MemberHandler = member match { + case member: DefDef if isTermMacro(member) => new TermMacroHandler(member) + case member: DefDef => new DefHandler(member) + case member: ValDef => new ValHandler(member) + case member: ModuleDef => new ModuleHandler(member) + case member: ClassDef => new ClassHandler(member) + case member: TypeDef => new TypeAliasHandler(member) + case member: Assign => new AssignHandler(member) + case member: Import => new ImportHandler(member) + case DocDef(_, documented) => chooseHandler(documented) + case member => new GenericHandler(member) + } + + sealed abstract class MemberDefHandler(override val member: MemberDef) extends MemberHandler(member) { + override def name: Name = member.name + def mods: Modifiers = member.mods + def keyword = member.keyword + def prettyName = name.decode + + override def definesImplicit = member.mods.isImplicit + override def definesTerm: Option[TermName] = Some(name.toTermName) filter (_ => name.isTermName) + override def definesType: Option[TypeName] = Some(name.toTypeName) filter (_ => name.isTypeName) + override def definedSymbols = if (symbol eq NoSymbol) Nil else List(symbol) + } + + /** Class to handle one member among all the members included + * in a single interpreter request. + */ + sealed abstract class MemberHandler(val member: Tree) { + def name: Name = nme.NO_NAME + def path = intp.originalPath(symbol) + def symbol = if (member.symbol eq null) NoSymbol else member.symbol + def definesImplicit = false + def definesValue = false + + def definesTerm = Option.empty[TermName] + def definesType = Option.empty[TypeName] + + lazy val referencedNames = ImportVarsTraverser(member) + def importedNames = List[Name]() + def definedNames = definesTerm.toList ++ definesType.toList + def definedSymbols = List[Symbol]() + + def extraCodeToEvaluate(req: Request): String = "" + def resultExtractionCode(req: Request): String = "" + + private def shortName = this.getClass.toString split '.' last + override def toString = shortName + referencedNames.mkString(" (refs: ", ", ", ")") + } + + class GenericHandler(member: Tree) extends MemberHandler(member) + + class ValHandler(member: ValDef) extends MemberDefHandler(member) { + val maxStringElements = 1000 // no need to mkString billions of elements + override def definesValue = true + + override def resultExtractionCode(req: Request): String = { + val isInternal = isUserVarName(name) && req.lookupTypeOf(name) == "Unit" + if (!mods.isPublic || isInternal) "" + else { + // if this is a lazy val we avoid evaluating it here + val resultString = + if (mods.isLazy) codegenln(false, "<lazy>") + else any2stringOf(path, maxStringElements) + + val vidString = + if (replProps.vids) s"""" + " @ " + "%%8x".format(System.identityHashCode($path)) + " """.trim + else "" + + """ + "%s%s: %s = " + %s""".format(string2code(prettyName), vidString, string2code(req typeOf name), resultString) + } + } + } + + class DefHandler(member: DefDef) extends MemberDefHandler(member) { + override def definesValue = flattensToEmpty(member.vparamss) // true if 0-arity + override def resultExtractionCode(req: Request) = + if (mods.isPublic) codegenln(name, ": ", req.typeOf(name)) else "" + } + + abstract class MacroHandler(member: DefDef) extends MemberDefHandler(member) { + override def definesValue = false + override def definesTerm: Option[TermName] = Some(name.toTermName) + override def definesType: Option[TypeName] = None + override def resultExtractionCode(req: Request) = if (mods.isPublic) codegenln(notification(req)) else "" + def notification(req: Request): String + } + + class TermMacroHandler(member: DefDef) extends MacroHandler(member) { + def notification(req: Request) = s"defined term macro $name: ${req.typeOf(name)}" + } + + class AssignHandler(member: Assign) extends MemberHandler(member) { + val Assign(lhs, rhs) = member + override lazy val name = newTermName(freshInternalVarName()) + + override def definesTerm = Some(name) + override def definesValue = true + override def extraCodeToEvaluate(req: Request) = + """val %s = %s""".format(name, lhs) + + /** Print out lhs instead of the generated varName */ + override def resultExtractionCode(req: Request) = { + val lhsType = string2code(req lookupTypeOf name) + val res = string2code(req fullPath name) + """ + "%s: %s = " + %s + "\n" """.format(string2code(lhs.toString), lhsType, res) + "\n" + } + } + + class ModuleHandler(module: ModuleDef) extends MemberDefHandler(module) { + override def definesTerm = Some(name.toTermName) + override def definesValue = true + + override def resultExtractionCode(req: Request) = codegenln("defined object ", name) + } + + class ClassHandler(member: ClassDef) extends MemberDefHandler(member) { + override def definedSymbols = List(symbol, symbol.companionSymbol) filterNot (_ == NoSymbol) + override def definesType = Some(name.toTypeName) + override def definesTerm = Some(name.toTermName) filter (_ => mods.isCase) + + override def resultExtractionCode(req: Request) = + codegenln("defined %s %s".format(keyword, name)) + } + + class TypeAliasHandler(member: TypeDef) extends MemberDefHandler(member) { + private def isAlias = mods.isPublic && treeInfo.isAliasTypeDef(member) + override def definesType = Some(name.toTypeName) filter (_ => isAlias) + + override def resultExtractionCode(req: Request) = + codegenln("defined type alias ", name) + "\n" + } + + class ImportHandler(imp: Import) extends MemberHandler(imp) { + val Import(expr, selectors) = imp + def targetType = intp.global.rootMirror.getModuleIfDefined("" + expr) match { + case NoSymbol => intp.typeOfExpression("" + expr) + case sym => sym.thisType + } + private def importableTargetMembers = importableMembers(targetType).toList + // wildcard imports, e.g. import foo._ + private def selectorWild = selectors filter (_.name == nme.USCOREkw) + // renamed imports, e.g. import foo.{ bar => baz } + private def selectorRenames = selectors map (_.rename) filterNot (_ == null) + + /** Whether this import includes a wildcard import */ + val importsWildcard = selectorWild.nonEmpty + + def implicitSymbols = importedSymbols filter (_.isImplicit) + def importedSymbols = individualSymbols ++ wildcardSymbols + + private val selectorNames = selectorRenames filterNot (_ == nme.USCOREkw) flatMap (_.bothNames) toSet + lazy val individualSymbols: List[Symbol] = exitingTyper(importableTargetMembers filter (m => selectorNames(m.name))) + lazy val wildcardSymbols: List[Symbol] = exitingTyper(if (importsWildcard) importableTargetMembers else Nil) + + /** Complete list of names imported by a wildcard */ + lazy val wildcardNames: List[Name] = wildcardSymbols map (_.name) + lazy val individualNames: List[Name] = individualSymbols map (_.name) + + /** The names imported by this statement */ + override lazy val importedNames: List[Name] = wildcardNames ++ individualNames + lazy val importsSymbolNamed: Set[String] = importedNames map (_.toString) toSet + + def importString = imp.toString + override def resultExtractionCode(req: Request) = codegenln(importString) + "\n" + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/NamedParam.scala b/src/repl/scala/tools/nsc/interpreter/NamedParam.scala new file mode 100644 index 0000000000..a0af72940a --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/NamedParam.scala @@ -0,0 +1,46 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import NamedParam._ +import scala.language.implicitConversions +import scala.reflect.runtime.{universe => ru} +import scala.reflect.{ClassTag, classTag} +import scala.tools.nsc.typechecker.{ TypeStrings } + +trait NamedParamCreator { + protected def freshName: () => String + + def apply[T: ru.TypeTag : ClassTag](name: String, x: T): NamedParam = new Typed[T](name, x) + def apply[T: ru.TypeTag : ClassTag](x: T): NamedParam = apply(freshName(), x) + def clazz(name: String, x: Any): NamedParam = new Untyped(name, x) + + implicit def tuple[T: ru.TypeTag : ClassTag](pair: (String, T)): NamedParam = apply(pair._1, pair._2) +} + +object NamedParam extends NamedParamCreator { + class Typed[T: ru.TypeTag : ClassTag](val name: String, val value: T) extends NamedParam { + val tpe = TypeStrings.fromTag[T] + } + class Untyped(val name: String, val value: Any) extends NamedParam { + val tpe = TypeStrings.fromValue(value) + } + + protected val freshName = { + var counter = 0 + () => { counter += 1; "p" + counter } + } +} + +case class NamedParamClass(name: String, tpe: String, value: Any) extends NamedParam { } + +trait NamedParam { + def name: String + def tpe: String + def value: Any + override def toString = name + ": " + tpe +} diff --git a/src/repl/scala/tools/nsc/interpreter/Naming.scala b/src/repl/scala/tools/nsc/interpreter/Naming.scala new file mode 100644 index 0000000000..57f3675ada --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/Naming.scala @@ -0,0 +1,105 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.util.Properties.lineSeparator + +/** This is for name logic which is independent of the compiler (notice there's no Global.) + * That includes at least generating, metaquoting, mangling, and unmangling. + */ +trait Naming { + def unmangle(str: String): String = { + val ESC = '\u001b' + val cleaned = removeIWPackages(removeLineWrapper(str)) + // Looking to exclude binary data which hoses the terminal, but + // let through the subset of it we need, like whitespace and also + // <ESC> for ansi codes. + val binaryChars = cleaned count (ch => ch < 32 && !ch.isWhitespace && ch != ESC) + // Lots of binary chars - translate all supposed whitespace into spaces + // except supposed line endings, otherwise scrubbed lines run together + if (binaryChars > 5) // more than one can count while holding a hamburger + cleaned map { + case c if lineSeparator contains c => c + case c if c.isWhitespace => ' ' + case c if c < 32 => '?' + case c => c + } + // Not lots - preserve whitespace and ESC + else + cleaned map (ch => if (ch.isWhitespace || ch == ESC) ch else if (ch < 32) '?' else ch) + } + + // The two name forms this is catching are the two sides of this assignment: + // + // $line3.$read.$iw.$iw.Bippy = + // $line3.$read$$iw$$iw$Bippy@4a6a00ca + + private def noMeta(s: String) = "\\Q" + s + "\\E" + private lazy val lineRegex = { + val sn = sessionNames + val members = List(sn.read, sn.eval, sn.print) map noMeta mkString ("(?:", "|", ")") + debugging("lineRegex")(noMeta(sn.line) + """\d+[./]""" + members + """[$.]""") + } + + private def removeLineWrapper(s: String) = s.replaceAll(lineRegex, "") + private def removeIWPackages(s: String) = s.replaceAll("""\$iw[$.]""", "") + + trait SessionNames { + // All values are configurable by passing e.g. -Dscala.repl.name.read=XXX + final def propOr(name: String): String = propOr(name, "$" + name) + final def propOr(name: String, default: String): String = + sys.props.getOrElse("scala.repl.name." + name, default) + + // Prefixes used in repl machinery. Default to $line, $read, etc. + def line = propOr("line") + def read = propOr("read") + def eval = propOr("eval") + def print = propOr("print") + def result = propOr("result") + + // The prefix for unnamed results: by default res0, res1, etc. + def res = propOr("res", "res") // INTERPRETER_VAR_PREFIX + // Internal ones + def ires = propOr("ires") + } + lazy val sessionNames: SessionNames = new SessionNames { } + + /** Generates names pre0, pre1, etc. via calls to apply method */ + class NameCreator(pre: String) { + private var x = -1 + var mostRecent: String = "" + + def apply(): String = { + x += 1 + mostRecent = pre + x + mostRecent + } + def reset(): Unit = x = -1 + def didGenerate(name: String) = + (name startsWith pre) && ((name drop pre.length) forall (_.isDigit)) + } + + private lazy val userVar = new NameCreator(sessionNames.res) // var name, like res0 + private lazy val internalVar = new NameCreator(sessionNames.ires) // internal var name, like $ires0 + + def isUserVarName(name: String) = userVar didGenerate name + def isInternalVarName(name: String) = internalVar didGenerate name + + val freshLineId = { + var x = 0 + () => { x += 1 ; x } + } + def freshUserVarName() = userVar() + def freshInternalVarName() = internalVar() + + def resetAllCreators() { + userVar.reset() + internalVar.reset() + } + + def mostRecentVar = userVar.mostRecent +} diff --git a/src/repl/scala/tools/nsc/interpreter/Parsed.scala b/src/repl/scala/tools/nsc/interpreter/Parsed.scala new file mode 100644 index 0000000000..672a6fd28f --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/Parsed.scala @@ -0,0 +1,60 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import util.returning + +/** One instance of a command buffer. + */ +class Parsed private ( + val buffer: String, + val cursor: Int, + val delimited: Char => Boolean +) extends Delimited { + def isEmpty = args.isEmpty + def isUnqualified = args.size == 1 + def isAtStart = cursor <= 0 + + private var _verbosity = 0 + + def verbosity = _verbosity + def withVerbosity(v: Int): this.type = returning[this.type](this)(_ => _verbosity = v) + + def args = toArgs(buffer take cursor).toList + def bufferHead = args.head + def headLength = bufferHead.length + 1 + def bufferTail = new Parsed(buffer drop headLength, cursor - headLength, delimited) withVerbosity verbosity + + def prev = new Parsed(buffer, cursor - 1, delimited) withVerbosity verbosity + def currentChar = buffer(cursor) + def currentArg = args.last + def position = + if (isEmpty) 0 + else if (isLastDelimiter) cursor + else cursor - currentArg.length + + def isFirstDelimiter = !isEmpty && isDelimiterChar(buffer.head) + def isLastDelimiter = !isEmpty && isDelimiterChar(buffer.last) + + def isQuoted = false // TODO + def isEscaped = !isAtStart && isEscapeChar(currentChar) && !isEscapeChar(prev.currentChar) + def isDelimiter = !isQuoted && !isEscaped && isDelimiterChar(currentChar) + + override def toString = "Parsed(%s / %d)".format(buffer, cursor) +} + +object Parsed { + val DefaultDelimiters = "[]{},`; \t".toSet + + private def onull(s: String) = if (s == null) "" else s + + def apply(s: String, cursor: Int): Parsed = apply(onull(s), cursor, DefaultDelimiters) + def apply(s: String, cursor: Int, delimited: Char => Boolean): Parsed = + new Parsed(onull(s), cursor, delimited) + + def dotted(s: String, cursor: Int): Parsed = new Parsed(onull(s), cursor, _ == '.') +} diff --git a/src/repl/scala/tools/nsc/interpreter/Pasted.scala b/src/repl/scala/tools/nsc/interpreter/Pasted.scala new file mode 100644 index 0000000000..f5db3d9e3a --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/Pasted.scala @@ -0,0 +1,101 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +/** If it looks like they're pasting in a scala interpreter + * transcript, remove all the formatting we inserted so we + * can make some sense of it. + * + * Most of the interesting code in here is due to my goal of + * "paste idempotence" i.e. the transcript resulting from pasting + * a transcript should itself be pasteable and should achieve + * the same result. + */ +abstract class Pasted { + def ContinueString: String + def PromptString: String + def interpret(line: String): Unit + + def matchesPrompt(line: String) = matchesString(line, PromptString) + def matchesContinue(line: String) = matchesString(line, ContinueString) + def running = isRunning + + private def matchesString(line: String, target: String): Boolean = ( + (line startsWith target) || + (line.nonEmpty && " \t".toSet(line.head) && matchesString(line.tail, target)) + ) + private def stripString(line: String, target: String) = line indexOf target match { + case -1 => line + case idx => line drop (idx + target.length) + } + private var isRunning = false + private val resReference = """(?<!^)(res\d+)""".r + private val resCreation = """^\s*(res\d+):.*""".r + private val resAssign = """^val (res\d+).*""".r + + private class PasteAnalyzer(val lines: List[String]) { + val referenced = lines flatMap (resReference findAllIn _.trim.stripPrefix("res")) toSet + val cmds = lines reduceLeft append split PromptString filterNot (_.trim == "") toList + + /** If it's a prompt or continuation line, strip the formatting bits and + * assemble the code. Otherwise ship it off to be analyzed for res references + * and discarded. + */ + def append(code: String, line: String): String = + if (matchesPrompt(line)) code + "\n" + line + else if (matchesContinue(line)) code + "\n" + stripString(line, ContinueString) + else fixResRefs(code, line) + + /** If the line looks like + * res15: Int + * + * and the additional conditions hold that: + * 1) res15 is referenced from elsewhere in the transcript + * 2) the preceding repl line is not "val res15 = ..." because that + * indicates it has already been "val-ified" on a previous paste + * + * then we go back in time to the preceding scala> prompt and + * rewrite the line containing <expr> as + * val res15 = { <expr> } + * and the rest as they say is rewritten history. + * + * In all other cases, discard the line. + */ + def fixResRefs(code: String, line: String) = line match { + case resCreation(resName) if referenced(resName) => + code.lastIndexOf(PromptString) match { + case -1 => code + case idx => + val (str1, str2) = code splitAt (idx + PromptString.length) + str2 match { + case resAssign(`resName`) => code + case _ => "%sval %s = { %s }".format(str1, resName, str2) + } + } + case _ => code + } + + def run() { + println("// Replaying %d commands from transcript.\n" format cmds.size) + cmds foreach { cmd => + print(PromptString) + interpret(cmd) + } + } + } + + /** Commands start on lines beginning with "scala>" and each successive + * line which begins with the continuation string is appended to that command. + * Everything else is discarded. When the end of the transcript is spotted, + * all the commands are replayed. + */ + def apply(lines: TraversableOnce[String]) = { + isRunning = true + try new PasteAnalyzer(lines.toList) run() + finally isRunning = false + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/Phased.scala b/src/repl/scala/tools/nsc/interpreter/Phased.scala new file mode 100644 index 0000000000..f625124e70 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/Phased.scala @@ -0,0 +1,143 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.collection.immutable +import scala.language.implicitConversions + +/** Mix this into an object and use it as a phasing + * swiss army knife. + */ +trait Phased { + val global: Global + import global._ + + private var active: PhaseName = NoPhaseName + private var multi: Seq[PhaseName] = Nil + + def get = active + def set(phase: PhaseName): Boolean = phase match { + case NoPhaseName => false + case name => active = name ; true + } + def setMulti(phases: Seq[PhaseName]): Boolean = { + if (phases contains NoPhaseName) false + else { + multi = phases + true + } + } + + private def parsePhaseChange(str: String): Option[Int] = { + if (str == "") Some(0) + else if (str startsWith ".prev") parsePhaseChange(str drop 5) map (_ - 1) + else if (str startsWith ".next") parsePhaseChange(str drop 5) map (_ + 1) + else str.head match { + case '+' | '-' => + val (num, rest) = str.tail.span(_.isDigit) + val diff = if (str.head == '+') num.toInt else -num.toInt + parsePhaseChange(rest) map (_ + diff) + case _ => + None + } + } + + /** Takes a string like 4, typer+2, typer.next, etc. + * and turns it into a PhaseName instance. + */ + private def parseInternal(str: String): PhaseName = { + if (str == "") NoPhaseName + else if (str forall (_.isDigit)) PhaseName(str.toInt) + else { + val (name, rest) = str.toLowerCase span (_.isLetter) + val start = PhaseName(name) + val change = parsePhaseChange(rest) + + if (start.isEmpty || change.isEmpty) NoPhaseName + else PhaseName(start.id + change.get) + } + } + def parse(str: String): PhaseName = + try parseInternal(str) + catch { case _: Exception => NoPhaseName } + + def atCurrent[T](body: => T): T = enteringPhase(get)(body) + def multi[T](body: => T): Seq[T] = multi map (ph => at(ph)(body)) + + def at[T](ph: PhaseName)(body: => T): T = { + val saved = get + set(ph) + try atCurrent(body) + finally set(saved) + } + def atMulti[T](phs: Seq[PhaseName])(body: => T): Seq[T] = { + val saved = multi + setMulti(phs) + try multi(body) + finally setMulti(saved) + } + + def atMap[T](phs: Seq[PhaseName])(body: => T): Seq[(PhaseName, T)] = + phs zip atMulti(phs)(body) + + object PhaseName { + implicit lazy val phaseNameOrdering: Ordering[PhaseName] = Ordering[Int] on (_.id) + + lazy val all = List( + Parser, Namer, Packageobjects, Typer, Superaccessors, Pickler, Refchecks, + Selectiveanf, Liftcode, Selectivecps, Uncurry, Tailcalls, Specialize, + Explicitouter, Erasure, Lazyvals, Lambdalift, Constructors, Flatten, Mixin, + Cleanup, Icode, Inliner, Closelim, Dce, Jvm, Terminal + ) + lazy val nameMap = all.map(x => x.name -> x).toMap withDefaultValue NoPhaseName + multi = all + + def apply(id: Int): PhaseName = all find (_.id == id) getOrElse NoPhaseName + implicit def apply(s: String): PhaseName = nameMap(s) + } + sealed abstract class PhaseName { + lazy val id = phase.id + lazy val name = toString.toLowerCase + def phase = currentRun.phaseNamed(name) + def isEmpty = this eq NoPhaseName + } + + case object Parser extends PhaseName + case object Namer extends PhaseName + case object Packageobjects extends PhaseName + case object Typer extends PhaseName + case object Superaccessors extends PhaseName + case object Pickler extends PhaseName + case object Refchecks extends PhaseName + case object Selectiveanf extends PhaseName + case object Liftcode extends PhaseName + case object Selectivecps extends PhaseName + case object Uncurry extends PhaseName + case object Tailcalls extends PhaseName + case object Specialize extends PhaseName + case object Explicitouter extends PhaseName + case object Erasure extends PhaseName + case object Lazyvals extends PhaseName + case object Lambdalift extends PhaseName + case object Constructors extends PhaseName + case object Flatten extends PhaseName + case object Mixin extends PhaseName + case object Cleanup extends PhaseName + case object Icode extends PhaseName + case object Inliner extends PhaseName + case object Closelim extends PhaseName + case object Dce extends PhaseName + case object Jvm extends PhaseName + case object Terminal extends PhaseName + case object NoPhaseName extends PhaseName { + override lazy val id = -1 + override lazy val name = phase.name + override def phase = NoPhase + } + + implicit def phaseEnumToPhase(name: PhaseName): Phase = name.phase +} diff --git a/src/repl/scala/tools/nsc/interpreter/Power.scala b/src/repl/scala/tools/nsc/interpreter/Power.scala new file mode 100644 index 0000000000..e517a16b32 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/Power.scala @@ -0,0 +1,326 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.collection.{ mutable, immutable } +import scala.util.matching.Regex +import scala.io.Codec +import java.net.{ URL, MalformedURLException } +import io.{ Path } +import scala.language.implicitConversions +import scala.reflect.runtime.{universe => ru} +import scala.reflect.{ClassTag, classTag} + +/** Collecting some power mode examples. + +scala> trait F[@specialized(Int) T] { def f: T = ??? } +defined trait F + +scala> trait G[@specialized(Long, Int) T] extends F[T] { override def f: T = super.f } +defined trait G + +scala> changesAfterEachPhase(intp("G").info.members filter (_.name.toString contains "super")) > +Gained after 1/parser { + method super$f +} + +Gained after 12/specialize { + method super$f$mcJ$sp + method super$f$mcI$sp +} + +Lost after 18/flatten { + method super$f$mcJ$sp + method super$f$mcI$sp + method super$f +} +*/ + +/** A class for methods to be injected into the intp in power mode. + */ +class Power[ReplValsImpl <: ReplVals : ru.TypeTag: ClassTag](val intp: IMain, replVals: ReplValsImpl) { + import intp.{ beQuietDuring, typeOfExpression, interpret, parse } + import intp.global._ + import definitions.{ compilerTypeFromTag, compilerSymbolFromTag} + + abstract class SymSlurper { + def isKeep(sym: Symbol): Boolean + def isIgnore(sym: Symbol): Boolean + def isRecur(sym: Symbol): Boolean + def isFinished(): Boolean + + val keep = mutable.HashSet[Symbol]() + val seen = mutable.HashSet[Symbol]() + def processed = keep.size + seen.size + def discarded = seen.size - keep.size + + def members(x: Symbol): List[Symbol] = + if (x.rawInfo.isComplete) x.info.members.toList + else Nil + + var lastCount = -1 + var pass = 0 + val unseenHistory = new mutable.ListBuffer[Int] + + def loop(todo: Set[Symbol]): Set[Symbol] = { + pass += 1 + val (repeats, unseen) = todo partition seen + unseenHistory += unseen.size + if (settings.verbose.value) { + println("%3d %s accumulated, %s discarded. This pass: %s unseen, %s repeats".format( + pass, keep.size, discarded, unseen.size, repeats.size)) + } + if (lastCount == processed || unseen.isEmpty || isFinished()) + return keep.toSet + + lastCount = processed + keep ++= (unseen filter isKeep filterNot isIgnore) + seen ++= unseen + loop(unseen filter isRecur flatMap members) + } + + def apply(sym: Symbol): Set[Symbol] = { + keep.clear() + seen.clear() + loop(Set(sym)) + } + } + + class PackageSlurper(packageClass: Symbol) extends SymSlurper { + /** Looking for dwindling returns */ + def droppedEnough() = unseenHistory.size >= 4 && { + unseenHistory takeRight 4 sliding 2 forall { it => + val List(a, b) = it.toList + a > b + } + } + + def isRecur(sym: Symbol) = true + def isIgnore(sym: Symbol) = sym.isAnonOrRefinementClass || (sym.name.toString contains "$mc") + def isKeep(sym: Symbol) = sym.hasTransOwner(packageClass) + def isFinished() = droppedEnough() + def slurp() = { + if (packageClass.isPackageClass) + apply(packageClass) + else { + repldbg("Not a package class! " + packageClass) + Set() + } + } + } + + private def customBanner = replProps.powerBanner.option flatMap (f => io.File(f).safeSlurp()) + private def customInit = replProps.powerInitCode.option flatMap (f => io.File(f).safeSlurp()) + + def banner = customBanner getOrElse """ + |** Power User mode enabled - BEEP WHIR GYVE ** + |** :phase has been set to 'typer'. ** + |** scala.tools.nsc._ has been imported ** + |** global._, definitions._ also imported ** + |** Try :help, :vals, power.<tab> ** + """.stripMargin.trim + + private def initImports = List( + "scala.tools.nsc._", + "scala.collection.JavaConverters._", + "intp.global.{ error => _, _ }", + "definitions.{ getClass => _, _ }", + "power.rutil._", + "replImplicits._", + "treedsl.CODE._" + ) + + def init = customInit match { + case Some(x) => x + case _ => initImports.mkString("import ", ", ", "") + } + + /** Starts up power mode and runs whatever is in init. + */ + def unleash(): Unit = beQuietDuring { + // First we create the ReplVals instance and bind it to $r + intp.bind("$r", replVals) + // Then we import everything from $r. + intp interpret ("import " + intp.originalPath("$r") + "._") + // And whatever else there is to do. + init.lines foreach (intp interpret _) + } + + trait LowPriorityInternalInfo { + implicit def apply[T: ru.TypeTag : ClassTag] : InternalInfo[T] = new InternalInfo[T](None) + } + object InternalInfo extends LowPriorityInternalInfo { } + + /** Now dealing with the problem of acidentally calling a method on Type + * when you're holding a Symbol and seeing the Symbol converted to the + * type of Symbol rather than the type of the thing represented by the + * symbol, by only implicitly installing one method, "?", and the rest + * of the conveniences exist on that wrapper. + */ + trait LowPriorityInternalInfoWrapper { } + class InternalInfoWrapper[T: ru.TypeTag : ClassTag](value: Option[T] = None) { + def ? : InternalInfo[T] = new InternalInfo[T](value) + } + + /** Todos... + * translate tag type arguments into applied types + * customizable symbol filter (had to hardcode no-spec to reduce noise) + */ + class InternalInfo[T](value: Option[T] = None)(implicit typeEvidence: ru.TypeTag[T], runtimeClassEvidence: ClassTag[T]) { + private def isSpecialized(s: Symbol) = s.name.toString contains "$mc" + private def isImplClass(s: Symbol) = s.name.toString endsWith "$class" + + /** Standard noise reduction filter. */ + def excludeMember(s: Symbol) = ( + isSpecialized(s) + || isImplClass(s) + || s.isAnonOrRefinementClass + || s.isAnonymousFunction + ) + def symbol = compilerSymbolFromTag(tag) + def tpe = compilerTypeFromTag(tag) + def members = membersUnabridged filterNot excludeMember + def membersUnabridged = tpe.members.toList + def pkg = symbol.enclosingPackage + def tag = typeEvidence + def runtimeClass = runtimeClassEvidence.runtimeClass + def shortClass = runtimeClass.getName split "[$.]" last + def baseClasses = tpe.baseClasses + + override def toString = value match { + case Some(x) => "%s (%s)".format(x, shortClass) + case _ => runtimeClass.getName + } + } + + trait LowPriorityPrettifier { + implicit object AnyPrettifier extends Prettifier[Any] { + def show(x: Any): Unit = prettify(x) foreach println + def prettify(x: Any): TraversableOnce[String] = x match { + case x: Name => List(x.decode) + case Tuple2(k, v) => List(prettify(k).toIterator ++ Iterator("->") ++ prettify(v) mkString " ") + case xs: Array[_] => xs.iterator flatMap prettify + case xs: TraversableOnce[_] => xs flatMap prettify + case x => List(Prettifier.stringOf(x)) + } + } + } + object StringPrettifier extends Prettifier[String] { + def show(x: String) = println(x) + def prettify(x: String) = List(Prettifier stringOf x) + } + object Prettifier extends LowPriorityPrettifier { + def stringOf(x: Any): String = scala.runtime.ScalaRunTime.stringOf(x) + def default[T] = new Prettifier[T] { + def prettify(x: T): TraversableOnce[String] = AnyPrettifier prettify x + def show(x: T): Unit = AnyPrettifier show x + } + } + trait Prettifier[T] { + def show(x: T): Unit + def prettify(x: T): TraversableOnce[String] + + def prettify(xs: TraversableOnce[T]): TraversableOnce[String] = xs flatMap (x => prettify(x)) + } + + abstract class PrettifierClass[T: Prettifier]() { + val pretty = implicitly[Prettifier[T]] + def value: Seq[T] + + def pp(f: Seq[T] => Seq[T]): Unit = + pretty prettify f(value) foreach (StringPrettifier show _) + + def freq[U](p: T => U) = (value.toSeq groupBy p mapValues (_.size)).toList sortBy (-_._2) map (_.swap) + + def >>(implicit ord: Ordering[T]): Unit = pp(_.sorted) + def >!(): Unit = pp(_.distinct) + def >(): Unit = pp(identity) + } + + class MultiPrettifierClass[T: Prettifier](val value: Seq[T]) extends PrettifierClass[T]() { } + class SinglePrettifierClass[T: Prettifier](single: T) extends PrettifierClass[T]() { + val value = List(single) + } + + class RichReplString(s: String) { + // make an url out of the string + def u: URL = ( + if (s contains ":") new URL(s) + else if (new JFile(s) exists) new JFile(s).toURI.toURL + else new URL("http://" + s) + ) + } + class RichInputStream(in: InputStream)(implicit codec: Codec) { + def bytes(): Array[Byte] = io.Streamable.bytes(in) + def slurp(): String = io.Streamable.slurp(in) + def <<(): String = slurp() + } + class RichReplURL(url: URL)(implicit codec: Codec) { + def slurp(): String = io.Streamable.slurp(url) + } + + trait Implicits1 { + // fallback + implicit def replPrinting[T](x: T)(implicit pretty: Prettifier[T] = Prettifier.default[T]) = + new SinglePrettifierClass[T](x) + } + trait Implicits2 extends Implicits1 { + class RichSymbol(sym: Symbol) { + // convenient type application + def apply(targs: Type*): Type = typeRef(NoPrefix, sym, targs.toList) + } + object symbolSubtypeOrdering extends Ordering[Symbol] { + def compare(s1: Symbol, s2: Symbol) = + if (s1 eq s2) 0 + else if (s1 isLess s2) -1 + else 1 + } + implicit lazy val powerSymbolOrdering: Ordering[Symbol] = Ordering[Name] on (_.name) + implicit lazy val powerTypeOrdering: Ordering[Type] = Ordering[Symbol] on (_.typeSymbol) + + implicit def replInternalInfo[T: ru.TypeTag : ClassTag](x: T): InternalInfoWrapper[T] = new InternalInfoWrapper[T](Some(x)) + implicit def replEnhancedStrings(s: String): RichReplString = new RichReplString(s) + implicit def replMultiPrinting[T: Prettifier](xs: TraversableOnce[T]): MultiPrettifierClass[T] = + new MultiPrettifierClass[T](xs.toSeq) + implicit def replPrettifier[T] : Prettifier[T] = Prettifier.default[T] + implicit def replTypeApplication(sym: Symbol): RichSymbol = new RichSymbol(sym) + + implicit def replInputStream(in: InputStream)(implicit codec: Codec) = new RichInputStream(in) + implicit def replEnhancedURLs(url: URL)(implicit codec: Codec): RichReplURL = new RichReplURL(url)(codec) + } + + trait ReplUtilities { + def module[T: ru.TypeTag] = ru.typeOf[T].typeSymbol.suchThat(_.isPackage) + def clazz[T: ru.TypeTag] = ru.typeOf[T].typeSymbol.suchThat(_.isClass) + def info[T: ru.TypeTag : ClassTag] = InternalInfo[T] + def ?[T: ru.TypeTag : ClassTag] = InternalInfo[T] + def sanitize(s: String): String = sanitize(s.getBytes()) + def sanitize(s: Array[Byte]): String = (s map { + case x if x.toChar.isControl => '?' + case x => x.toChar + }).mkString + + def strings(s: Seq[Byte]): List[String] = { + if (s.length == 0) Nil + else s dropWhile (_.toChar.isControl) span (x => !x.toChar.isControl) match { + case (next, rest) => next.map(_.toChar).mkString :: strings(rest) + } + } + } + + lazy val rutil: ReplUtilities = new ReplUtilities { } + lazy val phased: Phased = new { val global: intp.global.type = intp.global } with Phased { } + + def unit(code: String) = newCompilationUnit(code) + def trees(code: String) = parse(code) getOrElse Nil + + override def toString = s""" + |** Power mode status ** + |Default phase: ${phased.get} + |Names: ${intp.unqualifiedIds mkString " "} + """.stripMargin +} diff --git a/src/repl/scala/tools/nsc/interpreter/ReplConfig.scala b/src/repl/scala/tools/nsc/interpreter/ReplConfig.scala new file mode 100644 index 0000000000..3392ea0b5e --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/ReplConfig.scala @@ -0,0 +1,49 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.util.control.ControlThrowable +import util.Exceptional.unwrap +import util.stackTraceString + +trait ReplConfig { + lazy val replProps = new ReplProps + + class TapMaker[T](x: T) { + def tapDebug(msg: => String): T = tap(x => repldbg(parens(x))) + def tap[U](f: T => U): T = { + f(x) + x + } + } + + private def parens(x: Any) = "(" + x + ")" + private def echo(msg: => String) = + try Console println msg + catch { case x: AssertionError => Console.println("Assertion error printing debugging output: " + x) } + + private[nsc] def repldbg(msg: => String) = if (isReplDebug) echo(msg) + private[nsc] def repltrace(msg: => String) = if (isReplTrace) echo(msg) + private[nsc] def replinfo(msg: => String) = if (isReplInfo) echo(msg) + + private[nsc] def logAndDiscard[T](label: String, alt: => T): PartialFunction[Throwable, T] = { + case t: ControlThrowable => throw t + case t: Throwable => + repldbg(label + ": " + unwrap(t)) + repltrace(stackTraceString(unwrap(t))) + alt + } + private[nsc] def substituteAndLog[T](label: String, alt: => T)(body: => T): T = { + try body + catch logAndDiscard(label, alt) + } + + def isReplTrace: Boolean = replProps.trace + def isReplDebug: Boolean = replProps.debug || isReplTrace + def isReplInfo: Boolean = replProps.info || isReplDebug + def isReplPower: Boolean = replProps.power +} diff --git a/src/repl/scala/tools/nsc/interpreter/ReplDir.scala b/src/repl/scala/tools/nsc/interpreter/ReplDir.scala new file mode 100644 index 0000000000..5d386b47b7 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/ReplDir.scala @@ -0,0 +1,48 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import io.VirtualDirectory +import settings.MutableSettings +import scala.reflect.io.{ AbstractFile, PlainDirectory, Directory } +import scala.collection.generic.Clearable + +/** Directory to save .class files to. */ +trait ReplDir extends AbstractFile with Clearable { } + +private class ReplVirtualDir() extends VirtualDirectory("(memory)", None) with ReplDir { } +private class ReplRealDir(dir: Directory) extends PlainDirectory(dir) with ReplDir { + def clear() = { + dir.deleteRecursively() + dir.createDirectory() + } +} + +class ReplOutput(val dirSetting: MutableSettings#StringSetting) { + // outdir for generated classfiles - may be in-memory (the default), + // a generated temporary directory, or a specified outdir. + val dir: ReplDir = ( + if (dirSetting.isDefault) + new ReplVirtualDir() + else if (dirSetting.value == "") + new ReplRealDir(Directory.makeTemp("repl")) + else + new ReplRealDir(Directory(dirSetting.value)) + ) + + // print the contents hierarchically + def show(out: JPrintWriter) = { + def pp(root: AbstractFile, indentLevel: Int) { + val label = root.name + val spaces = " " * indentLevel + out.println(spaces + label) + if (root.isDirectory) + root.toList sortBy (_.name) foreach (x => pp(x, indentLevel + 1)) + } + pp(dir, 0) + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/ReplGlobal.scala b/src/repl/scala/tools/nsc/interpreter/ReplGlobal.scala new file mode 100644 index 0000000000..51fab3082e --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/ReplGlobal.scala @@ -0,0 +1,64 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import typechecker.Analyzer + +/** A layer on top of Global so I can guarantee some extra + * functionality for the repl. It doesn't do much yet. + */ +trait ReplGlobal extends Global { + // This exists mostly because using the reporter too early leads to deadlock. + private def echo(msg: String) { Console println msg } + + override def abort(msg: String): Nothing = { + echo("ReplGlobal.abort: " + msg) + super.abort(msg) + } + + override lazy val analyzer = new { + val global: ReplGlobal.this.type = ReplGlobal.this + } with Analyzer { + + override protected def findMacroClassLoader(): ClassLoader = { + val loader = super.findMacroClassLoader + macroLogVerbose("macro classloader: initializing from a REPL classloader: %s".format(global.classPath.asURLs)) + val virtualDirectory = globalSettings.outputDirs.getSingleOutput.get + new util.AbstractFileClassLoader(virtualDirectory, loader) {} + } + + override def newTyper(context: Context): Typer = new Typer(context) { + override def typed(tree: Tree, mode: Mode, pt: Type): Tree = { + val res = super.typed(tree, mode, pt) + tree match { + case Ident(name) if !tree.symbol.hasPackageFlag && !name.toString.startsWith("$") => + repldbg("typed %s: %s".format(name, res.tpe)) + case _ => + } + res + } + } + } + + object replPhase extends SubComponent { + val global: ReplGlobal.this.type = ReplGlobal.this + val phaseName = "repl" + val runsAfter = List[String]("typer") + val runsRightAfter = None + def newPhase(_prev: Phase): StdPhase = new StdPhase(_prev) { + def apply(unit: CompilationUnit) { + repldbg("Running replPhase on " + unit.body) + // newNamer(rootContext(unit)).enterSym(unit.body) + } + } + } + + override protected def computePhaseDescriptors: List[SubComponent] = { + addToPhasesSet(replPhase, "repl") + super.computePhaseDescriptors + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/ReplProps.scala b/src/repl/scala/tools/nsc/interpreter/ReplProps.scala new file mode 100644 index 0000000000..2364918494 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/ReplProps.scala @@ -0,0 +1,27 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.sys._ +import Prop._ + +class ReplProps { + private def bool(name: String) = BooleanProp.keyExists(name) + private def int(name: String) = IntProp(name) + + val info = bool("scala.repl.info") + val debug = bool("scala.repl.debug") + val trace = bool("scala.repl.trace") + val power = bool("scala.repl.power") + + val replAutorunCode = Prop[JFile]("scala.repl.autoruncode") + val powerInitCode = Prop[JFile]("scala.repl.power.initcode") + val powerBanner = Prop[JFile]("scala.repl.power.banner") + + val vids = bool("scala.repl.vids") + val maxPrintString = int("scala.repl.maxprintstring") +} diff --git a/src/repl/scala/tools/nsc/interpreter/ReplReporter.scala b/src/repl/scala/tools/nsc/interpreter/ReplReporter.scala new file mode 100644 index 0000000000..b20166d070 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/ReplReporter.scala @@ -0,0 +1,34 @@ +/* NSC -- new Scala compiler + * Copyright 2002-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import reporters._ +import IMain._ + +/** Like ReplGlobal, a layer for ensuring extra functionality. + */ +class ReplReporter(intp: IMain) extends ConsoleReporter(intp.settings, Console.in, new ReplStrippingWriter(intp)) { + def printUntruncatedMessage(msg: String) = withoutTruncating(printMessage(msg)) + + override def printMessage(msg: String) { + // Avoiding deadlock if the compiler starts logging before + // the lazy val is complete. + if (intp.isInitializeComplete) { + if (intp.totalSilence) { + if (isReplTrace) + super.printMessage("[silent] " + msg) + } + else super.printMessage(msg) + } + else Console.println("[init] " + msg) + } + + override def displayPrompt() { + if (intp.totalSilence) () + else super.displayPrompt() + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/ReplStrings.scala b/src/repl/scala/tools/nsc/interpreter/ReplStrings.scala new file mode 100644 index 0000000000..08472bbc64 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/ReplStrings.scala @@ -0,0 +1,32 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.reflect.internal.Chars + +trait ReplStrings { + /** 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 = { + val res = new StringBuilder + for (c <- str) c match { + case '"' | '\'' | '\\' => res += '\\' ; res += c + case _ if c.isControl => res ++= Chars.char2uescape(c) + case _ => res += c + } + res.toString + } + + def string2codeQuoted(str: String) = + "\"" + string2code(str) + "\"" + + def any2stringOf(x: Any, maxlen: Int) = + "scala.runtime.ScalaRunTime.replStringOf(%s, %s)".format(x, maxlen) + + def words(s: String) = s.trim split "\\s+" filterNot (_ == "") toList +} diff --git a/src/repl/scala/tools/nsc/interpreter/ReplVals.scala b/src/repl/scala/tools/nsc/interpreter/ReplVals.scala new file mode 100644 index 0000000000..ea100b25f2 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/ReplVals.scala @@ -0,0 +1,82 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.language.implicitConversions +import scala.reflect.api.{Universe => ApiUniverse} +import scala.reflect.runtime.{universe => ru} + +/** A class which the repl utilizes to expose predefined objects. + * The base implementation is empty; the standard repl implementation + * is StdReplVals. + */ +abstract class ReplVals { } + +class StdReplVals(final val r: ILoop) extends ReplVals { + final lazy val repl = r + final lazy val intp = r.intp + final lazy val power = r.power + final lazy val reader = r.in + final lazy val vals = this + final lazy val global: intp.global.type = intp.global + final lazy val isettings = intp.isettings + final lazy val completion = reader.completion + final lazy val history = reader.history + final lazy val phased = power.phased + final lazy val analyzer = global.analyzer + + object treedsl extends { val global: intp.global.type = intp.global } with ast.TreeDSL { } + + final lazy val typer = analyzer.newTyper( + analyzer.rootContext( + power.unit("").asInstanceOf[analyzer.global.CompilationUnit] + ) + ) + def lastRequest = intp.lastRequest + + class ReplImplicits extends power.Implicits2 { + import intp.global._ + + private val tagFn = ReplVals.mkCompilerTypeFromTag[intp.global.type](global) + implicit def mkCompilerTypeFromTag(sym: Symbol) = tagFn(sym) + } + + final lazy val replImplicits = new ReplImplicits + + def typed[T <: analyzer.global.Tree](tree: T): T = typer.typed(tree).asInstanceOf[T] +} + +object ReplVals { + /** Latest attempt to work around the challenge of foo.global.Type + * not being seen as the same type as bar.global.Type even though + * the globals are the same. Dependent method types to the rescue. + */ + def mkCompilerTypeFromTag[T <: Global](global: T) = { + import global._ + + /** We can't use definitions.compilerTypeFromTag directly because we're passing + * it to map and the compiler refuses to perform eta expansion on a method + * with a dependent return type. (Can this be relaxed?) To get around this + * I have this forwarder which widens the type and then cast the result back + * to the dependent type. + */ + def compilerTypeFromTag(t: ApiUniverse # WeakTypeTag[_]): Global#Type = + definitions.compilerTypeFromTag(t) + + class AppliedTypeFromTags(sym: Symbol) { + def apply[M](implicit m1: ru.TypeTag[M]): Type = + if (sym eq NoSymbol) NoType + else appliedType(sym, compilerTypeFromTag(m1).asInstanceOf[Type]) + + def apply[M1, M2](implicit m1: ru.TypeTag[M1], m2: ru.TypeTag[M2]): Type = + if (sym eq NoSymbol) NoType + else appliedType(sym, compilerTypeFromTag(m1).asInstanceOf[Type], compilerTypeFromTag(m2).asInstanceOf[Type]) + } + + (sym: Symbol) => new AppliedTypeFromTags(sym) + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/Results.scala b/src/repl/scala/tools/nsc/interpreter/Results.scala new file mode 100644 index 0000000000..e400906a58 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/Results.scala @@ -0,0 +1,22 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package interpreter + +object Results { + /** A result from the Interpreter 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/repl/scala/tools/nsc/interpreter/RichClass.scala b/src/repl/scala/tools/nsc/interpreter/RichClass.scala new file mode 100644 index 0000000000..36cdf65510 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/RichClass.scala @@ -0,0 +1,36 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.reflect.{ ClassTag, classTag } + +class RichClass[T](val clazz: Class[T]) { + def toTag: ClassTag[T] = ClassTag[T](clazz) + + // Sadly isAnonymousClass does not return true for scala anonymous + // classes because our naming scheme is not doing well against the + // jvm's many assumptions. + def isScalaAnonymous = ( + try clazz.isAnonymousClass || (clazz.getName contains "$anon$") + catch { case _: java.lang.InternalError => false } // good ol' "Malformed class name" + ) + + def supertags: List[ClassTag[_]] = supers map (_.toTag) + def superNames: List[String] = supers map (_.getName) + def interfaces: List[JClass] = supers filter (_.isInterface) + + def hasAncestorName(f: String => Boolean) = superNames exists f + def hasAncestor(f: JClass => Boolean) = supers exists f + + def supers: List[JClass] = { + def loop(x: JClass): List[JClass] = x.getSuperclass match { + case null => List(x) + case sc => x :: (x.getInterfaces.toList flatMap loop) ++ loop(sc) + } + loop(clazz).distinct + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/SimpleReader.scala b/src/repl/scala/tools/nsc/interpreter/SimpleReader.scala new file mode 100644 index 0000000000..2d0917d91f --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/SimpleReader.scala @@ -0,0 +1,40 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Stepan Koltsov + */ + +package scala.tools.nsc +package interpreter + +import java.io.{ BufferedReader } +import session.NoHistory + +/** Reads using standard JDK API */ +class SimpleReader( + in: BufferedReader, + out: JPrintWriter, + val interactive: Boolean) +extends InteractiveReader +{ + val history = NoHistory + val completion = NoCompletion + + def reset() = () + def redrawLine() = () + def readOneLine(prompt: String): String = { + if (interactive) { + out.print(prompt) + out.flush() + } + in.readLine() + } + def readOneKey(prompt: String) = sys.error("No char-based input in SimpleReader") +} + +object SimpleReader { + def defaultIn = Console.in + def defaultOut = new JPrintWriter(Console.out) + + def apply(in: BufferedReader = defaultIn, out: JPrintWriter = defaultOut, interactive: Boolean = true): SimpleReader = + new SimpleReader(in, out, interactive) +} diff --git a/src/repl/scala/tools/nsc/interpreter/StdReplTags.scala b/src/repl/scala/tools/nsc/interpreter/StdReplTags.scala new file mode 100644 index 0000000000..ebbb397a0c --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/StdReplTags.scala @@ -0,0 +1,15 @@ +package scala.tools.nsc +package interpreter + +import scala.tools.reflect.StdTags +import scala.reflect.runtime.{ universe => ru } + +trait StdReplTags extends StdTags { + lazy val tagOfStdReplVals = tagOfStaticClass[StdReplVals] + lazy val tagOfIMain = tagOfStaticClass[IMain] +} + +object StdReplTags extends StdTags with StdReplTags { + val u: ru.type = ru + val m = u.runtimeMirror(getClass.getClassLoader) +} diff --git a/src/repl/scala/tools/nsc/interpreter/package.scala b/src/repl/scala/tools/nsc/interpreter/package.scala new file mode 100644 index 0000000000..52a085080b --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/package.scala @@ -0,0 +1,157 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc + +import scala.language.implicitConversions +import scala.reflect.{ classTag, ClassTag } +import scala.reflect.runtime.{ universe => ru } +import scala.reflect.{ClassTag, classTag} +import scala.reflect.api.{Mirror, TypeCreator, Universe => ApiUniverse} + +/** The main REPL related classes and values are as follows. + * In addition to standard compiler classes Global and Settings, there are: + * + * History: an interface for session history. + * Completion: an interface for tab completion. + * ILoop (formerly InterpreterLoop): The umbrella class for a session. + * IMain (formerly Interpreter): Handles the evolving state of the session + * and handles submitting code to the compiler and handling the output. + * InteractiveReader: how ILoop obtains input. + * History: an interface for session history. + * Completion: an interface for tab completion. + * Power: a repository for more advanced/experimental features. + * + * ILoop contains { in: InteractiveReader, intp: IMain, settings: Settings, power: Power } + * InteractiveReader contains { history: History, completion: Completion } + * IMain contains { global: Global } + */ +package object interpreter extends ReplConfig with ReplStrings { + type JFile = java.io.File + type JClass = java.lang.Class[_] + type JList[T] = java.util.List[T] + type JCollection[T] = java.util.Collection[T] + type JPrintWriter = java.io.PrintWriter + type InputStream = java.io.InputStream + type OutputStream = java.io.OutputStream + + val IR = Results + + implicit def postfixOps = scala.language.postfixOps // make all postfix ops in this package compile without warning + + private[interpreter] implicit def javaCharSeqCollectionToScala(xs: JCollection[_ <: CharSequence]): List[String] = { + import scala.collection.JavaConverters._ + xs.asScala.toList map ("" + _) + } + + private[nsc] implicit def enrichClass[T](clazz: Class[T]) = new RichClass[T](clazz) + private[nsc] implicit def enrichAnyRefWithTap[T](x: T) = new TapMaker(x) + private[nsc] def debugging[T](msg: String)(x: T) = x.tapDebug(msg) + + private val ourClassloader = getClass.getClassLoader + + def staticTypeTag[T: ClassTag]: ru.TypeTag[T] = ru.TypeTag[T]( + ru.runtimeMirror(ourClassloader), + new TypeCreator { + def apply[U <: ApiUniverse with Singleton](m: Mirror[U]): U # Type = + m.staticClass(classTag[T].runtimeClass.getName).toTypeConstructor.asInstanceOf[U # Type] + }) + + /** This class serves to trick the compiler into treating a var + * (intp, in ILoop) as a stable identifier. + */ + implicit class IMainOps(val intp: IMain) { + import intp._ + import global.{ reporter => _, _ } + import definitions._ + + protected def echo(msg: String) = { + Console.out println msg + Console.out.flush() + } + + def implicitsCommand(line: String): String = { + def p(x: Any) = intp.reporter.printMessage("" + x) + + // If an argument is given, only show a source with that + // in its name somewhere. + val args = line split "\\s+" + val filtered = intp.implicitSymbolsBySource filter { + case (source, syms) => + (args contains "-v") || { + if (line == "") (source.fullName.toString != "scala.Predef") + else (args exists (source.name.toString contains _)) + } + } + + if (filtered.isEmpty) + return "No implicits have been imported other than those in Predef." + + filtered foreach { + case (source, syms) => + p("/* " + syms.size + " implicit members imported from " + source.fullName + " */") + + // This groups the members by where the symbol is defined + val byOwner = syms groupBy (_.owner) + val sortedOwners = byOwner.toList sortBy { case (owner, _) => exitingTyper(source.info.baseClasses indexOf owner) } + + sortedOwners foreach { + case (owner, members) => + // Within each owner, we cluster results based on the final result type + // if there are more than a couple, and sort each cluster based on name. + // This is really just trying to make the 100 or so implicits imported + // by default into something readable. + val memberGroups: List[List[Symbol]] = { + val groups = members groupBy (_.tpe.finalResultType) toList + val (big, small) = groups partition (_._2.size > 3) + val xss = ( + (big sortBy (_._1.toString) map (_._2)) :+ + (small flatMap (_._2)) + ) + + xss map (xs => xs sortBy (_.name.toString)) + } + + val ownerMessage = if (owner == source) " defined in " else " inherited from " + p(" /* " + members.size + ownerMessage + owner.fullName + " */") + + memberGroups foreach { group => + group foreach (s => p(" " + intp.symbolDefString(s))) + p("") + } + } + p("") + } + "" + } + + /** TODO - + * -n normalize + * -l label with case class parameter names + * -c complete - leave nothing out + */ + def typeCommandInternal(expr: String, verbose: Boolean): Unit = + symbolOfLine(expr) andAlso (echoTypeSignature(_, verbose)) + + def printAfterTyper(msg: => String) = + reporter printUntruncatedMessage exitingTyper(msg) + + private def replInfo(sym: Symbol) = + if (sym.isAccessor) dropNullaryMethod(sym.info) else sym.info + + def echoTypeStructure(sym: Symbol) = + printAfterTyper("" + deconstruct.show(replInfo(sym))) + + def echoTypeSignature(sym: Symbol, verbose: Boolean) = { + if (verbose) echo("// Type signature") + printAfterTyper("" + replInfo(sym)) + + if (verbose) { + echo("\n// Internal Type structure") + echoTypeStructure(sym) + } + } + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/session/FileBackedHistory.scala b/src/repl/scala/tools/nsc/interpreter/session/FileBackedHistory.scala new file mode 100644 index 0000000000..dddfb1b8f6 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/session/FileBackedHistory.scala @@ -0,0 +1,84 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter +package session + +import scala.tools.nsc.io._ +import FileBackedHistory._ + +/** TODO: file locking. + */ +trait FileBackedHistory extends JLineHistory with JPersistentHistory { + def maxSize: Int + protected lazy val historyFile: File = defaultFile + private var isPersistent = true + + locally { + load() + } + + def withoutSaving[T](op: => T): T = { + val saved = isPersistent + isPersistent = false + try op + finally isPersistent = saved + } + def addLineToFile(item: CharSequence): Unit = { + if (isPersistent) + append(item + "\n") + } + + /** Overwrites the history file with the current memory. */ + protected def sync(): Unit = { + val lines = asStrings map (_ + "\n") + historyFile.writeAll(lines: _*) + } + /** Append one or more lines to the history file. */ + protected def append(lines: String*): Unit = { + historyFile.appendAll(lines: _*) + } + + def load(): Unit = { + if (!historyFile.canRead) + historyFile.createFile() + + val lines: IndexedSeq[String] = { + try historyFile.lines().toIndexedSeq + catch { + // It seems that control characters in the history file combined + // with the default codec can lead to nio spewing exceptions. Rather + // than abandon hope we'll try to read it as ISO-8859-1 + case _: Exception => + try historyFile.lines("ISO-8859-1").toIndexedSeq + catch { case _: Exception => Vector() } + } + } + + repldbg("Loading " + lines.size + " into history.") + + // avoid writing to the history file + withoutSaving(lines takeRight maxSize foreach add) + // truncate the history file if it's too big. + if (lines.size > maxSize) { + repldbg("File exceeds maximum size: truncating to " + maxSize + " entries.") + sync() + } + moveToEnd() + } + + def flush(): Unit = () + def purge(): Unit = historyFile.truncate() +} + +object FileBackedHistory { + // val ContinuationChar = '\003' + // val ContinuationNL: String = Array('\003', '\n').mkString + import Properties.userHome + + def defaultFileName = ".scala_history" + def defaultFile: File = File(Path(userHome) / defaultFileName) +} diff --git a/src/repl/scala/tools/nsc/interpreter/session/History.scala b/src/repl/scala/tools/nsc/interpreter/session/History.scala new file mode 100644 index 0000000000..794d41adc7 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/session/History.scala @@ -0,0 +1,22 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter +package session + +/** An implementation-agnostic history interface which makes no + * reference to the jline classes. Very sparse right now. + */ +trait History { + def asStrings: List[String] + def index: Int + def size: Int +} +object NoHistory extends History { + def asStrings = Nil + def index = 0 + def size = 0 +} diff --git a/src/repl/scala/tools/nsc/interpreter/session/JLineHistory.scala b/src/repl/scala/tools/nsc/interpreter/session/JLineHistory.scala new file mode 100644 index 0000000000..18e0ee7c85 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/session/JLineHistory.scala @@ -0,0 +1,49 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter +package session + +/** A straight scalification of the jline interface which mixes + * in the sparse jline-independent one too. + */ +trait JLineHistory extends JHistory with History { + def size: Int + def isEmpty: Boolean + def index: Int + def clear(): Unit + def get(index: Int): CharSequence + def add(line: CharSequence): Unit + def replace(item: CharSequence): Unit + + def entries(index: Int): JListIterator[JEntry] + def entries(): JListIterator[JEntry] + def iterator: JIterator[JEntry] + + def current(): CharSequence + def previous(): Boolean + def next(): Boolean + def moveToFirst(): Boolean + def moveToLast(): Boolean + def moveTo(index: Int): Boolean + def moveToEnd(): Unit +} + +object JLineHistory { + class JLineFileHistory extends SimpleHistory with FileBackedHistory { + override def add(item: CharSequence): Unit = { + if (!isEmpty && last == item) + repldbg("Ignoring duplicate entry '" + item + "'") + else { + super.add(item) + addLineToFile(item) + } + } + override def toString = "History(size = " + size + ", index = " + index + ")" + } + + def apply(): JLineHistory = try new JLineFileHistory catch { case x: Exception => new SimpleHistory() } +} diff --git a/src/repl/scala/tools/nsc/interpreter/session/SimpleHistory.scala b/src/repl/scala/tools/nsc/interpreter/session/SimpleHistory.scala new file mode 100644 index 0000000000..89998e438a --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/session/SimpleHistory.scala @@ -0,0 +1,58 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter +package session + +import scala.collection.mutable.{ Buffer, ListBuffer } +import scala.collection.JavaConverters._ + +class SimpleHistory extends JLineHistory { + private var _index: Int = 0 + private val buf: Buffer[String] = new ListBuffer[String] + private def toEntries(): Seq[JEntry] = buf.zipWithIndex map { case (x, i) => Entry(i, x) } + private def setTo(num: Int) = { _index = num ; true } + private def minusOne = { _index -= 1 ; true } + private def plusOne = { _index += 1 ; true } + private def lastIndex = size - 1 + private def fail(msg: String): String = { + repldbg("Internal error in history(size %d, index %d): %s".format( + size, index, msg) + ) + "" + } + + case class Entry(index: Int, value: CharSequence) extends JEntry { + override def toString = value + } + + def maxSize: Int = 2500 + def last = if (isEmpty) fail("last") else buf.last + + def size = buf.size + def index = _index + def isEmpty = buf.isEmpty + def clear() = buf.clear() + def get(idx: Int): CharSequence = buf(idx) + def add(item: CharSequence): Unit = buf += item + def replace(item: CharSequence): Unit = { + buf trimEnd 1 + add(item) + } + def entries(idx: Int): JListIterator[JEntry] = toEntries().asJava.listIterator(idx) + def entries(): JListIterator[JEntry] = toEntries().asJava.listIterator() + def iterator: JIterator[JEntry] = toEntries().iterator.asJava + + def current() = if (index >= 0 && index < buf.size) buf(index) else fail("current()") + def previous() = (index > 0) && minusOne + def next() = (index <= lastIndex) && plusOne + def moveToFirst() = (size > 0) && (index != 0) && setTo(0) + def moveToLast() = (size > 0) && (index < lastIndex) && setTo(lastIndex) + def moveTo(idx: Int) = (idx > 0) && (idx <= lastIndex) && setTo(idx) + def moveToEnd(): Unit = setTo(size) + + def asStrings = buf.toList +} diff --git a/src/repl/scala/tools/nsc/interpreter/session/package.scala b/src/repl/scala/tools/nsc/interpreter/session/package.scala new file mode 100644 index 0000000000..c62cf21151 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/session/package.scala @@ -0,0 +1,23 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter +import scala.language.implicitConversions + +/** Files having to do with the state of a repl session: + * lines of text entered, types and terms defined, etc. + */ +package object session { + type JIterator[T] = java.util.Iterator[T] + type JListIterator[T] = java.util.ListIterator[T] + + type JEntry = scala.tools.jline.console.history.History.Entry + type JHistory = scala.tools.jline.console.history.History + type JMemoryHistory = scala.tools.jline.console.history.MemoryHistory + type JPersistentHistory = scala.tools.jline.console.history.PersistentHistory + + private[interpreter] implicit def charSequenceFix(x: CharSequence): String = x.toString +} |