summaryrefslogtreecommitdiff
path: root/src/interactive/scala/tools/nsc/interactive/CompilerControl.scala
blob: 462f4432cde16859ba630b1a76c49eae2e021150 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
/* NSC -- new Scala compiler
 * Copyright 2009-2013 Typesafe/Scala Solutions and LAMP/EPFL
 * @author Martin Odersky
 */
package scala.tools.nsc
package interactive

import scala.util.control.ControlThrowable
import scala.tools.nsc.io.AbstractFile
import scala.tools.nsc.util.FailedInterrupt
import scala.tools.nsc.util.EmptyAction
import scala.tools.nsc.util.WorkScheduler
import scala.reflect.internal.util.SourceFile
import scala.tools.nsc.util.InterruptReq

/** Interface of interactive compiler to a client such as an IDE
 *  The model the presentation compiler consists of the following parts:
 *
 *  unitOfFile: The map from sourcefiles to loaded units. A sourcefile/unit is loaded if it occurs in that map.
 *
 *  manipulated by: removeUnitOf, reloadSources.
 *
 *  A call to reloadSources will add the given sources to the loaded units, and
 *  start a new background compiler pass to compile all loaded units (with the indicated sources first).
 *  Each background compiler pass has its own typer run.
 *  The background compiler thread can be interrupted each time an AST node is
 *  completely typechecked in the following ways:

 *  1. by a new call to reloadSources. This starts a new background compiler pass with a new typer run.
 *  2. by a call to askTypeTree. This starts a new typer run if the forceReload parameter = true
 *  3. by a call to askTypeAt, askTypeCompletion, askScopeCompletion, askToDoFirst, askLinkPos, askLastType.
 *  4. by raising an exception in the scheduler.
 *  5. by passing a high-priority action wrapped in ask { ... }.
 *
 *  Actions under 1-3 can themselves be interrupted if they involve typechecking
 *  AST nodes. High-priority actions under 5 cannot; they always run to completion.
 *  So these high-priority actions should to be short.
 *
 *  Normally, an interrupted action continues after the interrupting action is finished.
 *  However, if the interrupting action created a new typer run, the interrupted
 *  action is aborted. If there's an outstanding response, it will be set to
 *  a Right value with a FreshRunReq exception.
 */
trait CompilerControl { self: Global =>

  type Response[T] = scala.tools.nsc.interactive.Response[T]

  /** The scheduler by which client and compiler communicate
   *  Must be initialized before starting compilerRunner
   */
  @volatile protected[interactive] var scheduler = new WorkScheduler

  /** Return the compilation unit attached to a source file, or None
   *  if source is not loaded.
   */
  def getUnitOf(s: SourceFile): Option[RichCompilationUnit] = getUnit(s)

  /** Run operation `op` on a compilation unit associated with given `source`.
   *  If source has a loaded compilation unit, this one is passed to `op`.
   *  Otherwise a new compilation unit is created, but not added to the set of loaded units.
   */
  def onUnitOf[T](source: SourceFile)(op: RichCompilationUnit => T): T =
    op(unitOfFile.getOrElse(source.file, new RichCompilationUnit(source)))

  /** Removes the CompilationUnit corresponding to the given SourceFile
   *  from consideration for recompilation.
   */
  def removeUnitOf(s: SourceFile): Option[RichCompilationUnit] = { toBeRemoved += s.file; unitOfFile get s.file }

  /** Returns the top level classes and objects that were deleted
   * in the editor since last time recentlyDeleted() was called.
   */
  def recentlyDeleted(): List[Symbol] = deletedTopLevelSyms.synchronized {
    val result = deletedTopLevelSyms
    deletedTopLevelSyms.clear()
    result.toList
  }

  /** Locate smallest tree that encloses position
   *  @pre Position must be loaded
   */
  def locateTree(pos: Position): Tree = onUnitOf(pos.source) { unit => new Locator(pos) locateIn unit.body }

  /** Locates smallest context that encloses position as an optional value.
   */
  def locateContext(pos: Position): Option[Context] =
    for (unit <- getUnit(pos.source); cx <- locateContext(unit.contexts, pos)) yield cx

  /** Returns the smallest context that contains given `pos`, throws FatalError if none exists.
   */
  def doLocateContext(pos: Position): Context = locateContext(pos) getOrElse {
    throw new FatalError("no context found for "+pos)
  }

