Skip to content

Commit 48f8c0f

Browse files
committed
Consume Unscoped caps assigned to outer variables
Also: Improve consume message
1 parent da00515 commit 48f8c0f

15 files changed

+210
-116
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ object Capabilities:
549549
final def levelOwner(using Context): Symbol =
550550
adjustOwner(computeOwner(mapUnscoped = true))
551551

552-
private def adjustOwner(owner: Symbol)(using Context): Symbol =
552+
final def adjustOwner(owner: Symbol)(using Context): Symbol =
553553
if !owner.exists
554554
|| owner.isClass && (!owner.is(Flags.Module) || owner.isStatic)
555555
|| owner.is(Flags.Method, butNot = Flags.Accessor)

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

Lines changed: 71 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ object SepCheck:
4646
/** The role in which a checked type appears, used for composing error messages */
4747
enum TypeRole:
4848
case Result(sym: Symbol, inferred: Boolean)
49-
case Argument(arg: Tree)
49+
case Argument(arg: Tree, meth: Symbol)
5050
case Qualifier(qual: Tree, meth: Symbol)
51+
case RHS(rhs: Tree, mvar: Symbol)
5152

5253
/** If this is a Result tole, the associated symbol, otherwise NoSymbol */
5354
def dclSym = this match
@@ -59,11 +60,20 @@ object SepCheck:
5960
case Result(sym, inferred) =>
6061
def inferredStr = if inferred then " inferred" else ""
6162
def resultStr = if sym.info.isInstanceOf[MethodicType] then " result" else ""
62-
i"$sym's$inferredStr$resultStr type"
63-
case TypeRole.Argument(_) =>
63+
i"${sym.sanitizedDescription}'s$inferredStr$resultStr type"
64+
case TypeRole.Argument(_, _) =>
6465
"the argument's adapted type"
6566
case TypeRole.Qualifier(_, meth) =>
66-
i"the type of the prefix to a call of $meth"
67+
i"the type of the prefix to a call of ${meth.sanitizedDescription}"
68+
case RHS(rhs, lhsSym) =>
69+
i"the type of the value assigned to $lhsSym"
70+
71+
/** A description how a reference was consumed in this role */
72+
def howConsumed(using Context): String = this match
73+
case TypeRole.Result(meth, _) => i"returned in the result of ${meth.sanitizedDescription}"
74+
case TypeRole.Argument(_, meth) => i"passed as a consume parameter to ${meth.sanitizedDescription}"
75+
case TypeRole.Qualifier(_, meth) => i"used as a prefix to consume ${meth.sanitizedDescription}"
76+
case TypeRole.RHS(_, mvar) => i"consumed in an assignment to $mvar"
6777
end TypeRole
6878

6979
/** A class for segmented sets of consumed references.
@@ -74,13 +84,13 @@ object SepCheck:
7484
/** The references in the set. The array should be treated as immutable in client code */
7585
def refs: Array[Capability]
7686

77-
/** The associated source positoons. The array should be treated as immutable in client code */
78-
def locs: Array[SrcPos]
87+
/** The associated source positoons and type roles. The array should be treated as immutable in client code */
88+
def locs: Array[(SrcPos, TypeRole)]
7989

8090
/** The number of references in the set */
8191
def size: Int
8292

83-
def toMap: Map[Capability, SrcPos] = refs.take(size).zip(locs).toMap
93+
def toMap: Map[Capability, (SrcPos, TypeRole)] = refs.take(size).zip(locs).toMap
8494

8595
def show(using Context) =
8696
s"[${toMap.map((ref, loc) => i"$ref -> $loc").toList}]"
@@ -89,13 +99,13 @@ object SepCheck:
8999
/** A fixed consumed set consisting of the given references `refs` and
90100
* associated source positions `locs`
91101
*/
92-
class ConstConsumedSet(val refs: Array[Capability], val locs: Array[SrcPos]) extends ConsumedSet:
102+
class ConstConsumedSet(val refs: Array[Capability], val locs: Array[(SrcPos, TypeRole)]) extends ConsumedSet:
93103
def size = refs.size
94104

