From 434941ab7ea47815a89c0e1b25e2d8ebcc54e68a Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 5 Dec 2025 19:35:10 +0100 Subject: [PATCH 1/8] Optimization: Introduce CompactAnnotation This one takes a type argument instead of as a tree argument. For now it's reserved for retains-like annotations. CompactAnnotations don't need tree maps and annotated types containing them hash properly. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 13 +- .../src/dotty/tools/dotc/cc/SepCheck.scala | 2 +- .../src/dotty/tools/dotc/cc/ccConfig.scala | 2 +- .../dotty/tools/dotc/core/Annotations.scala | 123 ++++++++++++++---- .../tools/dotc/core/tasty/TreePickler.scala | 9 +- .../tools/dotc/core/tasty/TreeUnpickler.scala | 12 +- .../tools/dotc/printing/RefinedPrinter.scala | 1 + .../{Annotations.scala => annotations.scala} | 0 8 files changed, 126 insertions(+), 36 deletions(-) rename tests/pos/{Annotations.scala => annotations.scala} (100%) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index af9a2abc7a9d..4c1e2b319d96 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -67,11 +67,16 @@ extension (tree: Tree) /** The type representing the capture set of @retains, @retainsCap or @retainsByName annotation. */ def retainedSet(using Context): Type = - tree match + val rcap = defn.RetainsCapAnnot + if tree.symbol == rcap || tree.symbol.maybeOwner == rcap then + defn.captureRoot.termRef + else tree match case Apply(TypeApply(_, refs :: Nil), _) => refs.tpe - case _ => - if tree.symbol.maybeOwner == defn.RetainsCapAnnot - then defn.captureRoot.termRef else NoType + case tree: TypeTree => + tree.tpe match + case AppliedType(_, refs :: Nil) => refs + case _ => NoType + case _ => NoType extension (tp: Type) 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/ccConfig.scala b/compiler/src/dotty/tools/dotc/cc/ccConfig.scala index 5ea8b17d8aaf..238b8cc60c4c 100644 --- a/compiler/src/dotty/tools/dotc/cc/ccConfig.scala +++ b/compiler/src/dotty/tools/dotc/cc/ccConfig.scala @@ -59,7 +59,7 @@ object ccConfig: /** Not used currently. Handy for trying out new features */ def newScheme(using ctx: Context): Boolean = - Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`) + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.9`) def allowUse(using Context): Boolean = Feature.sourceVersion.stable.isAtMost(SourceVersion.`3.7`) diff --git a/compiler/src/dotty/tools/dotc/core/Annotations.scala b/compiler/src/dotty/tools/dotc/core/Annotations.scala index d8645e4e2e4e..13d3dad3f907 100644 --- a/compiler/src/dotty/tools/dotc/core/Annotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Annotations.scala @@ -33,9 +33,16 @@ object Annotations { def derivedAnnotation(tree: Tree)(using Context): Annotation = if (tree eq this.tree) this else Annotation(tree) + def derivedClassAnnotation(cls: ClassSymbol)(using Context) = + Annotation(cls, tree.span) + /** 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 @@ -66,23 +73,8 @@ object Annotations { // 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 + // 2. Sanitize the arguments to prevent compilation time blowup. + // 3. Drop the annotation entirely if CC is not enabled somewhere. def rebuild(tree: Tree, mappedType: Type): Tree = tree match case Apply(fn, Nil) => cpy.Apply(tree)(rebuild(fn, mappedType), Nil) @@ -93,8 +85,12 @@ object Annotations { 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)) + if mappedType `eql` arg.tpe then + this + else if cc.ccConfig.newScheme then + CompactAnnotation(symbol.typeRef.appliedTo(mappedType)) + else + derivedAnnotation(rebuild(tree, mappedType)) case args => // Checks if `tm` would result in any change by applying it to types @@ -114,17 +110,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 +154,82 @@ object Annotations { case class ConcreteAnnotation(t: Tree) extends Annotation: def tree(using Context): Tree = t + case class CompactAnnotation(tp: Type) extends Annotation: + assert(tp.isInstanceOf[AppliedType | TypeRef], tp) + + def tree(using Context) = TypeTree(tp) + + override def symbol(using Context) = tp.typeSymbol + + override def derivedAnnotation(tree: Tree)(using Context): Annotation = + derivedAnnotation(tree.tpe) + + override def derivedClassAnnotation(cls: ClassSymbol)(using Context) = + derivedAnnotation(cls.typeRef) + + def derivedAnnotation(tp: Type)(using Context): Annotation = + if tp eq this.tp then this else CompactAnnotation(tp) + + override def arguments(using Context): List[Tree] = + argumentTypes.map(TypeTree(_)) + + override def argumentTypes(using Context): List[Type] = tp.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 + + override def mapWith(tm: TypeMap)(using Context): Annotation = + def derived(tp: Type) = + if tm.isRange(tp) then EmptyAnnotation else derivedAnnotation(tp) + def sanitizeArg(tp: Type) = tp match + case tp @ AppliedType(tycon, args) => + tp.derivedAppliedType(tycon, args.mapConserve(sanitize)) + case _ => + tp + if !symbol.isRetainsLike then derived(tm(tp)) + else if Feature.ccEnabledSomewhere then derived(sanitizeArg(tm(tp))) + else EmptyAnnotation // strip retains-like annotations unless capture checking is enabled + + override def refersToParamOf(tl: TermLambda)(using Context): Boolean = + refersToLambdaParam(tp, tl) + + override def hash: Int = tp.hash + override def eql(that: Annotation) = that match + case that: CompactAnnotation => this.tp `eql` that.tp + case _ => false + end CompactAnnotation + + /** 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. + */ + private def sanitize(tp: Type)(using Context): 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, ann.derivedClassAnnotation(defn.RetainsCapAnnot)) + case tp @ OrType(tp1, tp2) => + tp.derivedOrType(sanitize(tp1), sanitize(tp2)) + case _ => + tp + + 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 +311,9 @@ object Annotations { object Annotation { - def apply(tree: Tree): ConcreteAnnotation = ConcreteAnnotation(tree) + def apply(tree: Tree): Annotation = tree match + case tree: TypeTree => CompactAnnotation(tree.tpe) + case _ => ConcreteAnnotation(tree) def apply(cls: ClassSymbol, span: Span)(using Context): Annotation = apply(cls, Nil, span) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 1703a8a87d6e..e336e63629ec 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 CompactAnnotation(tp) => pickleType(tp) + case _ => + pickleTree(tpe.annot.tree) + annotatedTypeTrees += tpe.annot.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/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 37222fba3ee3..9adce9128062 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -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/tests/pos/Annotations.scala b/tests/pos/annotations.scala similarity index 100% rename from tests/pos/Annotations.scala rename to tests/pos/annotations.scala From 910529a5d42880dd7c6ff63203b5d8fdafccb95c Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Dec 2025 09:47:01 +0100 Subject: [PATCH 2/8] Represent all retains annotations as CompactAnnotations --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 23 ++-- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 4 - .../dotty/tools/dotc/cc/RetainingType.scala | 5 +- .../src/dotty/tools/dotc/cc/ccConfig.scala | 2 +- .../dotty/tools/dotc/core/Annotations.scala | 101 ++++++++---------- .../src/dotty/tools/dotc/core/Types.scala | 6 +- .../tools/dotc/transform/PostTyper.scala | 6 +- .../dotty/tools/dotc/typer/TypeAssigner.scala | 11 +- tests/neg-custom-args/captures/lazyvals.check | 6 +- .../captures/spread-problem.check | 4 +- .../neg-custom-args/captures/wf-reach-1.check | 6 +- 11 files changed, 88 insertions(+), 86 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 4c1e2b319d96..2adc81e51eb1 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -14,7 +14,7 @@ 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 @@ -545,13 +545,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 @@ -658,7 +668,8 @@ 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 RetainingTypes with Nothing 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 diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index d67e344830b1..1ce214ca707c 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 diff --git a/compiler/src/dotty/tools/dotc/cc/RetainingType.scala b/compiler/src/dotty/tools/dotc/cc/RetainingType.scala index 6fde63a0d3ac..00b6e170c054 100644 --- a/compiler/src/dotty/tools/dotc/cc/RetainingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/RetainingType.scala @@ -5,7 +5,7 @@ package cc import core.* import Types.*, Symbols.*, Contexts.* import ast.tpd.* -import Annotations.Annotation +import Annotations.CompactAnnotation import Decorators.i /** A builder and extractor for annotated types with @retains or @retainsByName annotations @@ -15,8 +15,7 @@ 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)) + AnnotatedType(tp, CompactAnnotation(annotCls.typeRef.appliedTo(typeElems))) def unapply(tp: AnnotatedType)(using Context): Option[(Type, Type)] = val sym = tp.annot.symbol diff --git a/compiler/src/dotty/tools/dotc/cc/ccConfig.scala b/compiler/src/dotty/tools/dotc/cc/ccConfig.scala index 238b8cc60c4c..5ea8b17d8aaf 100644 --- a/compiler/src/dotty/tools/dotc/cc/ccConfig.scala +++ b/compiler/src/dotty/tools/dotc/cc/ccConfig.scala @@ -59,7 +59,7 @@ object ccConfig: /** Not used currently. Handy for trying out new features */ def newScheme(using ctx: Context): Boolean = - Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.9`) + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`) def allowUse(using Context): Boolean = Feature.sourceVersion.stable.isAtMost(SourceVersion.`3.7`) diff --git a/compiler/src/dotty/tools/dotc/core/Annotations.scala b/compiler/src/dotty/tools/dotc/core/Annotations.scala index 13d3dad3f907..4dbc143b750c 100644 --- a/compiler/src/dotty/tools/dotc/core/Annotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Annotations.scala @@ -66,32 +66,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. Sanitize the arguments to prevent compilation time blowup. - // 3. Drop the annotation entirely if CC is not enabled somewhere. - - 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 if cc.ccConfig.newScheme then - CompactAnnotation(symbol.typeRef.appliedTo(mappedType)) - 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 @@ -184,17 +160,35 @@ object Annotations { case ConstantType(c) => Some(c) case _ => None + /** 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. + */ + private def sanitize(tp: Type)(using Context): 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, ann.derivedClassAnnotation(defn.RetainsCapAnnot)) + case tp @ OrType(tp1, tp2) => + tp.derivedOrType(sanitize(tp1), sanitize(tp2)) + case _ => + tp + override def mapWith(tm: TypeMap)(using Context): Annotation = - def derived(tp: Type) = - if tm.isRange(tp) then EmptyAnnotation else derivedAnnotation(tp) - def sanitizeArg(tp: Type) = tp match - case tp @ AppliedType(tycon, args) => - tp.derivedAppliedType(tycon, args.mapConserve(sanitize)) + val isRetains = symbol.isRetainsLike + if isRetains && !Feature.ccEnabledSomewhere then EmptyAnnotation + else tm(tp) match + case tp1 @ AppliedType(tycon, args) => + val args1 = if isRetains then args.mapConserve(sanitize) else args + derivedAnnotation(tp1.derivedAppliedType(tycon, args1)) + case tp1: TypeRef => + derivedAnnotation(tp1) case _ => - tp - if !symbol.isRetainsLike then derived(tm(tp)) - else if Feature.ccEnabledSomewhere then derived(sanitizeArg(tm(tp))) - else EmptyAnnotation // strip retains-like annotations unless capture checking is enabled + EmptyAnnotation override def refersToParamOf(tl: TermLambda)(using Context): Boolean = refersToLambdaParam(tp, tl) @@ -203,25 +197,13 @@ object Annotations { override def eql(that: Annotation) = that match case that: CompactAnnotation => this.tp `eql` that.tp case _ => false - end CompactAnnotation - /** 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. - */ - private def sanitize(tp: Type)(using Context): 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, ann.derivedClassAnnotation(defn.RetainsCapAnnot)) - case tp @ OrType(tp1, tp2) => - tp.derivedOrType(sanitize(tp1), sanitize(tp2)) - case _ => - tp + object CompactAnnotation: + 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 @@ -311,9 +293,12 @@ object Annotations { object Annotation { - def apply(tree: Tree): Annotation = tree match - case tree: TypeTree => CompactAnnotation(tree.tpe) - case _ => 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) @@ -328,7 +313,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 CompactAnnotation(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 = @@ -370,7 +357,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/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index dc54d14b0d4b..c60c94fd1b4f 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4227,8 +4227,10 @@ 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, + CompactAnnotation(defn.RetainsAnnot.typeRef.appliedTo(defn.NothingType))), + AnnotatedType(parent1, + CompactAnnotation(defn.RetainsCapAnnot.appliedRef))) else parent1 case _ => mapOver(tp) 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/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 From 7b1516933611d1eb6def67c174eff11aded486df Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Dec 2025 16:04:59 +0100 Subject: [PATCH 3/8] Special class for RetainingAnnotations --- .../tools/dotc/cc/RetainingAnnotation.scala | 38 +++++++++++ .../dotty/tools/dotc/cc/RetainingType.scala | 5 +- .../dotty/tools/dotc/core/Annotations.scala | 64 ++++++++----------- .../src/dotty/tools/dotc/core/Types.scala | 4 +- .../tools/dotc/core/tasty/TreePickler.scala | 8 +-- 5 files changed, 72 insertions(+), 47 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala 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..c8df971e91d9 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala @@ -0,0 +1,38 @@ +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): + + /** 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) + if ann.symbol.isRetainsLike && parent.typeSymbol != defn.Caps_CapSet => + tp.derivedAnnotatedType(parent, ann.derivedClassAnnotation(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 toCaptureSet(using Context): CaptureSet = + CaptureSet(argumentType(0).retainedElements*) +*/ +end RetainingAnnotation diff --git a/compiler/src/dotty/tools/dotc/cc/RetainingType.scala b/compiler/src/dotty/tools/dotc/cc/RetainingType.scala index 00b6e170c054..e38b75a8bcc3 100644 --- a/compiler/src/dotty/tools/dotc/cc/RetainingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/RetainingType.scala @@ -4,9 +4,6 @@ package cc import core.* import Types.*, Symbols.*, Contexts.* -import ast.tpd.* -import Annotations.CompactAnnotation -import Decorators.i /** A builder and extractor for annotated types with @retains or @retainsByName annotations * excluding CapturingTypes. @@ -15,7 +12,7 @@ object RetainingType: def apply(tp: Type, typeElems: Type, byName: Boolean = false)(using Context): Type = val annotCls = if byName then defn.RetainsByNameAnnot else defn.RetainsAnnot - AnnotatedType(tp, CompactAnnotation(annotCls.typeRef.appliedTo(typeElems))) + AnnotatedType(tp, RetainingAnnotation(annotCls.typeRef.appliedTo(typeElems))) def unapply(tp: AnnotatedType)(using Context): Option[(Type, Type)] = val sym = tp.annot.symbol diff --git a/compiler/src/dotty/tools/dotc/core/Annotations.scala b/compiler/src/dotty/tools/dotc/core/Annotations.scala index 4dbc143b750c..8cbe1abe85e6 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.* @@ -130,12 +130,12 @@ object Annotations { case class ConcreteAnnotation(t: Tree) extends Annotation: def tree(using Context): Tree = t - case class CompactAnnotation(tp: Type) extends Annotation: - assert(tp.isInstanceOf[AppliedType | TypeRef], tp) + class CompactAnnotation(val tpe: Type) extends Annotation: + assert(tpe.isInstanceOf[AppliedType | TypeRef], tpe) - def tree(using Context) = TypeTree(tp) + def tree(using Context) = TypeTree(tpe) - override def symbol(using Context) = tp.typeSymbol + override def symbol(using Context) = tpe.typeSymbol override def derivedAnnotation(tree: Tree)(using Context): Annotation = derivedAnnotation(tree.tpe) @@ -144,12 +144,12 @@ object Annotations { derivedAnnotation(cls.typeRef) def derivedAnnotation(tp: Type)(using Context): Annotation = - if tp eq this.tp then this else CompactAnnotation(tp) + 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] = tp.argTypes + override def argumentTypes(using Context): List[Type] = tpe.argTypes def argumentType(i: Int)(using Context): Type = val args = argumentTypes @@ -160,49 +160,39 @@ object Annotations { case ConstantType(c) => Some(c) case _ => None - /** 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. + /** 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. */ - private def sanitize(tp: Type)(using Context): 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, ann.derivedClassAnnotation(defn.RetainsCapAnnot)) - case tp @ OrType(tp1, tp2) => - tp.derivedOrType(sanitize(tp1), sanitize(tp2)) + 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 _ => - tp + EmptyAnnotation override def mapWith(tm: TypeMap)(using Context): Annotation = - val isRetains = symbol.isRetainsLike - if isRetains && !Feature.ccEnabledSomewhere then EmptyAnnotation - else tm(tp) match - case tp1 @ AppliedType(tycon, args) => - val args1 = if isRetains then args.mapConserve(sanitize) else args - derivedAnnotation(tp1.derivedAppliedType(tycon, args1)) - case tp1: TypeRef => - derivedAnnotation(tp1) - case _ => - EmptyAnnotation + assert(!symbol.isRetainsLike) + mapWithCtd(tm) override def refersToParamOf(tl: TermLambda)(using Context): Boolean = - refersToLambdaParam(tp, tl) + refersToLambdaParam(tpe, tl) - override def hash: Int = tp.hash + override def hash: Int = tpe.hash override def eql(that: Annotation) = that match - case that: CompactAnnotation => this.tp `eql` that.tp + 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 @@ -314,7 +304,7 @@ object Annotations { def apply(atp: Type, args: List[Tree], span: Span)(using Context): Annotation = if atp.typeSymbol.isRetainsLike && args.isEmpty - then CompactAnnotation(atp) + then RetainingAnnotation(atp) else apply(New(atp, args).withSpan(span)) /** Create an annotation where the tree is computed lazily. */ diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index c60c94fd1b4f..e45bac95fd3e 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4228,9 +4228,9 @@ object Types extends TypeUtils { if ann.symbol.isRetainsLike then range( AnnotatedType(parent1, - CompactAnnotation(defn.RetainsAnnot.typeRef.appliedTo(defn.NothingType))), + RetainingAnnotation(defn.RetainsAnnot.typeRef.appliedTo(defn.NothingType))), AnnotatedType(parent1, - CompactAnnotation(defn.RetainsCapAnnot.appliedRef))) + RetainingAnnotation(defn.RetainsCapAnnot.appliedRef))) 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 e336e63629ec..0146f8f5b896 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -286,10 +286,10 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { withLength: pickleType(tpe.parent, richTypes) tpe.annot match - case CompactAnnotation(tp) => pickleType(tp) - case _ => - pickleTree(tpe.annot.tree) - annotatedTypeTrees += tpe.annot.tree + 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) } From b9e6eb9c632d66219339be3f122cf3e2aa273646 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Dec 2025 17:37:56 +0100 Subject: [PATCH 4/8] Don't detour over tree when going from RetainingAnnotation to capture set --- .../src/dotty/tools/dotc/cc/Capability.scala | 13 +++--- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 28 ++++--------- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 4 +- .../dotty/tools/dotc/cc/CapturingType.scala | 4 +- .../tools/dotc/cc/RetainingAnnotation.scala | 11 +++-- .../dotty/tools/dotc/cc/RetainingType.scala | 11 ++--- compiler/src/dotty/tools/dotc/cc/Setup.scala | 42 +++++++++---------- .../tools/dotc/printing/PlainPrinter.scala | 4 +- .../tools/dotc/printing/RefinedPrinter.scala | 2 +- 9 files changed, 51 insertions(+), 68 deletions(-) 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 2adc81e51eb1..c1966954b1a4 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -54,28 +54,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. */ - 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 = val rcap = defn.RetainsCapAnnot if tree.symbol == rcap || tree.symbol.maybeOwner == rcap then defn.captureRoot.termRef else tree match case Apply(TypeApply(_, refs :: Nil), _) => refs.tpe - case tree: TypeTree => - tree.tpe match - case AppliedType(_, refs :: Nil) => refs - case _ => NoType case _ => NoType extension (tp: Type) @@ -101,8 +88,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.isStrict => + ann.retainedType.retainedElementsRaw case tp => tp.dealiasKeepAnnots match case tp: TypeRef if tp.symbol == defn.Caps_CapSet => @@ -244,10 +231,11 @@ 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 ann.isStrict && !parent.derivesFrom(defn.Caps_CapSet) => + 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) => diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 1ce214ca707c..8592a614d5c6 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1674,8 +1674,8 @@ object CaptureSet: else empty case CapturingType(parent, refs) => recur(parent) ++ refs - case tp @ AnnotatedType(parent, ann) if ann.symbol.isRetains => - recur(parent) ++ ann.tree.toCaptureSet + case tp @ AnnotatedType(parent, ann: RetainingAnnotation) if ann.isStrict => + recur(parent) ++ ann.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 index c8df971e91d9..6f5a175760b9 100644 --- a/compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala @@ -31,8 +31,13 @@ class RetainingAnnotation(tpe: Type) extends CompactAnnotation(tpe): 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) + def toCaptureSet(using Context): CaptureSet = - CaptureSet(argumentType(0).retainedElements*) -*/ + CaptureSet(retainedType.retainedElements*) + end RetainingAnnotation diff --git a/compiler/src/dotty/tools/dotc/cc/RetainingType.scala b/compiler/src/dotty/tools/dotc/cc/RetainingType.scala index e38b75a8bcc3..6cc36a6c9115 100644 --- a/compiler/src/dotty/tools/dotc/cc/RetainingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/RetainingType.scala @@ -14,12 +14,7 @@ object RetainingType: val annotCls = if byName then defn.RetainsByNameAnnot else defn.RetainsAnnot AnnotatedType(tp, RetainingAnnotation(annotCls.typeRef.appliedTo(typeElems))) - 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 + def unapply(tp: AnnotatedType)(using Context): Option[(Type, Type)] = tp.annot match + case ann: RetainingAnnotation => Some((tp.parent, ann.retainedType)) + case _ => None end RetainingType diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 5c8c581f729c..1d0a3821dee1 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) @@ -972,15 +970,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 +1009,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/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 8d43113bad2c..cfb52d8f8c08 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -317,8 +317,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 _ => diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 9adce9128062..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 = From e648b42f25a87a8a71ef4d828542e40a11ea0004 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Dec 2025 18:32:07 +0100 Subject: [PATCH 5/8] Cache result of toCaptureSet in RetainingAnnotation --- .../tools/dotc/cc/RetainingAnnotation.scala | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala index 6f5a175760b9..9b0629895da7 100644 --- a/compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala @@ -10,34 +10,38 @@ import config.Feature /** A class for annotations @retains, @retainsByName and @retainsCap */ class RetainingAnnotation(tpe: Type) extends CompactAnnotation(tpe): - /** 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) - if ann.symbol.isRetainsLike && parent.typeSymbol != defn.Caps_CapSet => - tp.derivedAnnotatedType(parent, ann.derivedClassAnnotation(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) - - def toCaptureSet(using Context): CaptureSet = - CaptureSet(retainedType.retainedElements*) + /** 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) + if ann.symbol.isRetainsLike && parent.typeSymbol != defn.Caps_CapSet => + tp.derivedAnnotatedType(parent, ann.derivedClassAnnotation(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 From effba5cebf7db472146bd848b4d35c4512881777 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Dec 2025 19:06:44 +0100 Subject: [PATCH 6/8] Use more RetainingAnnotation tests where it makes sense --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 5 +---- compiler/src/dotty/tools/dotc/transform/TreeChecker.scala | 4 ++-- compiler/src/dotty/tools/dotc/typer/Applications.scala | 1 - compiler/src/dotty/tools/dotc/typer/Checking.scala | 4 ++-- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index c1966954b1a4..f72740e49cbf 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -19,9 +19,6 @@ 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 @@ -661,7 +658,7 @@ class PathSelectionProto(val select: Select, val pt: Type) extends typer.ProtoTy */ class CleanupRetains(using Context) extends TypeMap: def apply(tp: Type): Type = tp match - case AnnotatedType(parent, annot) if annot.symbol.isRetainsLike => + case 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) 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 From d223e79ef649f1878ce04285ec2d201d0d2d15ac Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Dec 2025 13:10:16 +0100 Subject: [PATCH 7/8] Drop RetainingType We don't need the constructor anymore since `RetainingAnnotation` has equivalent functionality. And we don't need the deconstructor since there is already `CapturingOrRetainsType`. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 16 ++++++-------- .../tools/dotc/cc/RetainingAnnotation.scala | 8 ++++--- .../dotty/tools/dotc/cc/RetainingType.scala | 20 ----------------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 11 ++-------- .../dotty/tools/dotc/core/Annotations.scala | 6 ----- .../dotty/tools/dotc/core/Definitions.scala | 11 +++++----- .../src/dotty/tools/dotc/core/Types.scala | 6 ++--- .../tools/dotc/printing/PlainPrinter.scala | 22 ++++++++++--------- 8 files changed, 34 insertions(+), 66 deletions(-) delete mode 100644 compiler/src/dotty/tools/dotc/cc/RetainingType.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index f72740e49cbf..8f71b47f039f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -492,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 @@ -653,17 +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 with Nothing as argument if CC is enabled - * (we need to do that to keep by-name status). + * 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: RetainingAnnotation) => + 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/RetainingAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala index 9b0629895da7..96f83354f5cf 100644 --- a/compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/RetainingAnnotation.scala @@ -10,6 +10,8 @@ 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 `^`. @@ -20,9 +22,9 @@ class RetainingAnnotation(tpe: Type) extends CompactAnnotation(tpe): override protected def sanitize(tp: Type)(using Context): 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, ann.derivedClassAnnotation(defn.RetainsCapAnnot)) + 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 _ => 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 6cc36a6c9115..000000000000 --- a/compiler/src/dotty/tools/dotc/cc/RetainingType.scala +++ /dev/null @@ -1,20 +0,0 @@ -package dotty.tools -package dotc -package cc - -import core.* -import Types.*, Symbols.*, Contexts.* - -/** 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 - AnnotatedType(tp, RetainingAnnotation(annotCls.typeRef.appliedTo(typeElems))) - - def unapply(tp: AnnotatedType)(using Context): Option[(Type, Type)] = tp.annot match - case ann: RetainingAnnotation => Some((tp.parent, ann.retainedType)) - case _ => None -end RetainingType diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 1d0a3821dee1..0e0728e01668 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -813,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 @@ -856,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 _ => diff --git a/compiler/src/dotty/tools/dotc/core/Annotations.scala b/compiler/src/dotty/tools/dotc/core/Annotations.scala index 8cbe1abe85e6..87056deca845 100644 --- a/compiler/src/dotty/tools/dotc/core/Annotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Annotations.scala @@ -33,9 +33,6 @@ object Annotations { def derivedAnnotation(tree: Tree)(using Context): Annotation = if (tree eq this.tree) this else Annotation(tree) - def derivedClassAnnotation(cls: ClassSymbol)(using Context) = - Annotation(cls, tree.span) - /** All term arguments of this annotation in a single flat list */ def arguments(using Context): List[Tree] = tpd.allTermArguments(tree) @@ -140,9 +137,6 @@ object Annotations { override def derivedAnnotation(tree: Tree)(using Context): Annotation = derivedAnnotation(tree.tpe) - override def derivedClassAnnotation(cls: ClassSymbol)(using Context) = - derivedAnnotation(cls.typeRef) - def derivedAnnotation(tp: Type)(using Context): Annotation = if tp eq this.tpe then this else CompactAnnotation(tp) 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 e45bac95fd3e..be3b828b6edc 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4227,10 +4227,8 @@ object Types extends TypeUtils { val parent1 = mapOver(parent) if ann.symbol.isRetainsLike then range( - AnnotatedType(parent1, - RetainingAnnotation(defn.RetainsAnnot.typeRef.appliedTo(defn.NothingType))), - AnnotatedType(parent1, - RetainingAnnotation(defn.RetainsCapAnnot.appliedRef))) + 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/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index cfb52d8f8c08..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 => @@ -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 => From 3e8c9cf26dd929c3487a1ef67f27d9002fda62c2 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Dec 2025 15:18:02 +0100 Subject: [PATCH 8/8] Comments and general polishing --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 20 +++++++++---------- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 4 +--- .../dotty/tools/dotc/core/Annotations.scala | 10 ++++++++++ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 8f71b47f039f..217588c12770 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -52,15 +52,14 @@ def ccState(using Context): CCState = extension (tree: Tree) /** The type representing the capture set of @retains, @retainsCap or @retainsByName - * annotation tree. + * annotation tree (represented as an Apply node). */ - def retainedSet(using Context): Type = - val rcap = defn.RetainsCapAnnot - if tree.symbol == rcap || tree.symbol.maybeOwner == rcap then - defn.captureRoot.termRef - else tree match - case Apply(TypeApply(_, refs :: Nil), _) => refs.tpe - case _ => 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) @@ -85,7 +84,7 @@ extension (tp: Type) def retainedElementsRaw(using Context): List[Type] = tp match case OrType(tp1, tp2) => tp1.retainedElementsRaw ++ tp2.retainedElementsRaw - case AnnotatedType(tp1, ann: RetainingAnnotation) if tp1.derivesFrom(defn.Caps_CapSet) && ann.isStrict => + case AnnotatedType(tp1, ann: RetainingAnnotation) if tp1.derivesFrom(defn.Caps_CapSet) => ann.retainedType.retainedElementsRaw case tp => tp.dealiasKeepAnnots match @@ -229,7 +228,8 @@ extension (tp: Type) if tp.isBoxed || parent.derivesFrom(defn.Caps_CapSet) then tp else tp.boxed case tp @ AnnotatedType(parent, ann: RetainingAnnotation) - if ann.isStrict && !parent.derivesFrom(defn.Caps_CapSet) => + if !parent.derivesFrom(defn.Caps_CapSet) => + assert(ann.isStrict) CapturingType(parent, ann.toCaptureSet, boxed = true) case tp @ AnnotatedType(parent, ann) => tp.derivedAnnotatedType(parent.boxDeeply, ann) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 8592a614d5c6..5670129f3f56 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1672,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: RetainingAnnotation) if ann.isStrict => - recur(parent) ++ ann.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/core/Annotations.scala b/compiler/src/dotty/tools/dotc/core/Annotations.scala index 87056deca845..b177b20f4b21 100644 --- a/compiler/src/dotty/tools/dotc/core/Annotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Annotations.scala @@ -127,6 +127,16 @@ 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)