  private def postWorkItem(item: WorkItem) =
    if (item.onCompilerThread) item() else scheduler.postWorkItem(item)

  /** Makes sure a set of compilation units is loaded and parsed.
   *  Returns () to syncvar `response` on completion.
   *  Afterwards a new background compiler run is started with
   *  the given sources at the head of the list of to-be-compiled sources.
   */
  def askReload(sources: List[SourceFile], response: Response[Unit]) = {
    val superseded = scheduler.dequeueAll {
      case ri: ReloadItem if ri.sources == sources => Some(ri)
      case _ => None
    }
    superseded.foreach(_.response.set(()))
    postWorkItem(new ReloadItem(sources, response))
  }

  /** Removes source files and toplevel symbols, and issues a new typer run.
   *  Returns () to syncvar `response` on completion.
   */
  def askFilesDeleted(sources: List[SourceFile], response: Response[Unit]) = {
    postWorkItem(new FilesDeletedItem(sources, response))
  }

  /** Sets sync var `response` to the smallest fully attributed tree that encloses position `pos`.
   *  Note: Unlike for most other ask... operations, the source file belonging to `pos` needs not be loaded.
   */
  def askTypeAt(pos: Position, response: Response[Tree]) =
    postWorkItem(new AskTypeAtItem(pos, response))

  /** Sets sync var `response` to the fully attributed & typechecked tree contained in `source`.
   *  @pre `source` needs to be loaded.
   *  @note Deprecated because of race conditions in the typechecker when the background compiler
   *        is interrupted while typing the same `source`.
   *  @see  SI-6578
   */
  @deprecated("Use `askLoadedTyped` instead to avoid race conditions in the typechecker", "2.10.1")
  def askType(source: SourceFile, forceReload: Boolean, response: Response[Tree]) =
    postWorkItem(new AskTypeItem(source, forceReload, response))

  /** Sets sync var `response` to the position of the definition of the given link in
   *  the given sourcefile.
   *
   *  @param   sym      The symbol referenced by the link (might come from a classfile)
   *  @param   source   The source file that's supposed to contain the definition
   *  @param   response A response that will be set to the following:
   *                    If `source` contains a definition that is referenced by the given link
   *                    the position of that definition, otherwise NoPosition.
   *  Note: This operation does not automatically load `source`. If `source`
   *  is unloaded, it stays that way.
   */
  def askLinkPos(sym: Symbol, source: SourceFile, response: Response[Position]) =
    postWorkItem(new AskLinkPosItem(sym, source, response))

  /** Sets sync var `response` to doc comment information for a given symbol.
   *
   *  @param   sym        The symbol whose doc comment should be retrieved (might come from a classfile)
   *  @param   source     The source file that's supposed to contain the definition
   *  @param   site       The symbol where 'sym' is observed
   *  @param   fragments  All symbols that can contribute to the generated documentation
   *                      together with their source files.
   *  @param   response   A response that will be set to the following:
   *                      If `source` contains a definition of a given symbol that has a doc comment,
   *                      the (expanded, raw, position) triplet for a comment, otherwise ("", "", NoPosition).
   *  Note: This operation does not automatically load sources that are not yet loaded.
   */
  def askDocComment(sym: Symbol, source: SourceFile, site: Symbol, fragments: List[(Symbol,SourceFile)], response: Response[(String, String, Position)]): Unit =
    postWorkItem(new AskDocCommentItem(sym, source, site, fragments, response))

  @deprecated("Use method that accepts fragments", "2.10.2")
  def askDocComment(sym: Symbol, site: Symbol, source: SourceFile, response: Response[(String, String, Position)]): Unit =
    askDocComment(sym, source, site, (sym,source)::Nil, response)

  /** Sets sync var `response` to list of members that are visible
   *  as members of the tree enclosing `pos`, possibly reachable by an implicit.
   *  @pre  source is loaded
   */
  def askTypeCompletion(pos: Position, response: Response[List[Member]]) =
    postWorkItem(new AskTypeCompletionItem(pos, response))

  /** Sets sync var `response` to list of members that are visible
   *  as members of the scope enclosing `pos`.
   *  @pre  source is loaded
   */
  def askScopeCompletion(pos: Position, response: Response[List[Member]]) =
    postWorkItem(new AskScopeCompletionItem(pos, response))

