All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dotty.tools.dotc.Run.scala Maven / Gradle / Ivy

There is a newer version: 3.6.4-RC1-bin-20241220-0bfa1af-NIGHTLY
Show newest version
package dotty.tools
package dotc

import core.*
import Contexts.*
import Periods.*
import Symbols.*
import Scopes.*
import Names.Name
import Denotations.Denotation
import typer.Typer
import typer.ImportInfo.withRootImports
import Decorators.*
import io.AbstractFile
import Phases.{unfusedPhases, Phase}

import sbt.interfaces.ProgressCallback

import util.*
import reporting.{Suppression, Action, Profile, ActiveProfile, NoProfile}
import reporting.Diagnostic
import reporting.Diagnostic.Warning
import rewrites.Rewrites
import profile.Profiler
import printing.XprintMode
import typer.ImplicitRunInfo
import config.Feature
import StdNames.nme

import java.io.{BufferedWriter, OutputStreamWriter}
import java.nio.charset.StandardCharsets

import scala.collection.mutable
import scala.util.control.NonFatal
import scala.io.Codec

import Run.Progress
import scala.compiletime.uninitialized
import dotty.tools.dotc.transform.MegaPhase
import dotty.tools.dotc.transform.Pickler.AsyncTastyHolder

/** A compiler run. Exports various methods to compile source files */
class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with ConstraintRunInfo {

  /** Default timeout to stop looking for further implicit suggestions, in ms.
   *  This is usually for the first import suggestion; subsequent suggestions
   *  may get smaller timeouts. @see ImportSuggestions.reduceTimeBudget
   */
  private var myImportSuggestionBudget: Int =
    Int.MinValue // sentinel value; means whatever is set in command line option

  def importSuggestionBudget =
    if myImportSuggestionBudget == Int.MinValue then ictx.settings.XimportSuggestionTimeout.value
    else myImportSuggestionBudget

  def importSuggestionBudget_=(x: Int) =
    myImportSuggestionBudget = x

  /** If this variable is set to `true`, some core typer operations will
   *  return immediately. Currently these early abort operations are
   *  `Typer.typed` and `Implicits.typedImplicit`.
   */
  @volatile var isCancelled = false

  private var compiling = false

  private var myUnits: List[CompilationUnit] = Nil
  private var myUnitsCached: List[CompilationUnit] = Nil
  private var myFiles: Set[AbstractFile] = uninitialized

  // `@nowarn` annotations by source file, populated during typer
  private val mySuppressions: mutable.LinkedHashMap[SourceFile, mutable.ListBuffer[Suppression]] = mutable.LinkedHashMap.empty
  // source files whose `@nowarn` annotations are processed
  private val mySuppressionsComplete: mutable.Set[SourceFile] = mutable.Set.empty
  // warnings issued before a source file's `@nowarn` annotations are processed, suspended so that `@nowarn` can filter them
  private val mySuspendedMessages: mutable.LinkedHashMap[SourceFile, mutable.LinkedHashSet[Warning]] = mutable.LinkedHashMap.empty

  object suppressions:
    // When the REPL creates a new run (ReplDriver.compile), parsing is already done in the old context, with the
    // previous Run. Parser warnings were suspended in the old run and need to be copied over so they are not lost.
    // Same as scala/scala/commit/79ca1408c7.
    def initSuspendedMessages(oldRun: Run | Null) = if oldRun != null then
      mySuspendedMessages.clear()
      mySuspendedMessages ++= oldRun.mySuspendedMessages

    def suppressionsComplete(source: SourceFile) = source == NoSource || mySuppressionsComplete(source)

    def addSuspendedMessage(warning: Warning) =
      mySuspendedMessages.getOrElseUpdate(warning.pos.source, mutable.LinkedHashSet.empty) += warning

