Skip to content

Commit 06c925b

Browse files
authored
Alternative scheme for cc encapsulation (#18899)
Starting with a design document for exploring possible alternatives to the current encapsulation scheme in cc, this PR implements a different, simpler scheme from what we had before. - All type variables are treated as sealed, no explicit modifier necessary - Local roots are dropped as well - To regain expressiveness, we introduce reach capabilities x*
2 parents c196747 + 032fe8a commit 06c925b

File tree

159 files changed

+1083
-1187
lines changed

Some content is hidden

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

159 files changed

+1083
-1187
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1858,6 +1858,8 @@ object desugar {
18581858
Annotated(
18591859
AppliedTypeTree(ref(defn.SeqType), t),
18601860
New(ref(defn.RepeatedAnnot.typeRef), Nil :: Nil))
1861+
else if op.name == nme.CC_REACH then
1862+
Apply(ref(defn.Caps_reachCapability), t :: Nil)
18611863
else
18621864
assert(ctx.mode.isExpr || ctx.reporter.errorsReported || ctx.mode.is(Mode.Interactive), ctx.mode)
18631865
Select(t, op.name)

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -376,17 +376,6 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] =>
376376
case _ =>
377377
tree.tpe.isInstanceOf[ThisType]
378378
}
379-
380-
/** Under capture checking, an extractor for qualified roots `cap[Q]`.
381-
*/
382-
object QualifiedRoot:
383-
384-
def unapply(tree: Apply)(using Context): Option[String] = tree match
385-
case Apply(fn, Literal(lit) :: Nil) if fn.symbol == defn.Caps_capIn =>
386-
Some(lit.value.asInstanceOf[String])
387-
case _ =>
388-
None
389-
end QualifiedRoot
390379
}
391380

392381
trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] =>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte
6363
val elems = refs.elems.toList
6464
val elems1 = elems.mapConserve(tm)
6565
if elems1 eq elems then this
66-
else if elems1.forall(_.isInstanceOf[CaptureRef])
66+
else if elems1.forall(_.isTrackableRef)
6767
then derivedAnnotation(CaptureSet(elems1.asInstanceOf[List[CaptureRef]]*), boxed)
6868
else EmptyAnnotation
6969

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

Lines changed: 83 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,6 @@ class IllegalCaptureRef(tpe: Type) extends Exception(tpe.toString)
5959
/** Capture checking state, which is known to other capture checking components */
6060
class CCState:
6161

62-
/** Associates nesting level owners with the local roots valid in their scopes. */
63-
val localRoots: mutable.HashMap[Symbol, Symbol] = new mutable.HashMap
64-
6562
/** The last pair of capture reference and capture set where
6663
* the reference could not be added to the set due to a level conflict.
6764
*/
@@ -81,13 +78,13 @@ extension (tree: Tree)
8178

8279
/** Map tree with CaptureRef type to its type, throw IllegalCaptureRef otherwise */
8380
def toCaptureRef(using Context): CaptureRef = tree match
84-
case QualifiedRoot(outer) =>
85-
ctx.owner.levelOwnerNamed(outer)
86-
.orElse(defn.RootClass) // non-existing outer roots are reported in Setup's checkQualifiedRoots
87-
.localRoot.termRef
81+
case ReachCapabilityApply(arg) =>
82+
arg.toCaptureRef.reach
8883
case _ => tree.tpe match
89-
case ref: CaptureRef => ref
90-
case tpe => throw IllegalCaptureRef(tpe) // if this was compiled from cc syntax, problem should have been reported at Typer
84+
case ref: CaptureRef if ref.isTrackableRef =>
85+
ref
86+
case tpe =>
87+
throw IllegalCaptureRef(tpe) // if this was compiled from cc syntax, problem should have been reported at Typer
9188