  /** Asks to do unit corresponding to given source file on present and subsequent type checking passes.
   *  If the file is in the 'crashedFiles' ignore list it is removed and typechecked normally.
   */
  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.
   */
  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 =
    askLoadedTyped(source, false, response)

  /** If source if not yet loaded, get an outline view with askParseEntered.
   *  If source is loaded, wait for it to be typechecked.
   *  In both cases, set response to parsed (and possibly typechecked) tree.
   *  @param keepSrcLoaded If set to `true`, source file will be kept as a loaded unit afterwards.
   */
  def askStructure(keepSrcLoaded: Boolean)(source: SourceFile, response: Response[Tree]) = {
    getUnit(source) match {
      case Some(_) => askLoadedTyped(source, keepSrcLoaded, response)
      case None => askParsedEntered(source, keepSrcLoaded, response)
    }
  }

  /** Set sync var `response` to the parse tree of `source` with all top-level symbols entered.
   *  @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 askParsedEntered(source: SourceFile, keepLoaded: Boolean, response: Response[Tree]) =
    postWorkItem(new AskParsedEnteredItem(source, keepLoaded, response))


  /** Cancels current compiler run and start a fresh one where everything will be re-typechecked
   *  (but not re-loaded).
   */
  def askReset() = scheduler raise (new FreshRunReq)

  /** Tells the compile server to shutdown, and not to restart again */
  def askShutdown() = scheduler raise ShutdownReq

  /** Returns parse tree for source `source`. No symbols are entered. Syntax errors are reported.
   *
   *  This method is thread-safe and as such can safely run outside of the presentation
   *  compiler thread.
   */
  def parseTree(source: SourceFile): Tree = {
    newUnitParser(new CompilationUnit(source)).parse()
  }

  /** Asks for a computation to be done quickly on the presentation compiler thread */
  def ask[A](op: () => A): A = if (self.onCompilerThread) op() else scheduler doQuickly op

