<commit_before>package dotty.tools.dotc
package transform
package init

import core.*
import Contexts.*
import Symbols.*
import Types.*
import StdNames.*
import NameKinds.OuterSelectName

import ast.tpd.*
import config.Printers.init as printer
import reporting.trace as log

import Errors.*

import scala.collection.mutable
import scala.annotation.tailrec

object Semantic:

// ----- Domain definitions --------------------------------

  /** Abstract values
   *
   *  Value = Hot | Cold | Warm | ThisRef | Fun | RefSet
   *
   *                 Cold
   *        ┌──────►  ▲  ◄────┐  ◄────┐
   *        │         │       │       │
   *        │         │       │       │
   *        |         │       │       │
   *        |         │       │       │
   *     ThisRef     Warm   Fun    RefSet
   *        │         ▲       ▲       ▲
   *        │         │       │       │
   *        |         │       │       │
   *        ▲         │       │       │
   *        │         │       │       │
   *        └─────────┴───────┴───────┘
   *                  Hot
   *
   *   The diagram above does not reflect relationship between `RefSet`
   *   and other values. `RefSet` represents a set of values which could
   *   be `ThisRef`, `Warm` or `Fun`. The following ordering applies for
   *   RefSet:
   *
   *         R_a ⊑ R_b if R_a ⊆ R_b
   *
   *         V ⊑ R if V ∈ R
   *
   */
  sealed abstract class Value:
    def show(using Context): String = this match
      case ThisRef(klass) =>
        "ThisRef[" + klass.show + "]"
      case Warm(klass, outer, ctor, args) =>
        val argsText = if args.nonEmpty then ", args = " + args.map(_.show).mkString("(", ", ", ")") else ""
        "Warm[" + klass.show + "] { outer = " + outer.show + argsText + " }"
      case Fun(expr, thisV, klass) =>
        "Fun { this = " + thisV.show + ", owner = " + klass.show + " }"
      case RefSet(values) =>
        values.map(_.show).mkString("Set { ", ", ", " }")
      case _ =>
        this.toString()

    def isHot = this == Hot
    def isCold = this == Cold
    def isWarm = this.isInstanceOf[Warm]
    def isThisRef = this.isInstanceOf[ThisRef]

  /** A transitively initialized object */
  case object Hot extends Value

  /** An object with unknown initialization status */
  case object Cold extends Value

  sealed abstract class Ref extends Value:
    def klass: ClassSymbol
    def outer: Value

  /** A reference to the object under initialization pointed by `this` */
  case class ThisRef(klass: ClassSymbol) extends Ref:
    val outer = Hot

  /** An object with all fields initialized but reaches objects under initialization
   *
   *  We need to restrict nesting levels of `outer` to finitize the domain.
   */
  case class Warm(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref:

    /** If a warm value is in the process of populating parameters, class bodies are not executed. */
    private var populatingParams: Boolean = false

    def isPopulatingParams = populatingParams

    /** Ensure that outers and class parameters are initialized.
     *
     *  Fields in class body are not initialized.
     *
     *  We need to populate class parameters and outers for warm values for the
     *  following cases:
     *
     *  - Widen an already checked warm value to another warm value without
     *    corresponding object
     *
     *  - Using a warm value from the cache, whose corresponding object from
     *    the last iteration have been remove due to heap reversion
     *    {@see Cache.prepareForNextIteration}
     *
     *  After populating class parameters and outers, it is possible to lazily
     *  compute the field values in class bodies when they are accessed.
     */
    private def populateParams(): Contextual[this.type] = log("populating parameters", printer, (_: Warm).objekt.toString) {
      assert(!populatingParams, "the object is already populating parameters")
      populatingParams = true
      val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]
      extendTrace(klass.defTree) { this.callConstructor(ctor, args.map(arg => ArgInfo(arg, trace))) }
      populatingParams = false
      this
    }

    def ensureObjectExistsAndPopulated(): Contextual[this.type] =
      if cache.containsObject(this) then this
      else this.ensureFresh().populateParams()

  end Warm

  /** A function value */
  case class Fun(expr: Tree, thisV: Ref, klass: ClassSymbol) extends Value

  /** A value which represents a set of addresses
   *
   * It comes from `if` expressions.
   */
  case class RefSet(refs: List[Fun | Ref]) extends Value

  // end of value definition

  /** The abstract object which stores value about its fields and immediate outers.
   *
   *  Semantically it suffices to store the outer for `klass`. We cache other outers
   *  for performance reasons.
   *
   *  Note: Object is NOT a value.
   */
  case class Objekt(val klass: ClassSymbol, val fields: Map[Symbol, Value], val outers: Map[ClassSymbol, Value]):
    def field(f: Symbol): Value = fields(f)

    def outer(klass: ClassSymbol) = outers(klass)

    def hasOuter(klass: ClassSymbol) = outers.contains(klass)

    def hasField(f: Symbol) = fields.contains(f)

  object Promoted:
    class PromotionInfo:
      var isCurrentObjectPromoted: Boolean = false
      val values = mutable.Set.empty[Value]
      override def toString(): String = values.toString()

    /** Values that have been safely promoted */
    opaque type Promoted = PromotionInfo

    /** Note: don't use `val` to avoid incorrect sharing */
    def empty: Promoted = new PromotionInfo

    extension (promoted: Promoted)
      def isCurrentObjectPromoted: Boolean = promoted.isCurrentObjectPromoted
      def promoteCurrent(thisRef: ThisRef): Unit = promoted.isCurrentObjectPromoted = true
      def contains(value: Value): Boolean = promoted.values.contains(value)
      def add(value: Value): Unit = promoted.values += value
      def remove(value: Value): Unit = promoted.values -= value
    end extension
  end Promoted
  type Promoted = Promoted.Promoted

  import Promoted.*
  inline def promoted(using p: Promoted): Promoted = p

  /** Cache used in fixed point computation
   *
   *  The analysis computes the least fixed point for the cache (see doc for
   *   `ExprValueCache`).
   *
   *  For the fixed point computation to terminate, we need to make sure that
   *  the domain of the cache, i.e. the key pair (Ref, Tree) is finite. As the
   *  code is finite, we only need to carefully design the abstract domain to
   *  be finitary.
   *
   *  We also need to make sure that the computing function (i.e. the abstract
   *  interpreter) is monotone. Error handling breaks monotonicity of the
   *  abstract interpreter, because when an error happens, we always return
   *  the bottom value `Hot` for an expression. It is not a threat for
   *  termination because when an error happens, we stop the fixed point
   *  computation at the end of the iteration where the error happens. Care
   *  must be paid to tests of errors, monotonicity will be broken if we simply
   *  ignore the test errors (See `TryReporter`).
   *
   *  Note: It's tempting to use location of trees as key. That should
   *  be avoided as a template may have the same location as its single
   *  statement body. Macros may also create incorrect locations.
   */
  object Cache:
    /** Cache for expressions
     *
     *  Ref -> Tree -> Value
     *
     *  The first key is the value of `this` for the expression.
     *
     *  We do not need the heap in the key, because the value of an expression
     *  is only determined by the value of `this`. The heap is immutable: the
     *  abstract values for object fields never change within one iteration.
     *  The initial abstraction of a field is always a safe over-approximation
     *  thanks to monotonicity of initialization states.
     *
     *  If the heap is unstable in an iteration, the cache should also be
     *  unstable. This is because all values stored in the heap are also present
     *  in the cache. Therefore, we only need to track whether the cache is
     *  stable between two iterations.
     *
     *  The heap is not part of the fixed point computation -- we throw the
     *  unstable heap from last iteration away. In contrast, we use the unstable
     *  output cache from the last iteration as input for the next iteration.
     *  This is safe because the heap is determined by the cache -- it is a
     *  "local" data to the computing function, conceptually. Local data is
     *  always safe be discarded.
     *
     *  Now, if a fixed point is reached, the local data contains stable data
     *  that could be reused to check other classes. We employ this trick to
     *  improve performance of the analysis.
     */
    private type ExprValueCache = Map[Value, Map[TreeWrapper, Value]]

    /** The heap for abstract objects
     *
     *  The heap objects are immutable.
     */
    private type Heap = Map[Ref, Objekt]

    /** A wrapper for trees for storage in maps based on referential equality of trees. */
    private abstract class TreeWrapper:
      def tree: Tree

      override final def equals(other: Any): Boolean =
        other match
        case that: TreeWrapper => this.tree eq that.tree
        case _ => false

      override final def hashCode = tree.hashCode

    /** The immutable wrapper is intended to be stored as key in the heap. */
    private class ImmutableTreeWrapper(val tree: Tree) extends TreeWrapper

    /** For queries on the heap, reuse the same wrapper to avoid unnecessary allocation.
     *
     *  A `MutableTreeWrapper` is only ever used temporarily for querying a map,
     *  and is never inserted to the map.
     */
    private class MutableTreeWrapper extends TreeWrapper:
      var queryTree: Tree | Null = null
      def tree: Tree = queryTree match
        case tree: Tree => tree
        case null => ???

    class Cache:
      /** The cache for expression values from last iteration */
      private var last: ExprValueCache =  Map.empty

      /** The output cache for expression values
       *
       *  The output cache is computed based on the cache values `last` from the
       *  last iteration.
       *
       *  Both `last` and `current` are required to make sure an encountered
       *  expression is evaluated once in each iteration.
       */
      private var current: ExprValueCache = Map.empty

      /** Global cached values for expressions
       *
       *  The values are only added when a fixed point is reached.
       *
       *  It is intended to improve performance for computation related to warm values.
       */
      private var stable: ExprValueCache = Map.empty

      /** Whether the current heap is different from the last heap?
       *
       *  `changed == false` implies that the fixed point has been reached.
       */
      private var changed: Boolean = false

      /** Abstract heap stores abstract objects
       *
       *  The heap serves as cache of summaries for warm objects and is shared for checking all classes.
       *
       *  The fact that objects of `ThisRef` are stored in heap is just an engineering convenience.
       *  Technically, we can also store the object directly in `ThisRef`.
       *
       *  The heap contains objects of two conceptually distinct kinds.
       *
       *  - Objects that are also in `heapStable` are flow-insensitive views of already initialized objects that are
       *    cached for reuse in analysis of later classes. These objects and their fields should never change; this is
       *    enforced using assertions.
       *
       *  - Objects that are not (yet) in `heapStable` are the flow-sensitive abstract state of objects being analyzed
       *    in the current iteration of the analysis of the current class. Their fields do change flow-sensitively: more
       *    fields are added as fields become initialized. These objects are valid only within the current iteration and
       *    are removed when moving to a new iteration of analyzing the current class. When the analysis of a class
       *    reaches a fixed point, these now stable flow-sensitive views of the object at the end of the constructor
       *    of the analyzed class now become the flow-insensitive views of already initialized objects and can therefore
       *    be added to `heapStable`.
       */
      private var heap: Heap = Map.empty

      /** Used to revert heap to last stable heap. */
      private var heapStable: Heap = Map.empty

      /** Used to avoid allocation, its state does not matter */
      private given MutableTreeWrapper = new MutableTreeWrapper

      def hasChanged = changed

      def get(value: Value, expr: Tree): Option[Value] =
        current.get(value, expr) match
        case None => stable.get(value, expr)
        case res => res

      /** Backup the state of the cache
       *
       *  All the shared data structures must be immutable.
       */
      def backup(): Cache =
        val cache = new Cache
        cache.last = this.last
        cache.current = this.current
        cache.stable = this.stable
        cache.heap = this.heap
        cache.heapStable = this.heapStable
        cache.changed = this.changed
        cache

      /** Restore state from a backup */
      def restore(cache: Cache) =
        this.last = cache.last
        this.current = cache.current
        this.stable = cache.stable
        this.heap = cache.heap
        this.heapStable = cache.heapStable
        this.changed = cache.changed

      /** Copy the value of `(value, expr)` from the last cache to the current cache
       *
       *  It assumes the value is `Hot` if it doesn't exist in the last cache.
       *
       *  It updates the current caches if the values change.
       *
       *  The two caches are required because we want to make sure in a new iteration, an expression is evaluated once.
       */
      def assume(value: Value, expr: Tree, cacheResult: Boolean)(fun: => Value): Contextual[Value] =
        val assumeValue: Value =
          last.get(value, expr) match
          case Some(value) => value
          case None =>
            this.last = last.updatedNested(value, expr, Hot)
            Hot

        this.current = current.updatedNested(value, expr, assumeValue)

        val actual = fun
        if actual != assumeValue then
          this.changed = true
          this.current = this.current.updatedNested(value, expr, actual)
        else
          // It's tempting to cache the value in stable, but it's unsound.
          // The reason is that the current value may depend on other values
          // which might change.
          //
          // stable.put(value, expr, actual)
          ()
        end if

        actual
      end assume

      /** Commit current cache to stable cache. */
      private def commitToStableCache() =
        for
          (v, m) <- current
          if v.isWarm          // It's useless to cache value for ThisRef.
          (wrapper, res) <- m
        do
          this.stable = stable.updatedNestedWrapper(v, wrapper.asInstanceOf[ImmutableTreeWrapper], res)

      /** Prepare cache for the next iteration
       *
       *  1. Reset changed flag.
       *
       *  2. Use current cache as last cache and set current cache to be empty.
       *
       *  3. Revert heap to stable.
       */
      def prepareForNextIteration()(using Context) =
        this.changed = false
        this.last = this.current
        this.current = Map.empty
        this.heap = this.heapStable

      /** Prepare for checking next class
       *
       *  1. Reset changed flag.
       *
       *  2. Commit current cache to stable cache if not changed.
       *
       *  3. Update stable heap if not changed.
       *
       *  4. Reset last cache.
       */
      def prepareForNextClass()(using Context) =
        if this.changed then
          this.changed = false
          this.heap = this.heapStable
        else
          this.commitToStableCache()
          this.heapStable = this.heap

        this.last = Map.empty
        this.current = Map.empty

      def updateObject(ref: Ref, obj: Objekt) =
        assert(!this.heapStable.contains(ref))
        this.heap = this.heap.updated(ref, obj)

      def containsObject(ref: Ref) = heap.contains(ref)

      def getObject(ref: Ref) = heap(ref)
    end Cache

    extension (cache: ExprValueCache)
      private def get(value: Value, expr: Tree)(using queryWrapper: MutableTreeWrapper): Option[Value] =
        queryWrapper.queryTree = expr
        cache.get(value).flatMap(_.get(queryWrapper))

      private def removed(value: Value, expr: Tree)(using queryWrapper: MutableTreeWrapper) =
        queryWrapper.queryTree = expr
        val innerMap2 = cache(value).removed(queryWrapper)
        cache.updated(value, innerMap2)

      private def updatedNested(value: Value, expr: Tree, result: Value): ExprValueCache =
        val wrapper = new ImmutableTreeWrapper(expr)
        updatedNestedWrapper(value, wrapper, result)

      private def updatedNestedWrapper(value: Value, wrapper: ImmutableTreeWrapper, result: Value): ExprValueCache =
        val innerMap = cache.getOrElse(value, Map.empty[TreeWrapper, Value])
        val innerMap2 = innerMap.updated(wrapper, result)
        cache.updated(value, innerMap2)
    end extension
  end Cache

  import Cache.*

  inline def cache(using c: Cache): Cache = c