    def nowarnAction(dia: Diagnostic): Action.Warning.type | Action.Verbose.type | Action.Silent.type =
      mySuppressions.getOrElse(dia.pos.source, Nil).find(_.matches(dia)) match {
        case Some(s) =>
          s.markUsed()
          if (s.verbose) Action.Verbose
          else Action.Silent
        case _ =>
          Action.Warning
      }

    def addSuppression(sup: Suppression): Unit =
      val source = sup.annotPos.source
      mySuppressions.getOrElseUpdate(source, mutable.ListBuffer.empty) += sup

    def reportSuspendedMessages(source: SourceFile)(using Context): Unit = {
      // sort suppressions. they are not added in any particular order because of lazy type completion
      for (sups <- mySuppressions.get(source))
        mySuppressions(source) = sups.sortBy(sup => 0 - sup.start)
      mySuppressionsComplete += source
      mySuspendedMessages.remove(source).foreach(_.foreach(ctx.reporter.issueIfNotSuppressed))
    }

    def runFinished(hasErrors: Boolean): Unit =
      // report suspended messages (in case the run finished before typer)
      mySuspendedMessages.keysIterator.toList.foreach(reportSuspendedMessages)
      // report unused nowarns only if all all phases are done
      if !hasErrors && ctx.settings.WunusedHas.nowarn then
        for {
          source <- mySuppressions.keysIterator.toList
          sups   <- mySuppressions.remove(source)
          sup    <- sups.reverse
        } if (!sup.used)
          report.warning("@nowarn annotation does not suppress any warnings", sup.annotPos)

  /** The compilation units currently being compiled, this may return different
   *  results over time.
   */
  def units: List[CompilationUnit] = myUnits

  private def units_=(us: List[CompilationUnit]): Unit =
    myUnits = us

  var suspendedUnits: mutable.ListBuffer[CompilationUnit] = mutable.ListBuffer()
  var suspendedHints: mutable.Map[CompilationUnit, (String, Boolean)] = mutable.HashMap()

  /** Were any units suspended in the typer phase? if so then pipeline tasty can not complete. */
  var suspendedAtTyperPhase: Boolean = false

  def checkSuspendedUnits(newUnits: List[CompilationUnit])(using Context): Unit =
    if newUnits.isEmpty && suspendedUnits.nonEmpty && !ctx.reporter.errorsReported then
      val where =
        if suspendedUnits.size == 1 then i"in ${suspendedUnits.head}."
        else i"""among
                |
                |  ${suspendedUnits.toList}%, %
                |"""
      val enableXprintSuspensionHint =
        if ctx.settings.XprintSuspension.value then ""
        else "\n\nCompile with -Xprint-suspension for information."
      report.error(em"""Cyclic macro dependencies $where
                    |Compilation stopped since no further progress can be made.
                    |
                    |To fix this, place macros in one set of files and their callers in another.$enableXprintSuspensionHint""")

  /** The files currently being compiled (active or suspended).
   *  This may return different results over time.
   *  These files do not have to be source files since it's possible to compile
   *  from TASTY.
   */
  def files: Set[AbstractFile] = {
    if (myUnits ne myUnitsCached) {
      myUnitsCached = myUnits
      myFiles = (myUnits ++ suspendedUnits).map(_.source.file).toSet
    }
    myFiles
  }

  /** The source files of all late entered symbols, as a set */
  private val lateFiles = mutable.Set[AbstractFile]()

  /** A cache for static references to packages and classes */
  val staticRefs = util.EqHashMap[Name, Denotation](initialCapacity = 1024)

  /** Actions that need to be performed at the end of the current compilation run */
  private var finalizeActions = mutable.ListBuffer[() => Unit]()

  private var _progress: Progress | Null = null // Set if progress reporting is enabled

  private inline def trackProgress(using Context)(inline op: Context ?=> Progress => Unit): Unit =
    foldProgress(())(op)