9289
/** Convert a @retains or @retainsByName annotation tree to the capture set it represents.
9390
* For efficience, the result is cached as an Attachment on the tree.
@@ -166,7 +163,7 @@ extension (tp: Type)
166163
def forceBoxStatus(boxed: Boolean)(using Context): Type = tp.widenDealias match
167164
case tp @ CapturingType(parent, refs) if tp.isBoxed != boxed =>
168165
val refs1 = tp match
169-
case ref: CaptureRef if ref.isTracked => ref.singletonCaptureSet
166+
case ref: CaptureRef if ref.isTracked || ref.isReach => ref.singletonCaptureSet
170167
case _ => refs
171168
CapturingType(parent, refs1, boxed)
172169
case _ =>
@@ -206,12 +203,6 @@ extension (tp: Type)
206203
case _: TypeRef | _: AppliedType => tp.typeSymbol.hasAnnotation(defn.CapabilityAnnot)
207204
case _ => false
208205

209-
def isSealed(using Context): Boolean = tp match
210-
case tp: TypeParamRef => tp.underlying.isSealed
211-
case tp: TypeBounds => tp.hi.hasAnnotation(defn.Caps_SealedAnnot)
212-
case tp: TypeRef => tp.symbol.is(Sealed) || tp.info.isSealed // TODO: drop symbol flag?
213-
case _ => false
214-
215206
/** Drop @retains annotations everywhere */
216207
def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling
217208
val tm = new TypeMap:
@@ -222,6 +213,62 @@ extension (tp: Type)
222213
mapOver(t)
223214
tm(tp)
224215

216+
/** If `x` is a capture ref, its reach capability `x*`, represented internally
217+
* as `x @reachCapability`. `x*` stands for all capabilities reachable through `x`".
218+
* We have `{x} <: {x*} <: dcs(x)}` where the deep capture set `dcs(x)` of `x`
219+
* is the union of all capture sets that appear in covariant position in the
220+
* type of `x`. If `x` and `y` are different variables then `{x*}` and `{y*}`
221+
* are unrelated.
222+
*/
223+
def reach(using Context): CaptureRef =
224+
assert(tp.isTrackableRef)
225+
AnnotatedType(tp, Annotation(defn.ReachCapabilityAnnot, util.Spans.NoSpan))
226+
227+
/** If `ref` is a trackable capture ref, and `tp` has only covariant occurrences of a
228+
* universal capture set, replace all these occurrences by `{ref*}`. This implements
229+
* the new aspect of the (Var) rule, which can now be stated as follows:
230+
*
231+
* x: T in E
232+
* -----------
233+
* E |- x: T'
234+
*
235+
* where T' is T with (1) the toplevel capture set replaced by `{x}` and
236+
* (2) all covariant occurrences of cap replaced by `x*`, provided there
237+
* are no occurrences in `T` at other variances. (1) is standard, whereas
238+
* (2) is new.
239+
*
240+
* Why is this sound? Covariant occurrences of cap must represent capabilities
241+
* that are reachable from `x`, so they are included in the meaning of `{x*}`.
242+
* At the same time, encapsulation is still maintained since no covariant
243+
* occurrences of cap are allowed in instance types of type variables.
244+
*/
245+
def withReachCaptures(ref: Type)(using Context): Type =
246+
object narrowCaps extends TypeMap:
247+
var ok = true
248+
def apply(t: Type) = t.dealias match
249+
case t1 @ CapturingType(p, cs) if cs.isUniversal =>
250+
if variance > 0 then
251+
t1.derivedCapturingType(apply(p), ref.reach.singletonCaptureSet)
252+
else
253+
ok = false
254+
t
255+
case _ => t match
256+
case t @ CapturingType(p, cs) =>
257+
t.derivedCapturingType(apply(p), cs) // don't map capture set variables
258+
case t =>
259+
mapOver(t)
260+
ref match
261+
case ref: CaptureRef if ref.isTrackableRef =>
262+
val tp1 = narrowCaps(tp)
263+
if narrowCaps.ok then
264+
if tp1 ne tp then capt.println(i"narrow $tp of $ref to $tp1")
265+
tp1
266+
else
267+
capt.println(i"cannot narrow $tp of $ref to $tp1")
268+
tp
269+
case _ =>
270+
tp
271+
225272
extension (cls: ClassSymbol)
226273

