From 9b3c49a171ae32c5f2471dbf7f4b622d6ad42798 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 4 Jul 2011 09:19:14 +0000 Subject: Towards a scratchpad functionality for the IDE --- src/compiler/scala/reflect/internal/Trees.scala | 2 +- .../tools/nsc/interactive/CompilerControl.scala | 22 +++- .../scala/tools/nsc/interactive/Global.scala | 9 +- .../scala/tools/nsc/interactive/REPL.scala | 15 ++- .../tools/nsc/interactive/ScratchPadMaker.scala | 130 +++++++++++++++++++++ .../tools/nsc/scratchpad/CommentOutputStream.scala | 18 +++ .../scala/tools/nsc/scratchpad/CommentWriter.scala | 42 +++++++ .../scala/tools/nsc/scratchpad/Executor.scala | 28 +++++ .../tools/nsc/scratchpad/SourceInserter.scala | 112 ++++++++++++++++++ 9 files changed, 373 insertions(+), 5 deletions(-) create mode 100644 src/compiler/scala/tools/nsc/interactive/ScratchPadMaker.scala create mode 100644 src/compiler/scala/tools/nsc/scratchpad/CommentOutputStream.scala create mode 100644 src/compiler/scala/tools/nsc/scratchpad/CommentWriter.scala create mode 100644 src/compiler/scala/tools/nsc/scratchpad/Executor.scala create mode 100644 src/compiler/scala/tools/nsc/scratchpad/SourceInserter.scala (limited to 'src') diff --git a/src/compiler/scala/reflect/internal/Trees.scala b/src/compiler/scala/reflect/internal/Trees.scala index 4d202c6c60..5480d90c5a 100644 --- a/src/compiler/scala/reflect/internal/Trees.scala +++ b/src/compiler/scala/reflect/internal/Trees.scala @@ -1823,7 +1823,7 @@ trait Trees /*extends reflect.generic.Trees*/ { self: SymbolTable => // >: lo <: hi case ExistentialTypeTree(tpt, whereClauses) => (eliminated by uncurry) // tpt forSome { whereClauses } - + case SelectFromArray(_, _, _) => (created and eliminated by erasure) */ } diff --git a/src/compiler/scala/tools/nsc/interactive/CompilerControl.scala b/src/compiler/scala/tools/nsc/interactive/CompilerControl.scala index 747d74f96c..ef5791e6d1 100644 --- a/src/compiler/scala/tools/nsc/interactive/CompilerControl.scala +++ b/src/compiler/scala/tools/nsc/interactive/CompilerControl.scala @@ -203,6 +203,21 @@ trait CompilerControl { self: Global => def askParsedEntered(source: SourceFile, keepLoaded: Boolean, response: Response[Tree]) = postWorkItem(new AskParsedEnteredItem(source, keepLoaded, response)) + /** Set sync var `response` to a pair consisting of + * - the fully qualified name of the first top-level object definition in the file. + * or "" if there are no object definitions. + * - the text of the instrumented program which, when run, + * prints its output and all defined values in a comment column. + * + * @param source The source file to be analyzed + * @param keepLoaded If set to `true`, source file will be kept as a loaded unit afterwards. + * If keepLoaded is `false` the operation is run at low priority, only after + * everything is brought up to date in a regular type checker run. + * @param response The response. + */ + def askInstrumented(source: SourceFile, response: Response[(String, SourceFile)]) = + postWorkItem(new AskInstrumentedItem(source, response)) + /** Cancels current compiler run and start a fresh one where everything will be re-typechecked * (but not re-loaded). */ @@ -211,7 +226,7 @@ trait CompilerControl { self: Global => /** Tells the compile server to shutdown, and not to restart again */ def askShutdown() = scheduler raise ShutdownReq - @deprecated("use parseTree(source) instead") // deleted 2nd parameter, as thius has to run on 2.8 also. + @deprecated("use parseTree(source) instead") // deleted 2nd parameter, as this has to run on 2.8 also. def askParse(source: SourceFile, response: Response[Tree]) = respond(response) { parseTree(source) } @@ -312,6 +327,11 @@ trait CompilerControl { self: Global => def apply() = self.getParsedEntered(source, keepLoaded, response, this.onCompilerThread) override def toString = "getParsedEntered "+source+", keepLoaded = "+keepLoaded } + + class AskInstrumentedItem(val source: SourceFile, response: Response[(String, SourceFile)]) extends WorkItem { + def apply() = self.getInstrumented(source, response) + override def toString = "getInstrumented "+source + } } // ---------------- Interpreted exceptions ------------------- diff --git a/src/compiler/scala/tools/nsc/interactive/Global.scala b/src/compiler/scala/tools/nsc/interactive/Global.scala index af5b9a9882..4f1f0946ed 100644 --- a/src/compiler/scala/tools/nsc/interactive/Global.scala +++ b/src/compiler/scala/tools/nsc/interactive/Global.scala @@ -30,6 +30,7 @@ class Global(settings: Settings, reporter: Reporter, projectName: String = "") with RangePositions with ContextTrees with RichCompilationUnits + with ScratchPadMaker with Picklers { import definitions._ @@ -630,7 +631,7 @@ class Global(settings: Settings, reporter: Reporter, projectName: String = "") } /** A fully attributed tree corresponding to the entire compilation unit */ - private def typedTree(source: SourceFile, forceReload: Boolean): Tree = { + private[interactive] def typedTree(source: SourceFile, forceReload: Boolean): Tree = { informIDE("typedTree " + source + " forceReload: " + forceReload) val unit = getOrCreateUnitOf(source) if (forceReload) reset(unit) @@ -912,6 +913,12 @@ class Global(settings: Settings, reporter: Reporter, projectName: String = "") } } + def getInstrumented(source: SourceFile, response: Response[(String, SourceFile)]) { + respond(response) { + instrument(source) + } + } + // ---------------- Helper classes --------------------------- /** A transformer that replaces tree `from` with tree `to` in a given tree */ diff --git a/src/compiler/scala/tools/nsc/interactive/REPL.scala b/src/compiler/scala/tools/nsc/interactive/REPL.scala index 7545a2f714..f5b536a3f4 100644 --- a/src/compiler/scala/tools/nsc/interactive/REPL.scala +++ b/src/compiler/scala/tools/nsc/interactive/REPL.scala @@ -89,6 +89,7 @@ object REPL { val completeResult = new Response[List[comp.Member]] val typedResult = new Response[comp.Tree] val structureResult = new Response[comp.Tree] + val instrumentedResult = new Response[(String, SourceFile)] def makePos(file: String, off1: String, off2: String) = { val source = toSourceFile(file) @@ -111,6 +112,11 @@ object REPL { show(structureResult) } + def formatInstrumented(r: (String, SourceFile)) = { + val (name, source) = r + "toplevel object = "+name+", contents = \n"+source.content.mkString + } + loop { line => (line split " ").toList match { case "reload" :: args => @@ -132,6 +138,11 @@ object REPL { doComplete(makePos(file, off1, off2)) case List("complete", file, off1) => doComplete(makePos(file, off1, off1)) + case List("instrument", file) => + val source = toSourceFile(file) + comp.askReload(List(source), reloadResult) + comp.askInstrumented(source, instrumentedResult) + show(instrumentedResult, formatInstrumented) case List("quit") => comp.askShutdown() // deleted sys. as this has to run on 2.8 also @@ -146,9 +157,9 @@ object REPL { def toSourceFile(name: String) = new BatchSourceFile(new PlainFile(new java.io.File(name))) - def show[T](svar: Response[T]) { + def show[T](svar: Response[T], transform: T => Any = (x: T) => x) { svar.get match { - case Left(result) => println("==> "+result) + case Left(result) => println("==> "+transform(result)) case Right(exc) => exc.printStackTrace; println("ERROR: "+exc) } svar.clear() diff --git a/src/compiler/scala/tools/nsc/interactive/ScratchPadMaker.scala b/src/compiler/scala/tools/nsc/interactive/ScratchPadMaker.scala new file mode 100644 index 0000000000..a54513c9ab --- /dev/null +++ b/src/compiler/scala/tools/nsc/interactive/ScratchPadMaker.scala @@ -0,0 +1,130 @@ +package scala.tools.nsc +package interactive + +import util.{SourceFile, BatchSourceFile, RangePosition} +import collection.mutable.ArrayBuffer +import reflect.internal.Chars.isLineBreakChar + +trait ScratchPadMaker { self: Global => + + import definitions._ + + private case class Patch(offset: Int, text: String) + + private class Patcher(contents: Array[Char]) extends Traverser { + var objectName: String = "" + + private val patches = new ArrayBuffer[Patch] + private val toPrint = new ArrayBuffer[String] + private var skipped = 0 + private var resNum: Int = -1 + + private def nextRes(): String = { + resNum += 1 + "res$"+resNum + } + + private def nameType(name: String, tpe: Type): String = name+": "+tpe + + private def nameType(sym: Symbol): String = nameType(sym.name.toString, sym.tpe) + + private def literal(str: String) = "\"\"\"\""+str+"\"\"\"\"" + + private def addSkip(stat: Tree): Unit = { + if (stat.pos.start > skipped) { + for (msg <- toPrint) + patches += Patch(stat.pos.start, ";println("+msg+")") + } + var end = stat.pos.end + if (end > skipped) { + while (end < contents.length && !(isLineBreakChar(contents(end)))) end += 1 + patches += Patch(stat.pos.start, ";skip("+(end-skipped)+"); ") + skipped = end + } + } + + private def addSandbox(expr: Tree) = + patches += (Patch(expr.pos.start, "sandbox("), Patch(expr.pos.end, ")")) + + private def traverseStat(stat: Tree) = if (stat.pos.isInstanceOf[RangePosition]) { + stat match { + case ValDef(_, _, _, rhs) => + addSkip(stat) + if (stat.symbol.isLazy) + toPrint += literal(nameType(stat.symbol)+" = ") + else if (!stat.symbol.isSynthetic) { + addSandbox(rhs) + toPrint += literal(nameType(stat.symbol)+" = ")+" + "+stat.symbol.name + } + case DefDef(_, _, _, _, _, _) => + addSkip(stat) + toPrint += literal(nameType(stat.symbol)) + case Annotated(_, arg) => + traverse(arg) + case DocDef(_, defn) => + traverse(defn) + case _ => + if (stat.isTerm) { + addSkip(stat) + if (stat.tpe.typeSymbol == UnitClass) { + addSandbox(stat) + } else { + val resName = nextRes() + val dispResName = resName filter ('$' !=) + patches += Patch(stat.pos.start, "val "+resName+" = ") + addSandbox(stat) + toPrint += literal(nameType(dispResName, stat.tpe)+" = ")+" + "+resName + } + } + } + } + + override def traverse(tree: Tree): Unit = tree match { + case PackageDef(_, _) => + super.traverse(tree) + case ModuleDef(_, name, Template(_, _, body)) => + if (objectName.isEmpty) objectName = tree.symbol.fullName + body foreach traverseStat + case _ => + } + + /** The patched text. + * @require traverse is run first + */ + def result: Array[Char] = { + val reslen = contents.length + (patches map (_.text.length)).sum + val res = Array.ofDim[Char](reslen) + var lastOffset = 0 + var from = 0 + var to = 0 + for (Patch(offset, text) <- patches) { + val delta = offset - lastOffset + assert(delta >= 0) + Array.copy(contents, from, res, to, delta) + from += delta + to += delta + lastOffset = offset + text.copyToArray(res, to) + to += text.length + } + assert(contents.length - from == reslen - to) + Array.copy(contents, from, res, to, contents.length - from) + res + } + } + + /** Compute an instrumented version of a sourcefile. + * @param source The given sourcefile. + * @return A pair consisting of + * - the fully qualified name of the first top-level object definition in the file. + * or "" if there are no object definitions. + * - the text of the instrumented program which, when run, + * prints its output and all defined values in a comment column. + */ + protected def instrument(source: SourceFile): (String, SourceFile) = { + val tree = typedTree(source, true) + val patcher = new Patcher(source.content) + patcher.traverse(tree) + (patcher.objectName, new BatchSourceFile(source.file, patcher.result)) + } +} diff --git a/src/compiler/scala/tools/nsc/scratchpad/CommentOutputStream.scala b/src/compiler/scala/tools/nsc/scratchpad/CommentOutputStream.scala new file mode 100644 index 0000000000..92ccd79df9 --- /dev/null +++ b/src/compiler/scala/tools/nsc/scratchpad/CommentOutputStream.scala @@ -0,0 +1,18 @@ +package scala.tools.nsc.scratchpad + +import java.io.OutputStream + +class CommentOutputStream(out: CommentWriter, encoding: String = "") extends OutputStream { + + override def write(bs: Array[Byte]) = + out.write(if (encoding.isEmpty) new String(bs) else new String(bs, encoding)) + + override def write(bs: Array[Byte], off: Int, len: Int) = + out.write(if (encoding.isEmpty) new String(bs, off, len) else new String(bs, off, len, encoding)) + + override def write(ch: Int) = + write(Array(ch.toByte)) + + override def close() = out.close() + override def flush() = out.flush() +} \ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/scratchpad/CommentWriter.scala b/src/compiler/scala/tools/nsc/scratchpad/CommentWriter.scala new file mode 100644 index 0000000000..607aa97568 --- /dev/null +++ b/src/compiler/scala/tools/nsc/scratchpad/CommentWriter.scala @@ -0,0 +1,42 @@ +package scala.tools.nsc.scratchpad + +import java.io.Writer +import reflect.internal.Chars._ + + +class CommentWriter(underlying: SourceInserter, startCol: Int = 40, endCol: Int = 124) extends Writer { + + private def rightCol(marker: String) = { + while (underlying.column < startCol) underlying.write(' ') + underlying.write(marker) + } + + private var lastWasNL = false + + private def writeChar(ch: Char) = { + if (underlying.column >= endCol) { + underlying.write('\n'); rightCol("//| ") + } + if (underlying.column < startCol) rightCol("//> ") + underlying.write(ch) + lastWasNL = isLineBreakChar(ch) + } + + override def write(chs: Array[Char], off: Int, len: Int) = { + for (i <- off until off + len) writeChar(chs(i)) + flush() + } + + def skip(len: Int) { + if (lastWasNL) { + underlying.backspace() + lastWasNL = false + } + underlying.skip(len) + if (underlying.column >= startCol) underlying.write('\n') + } + + override def close() = underlying.close() + override def flush() = underlying.flush() +} + diff --git a/src/compiler/scala/tools/nsc/scratchpad/Executor.scala b/src/compiler/scala/tools/nsc/scratchpad/Executor.scala new file mode 100644 index 0000000000..510f6c5a3b --- /dev/null +++ b/src/compiler/scala/tools/nsc/scratchpad/Executor.scala @@ -0,0 +1,28 @@ +package scala.tools.nsc.scratchpad + +import java.io.{PrintStream, OutputStreamWriter, Writer} + +object Executor { + + println("exec started") + + def execute(name: String, si: SourceInserter) { + val oldOut = System.out + val oldErr = System.err + val cwr = new CommentWriter(si) + val newOut = new PrintStream(new CommentOutputStream(cwr)) + java.lang.System.setOut(newOut) + java.lang.System.setErr(newOut) + + try { + Class.forName(name).newInstance() + } catch { + case ex: Throwable => + ex.printStackTrace() + } finally { + cwr.close() + System.setOut(oldOut) + System.setErr(oldErr) + } + } +} \ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/scratchpad/SourceInserter.scala b/src/compiler/scala/tools/nsc/scratchpad/SourceInserter.scala new file mode 100644 index 0000000000..f6c91838db --- /dev/null +++ b/src/compiler/scala/tools/nsc/scratchpad/SourceInserter.scala @@ -0,0 +1,112 @@ +package scala.tools.nsc +package scratchpad + +import java.io.Writer +import util.SourceFile + +import reflect.internal.Chars._ + +object SourceInserter { + def stripRight(cs: Array[Char]): Array[Char] = { + val lines = + new String(cs) split "\n" + def leftPart(str: String) = + (str split """//>|//\|""").head + def isContinuation(str: String) = + ((str contains "//>") || (str contains "//|")) && (leftPart(str) forall isWhitespace) + def stripTrailingWS(str: String) = + str take (str lastIndexWhere (!isWhitespace(_))) + 1 + val prefixes = + lines filterNot isContinuation map leftPart map stripTrailingWS + (prefixes mkString "\n").toArray + } +} + +class SourceInserter(contents: Array[Char], start: Int = 0, tabInc: Int = 8) extends Writer { + + private var buf = contents + private var offset = start + private var hilen = 0 + + def length = offset + hilen + + private def currentColumn: Int = { + var i = offset + while (i > 0 && !isLineBreakChar(buf(i - 1))) i -= 1 + var col = 0 + while (i < offset) { + col = if (buf(i) == '\t') (col + tabInc) / tabInc * tabInc else col + 1 + i += 1 + } + col + } + + private var col = currentColumn + + def column = synchronized { col } + + private def addCapacity(n: Int) = { + val newlength = length + n + while (newlength > buf.length) { + val buf1 = Array.ofDim[Char](buf.length * 2) + Array.copy(buf, 0, buf1, 0, offset) + Array.copy(buf, buf.length - hilen, buf1, buf1.length - hilen, hilen) + buf = buf1 + } + } + + private def insertChar(ch: Char) = { + buf(offset) = ch.toChar + offset += 1 + ch match { + case LF => col = 0 + case '\t' => col = (col + tabInc) / tabInc * tabInc + case _ => col += 1 + } + } + + override def write(ch: Int) = synchronized { + addCapacity(1) + insertChar(ch.toChar) + } + + override def write(chs: Array[Char], off: Int, len: Int) = synchronized { + addCapacity(len) + for (i <- off until off + len) insertChar(chs(i)) + } + + override def close() { + buf = null + } + + override def flush() { + // signal buffer change + } + + def currentContents = synchronized { + if (length == buf.length) buf + else { + val res = Array.ofDim[Char](length) + Array.copy(buf, 0, res, 0, offset) + Array.copy(buf, buf.length - hilen, res, offset, hilen) + res + } + } + + def backspace() = synchronized { + offset -= 1 + if (offset > 0 && buf(offset) == LF && buf(offset - 1) == CR) offset -=1 + } + + def currentChar = synchronized { + buf(buf.length - hilen) + } + + def skip(len: Int) = synchronized { + for (i <- 0 until len) { + insertChar(currentChar) + hilen -= 1 + } + } +} + -- cgit v1.2.3