  private inline def foldProgress[T](using Context)(inline default: T)(inline op: Context ?=> Progress => T): T =
    val local = _progress
    if local != null then
      op(using ctx)(local)
    else
      default

  def didEnterUnit(unit: CompilationUnit)(using Context): Boolean =
    foldProgress(true /* should progress by default */)(_.tryEnterUnit(unit))

  def canProgress()(using Context): Boolean =
    foldProgress(true /* not cancelled by default */)(p => !p.checkCancellation())

  def doAdvanceUnit()(using Context): Unit =
    trackProgress: progress =>
      progress.currentUnitCount += 1 // trace that we completed a unit in the current (sub)phase
      progress.refreshProgress()

  def doAdvanceLate()(using Context): Unit =
    trackProgress: progress =>
      progress.currentLateUnitCount += 1 // trace that we completed a late compilation
      progress.refreshProgress()

  private def doEnterPhase(currentPhase: Phase)(using Context): Unit =
    trackProgress: progress =>
      progress.enterPhase(currentPhase)

  /** interrupt the thread and set cancellation state */
  private def cancelInterrupted(): Unit =
    try
      trackProgress(_.cancel())
    finally
      Thread.currentThread().nn.interrupt()

  private def doAdvancePhase(currentPhase: Phase, wasRan: Boolean)(using Context): Unit =
    trackProgress: progress =>
      progress.currentUnitCount = 0 // reset unit count in current (sub)phase
      progress.currentCompletedSubtraversalCount = 0 // reset subphase index to initial
      progress.seenPhaseCount += 1 // trace that we've seen a (sub)phase
      if wasRan then
        // add an extra traversal now that we completed a (sub)phase
        progress.completedTraversalCount += 1
      else
        // no subphases were ran, remove traversals from expected total
        progress.totalTraversals -= currentPhase.traversals

  private def tryAdvanceSubPhase()(using Context): Unit =
    trackProgress: progress =>
      if progress.canAdvanceSubPhase then
        progress.currentUnitCount = 0 // reset unit count in current (sub)phase
        progress.seenPhaseCount += 1 // trace that we've seen a (sub)phase
        progress.completedTraversalCount += 1 // add an extra traversal now that we completed a (sub)phase
        progress.currentCompletedSubtraversalCount += 1 // record that we've seen a subphase
        if !progress.isCancelled() then
          progress.tickSubphase()

  /** if true, then we are done writing pipelined TASTy files (i.e. finished in a previous run.) */
  private var myAsyncTastyWritten = false

  private var _asyncTasty: Option[AsyncTastyHolder] = None

  /** populated when this run needs to write pipeline TASTy files. */
  def asyncTasty: Option[AsyncTastyHolder] = _asyncTasty

  private def initializeAsyncTasty()(using Context): () => Unit =
    // should we provide a custom ExecutionContext?
    // currently it is just used to call the `apiPhaseCompleted` and `dependencyPhaseCompleted` callbacks in Zinc
    import scala.concurrent.ExecutionContext.Implicits.global
    val async = AsyncTastyHolder.init
    _asyncTasty = Some(async)
    () => async.cancel()

  /** Will be set to true if any of the compiled compilation units contains
   *  a pureFunctions language import.
   */
  var pureFunsImportEncountered = false

  /** Will be set to true if experimental.captureChecking is enabled
   *  or any of the compiled compilation units contains a captureChecking language import.
   */
  var ccEnabledSomewhere = Feature.enabledBySetting(Feature.captureChecking)(using ictx)

  private var myEnrichedErrorMessage = false

  def compile(files: List[AbstractFile]): Unit =
    try compileSources(files.map(runContext.getSource(_)))
    catch case NonFatal(ex) if !this.enrichedErrorMessage =>
      val files1 = if units.isEmpty then files else units.map(_.source.file)
      report.echo(this.enrichErrorMessage(s"exception occurred while compiling ${files1.map(_.path)}"))
      throw ex