// ----- Checker State -----------------------------------

  /** The state that threads through the interpreter */
  type Contextual[T] = (Context, Trace, Promoted, Cache, Reporter) ?=> T

// ----- Error Handling -----------------------------------

  object Trace:
    opaque type Trace = Vector[Tree]

    val empty: Trace = Vector.empty

    extension (trace: Trace)
      def add(node: Tree): Trace = trace :+ node
      def toVector: Vector[Tree] = trace

    def show(using trace: Trace, ctx: Context): String = buildStacktrace(trace, "\n")

    def position(using trace: Trace): Tree = trace.last
  type Trace = Trace.Trace

  import Trace.*
  def trace(using t: Trace): Trace = t
  inline def withTrace[T](t: Trace)(op: Trace ?=> T): T = op(using t)
  inline def extendTrace[T](node: Tree)(using t: Trace)(op: Trace ?=> T): T = op(using t.add(node))

  /** Error reporting */
  trait Reporter:
    def report(err: Error): Unit
    def reportAll(errs: Seq[Error]): Unit = for err <- errs do report(err)

  /** A TryReporter cannot be simply thrown away
   *
   *  Either `abort` should be called or the errors be reported.
   *
   *  If errors are ignored and `abort` is not called, the monotonicity of the
   *  computation function is not guaranteed, thus termination of fixed-point
   *  computation becomes a problem.
   */
  trait TryReporter extends Reporter:
    /**
     * Revert the cache to previous state.
     */
    def abort()(using Cache): Unit
    def errors: List[Error]

  object Reporter:
    class BufferedReporter extends Reporter:
      private val buf = new mutable.ArrayBuffer[Error]
      def errors = buf.toList
      def report(err: Error) = buf += err

    class TryBufferedReporter(backup: Cache) extends BufferedReporter with TryReporter:
      def abort()(using Cache): Unit = cache.restore(backup)

    class ErrorFound(val error: Error) extends Exception
    class StopEarlyReporter extends Reporter:
      def report(err: Error) = throw new ErrorFound(err)

    /** Capture all errors with a TryReporter
     *
     *  The TryReporter cannot be thrown away: either `abort` must be called or
     *  the errors must be reported.
     */
    def errorsIn(fn: Reporter ?=> Unit)(using Cache): TryReporter =
      val reporter = new TryBufferedReporter(cache.backup())
      fn(using reporter)
      reporter

    /** Stop on first error */
    def stopEarly(fn: Reporter ?=> Unit): List[Error] =
      val reporter: Reporter = new StopEarlyReporter

      try
        fn(using reporter)
        Nil
      catch case ex: ErrorFound =>
        ex.error :: Nil

    def hasErrors(fn: Reporter ?=> Unit)(using Cache): Boolean =
      val backup = cache.backup()
      val errors = stopEarly(fn)
      cache.restore(backup)
      errors.nonEmpty

  inline def reporter(using r: Reporter): Reporter = r

