Skip to content

Commit af9ecad

Browse files
committed
Allow only reach capabilities of parameters in deep capture sets
1 parent e007539 commit af9ecad

File tree

11 files changed

+63
-13
lines changed

11 files changed

+63
-13
lines changed

Diff for: compiler/src/dotty/tools/dotc/cc/CaptureOps.scala

+9-5
Original file line numberDiff line numberDiff line change
@@ -232,14 +232,11 @@ extension (tp: Type)
232232
*/
233233
def deepCaptureSet(using Context): CaptureSet =
234234
val dcs = CaptureSet.ofTypeDeeply(tp.widen.stripCapturing)
235-
def reachCanSubsumDcs =
236-
dcs.isUniversal
237-
|| dcs.elems.forall(c => c.pathOwner.isContainedIn(tp.pathOwner))
238235
if dcs.isAlwaysEmpty then tp.captureSet
239236
else tp match
240-
case tp @ ReachCapability(_) if reachCanSubsumDcs =>
237+
case tp @ ReachCapability(_) if tp.isParamPath =>
241238
tp.singletonCaptureSet
242-
case tp: SingletonCaptureRef if tp.isTrackableRef && reachCanSubsumDcs =>
239+
case tp: SingletonCaptureRef if tp.isTrackableRef && tp.isParamPath =>
243240
tp.reach.singletonCaptureSet
244241
case _ =>
245242
tp.captureSet ++ dcs
@@ -297,6 +294,13 @@ extension (tp: Type)
297294
case tp1: ThisType => tp1.cls
298295
case _ => NoSymbol
299296

297+
final def isParamPath(using Context): Boolean = tp.dealias match
298+
case tp1: NamedType =>
299+
tp1.prefix match
300+
case _: ThisType | NoPrefix => tp1.symbol.isOneOf(Param | ParamAccessor)
301+
case prefix => prefix.isParamPath
302+
case _ => false
303+
300304
/** If this is a unboxed capturing type with nonempty capture set, its boxed version.
301305
* Or, if type is a TypeBounds of capturing types, the version where the bounds are boxed.
302306
* The identity for all other types.

Diff for: compiler/src/dotty/tools/dotc/cc/CaptureSet.scala

+7-1
Original file line numberDiff line numberDiff line change
@@ -1119,12 +1119,18 @@ object CaptureSet:
11191119
* replaces caps with reach capabilties.
11201120
*/
11211121
def ofTypeDeeply(tp: Type)(using Context): CaptureSet =
1122+
object ReachesMap extends IdempotentCaptRefMap:
1123+
def apply(t: Type): Type = t match
1124+
case ReachCapability(ref) if !ref.isParamPath =>
1125+
defn.Caps_CapSet.typeRef.capturing(ref.deepCaptureSet)
1126+
case _ =>
1127+
t
11221128
val collect = new TypeAccumulator[CaptureSet]:
11231129
def apply(cs: CaptureSet, t: Type) =
11241130
if variance <= 0 then cs
11251131
else t.dealias match
11261132
case t @ CapturingType(p, cs1) =>
1127-
this(cs, p) ++ cs1
1133+
this(cs, p) ++ cs1.map(ReachesMap)
11281134
case t @ AnnotatedType(parent, ann) =>
11291135
this(cs, parent)
11301136
case t @ FunctionOrMethod(args, res @ Existential(_, _))

Diff for: compiler/src/dotty/tools/dotc/cc/Setup.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
751751
report.warning(em"redundant capture: $dom already accounts for $ref", pos)
752752

753753
if ref.captureSetOfInfo.elems.isEmpty && !ref.derivesFrom(defn.Caps_Capability) then
754-
report.error(em"$ref cannot be tracked since its capture set is empty", pos)
754+
val deepStr = if ref.isReach then " deep" else ""
755+
report.error(em"$ref cannot be tracked since its$deepStr capture set is empty", pos)
755756
check(parent.captureSet, parent)
756757

757758
val others =

Diff for: tests/neg-custom-args/captures/delayedRunops.check