  /** TODO: There's a fundamental design problem here: We assemble phases using `fusePhases`
   *  when we first build the compiler. But we modify them with -Yskip, -Ystop
   *  on each run. That modification needs to either transform the tree structure,
   *  or we need to assemble phases on each run, and take -Yskip, -Ystop into
   *  account. I think the latter would be preferable.
   */
  def compileSources(sources: List[SourceFile]): Unit =
    if (sources forall (_.exists)) {
      units = sources.map(CompilationUnit(_))
      compileUnits()
    }


  def compileUnits(us: List[CompilationUnit]): Unit = {
    units = us
    compileUnits()
  }

  def compileUnits(us: List[CompilationUnit], ctx: Context): Unit = {
    units = us
    compileUnits()(using ctx)
  }

  var profile: Profile = NoProfile

  private def compileUnits()(using Context) = Stats.maybeMonitored {
    if (!ctx.mode.is(Mode.Interactive)) // IDEs might have multi-threaded access, accesses are synchronized
      ctx.base.checkSingleThreaded()

    compiling = true

    profile =
      if ctx.settings.Vprofile.value
        || !ctx.settings.VprofileSortedBy.value.isEmpty
        || ctx.settings.VprofileDetails.value != 0
      then ActiveProfile(ctx.settings.VprofileDetails.value.max(0).min(1000))
      else NoProfile

    // If testing pickler, make sure to stop after pickling phase:
    val stopAfter =
      if (ctx.settings.YtestPickler.value) List("pickler")
      else ctx.settings.YstopAfter.value

    val runCtx = ctx.fresh
    runCtx.setProfiler(Profiler())

    val pluginPlan = ctx.base.addPluginPhases(ctx.base.phasePlan)
    val phases = ctx.base.fusePhases(pluginPlan,
      ctx.settings.Yskip.value, ctx.settings.YstopBefore.value, stopAfter, ctx.settings.Ycheck.value)
    ctx.base.usePhases(phases, runCtx)

    if ctx.settings.YnoDoubleBindings.value then
      ctx.base.checkNoDoubleBindings = true

    def runPhases(allPhases: Array[Phase])(using Context) = {
      var lastPrintedTree: PrintedTree = NoPrintedTree
      val profiler = ctx.profiler
      var phasesWereAdjusted = false

      var forceReachPhaseMaybe =
        if (ctx.isBestEffort && phases.exists(_.phaseName == "typer")) Some("typer")
        else None

      for phase <- allPhases do
        doEnterPhase(phase)
        val phaseWillRun = phase.isRunnable || forceReachPhaseMaybe.nonEmpty
        if phaseWillRun then
          Stats.trackTime(s"phase time ms/$phase") {
            val start = System.currentTimeMillis
            profiler.onPhase(phase):
              try units = phase.runOn(units)
              catch case _: InterruptedException => cancelInterrupted()
            if (ctx.settings.Xprint.value.containsPhase(phase))
              for (unit <- units)
                def printCtx(unit: CompilationUnit) = phase.printingContext(
                  ctx.fresh.setPhase(phase.next).setCompilationUnit(unit))
                lastPrintedTree = printTree(lastPrintedTree)(using printCtx(unit))

            if forceReachPhaseMaybe.contains(phase.phaseName) then
              forceReachPhaseMaybe = None

            report.informTime(s"$phase ", start)
            Stats.record(s"total trees at end of $phase", ast.Trees.ntrees)
            for (unit <- units)
              Stats.record(s"retained typed trees at end of $phase", unit.tpdTree.treeSize)
            ctx.typerState.gc()
          }
          if !phasesWereAdjusted then
            phasesWereAdjusted = true
            if !Feature.ccEnabledSomewhere then
              ctx.base.unlinkPhaseAsDenotTransformer(Phases.checkCapturesPhase.prev)
              ctx.base.unlinkPhaseAsDenotTransformer(Phases.checkCapturesPhase)
            end if
          end if
        end if
        doAdvancePhase(phase, wasRan = phaseWillRun)
      end for
      profiler.finished()
    }

    val fusedPhases = runCtx.base.allPhases
    if ctx.settings.explainCyclic.value then
      runCtx.setProperty(CyclicReference.Trace, new CyclicReference.Trace())
    runCtx.withProgressCallback: cb =>
      _progress = Progress(cb, this, fusedPhases.map(_.traversals).sum)
    val cancelAsyncTasty: () => Unit =
      if !myAsyncTastyWritten && Phases.picklerPhase.exists && !ctx.settings.XearlyTastyOutput.isDefault then
        initializeAsyncTasty()
      else () => {}

    runPhases(allPhases = fusedPhases)(using runCtx)
    cancelAsyncTasty()

    ctx.reporter.finalizeReporting()
    if (!ctx.reporter.hasErrors)
      Rewrites.writeBack()
    suppressions.runFinished(hasErrors = ctx.reporter.hasErrors)
    while (finalizeActions.nonEmpty && canProgress()) {
      val action = finalizeActions.remove(0)
      action()
    }
    compiling = false
  }