// ----- Operations on domains -----------------------------
  extension (a: Value)
    def join(b: Value): Value =
      (a, b) match
      case (Hot, _)  => b
      case (_, Hot)  => a

      case (Cold, _) => Cold
      case (_, Cold) => Cold

      case (a: (Fun | Warm | ThisRef), b: (Fun | Warm | ThisRef)) =>
        if a == b then a else RefSet(a :: b :: Nil)

      case (a: (Fun | Warm | ThisRef), RefSet(refs)) =>
        if refs.exists(_ == a) then b: Value // fix pickling test
        else RefSet(a :: refs)

      case (RefSet(refs), b: (Fun | Warm | ThisRef)) =>
        if refs.exists(_ == b) then a: Value // fix pickling test
        else RefSet(b :: refs)

      case (RefSet(refs1), RefSet(refs2)) =>
        val diff = refs2.filter(ref => refs1.forall(_ != ref))
        RefSet(refs1 ++ diff)

    /** Conservatively approximate the value with `Cold` or `Hot` */
    def widenArg: Contextual[Value] =
      a match
      case _: Ref | _: Fun =>
        val hasError = Reporter.hasErrors { a.promote("Argument cannot be promoted to hot") }
        if hasError then Cold else Hot

      case RefSet(refs) =>
        refs.map(_.widenArg).join

      case _ => a


  extension (values: Seq[Value])
    def join: Value =
      if values.isEmpty then Hot
      else values.reduce { (v1, v2) => v1.join(v2) }

    def widenArgs: Contextual[List[Value]] = values.map(_.widenArg).toList


  extension (ref: Ref)
    def objekt: Contextual[Objekt] =
      // TODO: improve performance
      ref match
        case warm: Warm => warm.ensureObjectExistsAndPopulated()
        case _ =>
      cache.getObject(ref)

    def ensureObjectExists()(using Cache): ref.type =
      if cache.containsObject(ref) then
        printer.println("object " + ref + " already exists")
        ref
      else
        ensureFresh()

    def ensureFresh()(using Cache): ref.type =
      val obj = Objekt(ref.klass, fields = Map.empty, outers = Map(ref.klass -> ref.outer))
      printer.println("reset object " + ref)
      cache.updateObject(ref, obj)
      ref

    /** Update field value of the abstract object
     *
     *  Invariant: fields are immutable and only set once
     */
    def updateField(field: Symbol, value: Value): Contextual[Unit] = log("set field " + field + " of " + ref + " to " + value) {
      val obj = objekt
      // We may reset the outers or params of a populated warm object.
      // This is the case if we need access the field of a warm object, which
      // requires population of parameters and outers; and later create an
      // instance of the exact warm object, whose initialization will reset
      // the outer and constructor parameters.
      //
      // See tests/init/neg/unsound1.scala
      val changed = !obj.hasField(field) || obj.field(field) != value
      def isParamUpdate = field.isOneOf(Flags.ParamAccessor | Flags.Param) && obj.field(field) == value
      assert(!obj.hasField(field) || isParamUpdate, field.show + " already init, new = " + value + ", old = " + obj.field(field) + ", ref = " + ref)
      val obj2 = obj.copy(fields = obj.fields.updated(field, value))
      if changed then cache.updateObject(ref, obj2)
    }

    /** Update the immediate outer of the given `klass` of the abstract object
     *
     *  Invariant: outers are immutable and only set once
     */
    def updateOuter(klass: ClassSymbol, value: Value): Contextual[Unit] = log("set outer " + klass + " of " + ref + " to " + value) {
      val obj = objekt
      // See the comment in `updateField` for setting the value twice.
      assert(!obj.hasOuter(klass) || obj.outer(klass) == value, klass.show + " already has outer, new = " + value + ", old = " + obj.outer(klass) + ", ref = " + ref)
      val obj2 = obj.copy(outers = obj.outers.updated(klass, value))
      cache.updateObject(ref, obj2)
    }
  end extension

  extension (value: Value)
    def ensureHot(msg: String): Contextual[Value] =
      value.promote(msg)
      value

    def select(field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = log("select " + field.show + ", this = " + value, printer, (_: Value).show) {
      if promoted.isCurrentObjectPromoted then Hot
      else value match {
        case Hot  =>
          Hot

        case Cold =>
          val error = AccessCold(field, trace.toVector)
          reporter.report(error)
          Hot

        case ref: Ref =>
          val target = if needResolve then resolve(ref.klass, field) else field
          if target.is(Flags.Lazy) then
            val rhs = target.defTree.asInstanceOf[ValDef].rhs
            eval(rhs, ref, target.owner.asClass, cacheResult = true)
          else if target.exists then
            val obj = ref.objekt
            if obj.hasField(target) then
              obj.field(target)
            else if ref.isInstanceOf[Warm] then
              assert(obj.klass.isSubClass(target.owner))
              if target.is(Flags.ParamAccessor) then
                // possible for trait parameters
                // see tests/init/neg/trait2.scala
                //
                // return `Hot` here, errors are reported in checking `ThisRef`
                Hot
              else if target.hasSource then
                val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs
                eval(rhs, ref, target.owner.asClass, cacheResult = true)
              else
                val error = CallUnknown(field, trace.toVector)
                reporter.report(error)
                Hot
            else
              val error = AccessNonInit(target, trace.toVector)
              reporter.report(error)
              Hot
          else
            if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then
              report.error("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position)
              Hot
            else
              // This is possible due to incorrect type cast.
              // See tests/init/pos/Type.scala
              Hot

        case fun: Fun =>
          report.error("[Internal error] unexpected tree in selecting a function, fun = " + fun.expr.show + Trace.show, fun.expr)
          Hot

        case RefSet(refs) =>
          refs.map(_.select(field, receiver)).join
      }
    }

    def call(meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) {
      def promoteArgs(): Contextual[Unit] = args.foreach(_.promote)

      def isSyntheticApply(meth: Symbol) =
        meth.is(Flags.Synthetic)
        && meth.name == nme.apply
        && meth.owner.is(Flags.Module)
        && meth.owner.companionClass.is(Flags.Case)

      def isAlwaysSafe(meth: Symbol) =
        (meth eq defn.Object_eq)
        || (meth eq defn.Object_ne)
        || (meth eq defn.Any_isInstanceOf)

      def checkArgsWithParametricity() =
        val methodType = atPhaseBeforeTransforms { meth.info.stripPoly }
        var allArgsHot = true
        val allParamTypes = methodType.paramInfoss.flatten.map(_.repeatedToSingle)
        val errors = allParamTypes.zip(args).flatMap { (info, arg) =>
          val tryReporter = Reporter.errorsIn { arg.promote }
          allArgsHot = allArgsHot && tryReporter.errors.isEmpty
          if tryReporter.errors.isEmpty then tryReporter.errors
          else
            info match
            case typeParamRef: TypeParamRef =>
              val bounds = typeParamRef.underlying.bounds
              val isWithinBounds = bounds.lo <:< defn.NothingType && defn.AnyType <:< bounds.hi
              def otherParamContains = allParamTypes.exists { param => param != typeParamRef && param.typeSymbol != defn.ClassTagClass && typeParamRef.occursIn(param) }
              // A non-hot method argument is allowed if the corresponding parameter type is a
              // type parameter T with Any as its upper bound and Nothing as its lower bound.
              // the other arguments should either correspond to a parameter type that is T
              // or that does not contain T as a component.
              if isWithinBounds && !otherParamContains then
                tryReporter.abort()
                Nil
              else
                tryReporter.errors
            case _ => tryReporter.errors
        }
        (errors, allArgsHot)

      // fast track if the current object is already initialized
      if promoted.isCurrentObjectPromoted then Hot
      else if isAlwaysSafe(meth) then Hot
      else if meth eq defn.Any_asInstanceOf then value
      else value match {
        case Hot  =>
          if isSyntheticApply(meth) && meth.hasSource then
            val klass = meth.owner.companionClass.asClass
            instantiate(klass, klass.primaryConstructor, args)
          else
            if receiver.typeSymbol.isStaticOwner then
              val (errors, allArgsHot) = checkArgsWithParametricity()
              if allArgsHot then
                Hot: Value
              else if errors.nonEmpty then
                reporter.reportAll(errors)
                Hot: Value
              else
                Cold: Value
            else
              promoteArgs()
              Hot

        case Cold =>
          promoteArgs()
          val error = CallCold(meth, trace.toVector)
          reporter.report(error)
          Hot

        case ref: Ref =>
          val isLocal = !meth.owner.isClass
          val target =
            if !needResolve then
              meth
            else if superType.exists then
              resolveSuper(ref.klass, superType, meth)
            else
              resolve(ref.klass, meth)

          if target.isOneOf(Flags.Method) then
            if target.hasSource then
              val cls = target.owner.enclosingClass.asClass
              val ddef = target.defTree.asInstanceOf[DefDef]
              val tryReporter = Reporter.errorsIn { promoteArgs() }
              // normal method call
              if tryReporter.errors.nonEmpty && isSyntheticApply(meth) then
                tryReporter.abort()
                val klass = meth.owner.companionClass.asClass
                val targetCls = klass.owner.lexicallyEnclosingClass.asClass
                val outer = resolveThis(targetCls, ref, meth.owner.asClass)
                outer.instantiate(klass, klass.primaryConstructor, args)
              else
                reporter.reportAll(tryReporter.errors)
                extendTrace(ddef) {
                  eval(ddef.rhs, ref, cls, cacheResult = true)
                }
            else if ref.canIgnoreMethodCall(target) then
              Hot
            else
              // no source code available
              promoteArgs()
              // try promoting the receiver as last resort
              val hasErrors = Reporter.hasErrors { ref.promote("try promote value to hot") }
              if hasErrors then
                val error = CallUnknown(target, trace.toVector)
                reporter.report(error)
              Hot
          else if target.exists then
            // method call resolves to a field
            val obj = ref.objekt
            if obj.hasField(target) then
              obj.field(target)
            else
              value.select(target, receiver, needResolve = false)
          else
            if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then
              report.error("Unexpected resolution failure: ref.klass = " + ref.klass.show + ", meth = " + meth.show + Trace.show, Trace.position)
              Hot
            else
              // This is possible due to incorrect type cast.
              // See tests/init/pos/Type.scala
              Hot

        case Fun(body, thisV, klass) =>
          // meth == NoSymbol for poly functions
          if meth.name.toString == "tupled" then value // a call like `fun.tupled`
          else
            promoteArgs()
            eval(body, thisV, klass, cacheResult = true)

        case RefSet(refs) =>
          refs.map(_.call(meth, args, receiver, superType)).join
      }
    }

    def callConstructor(ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("call " + ctor.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) {
      // init "fake" param fields for parameters of primary and secondary constructors
      def addParamsAsFields(args: List[Value], ref: Ref, ctorDef: DefDef) =
        val params = ctorDef.termParamss.flatten.map(_.symbol)
        assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size)
        for (param, value) <- params.zip(args) do
          ref.updateField(param, value)
          printer.println(param.show + " initialized with " + value)

      value match {
        case Hot | Cold | _: RefSet | _: Fun =>
          report.error("[Internal error] unexpected constructor call, meth = " + ctor + ", value = " + value + Trace.show, Trace.position)
          Hot

        case ref: Warm if ref.isPopulatingParams =>
          if ctor.hasSource then
            val cls = ctor.owner.enclosingClass.asClass
            val ddef = ctor.defTree.asInstanceOf[DefDef]
            val args2 = args.map(_.value).widenArgs
            addParamsAsFields(args2, ref, ddef)
            if ctor.isPrimaryConstructor then
              val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]
              extendTrace(cls.defTree) { init(tpl, ref, cls) }
            else
              val initCall = ddef.rhs match
                case Block(call :: _, _) => call
                case call => call
              extendTrace(ddef) { eval(initCall, ref, cls) }
            end if
          else
            Hot

        case ref: Ref =>
          if ctor.hasSource then
            val cls = ctor.owner.enclosingClass.asClass
            val ddef = ctor.defTree.asInstanceOf[DefDef]
            val args2 = args.map(_.value).widenArgs
            addParamsAsFields(args2, ref, ddef)
            if ctor.isPrimaryConstructor then
              val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]
              extendTrace(cls.defTree) { eval(tpl, ref, cls, cacheResult = true) }
              ref
            else
              extendTrace(ddef) { eval(ddef.rhs, ref, cls, cacheResult = true) }
          else if ref.canIgnoreMethodCall(ctor) then
            Hot
          else
            // no source code available
            val error = CallUnknown(ctor, trace.toVector)
            reporter.report(error)
            Hot
      }

    }

    /** Handle a new expression `new p.C` where `p` is abstracted by `value` */
    def instantiate(klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("instantiating " + klass.show + ", value = " + value + ", args = " + args.map(_.value.show), printer, (_: Value).show) {
      def tryLeak(warm: Warm, nonHotOuterClass: Symbol, argValues: List[Value]): Contextual[Value] =
        val argInfos2 = args.zip(argValues).map { (argInfo, v) => argInfo.copy(value = v) }
        val errors = Reporter.stopEarly {
          given Trace = Trace.empty
          warm.callConstructor(ctor, argInfos2)
        }
        if errors.nonEmpty then
          val indices =
            for
              (arg, i) <- argValues.zipWithIndex
              if arg.isCold
            yield
              i + 1

          val error = UnsafeLeaking(trace.toVector, errors.head, nonHotOuterClass, indices)
          reporter.report(error)
          Hot
        else
          warm

      if promoted.isCurrentObjectPromoted then Hot
      else value match {
        case Hot  =>
          var allHot = true
          val args2 = args.map { arg =>
            val hasErrors = Reporter.hasErrors { arg.promote }
            allHot = allHot && !hasErrors
            if hasErrors then arg.value.widenArg
            else Hot
          }

          if allHot then
            Hot
          else
            val outer = Hot
            val warm = Warm(klass, outer, ctor, args2).ensureObjectExists()
            tryLeak(warm, NoSymbol, args2)

        case Cold =>
          val error = CallCold(ctor, trace.toVector)
          reporter.report(error)
          Hot

        case ref: Ref =>
          // widen the outer to finitize the domain
          val outer = ref match
            case warm @ Warm(_, _: Warm, _, _) =>
              // the widened warm object might not exist in the heap
              warm.copy(outer = Cold).ensureObjectExistsAndPopulated()
            case _ => ref

          val argsWidened = args.map(_.value).widenArgs
          val warm = Warm(klass, outer, ctor, argsWidened).ensureObjectExists()
          if argsWidened.exists(_.isCold) then
            tryLeak(warm, klass.owner.lexicallyEnclosingClass, argsWidened)
          else
            val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) }
            warm.callConstructor(ctor, argInfos2)
            warm

        case Fun(body, thisV, klass) =>
          report.error("[Internal error] unexpected tree in instantiating a function, fun = " + body.show + Trace.show, Trace.position)
          Hot

        case RefSet(refs) =>
          refs.map(_.instantiate(klass, ctor, args)).join
      }
    }
  end extension

  extension (ref: Ref)
    def accessLocal(tmref: TermRef, klass: ClassSymbol): Contextual[Value] =
      val sym = tmref.symbol

      if sym.is(Flags.Param) && sym.owner.isConstructor then
        val enclosingClass = sym.owner.enclosingClass.asClass
        val thisValue2 = resolveThis(enclosingClass, ref, klass)
        thisValue2 match
        case Hot => Hot
        case ref: Ref => ref.objekt.field(sym)
        case _ =>
            report.error("[Internal error] unexpected this value accessing local variable, sym = " + sym.show + ", thisValue = " + thisValue2.show + Trace.show, Trace.position)
            Hot
      else if sym.is(Flags.Param) then
        Hot
      else
        sym.defTree match {
          case vdef: ValDef =>
            // resolve this for local variable
            val enclosingClass = sym.owner.enclosingClass.asClass
            val thisValue2 = resolveThis(enclosingClass, ref, klass)
            thisValue2 match
              case Hot => Hot

              case Cold => Cold

              case ref: Ref => eval(vdef.rhs, ref, enclosingClass)

              case _ =>
                 report.error("[Internal error] unexpected this value when accessing local variable, sym = " + sym.show + ", thisValue = " + thisValue2.show + Trace.show, Trace.position)
                 Hot
            end match

          case _ => Hot
        }
  end extension