+10
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,13 @@
33
| ^^^^
44
| reference ops* is not included in the allowed capture set {}
55
| of an enclosing function literal with expected type () -> Unit
6+
-- Error: tests/neg-custom-args/captures/delayedRunops.scala:21:13 -----------------------------------------------------
7+
21 | runOps(ops1) // error
8+
| ^^^^
9+
| reference (caps.cap : caps.Capability) is not included in the allowed capture set {}
10+
| of an enclosing function literal with expected type () -> Unit
11+
-- Error: tests/neg-custom-args/captures/delayedRunops.scala:27:13 -----------------------------------------------------
12+
27 | runOps(ops1) // error
13+
| ^^^^
14+
| reference ops* is not included in the allowed capture set {}
15+
| of an enclosing function literal with expected type () -> Unit

Diff for: tests/neg-custom-args/captures/delayedRunops.scala

+12
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,15 @@ import language.experimental.captureChecking
1313
() =>
1414
val ops1 = ops
1515
runOps(ops1) // error
16+
17+
// unsound: impure operation pretended pure
18+
def delayedRunOps2(ops: List[() => Unit]): () ->{} Unit =
19+
() =>
20+
val ops1: List[() => Unit] = ops
21+
runOps(ops1) // error
22+
23+
// unsound: impure operation pretended pure
24+
def delayedRunOps3(ops: List[() => Unit]): () ->{} Unit =
25+
() =>
26+
val ops1: List[() ->{ops*} Unit] = ops
27+
runOps(ops1) // error

Diff for: tests/neg-custom-args/captures/delayedRunops2.scala

+12
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,15 @@ def app[T, U](x: T, op: T => U): () ->{op} U =
88

99
def unsafeRunOps(ops: List[() => Unit]): () ->{} Unit =
1010
app[List[() ->{ops*} Unit], Unit](ops, runOps) // error
11+
12+
def app2[T, U](x: T, op: T => U): () ->{op} U =
13+
() =>
14+
def y: T = x
15+
op(y)
16+
17+
def unsafeRunOps2(ops: List[() => Unit]): () -> Unit =
18+
app2[List[() => Unit], Unit](ops, runOps: List[() => Unit] -> Unit) // error
19+
20+
21+
22+

Diff for: tests/neg-custom-args/captures/reaches.check

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
-- Error: tests/neg-custom-args/captures/reaches.scala:60:31 -----------------------------------------------------------
4848
60 | val leaked = usingFile[File^{id*}]: f => // error
4949
| ^^^
50-
| id* cannot be tracked since its capture set is empty
50+
| id* cannot be tracked since its deep capture set is empty
5151
-- Error: tests/neg-custom-args/captures/reaches.scala:61:18 -----------------------------------------------------------
5252
61 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error
5353
| ^^^
54-
| id* cannot be tracked since its capture set is empty
54+
| id* cannot be tracked since its deep capture set is empty

Diff for: tests/neg-custom-args/captures/wf-reach-1.check

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- Error: tests/neg-custom-args/captures/wf-reach-1.scala:2:17 ---------------------------------------------------------
2+
2 | val y: Object^{x*} = ??? // error
3+
| ^^
4+
| x* cannot be tracked since its deep capture set is empty

Diff for: tests/neg-custom-args/captures/wf-reach-1.scala

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def test(x: List[() -> Unit]) =
2+
val y: Object^{x*} = ??? // error

Diff for: tests/neg/leak-problem.scala

+3-4
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ def useBoxedAsync(x: Box[Async^]): Unit =
1515

1616
def useBoxedAsync1(x: Box[Async^]): Unit = x.get.read() // now ok
1717

18-
def test(): Unit =
19-
val xs: Box[Async^] = ???
18+
def test(xs: Box[Async^]): Unit =
2019
val xsLambda = () => useBoxedAsync(xs)
2120
val _: () ->{xs*} Unit = xsLambda
2221
val _: () -> Unit = xsLambda // error
@@ -27,8 +26,8 @@ def test(): Unit =
2726
t1.read()
2827

2928
val xsLambda2 = () => useBoxedAsync2(xs)
30-
val _: () ->{xs*} Unit = xsLambda
31-
val _: () -> Unit = xsLambda // error
29+
val _: () ->{xs*} Unit = xsLambda2
30+
val _: () -> Unit = xsLambda2 // error
3231

3332
val f: Box[Async^] => Unit = (x: Box[Async^]) => useBoxedAsync(x)
3433

File renamed without changes.

0 commit comments

Comments
 (0)