  private var myCompilingSuspended: Boolean = false

  /** Is this run started via a compilingSuspended? */
  def isCompilingSuspended: Boolean = myCompilingSuspended

  /** Compile units `us` which were suspended in a previous run,
   *  also signal if all necessary async tasty files were written in a previous run.
   */
  def compileSuspendedUnits(us: List[CompilationUnit], asyncTastyWritten: Boolean): Unit =
    myCompilingSuspended = true
    myAsyncTastyWritten = asyncTastyWritten
    for unit <- us do unit.suspended = false
    compileUnits(us)

  /** Enter top-level definitions of classes and objects contained in source file `file`.
   *  The newly added symbols replace any previously entered symbols.
   *  If `typeCheck = true`, also run typer on the compilation unit, and set
   *  `rootTreeOrProvider`.
   */
  def lateCompile(file: AbstractFile, typeCheck: Boolean)(using Context): Unit =
    if (!files.contains(file) && !lateFiles.contains(file)) {
      lateFiles += file

      val unit = CompilationUnit(ctx.getSource(file))
      val unitCtx = runContext.fresh
        .setCompilationUnit(unit)
        .withRootImports

      def process()(using Context) =
        ctx.typer.lateEnterUnit(typeCheck)(doTypeCheck =>
          if compiling then finalizeActions += doTypeCheck
          else doTypeCheck()
        )

      process()(using unitCtx)
    }

  private sealed trait PrintedTree
  private /*final*/ case class SomePrintedTree(phase: String, tree: String) extends PrintedTree
  private object NoPrintedTree extends PrintedTree

  private def printTree(last: PrintedTree)(using Context): PrintedTree = {
    val unit = ctx.compilationUnit
    val fusedPhase = ctx.phase.prev.megaPhase
    val echoHeader = f"[[syntax trees at end of $fusedPhase%25s]] // ${unit.source}"
    val tree = if ctx.isAfterTyper then unit.tpdTree else unit.untpdTree
    val treeString = fusedPhase.show(tree)

    last match {
      case SomePrintedTree(phase, lastTreeString) if lastTreeString == treeString =>
        report.echo(s"$echoHeader: unchanged since $phase")
        last

      case SomePrintedTree(phase, lastTreeString) if ctx.settings.XprintDiff.value || ctx.settings.XprintDiffDel.value =>
        val diff = DiffUtil.mkColoredCodeDiff(treeString, lastTreeString, ctx.settings.XprintDiffDel.value)
        report.echo(s"$echoHeader\n$diff\n")
        SomePrintedTree(fusedPhase.phaseName, treeString)

      case _ =>
        report.echo(s"$echoHeader\n$treeString\n")
        SomePrintedTree(fusedPhase.phaseName, treeString)
    }
  }