// ----- Promotion ----------------------------------------------------
  extension (ref: Ref)
    /** Whether the object is fully assigned
     *
     *  It means all fields and outers are set. For performance, we don't check
     *  outers here, because Scala semantics ensure that they are always set
     *  before any user code in the constructor.
     *
     *  Note that `isFullyFilled = true` does not mean we can use the
     *  object freely, as its fields or outers may still reach uninitialized
     *  objects.
     */
    def isFullyFilled: Contextual[Boolean] = log("isFullyFilled " + ref, printer) {
      val obj = ref.objekt
      ref.klass.baseClasses.forall { klass =>
        !klass.hasSource || {
          val nonInits = klass.info.decls.filter { member =>
            !member.isOneOf(Flags.Method | Flags.Lazy | Flags.Deferred)
            && !member.isType
            && !obj.hasField(member)
          }
          printer.println("nonInits = " + nonInits)
          nonInits.isEmpty
        }
      }
    }

    def nonInitFields(): Contextual[List[Symbol]] =
      val obj = ref.objekt
      ref.klass.baseClasses.flatMap { klass =>
        if klass.hasSource then
          klass.info.decls.filter { member =>
            !member.isOneOf(Flags.Method | Flags.Lazy | Flags.Deferred)
            && !member.isType
            && !obj.hasField(member)
          }
        else
          Nil
      }

  end extension

  extension (thisRef: ThisRef)
    def tryPromoteCurrentObject(): Contextual[Boolean] = log("tryPromoteCurrentObject ", printer) {
      if promoted.isCurrentObjectPromoted then
        true
      else if thisRef.isFullyFilled then
        // If we have all fields initialized, then we can promote This to hot.
        promoted.promoteCurrent(thisRef)
        true
      else
        false
    }

  extension (value: Value)
    /** Promotion of values to hot */
    def promote(msg: String): Contextual[Unit] = log("promoting " + value + ", promoted = " + promoted, printer) {
      if !promoted.isCurrentObjectPromoted then

        value match
        case Hot   =>

        case Cold  =>
          reporter.report(PromoteError(msg, trace.toVector))

        case thisRef: ThisRef =>
          val emptyFields = thisRef.nonInitFields()
          if emptyFields.isEmpty then
            promoted.promoteCurrent(thisRef)
          else
            val fields = "Non initialized field(s): " + emptyFields.map(_.show).mkString(", ") + "."
            reporter.report(PromoteError(msg + "\n" + fields, trace.toVector))

        case warm: Warm =>
          if !promoted.contains(warm) then
            promoted.add(warm)
            val errors = warm.tryPromote(msg)
            if errors.nonEmpty then promoted.remove(warm)
            reporter.reportAll(errors)

        case fun @ Fun(body, thisV, klass) =>
          if !promoted.contains(fun) then
            val errors = Reporter.stopEarly {
              val res = {
                given Trace = Trace.empty
                eval(body, thisV, klass)
              }
              given Trace = Trace.empty.add(body)
              res.promote("The function return value is not hot. Found = " + res.show + ". ")
            }
            if errors.nonEmpty then
              reporter.report(UnsafePromotion(msg, trace.toVector, errors.head))
            else
              promoted.add(fun)

        case RefSet(refs) =>
          refs.foreach(_.promote(msg))
    }
  end extension

  extension (warm: Warm)
    /** Try early promotion of warm objects
     *
     *  Promotion is expensive and should only be performed for small classes.
     *
     *  1. for each concrete method `m` of the warm object:
     *     call the method and promote the result
     *
     *  2. for each concrete field `f` of the warm object:
     *     promote the field value
     *
     *  If the object contains nested classes as members, the checker simply
     *  reports a warning to avoid expensive checks.
     *
     *  TODO: we need to revisit whether this is needed once we make the
     *  system more flexible in other dimentions: e.g. leak to
     *  methods or constructors, or use ownership for creating cold data structures.
     */
    def tryPromote(msg: String): Contextual[List[Error]] = log("promote " + warm.show + ", promoted = " + promoted, printer) {
      val classRef = warm.klass.appliedRef
      val hasInnerClass = classRef.memberClasses.filter(_.symbol.hasSource).nonEmpty
      if hasInnerClass then
        return PromoteError(msg + "Promotion cancelled as the value contains inner classes. ", trace.toVector) :: Nil

      val errors = Reporter.stopEarly {
        for klass <- warm.klass.baseClasses if klass.hasSource do
          for member <- klass.info.decls do
            if !member.isType && !member.isConstructor && member.hasSource  && !member.is(Flags.Deferred) then
              if member.is(Flags.Method, butNot = Flags.Accessor) then
                withTrace(Trace.empty) {
                  val args = member.info.paramInfoss.flatten.map(_ => ArgInfo(Hot, Trace.empty))
                  val res = warm.call(member, args, receiver = warm.klass.typeRef, superType = NoType)
                  res.promote("Cannot prove that the return value of " + member.show + " is hot. Found = " + res.show + ". ")
                }
              else
                withTrace(Trace.empty) {
                  val res = warm.select(member, receiver = warm.klass.typeRef)
                  res.promote("Cannot prove that the field " + member.show + " is hot. Found = " + res.show + ". ")
                }
          end for
        end for
      }

      if errors.isEmpty then Nil
      else UnsafePromotion(msg, trace.toVector, errors.head) :: Nil
    }

  end extension

// ----- Policies ------------------------------------------------------
  extension (value: Ref)
    /** Can the method call on `value` be ignored?
     *
     *  Note: assume overriding resolution has been performed.
     */
    def canIgnoreMethodCall(meth: Symbol)(using Context): Boolean =
      val cls = meth.owner
      cls == defn.AnyClass ||
      cls == defn.AnyValClass ||
      cls == defn.ObjectClass

// ----- Work list ---------------------------------------------------
  case class Task(value: ThisRef)

  class WorkList private[Semantic]():
    private val pendingTasks: mutable.ArrayBuffer[Task] = new mutable.ArrayBuffer<commit_msg>Faster promotion<commit_after>package dotty.tools.dotc
package transform
package init

import core.*
import Contexts.*
import Symbols.*
import Types.*
import StdNames.*
import NameKinds.OuterSelectName

import ast.tpd.*
import config.Printers.init as printer
import reporting.trace as log

import Errors.*

import scala.collection.mutable
import scala.annotation.tailrec

object Semantic:

// ----- Domain definitions --------------------------------

  /** Abstract values
   *
   *  Value = Hot | Cold | Warm | ThisRef | Fun | RefSet
   *
   *                 Cold
   *        ┌──────►  ▲  ◄────┐  ◄────┐
   *        │         │       │       │
   *        │         │       │       │
   *        |         │       │       │
   *        |         │       │       │
   *     ThisRef     Warm   Fun    RefSet
   *        │         ▲       ▲       ▲
   *        │         │       │       │
   *        |         │       │       │
   *        ▲         │       │       │
   *        │         │       │       │
   *        └─────────┴───────┴───────┘
   *                  Hot
   *
   *   The diagram above does not reflect relationship between `RefSet`
   *   and other values. `RefSet` represents a set of values which could
   *   be `ThisRef`, `Warm` or `Fun`. The following ordering applies for
   *   RefSet:
   *
   *         R_a ⊑ R_b if R_a ⊆ R_b
   *
   *         V ⊑ R if V ∈ R
   *
   */
  sealed abstract class Value:
    def show(using Context): String = this match
      case ThisRef(klass) =>
        "ThisRef[" + klass.show + "]"
      case Warm(klass, outer, ctor, args) =>
        val argsText = if args.nonEmpty then ", args = " + args.map(_.show).mkString("(", ", ", ")") else ""
        "Warm[" + klass.show + "] { outer = " + outer.show + argsText + " }"
      case Fun(expr, thisV, klass) =>
        "Fun { this = " + thisV.show + ", owner = " + klass.show + " }"
      case RefSet(values) =>
        values.map(_.show).mkString("Set { ", ", ", " }")
      case _ =>
        this.toString()

    def isHot = this == Hot
    def isCold = this == Cold
    def isWarm = this.isInstanceOf[Warm]
    def isThisRef = this.isInstanceOf[ThisRef]

  /** A transitively initialized object */
  case object Hot extends Value

  /** An object with unknown initialization status */
  case object Cold extends Value

  sealed abstract class Ref extends Value:
    def klass: ClassSymbol
    def outer: Value

  /** A reference to the object under initialization pointed by `this` */
  case class ThisRef(klass: ClassSymbol) extends Ref:
    val outer = Hot

  /** An object with all fields initialized but reaches objects under initialization
   *
   *  We need to restrict nesting levels of `outer` to finitize the domain.
   */
  case class Warm(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref:

    /** If a warm value is in the process of populating parameters, class bodies are not executed. */
    private var populatingParams: Boolean = false

    def isPopulatingParams = populatingParams

    /** Ensure that outers and class parameters are initialized.
     *
     *  Fields in class body are not initialized.
     *
     *  We need to populate class parameters and outers for warm values for the
     *  following cases:
     *
     *  - Widen an already checked warm value to another warm value without
     *    corresponding object
     *
     *  - Using a warm value from the cache, whose corresponding object from
     *    the last iteration have been remove due to heap reversion
     *    {@see Cache.prepareForNextIteration}
     *
     *  After populating class parameters and outers, it is possible to lazily
     *  compute the field values in class bodies when they are accessed.
     */
    private def populateParams(): Contextual[this.type] = log("populating parameters", printer, (_: Warm).objekt.toString) {
      assert(!populatingParams, "the object is already populating parameters")
      populatingParams = true
      val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]
      extendTrace(klass.defTree) { this.callConstructor(ctor, args.map(arg => ArgInfo(arg, trace))) }
      populatingParams = false
      this
    }

    def ensureObjectExistsAndPopulated(): Contextual[this.type] =
      if cache.containsObject(this) then this
      else this.ensureFresh().populateParams()

  end Warm

  /** A function value */
  case class Fun(expr: Tree, thisV: Ref, klass: ClassSymbol) extends Value

  /** A value which represents a set of addresses
   *
   * It comes from `if` expressions.
   */
  case class RefSet(refs: List[Fun | Ref]) extends Value

  // end of value definition

  /** The abstract object which stores value about its fields and immediate outers.
   *
   *  Semantically it suffices to store the outer for `klass`. We cache other outers
   *  for performance reasons.
   *
   *  Note: Object is NOT a value.
   */
  case class Objekt(val klass: ClassSymbol, val fields: Map[Symbol, Value], val outers: Map[ClassSymbol, Value]):
    def field(f: Symbol): Value = fields(f)

    def outer(klass: ClassSymbol) = outers(klass)

    def hasOuter(klass: ClassSymbol) = outers.contains(klass)

    def hasField(f: Symbol) = fields.contains(f)

  object Promoted:
    class PromotionInfo:
      var isCurrentObjectPromoted: Boolean = false
      val values = mutable.Set.empty[Value]
      override def toString(): String = values.toString()

    /** Values that have been safely promoted */
    opaque type Promoted = PromotionInfo

    /** Note: don't use `val` to avoid incorrect sharing */
    def empty: Promoted = new PromotionInfo

    extension (promoted: Promoted)
      def isCurrentObjectPromoted: Boolean = promoted.isCurrentObjectPromoted
      def promoteCurrent(thisRef: ThisRef): Unit = promoted.isCurrentObjectPromoted = true
      def contains(value: Value): Boolean = promoted.values.contains(value)
      def add(value: Value): Unit = promoted.values += value
      def remove(value: Value): Unit = promoted.values -= value
    end extension
  end Promoted
  type Promoted = Promoted.Promoted

  import Promoted.*
  inline def promoted(using p: Promoted): Promoted = p

  /** Cache used in fixed point computation
   *
   *  The analysis computes the least fixed point for the cache (see doc for
   *   `ExprValueCache`).
   *
   *  For the fixed point computation to terminate, we need to make sure that
   *  the domain of the cache, i.e. the key pair (Ref, Tree) is finite. As the
   *  code is finite, we only need to carefully design the abstract domain to
   *  be finitary.
   *
   *  We also need to make sure that the computing function (i.e. the abstract
   *  interpreter) is monotone. Error handling breaks monotonicity of the
   *  abstract interpreter, because when an error happens, we always return
   *  the bottom value `Hot` for an expression. It is not a threat for
   *  termination because when an error happens, we stop the fixed point
   *  computation at the end of the iteration where the error happens. Care
   *  must be paid to tests of errors, monotonicity will be broken if we simply
   *  ignore the test errors (See `TryReporter`).
   *
   *  Note: It's tempting to use location of trees as key. That should
   *  be avoided as a template may have the same location as its single
   *  statement body. Macros may also create incorrect locations.
   */
  object Cache:
    /** Cache for expressions
     *
     *  Ref -> Tree -> Value
     *
     *  The first key is the value of `this` for the expression.
     *
     *  We do not need the heap in the key, because the value of an expression
     *  is only determined by the value of `this`. The heap is immutable: the
     *  abstract values for object fields never change within one iteration.
     *  The initial abstraction of a field is always a safe over-approximation
     *  thanks to monotonicity of initialization states.
     *
     *  If the heap is unstable in an iteration, the cache should also be
     *  unstable. This is because all values stored in the heap are also present
     *  in the cache. Therefore, we only need to track whether the cache is
     *  stable between two iterations.
     *
     *  The heap is not part of the fixed point computation -- we throw the
     *  unstable heap from last iteration away. In contrast, we use the unstable
     *  output cache from the last iteration as input for the next iteration.
     *  This is safe because the heap is determined by the cache -- it is a
     *  "local" data to the computing function, conceptually. Local data is
     *  always safe be discarded.
     *
     *  Now, if a fixed point is reached, the local data contains stable data
     *  that could be reused to check other classes. We employ this trick to
     *  improve performance of the analysis.
     */
    private type ExprValueCache = Map[Value, Map[TreeWrapper, Value]]

    /** The heap for abstract objects
     *
     *  The heap objects are immutable.
     */
    private type Heap = Map[Ref, Objekt]

    /** A wrapper for trees for storage in maps based on referential equality of trees. */
    private abstract class TreeWrapper:
      def tree: Tree

      override final def equals(other: Any): Boolean =
        other match
        case that: TreeWrapper => this.tree eq that.tree
        case _ => false

      override final def hashCode = tree.hashCode

    /** The immutable wrapper is intended to be stored as key in the heap. */
    private class ImmutableTreeWrapper(val tree: Tree) extends TreeWrapper

    /** For queries on the heap, reuse the same wrapper to avoid unnecessary allocation.
     *
     *  A `MutableTreeWrapper` is only ever used temporarily for querying a map,
     *  and is never inserted to the map.
     */
    private class MutableTreeWrapper extends TreeWrapper:
      var queryTree: Tree | Null = null
      def tree: Tree = queryTree match
        case tree: Tree => tree
        case null => ???

    class Cache:
      /** The cache for expression values from last iteration */
      private var last: ExprValueCache =  Map.empty

      /** The output cache for expression values
       *
       *  The output cache is computed based on the cache values `last` from the
       *  last iteration.
       *
       *  Both `last` and `current` are required to make sure an encountered
       *  expression is evaluated once in each iteration.
       */
      private var current: ExprValueCache = Map.empty

      /** Global cached values for expressions
       *
       *  The values are only added when a fixed point is reached.
       *
       *  It is intended to improve performance for computation related to warm values.
       */
      private var stable: ExprValueCache = Map.empty

      /** Whether the current heap is different from the last heap?
       *
       *  `changed == false` implies that the fixed point has been reached.
       */
      private var changed: Boolean = false

      /** Abstract heap stores abstract objects
       *
       *  The heap serves as cache of summaries for warm objects and is shared for checking all classes.
       *
       *  The fact that objects of `ThisRef` are stored in heap is just an engineering convenience.
       *  Technically, we can also store the object directly in `ThisRef`.
       *
       *  The heap contains objects of two conceptually distinct kinds.
       *
       *  - Objects that are also in `heapStable` are flow-insensitive views of already initialized objects that are
       *    cached for reuse in analysis of later classes. These objects and their fields should never change; this is
       *    enforced using assertions.
       *
       *  - Objects that are not (yet) in `heapStable` are the flow-sensitive abstract state of objects being analyzed
       *    in the current iteration of the analysis of the current class. Their fields do change flow-sensitively: more
       *    fields are added as fields become initialized. These objects are valid only within the current iteration and
       *    are removed when moving to a new iteration of analyzing the current class. When the analysis of a class
       *    reaches a fixed point, these now stable flow-sensitive views of the object at the end of the constructor
       *    of the analyzed class now become the flow-insensitive views of already initialized objects and can therefore
       *    be added to `heapStable`.
       */
      private var heap: Heap = Map.empty

      /** Used to revert heap to last stable heap. */
      private var heapStable: Heap = Map.empty

      /** Used to avoid allocation, its state does not matter */
      private given MutableTreeWrapper = new MutableTreeWrapper

      def hasChanged = changed

      def get(value: Value, expr: Tree): Option[Value] =
        current.get(value, expr) match
        case None => stable.get(value, expr)
        case res => res

      /** Backup the state of the cache
       *
       *  All the shared data structures must be immutable.
       */
      def backup(): Cache =
        val cache = new Cache
        cache.last = this.last
        cache.current = this.current
        cache.stable = this.stable
        cache.heap = this.heap
        cache.heapStable = this.heapStable
        cache.changed = this.changed
        cache

      /** Restore state from a backup */
      def restore(cache: Cache) =
        this.last = cache.last
        this.current = cache.current
        this.stable = cache.stable
        this.heap = cache.heap
        this.heapStable = cache.heapStable
        this.changed = cache.changed

      /** Copy the value of `(value, expr)` from the last cache to the current cache
       *
       *  It assumes the value is `Hot` if it doesn't exist in the last cache.
       *
       *  It updates the current caches if the values change.
       *
       *  The two caches are required because we want to make sure in a new iteration, an expression is evaluated once.
       */
      def assume(value: Value, expr: Tree, cacheResult: Boolean)(fun: => Value): Contextual[Value] =
        val assumeValue: Value =
          last.get(value, expr) match
          case Some(value) => value
          case None =>
            this.last = last.updatedNested(value, expr, Hot)
            Hot

        this.current = current.updatedNested(value, expr, assumeValue)

        val actual = fun
        if actual != assumeValue then
          this.changed = true
          this.current = this.current.updatedNested(value, expr, actual)
        else
          // It's tempting to cache the value in stable, but it's unsound.
          // The reason is that the current value may depend on other values
          // which might change.
          //
          // stable.put(value, expr, actual)
          ()
        end if

        actual
      end assume

      /** Commit current cache to stable cache. */
      private def commitToStableCache() =
        for
          (v, m) <- current
          if v.isWarm          // It's useless to cache value for ThisRef.
          (wrapper, res) <- m
        do
          this.stable = stable.updatedNestedWrapper(v, wrapper.asInstanceOf[ImmutableTreeWrapper], res)

      /** Prepare cache for the next iteration
       *
       *  1. Reset changed flag.
       *
       *  2. Use current cache as last cache and set current cache to be empty.
       *
       *  3. Revert heap to stable.
       */
      def prepareForNextIteration()(using Context) =
        this.changed = false
        this.last = this.current
        this.current = Map.empty
        this.heap = this.heapStable

      /** Prepare for checking next class
       *
       *  1. Reset changed flag.
       *
       *  2. Commit current cache to stable cache if not changed.
       *
       *  3. Update stable heap if not changed.
       *
       *  4. Reset last cache.
       */
      def prepareForNextClass()(using Context) =
        if this.changed then
          this.changed = false
          this.heap = this.heapStable
        else
          this.commitToStableCache()
          this.heapStable = this.heap

        this.last = Map.empty
        this.current = Map.empty

      def updateObject(ref: Ref, obj: Objekt) =
        assert(!this.heapStable.contains(ref))
        this.heap = this.heap.updated(ref, obj)

      def containsObject(ref: Ref) = heap.contains(ref)

      def getObject(ref: Ref) = heap(ref)
    end Cache

    extension (cache: ExprValueCache)
      private def get(value: Value, expr: Tree)(using queryWrapper: MutableTreeWrapper): Option[Value] =
        queryWrapper.queryTree = expr
        cache.get(value).flatMap(_.get(queryWrapper))

      private def removed(value: Value, expr: Tree)(using queryWrapper: MutableTreeWrapper) =
        queryWrapper.queryTree = expr
        val innerMap2 = cache(value).removed(queryWrapper)
        cache.updated(value, innerMap2)

      private def updatedNested(value: Value, expr: Tree, result: Value): ExprValueCache =
        val wrapper = new ImmutableTreeWrapper(expr)
        updatedNestedWrapper(value, wrapper, result)

      private def updatedNestedWrapper(value: Value, wrapper: ImmutableTreeWrapper, result: Value): ExprValueCache =
        val innerMap = cache.getOrElse(value, Map.empty[TreeWrapper, Value])
        val innerMap2 = innerMap.updated(wrapper, result)
        cache.updated(value, innerMap2)
    end extension
  end Cache

  import Cache.*

  inline def cache(using c: Cache): Cache = c