95105
/** A mutable consumed set, which is initially empty */
96106
class MutConsumedSet extends ConsumedSet:
97107
var refs: Array[Capability] = new Array(4)
98-
var locs: Array[SrcPos] = new Array(4)
108+
var locs: Array[(SrcPos, TypeRole)] = new Array(4)
99109
var size = 0
100110
var directPeaks : Refs = emptyRefs
101111

@@ -110,12 +120,12 @@ object SepCheck:
110120
locs = double(locs)
111121

112122
/** If `ref` is in the set, its associated source position, otherwise `null` */
113-
def get(ref: Capability): SrcPos | Null =
123+
def get(ref: Capability): (SrcPos, TypeRole) | Null =
114124
var i = 0
115125
while i < size && (refs(i) ne ref) do i += 1
116126
if i < size then locs(i) else null
117127

118-
def clashing(ref: Capability)(using Context): SrcPos | Null =
128+
def clashing(ref: Capability)(using Context): (SrcPos, TypeRole) | Null =
119129
val refPeaks = ref.directPeaks
120130
if !directPeaks.sharedPeaks(refPeaks).isEmpty then
121131
var i = 0
@@ -126,7 +136,7 @@ object SepCheck:
126136
else null
127137

128138
/** If `ref` is not yet in the set, add it with given source position */
129-
def put(ref: Capability, loc: SrcPos)(using Context): Unit =
139+
def put(ref: Capability, loc: (SrcPos, TypeRole))(using Context): Unit =
130140
if get(ref) == null then
131141
ensureCapacity(1)
132142
refs(size) = ref
@@ -448,21 +458,22 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
448458
* @param loc the position where the capability was consumed
449459
* @param pos the position where the capability was used again
450460
*/
451-
def consumeError(ref: Capability, loc: SrcPos, pos: SrcPos)(using Context): Unit =
461+
def consumeError(ref: Capability, loc: (SrcPos, TypeRole), pos: SrcPos)(using Context): Unit =
462+
val (locPos, role) = loc
452463
report.error(
453-
em"""Separation failure: Illegal access to $ref, which was passed to a
454-
|consume parameter or was used as a prefix to a consume method on line ${loc.line + 1}
455-
|and therefore is no longer available.""",
464+
em"""Separation failure: Illegal access to $ref, which was ${role.howConsumed}
465+
|on line ${locPos.line + 1} and therefore is no longer available.""",
456466
pos)
457467