  def compileFromStrings(scalaSources: List[String], javaSources: List[String] = Nil): Unit = {
    def sourceFile(source: String, isJava: Boolean): SourceFile = {
      val uuid = java.util.UUID.randomUUID().toString
      val ext = if (isJava) "java" else "scala"
      val name = s"compileFromString-$uuid.$ext"
      SourceFile.virtual(name, source)
    }
    val sources =
      scalaSources.map(sourceFile(_, isJava = false)) ++
       javaSources.map(sourceFile(_, isJava = true))

    compileSources(sources)
  }

  /** Print summary of warnings and errors encountered */
  def printSummary(): Unit = {
    printMaxConstraint()
    val r = runContext.reporter
    if !r.errorsReported then
      profile.printSummary()
    r.summarizeUnreportedWarnings()
    r.printSummary()
  }

  override def reset(): Unit = {
    super[ImplicitRunInfo].reset()
    super[ConstraintRunInfo].reset()
    myCtx = null
    myUnits = Nil
    myUnitsCached = Nil
  }

  /** Produces the following contexts, from outermost to innermost
   *
   *    bootStrap:   A context with next available runId and a scope consisting of
   *                 the RootPackage _root_
   *    start        A context with RootClass as owner and the necessary initializations
   *                 for type checking.
   *    imports      For each element of RootImports, an import context
   */
  protected def rootContext(using Context): Context = {
    ctx.initialize()
    ctx.base.setPhasePlan(comp.phases)
    val rootScope = new MutableScope(0)
    val bootstrap = ctx.fresh
      .setPeriod(Period(comp.nextRunId, FirstPhaseId))
      .setScope(rootScope)
    rootScope.enter(ctx.definitions.RootPackage)(using bootstrap)
    var start = bootstrap.fresh
      .setOwner(defn.RootClass)
      .setTyper(new Typer)
      .addMode(Mode.ImplicitsEnabled)
      .setTyperState(ctx.typerState.fresh(ctx.reporter))
    if ctx.settings.YexplicitNulls.value && !Feature.enabledBySetting(nme.unsafeNulls) then
      start = start.addMode(Mode.SafeNulls)
    ctx.initialize()(using start) // re-initialize the base context with start

    // `this` must be unchecked for safe initialization because by being passed to setRun during
    // initialization, it is not yet considered fully initialized by the initialization checker
    start.setRun(this: @unchecked)
  }

  private var myCtx: Context | Null = rootContext(using ictx)

  /** The context created for this run */
  given runContext[Dummy_so_its_a_def]: Context = myCtx.nn
  assert(runContext.runId <= Periods.MaxPossibleRunId)
}

object Run {

  case class SubPhase(val name: String):
    override def toString: String = name

  class SubPhases(val phase: Phase):
    require(phase.exists)

    private def baseName: String = phase match
      case phase: MegaPhase => phase.shortPhaseName
      case phase => phase.phaseName

    val all = IArray.from(phase.subPhases.map(sub => s"$baseName[$sub]"))

    def next(using Context): Option[SubPhases] =
      val next0 = phase.megaPhase.next.megaPhase
      if next0.exists then Some(SubPhases(next0))
      else None

    def size: Int = all.size

    def subPhase(index: Int) =
      if index < all.size then all(index)
      else baseName


  private class Progress(cb: ProgressCallback, private val run: Run, val initialTraversals: Int):
    export cb.{cancel, isCancelled}

    var totalTraversals: Int = initialTraversals  // track how many phases we expect to run
    var currentUnitCount: Int = 0 // current unit count in the current (sub)phase
    var currentLateUnitCount: Int = 0 // current late unit count
    var completedTraversalCount: Int = 0 // completed traversals over all files
    var currentCompletedSubtraversalCount: Int = 0 // completed subphases in the current phase
    var seenPhaseCount: Int = 0 // how many phases we've seen so far