// ----- Checker State -----------------------------------

  /** The state that threads through the interpreter */
  type Contextual[T] = (Context, Trace, Promoted, Cache, Reporter) ?=> T

// ----- Error Handling -----------------------------------

  object Trace:
    opaque type Trace = Vector[Tree]

    val empty: Trace = Vector.empty

    extension (trace: Trace)
      def add(node: Tree): Trace = trace :+ node
      def toVector: Vector[Tree] = trace

    def show(using trace: Trace, ctx: Context): String = buildStacktrace(trace, "\n")

    def position(using trace: Trace): Tree = trace.last
  type Trace = Trace.Trace

  import Trace.*
  def trace(using t: Trace): Trace = t
  inline def withTrace[T](t: Trace)(op: Trace ?=> T): T = op(using t)
  inline def extendTrace[T](node: Tree)(using t: Trace)(op: Trace ?=> T): T = op(using t.add(node))

  /** Error reporting */
  trait Reporter:
    def report(err: Error): Unit
    def reportAll(errs: Seq[Error]): Unit = for err <- errs do report(err)

  /** A TryReporter cannot be simply thrown away
   *
   *  Either `abort` should be called or the errors be reported.
   *
   *  If errors are ignored and `abort` is not called, the monotonicity of the
   *  computation function is not guaranteed, thus termination of fixed-point
   *  computation becomes a problem.
   */
  trait TryReporter extends Reporter:
    /**
     * Revert the cache to previous state.
     */
    def abort()(using Cache): Unit
    def errors: List[Error]

  object Reporter:
    class BufferedReporter extends Reporter:
      private val buf = new mutable.ArrayBuffer[Error]
      def errors = buf.toList
      def report(err: Error) = buf += err

    class TryBufferedReporter(backup: Cache) extends BufferedReporter with TryReporter:
      def abort()(using Cache): Unit = cache.restore(backup)

    class ErrorFound(val error: Error) extends Exception
    class StopEarlyReporter extends Reporter:
      def report(err: Error) = throw new ErrorFound(err)

    /** Capture all errors with a TryReporter
     *
     *  The TryReporter cannot be thrown away: either `abort` must be called or
     *  the errors must be reported.
     */
    def errorsIn(fn: Reporter ?=> Unit)(using Cache): TryReporter =
      val reporter = new TryBufferedReporter(cache.backup())
      fn(using reporter)
      reporter

    /** Stop on first error */
    def stopEarly(fn: Reporter ?=> Unit): List[Error] =
      val reporter: Reporter = new StopEarlyReporter

      try
        fn(using reporter)
        Nil
      catch case ex: ErrorFound =>
        ex.error :: Nil

    def hasErrors(fn: Reporter ?=> Unit)(using Cache): Boolean =
      val backup = cache.backup()
      val errors = stopEarly(fn)
      cache.restore(backup)
      errors.nonEmpty

  inline def reporter(using r: Reporter): Reporter = r

