diff --git a/src/compiler/scala/tools/nsc/Global.scala b/src/compiler/scala/tools/nsc/Global.scala index e816ea58f1b1..44d57e960fc4 100644 --- a/src/compiler/scala/tools/nsc/Global.scala +++ b/src/compiler/scala/tools/nsc/Global.scala @@ -1692,10 +1692,11 @@ class Global(var currentSettings: Settings, reporter0: Reporter) def compileLate(unit: CompilationUnit): Unit = { addUnit(unit) - if (firstPhase ne null) { // we might get here during initialization, is a source is newer than the binary + if (firstPhase ne null) { // we might get here during initialization, if a source is newer than the binary val maxId = math.max(globalPhase.id, typerPhase.id) - firstPhase.iterator takeWhile (_.id < maxId) foreach (ph => - enteringPhase(ph)(ph.asInstanceOf[GlobalPhase] applyPhase unit)) + firstPhase.iterator.takeWhile(_.id < maxId).foreach { ph => + enteringPhase(ph)(ph.asInstanceOf[GlobalPhase].applyPhase(unit)) + } refreshProgress() } } diff --git a/src/interactive/scala/tools/nsc/interactive/CompilerControl.scala b/src/interactive/scala/tools/nsc/interactive/CompilerControl.scala index a988528b013d..01072808f446 100644 --- a/src/interactive/scala/tools/nsc/interactive/CompilerControl.scala +++ b/src/interactive/scala/tools/nsc/interactive/CompilerControl.scala @@ -193,17 +193,16 @@ trait CompilerControl { self: Global => def askToDoFirst(source: SourceFile) = postWorkItem(new AskToDoFirstItem(source)) - /** If source is not yet loaded, loads it, and starts a new run, otherwise - * continues with current pass. - * Waits until source is fully type checked and returns body in response. - * @param source The source file that needs to be fully typed. - * @param keepLoaded Whether to keep that file in the PC if it was not loaded before. If - the file is already loaded, this flag is ignored. - * @param response The response, which is set to the fully attributed tree of `source`. - * If the unit corresponding to `source` has been removed in the meantime - * the a NoSuchUnitError is raised in the response. + /** If source is not yet loaded, loads it, and starts a new run, otherwise continues with current pass. + * Waits until source is fully type checked and returns body in response. + * @param source The source file that needs to be fully typed. + * @param keepLoaded Whether to keep that file in the PC if it was not loaded before. If + * the file is already loaded, this flag is ignored. + * @param response The response, which is set to the fully attributed tree of `source`. + * If the unit corresponding to `source` has been removed in the meantime + * the a NoSuchUnitError is raised in the response. */ - def askLoadedTyped(source:SourceFile, keepLoaded: Boolean, response: Response[Tree]): Unit = + def askLoadedTyped(source: SourceFile, keepLoaded: Boolean, response: Response[Tree]): Unit = postWorkItem(new AskLoadedTypedItem(source, keepLoaded, response)) final def askLoadedTyped(source: SourceFile, response: Response[Tree]): Unit = @@ -260,15 +259,14 @@ trait CompilerControl { self: Global => if (self.onCompilerThread) { try { r set op() } catch { case exc: Throwable => r raise exc } - r } else { val ir = scheduler askDoQuickly op ir onComplete { case Left(result) => r set result case Right(exc) => r raise exc } - r } + r } def onCompilerThread = Thread.currentThread == compileRunner @@ -327,7 +325,7 @@ trait CompilerControl { self: Global => case class ReloadItem(sources: List[SourceFile], response: Response[Unit]) extends WorkItem { def apply() = reload(sources, response) - override def toString = "reload "+sources + override def toString = s"reload $sources" def raiseMissing() = response raise new MissingResponse diff --git a/src/interactive/scala/tools/nsc/interactive/ContextTrees.scala b/src/interactive/scala/tools/nsc/interactive/ContextTrees.scala index 43fa50776aec..894e68939081 100644 --- a/src/interactive/scala/tools/nsc/interactive/ContextTrees.scala +++ b/src/interactive/scala/tools/nsc/interactive/ContextTrees.scala @@ -191,4 +191,3 @@ trait ContextTrees { self: Global => } } } - diff --git a/src/interactive/scala/tools/nsc/interactive/Global.scala b/src/interactive/scala/tools/nsc/interactive/Global.scala index 1daf9b72960f..f94ebc5b17cb 100644 --- a/src/interactive/scala/tools/nsc/interactive/Global.scala +++ b/src/interactive/scala/tools/nsc/interactive/Global.scala @@ -25,9 +25,9 @@ import scala.reflect.internal.util.SourceFile import scala.tools.nsc.io.AbstractFile import scala.tools.nsc.reporters.Reporter import scala.tools.nsc.symtab.Flags.{ACCESSOR, PARAMACCESSOR} -import scala.tools.nsc.symtab._ +import scala.tools.nsc.symtab.BrowsingLoaders import scala.tools.nsc.typechecker.{Analyzer, Typers} -import scala.util.control.Breaks._ +//import scala.util.chaining._ import scala.util.control.ControlThrowable /** @@ -35,15 +35,14 @@ import scala.util.control.ControlThrowable * does not clear the comments table at every new typer run (those * being many and close between in this context). */ - trait CommentPreservingTypers extends Typers { self: Analyzer => - override def resetDocComments() = {} + override def resetDocComments() = () } trait InteractiveAnalyzer extends Analyzer { - val global : Global + val global: Global import global._ override def newTyper(context: Context): InteractiveTyper = new Typer(context) with InteractiveTyper @@ -155,15 +154,15 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") /** Print msg only when debugIDE is true. */ @inline final def debugLog(msg: => String) = - if (debugIDE) println("[%s] %s".format(projectName, msg)) + if (debugIDE) println(s"[$projectName] $msg") /** Inform with msg only when verboseIDE is true. */ @inline final def informIDE(msg: => String) = - if (verboseIDE) println("[%s][%s]".format(projectName, msg)) + if (verboseIDE) println(s"[$projectName][$msg]") // don't keep the original owner in presentation compiler runs // (the map will grow indefinitely, and the only use case is the backend) - override def defineOriginalOwner(sym: Symbol, owner: Symbol): Unit = { } + override def defineOriginalOwner(sym: Symbol, owner: Symbol): Unit = () override protected def synchronizeNames = true @@ -209,9 +208,9 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") /** A map that associates with each abstract file the set of responses that ware waiting * (via build) for the unit associated with the abstract file to be parsed and entered */ - protected var getParsedEnteredResponses = newResponseMap + protected val getParsedEnteredResponses = newResponseMap - private def cleanResponses(rmap: ResponseMap): Unit = { + private def cleanResponses(rmap: ResponseMap): Unit = for ((source, rs) <- rmap.toList) { for (r <- rs) { if (getUnit(source).isEmpty) @@ -222,7 +221,6 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") if (rmap(source).isEmpty) rmap -= source } - } override lazy val analyzer = new { val global: Global.this.type = Global.this @@ -320,12 +318,10 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") /** Called from parser, which signals hereby that a method definition has been parsed. */ - override def signalParseProgress(pos: Position): Unit = { + override def signalParseProgress(pos: Position): Unit = // We only want to be interruptible when running on the PC thread. - if(onCompilerThread) { + if (onCompilerThread) checkForMoreWork(pos) - } - } /** Called from typechecker, which signals hereby that a node has been completely typechecked. * If the node includes unit.targetPos, abandons run and returns newly attributed tree. @@ -351,17 +347,17 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") } throw new TyperResult(located) } - else { + else try { + debugLog(s"Typer signal done, checking for more work...") checkForMoreWork(old.pos) } catch { case ex: ValidateException => // Ignore, this will have been reported elsewhere - debugLog("validate exception caught: "+ex) + debugLog(s"validate exception caught: $ex") case ex: Throwable => log.flush() throw ex } - } } } @@ -383,7 +379,7 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") /** Called from typechecker every time a top-level class or object is entered. */ - override def registerTopLevelSym(sym: Symbol): Unit = { currentTopLevelSyms += sym } + override def registerTopLevelSym(sym: Symbol): Unit = currentTopLevelSyms.addOne(sym) protected type SymbolLoadersInInteractive = GlobalSymbolLoaders { val global: Global.this.type @@ -428,101 +424,99 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") * @param pos The position of the tree if polling while typechecking, NoPosition otherwise * */ - private[interactive] def pollForWork(pos: Position): Unit = { - var loop: Boolean = true - while (loop) { - breakable{ - loop = false - // TODO refactor to eliminate breakable/break/return? - (if (!interruptsEnabled) return): @nowarn("cat=lint-nonlocal-return") - if (pos == NoPosition || nodesSeen % yieldPeriod == 0) - Thread.`yield`() - - def nodeWithWork(): Option[WorkEvent] = - if (scheduler.moreWork || pendingResponse.isCancelled) Some(new WorkEvent(nodesSeen, System.currentTimeMillis)) - else None - - nodesSeen += 1 - logreplay("atnode", nodeWithWork()) match { - case Some(WorkEvent(id, _)) => - debugLog("some work at node "+id+" current = "+nodesSeen) - // assert(id >= nodesSeen) - moreWorkAtNode = id - case None => - } + @annotation.tailrec + final private[interactive] def pollForWork(pos: Position): Unit = { + if (!interruptsEnabled) + return + if (pos == NoPosition || nodesSeen % yieldPeriod == 0) + Thread.`yield`() + + def nodeWithWork(): Option[WorkEvent] = + if (scheduler.moreWork || pendingResponse.isCancelled) Some(new WorkEvent(nodesSeen, System.currentTimeMillis)) + else None + + nodesSeen += 1 + logreplay("atnode", nodeWithWork()) match { + case Some(WorkEvent(id, _)) => + debugLog(s"some work at node $id current = $nodesSeen") + // assert(id >= nodesSeen) + moreWorkAtNode = id + case None => + } - if (nodesSeen >= moreWorkAtNode) { - - logreplay("asked", scheduler.pollInterrupt()) match { - case Some(ir) => - try { - interruptsEnabled = false - debugLog("ask started"+timeStep) - ir.execute() - } finally { - debugLog("ask finished"+timeStep) - interruptsEnabled = true - } - loop = true; break() - case _ => - } + if (nodesSeen < moreWorkAtNode) + return - if (logreplay("cancelled", pendingResponse.isCancelled)) { - throw CancelException - } + logreplay("asked", scheduler.pollInterrupt()) match { + case Some(ir) => + try { + interruptsEnabled = false + debugLog(s"ask started$timeStep") + ir.execute() + } finally { + debugLog(s"ask finished$timeStep") + interruptsEnabled = true + } + pollForWork(pos) + case _ => + if (logreplay("cancelled", pendingResponse.isCancelled)) + throw CancelException - logreplay("exception thrown", scheduler.pollThrowable()) match { - case Some(ex: FreshRunReq) => - newTyperRun() + logreplay("exception thrown", scheduler.pollThrowable()) match { + case Some(ex: FreshRunReq) => + newTyperRun() minRunId = currentRunId demandNewCompilerRun() + case Some(ShutdownReq) => + scheduler.synchronized { // lock the work queue so no more items are posted while we clean it up + val units = scheduler.dequeueAll { + case item: WorkItem => Some(item.raiseMissing()) + case _ => Some(()) + } - case Some(ShutdownReq) => - scheduler.synchronized { // lock the work queue so no more items are posted while we clean it up - val units = scheduler.dequeueAll { - case item: WorkItem => Some(item.raiseMissing()) - case _ => Some(()) - } - - // don't forget to service interrupt requests - scheduler.dequeueAllInterrupts(_.execute()) - - debugLog("ShutdownReq: cleaning work queue (%d items)".format(units.size)) - debugLog("Cleanup up responses (%d loadedType pending, %d parsedEntered pending)" - .format(waitLoadedTypeResponses.size, getParsedEnteredResponses.size)) - checkNoResponsesOutstanding() + // don't forget to service interrupt requests + scheduler.dequeueAllInterrupts(_.execute()) - log.flush() - scheduler = new NoWorkScheduler - throw ShutdownReq - } + debugLog("ShutdownReq: cleaning work queue (%d items)".format(units.size)) + debugLog("Cleanup up responses (%d loadedType pending, %d parsedEntered pending)" + .format(waitLoadedTypeResponses.size, getParsedEnteredResponses.size)) + checkNoResponsesOutstanding() - case Some(ex: Throwable) => log.flush(); throw ex - case _ => - } + log.flush() + scheduler = new NoWorkScheduler + throw ShutdownReq + } + case Some(ex: Throwable) => + log.flush() + throw ex + case _ => + } - lastWasReload = false + lastWasReload = false - logreplay("workitem", scheduler.nextWorkItem()) match { - case Some(action) => - try { - debugLog("picked up work item at "+pos+": "+action+timeStep) - action() - debugLog("done with work item: "+action) - } finally { - debugLog("quitting work item: "+action+timeStep) - } - case None => - } + logreplay("workitem", scheduler.nextWorkItem()) match { + case Some(action) => + try { + debugLog(s"picked up work item at $pos: $action$timeStep") + action() + debugLog(s"done with work item: $action") + } finally { + debugLog(s"quitting work item: $action$timeStep") + } + case None => } - } - } + // end default + } // end match } protected def checkForMoreWork(pos: Position): Unit = { val typerRun = currentTyperRun pollForWork(pos) - if (typerRun != currentTyperRun) demandNewCompilerRun() + if (typerRun != currentTyperRun) { + debugLog(s"Checking for work has created a new run, demanding a new run while is${ + if (isOutOfDate) "" else " not"} out of date...") + demandNewCompilerRun() + } } // ----------------- The Background Runner Thread ----------------------- @@ -537,36 +531,33 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") * Compiler initialization may happen on a different thread (signalled by globalPhase being NoPhase) */ @elidable(elidable.WARNING) - override def assertCorrectThread(): Unit = { - assert(initializing || anyThread || onCompilerThread, - "Race condition detected: You are running a presentation compiler method outside the PC thread.[phase: %s]".format(globalPhase) + - " Please file a ticket with the current stack trace at https://www.assembla.com/spaces/scala-ide/support/tickets") - } + override def assertCorrectThread(): Unit = assert(initializing || anyThread || onCompilerThread, + s"Race condition detected! You are running a presentation compiler method off the PC thread. [phase: $globalPhase]" + ) /** Create a new presentation compiler runner. */ private def newRunnerThread(): Thread = { threadId += 1 - compileRunner = new PresentationCompilerThread(this, projectName) - compileRunner.setDaemon(true) - compileRunner + PresentationCompilerThread(this, projectName) } private def ensureUpToDate(unit: RichCompilationUnit) = if (!unit.isUpToDate && unit.status != JustParsed) reset(unit) // reparse previously typechecked units. /** Compile all loaded source files in the order given by `allSources`. + * + * Invoked by the PC thread task. Recurses if there are waiting responses. */ private[interactive] final def backgroundCompile(): Unit = { informIDE("Starting new presentation compiler type checking pass") reporter.reset() // remove any files in first that are no longer maintained by presentation compiler (i.e. closed) - allSources = allSources filter (s => unitOfFile contains (s.file)) + allSources = allSources.filter(s => unitOfFile.contains(s.file)) // ensure all loaded units are parsed for (s <- allSources; unit <- getUnit(s)) { - // checkForMoreWork(NoPosition) // disabled, as any work done here would be in an inconsistent state ensureUpToDate(unit) parseAndEnter(unit) serviceParsedEntered() @@ -577,18 +568,19 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") val limit = System.currentTimeMillis() + afterTypeDelay while (System.currentTimeMillis() < limit) { Thread.sleep(SleepTime) + debugLog(s"checkForMoreWork after sleeping") checkForMoreWork(NoPosition) } } // ensure all loaded units are typechecked - for (s <- allSources; if !ignoredFiles(s.file); unit <- getUnit(s)) { + for (s <- allSources if !ignoredFiles(s.file); unit <- getUnit(s)) try { if (!unit.isUpToDate) if (unit.problems.isEmpty || !settings.YpresentationStrict.value) typeCheck(unit) - else debugLog("%s has syntax errors. Skipped typechecking".format(unit)) - else debugLog("already up to date: "+unit) + else debugLog(s"$unit has syntax errors. Skipped typechecking") + else debugLog(s"already up to date: $unit") for (r <- waitLoadedTypeResponses(unit.source)) r set unit.body serviceParsedEntered() @@ -599,18 +591,14 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") case ex: Throwable => println("[%s]: exception during background compile: ".format(unit.source) + ex) ex.printStackTrace() - for (r <- waitLoadedTypeResponses(unit.source)) { + for (r <- waitLoadedTypeResponses(unit.source)) r.raise(ex) - } serviceParsedEntered() - lastException = Some(ex) ignoredFiles += unit.source.file println("[%s] marking unit as crashed (crashedFiles: %s)".format(unit, ignoredFiles)) - - reporter.error(unit.body.pos, "Presentation compiler crashed while type checking this file: %s".format(ex.toString())) + reporter.error(unit.body.pos, s"Presentation compiler crashed while type checking this file: $ex") } - } // move units removable after this run to the "to-be-removed" buffer toBeRemoved.synchronized { @@ -624,6 +612,7 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") // wind down if (waitLoadedTypeResponses.nonEmpty || getParsedEnteredResponses.nonEmpty) { + debugLog("Continue background compilation for waiting responses...") // need another cycle to treat those newTyperRun() backgroundCompile() @@ -662,7 +651,7 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") /** Parse unit and create a name index, unless this has already been done before */ private def parseAndEnter(unit: RichCompilationUnit): Unit = if (unit.status == NotLoaded) { - debugLog("parsing: "+unit) + debugLog(s"parsing: $unit") runReporting.clearSuppressionsComplete(unit.source) currentTyperRun.compileLate(unit) if (debugIDE && !reporter.hasErrors) validatePositions(unit.body) @@ -700,9 +689,8 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") } /** Move list of files to front of allSources */ - def moveToFront(fs: List[SourceFile]): Unit = { - allSources = fs ::: (allSources diff fs) - } + def moveToFront(fs: List[SourceFile]): Unit = + allSources = fs ::: allSources.diff(fs) // ----------------- Implementations of client commands ----------------------- @@ -719,9 +707,11 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") val result = results.head results = results.tail if (results.isEmpty) { - response set result + response.set(result) debugLog("responded"+timeStep) - } else response setProvisionally result + } + else + response.setProvisionally(result) } } } catch { @@ -760,7 +750,6 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") toBeRemoved.synchronized { toBeRemoved -= source.file } toBeRemovedAfterRun.synchronized { toBeRemovedAfterRun -= source.file } reset(unit) - //parseAndEnter(unit) } /** Make sure a set of compilation units is loaded and parsed */ @@ -859,15 +848,13 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") private def withTempUnits[T](sources: List[SourceFile])(f: (SourceFile => RichCompilationUnit) => T): T = { val unitOfSrc: SourceFile => RichCompilationUnit = src => unitOfFile(src.file) - sources filterNot (getUnit(_).isDefined) match { + sources.filterNot(getUnit(_).isDefined) match { case Nil => f(unitOfSrc) case unknown => reloadSources(unknown) - try { - f(unitOfSrc) - } finally - afterRunRemoveUnitsOf(unknown) + try f(unitOfSrc) + finally afterRunRemoveUnitsOf(unknown) } } @@ -931,42 +918,45 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") findMirrorSymbol(sym, u).pos } } else { - debugLog("link not in class "+sym+" "+source+" "+sym.owner) + debugLog(s"link not in class $sym $source ${sym.owner}") NoPosition } } } - private def forceDocComment(sym: Symbol, unit: RichCompilationUnit): Unit = { - unit.body foreachPartial { + private def forceDocComment(sym: Symbol, unit: RichCompilationUnit): Unit = + unit.body.foreachPartial { case DocDef(comment, defn) if defn.symbol == sym => fillDocComment(defn.symbol, comment) EmptyTree case _: ValOrDefDef => EmptyTree } - } /** Implements CompilerControl.askDocComment */ - private[interactive] def getDocComment(sym: Symbol, source: SourceFile, site: Symbol, fragments: List[(Symbol,SourceFile)], + private[interactive] def getDocComment(sym: Symbol, source: SourceFile, site: Symbol, + fragments: List[(Symbol, SourceFile)], response: Response[(String, String, Position)]): Unit = { informIDE(s"getDocComment $sym at $source, site $site") respond(response) { - withTempUnits(fragments.unzip._2){ units => - for((sym, src) <- fragments) { - val mirror = findMirrorSymbol(sym, units(src)) - if (mirror ne NoSymbol) forceDocComment(mirror, units(src)) + withTempUnits(fragments.unzip._2) { unitForSrc => + for ((sym, src) <- fragments) { + val mirror = findMirrorSymbol(sym, unitForSrc(src)) + if (mirror ne NoSymbol) forceDocComment(mirror, unitForSrc(src)) } - val mirror = findMirrorSymbol(sym, units(source)) + val mirror = findMirrorSymbol(sym, unitForSrc(source)) if (mirror eq NoSymbol) ("", "", NoPosition) else { - (expandedDocComment(mirror, site), rawDocComment(mirror), docCommentPos(mirror)) + val expansion = expandedDocComment(mirror, site) + debugLog(s"Expanded doc for $sym: $expansion") + (expansion, rawDocComment(mirror), docCommentPos(mirror)) } } } // New typer run to remove temp units and drop per-run caches that might refer to symbols entered from temp units. newTyperRun() + minRunId = currentRunId } def stabilizedType(tree: Tree): Type = tree match { @@ -1313,7 +1303,8 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") } /** Implements CompilerControl.askParsedEntered */ - private[interactive] def getParsedEntered(source: SourceFile, keepLoaded: Boolean, response: Response[Tree], onSameThread: Boolean = true): Unit = { + private[interactive] + def getParsedEntered(source: SourceFile, keepLoaded: Boolean, response: Response[Tree], onSameThread: Boolean): Unit = getUnit(source) match { case Some(unit) => getParsedEnteredNow(source, response) @@ -1328,17 +1319,15 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") getParsedEnteredResponses(source) += response } } - } /** Parses and enters given source file, storing parse tree in response */ - private def getParsedEnteredNow(source: SourceFile, response: Response[Tree]): Unit = { + private def getParsedEnteredNow(source: SourceFile, response: Response[Tree]): Unit = respond(response) { onUnitOf(source) { unit => parseAndEnter(unit) unit.body } } - } // ---------------- Helper classes --------------------------- @@ -1357,15 +1346,14 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") applyPhase(typerPhase, unit) } - /** Apply a phase to a compilation unit - * @return true iff typechecked correctly + /** Apply a phase to a compilation unit. */ - private def applyPhase(phase: Phase, unit: CompilationUnit): Unit = { + private def applyPhase(phase: Phase, unit: CompilationUnit): Unit = enteringPhase(phase) { phase.asInstanceOf[GlobalPhase] applyPhase unit } - } } def newTyperRun(): Unit = { + debugLog("new typer run") currentTyperRun = new TyperRun } diff --git a/src/interactive/scala/tools/nsc/interactive/PresentationCompilerThread.scala b/src/interactive/scala/tools/nsc/interactive/PresentationCompilerThread.scala index 47d42e8d79b3..207420897e73 100644 --- a/src/interactive/scala/tools/nsc/interactive/PresentationCompilerThread.scala +++ b/src/interactive/scala/tools/nsc/interactive/PresentationCompilerThread.scala @@ -12,47 +12,60 @@ package scala.tools.nsc.interactive +import scala.util.chaining._ + /** A presentation compiler thread. This is a lightweight class, delegating most * of its functionality to the compiler instance. * */ -final class PresentationCompilerThread(var compiler: Global, name: String = "") - extends Thread("Scala Presentation Compiler [" + name + "]") { +final class PresentationCompilerThread private (task: Runnable, name: String) extends Thread(task, name) - /** The presentation compiler loop. - */ - override def run(): Unit = { - compiler.debugLog("starting new runner thread") - while (compiler ne null) try { - compiler.checkNoResponsesOutstanding() - compiler.log.logreplay("wait for more work", { compiler.scheduler.waitForMoreWork(); true }) - compiler.pollForWork(compiler.NoPosition) - while (compiler.isOutOfDate) { - try { - compiler.backgroundCompile() - } catch { - case ex: FreshRunReq => - compiler.debugLog("fresh run req caught, starting new pass") - } - compiler.log.flush() - } - } catch { - case ex @ ShutdownReq => - compiler.debugLog("exiting presentation compiler") - compiler.log.close() +object PresentationCompilerThread { + + def apply(compiler: Global, name: String) = + new PresentationCompilerThread(new Task(compiler), s"Scala Presentation Compiler [$name]") + .tap(_.setDaemon(true)) - // make sure we don't keep around stale instances - compiler = null - case ex: Throwable => - compiler.log.flush() + private class Task(compiler: Global) extends Runnable { - ex match { + /** The presentation compiler loop. + */ + override def run(): Unit = { + compiler.debugLog("starting new runner thread") + def loop(): Unit = + try { + compiler.checkNoResponsesOutstanding() + compiler.log.logreplay("wait for more work", { compiler.scheduler.waitForMoreWork(); true }) + compiler.pollForWork(compiler.NoPosition) + while (compiler.isOutOfDate) { + try compiler.backgroundCompile() + catch { + case ex: FreshRunReq => compiler.debugLog("fresh run req caught, starting new pass") + } + compiler.log.flush() + } + loop() + } catch { + case ShutdownReq => + compiler.debugLog("exiting presentation compiler") + compiler.log.close() case ex: FreshRunReq => - compiler.debugLog("fresh run req caught outside presentation compiler loop; ignored") // This shouldn't be reported - case _ : Global#ValidateException => // This will have been reported elsewhere + compiler.log.flush() + compiler.debugLog("fresh run req caught outside presentation compiler loop; ignored") + // This shouldn't be reported + loop() + case _ : Global#ValidateException => + compiler.log.flush() compiler.debugLog("validate exception caught outside presentation compiler loop; ignored") - case _ => ex.printStackTrace(); compiler.informIDE("Fatal Error: "+ex) + // This will have been reported elsewhere + loop() + case ex: RuntimeException => + compiler.log.flush() + ex.printStackTrace() + compiler.informIDE(s"Fatal Error: $ex") + compiler.log.close() } + loop() } } } diff --git a/src/interactive/scala/tools/nsc/interactive/Response.scala b/src/interactive/scala/tools/nsc/interactive/Response.scala index fc09c87ae1d9..ce88e2a7f314 100644 --- a/src/interactive/scala/tools/nsc/interactive/Response.scala +++ b/src/interactive/scala/tools/nsc/interactive/Response.scala @@ -112,4 +112,13 @@ class Response[T] { complete = false cancelled = false } + + override def toString = + if (!isComplete) "Response incomplete" + else if (isCancelled) "Response cancelled" + else get(0L) match { + case None => "Response has no provisional result" + case Some(Right(t)) => s"Response failed: ${t.getMessage}" + case Some(Left(data)) => s"Response data: ${data}" + } } diff --git a/src/interactive/scala/tools/nsc/interactive/tests/InteractiveTest.scala b/src/interactive/scala/tools/nsc/interactive/tests/InteractiveTest.scala index 6d31a7c869ab..ff96077b571a 100644 --- a/src/interactive/scala/tools/nsc/interactive/tests/InteractiveTest.scala +++ b/src/interactive/scala/tools/nsc/interactive/tests/InteractiveTest.scala @@ -75,19 +75,15 @@ abstract class InteractiveTest protected def ++(tests: PresentationCompilerTestDef*): Unit = testActions ++= tests /** Test's entry point */ - def main(args: Array[String]): Unit = { - try execute() - finally askShutdown() - } + def main(args: Array[String]): Unit = try execute() finally askShutdown() - protected def execute(): Unit = { + protected def execute(): Unit = util.stringFromStream { ostream => Console.withOut(ostream) { loadSources() runDefaultTests() } }.linesIterator.filterNot(filterOutLines).map(normalize).foreach(println) - } protected def filterOutLines(line: String) = false protected def normalize(s: String) = s @@ -105,10 +101,8 @@ abstract class InteractiveTest } /** Run all defined `PresentationCompilerTestDef` */ - protected def runDefaultTests(): Unit = { - //TODO: integrate random tests!, i.e.: if (runRandomTests) randomTests(20, sourceFiles) + protected def runDefaultTests(): Unit = testActions.foreach(_.runTest()) - } /** Perform n random tests with random changes. */ /**** diff --git a/src/interactive/scala/tools/nsc/interactive/tests/InteractiveTestSettings.scala b/src/interactive/scala/tools/nsc/interactive/tests/InteractiveTestSettings.scala index 6b01262615e5..815ec454f037 100644 --- a/src/interactive/scala/tools/nsc/interactive/tests/InteractiveTestSettings.scala +++ b/src/interactive/scala/tools/nsc/interactive/tests/InteractiveTestSettings.scala @@ -16,10 +16,8 @@ package tests import java.io.File.pathSeparatorChar import java.io.File.separatorChar -import scala.tools.nsc.interactive.tests.core.PresentationCompilerInstance import scala.tools.nsc.io.Path -import core.Reporter -import core.TestSettings +import core.{PresentationCompilerInstance, Reporter, TestSettings} trait InteractiveTestSettings extends TestSettings with PresentationCompilerInstance { @@ -53,7 +51,7 @@ trait InteractiveTestSettings extends TestSettings with PresentationCompilerInst } // Make the --sourcepath path provided in the .flags file (if any) relative to the test's base directory - if(settings.sourcepath.isSetByUser) + if (settings.sourcepath.isSetByUser) settings.sourcepath.value = (baseDir / Path(settings.sourcepath.value)).path adjustPaths(settings.bootclasspath, settings.classpath, settings.javabootclasspath, settings.sourcepath) diff --git a/src/interactive/scala/tools/nsc/interactive/tests/core/AskCommand.scala b/src/interactive/scala/tools/nsc/interactive/tests/core/AskCommand.scala index 662b158aee7e..c9442e219e27 100644 --- a/src/interactive/scala/tools/nsc/interactive/tests/core/AskCommand.scala +++ b/src/interactive/scala/tools/nsc/interactive/tests/core/AskCommand.scala @@ -18,27 +18,23 @@ import scala.annotation.unused import scala.tools.nsc.interactive.Response import scala.reflect.internal.util.Position import scala.reflect.internal.util.SourceFile +import scala.util.chaining._ /** * A trait for defining commands that can be queried to the * presentation compiler. - * */ + */ trait AskCommand { /** presentation compiler's instance. */ protected val compiler: Global - /** - * Presentation compiler's `askXXX` actions work by doing side-effects - * on a `Response` instance passed as an argument during the `askXXX` - * call. - * The defined method `ask` is meant to encapsulate this behavior. - * */ - protected def ask[T](op: Response[T] => Unit): Response[T] = { - val r = new Response[T] - op(r) - r - } + /** Presentation compiler's `ask` actions work by doing side-effects + * on a `Response` instance passed as an argument during the `ask` call. + * The defined method `ask` is meant to encapsulate this behavior. + */ + protected def ask[T](op: Response[T] => Unit): Response[T] = + new Response[T]().tap(op) } /** Ask the presentation compiler to shut-down. */ @@ -53,17 +49,19 @@ trait AskParse extends AskCommand { /** `sources` need to be entirely parsed before running the test * (else commands such as `AskTypeCompletionAt` may fail simply because * the source's AST is not yet loaded). + * + * Submit each parse job and get the response sequentially; + * otherwise, parser progress update will run the next job. */ - def askParse(sources: Seq[SourceFile]): Unit = { - val responses = sources map (askParse(_)) - responses.foreach(_.get) // force source files parsing - } + def askParse(sources: Seq[SourceFile]): Unit = + for (source <- sources) { + val _ = askParse(source).get + } - private def askParse(src: SourceFile, keepLoaded: Boolean = true): Response[Tree] = { + private def askParse(src: SourceFile, keepLoaded: Boolean = true): Response[Tree] = ask { compiler.askParsedEntered(src, keepLoaded, _) } - } } /** Ask the presentation compiler to reload a sequence of `sources` */ diff --git a/src/interactive/scala/tools/nsc/interactive/tests/core/SourcesCollector.scala b/src/interactive/scala/tools/nsc/interactive/tests/core/SourcesCollector.scala index 69aae26aecaa..f1d4d79d9b52 100644 --- a/src/interactive/scala/tools/nsc/interactive/tests/core/SourcesCollector.scala +++ b/src/interactive/scala/tools/nsc/interactive/tests/core/SourcesCollector.scala @@ -12,8 +12,8 @@ package scala.tools.nsc.interactive.tests.core -import scala.reflect.internal.util.{SourceFile,BatchSourceFile} -import scala.tools.nsc.io.{AbstractFile,Path} +import scala.reflect.internal.util.{SourceFile, BatchSourceFile} +import scala.tools.nsc.io.{AbstractFile, Path} private[tests] object SourcesCollector { type SourceFilter = Path => Boolean diff --git a/test/files/presentation/doc.check b/test/files/presentation/doc.check index 5a3ff13151de..d9b17959f18d 100644 --- a/test/files/presentation/doc.check +++ b/test/files/presentation/doc.check @@ -1 +1 @@ -reload: Base.scala, Class.scala, Derived.scala +reload: Class.scala diff --git a/test/files/presentation/doc/doc.scala b/test/files/presentation/doc/doc.scala index 761bcd9c96d3..f4b1f922133b 100644 --- a/test/files/presentation/doc/doc.scala +++ b/test/files/presentation/doc/doc.scala @@ -1,8 +1,8 @@ //> using options -Xlint -Werror -import scala.reflect.internal.util.{ BatchSourceFile, SourceFile } -import scala.tools.nsc.doc -import scala.tools.nsc.doc.base._ -import scala.tools.nsc.doc.base.comment._ +import scala.reflect.internal.util.{BatchSourceFile, SourceFile} +import scala.tools.nsc.doc.{ScaladocAnalyzer, ScaladocGlobalTrait} +import scala.tools.nsc.doc.base.{CommentFactoryBase, LinkTo, MemberLookupBase} +import scala.tools.nsc.doc.base.comment.{Body, Comment} import scala.tools.nsc.interactive._ import scala.tools.nsc.interactive.tests._ @@ -38,9 +38,9 @@ object Test extends InteractiveTest { prepre + docComment(nTags) + prepost + post } - override lazy val compiler: Global { def getComment(sym: Symbol, source: SourceFile, fragments: List[(Symbol,SourceFile)]): Option[Comment] } = { + override lazy val compiler: Global { def getComment(sym: Symbol, source: SourceFile, fragments: List[(Symbol, SourceFile)]): Option[Comment] } = { prepareSettings(settings) - new Global(settings, compilerReporter) with MemberLookupBase with CommentFactoryBase with doc.ScaladocGlobalTrait { + new Global(settings, compilerReporter) with MemberLookupBase with CommentFactoryBase with ScaladocGlobalTrait { outer => val global: this.type = this @@ -48,7 +48,7 @@ object Test extends InteractiveTest { @annotation.nowarn override lazy val analyzer = new { val global: outer.type = outer - } with doc.ScaladocAnalyzer with InteractiveAnalyzer { + } with ScaladocAnalyzer with InteractiveAnalyzer { override def newTyper(context: Context): InteractiveTyper with ScaladocTyper = new Typer(context) with InteractiveTyper with ScaladocTyper } @@ -59,7 +59,7 @@ object Test extends InteractiveTest { def warnNoLink = false def findExternalLink(sym: Symbol, name: String) = None - def getComment(sym: Symbol, source: SourceFile, fragments: List[(Symbol,SourceFile)]): Option[Comment] = { + def getComment(sym: Symbol, source: SourceFile, fragments: List[(Symbol, SourceFile)]): Option[Comment] = { val docResponse = new Response[(String, String, Position)] askDocComment(sym, source, sym.owner, fragments, docResponse) docResponse.get.swap.toOption flatMap { @@ -74,12 +74,11 @@ object Test extends InteractiveTest { } override def runDefaultTests(): Unit = { - import compiler._ + import compiler.{NoSymbol, TermName, Tree, TypeName, askParsedEntered, getComment} def findSource(name: String) = sourceFiles.find(_.file.name == name).get val className = names.head - for (name <- names; - i <- 1 to tags.length) { + for (name <- names; i <- 1 to tags.length) { val newText = text(name, i) val source = findSource("Class.scala") val batch = new BatchSourceFile(source.file, newText.toCharArray) @@ -100,12 +99,13 @@ object Test extends InteractiveTest { if (toplevel eq NoSymbol) { val clazz = compiler.rootMirror.EmptyPackage.info.decl(TypeName(className)) val term = clazz.info.decl(TermName(name)) - if (term eq NoSymbol) clazz.info.decl(TypeName(name)) else - if (term.isAccessor) term.accessed else term - } else toplevel + if (term eq NoSymbol) clazz.info.decl(TypeName(name)) + else if (term.isAccessor) term.accessed else term + } + else toplevel } - getComment(sym, batch, (sym,batch)::Nil) match { + getComment(sym, batch, sym -> batch :: Nil) match { case None => println(s"Got no doc comment for $name") case Some(comment) => import comment._ @@ -117,38 +117,5 @@ object Test extends InteractiveTest { } } } - - // The remainder of this test has been found to fail intermittently on Windows - // only. The problem is difficult to isolate and reproduce; see - // https://github.com/scala/scala-dev/issues/72 for details. - // So if we're on Windows, let's just bail out here. - if (scala.util.Properties.isWin) return - - // Check inter-classes documentation one-time retrieved ok. - val baseSource = findSource("Base.scala") - val derivedSource = findSource("Derived.scala") - def existsText(where: Any, text: String): Boolean = where match { - case s: String => s contains text - case s: Seq[_] => s exists (existsText(_, text)) - case p: Product => p.productIterator exists (existsText(_, text)) - case c: Comment => existsText(c.body, text) - case x => throw new MatchError(x) - } - val (derived, base) = compiler.ask { () => - val derived = compiler.rootMirror.RootPackage.info.decl(newTermName("p")).info.decl(newTypeName("Derived")) - (derived, derived.ancestors(0)) - } - val cmt1 = getComment(derived, derivedSource, (base, baseSource)::(derived, derivedSource)::Nil) - if (!existsText(cmt1, "This is Derived comment")) - println("Unexpected Derived class comment:"+cmt1) - - val (fooDerived, fooBase) = compiler.ask { () => - val decl = derived.tpe.decl(newTermName("foo")) - (decl, decl.allOverriddenSymbols(0)) - } - - val cmt2 = getComment(fooDerived, derivedSource, (fooBase, baseSource)::(fooDerived, derivedSource)::Nil) - if (!existsText(cmt2, "Base method has documentation")) - println("Unexpected foo method comment:"+cmt2) } } diff --git a/test/files/presentation/dock.check b/test/files/presentation/dock.check new file mode 100644 index 000000000000..63e7cd4ae3ab --- /dev/null +++ b/test/files/presentation/dock.check @@ -0,0 +1 @@ +reload: Base.scala, Derived.scala diff --git a/test/files/presentation/dock/doc.scala b/test/files/presentation/dock/doc.scala new file mode 100644 index 000000000000..e012ae1d3f10 --- /dev/null +++ b/test/files/presentation/dock/doc.scala @@ -0,0 +1,92 @@ +//> using options -Xlint -Werror +import scala.reflect.internal.util.SourceFile +import scala.tools.nsc.doc.{ScaladocAnalyzer, ScaladocGlobalTrait} +import scala.tools.nsc.doc.base.{CommentFactoryBase, LinkTo, MemberLookupBase} +import scala.tools.nsc.doc.base.comment.Comment +import scala.tools.nsc.interactive.{/*CommentPreservingTypers,*/ Global, InteractiveAnalyzer} +import scala.tools.nsc.interactive.tests.{InteractiveTest} + +object Test extends InteractiveTest { + //override protected def argsString: String = "-Ypresentation-debug -Ypresentation-verbose" + + type TestGlobal = Global { + def getComment(sym: Symbol, source: SourceFile, fragments: List[(Symbol, SourceFile)]): Option[Comment] + } + + override lazy val compiler: TestGlobal = { + prepareSettings(settings) + new Global(settings, compilerReporter, "doctest") with MemberLookupBase with CommentFactoryBase with ScaladocGlobalTrait { + outer => + + val global: this.type = this + + @annotation.nowarn + override lazy val analyzer = new { + val global: outer.type = outer + } with ScaladocAnalyzer with InteractiveAnalyzer /*with CommentPreservingTypers*/ { + override def newTyper(context: Context): InteractiveTyper with ScaladocTyper = + new Typer(context) with InteractiveTyper with ScaladocTyper + } + + def chooseLink(links: List[LinkTo]): LinkTo = links.head + def internalLink(sym: Symbol, site: Symbol) = None + def toString(link: LinkTo) = link.toString + def warnNoLink = false + def findExternalLink(sym: Symbol, name: String) = None + + def getComment(sym: Symbol, source: SourceFile, fragments: List[(Symbol, SourceFile)]): Option[Comment] = { + val docResponse = new Response[(String, String, Position)] + askDocComment(sym, source, sym.owner, fragments, docResponse) + docResponse.get.swap.toOption.flatMap { + case (expanded, raw, pos) if !expanded.isEmpty => + Some(ask { () => parseAtSymbol(expanded, raw, pos, sym.owner) }) + case _ => None + } + } + } + } + + override def runDefaultTests(): Unit = dorun() + + def dorun(): Boolean = { + import compiler.{TermName, TypeName, getComment} + def findSource(name: String) = sourceFiles.find(_.file.name == name).get + + // Check inter-classes documentation one-time retrieved ok. + val baseSource = findSource("Base.scala") + val derivedSource = findSource("Derived.scala") + def existsText(where: Any, text: String): Boolean = where match { + case s: String => s.contains(text) + case s: Seq[_] => s.exists(existsText(_, text)) + case p: Product => p.productIterator.exists(existsText(_, text)) + case c: Comment => existsText(c.body, text) + case x => throw new MatchError(x) + } + val (derived, base) = compiler.ask { () => + val derived = compiler.rootMirror.RootPackage.info.decl(TermName("p")).info.decl(TypeName("Derived")) + (derived, derived.ancestors(0)) + } + val cmt1 = getComment(derived, derivedSource, (base, baseSource) :: (derived, derivedSource) :: Nil) + if (!existsText(cmt1, "This is Derived comment")) { + println(s"Unexpected Derived class comment: $cmt1") + return false + } + + val (fooDerived, fooBase) = compiler.ask { () => + val decl = derived.tpe.decl(TermName("foo")) + (decl, decl.allOverriddenSymbols.head) + } + + getComment(fooDerived, derivedSource, (fooBase, baseSource) :: (fooDerived, derivedSource) :: Nil) match { + case Some(cmt2) => + if (!existsText(cmt2, "Base method has documentation")) { + println(s"Unexpected foo method ($fooDerived) comment: $cmt2") + return false + } + case None => + println(s"Missing foo method ($fooDerived) comment") + return false + } + true + } +} diff --git a/test/files/presentation/doc/src/p/Base.scala b/test/files/presentation/dock/src/p/Base.scala similarity index 100% rename from test/files/presentation/doc/src/p/Base.scala rename to test/files/presentation/dock/src/p/Base.scala diff --git a/test/files/presentation/doc/src/p/Derived.scala b/test/files/presentation/dock/src/p/Derived.scala similarity index 100% rename from test/files/presentation/doc/src/p/Derived.scala rename to test/files/presentation/dock/src/p/Derived.scala