Skip to content

Commit 53bea78

Browse files
authored
Revise capability hierarchy and fix classifiers (#23656)
We now make `Capability` a sealed trait with two subtraits: `SharedCapability` and `ExclusiveCapability`. This forces one to pick one or the other when defining a new `Capability` type. Most tests were changed to use `SharedCapability` instead of `Capability`. Since classifiers are now much more common than before, this showed up some bugs and anomalies for classifyer handling, which are also addressed in this PR. Details: 1. Further improvement to error messages. We now explain why FreshCaps cannot subsume a capability. 3. We turn caching off by default for `captureSetOfInfo`. During experimentation I found that sometimes `captureSetOfInfo` could be cached too early, leading to a locked in universal cap in the info where we would expect to see a fresh cap. On the standard library it did not look like this had a noticeable performance impact. 4. Fixes to `tryClassifyAs` and `transClassifiers`. In particular, we need to distinguish `FreshCap`s that can still be classified from ones that cannot. Once a `FreshCap` is part of a constant capture set, it gets classified by the type that prefixes the set and that classification cannot be changed anymore. But other `FreshCap`s are created as members of variable sets and then their classification status is open and can be constrained further. 5. Convert a fresh cap to a reach capability only if the reach capability might subcapture the fresh cap. Since fresh caps are now classified that is not always the case. 6. Turn pre-type closure results on for parametric expected function types. This gives sometimes better errors and avoids premature mapings to scoped result types in closure bodies. 7. Use `cap.rd` as implicitly added capability only for references extending `ExclusiveCapability`. Use `cap` for the others. We also include now captures in the result type of a function in the `captureSetofInfo` of that function.
2 parents 278b1ed + 6e47806 commit 53bea78

File tree

178 files changed

+913
-455
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

178 files changed

+913
-455
lines changed

compiler/src/dotty/tools/dotc/cc/Capability.scala

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,39 @@ object Capabilities:
152152
val hiddenSet = CaptureSet.HiddenSet(owner, this: @unchecked)
153153
// fails initialization check without the @unchecked
154154

155+
/** Is this fresh cap (definitely) classified? If that's the case, the
156+
* classifier cannot be changed anymore.
157+
* We need to distinguish `FreshCap`s that can still be classified from
158+
* ones that cannot. Once a `FreshCap` is part of a constant capture set,
159+
* it gets classified by the type that prefixes the set and that classification
160+
* cannot be changed anymore. But other `FreshCap`s are created as members of
161+
* variable sets and then their classification status is open and can be
162+
* constrained further.
163+
*/
164+
private[Capabilities] var isClassified = false
165+
155166
override def equals(that: Any) = that match
156167
case that: FreshCap => this eq that
157168
case _ => false
158169

170+
/** Is this fresh cap at the right level to be able to subsume `ref`?
171+
*/
172+
def acceptsLevelOf(ref: Capability)(using Context): Boolean =
173+
if ccConfig.useFreshLevels && !CCState.collapseFresh then
174+
val refOwner = ref.levelOwner
175+
refOwner.isStaticOwner || ccOwner.isContainedIn(refOwner)
176+
else ref.core match
177+
case ResultCap(_) | _: ParamRef => false
178+
case _ => true
179+
180+
/** Classify this FreshCap as `cls`, provided `isClassified` is still false.
181+
* @param freeze Deterermines future `isClassified` state.
182+
*/
183+
def adoptClassifier(cls: ClassSymbol, freeze: Boolean)(using Context): Unit =
184+
if !isClassified then
185+
hiddenSet.adoptClassifier(cls)
186+
if freeze then isClassified = true
187+
159188
def descr(using Context) =
160189
val originStr = origin match
161190
case Origin.InDecl(sym) if sym.exists =>
@@ -477,7 +506,7 @@ object Capabilities:
477506

478507
def derivesFromCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_Capability)
479508
def derivesFromMutable(using Context): Boolean = derivesFromCapTrait(defn.Caps_Mutable)
480-
def derivesFromSharable(using Context): Boolean = derivesFromCapTrait(defn.Caps_Sharable)
509+
def derivesFromShared(using Context): Boolean = derivesFromCapTrait(defn.Caps_SharedCapability)
481510