// ----- Operations on domains -----------------------------
  extension (a: Value)
    def join(b: Value): Value =
      (a, b) match
      case (Hot, _)  => b
      case (_, Hot)  => a

      case (Cold, _) => Cold
      case (_, Cold) => Cold

      case (a: (Fun | Warm | ThisRef), b: (Fun | Warm | ThisRef)) =>
        if a == b then a else RefSet(a :: b :: Nil)

      case (a: (Fun | Warm | ThisRef), RefSet(refs)) =>
        if refs.exists(_ == a) then b: Value // fix pickling test
        else RefSet(a :: refs)

      case (RefSet(refs), b: (Fun | Warm | ThisRef)) =>
        if refs.exists(_ == b) then a: Value // fix pickling test
        else RefSet(b :: refs)

      case (RefSet(refs1), RefSet(refs2)) =>
        val diff = refs2.filter(ref => refs1.forall(_ != ref))
        RefSet(refs1 ++ diff)

    /** Conservatively approximate the value with `Cold` or `Hot` */
    def widenArg: Contextual[Value] =
      a match
      case _: Ref | _: Fun =>
        val hasError = Reporter.hasErrors { a.promote("Argument cannot be promoted to hot") }
        if hasError then Cold else Hot

      case RefSet(refs) =>
        refs.map(_.widenArg).join

      case _ => a


  extension (values: Seq[Value])
    def join: Value =
      if values.isEmpty then Hot
      else values.reduce { (v1, v2) => v1.join(v2) }

    def widenArgs: Contextual[List[Value]] = values.map(_.widenArg).toList


  extension (ref: Ref)
    def objekt: Contextual[Objekt] =
      // TODO: improve performance
      ref match
        case warm: Warm => warm.ensureObjectExistsAndPopulated()
        case _ =>
      cache.getObject(ref)

    def ensureObjectExists()(using Cache): ref.type =
      if cache.containsObject(ref) then
        printer.println("object " + ref + " already exists")
        ref
      else
        ensureFresh()

    def ensureFresh()(using Cache): ref.type =
      val obj = Objekt(ref.klass, fields = Map.empty, outers = Map(ref.klass -> ref.outer))
      printer.println("reset object " + ref)
      cache.updateObject(ref, obj)
      ref

    /** Update field value of the abstract object
     *
     *  Invariant: fields are immutable and only set once
     */
    def updateField(field: Symbol, value: Value): Contextual[Unit] = log("set field " + field + " of " + ref + " to " + value) {
      val obj = objekt
      // We may reset the outers or params of a populated warm object.
      // This is the case if we need access the field of a warm object, which
      // requires population of parameters and outers; and later create an
      // instance of the exact warm object, whose initialization will reset
      // the outer and constructor parameters.
      //
      // See tests/init/neg/unsound1.scala
      val changed = !obj.hasField(field) || obj.field(field) != value
      def isParamUpdate = field.isOneOf(Flags.ParamAccessor | Flags.Param) && obj.field(field) == value
      assert(!obj.hasField(field) || isParamUpdate, field.show + " already init, new = " + value + ", old = " + obj.field(field) + ", ref = " + ref)
      val obj2 = obj.copy(fields = obj.fields.updated(field, value))
      if changed then cache.updateObject(ref, obj2)
    }

    /** Update the immediate outer of the given `klass` of the abstract object
     *
     *  Invariant: outers are immutable and only set once
     */
    def updateOuter(klass: ClassSymbol, value: Value): Contextual[Unit] = log("set outer " + klass + " of " + ref + " to " + value) {
      val obj = objekt
      // See the comment in `updateField` for setting the value twice.
      assert(!obj.hasOuter(klass) || obj.outer(klass) == value, klass.show + " already has outer, new = " + value + ", old = " + obj.outer(klass) + ", ref = " + ref)
      val obj2 = obj.copy(outers = obj.outers.updated(klass, value))
      cache.updateObject(ref, obj2)
    }
  end extension

  extension (value: Value)
    def ensureHot(msg: String): Contextual[Value] =
      value.promote(msg)
      value

    def select(field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = log("select " + field.show + ", this = " + value, printer, (_: Value).show) {
      if promoted.isCurrentObjectPromoted then Hot
      else value match {
        case Hot  =>
          Hot

        case Cold =>
          val error = AccessCold(field, trace.toVector)
          reporter.report(error)
          Hot

        case ref: Ref =>
          val target = if needResolve then resolve(ref.klass, field) else field
          if target.is(Flags.Lazy) then
            val rhs = target.defTree.asInstanceOf[ValDef].rhs
            eval(rhs, ref, target.owner.asClass, cacheResult = true)
          else if target.exists then
            val obj = ref.objekt
            if obj.hasField(target) then
              obj.field(target)
            else if ref.isInstanceOf[Warm] then
              assert(obj.klass.isSubClass(target.owner))
              if target.is(Flags.ParamAccessor) then
                // possible for trait parameters
                // see tests/init/neg/trait2.scala
                //
                // return `Hot` here, errors are reported in checking `ThisRef`
                Hot
              else if target.hasSource then
                val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs
                eval(rhs, ref, target.owner.asClass, cacheResult = true)
              else
                val error = CallUnknown(field, trace.toVector)
                reporter.report(error)
                Hot
            else
              val error = AccessNonInit(target, trace.toVector)
              reporter.report(error)
              Hot
          else
            if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then
              report.error("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position)
              Hot
            else
              // This is possible due to incorrect type cast.
              // See tests/init/pos/Type.scala
              Hot

        case fun: Fun =>
          report.error("[Internal error] unexpected tree in selecting a function, fun = " + fun.expr.show + Trace.show, fun.expr)
          Hot

        case RefSet(refs) =>
          refs.map(_.select(field, receiver)).join
      }
    }

    def call(meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) {
      def promoteArgs(): Contextual[Unit] = args.foreach(_.promote)

      def isSyntheticApply(meth: Symbol) =
        meth.is(Flags.Synthetic)
        && meth.name == nme.apply
        && meth.owner.is(Flags.Module)
        && meth.owner.companionClass.is(Flags.Case)

      def isAlwaysSafe(meth: Symbol) =
        (meth eq defn.Object_eq)
        || (meth eq defn.Object_ne)
        || (meth eq defn.Any_isInstanceOf)

      def checkArgsWithParametricity() =
        val methodType = atPhaseBeforeTransforms { meth.info.stripPoly }
        var allArgsHot = true
        val allParamTypes = methodType.paramInfoss.flatten.map(_.repeatedToSingle)
        val errors = allParamTypes.zip(args).flatMap { (info, arg) =>
          val tryReporter = Reporter.errorsIn { arg.promote }
          allArgsHot = allArgsHot && tryReporter.errors.isEmpty
          if tryReporter.errors.isEmpty then tryReporter.errors
          else
            info match
            case typeParamRef: TypeParamRef =>
              val bounds = typeParamRef.underlying.bounds
              val isWithinBounds = bounds.lo <:< defn.NothingType && defn.AnyType <:< bounds.hi
              def otherParamContains = allParamTypes.exists { param => param != typeParamRef && param.typeSymbol != defn.ClassTagClass && typeParamRef.occursIn(param) }
              // A non-hot method argument is allowed if the corresponding parameter type is a
              // type parameter T with Any as its upper bound and Nothing as its lower bound.
              // the other arguments should either correspond to a parameter type that is T
              // or that does not contain T as a component.
              if isWithinBounds && !otherParamContains then
                tryReporter.abort()
                Nil
              else
                tryReporter.errors
            case _ => tryReporter.errors
        }
        (errors, allArgsHot)

      // fast track if the current object is already initialized
      if promoted.isCurrentObjectPromoted then Hot
      else if isAlwaysSafe(meth) then Hot
      else if meth eq defn.Any_asInstanceOf then value
      else value match {
        case Hot  =>
          if isSyntheticApply(meth) && meth.hasSource then
            val klass = meth.owner.companionClass.asClass
            instantiate(klass, klass.primaryConstructor, args)
          else
            if receiver.typeSymbol.isStaticOwner then
              val (errors, allArgsHot) = checkArgsWithParametricity()
              if allArgsHot then
                Hot: Value
              else if errors.nonEmpty then
                reporter.reportAll(errors)
                Hot: Value
              else
                Cold: Value
            else
              promoteArgs()
              Hot

        case Cold =>
          promoteArgs()
          val error = CallCold(meth, trace.toVector)
          reporter.report(error)
          Hot

        case ref: Ref =>
          val isLocal = !meth.owner.isClass
          val target =
            if !needResolve then
              meth
            else if superType.exists then
              resolveSuper(ref.klass, superType, meth)
            else
              resolve(ref.klass, meth)

          if target.isOneOf(Flags.Method) then
            if target.hasSource then
              val cls = target.owner.enclosingClass.asClass
              val ddef = target.defTree.asInstanceOf[DefDef]
              val tryReporter = Reporter.errorsIn { promoteArgs() }
              // normal method call
              if tryReporter.errors.nonEmpty && isSyntheticApply(meth) then
                tryReporter.abort()
                val klass = meth.owner.companionClass.asClass
                val targetCls = klass.owner.lexicallyEnclosingClass.asClass
                val outer = resolveThis(targetCls, ref, meth.owner.asClass)
                outer.instantiate(klass, klass.primaryConstructor, args)
              else
                reporter.reportAll(tryReporter.errors)
                extendTrace(ddef) {
                  eval(ddef.rhs, ref, cls, cacheResult = true)
                }
            else if ref.canIgnoreMethodCall(target) then
              Hot
            else
              // no source code available
              promoteArgs()
              // try promoting the receiver as last resort
              val hasErrors = Reporter.hasErrors { ref.promote("try promote value to hot") }
              if hasErrors then
                val error = CallUnknown(target, trace.toVector)
                reporter.report(error)
              Hot
          else if target.exists then
            // method call resolves to a field
            val obj = ref.objekt
            if obj.hasField(target) then
              obj.field(target)
            else
              value.select(target, receiver, needResolve = false)
          else
            if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then
              report.error("Unexpected resolution failure: ref.klass = " + ref.klass.show + ", meth = " + meth.show + Trace.show, Trace.position)
              Hot
            else
              // This is possible due to incorrect type cast.
              // See tests/init/pos/Type.scala
              Hot

        case Fun(body, thisV, klass) =>
          // meth == NoSymbol for poly functions
          if meth.name.toString == "tupled" then value // a call like `fun.tupled`
          else
            promoteArgs()
            eval(body, thisV, klass, cacheResult = true)

        case RefSet(refs) =>
          refs.map(_.call(meth, args, receiver, superType)).join
      }
    }

    def callConstructor(ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("call " + ctor.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) {
      // init "fake" param fields for parameters of primary and secondary constructors
      def addParamsAsFields(args: List[Value], ref: Ref, ctorDef: DefDef) =
        val params = ctorDef.termParamss.flatten.map(_.symbol)
        assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size)
        for (param, value) <- params.zip(args) do
          ref.updateField(param, value)
          printer.println(param.show + " initialized with " + value)

      value match {
        case Hot | Cold | _: RefSet | _: Fun =>
          report.error("[Internal error] unexpected constructor call, meth = " + ctor + ", value = " + value + Trace.show, Trace.position)
          Hot

        case ref: Warm if ref.isPopulatingParams =>
          if ctor.hasSource then
            val cls = ctor.owner.enclosingClass.asClass
            val ddef = ctor.defTree.asInstanceOf[DefDef]
            val args2 = args.map(_.value).widenArgs
            addParamsAsFields(args2, ref, ddef)
            if ctor.isPrimaryConstructor then
              val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]
              extendTrace(cls.defTree) { init(tpl, ref, cls) }
            else
              val initCall = ddef.rhs match
                case Block(call :: _, _) => call
                case call => call
              extendTrace(ddef) { eval(initCall, ref, cls) }
            end if
          else
            Hot

        case ref: Ref =>
          if ctor.hasSource then
            val cls = ctor.owner.enclosingClass.asClass
            val ddef = ctor.defTree.asInstanceOf[DefDef]
            val args2 = args.map(_.value).widenArgs
            addParamsAsFields(args2, ref, ddef)
            if ctor.isPrimaryConstructor then
              val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template]
              extendTrace(cls.defTree) { eval(tpl, ref, cls, cacheResult = true) }
              ref
            else
              extendTrace(ddef) { eval(ddef.rhs, ref, cls, cacheResult = true) }
          else if ref.canIgnoreMethodCall(ctor) then
            Hot
          else
            // no source code available
            val error = CallUnknown(ctor, trace.toVector)
            reporter.report(error)
            Hot
      }

    }

    /** Handle a new expression `new p.C` where `p` is abstracted by `value` */
    def instantiate(klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("instantiating " + klass.show + ", value = " + value + ", args = " + args.map(_.value.show), printer, (_: Value).show) {
      def tryLeak(warm: Warm, nonHotOuterClass: Symbol, argValues: List[Value]): Contextual[Value] =
        val argInfos2 = args.zip(argValues).map { (argInfo, v) => argInfo.copy(value = v) }
        val errors = Reporter.stopEarly {
          given Trace = Trace.empty
          warm.callConstructor(ctor, argInfos2)
        }
        if errors.nonEmpty then
          val indices =
            for
              (arg, i) <- argValues.zipWithIndex
              if arg.isCold
            yield
              i + 1

          val error = UnsafeLeaking(trace.toVector, errors.head, nonHotOuterClass, indices)
          reporter.report(error)
          Hot
        else
          warm

      if promoted.isCurrentObjectPromoted then Hot
      else value match {
        case Hot  =>
          var allHot = true
          val args2 = args.map { arg =>
            val hasErrors = Reporter.hasErrors { arg.promote }
            allHot = allHot && !hasErrors
            if hasErrors then arg.value.widenArg
            else Hot
          }

          if allHot then
            Hot
          else
            val outer = Hot
            val warm = Warm(klass, outer, ctor, args2).ensureObjectExists()
            tryLeak(warm, NoSymbol, args2)

        case Cold =>
          val error = CallCold(ctor, trace.toVector)
          reporter.report(error)
          Hot

        case ref: Ref =>
          // widen the outer to finitize the domain
          val outer = ref match
            case warm @ Warm(_, _: Warm, _, _) =>
              // the widened warm object might not exist in the heap
              warm.copy(outer = Cold).ensureObjectExistsAndPopulated()
            case _ => ref

          val argsWidened = args.map(_.value).widenArgs
          val warm = Warm(klass, outer, ctor, argsWidened).ensureObjectExists()
          if argsWidened.exists(_.isCold) then
            tryLeak(warm, klass.owner.lexicallyEnclosingClass, argsWidened)
          else
            val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) }
            warm.callConstructor(ctor, argInfos2)
            warm

        case Fun(body, thisV, klass) =>
          report.error("[Internal error] unexpected tree in instantiating a function, fun = " + body.show + Trace.show, Trace.position)
          Hot

        case RefSet(refs) =>
          refs.map(_.instantiate(klass, ctor, args)).join
      }
    }
  end extension

  extension (ref: Ref)
    def accessLocal(tmref: TermRef, klass: ClassSymbol): Contextual[Value] =
      val sym = tmref.symbol

      if sym.is(Flags.Param) && sym.owner.isConstructor then
        val enclosingClass = sym.owner.enclosingClass.asClass
        val thisValue2 = resolveThis(enclosingClass, ref, klass)
        thisValue2 match
        case Hot => Hot
        case ref: Ref => ref.objekt.field(sym)
        case _ =>
            report.error("[Internal error] unexpected this value accessing local variable, sym = " + sym.show + ", thisValue = " + thisValue2.show + Trace.show, Trace.position)
            Hot
      else if sym.is(Flags.Param) then
        Hot
      else
        sym.defTree match {
          case vdef: ValDef =>
            // resolve this for local variable
            val enclosingClass = sym.owner.enclosingClass.asClass
            val thisValue2 = resolveThis(enclosingClass, ref, klass)
            thisValue2 match
              case Hot => Hot

              case Cold => Cold

              case ref: Ref => eval(vdef.rhs, ref, enclosingClass)

              case _ =>
                 report.error("[Internal error] unexpected this value when accessing local variable, sym = " + sym.show + ", thisValue = " + thisValue2.show + Trace.show, Trace.position)
                 Hot
            end match

          case _ => Hot
        }
  end extension

