diff options
36 files changed, 1556 insertions, 64 deletions
diff --git a/src/dotty/tools/backend/jvm/DottyBackendInterface.scala b/src/dotty/tools/backend/jvm/DottyBackendInterface.scala index b6adba85a..5776cc8e2 100644 --- a/src/dotty/tools/backend/jvm/DottyBackendInterface.scala +++ b/src/dotty/tools/backend/jvm/DottyBackendInterface.scala @@ -32,7 +32,7 @@ import NameOps._ import StdNames.nme import NameOps._ -class DottyBackendInterface()(implicit ctx: Context) extends BackendInterface{ +class DottyBackendInterface(outputDirectory: AbstractFile)(implicit ctx: Context) extends BackendInterface{ type Symbol = Symbols.Symbol type Type = Types.Type type Tree = tpd.Tree @@ -734,7 +734,7 @@ class DottyBackendInterface()(implicit ctx: Context) extends BackendInterface{ def setter(clz: Symbol): Symbol = decorateSymbol(sym).setter def moduleSuffix: String = "" // todo: validate that names already have $ suffix - def outputDirectory: AbstractFile = new PlainDirectory(new Directory(new JFile(ctx.settings.d.value))) + def outputDirectory: AbstractFile = DottyBackendInterface.this.outputDirectory def pos: Position = sym.pos def throwsAnnotations: List[Symbol] = Nil diff --git a/src/dotty/tools/backend/jvm/GenBCode.scala b/src/dotty/tools/backend/jvm/GenBCode.scala index e8d196ce7..2d444d3be 100644 --- a/src/dotty/tools/backend/jvm/GenBCode.scala +++ b/src/dotty/tools/backend/jvm/GenBCode.scala @@ -29,6 +29,7 @@ import scala.tools.asm.tree._ import dotty.tools.dotc.util.{Positions, DotClass} import tpd._ import StdNames._ +import scala.reflect.io.{Directory, PlainDirectory, AbstractFile} import scala.tools.nsc.backend.jvm.opt.LocalOpt @@ -37,9 +38,11 @@ class GenBCode extends Phase { private val entryPoints = new mutable.HashSet[Symbol]() def registerEntryPoint(sym: Symbol) = entryPoints += sym + def outputDir(implicit ctx: Context): AbstractFile = + new PlainDirectory(new Directory(new JFile(ctx.settings.d.value))) def run(implicit ctx: Context): Unit = { - new GenBCodePipeline(entryPoints.toList, new DottyBackendInterface()(ctx))(ctx).run(ctx.compilationUnit.tpdTree) + new GenBCodePipeline(entryPoints.toList, new DottyBackendInterface(outputDir)(ctx))(ctx).run(ctx.compilationUnit.tpdTree) entryPoints.clear() } } diff --git a/src/dotty/tools/dotc/Bench.scala b/src/dotty/tools/dotc/Bench.scala index 47b5fd6dd..2fc38d78c 100644 --- a/src/dotty/tools/dotc/Bench.scala +++ b/src/dotty/tools/dotc/Bench.scala @@ -12,7 +12,7 @@ object Bench extends Driver { @sharable private var numRuns = 1 - def newCompiler(): Compiler = new Compiler + def newCompiler(implicit ctx: Context): Compiler = new Compiler private def ntimes(n: Int)(op: => Reporter): Reporter = (emptyReporter /: (0 until n)) ((_, _) => op) diff --git a/src/dotty/tools/dotc/Compiler.scala b/src/dotty/tools/dotc/Compiler.scala index 199657864..99b7bd880 100644 --- a/src/dotty/tools/dotc/Compiler.scala +++ b/src/dotty/tools/dotc/Compiler.scala @@ -74,13 +74,13 @@ class Compiler { new CapturedVars, // capturedVars has a transformUnit: no phases should introduce local mutable vars here new Constructors, // constructors changes decls in transformTemplate, no InfoTransformers should be added after it new FunctionalInterfaces, - new GetClass), // getClass transformation should be applied to specialized methods + new GetClass), // getClass transformation should be applied to specialized methods List(new LambdaLift, // in this mini-phase block scopes are incorrect. No phases that rely on scopes should be here new ElimStaticThis, new Flatten, + new DropEmptyCompanions, new RestoreScopes), - List(/*new PrivateToStatic,*/ - new ExpandPrivate, + List(new ExpandPrivate, new CollectEntryPoints, new LabelDefs), List(new GenBCode) diff --git a/src/dotty/tools/dotc/Driver.scala b/src/dotty/tools/dotc/Driver.scala index 7f22fc774..3437b86fc 100644 --- a/src/dotty/tools/dotc/Driver.scala +++ b/src/dotty/tools/dotc/Driver.scala @@ -10,7 +10,7 @@ abstract class Driver extends DotClass { val prompt = "\ndotc> " - protected def newCompiler(): Compiler + protected def newCompiler(implicit ctx: Context): Compiler protected def emptyReporter: Reporter = new StoreReporter(null) @@ -90,7 +90,7 @@ abstract class Driver extends DotClass { */ def process(args: Array[String], rootCtx: Context): Reporter = { val (fileNames, ctx) = setup(args, rootCtx) - doCompile(newCompiler(), fileNames)(ctx) + doCompile(newCompiler(ctx), fileNames)(ctx) } def main(args: Array[String]): Unit = { diff --git a/src/dotty/tools/dotc/FromTasty.scala b/src/dotty/tools/dotc/FromTasty.scala index d8d8b8b1e..8f29c882c 100644 --- a/src/dotty/tools/dotc/FromTasty.scala +++ b/src/dotty/tools/dotc/FromTasty.scala @@ -30,7 +30,7 @@ import ast.tpd._ * scala dotty.tools.dotc.FromTasty -Xprint:front extMethods.T */ object FromTasty extends Driver { - override def newCompiler(): Compiler = new TASTYCompiler + override def newCompiler(implicit ctx: Context): Compiler = new TASTYCompiler class TASTYCompiler extends Compiler { diff --git a/src/dotty/tools/dotc/Main.scala b/src/dotty/tools/dotc/Main.scala index 699a57234..6c473d8bb 100644 --- a/src/dotty/tools/dotc/Main.scala +++ b/src/dotty/tools/dotc/Main.scala @@ -2,10 +2,9 @@ package dotty.tools package dotc import core.Contexts.Context -import reporting.Reporter /* To do: */ object Main extends Driver { - override def newCompiler(): Compiler = new Compiler + override def newCompiler(implicit ctx: Context): Compiler = new Compiler } diff --git a/src/dotty/tools/dotc/Resident.scala b/src/dotty/tools/dotc/Resident.scala index 3ae369f27..18bb2ff4f 100644 --- a/src/dotty/tools/dotc/Resident.scala +++ b/src/dotty/tools/dotc/Resident.scala @@ -25,7 +25,7 @@ class Resident extends Driver { object residentCompiler extends Compiler - override def newCompiler(): Compiler = ??? + override def newCompiler(implicit ctx: Context): Compiler = ??? override def sourcesRequired = false diff --git a/src/dotty/tools/dotc/Run.scala b/src/dotty/tools/dotc/Run.scala index ba86e3e70..39fd42a64 100644 --- a/src/dotty/tools/dotc/Run.scala +++ b/src/dotty/tools/dotc/Run.scala @@ -82,6 +82,9 @@ class Run(comp: Compiler)(implicit ctx: Context) { compileSources(List(new SourceFile(virtualFile))) } + /** The context created for this run */ + def runContext = ctx + /** Print summary; return # of errors encountered */ def printSummary(): Reporter = { ctx.runInfo.printMaxConstraint() diff --git a/src/dotty/tools/dotc/config/ScalaSettings.scala b/src/dotty/tools/dotc/config/ScalaSettings.scala index 035b20130..65bc9ba23 100644 --- a/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -89,7 +89,7 @@ class ScalaSettings extends Settings.SettingGroup { val showPhases = BooleanSetting("-Xshow-phases", "Print a synopsis of compiler phases.") val sourceReader = StringSetting("-Xsource-reader", "classname", "Specify a custom method for reading source files.", "") val XnoValueClasses = BooleanSetting("-Xno-value-classes", "Do not use value classes. Helps debugging.") - + val XreplLineWidth = IntSetting("-Xrepl-line-width", "Maximial number of columns per line for REPL output", 390) val XoldPatmat = BooleanSetting("-Xoldpatmat", "Use the pre-2.10 pattern matcher. Otherwise, the 'virtualizing' pattern matcher is used in 2.10.") val XnoPatmatAnalysis = BooleanSetting("-Xno-patmat-analysis", "Don't perform exhaustivity/unreachability analysis. Also, ignore @switch annotation.") val XfullLubs = BooleanSetting("-Xfull-lubs", "Retains pre 2.10 behavior of less aggressive truncation of least upper bounds.") diff --git a/src/dotty/tools/dotc/core/Denotations.scala b/src/dotty/tools/dotc/core/Denotations.scala index 2af3f463d..b52c11201 100644 --- a/src/dotty/tools/dotc/core/Denotations.scala +++ b/src/dotty/tools/dotc/core/Denotations.scala @@ -590,7 +590,10 @@ object Denotations { } while (d ne denot) this case _ => - if (coveredInterval.containsPhaseId(ctx.phaseId)) staleSymbolError + if (coveredInterval.containsPhaseId(ctx.phaseId)) { + if (ctx.debug) ctx.traceInvalid(this) + staleSymbolError + } else NoDenotation } diff --git a/src/dotty/tools/dotc/core/Flags.scala b/src/dotty/tools/dotc/core/Flags.scala index 8c9db3a5c..02b341649 100644 --- a/src/dotty/tools/dotc/core/Flags.scala +++ b/src/dotty/tools/dotc/core/Flags.scala @@ -606,6 +606,7 @@ object Flags { final val AbstractFinal = allOf(Abstract, Final) final val AbstractSealed = allOf(Abstract, Sealed) final val SyntheticArtifact = allOf(Synthetic, Artifact) + final val SyntheticModule = allOf(Synthetic, Module) final val SyntheticTermParam = allOf(Synthetic, TermParam) final val SyntheticTypeParam = allOf(Synthetic, TypeParam) final val SyntheticCase = allOf(Synthetic, Case) diff --git a/src/dotty/tools/dotc/core/NameOps.scala b/src/dotty/tools/dotc/core/NameOps.scala index 6c1930c9f..81240a9fc 100644 --- a/src/dotty/tools/dotc/core/NameOps.scala +++ b/src/dotty/tools/dotc/core/NameOps.scala @@ -82,6 +82,7 @@ object NameOps { def isModuleVarName(name: Name): Boolean = name.stripAnonNumberSuffix endsWith MODULE_VAR_SUFFIX def isSelectorName = name.startsWith(" ") && name.tail.forall(_.isDigit) + def isLazyLocal = name.endsWith(nme.LAZY_LOCAL) /** Is name a variable name? */ def isVariableName: Boolean = name.length > 0 && { @@ -423,5 +424,11 @@ object NameOps { case NO_NAME => primitivePostfixMethodName case name => name } + + def lazyLocalName = name ++ nme.LAZY_LOCAL + def nonLazyName = { + assert(name.isLazyLocal) + name.dropRight(nme.LAZY_LOCAL.length) + } } } diff --git a/src/dotty/tools/dotc/core/Phases.scala b/src/dotty/tools/dotc/core/Phases.scala index 970a9297a..83ac64d53 100644 --- a/src/dotty/tools/dotc/core/Phases.scala +++ b/src/dotty/tools/dotc/core/Phases.scala @@ -347,6 +347,13 @@ object Phases { override def toString = phaseName } + /** Replace all instances of `oldPhaseClass` in `current` phases + * by the result of `newPhases` applied to the old phase. + */ + def replace(oldPhaseClass: Class[_ <: Phase], newPhases: Phase => List[Phase], current: List[List[Phase]]): List[List[Phase]] = + current.map(_.flatMap(phase => + if (oldPhaseClass.isInstance(phase)) newPhases(phase) else phase :: Nil)) + /** Dotty deviation: getClass yields Class[_], instead of [Class <: <type of receiver>]. * We can get back the old behavior using this decorator. We should also use the same * trick for standard getClass. diff --git a/src/dotty/tools/dotc/core/SymDenotations.scala b/src/dotty/tools/dotc/core/SymDenotations.scala index 4495b4096..705e1a4fa 100644 --- a/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/src/dotty/tools/dotc/core/SymDenotations.scala @@ -45,19 +45,54 @@ trait SymDenotations { this: Context => if (denot.is(ValidForever) || denot.isRefinementClass) true else { val initial = denot.initial - if (initial ne denot) - ctx.withPhase(initial.validFor.firstPhaseId).stillValid(initial.asSymDenotation) - else try { - val owner = denot.owner.denot - stillValid(owner) && ( - !owner.isClass - || owner.isRefinementClass - || (owner.unforcedDecls.lookupAll(denot.name) contains denot.symbol) - || denot.isSelfSym) - } catch { - case ex: StaleSymbol => false + val firstPhaseId = initial.validFor.firstPhaseId.max(ctx.typerPhase.id) + if ((initial ne denot) || ctx.phaseId != firstPhaseId) + ctx.withPhase(firstPhaseId).stillValidInOwner(initial.asSymDenotation) + else + stillValidInOwner(denot) + } + + private[SymDenotations] def stillValidInOwner(denot: SymDenotation): Boolean = try { + val owner = denot.owner.denot + stillValid(owner) && ( + !owner.isClass + || owner.isRefinementClass + || (owner.unforcedDecls.lookupAll(denot.name) contains denot.symbol) + || denot.isSelfSym) + } catch { + case ex: StaleSymbol => false + } + + /** Explain why symbol is invalid; used for debugging only */ + def traceInvalid(denot: Denotation): Boolean = { + def show(d: Denotation) = s"$d#${d.symbol.id}" + def explain(msg: String) = { + println(s"${show(denot)} is invalid at ${this.period} because $msg") + false + } + denot match { + case denot: SymDenotation => + def explainSym(msg: String) = explain(s"$msg\n defined = ${denot.definedPeriodsString}") + if (denot.is(ValidForever) || denot.isRefinementClass) true + else { + implicit val ctx: Context = this + val initial = denot.initial + if ((initial ne denot) || ctx.phaseId != initial.validFor.firstPhaseId) { + ctx.withPhase(initial.validFor.firstPhaseId).traceInvalid(initial.asSymDenotation) + } else try { + val owner = denot.owner.denot + if (!traceInvalid(owner)) explainSym("owner is invalid") + else if (!owner.isClass || owner.isRefinementClass || denot.isSelfSym) true + else if (owner.unforcedDecls.lookupAll(denot.name) contains denot.symbol) true + else explainSym(s"decls of ${show(owner)} are ${owner.unforcedDecls.lookupAll(denot.name).toList}, do not contain ${denot.symbol}") + } catch { + case ex: StaleSymbol => explainSym(s"$ex was thrown") + } } + case _ => + explain("denotation is not a SymDenotation") } + } } object SymDenotations { diff --git a/src/dotty/tools/dotc/repl/AbstractFileClassLoader.scala b/src/dotty/tools/dotc/repl/AbstractFileClassLoader.scala new file mode 100644 index 000000000..a3a463717 --- /dev/null +++ b/src/dotty/tools/dotc/repl/AbstractFileClassLoader.scala @@ -0,0 +1,31 @@ +package dotty.tools +package dotc +package repl + +import io.AbstractFile + +/** + * A class loader that loads files from a {@link scala.tools.nsc.io.AbstractFile}. + * + * @author Lex Spoon + */ +class AbstractFileClassLoader(root: AbstractFile, parent: ClassLoader) +extends ClassLoader(parent) +{ + override def findClass(name: String): Class[_] = { + var file: AbstractFile = root + val pathParts = name.split("[./]").toList + for (dirPart <- pathParts.init) { + file = file.lookupName(dirPart, true) + if (file == null) { + throw new ClassNotFoundException(name) + } + } + file = file.lookupName(pathParts.last+".class", false) + if (file == null) { + throw new ClassNotFoundException(name) + } + val bytes = file.toByteArray + defineClass(name, bytes, 0, bytes.length) + } +} diff --git a/src/dotty/tools/dotc/repl/CompilingInterpreter.scala b/src/dotty/tools/dotc/repl/CompilingInterpreter.scala new file mode 100644 index 000000000..7d1da1419 --- /dev/null +++ b/src/dotty/tools/dotc/repl/CompilingInterpreter.scala @@ -0,0 +1,811 @@ +package dotty.tools +package dotc +package repl + +import java.io.{File, PrintWriter, StringWriter, Writer} +import java.lang.{Class, ClassLoader} +import java.net.{URL, URLClassLoader} + +import scala.collection.immutable.ListSet +import scala.collection.mutable +import scala.collection.mutable.{ListBuffer, HashSet, ArrayBuffer} + +//import ast.parser.SyntaxAnalyzer +import io.{PlainFile, VirtualDirectory} +import scala.reflect.io.{PlainDirectory, Directory} +import reporting.{ConsoleReporter, Reporter} +import core.Flags +import util.{SourceFile, NameTransformer} +import io.ClassPath +import ast.Trees._ +import parsing.Parsers._ +import core._ +import dotty.tools.backend.jvm.GenBCode +import Symbols._, Types._, Contexts._, StdNames._, Names._, NameOps._ +import Decorators._ +import scala.util.control.NonFatal + +/** An interpreter for Scala code which is based on the `dotc` compiler. + * + * The overall approach is based on compiling the requested code and then + * using a Java classloader and Java reflection to run the code + * and access its results. + * + * In more detail, a single compiler instance is used + * to accumulate all successfully compiled or interpreted Scala code. To + * "interpret" a line of code, the compiler generates a fresh object that + * includes the line of code and which has public definition(s) to export + * all variables defined by that code. To extract the result of an + * interpreted line to show the user, a second "result object" is created + * which imports the variables exported by the above object and then + * exports a single definition named "result". To accommodate user expressions + * that read from variables or methods defined in previous statements, "import" + * statements are used. + * + * This interpreter shares the strengths and weaknesses of using the + * full compiler-to-Java. The main strength is that interpreted code + * behaves exactly as does compiled code, including running at full speed. + * The main weakness is that redefining classes and methods is not handled + * properly, because rebinding at the Java level is technically difficult. + * + * @author Moez A. Abdel-Gawad + * @author Lex Spoon + * @author Martin Odersky + * + * @param out The output to use for diagnostics + * @param ictx The context to use for initialization of the interpreter, + * needed to access the current classpath. + */ +class CompilingInterpreter(out: PrintWriter, ictx: Context) extends Compiler with Interpreter { + import ast.untpd._ + import CompilingInterpreter._ + + /** directory to save .class files to */ + val virtualDirectory = + if (ictx.settings.d.isDefault(ictx)) new VirtualDirectory("(memory)", None) + else new PlainDirectory(new Directory(new java.io.File(ictx.settings.d.value(ictx)))) // for now, to help debugging + + /** A GenBCode phase that uses `virtualDirectory` for its output */ + private class REPLGenBCode extends GenBCode { + override def outputDir(implicit ctx: Context) = virtualDirectory + } + + /** Phases of this compiler use `REPLGenBCode` instead of `GenBCode`. */ + override def phases = Phases.replace( + classOf[GenBCode], _ => new REPLGenBCode :: Nil, super.phases) + + /** whether to print out result lines */ + private var printResults: Boolean = true + + /** Temporarily be quiet */ + override def beQuietDuring[T](operation: => T): T = { + val wasPrinting = printResults + try { + printResults = false + operation + } finally { + printResults = wasPrinting + } + } + + private def newReporter = new ConsoleReporter(Console.in, out) { + override def printMessage(msg: String) = { + out.print(/*clean*/(msg) + "\n") + // Suppress clean for now for compiler messages + // Otherwise we will completely delete all references to + // line$object$ module classes. The previous interpreter did not + // have the project because the module class was written without the final `$' + // and therefore escaped the purge. We can turn this back on once + // we drop the final `$' from module classes. + out.flush() + } + } + + /** the previous requests this interpreter has processed */ + private val prevRequests = new ArrayBuffer[Request]() + + /** the compiler's classpath, as URL's */ + val compilerClasspath: List[URL] = ictx.platform.classPath(ictx).asURLs + + protected def parentClassLoader: ClassLoader = classOf[Interpreter].getClassLoader + + /* A single class loader is used for all commands interpreted by this Interpreter. + It would also be possible to create a new class loader for each command + to interpret. The advantages of the current approach are: + + - Expressions are only evaluated one time. This is especially + significant for I/O, e.g. "val x = Console.readLine" + + The main disadvantage is: + + - Objects, classes, and methods cannot be rebound. Instead, definitions + shadow the old ones, and old code objects refer to the old + definitions. + */ + /** class loader used to load compiled code */ + val classLoader: ClassLoader = { + val parent = new URLClassLoader(compilerClasspath.toArray, parentClassLoader) + new AbstractFileClassLoader(virtualDirectory, parent) + } + + // Set the current Java "context" class loader to this interpreter's class loader + Thread.currentThread.setContextClassLoader(classLoader) + + /** Parse a line into a sequence of trees. Returns None if the input is incomplete. */ + private def parse(line: String)(implicit ctx: Context): Option[List[Tree]] = { + var justNeedsMore = false + val reporter = newReporter + reporter.withIncompleteHandler { _ => _ => justNeedsMore = true } { + // simple parse: just parse it, nothing else + def simpleParse(code: String)(implicit ctx: Context): List[Tree] = { + val source = new SourceFile("<console>", code.toCharArray()) + val parser = new Parser(source) + val (selfDef, stats) = parser.templateStatSeq + stats + } + val trees = simpleParse(line)(ctx.fresh.setReporter(reporter)) + if (reporter.hasErrors) { + Some(Nil) // the result did not parse, so stop + } else if (justNeedsMore) { + None + } else { + Some(trees) + } + } + } + + /** Compile a SourceFile. Returns the root context of the run that compiled the file. + */ + def compileSources(sources: List[SourceFile])(implicit ctx: Context): Context = { + val reporter = newReporter + val run = newRun(ctx.fresh.setReporter(reporter)) + run.compileSources(sources) + run.runContext + } + + /** Compile a string. Returns true if there are no + * compilation errors, or false otherwise. + */ + def compileString(code: String)(implicit ctx: Context): Boolean = { + val runCtx = compileSources(List(new SourceFile("<script>", code.toCharArray))) + !runCtx.reporter.hasErrors + } + + override def interpret(line: String)(implicit ctx: Context): Interpreter.Result = { + // if (prevRequests.isEmpty) + // new Run(this) // initialize the compiler // (not sure this is needed) + // parse + parse(indentCode(line)) match { + case None => Interpreter.Incomplete + case Some(Nil) => Interpreter.Error // parse error or empty input + case Some(tree :: Nil) if tree.isTerm && !tree.isInstanceOf[Assign] => + interpret(s"val $newVarName =\n$line") + case Some(trees) => + val req = new Request(line, newLineName) + if (!req.compile()) + Interpreter.Error // an error happened during compilation, e.g. a type error + else { + val (interpreterResultString, succeeded) = req.loadAndRun() + if (printResults || !succeeded) + out.print(clean(interpreterResultString)) + if (succeeded) { + prevRequests += req + Interpreter.Success + } + else Interpreter.Error + } + } + } + + /** Trait collecting info about one of the statements of an interpreter request */ + private trait StatementInfo { + /** The statement */ + def statement: Tree + + /** The names defined previously and referred to in the statement */ + def usedNames: List[Name] + + /** The names defined in the statement */ + val boundNames: List[Name] + + /** Statement is an import that contains a wildcard */ + val importsWildcard: Boolean + + /** The names imported by the statement (if it is an import clause) */ + val importedNames: Seq[Name] + + /** Statement defines an implicit calue or method */ + val definesImplicit: Boolean + } + + /** One line of code submitted by the user for interpretation */ + private class Request(val line: String, val lineName: String)(implicit ctx: Context) { + private val trees = parse(line) match { + case Some(ts) => ts + case None => Nil + } + + /** name to use for the object that will compute "line" */ + private def objectName = lineName + INTERPRETER_WRAPPER_SUFFIX + + /** name of the object that retrieves the result from the above object */ + private def resultObjectName = "RequestResult$" + objectName + + private def chooseHandler(stat: Tree): StatementHandler = stat match { + case stat: DefDef => new DefHandler(stat) + case stat: ValDef => new ValHandler(stat) + case stat @ Assign(Ident(_), _) => new AssignHandler(stat) + case stat: ModuleDef => new ModuleHandler(stat) + case stat: TypeDef if stat.isClassDef => new ClassHandler(stat) + case stat: TypeDef => new TypeAliasHandler(stat) + case stat: Import => new ImportHandler(stat) +// case DocDef(_, documented) => chooseHandler(documented) + case stat => new GenericHandler(stat) + } + + private val handlers: List[StatementHandler] = trees.map(chooseHandler) + + /** all (public) names defined by these statements */ + private val boundNames = ListSet(handlers.flatMap(_.boundNames): _*).toList + + /** list of names used by this expression */ + private val usedNames: List[Name] = handlers.flatMap(_.usedNames) + + private val (importsPreamble, importsTrailer, accessPath) = + importsCode(usedNames.toSet) + + /** Code to access a variable with the specified name */ + private def fullPath(vname: String): String = s"$objectName$accessPath.`$vname`" + + /** Code to access a variable with the specified name */ + private def fullPath(vname: Name): String = fullPath(vname.toString) + + /** the line of code to compute */ + private def toCompute = line + + /** generate the source code for the object that computes this request + * TODO Reformulate in a functional way + */ + private def objectSourceCode: String = + stringFrom { code => + // header for the wrapper object + code.println("object " + objectName + " {") + code.print(importsPreamble) + code.println(indentCode(toCompute)) + handlers.foreach(_.extraCodeToEvaluate(this,code)) + code.println(importsTrailer) + //end the wrapper object + code.println(";}") + } + + /** Types of variables defined by this request. They are computed + after compilation of the main object */ + private var typeOf: Map[Name, String] = _ + + /** generate source code for the object that retrieves the result + from objectSourceCode */ + private def resultObjectSourceCode: String = + stringFrom(code => { + code.println("object " + resultObjectName) + code.println("{ val result: String = {") + code.println(objectName + accessPath + ";") // evaluate the object, to make sure its constructor is run + code.print("(\"\"") // print an initial empty string, so later code can + // uniformly be: + morestuff + handlers.foreach(_.resultExtractionCode(this, code)) + code.println("\n)}") + code.println(";}") + }) + + + /** Compile the object file. Returns whether the compilation succeeded. + * If all goes well, the "types" map is computed. */ + def compile(): Boolean = { + val compileCtx = compileSources( + List(new SourceFile("<console>", objectSourceCode.toCharArray))) + !compileCtx.reporter.hasErrors && { + this.typeOf = findTypes(compileCtx) + val resultCtx = compileSources( + List(new SourceFile("<console>", resultObjectSourceCode.toCharArray))) + !resultCtx.reporter.hasErrors + } + } + + /** Dig the types of all bound variables out of the compiler run. + * TODO: Change the interface so that we typecheck, and then transform + * directly. Treating the compiler as less of a blackbox will require + * much less magic here. + */ + private def findTypes(implicit ctx: Context): Map[Name, String] = { + def valAndVarNames = handlers.flatMap(_.valAndVarNames) + def defNames = handlers.flatMap(_.defNames) + + def getTypes(names: List[Name], nameMap: Name => Name): Map[Name, String] = { + /** the outermost wrapper object */ + val outerResObjSym: Symbol = + defn.EmptyPackageClass.info.decl(objectName.toTermName).symbol + + /** the innermost object inside the wrapper, found by + * following accessPath into the outer one. */ + val resObjSym = + (accessPath.split("\\.")).foldLeft(outerResObjSym) { (sym,str) => + if (str.isEmpty) sym + else + ctx.atPhase(ctx.typerPhase.next) { implicit ctx => + sym.info.member(str.toTermName).symbol + } + } + + names.foldLeft(Map.empty[Name,String]) { (map, name) => + val rawType = + ctx.atPhase(ctx.typerPhase.next) { implicit ctx => + resObjSym.info.member(name).info + } + + // the types are all =>T; remove the => + val cleanedType = rawType.widenExpr + + map + (name -> + ctx.atPhase(ctx.typerPhase.next) { implicit ctx => + cleanedType.show + }) + } + } + + val names1 = getTypes(valAndVarNames, n => n.toTermName.fieldName) + val names2 = getTypes(defNames, identity) + names1 ++ names2 + } + + /** load and run the code using reflection. + * @return A pair consisting of the run's result as a string, and + * a boolean indicating whether the run succeeded without throwing + * an exception. + */ + def loadAndRun(): (String, Boolean) = { + val interpreterResultObject: Class[_] = + Class.forName(resultObjectName, true, classLoader) + val resultValMethod: java.lang.reflect.Method = + interpreterResultObject.getMethod("result") + try { + (resultValMethod.invoke(interpreterResultObject).toString, true) + } catch { + case NonFatal(ex) => + def cause(ex: Throwable): Throwable = + if (ex.getCause eq null) ex else cause(ex.getCause) + val orig = cause(ex) + (stringFrom(str => orig.printStackTrace(str)), false) + } + } + + /** Compute imports that allow definitions from previous + * requests to be visible in a new request. Returns + * three pieces of related code as strings: + * + * 1. A _preamble_: An initial code fragment that should go before + * the code of the new request. + * + * 2. A _trailer_: A code fragment that should go after the code + * of the new request. + * + * 3. An _access path_ which can be traversed to access + * any bindings inside code wrapped by #1 and #2 . + * + * The argument is a set of Names that need to be imported. + * + * Limitations: This method is not as precise as it could be. + * (1) It does not process wildcard imports to see what exactly + * they import. + * (2) If it imports any names from a request, it imports all + * of them, which is not really necessary. + * (3) It imports multiple same-named implicits, but only the + * last one imported is actually usable. + */ + private def importsCode(wanted: Set[Name]): (String, String, String) = { + /** Narrow down the list of requests from which imports + * should be taken. Removes requests which cannot contribute + * useful imports for the specified set of wanted names. + */ + def reqsToUse: List[(Request, StatementInfo)] = { + /** Loop through a list of StatementHandlers and select + * which ones to keep. 'wanted' is the set of + * names that need to be imported. + */ + def select(reqs: List[(Request, StatementInfo)], wanted: Set[Name]): List[(Request, StatementInfo)] = { + reqs match { + case Nil => Nil + + case (req, handler) :: rest => + val keepit = + (handler.definesImplicit || + handler.importsWildcard || + handler.importedNames.exists(wanted.contains(_)) || + handler.boundNames.exists(wanted.contains(_))) + + val newWanted = + if (keepit) { + (wanted + ++ handler.usedNames + -- handler.boundNames + -- handler.importedNames) + } else { + wanted + } + + val restToKeep = select(rest, newWanted) + + if (keepit) + (req, handler) :: restToKeep + else + restToKeep + } + } + + val rhpairs = for { + req <- prevRequests.toList.reverse + handler <- req.handlers + } yield (req, handler) + + select(rhpairs, wanted).reverse + } + + val preamble = new StringBuffer + val trailingBraces = new StringBuffer + val accessPath = new StringBuffer + val impname = INTERPRETER_IMPORT_WRAPPER + val currentImps = mutable.Set[Name]() + + // add code for a new object to hold some imports + def addWrapper(): Unit = { + preamble.append("object " + impname + "{\n") + trailingBraces.append("}\n") + accessPath.append("." + impname) + currentImps.clear + } + + addWrapper() + + // loop through previous requests, adding imports + // for each one + for ((req, handler) <- reqsToUse) { + // If the user entered an import, then just use it + + // add an import wrapping level if the import might + // conflict with some other import + if (handler.importsWildcard || + currentImps.exists(handler.importedNames.contains)) + if (!currentImps.isEmpty) + addWrapper() + + if (handler.statement.isInstanceOf[Import]) + preamble.append(handler.statement.toString + ";\n") + + // give wildcard imports a import wrapper all to their own + if (handler.importsWildcard) + addWrapper() + else + currentImps ++= handler.importedNames + + // For other requests, import each bound variable. + // import them explicitly instead of with _, so that + // ambiguity errors will not be generated. Also, quote + // the name of the variable, so that we don't need to + // handle quoting keywords separately. + for (imv <- handler.boundNames) { + if (currentImps.contains(imv)) + addWrapper() + preamble.append("import ") + preamble.append(req.objectName + req.accessPath + ".`" + imv + "`;\n") + currentImps += imv + } + } + + addWrapper() // Add one extra wrapper, to prevent warnings + // in the frequent case of redefining + // the value bound in the last interpreter + // request. + + (preamble.toString, trailingBraces.toString, accessPath.toString) + } + + // ------ Handlers ------------------------------------------ + + /** Class to handle one statement among all the statements included + * in a single interpreter request. + */ + private sealed abstract class StatementHandler(val statement: Tree) extends StatementInfo { + val usedNames: List[Name] = { + val ivt = new UntypedTreeAccumulator[mutable.Set[Name]] { + override def apply(ns: mutable.Set[Name], tree: Tree)(implicit ctx: Context) = + tree match { + case Ident(name) => ns += name + case _ => foldOver(ns, tree) + } + } + ivt.foldOver(HashSet(), statement).toList + } + val boundNames: List[Name] = Nil + def valAndVarNames: List[Name] = Nil + def defNames: List[Name] = Nil + val importsWildcard = false + val importedNames: Seq[Name] = Nil + val definesImplicit = statement match { + case tree: MemberDef => tree.mods.is(Flags.Implicit) + case _ => false + } + + def extraCodeToEvaluate(req: Request, code: PrintWriter) = {} + def resultExtractionCode(req: Request, code: PrintWriter) = {} + } + + private class GenericHandler(statement: Tree) extends StatementHandler(statement) + + private class ValHandler(statement: ValDef) extends StatementHandler(statement) { + override val boundNames = List(statement.name) + override def valAndVarNames = boundNames + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + val vname = statement.name + if (!statement.mods.is(Flags.AccessFlags) && + !(isGeneratedVarName(vname.toString) && + req.typeOf(vname.encode) == "Unit")) { + val prettyName = vname.decode + code.print(" + \"" + prettyName + ": " + + string2code(req.typeOf(vname)) + + " = \" + " + + " (if(" + + req.fullPath(vname) + + ".asInstanceOf[AnyRef] != null) " + + " ((if(" + + req.fullPath(vname) + + ".toString().contains('\\n')) " + + " \"\\n\" else \"\") + " + + req.fullPath(vname) + ".toString() + \"\\n\") else \"null\\n\") ") + } + } + } + + private class DefHandler(defDef: DefDef) extends StatementHandler(defDef) { + override val boundNames = List(defDef.name) + override def defNames = boundNames + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + if (!defDef.mods.is(Flags.AccessFlags)) + code.print("+\"" + string2code(defDef.name.toString) + ": " + + string2code(req.typeOf(defDef.name)) + "\\n\"") + } + } + + private class AssignHandler(statement: Assign) extends StatementHandler(statement) { + val lhs = statement.lhs.asInstanceOf[Ident] // an unfortunate limitation + + val helperName = newInternalVarName().toTermName + override val valAndVarNames = List(helperName) + + override def extraCodeToEvaluate(req: Request, code: PrintWriter): Unit = { + code.println("val " + helperName + " = " + statement.lhs + ";") + } + + /** Print out lhs instead of the generated varName */ + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.print(" + \"" + lhs + ": " + + string2code(req.typeOf(helperName.encode)) + + " = \" + " + + string2code(req.fullPath(helperName)) + + " + \"\\n\"") + } + } + + private class ModuleHandler(module: ModuleDef) extends StatementHandler(module) { + override val boundNames = List(module.name) + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.println(" + \"defined module " + + string2code(module.name.toString) + + "\\n\"") + } + } + + private class ClassHandler(classdef: TypeDef) + extends StatementHandler(classdef) { + override val boundNames = + List(classdef.name) ::: + (if (classdef.mods.is(Flags.Case)) + List(classdef.name.toTermName) + else + Nil) + + // TODO: MemberDef.keyword does not include "trait"; + // otherwise it could be used here + def keyword: String = + if (classdef.mods.is(Flags.Trait)) "trait" else "class" + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.print( + " + \"defined " + + keyword + + " " + + string2code(classdef.name.toString) + + "\\n\"") + } + } + + private class TypeAliasHandler(typeDef: TypeDef) + extends StatementHandler(typeDef) { + override val boundNames = + if (!typeDef.mods.is(Flags.AccessFlags) && !typeDef.rhs.isInstanceOf[TypeBoundsTree]) + List(typeDef.name) + else + Nil + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.println(" + \"defined type alias " + + string2code(typeDef.name.toString) + "\\n\"") + } + } + + private class ImportHandler(imp: Import) extends StatementHandler(imp) { + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.println("+ \"" + imp.toString + "\\n\"") + } + + def isWildcardSelector(tree: Tree) = tree match { + case Ident(nme.USCOREkw) => true + case _ => false + } + + /** Whether this import includes a wildcard import */ + override val importsWildcard = imp.selectors.exists(isWildcardSelector) + + /** The individual names imported by this statement */ + override val importedNames: Seq[Name] = + imp.selectors.filterNot(isWildcardSelector).flatMap { + case sel: RefTree => List(sel.name.toTypeName, sel.name.toTermName) + case _ => Nil + } + } + + } // end Request + + // ------- String handling ---------------------------------- + + /** next line number to use */ + private var nextLineNo = 0 + + /** allocate a fresh line name */ + private def newLineName = { + val num = nextLineNo + nextLineNo += 1 + INTERPRETER_LINE_PREFIX + num + } + + /** next result variable number to use */ + private var nextVarNameNo = 0 + + /** allocate a fresh variable name */ + private def newVarName = { + val num = nextVarNameNo + nextVarNameNo += 1 + INTERPRETER_VAR_PREFIX + num + } + + /** next internal variable number to use */ + private var nextInternalVarNo = 0 + + /** allocate a fresh internal variable name */ + private def newInternalVarName() = { + val num = nextVarNameNo + nextVarNameNo += 1 + INTERPRETER_SYNTHVAR_PREFIX + num + } + + /** Check if a name looks like it was generated by newVarName */ + private def isGeneratedVarName(name: String): Boolean = + name.startsWith(INTERPRETER_VAR_PREFIX) && { + val suffix = name.drop(INTERPRETER_VAR_PREFIX.length) + suffix.forall(_.isDigit) + } + + /** generate a string using a routine that wants to write on a stream */ + private def stringFrom(writer: PrintWriter => Unit): String = { + val stringWriter = new StringWriter() + val stream = new NewLinePrintWriter(stringWriter) + writer(stream) + stream.close + stringWriter.toString + } + + /** Truncate a string if it is longer than settings.maxPrintString */ + private def truncPrintString(str: String)(implicit ctx: Context): String = { + val maxpr = ctx.settings.XreplLineWidth.value + + if (maxpr <= 0) + return str + + if (str.length <= maxpr) + return str + + val trailer = "..." + if (maxpr >= trailer.length+1) + return str.substring(0, maxpr-3) + trailer + + str.substring(0, maxpr) + } + + /** Clean up a string for output */ + private def clean(str: String)(implicit ctx: Context) = + truncPrintString(stripWrapperGunk(str)) + + /** Indent some code by the width of the scala> prompt. + * This way, compiler error messages read better. + */ + def indentCode(code: String) = { + val spaces = " " + + stringFrom(str => + for (line <- code.lines) { + str.print(spaces) + str.print(line + "\n") + str.flush() + }) + } +} + +/** Utility methods for the Interpreter. */ +object CompilingInterpreter { + val INTERPRETER_WRAPPER_SUFFIX = "$object" + val INTERPRETER_LINE_PREFIX = "line" + val INTERPRETER_VAR_PREFIX = "res" + val INTERPRETER_IMPORT_WRAPPER = "$iw" + val INTERPRETER_SYNTHVAR_PREFIX = "synthvar$" + + /** Delete a directory tree recursively. Use with care! + */ + private[repl] def deleteRecursively(path: File): Unit = { + path match { + case _ if !path.exists => + () + case _ if path.isDirectory => + for (p <- path.listFiles) + deleteRecursively(p) + path.delete + case _ => + path.delete + } + } + + /** Heuristically strip interpreter wrapper prefixes + * from an interpreter output string. + */ + def stripWrapperGunk(str: String): String = { + val wrapregex = "(line[0-9]+\\$object[$.])?(\\$iw[$.])*" + str.replaceAll(wrapregex, "") + } + + /** Convert a string into code that can recreate the string. + * This requires replacing all special characters by escape + * codes. It does not add the surrounding " marks. */ + def string2code(str: String): String = { + /** Convert a character to a backslash-u escape */ + def char2uescape(c: Char): String = { + var rest = c.toInt + val buf = new StringBuilder + for (i <- 1 to 4) { + buf ++= (rest % 16).toHexString + rest = rest / 16 + } + "\\" + "u" + buf.toString.reverse + } + val res = new StringBuilder + for (c <- str) { + if ("'\"\\" contains c) { + res += '\\' + res += c + } else if (!c.isControl) { + res += c + } else { + res ++= char2uescape(c) + } + } + res.toString + } +} diff --git a/src/dotty/tools/dotc/repl/ConsoleWriter.scala b/src/dotty/tools/dotc/repl/ConsoleWriter.scala new file mode 100644 index 000000000..9387f366a --- /dev/null +++ b/src/dotty/tools/dotc/repl/ConsoleWriter.scala @@ -0,0 +1,21 @@ +package dotty.tools +package dotc +package repl +import java.io.Writer + +/** A Writer that writes onto the Scala Console. + * + * @author Lex Spoon + * @version 1.0 + */ +class ConsoleWriter extends Writer { + def close = flush + + def flush = Console.flush + + def write(cbuf: Array[Char], off: Int, len: Int): Unit = + if (len > 0) + write(new String(cbuf, off, len)) + + override def write(str: String): Unit = Console.print(str) +} diff --git a/src/dotty/tools/dotc/repl/InteractiveReader.scala b/src/dotty/tools/dotc/repl/InteractiveReader.scala new file mode 100644 index 000000000..96c55ebd0 --- /dev/null +++ b/src/dotty/tools/dotc/repl/InteractiveReader.scala @@ -0,0 +1,31 @@ +package dotty.tools +package dotc +package repl + +/** Reads lines from an input stream */ +trait InteractiveReader { + def readLine(prompt: String): String + val interactive: Boolean +} + +/** TODO Enable jline support. + * The current Scala REPL know how to do this flexibly. + */ +object InteractiveReader { + /** Create an interactive reader. Uses JLine if the + * library is available, but otherwise uses a + * SimpleReader. */ + def createDefault(): InteractiveReader = new SimpleReader() + /* + { + try { + new JLineReader + } catch { + case e => + //out.println("jline is not available: " + e) //debug + new SimpleReader() + } + } +*/ + +} diff --git a/src/dotty/tools/dotc/repl/Interpreter.scala b/src/dotty/tools/dotc/repl/Interpreter.scala new file mode 100644 index 000000000..ea587a097 --- /dev/null +++ b/src/dotty/tools/dotc/repl/Interpreter.scala @@ -0,0 +1,36 @@ +package dotty.tools +package dotc +package repl + +import core.Contexts.Context + +/** This object defines the type of interpreter results */ +object Interpreter { + + /** A result from interpreting one line of input. */ + abstract sealed class Result + + /** The line was interpreted successfully. */ + case object Success extends Result + + /** The line was erroneous in some way. */ + case object Error extends Result + + /** The input was incomplete. The caller should request more input. + */ + case object Incomplete extends Result +} + +/** The exported functionality of the interpreter */ +trait Interpreter { + import Interpreter._ + + /** Interpret one line of input. All feedback, including parse errors + * and evaluation results, are printed via the context's reporter. + * reporter. Values defined are available for future interpreted strings. + */ + def interpret(line: String)(implicit ctx: Context): Result + + /** Suppress output during evaluation of `operation`. */ + def beQuietDuring[T](operation: => T): T +} diff --git a/src/dotty/tools/dotc/repl/InterpreterLoop.scala b/src/dotty/tools/dotc/repl/InterpreterLoop.scala new file mode 100644 index 000000000..eedec3c82 --- /dev/null +++ b/src/dotty/tools/dotc/repl/InterpreterLoop.scala @@ -0,0 +1,228 @@ +package dotty.tools +package dotc +package repl + +import java.io.{BufferedReader, File, FileReader, PrintWriter} +import java.io.IOException +import java.lang.{ClassLoader, System} +import scala.concurrent.{Future, Await} +import scala.concurrent.duration.Duration +import reporting.Reporter +import core._ +import Contexts._ +import annotation.tailrec +import scala.concurrent.ExecutionContext.Implicits.global + +/** The interactive shell. It provides a read-eval-print loop around + * the Interpreter class. + * After instantiation, clients should call the `run` method. + * + * @author Moez A. Abdel-Gawad + * @author Lex Spoon + * @author Martin Odersky + */ +class InterpreterLoop( + compiler: Compiler, + private var in: InteractiveReader, + out: PrintWriter)(implicit ctx: Context) { + + val interpreter = compiler.asInstanceOf[Interpreter] + + /** The context class loader at the time this object was created */ + protected val originalClassLoader = + Thread.currentThread.getContextClassLoader + + /** A reverse list of commands to replay if the user + * requests a :replay */ + var replayCommandsRev: List[String] = Nil + + /** A list of commands to replay if the user requests a :replay */ + def replayCommands = replayCommandsRev.reverse + + /** Record a command for replay should the user request a :replay */ + def addReplay(cmd: String) = + replayCommandsRev = cmd :: replayCommandsRev + + /** Close the interpreter */ + def closeInterpreter()(implicit ctx: Context): Unit = { + ctx.reporter.flush() + Thread.currentThread.setContextClassLoader(originalClassLoader) + } + + /** print a friendly help message */ + def printHelp(): Unit = { + printWelcome() + out.println("Type :load followed by a filename to load a Scala file.") + out.println("Type :replay to reset execution and replay all previous commands.") + out.println("Type :quit to exit the interpreter.") + } + + /** Print a welcome message */ + def printWelcome(): Unit = { + out.println(s"Welcome to Scala$version " + " (" + + System.getProperty("java.vm.name") + ", Java " + System.getProperty("java.version") + ")." ) + out.println("Type in expressions to have them evaluated.") + out.println("Type :help for more information.") + out.flush() + } + + /** Prompt to print when awaiting input */ + val prompt = "scala> " + val continuationPrompt = " | " + + val version = ".next (pre-alpha)" + + /** The first interpreted command always takes a couple of seconds + * due to classloading. To bridge the gap, we warm up the interpreter + * by letting it interpret a dummy line while waiting for the first + * line of input to be entered. + */ + def firstLine(): String = { + val futLine = Future(in.readLine(prompt)) + interpreter.beQuietDuring( + interpreter.interpret("val theAnswerToLifeInTheUniverseAndEverything = 21 * 2")) + Await.result(futLine, Duration.Inf) + } + + /** The main read-eval-print loop for the interpreter. It calls + * `command()` for each line of input. + */ + @tailrec final def repl(line: String = in.readLine(prompt)): Unit = + if (line != null) { + val (keepGoing, finalLineOpt) = command(line) + if (keepGoing) { + finalLineOpt.foreach(addReplay) + out.flush() + repl() + } + } + + /** interpret all lines from a specified file */ + def interpretAllFrom(filename: String): Unit = { + val fileIn = try { + new FileReader(filename) + } catch { + case _: IOException => + out.println("Error opening file: " + filename) + return + } + val oldIn = in + val oldReplay = replayCommandsRev + try { + val inFile = new BufferedReader(fileIn) + in = new SimpleReader(inFile, out, false) + out.println("Loading " + filename + "...") + out.flush + repl() + } finally { + in = oldIn + replayCommandsRev = oldReplay + fileIn.close + } + } + + /** create a new interpreter and replay all commands so far */ + def replay(): Unit = { + for (cmd <- replayCommands) { + out.println("Replaying: " + cmd) + out.flush() // because maybe cmd will have its own output + command(cmd) + out.println + } + } + + /** Run one command submitted by the user. Three values are returned: + * (1) whether to keep running, (2) the line to record for replay, + * if any. */ + def command(line: String): (Boolean, Option[String]) = { + def withFile(command: String)(action: String => Unit): Unit = { + val spaceIdx = command.indexOf(' ') + if (spaceIdx <= 0) { + out.println("That command requires a filename to be specified.") + return + } + val filename = command.substring(spaceIdx).trim + if (!new File(filename).exists) { + out.println("That file does not exist") + return + } + action(filename) + } + + val helpRegexp = ":h(e(l(p)?)?)?" + val quitRegexp = ":q(u(i(t)?)?)?" + val loadRegexp = ":l(o(a(d)?)?)?.*" + val replayRegexp = ":r(e(p(l(a(y)?)?)?)?)?.*" + + var shouldReplay: Option[String] = None + + if (line.matches(helpRegexp)) + printHelp() + else if (line.matches(quitRegexp)) + return (false, None) + else if (line.matches(loadRegexp)) { + withFile(line)(f => { + interpretAllFrom(f) + shouldReplay = Some(line) + }) + } + else if (line matches replayRegexp) + replay() + else if (line startsWith ":") + out.println("Unknown command. Type :help for help.") + else + shouldReplay = interpretStartingWith(line) + + (true, shouldReplay) + } + + /** Interpret expressions starting with the first line. + * Read lines until a complete compilation unit is available + * or until a syntax error has been seen. If a full unit is + * read, go ahead and interpret it. Return the full string + * to be recorded for replay, if any. + */ + def interpretStartingWith(code: String): Option[String] = { + interpreter.interpret(code) match { + case Interpreter.Success => Some(code) + case Interpreter.Error => None + case Interpreter.Incomplete => + if (in.interactive && code.endsWith("\n\n")) { + out.println("You typed two blank lines. Starting a new command.") + None + } else { + val nextLine = in.readLine(continuationPrompt) + if (nextLine == null) + None // end of file + else + interpretStartingWith(code + "\n" + nextLine) + } + } + } +/* + def loadFiles(settings: Settings) { + settings match { + case settings: GenericRunnerSettings => + for (filename <- settings.loadfiles.value) { + val cmd = ":load " + filename + command(cmd) + replayCommandsRev = cmd :: replayCommandsRev + out.println() + } + case _ => + } + } +*/ + def run(): Reporter = { + // loadFiles(settings) + try { + if (!ctx.reporter.hasErrors) { // if there are already errors, no sense to continue + printWelcome() + repl(firstLine()) + } + } finally { + closeInterpreter() + } + ctx.reporter + } +} diff --git a/src/dotty/tools/dotc/repl/Main.scala b/src/dotty/tools/dotc/repl/Main.scala new file mode 100644 index 000000000..b2b92299e --- /dev/null +++ b/src/dotty/tools/dotc/repl/Main.scala @@ -0,0 +1,29 @@ +package dotty.tools +package dotc +package repl + +/* This REPL was adapted from an old (2008-ish) version of the Scala + * REPL. The original version from which the adaptation was done is found in: + * + * https://github.com/odersky/legacy-svn-scala/tree/spoon + * + * The reason this version was picked instead of a more current one is that + * the older version is much smaller, therefore easier to port. It is also + * considerably less intertwined with nsc than later versions. + * + * There are a number of TODOs: + * + * - re-enable jline support (urgent, easy, see TODO in InteractiveReader.scala) + * - figure out why we can launch REPL only with `java`, not with `scala`. + * - make a doti command (urgent, easy) + * - create or port REPL tests (urgent, intermediate) + * - copy improvements of current Scala REPL wrt to this version + * (somewhat urgent, intermediate) + * - re-enable bindSettings (not urgent, easy, see TODO in InterpreterLoop.scala) + * - make string generation more functional (not urgent, easy) + * - better handling of ^C (not urgent, intermediate) + * - syntax highlighting (not urgent, intermediate) + * - integrate with presentation compiler for command completion (not urgent, hard) + */ +/** The main entry point of the REPL */ +object Main extends REPL
\ No newline at end of file diff --git a/src/dotty/tools/dotc/repl/NewLinePrintWriter.scala b/src/dotty/tools/dotc/repl/NewLinePrintWriter.scala new file mode 100644 index 000000000..8e36a0ae4 --- /dev/null +++ b/src/dotty/tools/dotc/repl/NewLinePrintWriter.scala @@ -0,0 +1,11 @@ +package dotty.tools +package dotc +package repl +import java.io.{Writer, PrintWriter} + +class NewLinePrintWriter(out: Writer, autoFlush: Boolean) +extends PrintWriter(out, autoFlush) { + def this(out: Writer) = this(out, false) + override def println(): Unit = { print("\n"); flush() } +} + diff --git a/src/dotty/tools/dotc/repl/REPL.scala b/src/dotty/tools/dotc/repl/REPL.scala new file mode 100644 index 000000000..2d6a3c742 --- /dev/null +++ b/src/dotty/tools/dotc/repl/REPL.scala @@ -0,0 +1,49 @@ +package dotty.tools +package dotc +package repl + +import core.Contexts.Context +import reporting.Reporter +import java.io.{BufferedReader, File, FileReader, PrintWriter} + +/** A compiler which stays resident between runs. + * Usage: + * + * > scala dotty.tools.dotc.Resident <options> <initial files> + * + * dotc> "more options and files to compile" + * + * ... + * + * dotc> :reset // reset all options to the ones passed on the command line + * + * ... + * + * dotc> :q // quit + */ +class REPL extends Driver { + + /** The default input reader */ + def input(implicit ctx: Context): InteractiveReader = { + val emacsShell = System.getProperty("env.emacs", "") != "" + //println("emacsShell="+emacsShell) //debug + if (ctx.settings.Xnojline.value || emacsShell) new SimpleReader() + else InteractiveReader.createDefault() + } + + /** The defult output writer */ + def output: PrintWriter = new NewLinePrintWriter(new ConsoleWriter, true) + + override def newCompiler(implicit ctx: Context): Compiler = + new repl.CompilingInterpreter(output, ctx) + + override def sourcesRequired = false + + override def doCompile(compiler: Compiler, fileNames: List[String])(implicit ctx: Context): Reporter = { + if (fileNames.isEmpty) + new InterpreterLoop(compiler, input, output).run() + else + ctx.error(s"don't now what to do with $fileNames%, %") + ctx.reporter + } +} diff --git a/src/dotty/tools/dotc/repl/SimpleReader.scala b/src/dotty/tools/dotc/repl/SimpleReader.scala new file mode 100644 index 000000000..9fd563382 --- /dev/null +++ b/src/dotty/tools/dotc/repl/SimpleReader.scala @@ -0,0 +1,23 @@ +package dotty.tools +package dotc +package repl + +import java.io.{BufferedReader, PrintWriter} + + +/** Reads using standard JDK API */ +class SimpleReader( + in: BufferedReader, + out: PrintWriter, + val interactive: Boolean) +extends InteractiveReader { + def this() = this(Console.in, new PrintWriter(Console.out), true) + + def readLine(prompt: String) = { + if (interactive) { + out.print(prompt) + out.flush() + } + in.readLine() + } +} diff --git a/src/dotty/tools/dotc/transform/DropEmptyCompanions.scala b/src/dotty/tools/dotc/transform/DropEmptyCompanions.scala new file mode 100644 index 000000000..550e3348f --- /dev/null +++ b/src/dotty/tools/dotc/transform/DropEmptyCompanions.scala @@ -0,0 +1,98 @@ +package dotty.tools.dotc +package transform + +import core._ +import DenotTransformers.SymTransformer +import Phases.Phase +import Contexts.Context +import Flags._ +import Symbols._ +import SymDenotations.SymDenotation +import ast.Trees._ +import collection.mutable +import Decorators._ +import NameOps._ +import TreeTransforms.MiniPhaseTransform +import dotty.tools.dotc.transform.TreeTransforms.TransformerInfo + +/** Remove companion objects that are empty + * Lots of constraints here: + * 1. It's impractical to place DropEmptyCompanions before lambda lift because dropped + * modules can be anywhere and have hard to trace references. + * 2. DropEmptyCompanions cannot be interleaved with LambdaLift or Flatten because + * they put things in liftedDefs sets which cause them to surface later. So + * removed modules resurface. + * 3. DropEmptyCompanions has to be before RestoreScopes. + * The solution to the constraints is to put DropEmptyCompanions between Flatten + * and RestoreScopes and to only start working once we are back on PackageDef + * level, so we know that all objects moved by LambdaLift and Flatten have arrived + * at their destination. + */ +class DropEmptyCompanions extends MiniPhaseTransform { thisTransform => + import ast.tpd._ + override def phaseName = "dropEmpty" + override def runsAfter: Set[Class[_ <: Phase]] = Set(classOf[Flatten]) + + override def transformPackageDef(pdef: PackageDef)(implicit ctx: Context, info: TransformerInfo) = { + + /** Is `tree` an empty companion object? */ + def isEmptyCompanion(tree: Tree) = tree match { + case TypeDef(_, impl: Template) if tree.symbol.is(SyntheticModule) && + tree.symbol.companionClass.exists && + impl.body.forall(_.symbol.isPrimaryConstructor) => + println(i"removing ${tree.symbol}") + true + case _ => + false + } + + val dropped = pdef.stats.filter(isEmptyCompanion).map(_.symbol).toSet + + /** Symbol is a $lzy field representing a module */ + def isLazyModuleVar(sym: Symbol) = + sym.name.isLazyLocal && + sym.owner.info.decl(sym.name.asTermName.nonLazyName).symbol.is(Module) + + /** Symbol should be dropped together with a dropped companion object. + * Such symbols are: + * - lzy fields pointing to modules, + * - vals and getters representing modules. + */ + def symIsDropped(sym: Symbol): Boolean = + (sym.is(Module) || isLazyModuleVar(sym)) && + dropped.contains(sym.info.resultType.typeSymbol) + + /** Tree should be dropped because it (is associated with) an empty + * companion object. Such trees are + * - module classes of empty companion objects + * - definitions of lazy module variables or assignments to them. + * - vals and getters for empty companion objects + */ + def toDrop(stat: Tree): Boolean = stat match { + case stat: TypeDef => dropped.contains(stat.symbol) + case stat: ValOrDefDef => symIsDropped(stat.symbol) + case stat: Assign => symIsDropped(stat.lhs.symbol) + case _ => false + } + + def prune(tree: Tree): Tree = tree match { + case tree @ TypeDef(name, impl @ Template(constr, _, _, _)) => + cpy.TypeDef(tree)( + rhs = cpy.Template(impl)( + constr = cpy.DefDef(constr)(rhs = pruneLocals(constr.rhs)), + body = pruneStats(impl.body))) + case _ => + tree + } + + def pruneStats(stats: List[Tree]) = + stats.filterConserve(!toDrop(_)).mapConserve(prune) + + def pruneLocals(expr: Tree) = expr match { + case Block(stats, expr) => cpy.Block(expr)(pruneStats(stats), expr) + case _ => expr + } + + cpy.PackageDef(pdef)(pdef.pid, pruneStats(pdef.stats)) + } +} diff --git a/src/dotty/tools/dotc/transform/Flatten.scala b/src/dotty/tools/dotc/transform/Flatten.scala index 9a047ef95..f0104e715 100644 --- a/src/dotty/tools/dotc/transform/Flatten.scala +++ b/src/dotty/tools/dotc/transform/Flatten.scala @@ -11,6 +11,7 @@ import collection.mutable import TreeTransforms.MiniPhaseTransform import dotty.tools.dotc.transform.TreeTransforms.TransformerInfo +/** Lift nested classes to toplevel */ class Flatten extends MiniPhaseTransform with SymTransformer { thisTransform => import ast.tpd._ override def phaseName = "flatten" diff --git a/src/dotty/tools/dotc/transform/LazyVals.scala b/src/dotty/tools/dotc/transform/LazyVals.scala index 1efc0826b..4a8ff83ca 100644 --- a/src/dotty/tools/dotc/transform/LazyVals.scala +++ b/src/dotty/tools/dotc/transform/LazyVals.scala @@ -63,8 +63,8 @@ class LazyVals extends MiniPhaseTransform with IdentityDenotTransformer { if (isField) { if (sym.isVolatile || - (sym.is(Flags.Module) && !sym.is(Flags.Synthetic))) - // module class is user-defined. + (sym.is(Flags.Module) && !sym.is(Flags.Synthetic))) + // module class is user-defined. // Should be threadsafe, to mimic safety guaranteed by global object transformMemberDefVolatile(tree) else if (sym.is(Flags.Module)) { // synthetic module @@ -101,7 +101,7 @@ class LazyVals extends MiniPhaseTransform with IdentityDenotTransformer { */ def transformSyntheticModule(tree: ValOrDefDef)(implicit ctx: Context) = { val sym = tree.symbol - val holderSymbol = ctx.newSymbol(sym.owner, sym.asTerm.name ++ nme.LAZY_LOCAL, + val holderSymbol = ctx.newSymbol(sym.owner, sym.asTerm.name.lazyLocalName, Flags.Synthetic, sym.info.widen.resultType).enteredAfter(this) val field = ValDef(holderSymbol, tree.rhs.changeOwnerAfter(sym, holderSymbol, this)) val getter = DefDef(sym.asTerm, ref(holderSymbol)) @@ -114,7 +114,7 @@ class LazyVals extends MiniPhaseTransform with IdentityDenotTransformer { */ def transformLocalDef(x: ValOrDefDef)(implicit ctx: Context) = { val valueInitter = x.rhs - val holderName = ctx.freshName(x.name ++ StdNames.nme.LAZY_LOCAL).toTermName + val holderName = ctx.freshName(x.name.asTermName.lazyLocalName).toTermName val initName = ctx.freshName(x.name ++ StdNames.nme.LAZY_LOCAL_INIT).toTermName val tpe = x.tpe.widen.resultType.widen @@ -206,7 +206,7 @@ class LazyVals extends MiniPhaseTransform with IdentityDenotTransformer { val claz = x.symbol.owner.asClass val tpe = x.tpe.widen.resultType.widen assert(!(x.mods is Flags.Mutable)) - val containerName = ctx.freshName(x.name ++ StdNames.nme.LAZY_LOCAL).toTermName + val containerName = ctx.freshName(x.name.asTermName.lazyLocalName).toTermName val containerSymbol = ctx.newSymbol(claz, containerName, x.symbol.flags &~ containerFlagsMask | containerFlags | Flags.Private, tpe, coord = x.symbol.coord @@ -367,7 +367,7 @@ class LazyVals extends MiniPhaseTransform with IdentityDenotTransformer { appendOffsetDefs += (companion.moduleClass -> new OffsetInfo(List(offsetTree), ord)) } - val containerName = ctx.freshName(x.name ++ StdNames.nme.LAZY_LOCAL).toTermName + val containerName = ctx.freshName(x.name.asTermName.lazyLocalName).toTermName val containerSymbol = ctx.newSymbol(claz, containerName, (x.mods &~ containerFlagsMask | containerFlags).flags, tpe, coord = x.symbol.coord).enteredAfter(this) val containerTree = ValDef(containerSymbol, defaultValue(tpe)) diff --git a/src/dotty/tools/dotc/transform/RestoreScopes.scala b/src/dotty/tools/dotc/transform/RestoreScopes.scala index fe24186ac..41da05691 100644 --- a/src/dotty/tools/dotc/transform/RestoreScopes.scala +++ b/src/dotty/tools/dotc/transform/RestoreScopes.scala @@ -23,35 +23,46 @@ class RestoreScopes extends MiniPhaseTransform with IdentityDenotTransformer { t import ast.tpd._ override def phaseName = "restoreScopes" - override def transformTypeDef(tree: TypeDef)(implicit ctx: Context, info: TransformerInfo) = { - val TypeDef(_, impl: Template) = tree - // - val restoredDecls = newScope - for (stat <- impl.constr :: impl.body) - if (stat.isInstanceOf[MemberDef] && stat.symbol.exists) - restoredDecls.enter(stat.symbol) + /* Note: We need to wait until we see a package definition because + * DropEmptyConstructors changes template members when analyzing the + * enclosing package definitions. So by the time RestoreScopes gets to + * see a typedef or template, it still might be changed by DropEmptyConstructors. + */ + override def transformPackageDef(pdef: PackageDef)(implicit ctx: Context, info: TransformerInfo) = { + pdef.stats.foreach(restoreScope) + pdef + } + + private def restoreScope(tree: Tree)(implicit ctx: Context, info: TransformerInfo) = tree match { + case TypeDef(_, impl: Template) => + val restoredDecls = newScope + for (stat <- impl.constr :: impl.body) + if (stat.isInstanceOf[MemberDef] && stat.symbol.exists) + restoredDecls.enter(stat.symbol) // Enter class in enclosing package scope, in case it was an inner class before flatten. // For top-level classes this does nothing. - val cls = tree.symbol.asClass - val pkg = cls.owner.asClass - - // Bring back companion links - val companionClass = cls.info.decls.lookup(nme.COMPANION_CLASS_METHOD) - val companionModule = cls.info.decls.lookup(nme.COMPANION_MODULE_METHOD) - - if (companionClass.exists) { - restoredDecls.enter(companionClass) - } - - if (companionModule.exists) { - restoredDecls.enter(companionModule) - } - - pkg.enter(cls) - val cinfo = cls.classInfo - tree.symbol.copySymDenotation( - info = cinfo.derivedClassInfo( // Dotty deviation: Cannot expand cinfo inline without a type error - decls = restoredDecls: Scope)).installAfter(thisTransform) - tree + val cls = tree.symbol.asClass + val pkg = cls.owner.asClass + + // Bring back companion links + val companionClass = cls.info.decls.lookup(nme.COMPANION_CLASS_METHOD) + val companionModule = cls.info.decls.lookup(nme.COMPANION_MODULE_METHOD) + + if (companionClass.exists) { + restoredDecls.enter(companionClass) + } + + if (companionModule.exists) { + restoredDecls.enter(companionModule) + } + + pkg.enter(cls) + val cinfo = cls.classInfo + tree.symbol.copySymDenotation( + info = cinfo.derivedClassInfo( // Dotty deviation: Cannot expand cinfo inline without a type error + decls = restoredDecls: Scope)).installAfter(thisTransform) + tree + case tree => tree } } + diff --git a/src/dotty/tools/dotc/typer/Namer.scala b/src/dotty/tools/dotc/typer/Namer.scala index ecbec7d07..c8d2b3418 100644 --- a/src/dotty/tools/dotc/typer/Namer.scala +++ b/src/dotty/tools/dotc/typer/Namer.scala @@ -508,7 +508,7 @@ class Namer { typer: Typer => if (sym is Module) moduleValSig(sym) else valOrDefDefSig(original, sym, Nil, Nil, identity)(localContext(sym).setNewScope) case original: DefDef => - val typer1 = new Typer + val typer1 = ctx.typer.newLikeThis nestedTyper(sym) = typer1 typer1.defDefSig(original, sym)(localContext(sym).setTyper(typer1)) case original: TypeDef => diff --git a/src/dotty/tools/dotc/typer/RefChecks.scala b/src/dotty/tools/dotc/typer/RefChecks.scala index 067694bfd..bb411c6b5 100644 --- a/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/src/dotty/tools/dotc/typer/RefChecks.scala @@ -708,7 +708,19 @@ object RefChecks { if (clazz.is(Abstract)) ctx.error("`abstract' modifier cannot be used with value classes", clazz.pos) if (!clazz.isStatic) - ctx.error("value class cannot be an inner class", clazz.pos) + ctx.error(s"value class may not be a ${if (clazz.owner.isTerm) "local class" else "member of another class"}", clazz.pos) + else { + val clParamAccessors = clazz.asClass.paramAccessors.filter(sym => sym.isTerm && !sym.is(Method)) + clParamAccessors match { + case List(param) => + if (param.is(Mutable)) + ctx.error("value class parameter must not be a var", param.pos) + if (param.is(PrivateLocal)) + ctx.error("value class parameter must not be private[this]", param.pos) + case _ => + ctx.error("value class needs to have exactly one val parameter", clazz.pos) + } + } stats.foreach(checkValueClassMember) } } diff --git a/src/dotty/tools/dotc/typer/Typer.scala b/src/dotty/tools/dotc/typer/Typer.scala index b65e3cd42..854bc2094 100644 --- a/src/dotty/tools/dotc/typer/Typer.scala +++ b/src/dotty/tools/dotc/typer/Typer.scala @@ -68,6 +68,8 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit */ private var importedFromRoot: Set[Symbol] = Set() + def newLikeThis: Typer = new Typer + /** Attribute an identifier consisting of a simple name or wildcard * * @param tree The tree representing the identifier. diff --git a/test/dotc/tests.scala b/test/dotc/tests.scala index 12b830738..70af32bd3 100644 --- a/test/dotc/tests.scala +++ b/test/dotc/tests.scala @@ -185,6 +185,7 @@ class tests extends CompilerTest { @Test def neg_validateRefchecks = compileFile(negDir, "validate-refchecks", xerrors = 2) @Test def neg_skolemize = compileFile(negDir, "skolemize", xerrors = 2) @Test def neg_nested_bounds = compileFile(negDir, "nested_bounds", xerrors = 1) + @Test def neg_valueClasses = compileFile(negDir, "valueClasses", xerrors = 4) @Test def run_all = runFiles(runDir) diff --git a/tests/neg/valueClasses.scala b/tests/neg/valueClasses.scala new file mode 100644 index 000000000..ae90ef63c --- /dev/null +++ b/tests/neg/valueClasses.scala @@ -0,0 +1,10 @@ +class A1 { + class A2(x: Int) extends AnyVal // error: value class may not be a member of another class +} +class B1 { + def test = { + class B2(x: Int) extends AnyVal // error: value class may not be a local class + } +} +class C(private[this] val u: Int) extends AnyVal // error: value class parameter must not be private[this] +class D(u: Int) extends AnyVal // error: value class parameter must not be private[this] diff --git a/tests/pending/run/t920.scala b/tests/pending/run/t920.scala new file mode 100644 index 000000000..a9874e1a8 --- /dev/null +++ b/tests/pending/run/t920.scala @@ -0,0 +1,25 @@ +object Test { + trait A; + trait Foo0 { def foo : A; } + trait Baz extends Foo0; + trait B extends A { + def initialize = { + trait Foo extends Test.Foo0 { + def foo : B.this.type = B.this; + } + class baz extends Baz with Foo { + override def toString = "baz" + } + Console.println(new baz); + } + } + object b extends B; + def main(args : Array[String]) : Unit = { + b.initialize; + } + class XYZ +} + + + + diff --git a/tests/run/t920.scala b/tests/run/t920.scala index 6a7f122d5..d42388c4a 100644 --- a/tests/run/t920.scala +++ b/tests/run/t920.scala @@ -13,8 +13,12 @@ object Test { Console.println(new baz); } } - object b extends B; + object bb extends B; def main(args : Array[String]) : Unit = { - b.initialize; + bb.initialize; } } + + + + |