diff --git a/compiler/src/dotty/tools/dotc/cc/Capability.scala b/compiler/src/dotty/tools/dotc/cc/Capability.scala index b69c8ac52b66..efdd6494788e 100644 --- a/compiler/src/dotty/tools/dotc/cc/Capability.scala +++ b/compiler/src/dotty/tools/dotc/cc/Capability.scala @@ -1035,14 +1035,13 @@ object Capabilities: else t match case t @ CapturingType(_, _) => mapOver(t) + case t @ AnnotatedType(parent, ann: RetainingAnnotation) + if ann.isStrict && ann.toCaptureSet.containsCap => + // Applying `this` can cause infinite recursion in some cases during printing. + // scalac -Xprint:all tests/pos/i23885/S_1.scala tests/pos/i23885/S_2.scala + mapOver(CapturingType(this(parent), ann.toCaptureSet)) case t @ AnnotatedType(parent, ann) => - val parent1 = this(parent) - if ann.symbol.isRetains && ann.tree.toCaptureSet.containsCap then - // Applying `this` can cause infinite recursion in some cases during printing. - // scalac -Xprint:all tests/pos/i23885/S_1.scala tests/pos/i23885/S_2.scala - mapOver(CapturingType(parent1, ann.tree.toCaptureSet)) - else - t.derivedAnnotatedType(parent1, ann) + t.derivedAnnotatedType(this(parent), ann) case defn.RefinedFunctionOf(_) => t // stop at dependent function types case _ => diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index af9a2abc7a9d..217588c12770 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -14,14 +14,11 @@ import Annotations.Annotation import CaptureSet.VarState import Capabilities.* import Mutability.isStatefulType -import StdNames.nme +import StdNames.{nme, tpnme} import config.Feature import NameKinds.TryOwnerName import typer.ProtoTypes.WildcardSelectionProto -/** Attachment key for capturing type trees */ -private val Captures: Key[CaptureSet] = Key() - /** Are we at checkCaptures phase? */ def isCaptureChecking(using Context): Boolean = ctx.phaseId == Phases.checkCapturesPhaseId @@ -54,24 +51,15 @@ def ccState(using Context): CCState = extension (tree: Tree) - /** Convert a @retains or @retainsByName annotation tree to the capture set it represents. - * For efficience, the result is cached as an Attachment on the tree. + /** The type representing the capture set of @retains, @retainsCap or @retainsByName + * annotation tree (represented as an Apply node). */ - def toCaptureSet(using Context): CaptureSet = - tree.getAttachment(Captures) match - case Some(refs) => refs - case None => - val refs = CaptureSet(tree.retainedSet.retainedElements*) - tree.putAttachment(Captures, refs) - refs - - /** The type representing the capture set of @retains, @retainsCap or @retainsByName annotation. */ - def retainedSet(using Context): Type = - tree match - case Apply(TypeApply(_, refs :: Nil), _) => refs.tpe - case _ => - if tree.symbol.maybeOwner == defn.RetainsCapAnnot - then defn.captureRoot.termRef else NoType + def retainedSet(using Context): Type = tree match + case Apply(TypeApply(_, refs :: Nil), _) => refs.tpe + case _ => + if tree.symbol.maybeOwner == defn.RetainsCapAnnot + then defn.captureRoot.termRef + else NoType extension (tp: Type) @@ -96,8 +84,8 @@ extension (tp: Type) def retainedElementsRaw(using Context): List[Type] = tp match case OrType(tp1, tp2) => tp1.retainedElementsRaw ++ tp2.retainedElementsRaw - case AnnotatedType(tp1, ann) if tp1.derivesFrom(defn.Caps_CapSet) && ann.symbol.isRetains => - ann.tree.retainedSet.retainedElementsRaw + case AnnotatedType(tp1, ann: RetainingAnnotation) if tp1.derivesFrom(defn.Caps_CapSet) => + ann.retainedType.retainedElementsRaw case tp => tp.dealiasKeepAnnots match case tp: TypeRef if tp.symbol == defn.Caps_CapSet => @@ -239,10 +227,12 @@ extension (tp: Type) case tp @ CapturingType(parent, refs) => if tp.isBoxed || parent.derivesFrom(defn.Caps_CapSet) then tp else tp.boxed + case tp @ AnnotatedType(parent, ann: RetainingAnnotation) + if !parent.derivesFrom(defn.Caps_CapSet) => + assert(ann.isStrict) + CapturingType(parent, ann.toCaptureSet, boxed = true) case tp @ AnnotatedType(parent, ann) => - if ann.symbol.isRetains && !parent.derivesFrom(defn.Caps_CapSet) - then CapturingType(parent, ann.tree.toCaptureSet, boxed = true) - else tp.derivedAnnotatedType(parent.boxDeeply, ann) + tp.derivedAnnotatedType(parent.boxDeeply, ann) case tp: (Capability & SingletonType) if tp.isTrackableRef && !tp.isAlwaysPure => recur(CapturingType(tp, CaptureSet(tp))) case tp1 @ AppliedType(tycon, args) if defn.isNonRefinedFunction(tp1) => @@ -502,8 +492,7 @@ extension (cls: ClassSymbol) defn.pureBaseClasses.contains(bc) || bc.is(CaptureChecked) && bc.givenSelfType.dealiasKeepAnnots.match - case CapturingType(_, refs) => refs.isAlwaysEmpty - case RetainingType(_, refs) => refs.retainedElements.isEmpty + case CapturingOrRetainsType(_, refs) => refs.isAlwaysEmpty case selfType => isCaptureChecking // At Setup we have not processed self types yet, so // unless a self type is explicitly given, we can't tell @@ -540,13 +529,23 @@ extension (cls: ClassSymbol) extension (sym: Symbol) - /** This symbol is one of `retains` or `retainsCap` */ + private def inScalaAnnotation(using Context): Boolean = + sym.maybeOwner.name == tpnme.annotation + && sym.owner.owner == defn.ScalaPackageClass + + /** Is this symbol one of `retains` or `retainsCap`? + * Try to avoid cycles by not forcing definition symbols except scala package. + */ def isRetains(using Context): Boolean = - sym == defn.RetainsAnnot || sym == defn.RetainsCapAnnot + (sym.name == tpnme.retains || sym.name == tpnme.retainsCap) + && inScalaAnnotation - /** This symbol is one of `retains`, `retainsCap`, or`retainsByName` */ + /** Is this symbol one of `retains`, `retainsCap`, or`retainsByName`? + * Try to avoid cycles by not forcing definition symbols except scala package. + */ def isRetainsLike(using Context): Boolean = - isRetains || sym == defn.RetainsByNameAnnot + (sym.name == tpnme.retains || sym.name == tpnme.retainsCap || sym.name == tpnme.retainsByName) + && inScalaAnnotation /** A class is pure if: * - one its base types has an explicitly declared self type with an empty capture set @@ -653,16 +652,16 @@ class PathSelectionProto(val select: Select, val pt: Type) extends typer.ProtoTy def selector(using Context): Symbol = select.symbol /** Drop retains annotations in the inferred type if CC is not enabled - * or transform them into RetainingTypes if CC is enabled. + * or transform them into retains annotations with Nothing (i.e. empty set) as + * argument if CC is enabled (we need to do that to keep by-name status). */ class CleanupRetains(using Context) extends TypeMap: def apply(tp: Type): Type = tp match - case AnnotatedType(parent, annot) if annot.symbol.isRetainsLike => + case tp @ AnnotatedType(parent, annot: RetainingAnnotation) => if Feature.ccEnabled then - if annot.symbol == defn.RetainsAnnot || annot.symbol == defn.RetainsByNameAnnot then - RetainingType(parent, defn.NothingType, byName = annot.symbol == defn.RetainsByNameAnnot) - else mapOver(tp) - else apply(parent) + if annot.symbol == defn.RetainsCapAnnot then tp + else AnnotatedType(this(parent), RetainingAnnotation(annot.symbol.asClass, defn.NothingType)) + else this(parent) case _ => mapOver(tp) /** A base class for extractors that match annotated types with a specific diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index d67e344830b1..5670129f3f56 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -536,10 +536,6 @@ sealed abstract class CaptureSet extends Showable: /** More info enabled by -Y flags */ def optionalInfo(using Context): String = "" - /** A regular @retains or @retainsByName annotation with the elements of this set as arguments. */ - def toRegularAnnotation(cls: Symbol)(using Context): Annotation = - Annotation(CaptureAnnotation(this, boxed = false)(cls).tree) - override def toText(printer: Printer): Text = printer.toTextCaptureSet(this) ~~ description @@ -1676,10 +1672,8 @@ object CaptureSet: case tp: (TypeRef | TypeParamRef) => if tp.derivesFrom(defn.Caps_CapSet) then tp.captureSet else empty - case CapturingType(parent, refs) => + case CapturingOrRetainsType(parent, refs) => recur(parent) ++ refs - case tp @ AnnotatedType(parent, ann) if ann.symbol.isRetains => - recur(parent) ++ ann.tree.toCaptureSet case tpd @ defn.RefinedFunctionOf(rinfo: MethodOrPoly) if followResult => ofType(tpd.parent, followResult = false) // pick up capture set from parent type ++ recur(rinfo.resType).freeInResult(rinfo) // add capture set of result diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index be8bccdcdaf1..1e4970c175b2 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -60,7 +60,7 @@ object CapturingType: case AnnotatedType(parent, ann: CaptureAnnotation) if isCaptureCheckingOrSetup => Some((parent, ann.refs)) - case AnnotatedType(parent, ann) if ann.symbol.isRetains && alsoRetains => + case AnnotatedType(parent, ann: RetainingAnnotation) if ann.isStrict && alsoRetains => // There are some circumstances where we cannot map annotated types // with retains annotations to capturing types, so this second recognizer // path still has to exist. One example is when checking capture sets @@ -75,7 +75,7 @@ object CapturingType: // // TODO In other situations we expect that the type is already transformed to a // CapturingType and we should crash if this not the case. - try Some((parent, ann.tree.toCaptureSet)) + try Some((parent, ann.toCaptureSet)) catch case ex: IllegalCaptureRef => None case _ => None diff --git a/compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala new file mode 100644 index 000000000000..96f83354f5cf --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala @@ -0,0 +1,49 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.* +import Annotations.{Annotation, CompactAnnotation, EmptyAnnotation} +import config.Feature + +/** A class for annotations @retains, @retainsByName and @retainsCap */ +class RetainingAnnotation(tpe: Type) extends CompactAnnotation(tpe): + + def this(cls: ClassSymbol, args: Type*)(using Context) = this(cls.typeRef.appliedTo(args.toList)) + + /** Sanitize @retains arguments to approximate illegal types that could cause a compilation + * time blowup before they are dropped ot detected. This means mapping all all skolems + * (?n: T) to (?n: Any), and mapping all recursive captures that are not on CapSet to `^`. + * Skolems and capturing types on types other than CapSet are not allowed in a + * @retains annotation anyway, so the underlying type does not matter as long as it is also + * illegal. See i24556.scala and i24556a.scala. + */ + override protected def sanitize(tp: Type)(using Context): Type = tp match + case SkolemType(_) => + SkolemType(defn.AnyType) + case tp @ AnnotatedType(parent, ann: RetainingAnnotation) + if parent.typeSymbol != defn.Caps_CapSet && ann.symbol != defn.RetainsCapAnnot => + AnnotatedType(parent, RetainingAnnotation(defn.RetainsCapAnnot)) + case tp @ OrType(tp1, tp2) => + tp.derivedOrType(sanitize(tp1), sanitize(tp2)) + case _ => + tp + + override def mapWith(tm: TypeMap)(using Context): Annotation = + if Feature.ccEnabledSomewhere then mapWithCtd(tm) else EmptyAnnotation + + def isStrict(using Context): Boolean = symbol.isRetains + + def retainedType(using Context): Type = + if symbol == defn.RetainsCapAnnot then defn.captureRoot.termRef + else argumentType(0) + + private var myCaptureSet: CaptureSet | Null = null + + def toCaptureSet(using Context): CaptureSet = + if myCaptureSet == null then + myCaptureSet = CaptureSet(retainedType.retainedElements*) + myCaptureSet.nn + +end RetainingAnnotation diff --git a/compiler/src/dotty/tools/dotc/cc/RetainingType.scala b/compiler/src/dotty/tools/dotc/cc/RetainingType.scala deleted file mode 100644 index 6fde63a0d3ac..000000000000 --- a/compiler/src/dotty/tools/dotc/cc/RetainingType.scala +++ /dev/null @@ -1,29 +0,0 @@ -package dotty.tools -package dotc -package cc - -import core.* -import Types.*, Symbols.*, Contexts.* -import ast.tpd.* -import Annotations.Annotation -import Decorators.i - -/** A builder and extractor for annotated types with @retains or @retainsByName annotations - * excluding CapturingTypes. - */ -object RetainingType: - - def apply(tp: Type, typeElems: Type, byName: Boolean = false)(using Context): Type = - val annotCls = if byName then defn.RetainsByNameAnnot else defn.RetainsAnnot - val annotTree = New(AppliedType(annotCls.typeRef, typeElems :: Nil), Nil) - AnnotatedType(tp, Annotation(annotTree)) - - def unapply(tp: AnnotatedType)(using Context): Option[(Type, Type)] = - val sym = tp.annot.symbol - if sym.isRetainsLike then - tp.annot match - case _: CaptureAnnotation => None - case ann => Some((tp.parent, ann.tree.retainedSet)) - else - None -end RetainingType diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 6f1070c7ddea..482f23af71bb 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -919,7 +919,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: assert(mtps.hasSameLengthAs(argss), i"diff for $fn: ${fn.symbol} /// $mtps /// $argss") val mtpsWithArgs = mtps.zip(argss) val argMap = mtpsWithArgs.toMap - val deps = mutable.HashMap[Tree, List[Tree]]().withDefaultValue(Nil) + val deps = mutable.LinkedHashMap[Tree, List[Tree]]().withDefaultValue(Nil) def argOfDep(dep: Capability): Option[Tree] = dep.stripReach match diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 5c8c581f729c..0e0728e01668 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -433,23 +433,21 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case t @ CapturingType(parent, refs) => checkRetainsOK: t.derivedCapturingType(stripImpliedCaptureSet(this(parent)), refs) - case t @ AnnotatedType(parent, ann) => - val parent1 = this(parent) - if ann.symbol.isRetains then - val parent2 = stripImpliedCaptureSet(parent1) + case t @ AnnotatedType(parent, ann: RetainingAnnotation) if ann.isStrict => + val parent1 = stripImpliedCaptureSet(this(parent)) + if !tptToCheck.isEmpty then + checkWellformedLater(parent1, ann, tptToCheck) + try + checkRetainsOK: + CapturingType(parent1, ann.toCaptureSet) + catch case ex: IllegalCaptureRef => if !tptToCheck.isEmpty then - checkWellformedLater(parent2, ann.tree, tptToCheck) - try - checkRetainsOK: - CapturingType(parent2, ann.tree.toCaptureSet) - catch case ex: IllegalCaptureRef => - if !tptToCheck.isEmpty then - report.error(em"Illegal capture reference: ${ex.getMessage}", tptToCheck.srcPos) - parent2 - else if ann.symbol == defn.UncheckedCapturesAnnot then - makeUnchecked(apply(parent)) - else - t.derivedAnnotatedType(parent1, ann) + report.error(em"Illegal capture reference: ${ex.getMessage}", tptToCheck.srcPos) + parent1 + case t @ AnnotatedType(parent, ann) => + if ann.symbol == defn.UncheckedCapturesAnnot + then makeUnchecked(this(parent)) + else t.derivedAnnotatedType(this(parent), ann) case throwsAlias(res, exc) => this(expandThrowsAlias(res, exc, Nil)) case t @ AppliedType(tycon, args) @@ -815,10 +813,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: */ private def instanceCanBeImpure(tp: Type)(using Context): Boolean = { tp.dealiasKeepAnnots match - case CapturingType(_, refs) => + case CapturingOrRetainsType(_, refs) => !refs.isAlwaysEmpty - case RetainingType(parent, refs) => - !refs.retainedElements.isEmpty case tp: (TypeRef | AppliedType) => val sym = tp.typeSymbol if sym.isClass @@ -858,15 +854,10 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: needsVariable(tp.tp1) && needsVariable(tp.tp2) case tp: OrType => needsVariable(tp.tp1) || needsVariable(tp.tp2) - case CapturingType(parent, refs) => + case CapturingOrRetainsType(parent, refs) => needsVariable(parent) && refs.isConst // if refs is a variable, no need to add another && !refs.isUniversal // if refs is {cap}, an added variable would not change anything - case RetainingType(parent, refs) => - needsVariable(parent) - && !refs.retainedElements.exists: - case ref: TermRef => ref.isCapRef - case _ => false case AnnotatedType(parent, _) => needsVariable(parent) case _ => @@ -972,15 +963,13 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * @param ann the original retains annotation * @param tpt the tree for which an error or warning should be reported */ - private def checkWellformed(parent: Type, ann: Tree, tpt: Tree)(using Context): Unit = - capt.println(i"checkWF post $parent ${ann.retainedSet} in $tpt") + private def checkWellformed(parent: Type, ann: RetainingAnnotation, tpt: Tree)(using Context): Unit = + capt.println(i"checkWF post $parent ${ann.retainedType} in $tpt") try - var retained = ann.retainedSet.retainedElements.toArray + var retained = ann.retainedType.retainedElements.toArray for i <- 0 until retained.length do val ref = retained(i) - def pos = - if ann.span.exists then ann.srcPos - else tpt.srcPos + def pos = tpt.srcPos def check(others: CaptureSet, dom: Type | CaptureSet): Unit = if others.accountsFor(ref) then @@ -1013,7 +1002,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * recheck because we find out only then whether capture sets are empty or * capabilities are redundant. */ - private def checkWellformedLater(parent: Type, ann: Tree, tpt: Tree)(using Context): Unit = + private def checkWellformedLater(parent: Type, ann: RetainingAnnotation, tpt: Tree)(using Context): Unit = if !tpt.span.isZeroExtent && enclosingInlineds.isEmpty then todoAtPostCheck += (ctx1 => checkWellformed(parent, ann, tpt)(using ctx1.withOwner(ctx.owner))) diff --git a/compiler/src/dotty/tools/dotc/core/Annotations.scala b/compiler/src/dotty/tools/dotc/core/Annotations.scala index d8645e4e2e4e..b177b20f4b21 100644 --- a/compiler/src/dotty/tools/dotc/core/Annotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Annotations.scala @@ -7,7 +7,7 @@ import ast.tpd, tpd.* import util.Spans.Span import printing.{Showable, Printer} import printing.Texts.Text -import cc.isRetainsLike +import cc.{isRetainsLike, RetainingAnnotation} import config.Feature import Decorators.* @@ -36,6 +36,10 @@ object Annotations { /** All term arguments of this annotation in a single flat list */ def arguments(using Context): List[Tree] = tpd.allTermArguments(tree) + /** All type arguments of this annotation in a single flat list */ + def argumentTypes(using Context): List[Type] = + tpd.allArguments(tree).filterConserve(_.isType).tpes + def argument(i: Int)(using Context): Option[Tree] = { val args = arguments if (i < args.length) Some(args(i)) else None @@ -59,43 +63,8 @@ object Annotations { def mapWith(tm: TypeMap)(using Context): Annotation = tpd.allArguments(tree) match case Nil => this - case arg :: Nil if symbol.isRetainsLike => - // Use a more efficient scheme to map retains and retainsByName annotations: - // 1. Map the type argument to a simple TypeTree instead of tree-mapping - // the original tree. TODO Try to use this scheme for other annotations that - // take only type arguments as well. We should wait until after 3.9 LTS to - // do this, though. - // 2. Map all skolems (?n: T) to (?n: Any), and map all recursive captures of - // that are not on CapSet to `^`. Skolems and capturing types on types - // other than CapSet are not allowed in a retains annotation anyway, - // so the underlying type does not matter. This simplification prevents - // exponential blowup in some cases. See i24556.scala and i24556a.scala. - // 3. Drop the annotation entirely if CC is not enabled somehwere. - - def sanitize(tp: Type): Type = tp match - case SkolemType(_) => - SkolemType(defn.AnyType) - case tp @ AnnotatedType(parent, ann) - if ann.symbol.isRetainsLike && parent.typeSymbol != defn.Caps_CapSet => - tp.derivedAnnotatedType(parent, Annotation(defn.RetainsCapAnnot, ann.tree.span)) - case tp @ OrType(tp1, tp2) => - tp.derivedOrType(sanitize(tp1), sanitize(tp2)) - case _ => - tp - - def rebuild(tree: Tree, mappedType: Type): Tree = tree match - case Apply(fn, Nil) => cpy.Apply(tree)(rebuild(fn, mappedType), Nil) - case TypeApply(fn, arg :: Nil) => cpy.TypeApply(tree)(fn, TypeTree(mappedType) :: Nil) - case Block(Nil, expr) => rebuild(expr, mappedType) - - if !Feature.ccEnabledSomewhere then - EmptyAnnotation // strip retains-like annotations unless capture checking is enabled - else - val mappedType = sanitize(tm(arg.tpe)) - if mappedType `eql` arg.tpe then this - else derivedAnnotation(rebuild(tree, mappedType)) - + assert(false, s"unexpected symbol $symbol for ConcreteAnnotation $this in ${ctx.source}, this should be a CompactAnnotation") case args => // Checks if `tm` would result in any change by applying it to types // inside the annotations' arguments and checking if the resulting types @@ -114,17 +83,12 @@ object Annotations { /** Does this annotation refer to a parameter of `tl`? */ def refersToParamOf(tl: TermLambda)(using Context): Boolean = - def isLambdaParam(t: Type) = t match - case TermParamRef(tl1, _) => tl eq tl1 - case _ => false - val acc = new TreeAccumulator[Boolean]: def apply(x: Boolean, t: Tree)(using Context) = if x then true - else if t.isType then - t.tpe.existsPart(isLambdaParam, stopAt = StopAt.Static) + else if t.isType then refersToLambdaParam(t.tpe, tl) else t match - case id: (Ident | This) => isLambdaParam(id.tpe.stripped) + case id: (Ident | This) => isLambdaParam(id.tpe.stripped, tl) case _ => foldOver(x, t) tpd.allArguments(tree).exists(acc(false, _)) @@ -163,6 +127,85 @@ object Annotations { case class ConcreteAnnotation(t: Tree) extends Annotation: def tree(using Context): Tree = t + /** A class for optimized, compact annotations that are defined by a type + * instead of a tree. This makes mapping such annotations a lot faster and safer. + * In fact, in retrospect, most annotations would better be represented as + * CompactAnnotations. + * + * CompactAnnotation is extended by cc.RetainingAnnotation, which is reserved + * for @retains, @retainsByName and @retainsCap. For now there are no + * CompactAnnotations other than RetainingAnnotations but this could be changed + * in the future, after 3.9 has shipped. + */ + class CompactAnnotation(val tpe: Type) extends Annotation: + assert(tpe.isInstanceOf[AppliedType | TypeRef], tpe) + + def tree(using Context) = TypeTree(tpe) + + override def symbol(using Context) = tpe.typeSymbol + + override def derivedAnnotation(tree: Tree)(using Context): Annotation = + derivedAnnotation(tree.tpe) + + def derivedAnnotation(tp: Type)(using Context): Annotation = + if tp eq this.tpe then this else CompactAnnotation(tp) + + override def arguments(using Context): List[Tree] = + argumentTypes.map(TypeTree(_)) + + override def argumentTypes(using Context): List[Type] = tpe.argTypes + + def argumentType(i: Int)(using Context): Type = + val args = argumentTypes + if i < args.length then args(i) else NoType + + override def argumentConstant(i: Int)(using Context): Option[Constant] = + argumentType(i).normalized match + case ConstantType(c) => Some(c) + case _ => None + + /** A hook to transform the type argument of a mapped annotation. Overridden in + * RetainingAnnotation to avoid compilation time blowups for annotations that + * are not valid capture annotations. + */ + protected def sanitize(tp: Type)(using Context): Type = tp + + protected def mapWithCtd(tm: TypeMap)(using Context): Annotation = tm(tpe) match + case tp1 @ AppliedType(tycon, args) => + derivedAnnotation(tp1.derivedAppliedType(tycon, args.mapConserve(sanitize))) + case tp1: TypeRef => + derivedAnnotation(tp1) + case _ => + EmptyAnnotation + + override def mapWith(tm: TypeMap)(using Context): Annotation = + assert(!symbol.isRetainsLike) + mapWithCtd(tm) + + override def refersToParamOf(tl: TermLambda)(using Context): Boolean = + refersToLambdaParam(tpe, tl) + + override def hash: Int = tpe.hash + override def eql(that: Annotation) = that match + case that: CompactAnnotation => this.tpe `eql` that.tpe + case _ => false + + object CompactAnnotation: + def apply(tp: Type)(using Context): CompactAnnotation = + if tp.typeSymbol.isRetainsLike then RetainingAnnotation(tp) + else new CompactAnnotation(tp) + def apply(tree: Tree)(using Context): CompactAnnotation = + val argTypes = tpd.allArguments(tree).map(_.tpe) + apply(annotClass(tree).typeRef.appliedTo(argTypes)) + end CompactAnnotation + + private def isLambdaParam(t: Type, tl: TermLambda): Boolean = t match + case TermParamRef(tl1, _) => tl eq tl1 + case _ => false + + private def refersToLambdaParam(tp: Type, tl: TermLambda)(using Context): Boolean = + tp.existsPart(isLambdaParam(_, tl), stopAt = StopAt.Static) + abstract class LazyAnnotation extends Annotation { protected var mySym: Symbol | (Context ?=> Symbol) | Null override def symbol(using parentCtx: Context): Symbol = @@ -244,7 +287,12 @@ object Annotations { object Annotation { - def apply(tree: Tree): ConcreteAnnotation = ConcreteAnnotation(tree) + def apply(tree: Tree)(using Context): Annotation = tree match + case tree: TypeTree => + CompactAnnotation(tree.tpe) + case _ => + if annotClass(tree).isRetainsLike then CompactAnnotation(tree) + else ConcreteAnnotation(tree) def apply(cls: ClassSymbol, span: Span)(using Context): Annotation = apply(cls, Nil, span) @@ -259,7 +307,9 @@ object Annotations { apply(atp, arg :: Nil, span) def apply(atp: Type, args: List[Tree], span: Span)(using Context): Annotation = - apply(New(atp, args).withSpan(span)) + if atp.typeSymbol.isRetainsLike && args.isEmpty + then RetainingAnnotation(atp) + else apply(New(atp, args).withSpan(span)) /** Create an annotation where the tree is computed lazily. */ def deferred(sym: Symbol)(treeFn: Context ?=> Tree): Annotation = @@ -301,7 +351,7 @@ object Annotations { * to indicate that the resulting typemap should drop the annotation * (in derivedAnnotatedType). */ - @sharable val EmptyAnnotation = Annotation(EmptyTree) + @sharable val EmptyAnnotation = ConcreteAnnotation(EmptyTree) def ThrowsAnnotation(cls: ClassSymbol)(using Context): Annotation = { val tref = cls.typeRef diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 8d305eef16e1..c002ccce874e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -15,7 +15,7 @@ import Comments.{Comment, docCtx} import util.Spans.NoSpan import config.Feature import Symbols.requiredModuleRef -import cc.{CaptureSet, RetainingType} +import cc.{CaptureSet, RetainingAnnotation} import ast.tpd.ref import scala.annotation.tailrec @@ -122,8 +122,9 @@ class Definitions { denot.info = TypeAlias( HKTypeLambda(argParamNames :+ "R".toTypeName, argVariances :+ Covariant)( tl => List.fill(arity + 1)(TypeBounds.empty), - tl => RetainingType(underlyingClass.typeRef.appliedTo(tl.paramRefs), - captureRoot.termRef) + tl => AnnotatedType( + underlyingClass.typeRef.appliedTo(tl.paramRefs), + RetainingAnnotation(defn.RetainsCapAnnot)) )) else val cls = denot.asClass.classSymbol @@ -1347,8 +1348,8 @@ class Definitions { */ object ByNameFunction: def apply(tp: Type)(using Context): Type = tp match - case tp @ RetainingType(tp1, refSet) if tp.annot.symbol == RetainsByNameAnnot => - RetainingType(apply(tp1), refSet) + case tp @ AnnotatedType(tp1, ann: RetainingAnnotation) if ann.symbol == RetainsByNameAnnot => + AnnotatedType(apply(tp1), RetainingAnnotation(defn.RetainsAnnot, ann.argumentTypes*)) case _ => defn.ContextFunction0.typeRef.appliedTo(tp :: Nil) def unapply(tp: Type)(using Context): Option[Type] = tp match diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index dc54d14b0d4b..be3b828b6edc 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4227,8 +4227,8 @@ object Types extends TypeUtils { val parent1 = mapOver(parent) if ann.symbol.isRetainsLike then range( - AnnotatedType(parent1, CaptureSet.empty.toRegularAnnotation(ann.symbol)), - AnnotatedType(parent1, CaptureSet.universal.toRegularAnnotation(ann.symbol))) + AnnotatedType(parent1, RetainingAnnotation(defn.RetainsAnnot, defn.NothingType)), + AnnotatedType(parent1, RetainingAnnotation(defn.RetainsCapAnnot))) else parent1 case _ => mapOver(tp) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 1703a8a87d6e..0146f8f5b896 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -283,8 +283,13 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { } case tpe: AnnotatedType => writeByte(ANNOTATEDtype) - withLength { pickleType(tpe.parent, richTypes); pickleTree(tpe.annot.tree) } - annotatedTypeTrees += tpe.annot.tree + withLength: + pickleType(tpe.parent, richTypes) + tpe.annot match + case ann: CompactAnnotation => pickleType(ann.tpe) + case ann => + pickleTree(ann.tree) + annotatedTypeTrees += ann.tree case tpe: AndType => writeByte(ANDtype) withLength { pickleType(tpe.tp1, richTypes); pickleType(tpe.tp2, richTypes) } diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 03e8bad654f7..481843b9f8b6 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -354,6 +354,11 @@ class TreeUnpickler(reader: TastyReader, op } + /** Can `tag` start a type argument of a CompactAnnotation? */ + def isCompactAnnotTypeTag(tag: Int): Boolean = tag match + case APPLIEDtype | SHAREDtype | TYPEREF | TYPEREFdirect | TYPEREFsymbol | TYPEREFin => true + case _ => false + def readLengthType(): Type = { val end = readEnd() @@ -420,7 +425,12 @@ class TreeUnpickler(reader: TastyReader, val hi = readVariances(readType()) createNullableTypeBounds(lo, hi) case ANNOTATEDtype => - AnnotatedType(readType(), Annotation(readTree())) + val parent = readType() + val ann = + if isCompactAnnotTypeTag(nextByte) + then CompactAnnotation(readType()) + else Annotation(readTree()) + AnnotatedType(parent, ann) case ANDtype => AndType(readType(), readType()) case ORtype => diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 8d43113bad2c..436862e4113a 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -291,10 +291,6 @@ class PlainPrinter(_ctx: Context) extends Printer { && (!parent.derivesFromStateful || refs.isReadOnly) then toText(parent) else toTextCapturing(parent, refs, boxText) - case tp @ RetainingType(parent, refSet) => - if Feature.ccEnabledSomewhere then - toTextCapturing(parent, refSet.retainedElementsRaw, "") ~ Str("R").provided(printDebug) - else toText(parent) case tp: PreviousErrorType if ctx.settings.XprintTypes.value => "" // do not print previously reported error message because they may try to print this error type again recursively case tp: ErrorType => @@ -317,8 +313,8 @@ class PlainPrinter(_ctx: Context) extends Printer { } case ExprType(restp) => def arrowText: Text = restp match - case AnnotatedType(parent, ann) if ann.symbol == defn.RetainsByNameAnnot => - ann.tree.retainedSet.retainedElementsRaw match + case AnnotatedType(parent, ann: RetainingAnnotation) if !ann.isStrict => + ann.retainedType.retainedElementsRaw match case ref :: Nil if ref.isCapRef => Str("=>") case refs => Str("->") ~ toTextRetainedElems(refs) case _ => @@ -335,12 +331,18 @@ class PlainPrinter(_ctx: Context) extends Printer { toTextGlobal(tp.resultType) } case AnnotatedType(tpe, annot) => - if defn.SilentAnnots.contains(annot.symbol) && !printDebug then - toText(tpe) - else if annot.isInstanceOf[CaptureAnnotation] then - toTextLocal(tpe) ~ "^" ~ toText(annot) - else - toTextLocal(tpe) ~ " " ~ toText(annot) + annot match + case annot: RetainingAnnotation => + if Feature.ccEnabledSomewhere then + toTextCapturing(tpe, annot.retainedType.retainedElementsRaw, "") + ~ Str("R").provided(printDebug) + else toText(tpe) + case annot: CaptureAnnotation => + toTextLocal(tpe) ~ "^" ~ toText(annot) + case _ if defn.SilentAnnots.contains(annot.symbol) && !printDebug => + toText(tpe) + case _ => + toTextLocal(tpe) ~ " " ~ toText(annot) case FlexibleType(_, tpe) => "(" ~ toText(tpe) ~ ")?" case tp: TypeVar => diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 37222fba3ee3..5a6124650de8 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -705,7 +705,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { toTextTemplate(tree) case Annotated(arg, annot) => def captureSet = - annot.asInstanceOf[tpd.Tree].toCaptureSet + CaptureSet(annot.asInstanceOf[tpd.Tree].retainedSet.retainedElements*) def toTextAnnot = toTextLocal(arg) ~~ annotText(annot.symbol.enclosingClass, annot) def toTextRetainsAnnot = @@ -1165,6 +1165,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case Select(qual, nme.CONSTRUCTOR) => recur(qual) case id @ Ident(tpnme.BOUNDTYPE_ANNOT) => "@" ~ toText(id.symbol.name) case New(tpt) => recur(tpt) + case t: tpd.TypeTree if t.tpe.isInstanceOf[AppliedType] => "@" ~ toText(t.tpe) case _ => val annotSym = sym.orElse(tree.symbol.enclosingClass) if annotSym.exists then annotText(annotSym) else s"@${t.show}" diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 62561ad2d467..cb3f156a64a6 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -201,8 +201,10 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => inJavaAnnot = annot.symbol.is(JavaDefined) if (inJavaAnnot) checkValidJavaAnnotation(annot) try - val annotCtx = if annot.hasAttachment(untpd.RetainsAnnot) - then ctx.addMode(Mode.InCaptureSet) else ctx + val annotCtx = + if annot.hasAttachment(untpd.RetainsAnnot) + then ctx.addMode(Mode.InCaptureSet) + else ctx transform(annot)(using annotCtx) finally inJavaAnnot = saved } diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index 0b0be661d2f7..35bf490cd74b 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -24,7 +24,7 @@ import collection.mutable import ProtoTypes.* import staging.StagingLevel import inlines.Inlines.inInlineMethod -import cc.{isRetainsLike, CaptureAnnotation} +import cc.RetainingAnnotation import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions @@ -187,7 +187,7 @@ object TreeChecker { case tp: TypeVar => assert(tp.isInstantiated, s"Uninstantiated type variable: ${tp.show}, tree = ${tree.show}") apply(tp.underlying) - case tp @ AnnotatedType(underlying, annot) if annot.symbol.isRetainsLike && !annot.isInstanceOf[CaptureAnnotation] => + case tp @ AnnotatedType(underlying, annot: RetainingAnnotation) => val underlying1 = this(underlying) val annot1 = insideRetainingAnnot: annot.mapWith(this) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index e7e4c52d40c7..6000e403a68f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -39,7 +39,6 @@ import annotation.threadUnsafe import scala.annotation.tailrec import scala.util.control.NonFatal -import dotty.tools.dotc.cc.isRetains object Applications { import tpd.* diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index d975e2341111..a8f0708df39e 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -37,7 +37,7 @@ import config.Feature, Feature.{sourceVersion, modularity} import config.SourceVersion.* import config.MigrationVersion import printing.Formatting.hlAsKeyword -import cc.{isCaptureChecking, isRetainsLike} +import cc.{isCaptureChecking, RetainingAnnotation} import cc.Mutability.isUpdateMethod import collection.mutable @@ -815,7 +815,7 @@ object Checking { declaredParents = tp.declaredParents.map(p => transformedParent(apply(p))) ) - case tp @ AnnotatedType(underlying, annot) if annot.symbol.isRetainsLike => + case tp @ AnnotatedType(underlying, annot: RetainingAnnotation) => val underlying1 = this(underlying) val saved = inCaptureSet inCaptureSet = true diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index 55b6384c9e89..2c74a50b8f52 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -11,7 +11,6 @@ import NameOps.* import collection.mutable import reporting.* import Checking.{checkNoPrivateLeaks, checkNoWildcard} -import cc.CaptureSet import util.Property import transform.Splicer @@ -570,10 +569,14 @@ trait TypeAssigner { def assignType(tree: untpd.Export)(using Context): Export = tree.withType(defn.UnitType) - def assignType(tree: untpd.Annotated, arg: Tree, annot: Tree)(using Context): Annotated = { + def assignType(tree: untpd.Annotated, arg: Tree, annotTree: Tree)(using Context): Annotated = assert(tree.isType) // annotating a term is done via a Typed node, can't use Annotate directly - tree.withType(AnnotatedType(arg.tpe, Annotation(annot))) - } + if annotClass(annotTree).exists then + tree.withType(AnnotatedType(arg.tpe, Annotation(annotTree))) + else + // this can happen if cyclic reference errors occurred when typing the annotation + tree.withType( + errorType(em"Malformed annotation $tree, will be ignored", annotTree.srcPos)) def assignType(tree: untpd.PackageDef, pid: Tree)(using Context): PackageDef = tree.withType(pid.symbol.termRef) diff --git a/tests/neg-custom-args/captures/lazyvals.check b/tests/neg-custom-args/captures/lazyvals.check index b84a75f7bcd3..190f58d08817 100644 --- a/tests/neg-custom-args/captures/lazyvals.check +++ b/tests/neg-custom-args/captures/lazyvals.check @@ -16,7 +16,7 @@ | Note that capability console is not included in capture set {x}. | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/lazyvals.scala:16:18 ---------------------------------------------------------- +-- Error: tests/neg-custom-args/captures/lazyvals.scala:16:12 ---------------------------------------------------------- 16 | val fun3: () ->{x} String = () => x() // error // error - | ^ - | (x : () -> String) cannot be tracked since its capture set is empty + | ^^^^^^^^^^^^^^^ + | (x : () -> String) cannot be tracked since its capture set is empty diff --git a/tests/neg-custom-args/captures/spread-problem.check b/tests/neg-custom-args/captures/spread-problem.check index 031c87629838..810381b61859 100644 --- a/tests/neg-custom-args/captures/spread-problem.check +++ b/tests/neg-custom-args/captures/spread-problem.check @@ -7,6 +7,8 @@ 11 | race(src1, src2) // error | ^^^^^^^^^^ | Found: (Source[T]^, Source[T]^) - | Required: Seq[Source[T]^] + | Required: Seq[Source[T]^{C}] + | + | where: C is a type variable with constraint >: scala.caps.CapSet and <: scala.caps.CapSet^ | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/wf-reach-1.check b/tests/neg-custom-args/captures/wf-reach-1.check index 6a3ac9771a11..a61edede72e2 100644 --- a/tests/neg-custom-args/captures/wf-reach-1.check +++ b/tests/neg-custom-args/captures/wf-reach-1.check @@ -1,4 +1,4 @@ --- Error: tests/neg-custom-args/captures/wf-reach-1.scala:2:17 --------------------------------------------------------- +-- Error: tests/neg-custom-args/captures/wf-reach-1.scala:2:9 ---------------------------------------------------------- 2 | val y: Object^{x*} = ??? // error - | ^^ - | x* cannot be tracked since its deep capture set is empty + | ^^^^^^^^^^^ + | x* cannot be tracked since its deep capture set is empty diff --git a/tests/pos/Annotations.scala b/tests/pos/annotations.scala similarity index 100% rename from tests/pos/Annotations.scala rename to tests/pos/annotations.scala