Skip to content

Commit e007539

Browse files
committed
Fix computation of deep capture set.
A deep capture set should not be shortened to a reach capability `x*` if there are elements in the underlying set that live longer than `x`.
1 parent 1836fb3 commit e007539

File tree

5 files changed

+58
-15
lines changed

5 files changed

+58
-15
lines changed

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

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -222,19 +222,27 @@ extension (tp: Type)
222222
case tp: SingletonCaptureRef => tp.captureSetOfInfo
223223
case _ => CaptureSet.ofType(tp, followResult = false)
224224

225-
/** The deep capture set of a type.
226-
* For singleton capabilities `x` and reach capabilities `x*`, this is `{x*}`, provided
227-
* the underlying capture set resulting from traversing the type is non-empty.
228-
* For other types this is the union of all covariant capture sets embedded
229-
* in the type, as computed by `CaptureSet.ofTypeDeeply`.
225+
/** The deep capture set of a type. This is by default the union of all
226+
* covariant capture sets embedded in the widened type, as computed by
227+
* `CaptureSet.ofTypeDeeply`. If that set is nonempty, and the type is
228+
* a singleton capability `x` or a reach capability `x*`, the deep capture
229+
* set can be narrowed to`{x*}`. However, A deep capture set should not be
230+
* narrowed to a reach capability `x*` if there are elements in the underlying
231+
* set that live longer than `x`. See `delayedRunops.scala` for a test case.
230232
*/
231233
def deepCaptureSet(using Context): CaptureSet =
232234
val dcs = CaptureSet.ofTypeDeeply(tp.widen.stripCapturing)
235+
def reachCanSubsumDcs =
236+
dcs.isUniversal
237+
|| dcs.elems.forall(c => c.pathOwner.isContainedIn(tp.pathOwner))
233238
if dcs.isAlwaysEmpty then tp.captureSet
234239
else tp match
235-
case tp @ ReachCapability(_) => tp.singletonCaptureSet
236-
case tp: SingletonCaptureRef if tp.isTrackableRef => tp.reach.singletonCaptureSet
237-
case _ => tp.captureSet ++ dcs
240+
case tp @ ReachCapability(_) if reachCanSubsumDcs =>
241+
tp.singletonCaptureSet
242+
case tp: SingletonCaptureRef if tp.isTrackableRef && reachCanSubsumDcs =>
243+
tp.reach.singletonCaptureSet
244+
case _ =>
245+
tp.captureSet ++ dcs
238246

239247
/** A type capturing `ref` */
240248
def capturing(ref: CaptureRef)(using Context): Type =
@@ -277,8 +285,18 @@ extension (tp: Type)
277285
/** The first element of this path type */
278286
final def pathRoot(using Context): Type = tp.dealias match
279287
case tp1: NamedType if tp1.symbol.owner.isClass => tp1.prefix.pathRoot
288+
case tp1 @ ReachCapability(tp2) => tp2.pathRoot
280289
case _ => tp
281290

291+
/** If this part starts with `C.this`, the class `C`.
292+
* Otherwise, if it starts with a reference `r`, `r`'s owner.
293+
* Otherwise NoSymbol.
294+
*/
295+
final def pathOwner(using Context): Symbol = pathRoot match
296+
case tp1: NamedType => tp1.symbol.owner
297+
case tp1: ThisType => tp1.cls
298+
case _ => NoSymbol
299+
282300
/** If this is a unboxed capturing type with nonempty capture set, its boxed version.
283301
* Or, if type is a TypeBounds of capturing types, the version where the bounds are boxed.
284302
* The identity for all other types.

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,13 +1090,8 @@ class CheckCaptures extends Recheck, SymTransformer:
10901090
(erefs /: erefs.elems): (erefs, eref) =>
10911091
eref match
10921092
case eref: ThisType if isPureContext(ctx.owner, eref.cls) =>
1093-
def isOuterRef(aref: Type): Boolean = aref.pathRoot match
1094-
case aref: NamedType => eref.cls.isProperlyContainedIn(aref.symbol.owner)
1095-
case aref: ThisType => eref.cls.isProperlyContainedIn(aref.cls)
1096-
case _ => false
1097-
1098-
val outerRefs = arefs.filter(isOuterRef)
1099-
1093+
val outerRefs = arefs.filter: aref =>
1094+
eref.cls.isProperlyContainedIn(aref.pathOwner)
11001095
// Include implicitly added outer references in the capture set of the class of `eref`.
11011096
for outerRef <- outerRefs.elems do
11021097
if !erefs.elems.contains(outerRef)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Error: tests/neg-custom-args/captures/delayedRunops.scala:15:13 -----------------------------------------------------
2+
15 | runOps(ops1) // error
3+
| ^^^^
4+
| reference ops* is not included in the allowed capture set {}
5+
| of an enclosing function literal with expected type () -> Unit
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import language.experimental.captureChecking
2+
3+
// ok
4+
def runOps(ops: List[() => Unit]): Unit =
5+
ops.foreach(op => op())
6+
7+
// ok
8+
def delayedRunOps(ops: List[() => Unit]): () ->{ops*} Unit =
9+
() => runOps(ops)
10+
11+
// unsound: impure operation pretended pure
12+
def delayedRunOps1(ops: List[() => Unit]): () ->{} Unit =
13+
() =>
14+
val ops1 = ops
15+
runOps(ops1) // error
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import language.experimental.captureChecking
2+
3+
def runOps(ops: List[() => Unit]): Unit =
4+
ops.foreach(op => op())
5+
6+
def app[T, U](x: T, op: T => U): () ->{op} U =
7+
() => op(x)
8+
9+
def unsafeRunOps(ops: List[() => Unit]): () ->{} Unit =
10+
app[List[() ->{ops*} Unit], Unit](ops, runOps) // error

0 commit comments

Comments
 (0)