227274
def pureBaseClass(using Context): Option[Symbol] =
@@ -281,24 +328,13 @@ extension (sym: Symbol)
281328
&& sym != defn.Caps_unsafeBox
282329
&& sym != defn.Caps_unsafeUnbox
283330

284-
/** Can this symbol possibly own a local root?
285-
* TODO: Disallow anonymous functions?
331+
/** Does this symbol define a level where we do not want to let local variables
332+
* escape into outer capture sets?
286333
*/
287334
def isLevelOwner(using Context): Boolean =
288335
sym.isClass
289336
|| sym.is(Method, butNot = Accessor)
290337

291-
/** The level owner enclosing `sym` which has the given name, or NoSymbol
292-
* if none exists.
293-
*/
294-
def levelOwnerNamed(name: String)(using Context): Symbol =
295-
def recur(sym: Symbol): Symbol =
296-
if sym.name.toString == name then
297-
if sym.isLevelOwner then sym else NoSymbol
298-
else if sym == defn.RootClass then NoSymbol
299-
else recur(sym.owner)
300-
recur(sym)
301-
302338
/** The owner of the current level. Qualifying owners are
303339
* - methods other than constructors and anonymous functions
304340
* - anonymous functions, provided they either define a local
@@ -313,14 +349,6 @@ extension (sym: Symbol)
313349
else recur(sym.owner)
314350
recur(sym)
315351

316-
/** The local root corresponding to sym's level owner */
317-
def localRoot(using Context): Symbol =
318-
val owner = sym.levelOwner
319-
assert(owner.exists)
320-
def newRoot = newSymbol(if owner.isClass then newLocalDummy(owner) else owner,
321-
nme.LOCAL_CAPTURE_ROOT, Synthetic, defn.Caps_Cap.typeRef)
322-
ccState.localRoots.getOrElseUpdate(owner, newRoot)
323-
324352
/** The outermost symbol owned by both `sym` and `other`. if none exists
325353
* since the owning scopes of `sym` and `other` are not nested, invoke
326354
* `onConflict` to return a symbol.
@@ -342,3 +370,21 @@ extension (tp: AnnotatedType)
342370
def isBoxed(using Context): Boolean = tp.annot match
343371
case ann: CaptureAnnotation => ann.boxed
344372
case _ => false
373+
374+
/** An extractor for `caps.reachCapability(ref)`, which is used to express a reach
375+
* capability as a tree in a @retains annotation.
376+
*/
377+
object ReachCapabilityApply:
378+
def unapply(tree: Apply)(using Context): Option[Tree] = tree match
379+
case Apply(reach, arg :: Nil) if reach.symbol == defn.Caps_reachCapability => Some(arg)
380+
case _ => None
381+
382+
/** An extractor for `ref @annotation.internal.reachCapability`, which is used to express
383+
* the reach capability `ref*` as a type.
384+
*/
385+
object ReachCapability:
386+
def unapply(tree: AnnotatedType)(using Context): Option[SingletonCaptureRef] = tree match
387+
case AnnotatedType(parent: SingletonCaptureRef, ann)
388+
if ann.symbol == defn.ReachCapabilityAnnot => Some(parent)
389+
case _ => None
390+

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

Lines changed: 47 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,8 @@ sealed abstract class CaptureSet extends Showable:
7777

7878
/** Does this capture set contain the root reference `cap` as element? */
7979
final def isUniversal(using Context) =
80-
elems.exists(_.isUniversalRootCapability)
81-
82-
/** Does this capture set contain the root reference `cap` as element? */
83-
final def containsRoot(using Context) =
8480
elems.exists(_.isRootCapability)
8581