  /** Asks for a computation to be done on presentation compiler thread, returning
   *  a response with the result or an exception
   */
  def askForResponse[A](op: () => A): Response[A] = {
    val r = new Response[A]
    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
    }
  }

  def onCompilerThread = Thread.currentThread == compileRunner

  /** Info given for every member found by completion
   */
  abstract class Member {
    def prefix: Type
    val sym: Symbol
    val tpe: Type
    val accessible: Boolean
    def implicitlyAdded = false
    def symNameDropLocal: Name = sym.name.dropLocal

    private def accessible_s = if (accessible) "" else "[inaccessible] "
    def forceInfoString = {
      definitions.fullyInitializeSymbol(sym)
      definitions.fullyInitializeType(tpe)
      infoString
    }
    def infoString = s"$accessible_s${sym.defStringSeenAs(tpe)}"
  }

  case class TypeMember(
    sym: Symbol,
    tpe: Type,
    accessible: Boolean,
    inherited: Boolean,
    viaView: Symbol) extends Member {
    // should be a case class parameter, but added as a var instead to preserve compatibility with the IDE
    var prefix: Type = NoType
    override def implicitlyAdded = viaView != NoSymbol
  }

  case class ScopeMember(
    sym: Symbol,
    tpe: Type,
    accessible: Boolean,
    viaImport: Tree) extends Member {
    // should be a case class parameter, but added as a var instead to preserve compatibility with the IDE
    var prefix: Type = NoType
  }

  // items that get sent to scheduler

  abstract class WorkItem extends (() => Unit) {
    val onCompilerThread = self.onCompilerThread

    /** Raise a MissingResponse, if the work item carries a response. */
    def raiseMissing(): Unit
  }

  case class ReloadItem(sources: List[SourceFile], response: Response[Unit]) extends WorkItem {
    def apply() = reload(sources, response)
    override def toString = "reload "+sources

    def raiseMissing() =
      response raise new MissingResponse
  }

  case class FilesDeletedItem(sources: List[SourceFile], response: Response[Unit]) extends WorkItem {
    def apply() = filesDeleted(sources, response)
    override def toString = "files deleted "+sources

    def raiseMissing() =
      response raise new MissingResponse
  }

  case class AskTypeAtItem(pos: Position, response: Response[Tree]) extends WorkItem {
    def apply() = self.getTypedTreeAt(pos, response)
    override def toString = "typeat "+pos.source+" "+pos.show

    def raiseMissing() =
      response raise new MissingResponse
  }

  case class AskTypeItem(source: SourceFile, forceReload: Boolean, response: Response[Tree]) extends WorkItem {
    def apply() = self.getTypedTree(source, forceReload, response)
    override def toString = "typecheck"

    def raiseMissing() =
      response raise new MissingResponse
  }

  case class AskTypeCompletionItem(pos: Position, response: Response[List[Member]]) extends WorkItem {
    def apply() = self.getTypeCompletion(pos, response)
    override def toString = "type completion "+pos.source+" "+pos.show

    def raiseMissing() =
      response raise new MissingResponse
  }

  case class AskScopeCompletionItem(pos: Position, response: Response[List[Member]]) extends WorkItem {
    def apply() = self.getScopeCompletion(pos, response)
    override def toString = "scope completion "+pos.source+" "+pos.show

    def raiseMissing() =
      response raise new MissingResponse
  }

  class AskToDoFirstItem(val source: SourceFile) extends WorkItem {
    def apply() = {
      moveToFront(List(source))
      enableIgnoredFile(source.file)
    }
    override def toString = "dofirst "+source

    def raiseMissing() = ()
  }

  case class AskLinkPosItem(sym: Symbol, source: SourceFile, response: Response[Position]) extends WorkItem {
    def apply() = self.getLinkPos(sym, source, response)
    override def toString = "linkpos "+sym+" in "+source

    def raiseMissing() =
      response raise new MissingResponse
  }

  case class AskDocCommentItem(sym: Symbol, source: SourceFile, site: Symbol, fragments: List[(Symbol,SourceFile)], response: Response[(String, String, Position)]) extends WorkItem {
    def apply() = self.getDocComment(sym, source, site, fragments, response)
    override def toString = "doc comment "+sym+" in "+source+" with fragments:"+fragments.mkString("(", ",", ")")

    def raiseMissing() =
      response raise new MissingResponse
  }

  case class AskLoadedTypedItem(source: SourceFile, keepLoaded: Boolean, response: Response[Tree]) extends WorkItem {
    def apply() = self.waitLoadedTyped(source, response, keepLoaded, this.onCompilerThread)
    override def toString = "wait loaded & typed "+source

    def raiseMissing() =
      response raise new MissingResponse
  }

  case class AskParsedEnteredItem(source: SourceFile, keepLoaded: Boolean, response: Response[Tree]) extends WorkItem {
    def apply() = self.getParsedEntered(source, keepLoaded, response, this.onCompilerThread)
    override def toString = "getParsedEntered "+source+", keepLoaded = "+keepLoaded

    def raiseMissing() =
      response raise new MissingResponse
  }

  /** A do-nothing work scheduler that responds immediately with MissingResponse.
   *
   *  Used during compiler shutdown.
   */
  class NoWorkScheduler extends WorkScheduler {

    override def postWorkItem(action: Action) = synchronized {
      action match {
        case w: WorkItem => w.raiseMissing()
        case e: EmptyAction => // do nothing
        case _ => println("don't know what to do with this " + action.getClass)
      }
    }

    override def doQuickly[A](op: () => A): A = {
      throw new FailedInterrupt(new Exception("Posted a work item to a compiler that's shutting down"))
    }

    override def askDoQuickly[A](op: () => A): InterruptReq { type R = A } = {
      val ir = new InterruptReq {
        type R = A
        val todo = () => throw new MissingResponse
      }
      ir.execute()
      ir
    }

  }

}

  // ---------------- Interpreted exceptions -------------------

/** Signals a request for a fresh background compiler run.
 *  Note: The object has to stay top-level so that the PresentationCompilerThread may access it.
 */
class FreshRunReq extends ControlThrowable

/** Signals a request for a shutdown of the presentation compiler.
 *  Note: The object has to stay top-level so that the PresentationCompilerThread may access it.
 */
object ShutdownReq extends ControlThrowable

class NoSuchUnitError(file: AbstractFile) extends Exception("no unit found for file "+file)

class MissingResponse extends Exception("response missing")