// ----- Promotion ----------------------------------------------------
  extension (ref: Ref)
    /** Whether the object is fully assigned
     *
     *  It means all fields and outers are set. For performance, we don't check
     *  outers here, because Scala semantics ensure that they are always set
     *  before any user code in the constructor.
     *
     *  Note that `isFullyFilled = true` does not mean we can use the
     *  object freely, as its fields or outers may still reach uninitialized
     *  objects.
     */
    def isFullyFilled: Contextual[Boolean] = log("isFullyFilled " + ref, printer) {
      val obj = ref.objekt
      ref.klass.baseClasses.forall { klass =>
        !klass.hasSource || {
          val nonInits = klass.info.decls.filter { member =>
            !member.isOneOf(Flags.Method | Flags.Lazy | Flags.Deferred)
            && !member.isType
            && !obj.hasField(member)
          }
          printer.println("nonInits = " + nonInits)
          nonInits.isEmpty
        }
      }
    }

    def nonInitFields(): Contextual[List[Symbol]] =
      val obj = ref.objekt
      ref.klass.baseClasses.flatMap { klass =>
        if klass.hasSource then
          klass.info.decls.filter { member =>
            !member.isOneOf(Flags.Method | Flags.Lazy | Flags.Deferred)
            && !member.isType
            && !obj.hasField(member)
          }
        else
          Nil
      }

  end extension

  extension (thisRef: ThisRef)
    def tryPromoteCurrentObject(): Contextual[Boolean] = log("tryPromoteCurrentObject ", printer) {
      if promoted.isCurrentObjectPromoted then
        true
      else if thisRef.isFullyFilled then
        // If we have all fields initialized, then we can promote This to hot.
        promoted.promoteCurrent(thisRef)
        true
      else
        false
    }

  extension (value: Value)
    /** Promotion of values to hot */
    def promote(msg: String): Contextual[Unit] = log("promoting " + value + ", promoted = " + promoted, printer) {
      if !promoted.isCurrentObjectPromoted then

        value match
        case Hot   =>

        case Cold  =>
          reporter.report(PromoteError(msg, trace.toVector))

        case thisRef: ThisRef =>
          val emptyFields = thisRef.nonInitFields()
          if emptyFields.isEmpty then
            promoted.promoteCurrent(thisRef)
          else
            val fields = "Non initialized field(s): " + emptyFields.map(_.show).mkString(", ") + "."
            reporter.report(PromoteError(msg + "\n" + fields, trace.toVector))

        case warm: Warm =>
          if !promoted.contains(warm) then
            promoted.add(warm)
            val errors = warm.tryPromote(msg)
            if errors.nonEmpty then promoted.remove(warm)
            reporter.reportAll(errors)

        case fun @ Fun(body, thisV, klass) =>
          if !promoted.contains(fun) then
            val errors = Reporter.stopEarly {
              val res = {
                given Trace = Trace.empty
                eval(body, thisV, klass)
              }
              given Trace = Trace.empty.add(body)
              res.promote("The function return value is not hot. Found = " + res.show + ". ")
            }
            if errors.nonEmpty then
              reporter.report(UnsafePromotion(msg, trace.toVector, errors.head))
            else
              promoted.add(fun)

        case RefSet(refs) =>
          refs.foreach(_.promote(msg))
    }
  end extension

  extension (warm: Warm)
    /** Try early promotion of warm objects
     *
     *  Promotion is expensive and should only be performed for small classes.
     *
     *  1. for each concrete method `m` of the warm object:
     *     call the method and promote the result
     *
     *  2. for each concrete field `f` of the warm object:
     *     promote the field value
     *
     *  If the object contains nested classes as members, the checker simply
     *  reports a warning to avoid expensive checks.
     *
     *  TODO: we need to revisit whether this is needed once we make the
     *  system more flexible in other dimentions: e.g. leak to
     *  methods or constructors, or use ownership for creating cold data structures.
     */
    def tryPromote(msg: String): Contextual[List[Error]] = log("promote " + warm.show + ", promoted = " + promoted, printer) {
      val classRef = warm.klass.appliedRef
      val hasInnerClass = classRef.memberClasses.filter(_.symbol.hasSource).nonEmpty
      if hasInnerClass then
        return PromoteError(msg + "Promotion cancelled as the value contains inner classes. ", trace.toVector) :: Nil

      val errors = Reporter.stopEarly {
        for klass <- warm.klass.baseClasses if klass.hasSource do
          val obj = warm.objekt
          val outer = obj.outer(klass)
          val ctor = klass.primaryConstructor
          val hotSegmentCountered = !ctor.hasSource || outer.isHot && {
            val ctorDef = ctor.defTree.asInstanceOf[DefDef]
            val params = ctorDef.termParamss.flatten.map(_.symbol)
            // We have cached all parameters on the object
            params.forall(param => obj.field(param).isHot)
          }

          // If the outer and parameters of a class are all hot, then accessing fields and methods of the current
          // segment of the object should be OK. They may only create problems via virtual method calls on `this`, but
          // those methods are checked as part of the check for the class where they are defined.
          if !hotSegmentCountered then
            for member <- klass.info.decls do
              if !member.isType && !member.isConstructor && member.hasSource  && !member.is(Flags.Deferred) then
                if member.is(Flags.Method, butNot = Flags.Accessor) then
                  withTrace(Trace.empty) {
                    val args = member.info.paramInfoss.flatten.map(_ => ArgInfo(Hot, Trace.empty))
                    val res = warm.call(member, args, receiver = warm.klass.typeRef, superType = NoType)
                    res.promote("Cannot prove that the return value of " + member.show + " is hot. Found = " + res.show + ". ")
                  }
                else
                  withTrace(Trace.empty) {
                    val res = warm.select(member, receiver = warm.klass.typeRef)
                    res.promote("Cannot prove that the field " + member.show + " is hot. Found = " + res.show + ". ")
                  }
            end for
        end for
      }

      if errors.isEmpty then Nil
      else UnsafePromotion(msg, trace.toVector, errors.head) :: Nil
    }

  end extension

// ----- Policies ------------------------------------------------------
  extension (value: Ref)
    /** Can the method call on `value` be ignored?
     *
     *  Note: assume overriding resolution has been performed.
     */
    def canIgnoreMethodCall(meth: Symbol)(using Context): Boolean =
      val cls = meth.owner
      cls == defn.AnyClass ||
      cls == defn.AnyValClass ||
      cls == defn.ObjectClass

// ----- Work list ---------------------------------------------------
  case class Task(value: ThisRef)

  class WorkList private[Semantic]():
    private val pendingTasks: mutable.ArrayBuffer[Task] = new mutable.ArrayBuffer
{'fraction_non_alphanumeric': 0.09661583908944785, 'fraction_numerical': 0.0009991745948998653, 'mean_word_length': 2.0580205911657257, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 182, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 18, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '978621', 'n_tokens_mistral': 25252, 'n_tokens_neox': 24866, 'n_words': 8466}