diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 5c17cbbba5a6..32517a0c72f8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -592,6 +592,7 @@ extension (sym: Symbol) && !defn.isPolymorphicAfterErasure(sym) && !defn.isTypeTestOrCast(sym) + /** It's a parameter accessor that is not annotated @constructorOnly or @uncheckedCaptures */ def isRefiningParamAccessor(using Context): Boolean = sym.is(ParamAccessor) && { diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index d51ce1b08dbd..ea979e0b9f7f 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -337,6 +337,7 @@ object Existential: if needsWrap then wrapped else tp end mapCap + /** Map `cap` in function results to fresh existentials */ def mapCapInResults(fail: Message => Unit)(using Context): TypeMap = new: def mapFunOrMethod(tp: Type, args: List[Type], res: Type): Type = diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 86cf8606c570..98c48b562406 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -13,7 +13,7 @@ import ast.tpd, tpd.* import transform.{PreRecheck, Recheck}, Recheck.* import CaptureSet.{IdentityCaptRefMap, IdempotentCaptRefMap} import Synthetics.isExcluded -import util.Property +import util.{Property, SimpleIdentitySet} import reporting.Message import printing.{Printer, Texts}, Texts.{Text, Str} import collection.mutable @@ -21,6 +21,8 @@ import CCState.* /** Operations accessed from CheckCaptures */ trait SetupAPI: + + /** The operation to recheck a ValDef or DefDef */ type DefRecheck = (tpd.ValOrDefDef, Symbol) => Context ?=> Type /** Setup procedure to run for each compilation unit @@ -50,7 +52,9 @@ object Setup: end Setup import Setup.* -/** A tree traverser that prepares a compilation unit to be capture checked. +/** Phase that sets up everthing for capture checking. + * + * A tree traverser that prepares a compilation unit to be capture checked. * It does the following: * - For every inferred type, drop any retains annotations, * add capture sets to all its parts, add refinements to class types and function types. @@ -68,8 +72,16 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: override def isRunnable(using Context) = super.isRunnable && Feature.ccEnabledSomewhere + /** A set containing symbols whose denotation is in train of being updated + * Used to suppress transforming the denotations of these symbols. + */ private val toBeUpdated = new mutable.HashSet[Symbol] + /** Drops `private` from the flags of `symd` provided it is + * a parameter accessor that's not `constructorOnly` or `uncheckedCaptured` + * and that contains at least one @retains in co- or in-variant position. + * The @retains mught be implicit for a type deriving from `Capability`. + */ private def newFlagsFor(symd: SymDenotation)(using Context): FlagSet = object containsCovarRetains extends TypeAccumulator[Boolean]: @@ -93,30 +105,17 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else symd.flags end newFlagsFor + /** Symbol is a term member of a class that was not capture checked + * The info of these symbols is made fluid. + */ def isPreCC(sym: Symbol)(using Context): Boolean = sym.isTerm && sym.maybeOwner.isClass && !sym.is(Module) && !sym.owner.is(CaptureChecked) && !defn.isFunctionSymbol(sym.owner) - private def fluidify(using Context) = new TypeMap with IdempotentCaptRefMap: - def apply(t: Type): Type = t match - case t: MethodType => - mapOver(t) - case t: TypeLambda => - t.derivedLambdaType(resType = this(t.resType)) - case CapturingType(_, _) => - t - case _ => - val t1 = t match - case t @ defn.RefinedFunctionOf(rinfo: MethodType) => - t.derivedRefinedType(t.parent, t.refinedName, this(rinfo)) - case _ => - mapOver(t) - if variance > 0 then t1 - else decorate(t1, addedSet = Function.const(CaptureSet.Fluid)) - - /** - Reset `private` flags of parameter accessors so that we can refine them + /** The symbol transformer of this phase. + * - Resets `private` flags of parameter accessors so that we can refine them * in Setup if they have non-empty capture sets. * - Special handling of some symbols defined for case classes. * Enabled only until recheck is finished, and provided some compilation unit @@ -126,7 +125,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if !pastRecheck && Feature.ccEnabledSomewhere then val sym = symd.symbol def mappedInfo = - if toBeUpdated.contains(sym) then symd.info + if toBeUpdated.contains(sym) + then symd.info // don't transform symbols that will anyway be updated else transformExplicitType(symd.info) if Synthetics.needsTransform(symd) then Synthetics.transform(symd, mappedInfo) @@ -175,7 +175,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tp: MethodOrPoly => tp // don't box results of methods outside refinements case _ => recur(tp) - /** Perform the following transformation steps everywhere in a type: + /** Transform the type of an InferredTypeTree by performing the following transformation + * steps everywhere in the type: * 1. Drop retains annotations * 2. Turn plain function types into dependent function types, so that * we can refer to their parameters in capture sets. Currently this is @@ -194,8 +195,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: override def toString = "map inferred" /** Refine a possibly applied class type C where the class has tracked parameters - * x_1: T_1, ..., x_n: T_n to C { val x_1: CV_1 T_1, ..., val x_n: CV_n T_n } - * where CV_1, ..., CV_n are fresh capture sets. + * x_1: T_1, ..., x_n: T_n to C { val x_1: T_1^{CV_1}, ..., val x_n: T_n^{CV_n} } + * where CV_1, ..., CV_n are fresh capture set variables. */ def addCaptureRefinements(tp: Type): Type = tp match case _: TypeRef | _: AppliedType if refine && tp.typeParams.isEmpty => @@ -279,14 +280,22 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: try val tp1 = mapInferred(refine = true)(tp) val tp2 = Existential.mapCapInResults(_ => assert(false))(tp1) - if tp2 ne tp then capt.println(i"expanded implicit in ${ctx.owner}: $tp --> $tp1 --> $tp2") + if tp2 ne tp then capt.println(i"expanded inferred in ${ctx.owner}: $tp --> $tp1 --> $tp2") tp2 catch case ex: AssertionError => println(i"error while mapping inferred $tp") throw ex end transformInferredType - private def transformExplicitType(tp: Type, tptToCheck: Option[Tree] = None)(using Context): Type = + /** Transform an explicitly given type by performing the following transformation + * steps everywhere in the type: + * 1. Expand throws aliases to contextual function type with CanThrow parameters + * 2. Map types with retains annotations to CapturingTypes + * 3. Add universal capture sets to types deriving from Capability + * 4. Map `cap` in function result types to existentially bound variables. + * 5. Schedule deferred well-formed tests for types with retains annotations. + */ + private def transformExplicitType(tp: Type, tptToCheck: Tree = EmptyTree)(using Context): Type = val toCapturing = new DeepTypeMap: override def toString = "expand aliases" @@ -315,6 +324,10 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: CapturingType(fntpe, cs, boxed = false) else fntpe + /** If C derives from Capability and we have a C^cs in source, we leave it as is + * instead of expanding it to C^{cap}^cs. We do this by stripping capability-generated + * universal capture sets from the parent of a CapturingType. + */ def stripImpliedCaptureSet(tp: Type): Type = tp match case tp @ CapturingType(parent, refs) if (refs eq defn.universalCSImpliedByCapability) && !tp.isBoxedCapturing => @@ -330,8 +343,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val parent1 = this(parent) if ann.symbol.isRetains then val parent2 = stripImpliedCaptureSet(parent1) - for tpt <- tptToCheck do - checkWellformedLater(parent2, ann.tree, tpt) + if !tptToCheck.isEmpty then + checkWellformedLater(parent2, ann.tree, tptToCheck) CapturingType(parent2, ann.tree.toCaptureSet) else t.derivedAnnotatedType(parent1, ann) @@ -345,7 +358,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: end toCapturing def fail(msg: Message) = - for tree <- tptToCheck do report.error(msg, tree.srcPos) + if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos) val tp1 = toCapturing(tp) val tp2 = Existential.mapCapInResults(fail)(tp1) @@ -353,13 +366,13 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: tp2 end transformExplicitType - /** Transform type of type tree, and remember the transformed type as the type the tree */ + /** Transform type of tree, and remember the transformed type as the type the tree */ private def transformTT(tree: TypeTree, boxed: Boolean)(using Context): Unit = if !tree.hasRememberedType then val transformed = if tree.isInferred then transformInferredType(tree.tpe) - else transformExplicitType(tree.tpe, tptToCheck = Some(tree)) + else transformExplicitType(tree.tpe, tptToCheck = tree) tree.rememberType(if boxed then box(transformed) else transformed) /** Substitute parameter symbols in `from` to paramRefs in corresponding @@ -405,27 +418,32 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: toBeUpdated += sym sym.updateInfo(thisPhase, info, newFlagsFor(sym)) toBeUpdated -= sym - sym.namedType match - case ref: CaptureRef if ref.isTrackableRef => ref.invalidateCaches() // TODO: needed? - case _ => + /** The info of `sym` at the CheckCaptures phase */ extension (sym: Symbol) def nextInfo(using Context): Type = atPhase(thisPhase.next)(sym.info) + /** A traverser that adds knownTypes and updates symbol infos */ def setupTraverser(recheckDef: DefRecheck) = new TreeTraverserWithPreciseImportContexts: + /** Transform the type of a val or var or the result type of a def */ def transformResultType(tpt: TypeTree, sym: Symbol)(using Context): Unit = + // First step: Transform the type and record it as knownType of tpt. try transformTT(tpt, boxed = sym.is(Mutable, butNot = Method) && !ccConfig.useSealed && !sym.hasAnnotation(defn.UncheckedCapturesAnnot), - // types of mutable variables are boxed in pre 3.3 code + // Under the sealed policy, we disallow root capabilities in the type of mutable + // variables, no need to box them here. ) catch case ex: IllegalCaptureRef => capt.println(i"fail while transforming result type $tpt of $sym") throw ex + + // Second step: Add descriptions for all capture set variables created in + // step one stating that they belong to `sym`. val addDescription = new TypeTraverser: def traverse(tp: Type) = tp match case tp @ CapturingType(parent, refs) => @@ -435,6 +453,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => traverseChildren(tp) addDescription.traverse(tpt.knownType) + end transformResultType def traverse(tree: Tree)(using Context): Unit = tree match @@ -449,7 +468,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: paramss.foreach(traverse) transformResultType(tpt, meth) traverse(tree.rhs) - //println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}") case tree @ ValDef(_, tpt: TypeTree, _) => val sym = tree.symbol @@ -457,7 +475,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val defCtx = if sym.isOneOf(TermParamOrAccessor) then ctx else ctx.withOwner(sym) inContext(defCtx): transformResultType(tpt, sym) - capt.println(i"mapped $tree = ${tpt.knownType}") traverse(tree.rhs) case tree @ TypeApply(fn, args) => @@ -486,30 +503,33 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: checkProperUse(tree) end traverse + /** Processing done on node `tree` after its children are traversed */ def postProcess(tree: Tree)(using Context): Unit = tree match case tree: TypeTree => transformTT(tree, boxed = false) case tree: ValOrDefDef => + // Make sure denotation of tree's symbol is correct val sym = tree.symbol - /** The return type of a constructor instantiated with local type and value - * parameters. Constructors have `unit` result type, that's why we can't - * get this type by reading the result type tree, and have to construct it - * explicitly. - */ + // The return type of a constructor instantiated with local type and value + // parameters. Constructor defs have `Unit` as result type tree, that's why + // we can't get this type by reading the result type tree, and have to construct + // it explicitly. def constrReturnType(info: Type, psymss: List[List[Symbol]]): Type = info match case info: MethodOrPoly => constrReturnType(info.instantiate(psymss.head.map(_.namedType)), psymss.tail) case _ => info - /** The local result type, which is the known type of the result type tree, - * with special treatment for constructors. - */ + // The local result type, which is the known type of the result type tree, + // with special treatment for constructors. def localReturnType = if sym.isConstructor then constrReturnType(sym.info, sym.paramSymss) else tree.tpt.knownType + // A test whether parameter signature might change. This returns true if one of + // the parameters has a remembered type. The idea here is that we store a remembered + // type only if the transformed type is different from the original. def paramSignatureChanges = tree.match case tree: DefDef => tree.paramss.nestedExists: @@ -523,7 +543,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: tree.tpt.hasRememberedType && !sym.isConstructor || paramSignatureChanges // Replace an existing symbol info with inferred types where capture sets of - // TypeParamRefs and TermParamRefs put in correspondence by BiTypeMaps with the + // TypeParamRefs and TermParamRefs are put in correspondence by BiTypeMaps with the // capture sets of the types of the method's parameter symbols and result type. def integrateRT( info: Type, // symbol info to replace @@ -560,6 +580,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if prevLambdas.isEmpty then resType else SubstParams(prevPsymss, prevLambdas)(resType) + // If there's a change in the signature, update the info of `sym` if sym.exists && signatureChanges then val newInfo = Existential.mapCapInResults(report.error(_, tree.srcPos)): @@ -576,9 +597,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // the expected type and only afterwards we recheck the definition newInfo else new LazyType: + // infos of other methods are determined from their definitions, which + // are checked on demand def complete(denot: SymDenotation)(using Context) = - // infos of other methods are determined from their definitions which - // are checked on demand assert(ctx.phase == thisPhase.next, i"$sym") capt.println(i"forcing $sym, printing = ${ctx.mode.is(Mode.Printing)}") //if ctx.mode.is(Mode.Printing) then new Error().printStackTrace() @@ -594,9 +615,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case cls: ClassSymbol => inNestedLevelUnless(cls.is(Module)): val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo - def innerModule = cls.is(ModuleClass) && !cls.isStatic + + // Compute new self type + def isInnerModule = cls.is(ModuleClass) && !cls.isStatic val selfInfo1 = - if (selfInfo ne NoType) && !innerModule then + if (selfInfo ne NoType) && !isInnerModule then // if selfInfo is explicitly given then use that one, except if // self info applies to non-static modules, these still need to be inferred selfInfo @@ -611,8 +634,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // self types (to which we also add nested module classes), provided they are // neither pure, nor are publicily extensible with an unconstrained self type. CapturingType(cinfo.selfType, CaptureSet.Var(cls, level = currentLevel)) + + // Compute new parent types val ps1 = inContext(ctx.withOwner(cls)): ps.mapConserve(transformExplicitType(_)) + + // Install new types and if it is a module class also update module object if (selfInfo1 ne selfInfo) || (ps1 ne ps) then val newInfo = ClassInfo(prefix, cls, ps1, decls, selfInfo1) updateInfo(cls, newInfo) @@ -627,6 +654,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => end postProcess + /** Check that @use annotations only appear on parameters and not on anonymous function parameters */ def checkProperUse(tree: Tree)(using Context): Unit = tree match case tree: MemberDef => def useAllowed(sym: Symbol) = @@ -638,7 +666,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: end checkProperUse end setupTraverser - /** Checks whether an abstract type could be impure. See also: [[needsVariable]]. */ +// --------------- Adding capture set variables ---------------------------------- + + /** Checks whether an abstract type or its bound could be impure. If that's the case, + * the abstract type gets a capture set variable in `decorate`. + * See also: [[needsVariable]]. + */ private def instanceCanBeImpure(tp: Type)(using Context): Boolean = { tp.dealiasKeepAnnots match case CapturingType(_, refs) => @@ -661,7 +694,13 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: false }.showing(i"instance can be impure $tp = $result", capt) - /** Should a capture set variable be added on type `tp`? */ + /** Should a capture set variable be added on type `tp`? + * Notable exceptions are: + * - If type refers to a class which is Pure, or it is Any, it does not need a variable. + * - If type is an abstract or parameter type we decide according to `instanceCanBeImpure` + * - If type is a capturing type that has already a capture set variable or has + * the universal capture set, it does not need a variable. + */ def needsVariable(tp: Type)(using Context): Boolean = { tp.typeParams.isEmpty && tp.match case tp: (TypeRef | AppliedType) => @@ -693,6 +732,61 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: false }.showing(i"can have inferred capture $tp = $result", captDebug) + /** Add a capture set variable or set to `tp` if necessary. + * Dealias `tp` (but keep annotations and opaque types) if doing + * so ends up adding a capture set. + * @param tp the type to add a capture set to + * @param added A function producing the added capture set from a set of initial elements. + */ + def decorate(tp: Type, added: CaptureSet.Refs => CaptureSet)(using Context): Type = + if tp.typeSymbol == defn.FromJavaObjectSymbol then + // For capture checking, we assume Object from Java is the same as Any + tp + else + def maybeAdd(target: Type, fallback: Type) = + if needsVariable(target) then + target match + case CapturingType(_, CaptureSet.Fluid) => + target + case CapturingType(target1, cs1) if cs1.isConst => + CapturingType(target1, added(cs1.elems)) + case _ => + CapturingType(target, added(SimpleIdentitySet.empty)) + else fallback + val dealiased = tp.dealiasKeepAnnotsAndOpaques + if dealiased ne tp then + val transformed = transformInferredType(dealiased) + maybeAdd(transformed, if transformed ne dealiased then transformed else tp) + else maybeAdd(tp, tp) + + /** Add a capture set variable to `tp` if necessary. */ + private def addVar(tp: Type, owner: Symbol)(using Context): Type = + decorate(tp, CaptureSet.Var(owner, _, level = currentLevel)) + + /** A map that adds capture sets at all contra- and invariant positions + * in a type where a capture set would be needed. This is used to make types + * that were not capture checked compatible with types that are capture checked. + * We don't need to add in covariant positions since pure types are + * anyway compatible with capturing types. + */ + private def fluidify(using Context) = new TypeMap with IdempotentCaptRefMap: + def apply(t: Type): Type = t match + case t: MethodType => + mapOver(t) + case t: TypeLambda => + t.derivedLambdaType(resType = this(t.resType)) + case CapturingType(_, _) => + t + case _ => + val t1 = t match + case t @ defn.RefinedFunctionOf(rinfo: MethodType) => + // Just refine the apply method, don't bother with the parent + t.derivedRefinedType(t.parent, t.refinedName, this(rinfo)) + case _ => + mapOver(t) + if variance > 0 then t1 + else decorate(t1, Function.const(CaptureSet.Fluid)) + /** Pull out an embedded capture set from a part of `tp` */ def normalizeCaptures(tp: Type)(using Context): Type = tp match case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) => @@ -724,32 +818,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => tp - /** Add a capture set variable to `tp` if necessary, or maybe pull out - * an embedded capture set variable from a part of `tp`. - */ - def decorate(tp: Type, addedSet: Type => CaptureSet)(using Context): Type = - if tp.typeSymbol == defn.FromJavaObjectSymbol then - // For capture checking, we assume Object from Java is the same as Any - tp - else - def maybeAdd(target: Type, fallback: Type) = - if needsVariable(target) then CapturingType(target, addedSet(target)) - else fallback - val dealiased = tp.dealiasKeepAnnotsAndOpaques - if dealiased ne tp then - val transformed = transformInferredType(dealiased) - maybeAdd(transformed, if transformed ne dealiased then transformed else tp) - else maybeAdd(tp, tp) - - /** Add a capture set variable to `tp` if necessary, or maybe pull out - * an embedded capture set variable from a part of `tp`. + /** Run setup on a compilation unit with given `tree`. + * @param recheckDef the function to run for completing a val or def */ - private def addVar(tp: Type, owner: Symbol)(using Context): Type = - decorate(tp, - addedSet = _.dealias.match - case CapturingType(_, refs) => CaptureSet.Var(owner, refs.elems, level = currentLevel) - case _ => CaptureSet.Var(owner, level = currentLevel)) - def setupUnit(tree: Tree, recheckDef: DefRecheck)(using Context): Unit = setupTraverser(recheckDef).traverse(tree)(using ctx.withPhase(thisPhase)) @@ -764,8 +835,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * This check is performed after capture sets are computed in phase cc. * Note: We need to perform the check on the original annotation rather than its * capture set since the conversion to a capture set already eliminates redundant elements. + * @param parent the parent of the capturing type + * @param ann the original retains annotation + * @param tpt the tree for which an error or warning should be reported */ - private def checkWellformedPost(parent: Type, ann: Tree, tpt: Tree)(using Context): Unit = + private def checkWellformed(parent: Type, ann: Tree, tpt: Tree)(using Context): Unit = capt.println(i"checkWF post $parent ${ann.retainedElems} in $tpt") var retained = ann.retainedElems.toArray for i <- 0 until retained.length do @@ -794,14 +868,18 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: check(remaining, remaining) end for end for - end checkWellformedPost + end checkWellformed - /** Check well formed at post check time */ + /** Check well formed at post check time. We need to wait until after + * recheck because we find out only then whether capture sets are empty or + * capture references are redundant. + */ private def checkWellformedLater(parent: Type, ann: Tree, tpt: Tree)(using Context): Unit = if !tpt.span.isZeroExtent && enclosingInlineds.isEmpty then todoAtPostCheck += (ctx1 => - checkWellformedPost(parent, ann, tpt)(using ctx1.withOwner(ctx.owner))) + checkWellformed(parent, ann, tpt)(using ctx1.withOwner(ctx.owner))) + /** Run all well formedness tests that were deferred to post check */ def postCheck()(using Context): Unit = for chk <- todoAtPostCheck do chk(ctx) todoAtPostCheck.clear()