86-
/** Does this capture set disallow an addiiton of `cap`, whereas it
87-
* might allow an addition of a local root?
88-
*/
89-
final def disallowsUniversal(using Context) =
90-
if isConst then !isUniversal && elems.exists(_.isLocalRootCapability)
91-
else asVar.noUniversal
92-
9382
/** Try to include an element in this capture set.
9483
* @param elem The element to be added
9584
* @param origin The set that originated the request, or `empty` if the request came from outside.
@@ -154,39 +143,21 @@ sealed abstract class CaptureSet extends Showable:
154143
cs.addDependent(this)(using ctx, UnrecordedState)
155144
this
156145

157-
extension (x: CaptureRef)(using Context)
158-
159-
/* x subsumes y if one of the following is true:
160-
* - x is the same as y,
161-
* - x is a this reference and y refers to a field of x
162-
* - x is a super root of y
163-
*/
164-
private def subsumes(y: CaptureRef) =
146+
/** x subsumes x
147+
* this subsumes this.f
148+
* x subsumes y ==> x* subsumes y
149+
* x subsumes y ==> x* subsumes y*
150+
*/
151+
extension (x: CaptureRef)
152+
private def subsumes(y: CaptureRef)(using Context): Boolean =
165153
(x eq y)
166-
|| x.isSuperRootOf(y)
154+
|| x.isRootCapability
167155
|| y.match
168-
case y: TermRef => y.prefix eq x
156+
case y: TermRef => !y.isReach && (y.prefix eq x)
157+
case _ => false
158+
|| x.match
159+
case ReachCapability(x1) => x1.subsumes(y.stripReach)
169160
case _ => false
170-
171-
/** x <:< cap, cap[x] <:< cap
172-
* cap[y] <:< cap[x] if y encloses x
173-
* y <:< cap[x] if y's level owner encloses x's local root owner
174-
*/
175-
private def isSuperRootOf(y: CaptureRef): Boolean = x match
176-
case x: TermRef =>
177-
x.isUniversalRootCapability
178-
|| x.isLocalRootCapability && !y.isUniversalRootCapability && {
179-
val xowner = x.localRootOwner
180-
y match
181-
case y: TermRef =>
182-
xowner.isContainedIn(y.symbol.levelOwner)
183-
case y: ThisType =>
184-
xowner.isContainedIn(y.cls)
185-
case _ =>
186-
false
187-
}
188-
case _ => false
189-
end extension
190161

191162
/** {x} <:< this where <:< is subcapturing, but treating all variables
192163
* as frozen.
@@ -210,7 +181,7 @@ sealed abstract class CaptureSet extends Showable:
210181
*/
211182
def mightAccountFor(x: CaptureRef)(using Context): Boolean =
212183
reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true) {
213-
elems.exists(elem => elem.subsumes(x) || elem.isRootCapability)
184+
elems.exists(_.subsumes(x))
214185
|| !x.isRootCapability
215186
&& {
216187
val elems = x.captureSetOfInfo.elems
@@ -516,7 +487,7 @@ object CaptureSet:
516487
else
517488
//if id == 34 then assert(!elem.isUniversalRootCapability)
518489
elems += elem
519-
if elem.isUniversalRootCapability then
490+
if elem.isRootCapability then
520491
rootAddedHandler()
521492
newElemAddedHandler(elem)
522493
// assert(id != 5 || elems.size != 3, this)
@@ -527,19 +498,17 @@ object CaptureSet:
527498
res.addToTrace(this)
528499

529500
private def levelOK(elem: CaptureRef)(using Context): Boolean =
530-
if elem.isUniversalRootCapability then !noUniversal
501+
if elem.isRootCapability then !noUniversal
531502
else elem match
532-
case elem: TermRef =>
533-
if levelLimit.exists then
534-
var sym = elem.symbol
535-
if sym.isLevelOwner then sym = sym.owner
536-
levelLimit.isContainedIn(sym.levelOwner)
537-
else true
538-
case elem: ThisType =>
539-
if levelLimit.exists then
540-
levelLimit.isContainedIn(elem.cls.levelOwner)
541-
else true
542-
case elem: TermParamRef =>
503+
case elem: TermRef if levelLimit.exists =>
504+
var sym = elem.symbol
505+
if sym.isLevelOwner then sym = sym.owner
506+
levelLimit.isContainedIn(sym.levelOwner)
507+
case elem: ThisType if levelLimit.exists =>
508+
levelLimit.isContainedIn(elem.cls.levelOwner)
509+
case ReachCapability(elem1) =>
510+
levelOK(elem1)
511+
case _ =>
543512
true
544513

545514
def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult =
@@ -567,10 +536,9 @@ object CaptureSet:
567536
* of this set. The universal set {cap} is a sound fallback.
568537
*/
569538
final def upperApprox(origin: CaptureSet)(using Context): CaptureSet =
570-
if isConst then this
571-
else if elems.exists(_.isRootCapability) then
572-
CaptureSet(elems.filter(_.isRootCapability).toList*)
573-
else if computingApprox then
539+
if isConst then
540+
this
541+
else if elems.exists(_.isRootCapability) || computingApprox then
574542
universal
575543
else
576544
computingApprox = true
@@ -633,6 +601,13 @@ object CaptureSet:
633601
override def toString = s"Var$id$elems"
634602
end Var
635603

604+
/** Variables that represent refinements of class parameters can have the universal
605+
* capture set, since they represent only what is the result of the constructor.
606+
* Test case: Without that tweak, logger.scala would not compile.
607+
*/
608+
class RefiningVar(directOwner: Symbol)(using Context) extends Var(directOwner):
609+
override def disallowRootCapability(handler: () => Context ?=> Unit)(using Context) = this
610+
636611
/** A variable that is derived from some other variable via a map or filter. */
637612
abstract class DerivedVar(owner: Symbol, initialElems: Refs)(using @constructorOnly ctx: Context)
638613
extends Var(owner, initialElems):
@@ -1037,6 +1012,8 @@ object CaptureSet:
10371012
/** The capture set of the type underlying CaptureRef */
10381013
def ofInfo(ref: CaptureRef)(using Context): CaptureSet = ref match
10391014
case ref: TermRef if ref.isRootCapability => ref.singletonCaptureSet
1015+
case ReachCapability(ref1) => deepCaptureSet(ref1.widen)
1016+
.showing(i"Deep capture set of $ref: ${ref1.widen} = $result", capt)
10401017
case _ => ofType(ref.underlying, followResult = true)
10411018

10421019
/** Capture set of a type */
@@ -1078,6 +1055,16 @@ object CaptureSet:
10781055
recur(tp)
10791056
.showing(i"capture set of $tp = $result", captDebug)
10801057

1058+
private def deepCaptureSet(tp: Type)(using Context): CaptureSet =
1059+
val collect = new TypeAccumulator[CaptureSet]:
1060+
def apply(cs: CaptureSet, t: Type) = t.dealias match
1061+
case t @ CapturingType(p, cs1) =>
1062+
val cs2 = apply(cs, p)
1063+
if variance > 0 then cs2 ++ cs1 else cs2
1064+
case _ =>
1065+
foldOver(cs, t)
1066+
collect(CaptureSet.empty, tp)
1067+
10811068
private val ShownVars: Property.Key[mutable.Set[Var]] = Property.Key()
10821069

10831070
/** Perform `op`. Under -Ycc-debug, collect and print info about all variables reachable
@@ -1115,7 +1102,7 @@ object CaptureSet:
11151102
override def toAdd(using Context) =
11161103
for CompareResult.LevelError(cs, ref) <- ccState.levelError.toList yield
11171104
ccState.levelError = None
1118-
if ref.isUniversalRootCapability then
1105+
if ref.isRootCapability then
11191106
i"""
11201107
|
11211108
|Note that the universal capability `cap`

0 commit comments

Comments
 (0)