Skip to content

Changes around reaches and uses #23584

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions compiler/src/dotty/tools/dotc/cc/Capability.scala
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ object Capabilities:
val hiddenSet = CaptureSet.HiddenSet(owner, this: @unchecked)
// fails initialization check without the @unchecked

//assert(rootId != 6, origin)

override def equals(that: Any) = that match
case that: FreshCap => this eq that
case _ => false
Expand Down Expand Up @@ -808,6 +810,7 @@ object Capabilities:
case LambdaActual(restp: Type)
case OverriddenType(member: Symbol)
case DeepCS(ref: TypeRef)
case Parameter(param: Symbol)
case Unknown

def explanation(using Context): String = this match
Expand Down Expand Up @@ -841,6 +844,8 @@ object Capabilities:
i" when instantiating upper bound of member overridden by $member"
case DeepCS(ref: TypeRef) =>
i" when computing deep capture set of $ref"
case Parameter(param) =>
i" of parameter $param of ${param.owner}"
case Unknown =>
""
end Origin
Expand Down Expand Up @@ -907,8 +912,8 @@ object Capabilities:
CapToFresh(origin)(tp)

/** Maps fresh to cap */
def freshToCap(tp: Type)(using Context): Type =
CapToFresh(Origin.Unknown).inverse(tp)
def freshToCap(param: Symbol, tp: Type)(using Context): Type =
CapToFresh(Origin.Parameter(param)).inverse(tp)

/** Map top-level free existential variables one-to-one to Fresh instances */
def resultToFresh(tp: Type, origin: Origin)(using Context): Type =
Expand Down
40 changes: 30 additions & 10 deletions compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,17 @@ extension (tp: Type)
case AnnotatedType(tp1, ann) if tp1.derivesFrom(defn.Caps_CapSet) && ann.symbol.isRetains =>
ann.tree.retainedSet.retainedElementsRaw
case tp =>
// Nothing is a special type to represent the empty set
if tp.isNothingType then Nil
else tp :: Nil // should be checked by wellformedness
tp.dealiasKeepAnnots match
case tp: TypeRef if tp.symbol == defn.Caps_CapSet =>
// This can happen in cases where we try to type an eta expansion `$x => f($x)`
// from a polymorphic target type using capture sets. In that case the parameter type
// of $x is not treated as inferred is approximated to CapSet. An example is
// capset-problem.scala. We handle these cases by appromxating to the empty set.
Nil
case _ =>
// Nothing is a special type to represent the empty set
if tp.isNothingType then Nil
else tp :: Nil // should be checked by wellformedness

/** A list of capabilities of a retained set. */
def retainedElements(using Context): List[Capability] =
Expand Down Expand Up @@ -571,18 +579,30 @@ extension (sym: Symbol)
def hasTrackedParts(using Context): Boolean =
!CaptureSet.ofTypeDeeply(sym.info).isAlwaysEmpty

/** `sym` itself or its info is annotated @use or it is a type parameter with a matching
* @use-annotated term parameter that contains `sym` in its deep capture set.
/** Until 3.7:
* `sym` itself or its info is annotated @use or it is a type parameter with a matching
* @use-annotated term parameter that contains `sym` in its deep capture set.
* From 3.8:
* `sym` is a capset parameter without a `@reserve` annotation that
* - belongs to a class in a class, or
* - belongs to a method where it appears in a the deep capture set of a following term parameter of the same method.
*/
def isUseParam(using Context): Boolean =
sym.hasAnnotation(defn.UseAnnot)
|| sym.info.hasAnnotation(defn.UseAnnot)
|| sym.is(TypeParam)
&& sym.owner.rawParamss.nestedExists: param =>
param.is(TermParam) && param.hasAnnotation(defn.UseAnnot)
&& param.info.deepCaptureSet.elems.exists:
case c: TypeRef => c.symbol == sym
case _ => false
&& !sym.info.hasAnnotation(defn.ReserveAnnot)
&& (sym.owner.isClass
|| sym.owner.rawParamss.nestedExists: param =>
param.is(TermParam)
&& (!ccConfig.allowUse || param.hasAnnotation(defn.UseAnnot))
&& param.info.deepCaptureSet.elems.exists:
case c: TypeRef => c.symbol == sym
case _ => false
|| {
//println(i"not is use param $sym")
false
})

/** `sym` or its info is annotated with `@consume`. */
def isConsumeParam(using Context): Boolean =
Expand Down
14 changes: 7 additions & 7 deletions compiler/src/dotty/tools/dotc/cc/CaptureSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -453,11 +453,11 @@ sealed abstract class CaptureSet extends Showable:
case elem: FreshCap => elem.ccOwner.isContainedIn(rootLimit)
case _ => false

