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

dotty.tools.dotc.cc.CheckCaptures.scala Maven / Gradle / Ivy

package dotty.tools
package dotc
package cc

import core.*
import Phases.*, DenotTransformers.*, SymDenotations.*
import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.*
import Types.*, StdNames.*, Denotations.*
import config.Printers.{capt, recheckr, noPrinter}
import config.{Config, Feature}
import ast.{tpd, untpd, Trees}
import Trees.*
import typer.RefChecks.{checkAllOverrides, checkSelfAgainstParents, OverridingPairsChecker}
import typer.Checking.{checkBounds, checkAppliedTypesIn}
import typer.ErrorReporting.{Addenda, NothingToAdd, err}
import typer.ProtoTypes.{LhsProto, WildcardSelectionProto}
import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property}
import transform.{Recheck, PreRecheck, CapturedVars}
import Recheck.*
import scala.collection.mutable
import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult}
import CCState.*
import StdNames.nme
import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind}
import reporting.{trace, Message, OverrideError}

/** The capture checker */
object CheckCaptures:
  import ast.tpd.*

  val name: String = "cc"
  val description: String = "capture checking"

  enum EnvKind:
    case Regular        // normal case
    case NestedInOwner  // environment is a temporary one nested in the owner's environment,
                        // and does not have a different actual owner symbol
                        // (this happens when doing box adaptation).
    case ClosureResult  // environment is for the result of a closure
    case Boxed          // environment is inside a box (in which case references are not counted)

  /** A class describing environments.
   *  @param owner     the current owner
   *  @param kind      the environment's kind
   *  @param captured  the capture set containing all references to tracked free variables outside of boxes
   *  @param outer0    the next enclosing environment
   */
  case class Env(
      owner: Symbol,
      kind: EnvKind,
      captured: CaptureSet,
      outer0: Env | Null):

    def outer = outer0.nn

    def isOutermost = outer0 == null

    /** If an environment is open it tracks free references */
    def isOpen = !captured.isAlwaysEmpty && kind != EnvKind.Boxed

    def outersIterator: Iterator[Env] = new:
      private var cur = Env.this
      def hasNext = !cur.isOutermost
      def next(): Env =
        val res = cur
        cur = cur.outer
        res

    def ownerString(using Context): String =
      if owner.isAnonymousFunction then "enclosing function" else owner.show
  end Env

  /** Similar normal substParams, but this is an approximating type map that
   *  maps parameters in contravariant capture sets to the empty set.
   */
  final class SubstParamsMap(from: BindingType, to: List[Type])(using Context)
  extends ApproximatingTypeMap, IdempotentCaptRefMap:
    def apply(tp: Type): Type =
      tp match
        case tp: ParamRef =>
          if tp.binder == from then to(tp.paramNum) else tp
        case tp: NamedType =>
          if tp.prefix `eq` NoPrefix then tp
          else tp.derivedSelect(apply(tp.prefix))
        case _: ThisType =>
          tp
        case _ =>
          mapOver(tp)
  end SubstParamsMap

  final class SubstParamsBiMap(from: LambdaType, to: List[Type])(using Context)
  extends BiTypeMap:
    thisMap =>

    def apply(tp: Type): Type = tp match
      case tp: ParamRef =>
        if tp.binder == from then to(tp.paramNum) else tp
      case tp: NamedType =>
        if tp.prefix `eq` NoPrefix then tp
        else tp.derivedSelect(apply(tp.prefix))
      case _: ThisType =>
        tp
      case _ =>
        mapOver(tp)

    lazy val inverse = new BiTypeMap:
      def apply(tp: Type): Type = tp match
        case tp: NamedType =>
          var idx = 0
          var to1 = to
          while idx < to.length && (tp ne to(idx)) do
            idx += 1
            to1 = to1.tail
          if idx < to.length then from.paramRefs(idx)
          else if tp.prefix `eq` NoPrefix then tp
          else tp.derivedSelect(apply(tp.prefix))
        case _: ThisType =>
          tp
        case _ =>
          mapOver(tp)
      def inverse = thisMap
  end SubstParamsBiMap

  /** Check that a @retains annotation only mentions references that can be tracked.
   *  This check is performed at Typer.
   */
  def checkWellformed(parent: Tree, ann: Tree)(using Context): Unit =
    def check(elem: Tree, pos: SrcPos): Unit = elem.tpe match
      case ref: CaptureRef =>
        if !ref.isTrackableRef then
          report.error(em"$elem cannot be tracked since it is not a parameter or local value", pos)
      case tpe =>
        report.error(em"$elem: $tpe is not a legal element of a capture set", pos)
    for elem <- ann.retainedElems do
      elem match
        case CapsOfApply(arg) =>
          def isLegalCapsOfArg =
            arg.symbol.isAbstractOrParamType && arg.symbol.info.derivesFrom(defn.Caps_CapSet)
          if !isLegalCapsOfArg then
            report.error(
              em"""$arg is not a legal prefix for `^` here,
                  |is must be a type parameter or abstract type with a caps.CapSet upper bound.""",
              elem.srcPos)
        case ReachCapabilityApply(arg) => check(arg, elem.srcPos)
        case _ => check(elem, elem.srcPos)

  /** Report an error if some part of `tp` contains the root capability in its capture set
   *  or if it refers to an unsealed type parameter that could possibly be instantiated with
   *  cap in a way that's visible at the type.
   */
  private def disallowRootCapabilitiesIn(tp: Type, carrier: Symbol, what: String, have: String, addendum: String, pos: SrcPos)(using Context) =
    val check = new TypeTraverser:

      private val seen = new EqHashSet[TypeRef]

      def traverse(t: Type) =
        t.dealiasKeepAnnots match
          case t: TypeRef =>
            if !seen.contains(t) then
              seen += t
              traverseChildren(t)

              // Check the lower bound of path dependent types.
              // See issue #19330.
              val isMember = t.prefix ne NoPrefix
              t.info match
                case TypeBounds(lo, _) if isMember => traverse(lo)
                case _ =>
          case AnnotatedType(_, ann) if ann.symbol == defn.UncheckedCapturesAnnot =>
            ()
          case CapturingType(parent, refs) =>
            if variance >= 0 then
              refs.disallowRootCapability: () =>
                def part = if t eq tp then "" else i"the part $t of "
                report.error(
                  em"""$what cannot $have $tp since
                      |${part}that type captures the root capability `cap`.
                      |$addendum""",
                  pos)
            traverse(parent)
          case t =>
            traverseChildren(t)
    if ccConfig.useSealed then check.traverse(tp)
  end disallowRootCapabilitiesIn

  /** Attachment key for bodies of closures, provided they are values */
  val ClosureBodyValue = Property.Key[Unit]

  /** A prototype that indicates selection with an immutable value */
  class PathSelectionProto(val sym: Symbol, val pt: Type)(using Context) extends WildcardSelectionProto