482511
/** The capture set consisting of exactly this reference */
483512
def singletonCaptureSet(using Context): CaptureSet.Const =
@@ -495,7 +524,11 @@ object Capabilities:
495524
def isProvisional = this.core match
496525
case core: TypeProxy => !core.underlying.exists || core.underlying.isProvisional
497526
case _ => false
498-
if !isCaptureChecking || ctx.mode.is(Mode.IgnoreCaptures) || isProvisional then
527+
if !ccConfig.cacheCaptureSetOfInfo
528+
|| !isCaptureChecking
529+
|| ctx.mode.is(Mode.IgnoreCaptures)
530+
|| isProvisional
531+
then
499532
myCaptureSet = null
500533
else
501534
myCaptureSet = computed
@@ -524,7 +557,8 @@ object Capabilities:
524557
case Reach(_) =>
525558
captureSetOfInfo.transClassifiers
526559
case self: CoreCapability =>
527-
joinClassifiers(toClassifiers(self.classifier), captureSetOfInfo.transClassifiers)
560+
if self.derivesFromCapability then toClassifiers(self.classifier)
561+
else captureSetOfInfo.transClassifiers
528562
if myClassifiers != UnknownClassifier then
529563
classifiersValid == currentId
530564
myClassifiers
@@ -534,7 +568,8 @@ object Capabilities:
534568
cls == defn.AnyClass
535569
|| this.match
536570
case self: FreshCap =>
537-
self.hiddenSet.tryClassifyAs(cls)
571+
if self.isClassified then self.hiddenSet.classifier.derivesFrom(cls)
572+
else self.hiddenSet.tryClassifyAs(cls)
538573
case self: RootCapability =>
539574
true
540575
case Restricted(_, cls1) =>
@@ -547,8 +582,8 @@ object Capabilities:
547582
case Reach(_) =>
548583
captureSetOfInfo.tryClassifyAs(cls)
549584
case self: CoreCapability =>
550-
self.classifier.isSubClass(cls)
551-
&& captureSetOfInfo.tryClassifyAs(cls)
585+
if self.derivesFromCapability then self.derivesFrom(cls)
586+
else captureSetOfInfo.tryClassifyAs(cls)
552587

553588
def isKnownClassifiedAs(cls: ClassSymbol)(using Context): Boolean =
554589
transClassifiers match
@@ -677,7 +712,7 @@ object Capabilities:
677712
case _ => true
678713

679714
vs.ifNotSeen(this)(x.hiddenSet.elems.exists(_.subsumes(y)))
680-
|| levelOK
715+
|| x.acceptsLevelOf(y)
681716
&& ( y.tryClassifyAs(x.hiddenSet.classifier)
682717
|| { capt.println(i"$y cannot be classified as $x"); false }
683718
)
@@ -686,7 +721,7 @@ object Capabilities:
686721
case x: ResultCap =>
687722
val result = y match
688723
case y: ResultCap => vs.unify(x, y)
689-
case _ => y.derivesFromSharable
724+
case _ => y.derivesFromShared
690725
if !result then
691726
TypeComparer.addErrorNote(CaptureSet.ExistentialSubsumesFailure(x, y))
692727
result
@@ -696,7 +731,7 @@ object Capabilities:
696731
case _: ResultCap => false
697732
case _: FreshCap if CCState.collapseFresh => true
698733
case _ =>
699-
y.derivesFromSharable
734+
y.derivesFromShared
700735
|| canAddHidden && vs != VarState.HardSeparate && CCState.capIsRoot
701736
case Restricted(x1, cls) =>
702737
y.isKnownClassifiedAs(cls) && x1.maxSubsumes(y, canAddHidden)

compiler/src/dotty/tools/dotc/cc/CaptureOps.scala

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,8 @@ extension (tp: Type)
378378

379379
def derivesFromCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_Capability)
380380
def derivesFromMutable(using Context): Boolean = derivesFromCapTrait(defn.Caps_Mutable)
381-
def derivesFromSharedCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_Sharable)
381+
def derivesFromShared(using Context): Boolean = derivesFromCapTrait(defn.Caps_SharedCapability)
382+
def derivesFromExclusive(using Context): Boolean = derivesFromCapTrait(defn.Caps_ExclusiveCapability)
382383

