Skip to content
Prev Previous commit
Next Next commit
Represent all retains annotations as CompactAnnotations
  • Loading branch information
odersky committed Dec 8, 2025
commit 910529a5d42880dd7c6ff63203b5d8fdafccb95c
23 changes: 17 additions & 6 deletions compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions compiler/src/dotty/tools/dotc/cc/CaptureSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 2 additions & 3 deletions compiler/src/dotty/tools/dotc/cc/RetainingType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/cc/ccConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
101 changes: 44 additions & 57 deletions compiler/src/dotty/tools/dotc/core/Annotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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 =
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions compiler/src/dotty/tools/dotc/transform/PostTyper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
11 changes: 7 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import NameOps.*
import collection.mutable
import reporting.*
import Checking.{checkNoPrivateLeaks, checkNoWildcard}
import cc.CaptureSet
import util.Property
import transform.Splicer

Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions tests/neg-custom-args/captures/lazyvals.check
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion tests/neg-custom-args/captures/spread-problem.check
Original file line number Diff line number Diff line change
Expand Up @@ -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`
6 changes: 3 additions & 3 deletions tests/neg-custom-args/captures/wf-reach-1.check
Original file line number Diff line number Diff line change
@@ -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