class CheckCaptures extends Recheck, SymTransformer:
  thisPhase =>

  import ast.tpd.*
  import CheckCaptures.*

  override def phaseName: String = CheckCaptures.name

  override def description: String = CheckCaptures.description

  override def isRunnable(using Context) = super.isRunnable && Feature.ccEnabledSomewhere

  def newRechecker()(using Context) = CaptureChecker(ctx)

  override def run(using Context): Unit =
    if Feature.ccEnabled then
      super.run

  val ccState1 = new CCState // Dotty problem: Rename to ccState ==> Crash in ExplicitOuter

  class CaptureChecker(ictx: Context) extends Rechecker(ictx):

    override def keepType(tree: Tree) =
      super.keepType(tree)
      || tree.isInstanceOf[Try]  // type of `try` needs tp be checked for * escapes

    /** Instantiate capture set variables appearing contra-variantly to their
     *  upper approximation.
     */
    private def interpolator(startingVariance: Int = 1)(using Context) = new TypeTraverser:
      variance = startingVariance
      override def traverse(t: Type) = t match
        case t @ CapturingType(parent, refs) =>
          refs match
            case refs: CaptureSet.Var if variance < 0 => refs.solve()
            case _ =>
          traverse(parent)
        case t @ defn.RefinedFunctionOf(rinfo) =>
          traverse(rinfo)
        case _ =>
          traverseChildren(t)

    /** If `tpt` is an inferred type, interpolate capture set variables appearing contra-
     *  variantly in it.
     */
    private def interpolateVarsIn(tpt: Tree)(using Context): Unit =
      if tpt.isInstanceOf[InferredTypeTree] then
        interpolator().traverse(tpt.knownType)
          .showing(i"solved vars in ${tpt.knownType}", capt)
      for msg <- ccState.approxWarnings do
        report.warning(msg, tpt.srcPos)
      ccState.approxWarnings.clear()

    /** Assert subcapturing `cs1 <: cs2` */
    def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) =
      assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2")

    def checkOK(res: CompareResult, prefix: => String, pos: SrcPos, provenance: => String = "")(using Context): Unit =
      if !res.isOK then
        def toAdd: String = CaptureSet.levelErrors.toAdd.mkString
        def descr: String =
          val d = res.blocking.description
          if d.isEmpty then provenance else ""
        report.error(em"$prefix included in the allowed capture set ${res.blocking}$descr$toAdd", pos)

    /** Check subcapturing `{elem} <: cs`, report error on failure */
    def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos, provenance: => String = "")(using Context) =
      checkOK(
          elem.singletonCaptureSet.subCaptures(cs, frozen = false),
          i"$elem cannot be referenced here; it is not",
          pos, provenance)

    /** Check subcapturing `cs1 <: cs2`, report error on failure */
    def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos,
        provenance: => String = "", cs1description: String = "")(using Context) =
      checkOK(
          cs1.subCaptures(cs2, frozen = false),
          if cs1.elems.size == 1 then i"reference ${cs1.elems.toList.head}$cs1description is not"
          else i"references $cs1$cs1description are not all",
          pos, provenance)

    def showRef(ref: CaptureRef)(using Context): String =
        ctx.printer.toTextCaptureRef(ref).show

    // Uses 4-space indent as a trial
    private def checkReachCapsIsolated(tpe: Type, pos: SrcPos)(using Context): Unit =

        object checker extends TypeTraverser:
            var refVariances: Map[Boolean, Int] = Map.empty
            var seenReach: CaptureRef | Null = null
            def traverse(tp: Type) =
                tp.dealias match
                case CapturingType(parent, refs) =>
                    traverse(parent)
                    for ref <- refs.elems do
                        if ref.isReach && !ref.stripReach.isInstanceOf[TermParamRef]
                            || ref.isRootCapability
                        then
                            val isReach = ref.isReach
                            def register() =
                                refVariances = refVariances.updated(isReach, variance)
                                seenReach = ref
                            refVariances.get(isReach) match
                                case None => register()
                                case Some(v) => if v != 0 && variance == 0 then register()
                case _ =>
                    traverseChildren(tp)

        checker.traverse(tpe)
        if checker.refVariances.size == 2
            && checker.refVariances(true) >= 0
            && checker.refVariances(false) <= 0
        then
            report.error(
                em"""Reach capability ${showRef(checker.seenReach.nn)} and universal capability cap cannot both
                    |appear in the type $tpe of this expression""",
                pos)
    end checkReachCapsIsolated

    /** The current environment */
    private val rootEnv: Env = inContext(ictx):
      Env(defn.RootClass, EnvKind.Regular, CaptureSet.empty, null)
    private var curEnv = rootEnv

    /** Currently checked closures and their expected types, used for error reporting */
    private var openClosures: List[(Symbol, Type)] = Nil

    private val myCapturedVars: util.EqHashMap[Symbol, CaptureSet] = EqHashMap()

    /** A list of actions to perform at postCheck. The reason to defer these actions
     *  is that it is sometimes better for type inference to not constrain too early
     *  with a checkConformsExpr.
     */
    private var todoAtPostCheck = new mutable.ListBuffer[() => Unit]

    /** If `sym` is a class or method nested inside a term, a capture set variable representing
     *  the captured variables of the environment associated with `sym`.
     */
    def capturedVars(sym: Symbol)(using Context): CaptureSet =
      myCapturedVars.getOrElseUpdate(sym,
        if sym.ownersIterator.exists(_.isTerm)
        then CaptureSet.Var(sym.owner, level = sym.ccLevel)
        else CaptureSet.empty)

    /** For all nested environments up to `limit` or a closed environment perform `op`,
     *  but skip environmenrts directly enclosing environments of kind ClosureResult.
     */
    def forallOuterEnvsUpTo(limit: Symbol)(op: Env => Unit)(using Context): Unit =
      def recur(env: Env, skip: Boolean): Unit =
        if env.isOpen && env.owner != limit then
          if !skip then op(env)
          if !env.isOutermost then
            var nextEnv = env.outer
            if env.owner.isConstructor then
              if nextEnv.owner != limit && !nextEnv.isOutermost then
                nextEnv = nextEnv.outer
            recur(nextEnv, skip = env.kind == EnvKind.ClosureResult)
      recur(curEnv, skip = false)

    /** A description where this environment comes from */
    private def provenance(env: Env)(using Context): String =
      val owner = env.owner
      if owner.isAnonymousFunction then
        val expected = openClosures
          .find(_._1 == owner)
          .map(_._2)
          .getOrElse(owner.info.toFunctionType())
        i"\nof an enclosing function literal with expected type $expected"
      else
        i"\nof the enclosing ${owner.showLocated}"


    /** Include `sym` in the capture sets of all enclosing environments nested in the
     *  the environment in which `sym` is defined.
     */
    def markFree(sym: Symbol, pos: SrcPos)(using Context): Unit =
      markFree(sym, sym.termRef, pos)

    def markFree(sym: Symbol, ref: TermRef, pos: SrcPos)(using Context): Unit =
      if sym.exists && ref.isTracked then
        forallOuterEnvsUpTo(sym.enclosure): env =>
          capt.println(i"Mark $sym with cs ${ref.captureSet} free in ${env.owner}")
          checkElem(ref, env.captured, pos, provenance(env))

    /** Make sure (projected) `cs` is a subset of the capture sets of all enclosing
     *  environments. At each stage, only include references from `cs` that are outside
     *  the environment's owner
     */
    def markFree(cs: CaptureSet, pos: SrcPos)(using Context): Unit =
      // A captured reference with the symbol `sym` is visible from the environment
      // if `sym` is not defined inside the owner of the environment.
      inline def isVisibleFromEnv(sym: Symbol, env: Env) =
        if env.kind == EnvKind.NestedInOwner then
          !sym.isProperlyContainedIn(env.owner)
        else
          !sym.isContainedIn(env.owner)

      def checkSubsetEnv(cs: CaptureSet, env: Env)(using Context): Unit =
        // Only captured references that are visible from the environment
        // should be included.
        val included = cs.filter: c =>
          c.stripReach match
            case ref: NamedType =>
              val refSym = ref.symbol
              val refOwner = refSym.owner
              val isVisible = isVisibleFromEnv(refOwner, env)
              if isVisible && !ref.isRootCapability then
                ref match
                  case ref: TermRef if ref.prefix `ne` NoPrefix =>
                    // If c is a path of a class defined outside the environment,
                    // we check the capture set of its info.
                    checkSubsetEnv(ref.captureSetOfInfo, env)
                  case _ =>
              if !isVisible
                  && (c.isReach || ref.isType)
                  && (!ccConfig.useSealed || refSym.is(Param))
                  && refOwner == env.owner
              then
                if refSym.hasAnnotation(defn.UnboxAnnot) then
                  capt.println(i"exempt: $ref in $refOwner")
                else
                  // Reach capabilities that go out of scope have to be approximated
                  // by their underlying capture set, which cannot be universal.
                  // Reach capabilities of @unboxed parameters are exempted.
                  val cs = CaptureSet.ofInfo(c)
                  cs.disallowRootCapability: () =>
                    report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", pos)
                  checkSubset(cs, env.captured, pos, provenance(env))
              isVisible
            case ref: ThisType => isVisibleFromEnv(ref.cls, env)
            case _ => false
        checkSubset(included, env.captured, pos, provenance(env))
        capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}")

      if !cs.isAlwaysEmpty then
        forallOuterEnvsUpTo(ctx.owner.topLevelClass): env =>
          checkSubsetEnv(cs, env)
    end markFree

    /** Include references captured by the called method in the current environment stack */
    def includeCallCaptures(sym: Symbol, pos: SrcPos)(using Context): Unit =
      if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos)

    private val prefixCalls = util.EqHashSet[GenericApply]()
    private val unboxedArgs = util.EqHashSet[Tree]()

    def handleCall(meth: Symbol, call: GenericApply, eval: () => Type)(using Context): Type =
      if prefixCalls.remove(call) then return eval()

      val unboxedParamNames =
        meth.rawParamss.flatMap: params =>
          params.collect:
            case param if param.hasAnnotation(defn.UnboxAnnot) =>
              param.name
        .toSet

      def markUnboxedArgs(call: GenericApply): Unit = call.fun.tpe.widen match
        case MethodType(pnames) =>
          for (pname, arg) <- pnames.lazyZip(call.args) do
            if unboxedParamNames.contains(pname) then
              unboxedArgs.add(arg)
        case _ =>

      def markPrefixCalls(tree: Tree): Unit = tree match
        case tree: GenericApply =>
          prefixCalls.add(tree)
          markUnboxedArgs(tree)
          markPrefixCalls(tree.fun)
        case _ =>

      markUnboxedArgs(call)
      markPrefixCalls(call.fun)
      val res = eval()
      includeCallCaptures(meth, call.srcPos)
      res
    end handleCall

    override def recheckIdent(tree: Ident, pt: Type)(using Context): Type =
      if tree.symbol.is(Method) then
        if tree.symbol.info.isParameterless then
          // there won't be an apply; need to include call captures now
          includeCallCaptures(tree.symbol, tree.srcPos)
      else if !tree.symbol.isStatic then
        //debugShowEnvs()
        def addSelects(ref: TermRef, pt: Type): TermRef = pt match
          case pt: PathSelectionProto if ref.isTracked =>
            // if `ref` is not tracked then the selection could not give anything new
            // class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters.
            addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt)
          case _ => ref
        val ref = tree.symbol.termRef
        val pathRef = addSelects(ref, pt)
        //if pathRef ne ref then
        //  println(i"add selects $ref --> $pathRef")
        markFree(tree.symbol, if false then ref else pathRef, tree.srcPos)
      super.recheckIdent(tree, pt)

    override def selectionProto(tree: Select, pt: Type)(using Context): Type =
      val sym = tree.symbol
      if !sym.isOneOf(UnstableValueFlags) && !sym.isStatic then PathSelectionProto(sym, pt)
      else super.selectionProto(tree, pt)

    /** A specialized implementation of the selection rule.
     *
     *  E |- f: T{ m: R^Cr }^{f}
     *  ------------------------
     *  E |- f.m: R^C
     *
     *  The implementation picks as `C` one of `{f}` or `Cr`, depending on the
     *  outcome of a `mightSubcapture` test. It picks `{f}` if this might subcapture Cr
     *  and Cr otherwise.
     */
    override def recheckSelection(tree: Select, qualType: Type, name: Name, pt: Type)(using Context) = {
      def disambiguate(denot: Denotation): Denotation = denot match
        case MultiDenotation(denot1, denot2) =>
          // This case can arise when we try to merge multiple types that have different
          // capture sets on some part. For instance an asSeenFrom might produce
          // a bi-mapped capture set arising from a substition. Applying the same substitution
          // to the same type twice will nevertheless produce different capture sets which can
          // lead to a failure in disambiguation since neither alternative is better than the
          // other in a frozen constraint. An example test case is disambiguate-select.scala.
          // We address the problem by disambiguating while ignoring all capture sets as a fallback.
          withMode(Mode.IgnoreCaptures) {
            disambiguate(denot1).meet(disambiguate(denot2), qualType)
          }
        case _ => denot

      val selType = recheckSelection(tree, qualType, name, disambiguate)
      val selWiden = selType.widen

      if pt == LhsProto
          || qualType.isBoxedCapturing
          || selType.isTrackableRef
          || selWiden.isBoxedCapturing
          || selWiden.captureSet.isAlwaysEmpty
      then
        selType
      else
        val qualCs = qualType.captureSet
        val selCs = selType.captureSet
        capt.println(i"pick one of $qualType, ${selType.widen}, $qualCs, $selCs ${selWiden.captureSet} in $tree")

        if qualCs.mightSubcapture(selCs)
            && !selCs.mightSubcapture(qualCs)
            && !pt.stripCapturing.isInstanceOf[SingletonType]
        then
          selWiden.stripCapturing.capturing(qualCs)
            .showing(i"alternate type for select $tree: $selType --> $result, $qualCs / $selCs", capt)
        else
          selType
    }//.showing(i"recheck sel $tree, $qualType = $result")

    override def recheckApply(tree: Apply, pt: Type)(using Context): Type =
      val meth = tree.fun.symbol

      // Unsafe box/unbox handling, only for versions < 3.3
      def mapArgUsing(f: Type => Type) =
        val arg :: Nil = tree.args: @unchecked
        val argType0 = f(recheckStart(arg, pt))
        val argType = super.recheckFinish(argType0, arg, pt)
        super.recheckFinish(argType, tree, pt)

      if meth == defn.Caps_unsafeAssumePure then
        val arg :: Nil = tree.args: @unchecked
        val argType0 = recheck(arg, pt.capturing(CaptureSet.universal))
        val argType =
          if argType0.captureSet.isAlwaysEmpty then argType0
          else argType0.widen.stripCapturing
        capt.println(i"rechecking $arg with $pt: $argType")
        super.recheckFinish(argType, tree, pt)
      else if meth == defn.Caps_unsafeBox then
        mapArgUsing(_.forceBoxStatus(true))
      else if meth == defn.Caps_unsafeUnbox then
        mapArgUsing(_.forceBoxStatus(false))
      else if meth == defn.Caps_unsafeBoxFunArg then
        def forceBox(tp: Type): Type = tp.strippedDealias match
          case defn.FunctionOf(paramtpe :: Nil, restpe, isContextual) =>
            defn.FunctionOf(paramtpe.forceBoxStatus(true) :: Nil, restpe, isContextual)
          case tp @ RefinedType(parent, rname, rinfo: MethodType) =>
            tp.derivedRefinedType(parent, rname,
              rinfo.derivedLambdaType(
                paramInfos = rinfo.paramInfos.map(_.forceBoxStatus(true))))
          case tp @ CapturingType(parent, refs) =>
            tp.derivedCapturingType(forceBox(parent), refs)
        mapArgUsing(forceBox)
      else
        handleCall(meth, tree, () => super.recheckApply(tree, pt))
    end recheckApply

    protected override
    def recheckArg(arg: Tree, formal: Type)(using Context): Type =
      val argType = recheck(arg, formal)
      if unboxedArgs.contains(arg) then
        capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}")
        markFree(argType.deepCaptureSet, arg.srcPos)
      argType

    /** A specialized implementation of the apply rule.
     *
     *  E |- q: Tq^Cq
     *  E |- q.f: Ta^Ca ->Cf Tr^Cr
     *  E |- a: Ta
     *  ---------------------
     *  E |- f(a): Tr^C
     *
     *  If the function `f` does not have an `@unboxed` parameter, then
     *  any unboxing it does would be charged to the environment of the function
     *  so they have to appear in Cq. Since any capabilities of the result of the
     *  application must already be present in the application, an upper
     *  approximation of the result capture set is Cq \union Ca, where `Ca`
     *  is the capture set of the argument.
     *  If the function `f` does have an `@unboxed` parameter, then it could in addition
     *  unbox reach capabilities over its formal parameter. Therefore, the approximation
     *  would be `Cq \union dcs(Ca)` instead.
     *  If the approximation is known to subcapture the declared result Cr, we pick it for C
     *  otherwise we pick Cr.
     */
    protected override
    def recheckApplication(tree: Apply, qualType: Type, funType: MethodType, argTypes: List[Type])(using Context): Type =
      val appType = Existential.toCap(super.recheckApplication(tree, qualType, funType, argTypes))
      val qualCaptures = qualType.captureSet
      val argCaptures =
        for (arg, argType) <- tree.args.lazyZip(argTypes) yield
          if unboxedArgs.remove(arg) // need to ensure the remove happens, that's why argCaptures is computed even if not needed.
          then argType.deepCaptureSet
          else argType.captureSet
      appType match
        case appType @ CapturingType(appType1, refs)
        if qualType.exists
            && !tree.fun.symbol.isConstructor
            && qualCaptures.mightSubcapture(refs)
            && argCaptures.forall(_.mightSubcapture(refs)) =>
          val callCaptures = argCaptures.foldLeft(qualCaptures)(_ ++ _)
          appType.derivedCapturingType(appType1, callCaptures)
            .showing(i"narrow $tree: $appType, refs = $refs, qual-cs = ${qualType.captureSet} = $result", capt)
        case appType =>
          appType

    private def isDistinct(xs: List[Type]): Boolean = xs match
      case x :: xs1 => xs1.isEmpty || !xs1.contains(x) && isDistinct(xs1)
      case Nil => true

    /** Handle an application of method `sym` with type `mt` to arguments of types `argTypes`.
     *  This means:
     *   - Instantiate result type with actual arguments
     *   - If call is to a constructor:
     *      - remember types of arguments corresponding to tracked
     *        parameters in refinements.
     *      - add capture set of instantiated class to capture set of result type.
     *  If all argument types are mutually different trackable capture references, use a BiTypeMap,
     *  since that is more precise. Otherwise use a normal idempotent map, which might lose information
     *  in the case where the result type contains captureset variables that are further
     *  constrained afterwards.
     */
    override def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type =
      val ownType =
        if !mt.isResultDependent then
          mt.resType
        else if argTypes.forall(_.isTrackableRef) && isDistinct(argTypes) then
          SubstParamsBiMap(mt, argTypes)(mt.resType)
        else
          SubstParamsMap(mt, argTypes)(mt.resType)

      if sym.isConstructor then
        val cls = sym.owner.asClass

        /** First half of result pair:
         *  Refine the type of a constructor call `new C(t_1, ..., t_n)`
         *  to C{val x_1: T_1, ..., x_m: T_m} where x_1, ..., x_m are the tracked
         *  parameters of C and T_1, ..., T_m are the types of the corresponding arguments.
         *
         *  Second half: union of all capture sets of arguments to tracked parameters.
         */
        def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) =
          var refined: Type = core
          var allCaptures: CaptureSet =
            if core.derivesFromCapability then defn.universalCSImpliedByCapability else initCs
          for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do
            val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol
            if !getter.is(Private) && getter.hasTrackedParts then
              refined = RefinedType(refined, getterName, argType)
              allCaptures ++= argType.captureSet
          (refined, allCaptures)

        /** Augment result type of constructor with refinements and captures.
         *  @param  core   The result type of the constructor
         *  @param  initCs The initial capture set to add, not yet counting capture sets from arguments
         */
        def augmentConstructorType(core: Type, initCs: CaptureSet): Type = core match
          case core: MethodType =>
            // more parameters to follow; augment result type
            core.derivedLambdaType(resType = augmentConstructorType(core.resType, initCs))
          case CapturingType(parent, refs) =>
            // can happen for curried constructors if instantiate of a previous step
            // added capture set to result.
            augmentConstructorType(parent, initCs ++ refs)
          case _ =>
            val (refined, cs) = addParamArgRefinements(core, initCs)
            refined.capturing(cs)

        augmentConstructorType(ownType, capturedVars(cls) ++ capturedVars(sym))
          .showing(i"constr type $mt with $argTypes%, % in $cls = $result", capt)
      else ownType
    end instantiate

    override def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type =
      val meth = tree.symbol
      if ccConfig.useSealed then
        val TypeApply(fn, args) = tree
        val polyType = atPhase(thisPhase.prev):
          fn.tpe.widen.asInstanceOf[TypeLambda]
        def isExempt(sym: Symbol) =
          sym.isTypeTestOrCast || sym == defn.Compiletime_erasedValue
        for case (arg: TypeTree, formal, pname) <- args.lazyZip(polyType.paramRefs).lazyZip((polyType.paramNames)) do
          if !isExempt(meth) then
            def where = if meth.exists then i" in an argument of $meth" else ""
            disallowRootCapabilitiesIn(arg.knownType, NoSymbol,
              i"Sealed type variable $pname", "be instantiated to",
              i"This is often caused by a local capability$where\nleaking as part of its result.",
              tree.srcPos)
      try handleCall(meth, tree, () => Existential.toCap(super.recheckTypeApply(tree, pt)))
      finally checkContains(tree)
    end recheckTypeApply

    /** Faced with a tree of form `caps.contansImpl[CS, r.type]`, check that `R` is a tracked
     *  capability and assert that `{r} <:CS`.
     */
    def checkContains(tree: TypeApply)(using Context): Unit = tree match
      case ContainsImpl(csArg, refArg) =>
        val cs = csArg.knownType.captureSet
        val ref = refArg.knownType
        capt.println(i"check contains $cs , $ref")
        ref match
          case ref: CaptureRef if ref.isTracked =>
            checkElem(ref, cs, tree.srcPos)
          case _ =>
            report.error(em"$refArg is not a tracked capability", refArg.srcPos)
      case _ =>

    override def recheckBlock(tree: Block, pt: Type)(using Context): Type =
      inNestedLevel(super.recheckBlock(tree, pt))

    override def recheckClosure(tree: Closure, pt: Type, forceDependent: Boolean)(using Context): Type =
      val cs = capturedVars(tree.meth.symbol)
      capt.println(i"typing closure $tree with cvs $cs")
      super.recheckClosure(tree, pt, forceDependent).capturing(cs)
        .showing(i"rechecked closure $tree / $pt = $result", capt)

    override def recheckClosureBlock(mdef: DefDef, expr: Closure, pt: Type)(using Context): Type =
      mdef.rhs match
        case rhs @ closure(_, _, _) =>
          // In a curried closure `x => y => e` don't leak capabilities retained by
          // the second closure `y => e` into the first one. This is an approximation
          // of the CC rule which says that a closure contributes captures to its
          // environment only if a let-bound reference to the closure is used.
          mdef.rhs.putAttachment(ClosureBodyValue, ())
        case _ =>

      openClosures = (mdef.symbol, pt) :: openClosures
      try
        // Constrain closure's parameters and result from the expected type before
        // rechecking the body.
        val res = recheckClosure(expr, pt, forceDependent = true)
        if !(isEtaExpansion(mdef) && ccConfig.handleEtaExpansionsSpecially) then
          // If closure is an eta expanded method reference it's better to not constrain
          // its internals early since that would give error messages in generated code
          // which are less intelligible.
          // Example is the line `a = x` in neg-custom-args/captures/vars.scala.
          // For all other closures, early constraints are preferred since they
          // give more localized error messages.
          val res1 = Existential.toCapDeeply(res)
          val pt1 = Existential.toCapDeeply(pt)
            // We need to open existentials here in order not to get vars mixed up in them
            // We do the proper check with existentials when we are finished with the closure block.
          capt.println(i"pre-check closure $expr of type $res1 against $pt1")
          checkConformsExpr(res1, pt1, expr)
        recheckDef(mdef, mdef.symbol)
        res
      finally
        openClosures = openClosures.tail
    end recheckClosureBlock

    override def seqLiteralElemProto(tree: SeqLiteral, pt: Type, declared: Type)(using Context) =
      super.seqLiteralElemProto(tree, pt, declared).boxed

    /** Maps mutable variables to the symbols that capture them (in the
     *  CheckCaptures sense, i.e. symbol is referred to from a different method
     *  than the one it is defined in).
     */
    private val capturedBy = util.HashMap[Symbol, Symbol]()

    /** Maps anonymous functions appearing as function arguments to
     *  the function that is called.
     */
    private val anonFunCallee = util.HashMap[Symbol, Symbol]()

    /** Populates `capturedBy` and `anonFunCallee`. Called by `checkUnit`.
     */
    private def collectCapturedMutVars(using Context) = new TreeTraverser:
      def traverse(tree: Tree)(using Context) = tree match
        case id: Ident =>
          val sym = id.symbol
          if sym.is(Mutable, butNot = Method) && sym.owner.isTerm then
            val enclMeth = ctx.owner.enclosingMethod
            if sym.enclosingMethod != enclMeth then
              capturedBy(sym) = enclMeth
        case Apply(fn, args) =>
          for case closureDef(mdef) <- args do
            anonFunCallee(mdef.symbol) = fn.symbol
          traverseChildren(tree)
        case Inlined(_, bindings, expansion) =>
          traverse(bindings)
          traverse(expansion)
        case mdef: DefDef =>
          if !mdef.symbol.isInlineMethod then traverseChildren(tree)
        case _ =>
          traverseChildren(tree)

    override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type =
      try
        if sym.is(Module) then sym.info // Modules are checked by checking the module class
        else
          if sym.is(Mutable) && !sym.hasAnnotation(defn.UncheckedCapturesAnnot) then
            val (carrier, addendum) = capturedBy.get(sym) match
              case Some(encl) =>
                val enclStr =
                  if encl.isAnonymousFunction then
                    val location = anonFunCallee.get(encl) match
                      case Some(meth) if meth.exists => i" argument in a call to $meth"
                      case _ => ""
                    s"an anonymous function$location"
                  else encl.show
                (NoSymbol, i"\nNote that $sym does not count as local since it is captured by $enclStr")
              case _ =>
                (sym, "")
            disallowRootCapabilitiesIn(
              tree.tpt.knownType, carrier, i"Mutable $sym", "have type", addendum, sym.srcPos)
          checkInferredResult(super.recheckValDef(tree, sym), tree)
      finally
        if !sym.is(Param) then
          // Parameters with inferred types belong to anonymous methods. We need to wait
          // for more info from the context, so we cannot interpolate. Note that we cannot
          // expect to have all necessary info available at the point where the anonymous
          // function is compiled since we do not propagate expected types into blocks.
          interpolateVarsIn(tree.tpt)

    override def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Type =
      if Synthetics.isExcluded(sym) then sym.info
      else
        val saved = curEnv
        val localSet = capturedVars(sym)
        if !localSet.isAlwaysEmpty then
          curEnv = Env(sym, EnvKind.Regular, localSet, curEnv)

        // ctx with AssumedContains entries for each Contains parameter
        val bodyCtx =
          var ac = CaptureSet.assumedContains
          for paramSyms <- sym.paramSymss do
            for case ContainsParam(cs, ref) <- paramSyms do
              ac = ac.updated(cs, ac.getOrElse(cs, SimpleIdentitySet.empty) + ref)
          if ac.isEmpty then ctx
          else ctx.withProperty(CaptureSet.AssumedContains, Some(ac))

        inNestedLevel: // TODO: needed here?
          try checkInferredResult(super.recheckDefDef(tree, sym)(using bodyCtx), tree)
          finally
            if !sym.isAnonymousFunction then
              // Anonymous functions propagate their type to the enclosing environment
              // so it is not in general sound to interpolate their types.
              interpolateVarsIn(tree.tpt)
            curEnv = saved
    end recheckDefDef

    /** If val or def definition with inferred (result) type is visible
     *  in other compilation units, check that the actual inferred type
     *  conforms to the expected type where all inferred capture sets are dropped.
     *  This ensures that if files compile separately, they will also compile
     *  in a joint compilation.
     */
    def checkInferredResult(tp: Type, tree: ValOrDefDef)(using Context): Type =
      val sym = tree.symbol

      def canUseInferred =    // If canUseInferred is false, all capturing types in the type of `sym` need to be given explicitly
        sym.is(Private)                   // private symbols can always have inferred types
        || sym.name.is(DefaultGetterName) // default getters are exempted since otherwise it would be
                                          // too annoying. This is a hole since a defualt getter's result type
                                          // might leak into a type variable.
        ||                                // non-local symbols cannot have inferred types since external capture types are not inferred
          sym.isLocalToCompilationUnit    // local symbols still need explicit types if
          && !sym.owner.is(Trait)         // they are defined in a trait, since we do OverridingPairs checking before capture inference

      def addenda(expected: Type) = new Addenda:
        override def toAdd(using Context) =
          def result = if tree.isInstanceOf[ValDef] then"" else " result"
          i"""
           |
           |Note that the expected type $expected
           |is the previously inferred$result type of $sym
           |which is also the type seen in separately compiled sources.
           |The new inferred type $tp
           |must conform to this type.""" :: Nil

      tree.tpt match
        case tpt: InferredTypeTree if !canUseInferred =>
          val expected = tpt.tpe.dropAllRetains
          todoAtPostCheck += (() => checkConformsExpr(tp, expected, tree.rhs, addenda(expected)))
        case _ =>
      tp
    end checkInferredResult

    /** Class-specific capture set relations:
     *   1. The capture set of a class includes the capture sets of its parents.
     *   2. The capture set of the self type of a class includes the capture set of the class.
     *   3. The capture set of the self type of a class includes the capture set of every class parameter,
     *      unless the parameter is marked @constructorOnly.
     *   4. If the class extends a pure base class, the capture set of the self type must be empty.
     */
    override def recheckClassDef(tree: TypeDef, impl: Template, cls: ClassSymbol)(using Context): Type =
      val saved = curEnv
      val localSet = capturedVars(cls)
      for parent <- impl.parents do // (1)
        checkSubset(capturedVars(parent.tpe.classSymbol), localSet, parent.srcPos,
          i"\nof the references allowed to be captured by $cls")
      if !localSet.isAlwaysEmpty then
        curEnv = Env(cls, EnvKind.Regular, localSet, curEnv)
      try
        val thisSet = cls.classInfo.selfType.captureSet.withDescription(i"of the self type of $cls")
        checkSubset(localSet, thisSet, tree.srcPos) // (2)
        for param <- cls.paramGetters do
          if !param.hasAnnotation(defn.ConstructorOnlyAnnot)
            && !param.hasAnnotation(defn.UntrackedCapturesAnnot) then
            checkSubset(param.termRef.captureSet, thisSet, param.srcPos) // (3)
        for pureBase <- cls.pureBaseClass do // (4)
          def selfType = impl.body
            .collect:
              case TypeDef(tpnme.SELF, rhs) => rhs
            .headOption
            .getOrElse(tree)
            .orElse(tree)
          checkSubset(thisSet,
            CaptureSet.empty.withDescription(i"of pure base class $pureBase"),
            selfType.srcPos, cs1description = " captured by this self type")
        inNestedLevelUnless(cls.is(Module)):
          super.recheckClassDef(tree, impl, cls)
      finally
        curEnv = saved

    /** If type is of the form `T @requiresCapability(x)`,
     *  mark `x` as free in the current environment. This is used to require the
     *  correct `CanThrow` capability when encountering a `throw`.
     */
    override def recheckTyped(tree: Typed)(using Context): Type =
      tree.tpt.tpe match
        case AnnotatedType(_, annot) if annot.symbol == defn.RequiresCapabilityAnnot =>
          annot.tree match
            case Apply(_, cap :: Nil) =>
              markFree(cap.symbol, tree.srcPos)
            case _ =>
        case _ =>
      super.recheckTyped(tree)

    override def recheckTry(tree: Try, pt: Type)(using Context): Type =
      val tp = super.recheckTry(tree, pt)
      if ccConfig.useSealed && Feature.enabled(Feature.saferExceptions) then
        disallowRootCapabilitiesIn(tp, ctx.owner,
          "result of `try`", "have type",
          "This is often caused by a locally generated exception capability leaking as part of its result.",
          tree.srcPos)
      tp

    /* Currently not needed, since capture checking takes place after ElimByName.
     * Keep around in case we need to get back to it
    def recheckByNameArg(tree: Tree, pt: Type)(using Context): Type =
      val closureDef(mdef) = tree: @unchecked
      val arg = mdef.rhs
      val localSet = CaptureSet.Var()
      curEnv = Env(mdef.symbol, localSet, isBoxed = false, curEnv)
      val result =
        try
          inContext(ctx.withOwner(mdef.symbol)) {
            recheckStart(arg, pt).capturing(localSet)
          }
        finally curEnv = curEnv.outer
      recheckFinish(result, arg, pt)
    */

    /** If expected type `pt` is boxed and the tree is a function or a reference,
     *  don't propagate free variables.
     *  Otherwise, if the result type is boxed, simulate an unboxing by
     *  adding all references in the boxed capture set to the current environment.
     */
    override def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type =
      val saved = curEnv
      tree match
        case _: RefTree | closureDef(_) if pt.isBoxedCapturing =>
          curEnv = Env(curEnv.owner, EnvKind.Boxed, CaptureSet.Var(curEnv.owner, level = currentLevel), curEnv)
        case _ if tree.hasAttachment(ClosureBodyValue) =>
          curEnv = Env(curEnv.owner, EnvKind.ClosureResult, CaptureSet.Var(curEnv.owner, level = currentLevel), curEnv)
        case _ =>
      val res =
        try
          if capt eq noPrinter then
            super.recheck(tree, pt)
          else
            trace.force(i"rechecking $tree with pt = $pt", recheckr, show = true):
              super.recheck(tree, pt)
        catch case ex: NoCommonRoot =>
          report.error(ex.getMessage.nn)
          tree.tpe
        finally curEnv = saved
      if tree.isTerm then
        if !ccConfig.useExistentials then
          checkReachCapsIsolated(res.widen, tree.srcPos)
        if !pt.isBoxedCapturing && pt != LhsProto then
          markFree(res.boxedCaptureSet, tree.srcPos)
      res

    override def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type =
      def needsUniversalCheck = tree match
        case _: RefTree | _: Apply | _: TypeApply => tree.symbol.unboxesResult
        case _: Try => true
        case _ => false

      object checkNotUniversal extends TypeTraverser:
        def traverse(tp: Type) =
         tp.dealias match
          case wtp @ CapturingType(parent, refs) =>
            if variance > 0 then
              refs.disallowRootCapability: () =>
                def part = if wtp eq tpe.widen then "" else i" in its part $wtp"
                report.error(
                  em"""The expression's type ${tpe.widen} is not allowed to capture the root capability `cap`$part.
                    |This usually means that a capability persists longer than its allowed lifetime.""",
                  tree.srcPos)
            if !wtp.isBoxed then traverse(parent)
          case tp =>
            traverseChildren(tp)

      if !ccConfig.useSealed
          && !tpe.hasAnnotation(defn.UncheckedCapturesAnnot)
          && needsUniversalCheck
          && tpe.widen.isValueType
      then
        checkNotUniversal.traverse(tpe.widen)
      super.recheckFinish(tpe, tree, pt)
    end recheckFinish

    // ------------------ Adaptation -------------------------------------
    //
    // Adaptations before checking conformance of actual vs expected:
    //
    //   - Convert function to dependent function if expected type is a dependent function type
    //     (c.f. alignDependentFunction).
    //   - Relax expected capture set containing `this.type`s by adding references only
    //     accessible through those types (c.f. addOuterRefs, also #14930 for a discussion).
    //   - Adapt box status and environment capture sets by simulating box/unbox operations.
    //   - Instantiate covariant occurrenves of `cap` in actual to reach capabilities.

    private inline val debugSuccesses = false

    type BoxErrors = mutable.ListBuffer[Message] | Null

    private def boxErrorAddenda(boxErrors: BoxErrors) =
      if boxErrors == null then NothingToAdd
      else new Addenda:
        override def toAdd(using Context): List[String] =
          boxErrors.toList.map: msg =>
            i"""
              |
              |Note that ${msg.toString}"""

    private def addApproxAddenda(using Context) =
      new TypeAccumulator[Addenda]:
        def apply(add: Addenda, t: Type) = t match
          case CapturingType(t, CaptureSet.EmptyWithProvenance(ref, mapped)) =>
            /* val (origCore, kind) = original match
              case tp @ AnnotatedType(parent, ann) if ann.hasSymbol(defn.ReachCapabilityAnnot) =>
                (parent, " deep")
              case _ =>
                (original, "")*/
            add ++ new Addenda:
              override def toAdd(using Context): List[String] =
                i"""
                   |
                   |Note that a capability $ref in a capture set appearing in contravariant position
                   |was mapped to $mapped which is not a capability. Therefore, it was under-approximated to the empty set."""
                :: Nil
          case _ =>
            foldOver(add, t)

    /** Massage `actual` and `expected` types before checking conformance.
     *  Massaging is done by the methods following this one:
     *   - align dependent function types and add outer references in the expected type
     *   - adapt boxing in the actual type
     *  If the resulting types are not compatible, try again with an actual type
     *  where local capture roots are instantiated to root variables.
     */
    override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type =
      var expected1 = alignDependentFunction(expected, actual.stripCapturing)
      val boxErrors = new mutable.ListBuffer[Message]
      val actualBoxed = adapt(actual, expected1, tree.srcPos, boxErrors)
      //println(i"check conforms $actualBoxed <<< $expected1")

      if actualBoxed eq actual then
        // Only `addOuterRefs` when there is no box adaptation
        expected1 = addOuterRefs(expected1, actual, tree.srcPos)
      if isCompatible(actualBoxed, expected1) then
        if debugSuccesses then tree match
            case Ident(_) =>
              println(i"SUCCESS $tree:\n${TypeComparer.explained(_.isSubType(actual, expected))}")
            case _ =>
        actualBoxed
      else
        capt.println(i"conforms failed for ${tree}: $actual vs $expected")
        err.typeMismatch(tree.withType(actualBoxed), expected1,
            addApproxAddenda(
              addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors),
              expected1))
        actual
    end checkConformsExpr

    /** Turn `expected` into a dependent function when `actual` is dependent. */
    private def alignDependentFunction(expected: Type, actual: Type)(using Context): Type =
      def recur(expected: Type): Type = expected.dealias match
        case expected0 @ CapturingType(eparent, refs) =>
          val eparent1 = recur(eparent)
          if eparent1 eq eparent then expected
          else CapturingType(eparent1, refs, boxed = expected0.isBoxed)
        case expected @ defn.FunctionOf(args, resultType, isContextual)
        if defn.isNonRefinedFunction(expected) =>
          actual match
            case defn.RefinedFunctionOf(rinfo: MethodType) =>
              depFun(args, resultType, isContextual, rinfo.paramNames)
            case _ => expected
        case _ => expected
      recur(expected)

    /** For the expected type, implement the rule outlined in #14390:
     *   - when checking an expression `a: Ta^Ca` against an expected type `Te^Ce`,
     *   - where the capture set `Ce` contains Cls.this,
     *   - and where all method definitions enclosing `a` inside class `Cls`
     *     have only pure parameters,
     *   - add to `Ce` all references to variables or this-references in `Ca`
     *     that are outside `Cls`. These are all accessed through `Cls.this`,
     *     so we can assume they are already accounted for by `Ce` and adding
     *     them explicitly to `Ce` changes nothing.
     *   - To make up for this, we also add these variables to the capture set of `Cls`,
     *     so that all instances of `Cls` will capture these outer references.
     *  So in a sense we use `{Cls.this}` as a placeholder for certain outer captures.
     *  that we needed to be subsumed by `Cls.this`.
     */
    private def addOuterRefs(expected: Type, actual: Type, pos: SrcPos)(using Context): Type =

      def isPure(info: Type): Boolean = info match
        case info: PolyType => isPure(info.resType)
        case info: MethodType => info.paramInfos.forall(_.captureSet.isAlwaysEmpty) && isPure(info.resType)
        case _ => true

      def isPureContext(owner: Symbol, limit: Symbol): Boolean =
        if owner == limit then true
        else if !owner.exists then false
        else isPure(owner.info) && isPureContext(owner.owner, limit)

      // Augment expeced capture set `erefs` by all references in actual capture
      // set `arefs` that are outside some `C.this.type` reference in `erefs` for an enclosing
      // class `C`. If an added reference is not a ThisType itself, add it to the capture set
      // (i.e. use set) of the `C`. This makes sure that any outer reference implicitly subsumed
      // by `C.this` becomes a capture reference of every instance of `C`.
      def augment(erefs: CaptureSet, arefs: CaptureSet): CaptureSet =
        (erefs /: erefs.elems): (erefs, eref) =>
          eref match
            case eref: ThisType if isPureContext(ctx.owner, eref.cls) =>

              def pathRoot(aref: Type): Type = aref match
                case aref: NamedType if aref.symbol.owner.isClass => pathRoot(aref.prefix)
                case _ => aref

              def isOuterRef(aref: Type): Boolean = pathRoot(aref) match
                case aref: NamedType => eref.cls.isProperlyContainedIn(aref.symbol.owner)
                case aref: ThisType => eref.cls.isProperlyContainedIn(aref.cls)
                case _ => false

              val outerRefs = arefs.filter(isOuterRef)

              // Include implicitly added outer references in the capture set of the class of `eref`.
              for outerRef <- outerRefs.elems do
                if !erefs.elems.contains(outerRef)
                    && !pathRoot(outerRef).isInstanceOf[ThisType]
                    // we don't need to add outer ThisTypes as these are anyway added as path
                    // prefixes at the use site. And this exemption is required since capture sets
                    // of non-local classes are always empty, so we can't add an outer this to them.
                then
                  def provenance =
                    i""" of the enclosing class ${eref.cls}.
                       |The reference was included since we tried to establish that $arefs <: $erefs"""
                  checkElem(outerRef, capturedVars(eref.cls), pos, provenance)

              erefs ++ outerRefs
            case _ =>
              erefs

      expected match
        case CapturingType(ecore, erefs) =>
          val erefs1 = augment(erefs, actual.captureSet)
          if erefs1 ne erefs then
            capt.println(i"augmented $expected from ${actual.captureSet} --> $erefs1")
          expected.derivedCapturingType(ecore, erefs1)
        case _ =>
          expected
    end addOuterRefs

    /** A debugging method for showing the envrionments during capture checking. */
    private def debugShowEnvs()(using Context): Unit =
      def showEnv(env: Env): String = i"Env(${env.owner}, ${env.kind}, ${env.captured})"
      val sb = StringBuilder()
      @annotation.tailrec def walk(env: Env | Null): Unit =
        if env != null then
          sb ++= showEnv(env)
          sb ++= "\n"
          walk(env.outer0)
      sb ++= "===== Current Envs ======\n"
      walk(curEnv)
      sb ++= "===== End          ======\n"
      println(sb.result())

    /** Adapt `actual` type to `expected` type by inserting boxing and unboxing conversions
     *
     *  @param alwaysConst  always make capture set variables constant after adaptation
     */
    def adaptBoxed(actual: Type, expected: Type, pos: SrcPos, covariant: Boolean, alwaysConst: Boolean, boxErrors: BoxErrors)(using Context): Type =

      def recur(actual: Type, expected: Type, covariant: Boolean): Type =

        /** Adapt the inner shape type: get the adapted shape type, and the capture set leaked during adaptation
         *  @param boxed   if true we adapt to a boxed expected type
         */
        def adaptShape(actualShape: Type, boxed: Boolean): (Type, CaptureSet) = actualShape match
          case FunctionOrMethod(aargs, ares) =>
            val saved = curEnv
            curEnv = Env(
              curEnv.owner, EnvKind.NestedInOwner,
              CaptureSet.Var(curEnv.owner, level = currentLevel),
              if boxed then null else curEnv)
            try
              val (eargs, eres) = expected.dealias.stripCapturing match
                case FunctionOrMethod(eargs, eres) => (eargs, eres)
                case _ => (aargs.map(_ => WildcardType), WildcardType)
              val aargs1 = aargs.zipWithConserve(eargs):
                recur(_, _, !covariant)
              val ares1 = recur(ares, eres, covariant)
              val resTp =
                if (aargs1 eq aargs) && (ares1 eq ares) then actualShape // optimize to avoid redundant matches
                else actualShape.derivedFunctionOrMethod(aargs1, ares1)
              (resTp, CaptureSet(curEnv.captured.elems))
            finally curEnv = saved
          case _ =>
            (actualShape, CaptureSet())
        end adaptShape

        def adaptStr = i"adapting $actual ${if covariant then "~~>" else "<~~"} $expected"

        actual match
          case actual @ Existential(_, actualUnpacked) =>
            return Existential.derivedExistentialType(actual):
                recur(actualUnpacked, expected, covariant)
          case _ =>
        expected match
          case expected @ Existential(_, expectedUnpacked) =>
            return recur(actual, expectedUnpacked, covariant)
          case _: WildcardType =>
            return actual
          case _ =>

        trace(adaptStr, capt, show = true) {

        // Decompose the actual type into the inner shape type, the capture set and the box status
        val actualShape = if actual.isFromJavaObject then actual else actual.stripCapturing
        val actualIsBoxed = actual.isBoxedCapturing

        // A box/unbox should be inserted, if the actual box status mismatches with the expectation
        val needsAdaptation = actualIsBoxed != expected.isBoxedCapturing
        // Whether to insert a box or an unbox?
        val insertBox = needsAdaptation && covariant != actualIsBoxed

        // Adapt the inner shape type: get the adapted shape type, and the capture set leaked during adaptation
        val (adaptedShape, leaked) = adaptShape(actualShape, insertBox)

        // Capture set of the term after adaptation
        val captures =
          val cs = actual.captureSet
          if covariant then cs ++ leaked
          else
            if !leaked.subCaptures(cs, frozen = false).isOK then
              report.error(
                em"""$expected cannot be box-converted to $actual
                    |since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", pos)
            cs

        // Compute the adapted type
        def adaptedType(resultBoxed: Boolean) =
          if (adaptedShape eq actualShape) && leaked.isAlwaysEmpty && actualIsBoxed == resultBoxed
          then actual
          else adaptedShape
            .capturing(if alwaysConst then CaptureSet(captures.elems) else captures)
            .forceBoxStatus(resultBoxed)

        if needsAdaptation then
          val criticalSet =          // the set which is not allowed to have `cap`
            if covariant then captures    // can't box with `cap`
            else expected.captureSet // can't unbox with `cap`
          def msg = em"""$actual cannot be box-converted to $expected
                        |since at least one of their capture sets contains the root capability `cap`"""
          def allowUniversalInBoxed =
            ccConfig.useSealed
            || expected.hasAnnotation(defn.UncheckedCapturesAnnot)
            || actual.widen.hasAnnotation(defn.UncheckedCapturesAnnot)
          if criticalSet.isUnboxable && expected.isValueType && !allowUniversalInBoxed then
            // We can't box/unbox the universal capability. Leave `actual` as it is
            // so we get an error in checkConforms. Add the error message generated
            // from boxing as an addendum. This tends to give better error
            // messages than disallowing the root capability in `criticalSet`.
            if boxErrors != null then boxErrors += msg
            if ctx.settings.YccDebug.value then
              println(i"cannot box/unbox $actual vs $expected")
            actual
          else
            if !allowUniversalInBoxed then
              // Disallow future addition of `cap` to `criticalSet`.
              criticalSet.disallowRootCapability: () =>
                report.error(msg, pos)
            if !insertBox then  // unboxing
              //debugShowEnvs()
              markFree(criticalSet, pos)
            adaptedType(!actualIsBoxed)
        else
          adaptedType(actualIsBoxed)
        }
      end recur

      recur(actual, expected, covariant)
    end adaptBoxed

    /** If actual is a tracked CaptureRef `a` and widened is a capturing type T^C,
     *  improve `T^C` to `T^{a}`, following the VAR rule of CC.
     */
    private def improveCaptures(widened: Type, actual: Type)(using Context): Type = actual match
      case ref: CaptureRef if ref.isTracked =>
        widened match
          case CapturingType(p, refs) if ref.singletonCaptureSet.mightSubcapture(refs) =>
            widened.derivedCapturingType(p, ref.singletonCaptureSet)
              .showing(i"improve $widened to $result", capt)
          case _ => widened
      case _ => widened

    /** Adapt `actual` type to `expected` type by inserting boxing and unboxing conversions
     *
     *  @param alwaysConst  always make capture set variables constant after adaptation
     */
    def adapt(actual: Type, expected: Type, pos: SrcPos, boxErrors: BoxErrors)(using Context): Type =
      if expected == LhsProto || expected.isSingleton && actual.isSingleton then
        actual
      else
        val widened = improveCaptures(actual.widen.dealiasKeepAnnots, actual)
        val adapted = adaptBoxed(
            widened.withReachCaptures(actual), expected, pos,
            covariant = true, alwaysConst = false, boxErrors)
        if adapted eq widened then actual
        else adapted.showing(i"adapt boxed $actual vs $expected = $adapted", capt)
    end adapt

    /** Check overrides again, taking capture sets into account.
    *  TODO: Can we avoid doing overrides checks twice?
    *  We need to do them here since only at this phase CaptureTypes are relevant
    *  But maybe we can then elide the check during the RefChecks phase under captureChecking?
    */
    def checkOverrides = new TreeTraverser:
      class OverridingPairsCheckerCC(clazz: ClassSymbol, self: Type, srcPos: SrcPos)(using Context) extends OverridingPairsChecker(clazz, self):
        /** Check subtype with box adaptation.
        *  This function is passed to RefChecks to check the compatibility of overriding pairs.
        *  @param sym  symbol of the field definition that is being checked
        */
        override def checkSubType(actual: Type, expected: Type)(using Context): Boolean =
          val expected1 = alignDependentFunction(addOuterRefs(expected, actual, srcPos), actual.stripCapturing)
          val actual1 =
            val saved = curEnv
            try
              curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv)
              val adapted =
                adaptBoxed(actual, expected1, srcPos, covariant = true, alwaysConst = true, null)
              actual match
                case _: MethodType =>
                  // We remove the capture set resulted from box adaptation for method types,
                  // since class methods are always treated as pure, and their captured variables
                  // are charged to the capture set of the class (which is already done during
                  // box adaptation).
                  adapted.stripCapturing
                case _ => adapted
            finally curEnv = saved
          actual1 frozen_<:< expected1

        override def needsCheck(overriding: Symbol, overridden: Symbol)(using Context): Boolean =
          !setup.isPreCC(overriding) && !setup.isPreCC(overridden)

        override def checkInheritedTraitParameters: Boolean = false

        /** Check that overrides don't change the @unbox status of their parameters */
        override def additionalChecks(member: Symbol, other: Symbol)(using Context): Unit =
          for
            (params1, params2) <- member.rawParamss.lazyZip(other.rawParamss)
            (param1, param2) <- params1.lazyZip(params2)
          do
            if param1.hasAnnotation(defn.UnboxAnnot) != param2.hasAnnotation(defn.UnboxAnnot) then
              report.error(
                OverrideError(
                    i"has a parameter ${param1.name} with different @unbox status than the corresponding parameter in the overridden definition",
                    self, member, other, self.memberInfo(member), self.memberInfo(other)
                  ),
                if member.owner == clazz then member.srcPos else clazz.srcPos
              )
      end OverridingPairsCheckerCC

      def traverse(t: Tree)(using Context) =
        t match
          case t: Template =>
            checkAllOverrides(ctx.owner.asClass, OverridingPairsCheckerCC(_, _, t))
          case _ =>
        traverseChildren(t)

    private val completed = new mutable.HashSet[Symbol]

    override def skipRecheck(sym: Symbol)(using Context): Boolean =
      completed.contains(sym)

    /** Check a ValDef or DefDef as an action performed in a completer. Since
     *  these checks can appear out of order, we need to firsty create the correct
     *  environment for checking the definition.
     */
    def completeDef(tree: ValOrDefDef, sym: Symbol)(using Context): Type =
      val saved = curEnv
      try
        // Setup environment to reflect the new owner.
        val envForOwner: Map[Symbol, Env] = curEnv.outersIterator
          .takeWhile(e => !capturedVars(e.owner).isAlwaysEmpty) // no refs can leak beyind this point
          .map(e => (e.owner, e))
          .toMap
        def restoreEnvFor(sym: Symbol): Env =
          val localSet = capturedVars(sym)
          if localSet.isAlwaysEmpty then rootEnv
          else envForOwner.get(sym) match
            case Some(e) => e
            case None => Env(sym, EnvKind.Regular, localSet, restoreEnvFor(sym.owner))
        curEnv = restoreEnvFor(sym.owner)
        capt.println(i"Complete $sym in ${curEnv.outersIterator.toList.map(_.owner)}")
        try recheckDef(tree, sym)
        finally completed += sym
      finally
        curEnv = saved

    private val setup: SetupAPI = thisPhase.prev.asInstanceOf[Setup]

    override def checkUnit(unit: CompilationUnit)(using Context): Unit =
      setup.setupUnit(unit.tpdTree, completeDef)
      collectCapturedMutVars.traverse(unit.tpdTree)

      if ctx.settings.YccPrintSetup.value then
        val echoHeader = "[[syntax tree at end of cc setup]]"
        val treeString = show(unit.tpdTree)
        report.echo(s"$echoHeader\n$treeString\n")

      withCaptureSetsExplained:
        super.checkUnit(unit)
        checkOverrides.traverse(unit.tpdTree)
        postCheck(unit.tpdTree)
        checkSelfTypes(unit.tpdTree)
        postCheckWF(unit.tpdTree)
        if ctx.settings.YccDebug.value then
          show(unit.tpdTree) // this does not print tree, but makes its variables visible for dependency printing

    /** Check that self types of subclasses conform to self types of super classes.
     *  (See comment below how this is achieved). The check assumes that classes
     *  without an explicit self type have the universal capture set `{cap}` on the
     *  self type. If a class without explicit self type is not `effectivelySealed`
     *  it is checked that the inferred self type is universal, in order to assure
     *  that joint and separate compilation give the same result.
     */
    def checkSelfTypes(unit: tpd.Tree)(using Context): Unit =
      val parentTrees = mutable.HashMap[Symbol, List[Tree]]()
      unit.foreachSubTree {
        case cdef @ TypeDef(_, impl: Template) => parentTrees(cdef.symbol) = impl.parents
        case _ =>
      }
      // Perform self type checking. The problem here is that `checkParents` compares a
      // self type of a subclass with the result of an asSeenFrom of the self type of the
      // superclass. That's no good. We need to constrain the original superclass self type
      // capture set, not the set mapped by asSeenFrom.
      //
      // Instead, we proceed from parent classes to child classes. For every class
      // we first check its parents, and then interpolate the self type to an
      // upper approximation that satisfies all constraints on its capture set.
      // That means all capture sets of parent self types are constants, so mapping
      // them with asSeenFrom is OK.
      while parentTrees.nonEmpty do
        val roots = parentTrees.keysIterator.filter {
          cls => !parentTrees(cls).exists(ptree => parentTrees.contains(ptree.tpe.classSymbol))
        }
        assert(roots.nonEmpty)
        for case root: ClassSymbol <- roots do
          inContext(ctx.fresh.setOwner(root)):
            checkSelfAgainstParents(root, root.baseClasses)
            val selfType = root.asClass.classInfo.selfType
            interpolator(startingVariance = -1).traverse(selfType)
            selfType match
              case CapturingType(_, refs: CaptureSet.Var)
              if !root.isEffectivelySealed
                  && !refs.elems.exists(_.isRootCapability)
                  && !root.matchesExplicitRefsInBaseClass(refs)
              =>
                // Forbid inferred self types unless they are already implied by an explicit
                // self type in a parent.
                report.error(
                  em"""$root needs an explicitly declared self type since its
                      |inferred self type $selfType
                      |is not visible in other compilation units that define subclasses.""",
                  root.srcPos)
              case _ =>
            parentTrees -= root
            capt.println(i"checked $root with $selfType")
    end checkSelfTypes

    /** Heal ill-formed capture sets in the type parameter.
     *
     *  We can push parameter refs into a capture set in type parameters
     *  that this type parameter can't see.
     *  For example, when capture checking the following expression:
     *
     *    def usingLogFile[T](op: File^ => T): T = ...
     *
     *    usingLogFile[box ?1 () -> Unit] { (f: File^) => () => { f.write(0) } }
     *
     *  We may propagate `f` into ?1, making ?1 ill-formed.
     *  This also causes soundness issues, since `f` in ?1 should be widened to `cap`,
     *  giving rise to an error that `cap` cannot be included in a boxed capture set.
     *
     *  To solve this, we still allow ?1 to capture parameter refs like `f`, but
     *  compensate this by pushing the widened capture set of `f` into ?1.
     *  This solves the soundness issue caused by the ill-formness of ?1.
     */
    private def healTypeParam(tree: Tree, paramName: TypeName, meth: Symbol)(using Context): Unit =
      val checker = new TypeTraverser:
        private var allowed: SimpleIdentitySet[TermParamRef] = SimpleIdentitySet.empty

        private def isAllowed(ref: CaptureRef): Boolean = ref match
          case ref: TermParamRef => allowed.contains(ref)
          case _ => true

        private def healCaptureSet(cs: CaptureSet): Unit =
          cs.ensureWellformed: elem =>
            ctx ?=>
              var seen = new util.HashSet[CaptureRef]
              def recur(ref: CaptureRef): Unit = ref.stripReach match
                case ref: TermParamRef
                if !allowed.contains(ref) && !seen.contains(ref) =>
                  seen += ref
                  if ref.isMaxCapability then
                    report.error(i"escaping local reference $ref", tree.srcPos)
                  else
                    val widened = ref.captureSetOfInfo
                    val added = widened.filter(isAllowed(_))
                    capt.println(i"heal $ref in $cs by widening to $added")
                    if !added.subCaptures(cs, frozen = false).isOK then
                      val location = if meth.exists then i" of ${meth.showLocated}" else ""
                      val paramInfo =
                        if ref.paramName.info.kind.isInstanceOf[UniqueNameKind]
                        then i"${ref.paramName} from ${ref.binder}"
                        else i"${ref.paramName}"
                      val debugSetInfo = if ctx.settings.YccDebug.value then i" $cs" else ""
                      report.error(
                        i"local reference $paramInfo leaks into outer capture set$debugSetInfo of type parameter $paramName$location",
                        tree.srcPos)
                    else
                      widened.elems.foreach(recur)
                case _ =>
              recur(elem)

        def traverse(tp: Type) =
          tp match
            case CapturingType(parent, refs) =>
              healCaptureSet(refs)
              traverse(parent)
            case defn.RefinedFunctionOf(rinfo: MethodType) =>
              traverse(rinfo)
            case tp: TermLambda =>
              val saved = allowed
              try
                tp.paramRefs.foreach(allowed += _)
                traverseChildren(tp)
              finally allowed = saved
            case _ =>
              traverseChildren(tp)

      if tree.isInstanceOf[InferredTypeTree] then
        checker.traverse(tree.knownType)
    end healTypeParam

    def checkArraysAreSealedIn(tp: Type, pos: SrcPos)(using Context): Unit =
      val check = new TypeTraverser:
        def traverse(t: Type): Unit =
          t match
            case AppliedType(tycon, arg :: Nil) if tycon.typeSymbol == defn.ArrayClass =>
              if !(pos.span.isSynthetic && ctx.reporter.errorsReported)
                && !arg.typeSymbol.name.is(WildcardParamName)
              then
                CheckCaptures.disallowRootCapabilitiesIn(arg, NoSymbol,
                  "Array", "have element type",
                  "Since arrays are mutable, they have to be treated like variables,\nso their element type must be sealed.",
                  pos)
              traverseChildren(t)
            case defn.RefinedFunctionOf(rinfo: MethodType) =>
              traverse(rinfo)
            case _ =>
              traverseChildren(t)
      check.traverse(tp)

    /** Perform the following kinds of checks
     *   - Check that arguments of TypeApplys and AppliedTypes conform to their bounds.
     *   - Heal ill-formed capture sets of type parameters. See `healTypeParam`.
     */
    def postCheck(unit: tpd.Tree)(using Context): Unit =
      val checker = new TreeTraverser:
        def traverse(tree: Tree)(using Context): Unit =
          val lctx = tree match
            case _: DefTree | _: TypeDef if tree.symbol.exists => ctx.withOwner(tree.symbol)
            case _ => ctx
          trace(i"post check $tree"):
            traverseChildren(tree)(using lctx)
            check(tree)
        def check(tree: Tree)(using Context) = tree match
          case TypeApply(fun, args) =>
            fun.knownType.widen match
              case tl: PolyType =>
                val normArgs = args.lazyZip(tl.paramInfos).map: (arg, bounds) =>
                  arg.withType(arg.knownType.forceBoxStatus(
                    bounds.hi.isBoxedCapturing | bounds.lo.isBoxedCapturing))
                checkBounds(normArgs, tl)
                args.lazyZip(tl.paramNames).foreach(healTypeParam(_, _, fun.symbol))
              case _ =>
          case tree: TypeTree =>
            checkArraysAreSealedIn(tree.tpe, tree.srcPos)
          case _ =>
        end check
      end checker

      checker.traverse(unit)(using ctx.withOwner(defn.RootClass))
      if !ctx.reporter.errorsReported then
        // We dont report errors here if previous errors were reported, because other
        // errors often result in bad applied types, but flagging these bad types gives
        // often worse error messages than the original errors.
        val checkApplied = new TreeTraverser:
          def traverse(t: Tree)(using Context) = t match
            case tree: InferredTypeTree =>
            case tree: New =>
            case tree: TypeTree => checkAppliedTypesIn(tree.withKnownType)
            case _ => traverseChildren(t)
        checkApplied.traverse(unit)
    end postCheck

    /** Perform the following kinds of checks:
     *   - Check all explicitly written capturing types for well-formedness using `checkWellFormedPost`.
     *   - Check that publicly visible inferred types correspond to the type
     *     they have without capture checking.
     */
    def postCheckWF(unit: tpd.Tree)(using Context): Unit =
      for chk <- todoAtPostCheck do chk()
      setup.postCheck()
  end CaptureChecker
end CheckCaptures




© 2015 - 2025 Weber Informatics LLC | Privacy Policy