383384
/** Drop @retains annotations everywhere */
384385
def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling
@@ -401,9 +402,12 @@ extension (tp: Type)
401402
if variance <= 0 then t
402403
else t.dealias match
403404
case t @ CapturingType(p, cs) if cs.containsCapOrFresh =>
404-
change = true
405405
val reachRef = if cs.isReadOnly then ref.reach.readOnly else ref.reach
406-
t.derivedCapturingType(apply(p), reachRef.singletonCaptureSet)
406+
if reachRef.singletonCaptureSet.mightSubcapture(cs) then
407+
change = true
408+
t.derivedCapturingType(apply(p), reachRef.singletonCaptureSet)
409+
else
410+
t
407411
case t @ AnnotatedType(parent, ann) =>
408412
// Don't map annotations, which includes capture sets
409413
t.derivedAnnotatedType(this(parent), ann)
@@ -599,6 +603,11 @@ extension (sym: Symbol)
599603
if sym.is(Method) && sym.owner.isClass then isReadOnlyMethod
600604
else sym.owner.isInReadOnlyMethod
601605

606+
def qualString(prefix: String)(using Context): String =
607+
if !sym.exists then ""
608+
else if sym.isAnonymousFunction then i" $prefix enclosing function"
609+
else i" $prefix $sym"
610+
602611
extension (tp: AnnotatedType)
603612
/** Is this a boxed capturing type? */
604613
def isBoxed(using Context): Boolean = tp.annot match
@@ -647,7 +656,9 @@ object OnlyCapability:
647656

648657
def unapply(tree: AnnotatedType)(using Context): Option[(Type, ClassSymbol)] = tree match
649658
case AnnotatedType(parent: Type, ann) if ann.hasSymbol(defn.OnlyCapabilityAnnot) =>
650-
Some((parent, ann.tree.tpe.argTypes.head.classSymbol.asClass))
659+
ann.tree.tpe.argTypes.head.classSymbol match
660+
case cls: ClassSymbol => Some((parent, cls))
661+
case _ => None
651662
case _ => None
652663
end OnlyCapability
653664

compiler/src/dotty/tools/dotc/cc/CaptureSet.scala

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,19 @@ sealed abstract class CaptureSet extends Showable:
263263
*/
264264
def mightAccountFor(x: Capability)(using Context): Boolean =
265265
reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true):
266-
CCState.withCollapsedFresh: // OK here since we opportunistically choose an alternative which gets checked later
266+
CCState.withCollapsedFresh:
267+
// withCollapsedFresh should be dropped. The problem is that since our level checking
268+
// does not deal with classes well, we get false negatives here. Observed in the line
269+
//
270+
// stateFromIteratorConcatSuffix(it)(flatMapImpl(rest, f).state))))
271+
//
272+
// in cc-lib's LazyListIterable.scala.
267273
TypeComparer.noNotes:
268274
elems.exists(_.subsumes(x)(using ctx)(using VarState.ClosedUnrecorded))
269275
|| !x.isTerminalCapability
270276
&& {
271-
val elems = x.captureSetOfInfo.elems
272-
!elems.isEmpty && elems.forall(mightAccountFor)
277+
val xelems = x.captureSetOfInfo.elems
278+
!xelems.isEmpty && xelems.forall(mightAccountFor)
273279
}
274280

275281
/** A more optimistic version of subCaptures used to choose one of two typing rules
@@ -436,9 +442,18 @@ sealed abstract class CaptureSet extends Showable:
436442
def adoptClassifier(cls: ClassSymbol)(using Context): Unit =
437443
for elem <- elems do
438444
elem.stripReadOnly match
439-
case fresh: FreshCap => fresh.hiddenSet.adoptClassifier(cls)
445+
case fresh: FreshCap => fresh.adoptClassifier(cls, freeze = isConst)
440446
case _ =>
441447

448+
/** All capabilities of this set except those Termrefs and FreshCaps that
449+
* are bound by `mt`.
450+
*/
451+
def freeInResult(mt: MethodicType)(using Context): CaptureSet =
452+
filter:
453+
case TermParamRef(binder, _) => binder ne mt
454+
case ResultCap(binder) => binder ne mt
455+
case _ => true
456+
442457
/** A bad root `elem` is inadmissible as a member of this set. What is a bad roots depends
443458
* on the value of `rootLimit`.
444459
* If the limit is null, all capture roots are good.
@@ -611,9 +626,13 @@ object CaptureSet:
611626
then i" under-approximating the result of mapping $ref to $mapped"
612627
else ""
613628

629+
private def capImpliedByCapability(parent: Type)(using Context): Capability =
630+
if parent.derivesFromExclusive then GlobalCap.readOnly else GlobalCap
631+
614632
/* The same as {cap.rd} but generated implicitly for references of Capability subtypes.
615633
*/
616-
case class CSImpliedByCapability() extends Const(SimpleIdentitySet(GlobalCap.readOnly))
634+
class CSImpliedByCapability(parent: Type)(using @constructorOnly ctx: Context)
635+
extends Const(SimpleIdentitySet(capImpliedByCapability(parent)))
617636