458468
/** Report a failure where a capability is consumed in a loop.
459469
* @param ref the capability
460470
* @param pos the position where the capability was consumed
461471
*/
462-
def consumeInLoopError(ref: Capability, pos: SrcPos)(using Context): Unit =
472+
def consumeInLoopError(ref: Capability, loc: (SrcPos, TypeRole))(using Context): Unit =
473+
val (pos, role) = loc
463474
report.error(
464475
em"""Separation failure: $ref appears in a loop, therefore it cannot
465-
|be passed to a consume parameter or be used as a prefix of a consume method call.""",
476+
|be ${role.howConsumed}.""",
466477
pos)
467478

468479
// ------------ Checks -----------------------------------------------------
@@ -535,7 +546,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
535546

536547
if arg.needsSepCheck then
537548
//println(i"testing $arg, formal = ${arg.formalType}, peaks = ${argPeaks.actual}/${argPeaks.hidden} against ${currentPeaks.actual}")
538-
checkType(arg.formalType, arg.srcPos, TypeRole.Argument(arg))
549+
checkType(arg.formalType, arg.srcPos, TypeRole.Argument(arg, fn.symbol))
539550
// 2. test argPeaks.hidden against previously captured actuals
540551
if !argPeaks.hidden.sharedPeaks(currentPeaks.actual).isEmpty then
541552
val clashing = clashingPart(argPeaks.hidden, _.actual)
@@ -558,6 +569,35 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
558569
sepApplyError(fn, parts, arg, app)
559570
end checkApply
560571

572+
def checkAssign(tree: Assign)(using Context): Unit =
573+
val Assign(lhs, rhs) = tree
574+
lhs.nuType match
575+
case lhsType: ObjectCapability if lhsType.pathOwner.exists =>
576+
val lhsOwner = lhsType.pathOwner
577+
578+
/** A reference escapes into an outer var if it would have produced a
579+
* level error if it did not have an Unscoped, unprefixed FreshCap
580+
* in some underlying capture set.
581+
*/
582+
def escapes(ref: Capability): Boolean = ref.pathRoot match
583+
case ref @ FreshCap(NoPrefix)
584+
if ref.classifier.derivesFrom(defn.Caps_Unscoped) =>
585+
// we have an escaping reference if the FreshCap's adjustded owner
586+
// is properly contained inside the scope of the variable.
587+
ref.adjustOwner(ref.ccOwner).isProperlyContainedIn(lhsOwner)
588+
case _ =>
589+
ref.visibility.isProperlyContainedIn(lhsOwner) // ref itself is not levelOK
590+
&& !ref.isTerminalCapability // ... and ...
591+
&& ref.captureSetOfInfo.elems.exists(escapes) // some underlying capability escapes
592+
593+
val escaping = spanCaptures(rhs).filter(escapes)
594+
capt.println(i"check assign $tree, $lhsOwner, escaping = $escaping, ${escaping.directFootprint.nonPeaks}")
595+
checkConsumedRefs(escaping.directFootprint.nonPeaks, rhs.nuType,
596+
TypeRole.RHS(rhs, lhs.symbol),
597+
s"the value assigned to ${lhs.symbol} refers to", tree.srcPos)
598+
case _ =>
599+
end checkAssign
600+
561601
/** 1. Check that the capabilities used at `tree` don't overlap with
562602
* capabilities hidden by a previous definition.
563603
* 2. Also check that none of the used capabilities was consumed before.
@@ -567,7 +607,6 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
567607
if !used.isEmpty then
568608
capt.println(i"check use $tree: $used")
569609
val usedPeaks = used.allPeaks
570-
val overlap = defsShadow.allPeaks.sharedPeaks(usedPeaks)
571610
if !defsShadow.allPeaks.sharedPeaks(usedPeaks).isEmpty then
572611
// Drop all Selects unless they select from a `this`
573612
def pathRoot(tree: Tree): Tree = tree match
@@ -595,10 +634,10 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
595634
end if
596635

597636
for ref <- used do
598-
val pos = consumed.clashing(ref)
599-
if pos != null then
637+
val loc = consumed.clashing(ref)
638+
if loc != null then
600639
// println(i"consumed so far ${consumed.refs.toList} with peaks ${consumed.directPeaks.toList}, used = $used, exposed = ${ref.directPeaks }")
601-
consumeError(ref, pos, tree.srcPos)
640+
consumeError(ref, loc, tree.srcPos)
602641
end checkUse
603642

604643
/** If `tp` denotes some version of a singleton capability `x.type` the set `{x, x*}`
@@ -673,10 +712,10 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
673712
pos)
674713

675714
role match
676-
case _: TypeRole.Argument | _: TypeRole.Qualifier =>
715+
case _: TypeRole.Argument | _: TypeRole.Qualifier | _: TypeRole.RHS =>
677716
for ref <- refsToCheck do
678717
if !ref.stripReadOnly.isKnownClassifiedAs(defn.Caps_SharedCapability) then
679-
consumed.put(ref, pos)
718+
consumed.put(ref, (pos, role))
680719
case _ =>
681720
end checkConsumedRefs
682721

@@ -696,7 +735,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
696735
*/
697736
extension (refs: Refs) def pruned =
698737
val deductedType = role match
699-
case TypeRole.Argument(arg) => arg.tpe
738+
case TypeRole.Argument(arg, _) => arg.tpe
700739
case _ => tpe
701740
refs.deductSymRefs(role.dclSym).deduct(explicitRefs(deductedType))
702741

@@ -809,7 +848,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
809848
val refs = tpe.deepCaptureSet.elems
810849
val toCheck = refs.transHiddenSet.directFootprint.nonPeaks.deduct(refs.directFootprint.nonPeaks)
811850
checkConsumedRefs(toCheck, tpe, role, i"${role.description} $tpe hides", pos)
812-
case TypeRole.Argument(arg) =>
851+
case TypeRole.Argument(arg, _) =>
813852
if tpe.hasAnnotation(defn.ConsumeAnnot) then
814853
val capts = spanCaptures(arg).directFootprint.nonPeaks
815854
checkConsumedRefs(capts, tpe, role, i"argument to consume parameter with type ${arg.nuType} refers to", pos)
@@ -982,6 +1021,9 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
9821021
tree.tpe match
9831022
case _: MethodOrPoly =>
9841023
case _ => traverseApply(tree)
1024+
case tree: Assign =>
1025+
traverseChildren(tree)
1026+
checkAssign(tree)
9851027
case _: Block | _: Template =>
9861028
traverseSection(tree)
9871029
case tree: ValDef =>
@@ -1029,8 +1071,8 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
10291071
case tree: WhileDo =>
10301072
val loopConsumed = consumed.segment(traverseChildren(tree))
10311073
if loopConsumed.size != 0 then
1032-
val (ref, pos) = loopConsumed.toMap.head
1033-
consumeInLoopError(ref, pos)
1074+
val (ref, loc) = loopConsumed.toMap.head
1075+
consumeInLoopError(ref, loc)
10341076
case _ =>
10351077
traverseChildren(tree)
10361078
end SepCheck

tests/neg-custom-args/captures/classified-inheritance.check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
-- Error: tests/neg-custom-args/captures/classified-inheritance.scala:10:6 ---------------------------------------------
66
10 |class C3 extends Matrix, Async // error
77
| ^
8-
| class C3 inherits two unrelated classifier traits: trait Control and trait Read
8+
| class C3 inherits two unrelated classifier traits: trait Control and trait Unscoped

tests/neg-custom-args/captures/classified-inheritance.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ class C1 extends caps.Control, caps.SharedCapability // OK
55
class C2 extends caps.Control, caps.Read // error
66

77
trait Async extends caps.Control
8-
class Matrix extends caps.Read
8+
class Matrix extends caps.Unscoped
99

1010
class C3 extends Matrix, Async // error
Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,21 @@
11
-- Error: tests/neg-custom-args/captures/i24373.scala:28:2 -------------------------------------------------------------
22
28 | x1.sink1 // error
33
| ^^
4-
| Separation failure: Illegal access to (x1 : Foo^), which was passed to a
5-
| consume parameter or was used as a prefix to a consume method on line 27
6-
| and therefore is no longer available.
4+
| Separation failure: Illegal access to (x1 : Foo^), which was used as a prefix to consume method sink1
5+
| on line 27 and therefore is no longer available.
76
|
87
| where: ^ refers to a fresh root capability in the type of value x1
98
-- Error: tests/neg-custom-args/captures/i24373.scala:32:2 -------------------------------------------------------------
109
32 | x2.sink2 // error
1110
| ^^
12-
| Separation failure: Illegal access to (x2 : Bar^), which was passed to a
13-
| consume parameter or was used as a prefix to a consume method on line 31
14-
| and therefore is no longer available.
11+
| Separation failure: Illegal access to (x2 : Bar^), which was used as a prefix to consume method sink2
12+
| on line 31 and therefore is no longer available.
1513
|
1614
| where: ^ refers to a fresh root capability in the type of value x2
1715
-- Error: tests/neg-custom-args/captures/i24373.scala:49:8 -------------------------------------------------------------
1816
49 | sink6(x6) // error
1917
| ^^
20-
| Separation failure: Illegal access to (x6 : Bar^), which was passed to a
21-
| consume parameter or was used as a prefix to a consume method on line 48
22-
| and therefore is no longer available.
18+
| Separation failure: Illegal access to (x6 : Bar^), which was passed as a consume parameter to method sink6
19+
| on line 48 and therefore is no longer available.
2320
|
24-
| where: ^ refers to a fresh root capability in the type of value x6
21+
| where: ^ refers to a fresh root capability in the type of value x6
Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,38 @@
11
-- Error: tests/neg-custom-args/captures/i24373a.scala:15:8 ------------------------------------------------------------
22
15 | sink2(x1) // error
33
| ^^
4-
| Separation failure: Illegal access to (x1 : Bar^), which was passed to a
5-
| consume parameter or was used as a prefix to a consume method on line 13
6-
| and therefore is no longer available.
4+
| Separation failure: Illegal access to (x1 : Bar^), which was passed as a consume parameter to method sink1
5+
| on line 13 and therefore is no longer available.
76
|
8-
| where: ^ refers to a fresh root capability in the type of value x1
7+
| where: ^ refers to a fresh root capability in the type of value x1
98
-- Error: tests/neg-custom-args/captures/i24373a.scala:19:8 ------------------------------------------------------------
109
19 | sink1(x2) // error
1110
| ^^
12-
| Separation failure: Illegal access to x2.rd, which was passed to a
13-
| consume parameter or was used as a prefix to a consume method on line 18
14-
| and therefore is no longer available.
11+
| Separation failure: Illegal access to x2.rd, which was passed as a consume parameter to method sink2
12+
| on line 18 and therefore is no longer available.
1513
-- Error: tests/neg-custom-args/captures/i24373a.scala:20:8 ------------------------------------------------------------
1614
20 | sink2(x2) // error
1715
| ^^
18-
| Separation failure: Illegal access to (x2 : Bar^), which was passed to a
19-
| consume parameter or was used as a prefix to a consume method on line 18
20-
| and therefore is no longer available.
16+
| Separation failure: Illegal access to (x2 : Bar^), which was passed as a consume parameter to method sink2
17+
| on line 18 and therefore is no longer available.
2118
|
22-
| where: ^ refers to a fresh root capability in the type of value x2
19+
| where: ^ refers to a fresh root capability in the type of value x2
2320
-- Error: tests/neg-custom-args/captures/i24373a.scala:25:8 ------------------------------------------------------------
2421
25 | sink2(x3) // error
2522
| ^^
26-
| Separation failure: Illegal access to (x3 : Bar^), which was passed to a
27-
| consume parameter or was used as a prefix to a consume method on line 23
28-
| and therefore is no longer available.
23+
| Separation failure: Illegal access to (x3 : Bar^), which was passed as a consume parameter to method sink3
24+
| on line 23 and therefore is no longer available.
2925
|
30-
| where: ^ refers to a fresh root capability in the type of value x3
26+
| where: ^ refers to a fresh root capability in the type of value x3
3127
-- Error: tests/neg-custom-args/captures/i24373a.scala:29:8 ------------------------------------------------------------
3228
29 | sink3(x4) // error
3329
| ^^
34-
| Separation failure: Illegal access to x4.rd, which was passed to a
35-
| consume parameter or was used as a prefix to a consume method on line 28
36-
| and therefore is no longer available.
30+
| Separation failure: Illegal access to x4.rd, which was passed as a consume parameter to method sink2
31+
| on line 28 and therefore is no longer available.
3732
-- Error: tests/neg-custom-args/captures/i24373a.scala:30:8 ------------------------------------------------------------
3833
30 | sink2(x4) // error
3934
| ^^
40-
| Separation failure: Illegal access to (x4 : Bar^), which was passed to a
41-
| consume parameter or was used as a prefix to a consume method on line 28
42-
| and therefore is no longer available.
35+
| Separation failure: Illegal access to (x4 : Bar^), which was passed as a consume parameter to method sink2
36+
| on line 28 and therefore is no longer available.
4337
|
44-
| where: ^ refers to a fresh root capability in the type of value x4
38+
| where: ^ refers to a fresh root capability in the type of value x4

0 commit comments

Comments
 (0)