/** Invoke `handler` if this set has (or later aquires) a root capability.
* Excluded are Fresh instances unless their ccOwner is contained in `upto`.
/** Invoke `handler` if this set has (or later aquires) a bad root capability.
* Fresh instances count as good as long as their ccOwner is outside `upto`.
* If `upto` is NoSymbol, all Fresh instances are admitted.
*/
def disallowRootCapability(upto: Symbol)(handler: () => Context ?=> Unit)(using Context): this.type =
def disallowBadRoots(upto: Symbol)(handler: () => Context ?=> Unit)(using Context): this.type =
if elems.exists(isBadRoot(upto, _)) then handler()
this

Expand Down Expand Up @@ -698,7 +698,7 @@ object CaptureSet:
private def narrowClassifier(cls: ClassSymbol)(using Context): Unit =
val newClassifier = leastClassifier(classifier, cls)
if newClassifier == defn.NothingClass then
println(i"conflicting classifications for $this, was $classifier, now $cls")
capt.println(i"conflicting classifications for $this, was $classifier, now $cls")
myClassifier = newClassifier

override def adoptClassifier(cls: ClassSymbol)(using Context): Unit =
Expand Down Expand Up @@ -817,10 +817,10 @@ object CaptureSet:
|| isConst
|| varState.canRecord && { includeDep(cs); true }

override def disallowRootCapability(upto: Symbol)(handler: () => Context ?=> Unit)(using Context): this.type =
override def disallowBadRoots(upto: Symbol)(handler: () => Context ?=> Unit)(using Context): this.type =
rootLimit = upto
rootAddedHandler = handler
super.disallowRootCapability(upto)(handler)
super.disallowBadRoots(upto)(handler)

override def ensureWellformed(handler: Capability => (Context) ?=> Unit)(using Context): this.type =
newElemAddedHandler = handler
Expand Down Expand Up @@ -922,7 +922,7 @@ object CaptureSet:
* Test case: Without that tweak, logger.scala would not compile.
*/
class RefiningVar(owner: Symbol)(using Context) extends Var(owner):
override def disallowRootCapability(upto: Symbol)(handler: () => Context ?=> Unit)(using Context) = this
override def disallowBadRoots(upto: Symbol)(handler: () => Context ?=> Unit)(using Context) = this

/** A variable that is derived from some other variable via a map or filter. */
abstract class DerivedVar(owner: Symbol, initialElems: Refs)(using @constructorOnly ctx: Context)
Expand Down
92 changes: 52 additions & 40 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ object CheckCaptures:
cur = cur.outer
res

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

/** Similar normal substParams, but this is an approximating type map that
Expand Down Expand Up @@ -123,11 +122,11 @@ object CheckCaptures:
report.error(em"$elem: $tpe is not a legal element of a capture set", ann.srcPos)
ann.retainedSet.retainedElementsRaw.foreach(check)

/** Under the sealed policy, report an error if some part of `tp` contains the
* root capability in its capture set or if it refers to a type parameter that
* could possibly be instantiated with cap in a way that's visible at the type.
/** Disallow bad roots anywhere in type `tp``.
* @param upto controls up to which owner local fresh capabilities should be disallowed.
* See disallowBadRoots for details.
*/
private def disallowRootCapabilitiesIn(tp: Type, upto: Symbol, what: String, have: String, addendum: String, pos: SrcPos)(using Context) =
private def disallowBadRootsIn(tp: Type, upto: Symbol, what: => String, have: => String, addendum: => String, pos: SrcPos)(using Context) =
val check = new TypeTraverser:

private val seen = new EqHashSet[TypeRef]
Expand All @@ -154,7 +153,7 @@ object CheckCaptures:
case CapturingType(parent, refs) =>
if variance >= 0 then
val openScopes = openExistentialScopes
refs.disallowRootCapability(upto): () =>
refs.disallowBadRoots(upto): () =>
def part =
if t eq tp then ""
else
Expand Down Expand Up @@ -182,7 +181,10 @@ object CheckCaptures:
case t =>
traverseChildren(t)
check.traverse(tp)
end disallowRootCapabilitiesIn
end disallowBadRootsIn

private def ownerStr(owner: Symbol)(using Context): String =
if owner.isAnonymousFunction then "enclosing function" else owner.show

trait CheckerAPI:
/** Complete symbol info of a val or a def */
Expand Down Expand Up @@ -493,7 +495,7 @@ class CheckCaptures extends Recheck, SymTransformer:
case c1: CoreCapability =>
CaptureSet.ofType(c1.widen, followResult = false)
capt.println(i"Widen reach $c to $underlying in ${env.owner}")
underlying.disallowRootCapability(NoSymbol): () =>
underlying.disallowBadRoots(NoSymbol): () =>
report.error(em"Local capability $c in ${env.ownerString} cannot have `cap` as underlying capture set", tree.srcPos)
recur(underlying, env, lastEnv)

Expand Down Expand Up @@ -537,21 +539,26 @@ class CheckCaptures extends Recheck, SymTransformer:
// Under deferredReaches, don't propagate out of methods inside terms.
// The use set of these methods will be charged when that method is called.

recur(cs, curEnv, null)
useInfos += ((tree, cs, curEnv))
if !cs.isAlwaysEmpty then
recur(cs, curEnv, null)
useInfos += ((tree, cs, curEnv))
end markFree

/** If capability `c` refers to a parameter that is not @use declared, report an error.
/** If capability `c` refers to a parameter that is not implicitly or explicitly
* @use declared, report an error.
*/
def checkUseDeclared(c: Capability, pos: SrcPos)(using Context): Unit =
c.paramPathRoot match
case ref: NamedType if !ref.symbol.isUseParam =>
val what = if ref.isType then "Capture set parameter" else "Local reach capability"
val owner = ref.symbol.owner
val ownerStr = if owner.isAnonymousFunction then "enclosing function" else owner.show
def mitigation =
if ccConfig.allowUse
then i"\nTo allow this, the ${ref.symbol} should be declared with a @use annotation."
else if !ref.isType then i"\nYou could try to abstract the capabilities referred to by $c in a capset variable."
else ""
report.error(
em"""$what $c leaks into capture scope of $ownerStr.
|To allow this, the ${ref.symbol} should be declared with a @use annotation""", pos)
em"$what $c leaks into capture scope of ${ownerStr(ref.symbol.owner)}.$mitigation",
pos)
case _ =>

/** Include references captured by the called method in the current environment stack */
Expand All @@ -574,16 +581,16 @@ class CheckCaptures extends Recheck, SymTransformer:
case _ =>
tp

/** Under the sealed policy, disallow the root capability in type arguments.
* Type arguments come either from a TypeApply node or from an AppliedType
/** Type arguments come either from a TypeApply node or from an AppliedType
* which represents a trait parent in a template.
* Also, if a corresponding formal type parameter is declared or implied @use,
* charge the deep capture set of the argument to the environent.
* - Disallow global cap and result caps in such arguments.
* - If a corresponding formal type parameter is declared or implied @use,
* charge the deep capture set of the argument to the environent.
* @param fn the type application, of type TypeApply or TypeTree
* @param sym the constructor symbol (could be a method or a val or a class)
* @param args the type arguments
*/
def disallowCapInTypeArgs(fn: Tree, sym: Symbol, args: List[Tree])(using Context): Unit =
def markFreeTypeArgs(fn: Tree, sym: Symbol, args: List[Tree])(using Context): Unit =
def isExempt = sym.isTypeTestOrCast || defn.capsErasedValueMethods.contains(sym)
if !isExempt then
val paramNames = atPhase(thisPhase.prev):
Expand All @@ -596,17 +603,17 @@ class CheckCaptures extends Recheck, SymTransformer:

for case (arg: TypeTree, pname) <- args.lazyZip(paramNames) do
def where = if sym.exists then i" in an argument of $sym" else ""
val (addendum, errTree) =
def addendum =
if arg.isInferred
then (i"\nThis is often caused by a local capability$where\nleaking as part of its result.", fn)
else if arg.span.exists then ("", arg)
else ("", fn)
disallowRootCapabilitiesIn(arg.nuType, NoSymbol,
then i"\nThis is often caused by a local capability$where\nleaking as part of its result."
else ""
def errTree = if !arg.isInferred && arg.span.exists then arg else fn
disallowBadRootsIn(arg.nuType, NoSymbol,
i"Type variable $pname of $sym", "be instantiated to", addendum, errTree.srcPos)

val param = fn.symbol.paramNamed(pname)
if param.isUseParam then markFree(arg.nuType.deepCaptureSet, errTree)
end disallowCapInTypeArgs
end markFreeTypeArgs

/** Rechecking idents involves:
* - adding call captures for idents referring to methods
Expand Down Expand Up @@ -875,7 +882,7 @@ class CheckCaptures extends Recheck, SymTransformer:
case fun @ Select(qual, nme.apply) => qual.symbol.orElse(fun.symbol)
case fun => fun.symbol
def methDescr = if meth.exists then i"$meth's type " else ""
disallowCapInTypeArgs(tree.fun, meth, tree.args)
markFreeTypeArgs(tree.fun, meth, tree.args)
val funType = super.recheckTypeApply(tree, pt)
val res = resultToFresh(funType, Origin.ResultInstance(funType, meth))
includeCallCaptures(tree.symbol, res, tree)
Expand Down Expand Up @@ -924,7 +931,7 @@ class CheckCaptures extends Recheck, SymTransformer:
assert(params.hasSameLengthAs(argTypes), i"$mdef vs $pt, ${params}")
for (argType, param) <- argTypes.lazyZip(params) do
val paramTpt = param.asInstanceOf[ValDef].tpt
val paramType = freshToCap(paramTpt.nuType)
val paramType = freshToCap(param.symbol, paramTpt.nuType)
checkConformsExpr(argType, paramType, param)
.showing(i"compared expected closure formal $argType against $param with ${paramTpt.nuType}", capt)
if ccConfig.preTypeClosureResults && !(isEtaExpansion(mdef) && ccConfig.handleEtaExpansionsSpecially) then
Expand Down Expand Up @@ -997,7 +1004,7 @@ class CheckCaptures extends Recheck, SymTransformer:
i"\n\nNote that $sym does not count as local since it is captured by $enclStr"
case _ =>
""
disallowRootCapabilitiesIn(
disallowBadRootsIn(
tree.tpt.nuType, NoSymbol, i"Mutable $sym", "have type", addendum, sym.srcPos)
checkInferredResult(super.recheckValDef(tree, sym), tree)
finally
Expand Down Expand Up @@ -1180,7 +1187,7 @@ class CheckCaptures extends Recheck, SymTransformer:
for case tpt: TypeTree <- impl.parents do
tpt.tpe match
case AppliedType(fn, args) =>
disallowCapInTypeArgs(tpt, fn.typeSymbol, args.map(TypeTree(_)))
markFreeTypeArgs(tpt, fn.typeSymbol, args.map(TypeTree(_)))
case _ =>
ccState.inNestedLevelUnless(cls.is(Module)):
super.recheckClassDef(tree, impl, cls)
Expand Down Expand Up @@ -1213,7 +1220,7 @@ class CheckCaptures extends Recheck, SymTransformer:
recheck(tree.expr, pt)
val tp = recheckTryRest(bodyType, tree.cases, tree.finalizer, pt)
if Feature.enabled(Feature.saferExceptions) then
disallowRootCapabilitiesIn(tp, ctx.owner,
disallowBadRootsIn(tp, ctx.owner,
"The result of `try`", "have type",
"\nThis is often caused by a locally generated exception capability leaking as part of its result.",
tree.srcPos)
Expand Down Expand Up @@ -1580,11 +1587,9 @@ class CheckCaptures extends Recheck, SymTransformer:
private def improveCaptures(widened: Type, prefix: Type)(using Context): Type = prefix match
case ref: Capability if ref.isTracked =>
widened match
case widened @ CapturingType(p, refs) if ref.singletonCaptureSet.mightSubcapture(refs) =>
val improvedCs =
if widened.isBoxed then ref.reach.singletonCaptureSet
else ref.singletonCaptureSet
widened.derivedCapturingType(p, improvedCs)
case widened @ CapturingType(p, refs)
if ref.singletonCaptureSet.mightSubcapture(refs) && !widened.isBoxed =>
widened.derivedCapturingType(p, ref.singletonCaptureSet)
.showing(i"improve $widened to $result", capt)
case _ => widened
case _ => widened
Expand Down Expand Up @@ -1786,7 +1791,7 @@ class CheckCaptures extends Recheck, SymTransformer:

override def checkInheritedTraitParameters: Boolean = false

/** Check that overrides don't change the @use or @consume status of their parameters */
/** Check that overrides don't change the @use, @consume, or @reserve status of their parameters */
override def additionalChecks(member: Symbol, other: Symbol)(using Context): Unit =
for
(params1, params2) <- member.rawParamss.lazyZip(other.rawParamss)
Expand All @@ -1803,6 +1808,7 @@ class CheckCaptures extends Recheck, SymTransformer:

checkAnnot(defn.UseAnnot)
checkAnnot(defn.ConsumeAnnot)
checkAnnot(defn.ReserveAnnot)
end OverridingPairsCheckerCC

def traverse(t: Tree)(using Context) =
Expand Down Expand Up @@ -1985,7 +1991,7 @@ class CheckCaptures extends Recheck, SymTransformer:
if !(pos.span.isSynthetic && ctx.reporter.errorsReported)
&& !arg.typeSymbol.name.is(WildcardParamName)
then
CheckCaptures.disallowRootCapabilitiesIn(arg, NoSymbol,
CheckCaptures.disallowBadRootsIn(arg, NoSymbol,
"Array", "have element type", "",
pos)
traverseChildren(t)
Expand Down Expand Up @@ -2030,7 +2036,13 @@ class CheckCaptures extends Recheck, SymTransformer:
case c: DerivedCapability =>
checkElem(c.underlying)
case c: FreshCap =>
check(c.hiddenSet)
c.origin match
case Origin.Parameter(param) =>
report.error(
em"Local $c created in type of $param leaks into capture scope of ${ownerStr(param.owner)}",
tree.srcPos)
case _ =>
check(c.hiddenSet)
case _ =>

check(uses)
Expand Down
Loading
Loading