618637
/** A special capture set that gets added to the types of symbols that were not
619638
* themselves capture checked, in order to admit arbitrary corresponding capture
@@ -692,6 +711,9 @@ object CaptureSet:
692711
*/
693712
private[CaptureSet] var rootLimit: Symbol | Null = null
694713

714+
def isBadRoot(elem: Capability)(using Context): Boolean =
715+
isBadRoot(rootLimit, elem)
716+
695717
private var myClassifier: ClassSymbol = defn.AnyClass
696718
def classifier: ClassSymbol = myClassifier
697719

@@ -743,13 +765,14 @@ object CaptureSet:
743765
else if !levelOK(elem) then
744766
failWith(IncludeFailure(this, elem, levelError = true)) // or `elem` is not visible at the level of the set.
745767
else if !elem.tryClassifyAs(classifier) then
768+
//println(i"cannot classify $elem as $classifier, ${elem.asInstanceOf[CoreCapability].classifier}")
746769
failWith(IncludeFailure(this, elem))
747770
else
748771
// id == 108 then assert(false, i"trying to add $elem to $this")
749772
assert(elem.isWellformed, elem)
750773
assert(!this.isInstanceOf[HiddenSet] || summon[VarState].isSeparating, summon[VarState])
751774
includeElem(elem)
752-
if isBadRoot(rootLimit, elem) then
775+
if isBadRoot(elem) then
753776
rootAddedHandler()
754777
val normElem = if isMaybeSet then elem else elem.stripMaybe
755778
// assert(id != 5 || elems.size != 3, this)
@@ -778,7 +801,7 @@ object CaptureSet:
778801
case _ => foldOver(b, t)
779802
find(false, binder)
780803

781-
private def levelOK(elem: Capability)(using Context): Boolean = elem match
804+
def levelOK(elem: Capability)(using Context): Boolean = elem match
782805
case _: FreshCap =>
783806
!level.isDefined
784807
|| ccState.symLevel(elem.ccOwner) <= level
@@ -1246,7 +1269,13 @@ object CaptureSet:
12461269
* when a subsumes check decides that an existential variable `ex` cannot be
12471270
* instantiated to the other capability `other`.
12481271
*/
1249-
case class ExistentialSubsumesFailure(val ex: ResultCap, val other: Capability) extends ErrorNote
1272+
case class ExistentialSubsumesFailure(val ex: ResultCap, val other: Capability) extends ErrorNote:
1273+
def description(using Context): String =
1274+
def reason =
1275+
if other.isTerminalCapability then ""
1276+
else " since that capability is not a `Sharable` capability"
1277+
i"""the existential capture root in ${ex.originalBinder.resType}
1278+
|cannot subsume the capability $other$reason."""
12501279

12511280
/** Failure indicating that `elem` cannot be included in `cs` */
12521281
case class IncludeFailure(cs: CaptureSet, elem: Capability, levelError: Boolean = false) extends ErrorNote, Showable:
@@ -1258,6 +1287,38 @@ object CaptureSet:
12581287
res.myTrace = cs1 :: this.myTrace
12591288
res
12601289