    private var currPhase: Phase = uninitialized      // initialized by enterPhase
    private var subPhases: SubPhases = uninitialized  // initialized by enterPhase
    private var currPhaseName: String = uninitialized // initialized by enterPhase
    private var nextPhaseName: String = uninitialized // initialized by enterPhase

    /** Enter into a new real phase, setting the current and next (sub)phases */
    def enterPhase(newPhase: Phase)(using Context): Unit =
      if newPhase ne currPhase then
        currPhase = newPhase
        subPhases = SubPhases(newPhase)
        tickSubphase()

    def canAdvanceSubPhase: Boolean =
      currentCompletedSubtraversalCount + 1 < subPhases.size

    /** Compute the current (sub)phase name and next (sub)phase name */
    def tickSubphase()(using Context): Unit =
      val index = currentCompletedSubtraversalCount
      val s = subPhases
      currPhaseName = s.subPhase(index)
      nextPhaseName =
        if index + 1 < s.all.size then s.subPhase(index + 1)
        else s.next match
          case None => ""
          case Some(next0) => next0.subPhase(0)
      if seenPhaseCount > 0 then
        refreshProgress()


    /** Counts the number of completed full traversals over files, plus the number of units in the current phase */
    private def currentProgress(): Int =
      completedTraversalCount * work() + currentUnitCount + currentLateUnitCount

    /**Total progress is computed as the sum of
     * - the number of traversals we expect to make over all files
     * - the number of late compilations
     */
    private def totalProgress(): Int =
      totalTraversals * work() + run.lateFiles.size

    private def work(): Int = run.files.size

    private def requireInitialized(): Unit =
      require((currPhase: Phase | Null) != null, "enterPhase was not called")

    def checkCancellation(): Boolean =
      if Thread.interrupted() then cancel()
      isCancelled()

    /** trace that we are beginning a unit in the current (sub)phase, unless cancelled */
    def tryEnterUnit(unit: CompilationUnit): Boolean =
      if checkCancellation() then false
      else
        requireInitialized()
        cb.informUnitStarting(currPhaseName, unit)
        true

    /** trace the current progress out of the total, in the current (sub)phase, reporting the next (sub)phase */
    def refreshProgress()(using Context): Unit =
      requireInitialized()
      val total = totalProgress()
      if total > 0 && !cb.progress(currentProgress(), total, currPhaseName, nextPhaseName) then
        cancel()

  extension (run: Run | Null)

    /** record that the current phase has begun for the compilation unit of the current Context */
    def enterUnit(unit: CompilationUnit)(using Context): Boolean =
      if run != null then run.didEnterUnit(unit)
      else true // don't check cancellation if we're not tracking progress

    /** check progress cancellation, true if not cancelled */
    def enterRegion()(using Context): Boolean =
      if run != null then run.canProgress()
      else true // don't check cancellation if we're not tracking progress

    /** advance the unit count and record progress in the current phase */
    def advanceUnit()(using Context): Unit =
      if run != null then run.doAdvanceUnit()

    /** if there exists another subphase, switch to it and record progress */
    def enterNextSubphase()(using Context): Unit =
      if run != null then run.tryAdvanceSubPhase()

    /** advance the late count and record progress in the current phase */
    def advanceLate()(using Context): Unit =
      if run != null then run.doAdvanceLate()

    def enrichedErrorMessage: Boolean = if run == null then false else run.myEnrichedErrorMessage
    def enrichErrorMessage(errorMessage: String)(using Context): String =
      if run == null then
        report.enrichErrorMessage(errorMessage)
      else if !run.enrichedErrorMessage then
        run.myEnrichedErrorMessage = true
        report.enrichErrorMessage(errorMessage)
      else
        errorMessage
    def doNotEnrichErrorMessage: Unit =
      if run != null then run.myEnrichedErrorMessage = true
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy