Skip to content

Commit 3f7f640

Browse files
committed
Augment exclusivity tests with required flag
1 parent 98fef62 commit 3f7f640

File tree

9 files changed

+105
-48
lines changed

9 files changed

+105
-48
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,11 +441,13 @@ object Capabilities:
441441
/** An exclusive capability is a capability that derives
442442
* indirectly from a maximal capability without going through
443443
* a read-only capability or a capability classified as SharedCapability first.
444+
* @param required if true, exclusivity can be obtained by setting the mutability
445+
* status of some capture set variable from Ignored to Writer.
444446
*/
445-
final def isExclusive(using Context): Boolean =
447+
final def isExclusive(required: Boolean = false)(using Context): Boolean =
446448
!isReadOnly
447449
&& !classifier.derivesFrom(defn.Caps_SharedCapability)
448-
&& (isTerminalCapability || captureSetOfInfo.isExclusive)
450+
&& (isTerminalCapability || captureSetOfInfo.isExclusive(required))
449451

450452
/** Similar to isExlusive, but also includes capabilties with capture
451453
* set variables in their info whose status is still open.

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,20 @@ sealed abstract class CaptureSet extends Showable:
148148

149149
final def isAlwaysReadOnly(using Context): Boolean = isConst && isReadOnly
150150

151-
final def isExclusive(using Context): Boolean =
152-
elems.exists(_.isExclusive)
151+
/** Is capture set exclusive? If `required` is true, a variable capture set
152+
* is forced to Writer mutability which makes it exclusive. Otherwise a set
153+
* is exclusive if one of its elements is exclusive.
154+
* Possible issue: If required is true, and the set is a constant, with
155+
* multiple elements that each have a variable capture set, then we make
156+
* the set exclusive by updating the first such variable capture set with
157+
* Ignore mutability to have Write mutability. That makes the effect
158+
* order dependent.
159+
*/
160+
def isExclusive(required: Boolean = false)(using Context): Boolean =
161+
if required && !isConst && mutability == Ignored then
162+
mutability = Writer
163+
mutability == Writer
164+
|| elems.exists(_.isExclusive(required))
153165

154166
/** Similar to isExclusive, but also includes capture set variables
155167
* with unknown status.

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ class CheckCaptures extends Recheck, SymTransformer:
572572

573573
def checkReadOnlyMethod(included: CaptureSet, meth: Symbol): Unit =
574574
included.checkAddedElems: elem =>
575-
if elem.isExclusive then
575+
if elem.isExclusive() then
576576
report.error(
577577
em"""Read-only $meth accesses exclusive capability $elem;
578578
|$meth should be declared an update method to allow this.""",
@@ -2256,10 +2256,9 @@ class CheckCaptures extends Recheck, SymTransformer:
22562256
val pcls = parent.nuType.classSymbol
22572257
val parentIsExclusive =
22582258
if parent.isType then
2259-
capturedVars(pcls).isExclusive
2260-
|| captureSetImpliedByFields(pcls.asClass, parent.nuType).isExclusive
2261-
2262-
else parent.nuType.captureSet.isExclusive
2259+
capturedVars(pcls).isExclusive()
2260+
|| captureSetImpliedByFields(pcls.asClass, parent.nuType).isExclusive()
2261+
else parent.nuType.captureSet.isExclusive()
22632262
if parentIsExclusive then
22642263
report.error(
22652264
em"""illegal inheritance: $cls which extends `Stateful` is not allowed to also extend $pcls

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

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,24 +83,32 @@ object Mutability:
8383
if mbr.symbol.is(Method) then mbr.symbol.isUpdateMethod
8484
else !mbr.symbol.hasAnnotation(defn.UntrackedCapturesAnnot)
8585

86-
/** OK, except if `tp` extends `Stateful` but `tp`'s capture set is non-exclusive */
87-
private def exclusivity(using Context): Exclusivity =
86+
/** OK, except if `tp` extends `Stateful` but `tp`'s capture set is non-exclusive
87+
* @param required if true, exclusivity can be obtained by setting the mutability
88+
* status of some capture set variable from Ignored to Writer.
89+
*/
90+
private def exclusivity(required: Boolean)(using Context): Exclusivity =
8891
if tp.derivesFrom(defn.Caps_Stateful) then
8992
tp match
90-
case tp: Capability if tp.isExclusive => Exclusivity.OK
91-
case _ => tp.captureSet.exclusivity(tp)
93+
case tp: Capability if tp.isExclusive(required) => Exclusivity.OK
94+
case _ =>
95+
if tp.captureSet.isExclusive(required) then Exclusivity.OK
96+
else Exclusivity.ReadOnly(tp)
9297
else Exclusivity.OK
9398

94-
/** Test conditions (1) and (2) listed in `adaptReadOnly` below */
95-
private def exclusivityInContext(using Context): Exclusivity = tp match
99+
/** Test conditions (1) and (2) listed in `adaptReadOnly` below
100+
* @param required if true, exclusivity can be obtained by setting the mutability
101+
* status of some capture set variable from Ignored to Writer.
102+
*/
103+
private def exclusivityInContext(required: Boolean = false)(using Context): Exclusivity = tp match
96104
case tp: ThisType =>
97105
if tp.derivesFrom(defn.Caps_Stateful)
98106
then ctx.owner.inExclusivePartOf(tp.cls)
99107
else Exclusivity.OK
100108
case tp @ TermRef(prefix: Capability, _) =>
101-
prefix.exclusivityInContext.andAlso(tp.exclusivity)
109+
prefix.exclusivityInContext(required).andAlso(tp.exclusivity(required))
102110
case _ =>
103-
tp.exclusivity
111+
tp.exclusivity(required)
104112

105113
def expectsReadOnly(using Context): Boolean = tp match
106114
case tp: PathSelectionProto =>
@@ -109,10 +117,6 @@ object Mutability:
109117
tp.isValueType
110118
&& (!tp.isStatefulType || tp.captureSet.mutability == CaptureSet.Mutability.Reader)
111119

112-
extension (cs: CaptureSet)
113-
private def exclusivity(tp: Type)(using Context): Exclusivity =
114-
if cs.isExclusive then Exclusivity.OK else Exclusivity.ReadOnly(tp)
115-
116120
extension (ref: TermRef | ThisType)
117121
/** Map `ref` to `ref.readOnly` if its type extends Mutble, and one of the
118122
* following is true:
@@ -122,7 +126,7 @@ object Mutability:
122126
*/
123127
def adjustReadOnly(pt: Type)(using Context): Capability = {
124128
if ref.derivesFromStateful
125-
&& (pt.expectsReadOnly || ref.exclusivityInContext != Exclusivity.OK)
129+
&& (pt.expectsReadOnly || ref.exclusivityInContext() != Exclusivity.OK)
126130
then ref.readOnly
127131
else ref
128132
}.showing(i"Adjust RO $ref vs $pt = $result", capt)
@@ -131,7 +135,7 @@ object Mutability:
131135
* of a field of `qualType`.
132136
*/
133137
def checkUpdate(qualType: Type, pos: SrcPos)(msg: => String)(using Context): Unit =
134-
qualType.exclusivityInContext match
138+
qualType.exclusivityInContext(required = true) match
135139
case Exclusivity.OK =>
136140
case err =>
137141
report.error(em"$msg\nsince ${err.description(qualType)}.", pos)
@@ -238,8 +242,8 @@ object Mutability:
238242
case improved @ CapturingType(parent, refs)
239243
if parent.derivesFrom(defn.Caps_Stateful)
240244
&& expected.isValueType
241-
&& refs.isExclusive
242-
&& !original.exclusivityInContext.isOK =>
245+
&& refs.isExclusive()
246+
&& !original.exclusivityInContext().isOK =>
243247
improved.derivedCapturingType(parent, refs.readOnly)
244248
.showing(i"Adapted readonly $improved for $tree with original = $original in ${ctx.owner} --> $result", capt)
245249
case improved =>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ object SepCheck:
260260
*/
261261
def common(refs1: Refs, refs2: Refs) =
262262
refs1.filter: ref =>
263-
ref.isExclusive && refs2.exists(_.stripReadOnly.covers(ref))
263+
ref.isExclusive() && refs2.exists(_.stripReadOnly.covers(ref))
264264
++
265265
refs1
266266
.filter:
Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/ref-with-file.scala:15:12 --------------------------------
2-
15 | withFile: f => // error
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/ref-with-file.scala:18:12 --------------------------------
2+
18 | withFile: f => // error
33
| ^
44
|Found: (f: File^'s1) ->'s2 Ref[File^'s3]^
55
|Required: (f: File^²) => Ref[File^'s4]^'s5
@@ -10,12 +10,12 @@
1010
| ^ refers to a fresh root capability classified as Unscoped in the type of value r
1111
| ^² refers to the universal root capability
1212
| cap is a fresh root capability created in anonymous function of type (f: File^'s1): Ref[File^'s3]^ of parameter parameter f² of method $anonfun
13-
16 | val r = Ref(f)
14-
17 | r
13+
19 | val r = Ref(f)
14+
20 | r
1515
|
1616
| longer explanation available when compiling with `-explain`
17-
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/ref-with-file.scala:18:12 --------------------------------
18-
18 | withFile: f => // error
17+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/ref-with-file.scala:21:12 --------------------------------
18+
21 | withFile: f => // error
1919
| ^
2020
|Found: (f: File^'s6) ->'s7 (??? : -> Nothing)
2121
|Required: (f: File^) => Nothing
@@ -25,7 +25,22 @@
2525
|where: => refers to a fresh root capability created in method Test when checking argument to parameter op of method withFile
2626
| ^ refers to the universal root capability
2727
| cap is a fresh root capability created in anonymous function of type (f: File^'s6): (??? : -> Nothing) of parameter parameter f² of method $anonfun
28-
19 | val r = Ref(f)
29-
20 | ???
28+
22 | val r = Ref(f)
29+
23 | ???
30+
|
31+
| longer explanation available when compiling with `-explain`
32+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/ref-with-file.scala:25:18 --------------------------------
33+
25 | withFileAndRef: (f, r: Ref[String]^) => // error
34+
| ^
35+
|Capability cap outlives its scope: it leaks into outer capture set 's8 which is owned by method Test.
36+
|The leakage occurred when trying to match the following types:
37+
|
38+
|Found: (f: File^'s9, r: Ref[String]^) ->'s10 Ref[String]^
39+
|Required: (f: File^, r: Ref[String]^) => Ref[String]^'s8
40+
|
41+
|where: => refers to a fresh root capability created in method Test when checking argument to parameter op of method withFileAndRef
42+
| ^ and cap refer to the universal root capability
43+
26 | r.put(f.read())
44+
27 | r
3045
|
3146
| longer explanation available when compiling with `-explain`

tests/neg-custom-args/captures/ref-with-file.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ class File:
1111
def withFile[T](op: (f: File^) => T): T =
1212
op(new File)
1313

14+
def withFileAndRef[T](op: (f: File^, r: Ref[String]^) => T): T =
15+
op(File(), Ref(""))
16+
1417
def Test =
1518
withFile: f => // error
1619
val r = Ref(f)
@@ -19,5 +22,7 @@ def Test =
1922
val r = Ref(f)
2023
???
2124

22-
25+
withFileAndRef: (f, r: Ref[String]^) => // error
26+
r.put(f.read())
27+
r
2328

tests/new/test.scala

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
1-
class Ref[T](init: T) extends caps.Mutable:
2-
var x: T = init
3-
update def set(x: T) = this.x = x
4-
5-
class Ref2[T](init: T) extends caps.Mutable:
6-
val x = Ref[T](init)
7-
8-
def test =
9-
val r4 = () =>
10-
val r = Ref2(22)
11-
r
12-
//val _: Ref2[Int]^{r4*} = r4()
13-
r4().x.set(33) // ok
1+
import caps.*
2+
3+
class Ref[T](init: T) extends caps.Stateful, Unscoped:
4+
var x = init
5+
def get: T = x
6+
update def put(y: T): Unit = x = y
7+
8+
class File:
9+
def read(): String = ???
10+
11+
def withFile[T](op: (f: File^) => T): T =
12+
op(new File)
13+
14+
def withFileAndRef[T](op: (f: File^, r: Ref[String]^) => T): T =
15+
op(File(), Ref(""))
16+
17+
def Test =
18+
/*
19+
withFileAndRef: (f, r: Ref[String]^) =>
20+
r.put(f.read())
21+
*/
22+
23+
withFileAndRef: (f, r) =>
24+
r.put(f.read())
25+
26+
27+

tests/pos-custom-args/captures/ref-with-file.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@ class File:
1111
def withFile[T](op: (f: File^) => T): T =
1212
op(new File)
1313

14+
def withFileAndRef[T](op: (f: File^, r: Ref[String]^) => T): T =
15+
op(File(), Ref(""))
16+
1417
def Test =
1518
withFile: f =>
1619
val r = Ref(f.read())
1720
r
21+
withFileAndRef: (f, r: Ref[String]^) =>
22+
r.put(f.read())
23+
1824

1925

0 commit comments

Comments
 (0)