1290+
def description(using Context): String =
1291+
def why =
1292+
val reasons = cs.elems.toList.collect:
1293+
case c: FreshCap if !c.acceptsLevelOf(elem) =>
1294+
i"$elem${elem.levelOwner.qualString("in")} is not visible from $c${c.ccOwner.qualString("in")}"
1295+
case c: FreshCap if !elem.tryClassifyAs(c.hiddenSet.classifier) =>
1296+
i"$c is classified as ${c.hiddenSet.classifier} but $elem is not"
1297+
if reasons.isEmpty then ""
1298+
else reasons.mkString("\nbecause ", "\nand ", "")
1299+
cs match
1300+
case cs: Var =>
1301+
if !cs.levelOK(elem) then
1302+
val levelStr = elem match
1303+
case ref: TermRef => i", defined in ${ref.symbol.maybeOwner}"
1304+
case _ => ""
1305+
i"""capability ${elem}$levelStr
1306+
|cannot be included in outer capture set $cs"""
1307+
else if !elem.tryClassifyAs(cs.classifier) then
1308+
i"""capability ${elem} is not classified as ${cs.classifier}, therefore it
1309+
|cannot be included in capture set $cs of ${cs.classifier} elements"""
1310+
else if cs.isBadRoot(elem) then
1311+
elem match
1312+
case elem: FreshCap =>
1313+
i"""local capability $elem created in ${elem.ccOwner}
1314+
|cannot be included in outer capture set $cs"""
1315+
case _ =>
1316+
i"universal capability $elem cannot be included in capture set $cs"
1317+
else
1318+
i"capability $elem cannot be included in capture set $cs"
1319+
case _ =>
1320+
i"capability $elem is not included in capture set $cs$why"
1321+
12611322
override def toText(printer: Printer): Text =
12621323
inContext(printer.printerContext):
12631324
if levelError then
@@ -1274,7 +1335,11 @@ object CaptureSet:
12741335
* @param lo the lower type of the orginal type comparison, or NoType if not known
12751336
* @param hi the upper type of the orginal type comparison, or NoType if not known
12761337
*/
1277-
case class MutAdaptFailure(cs: CaptureSet, lo: Type = NoType, hi: Type = NoType) extends ErrorNote
1338+
case class MutAdaptFailure(cs: CaptureSet, lo: Type = NoType, hi: Type = NoType) extends ErrorNote:
1339+
def description(using Context): String =
1340+
def ofType(tp: Type) = if tp.exists then i"of the mutable type $tp" else "of a mutable type"
1341+
i"""$cs is an exclusive capture set ${ofType(hi)},
1342+
|it cannot subsume a read-only capture set ${ofType(lo)}."""
12781343

12791344
/** A VarState serves as a snapshot mechanism that can undo
12801345
* additions of elements or super sets if an operation fails
@@ -1487,14 +1552,10 @@ object CaptureSet:
14871552
// `ref` will not seem subsumed by other capabilities in a `++`.
14881553
universal
14891554
case c: CoreCapability =>
1490-
ofType(c.underlying, followResult = false)
1555+
ofType(c.underlying, followResult = true)
14911556

14921557
/** Capture set of a type
14931558
* @param followResult If true, also include capture sets of function results.
1494-
* This mode is currently not used. It could be interesting
1495-
* when we change the system so that the capture set of a function
1496-
* is the union of the capture sets if its span.
1497-
* In this case we should use `followResult = true` in the call in ofInfo above.
14981559
*/
14991560
def ofType(tp: Type, followResult: Boolean)(using Context): CaptureSet =
15001561
def recur(tp: Type): CaptureSet = trace(i"ofType $tp, ${tp.getClass} $followResult", show = true):
@@ -1510,13 +1571,9 @@ object CaptureSet:
15101571
recur(parent) ++ refs
15111572
case tp @ AnnotatedType(parent, ann) if ann.symbol.isRetains =>
15121573
recur(parent) ++ ann.tree.toCaptureSet
1513-
case tpd @ defn.RefinedFunctionOf(rinfo: MethodType) if followResult =>
1574+
case tpd @ defn.RefinedFunctionOf(rinfo: MethodOrPoly) if followResult =>
15141575
ofType(tpd.parent, followResult = false) // pick up capture set from parent type
1515-
++ recur(rinfo.resType) // add capture set of result
1516-
.filter:
1517-
case TermParamRef(binder, _) => binder ne rinfo
1518-
case ResultCap(binder) => binder ne rinfo
1519-
case _ => true
1576+
++ recur(rinfo.resType).freeInResult(rinfo) // add capture set of result
15201577
case tpd @ AppliedType(tycon, args) =>
15211578
if followResult && defn.isNonRefinedFunction(tpd) then
15221579
recur(args.last)

compiler/src/dotty/tools/dotc/cc/CapturingType.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ object CapturingType:
4040
apply(parent1, refs ++ refs1, boxed)
4141
case _ =>
4242
if parent.derivesFromMutable then refs.setMutable()
43-
val classifier = parent.classifier
4443
refs.adoptClassifier(parent.classifier)
4544
AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot))
4645

0 commit comments

Comments
 (0)