Skip to content

Commit 84245ac

Browse files
committed
Emit warnings for redundant capture references
1 parent 62a11b9 commit 84245ac

File tree

6 files changed

+38
-11
lines changed

6 files changed

+38
-11
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ object CaptureOps:
1616
case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems
1717
case _ => Nil
1818

19+
extension (tree: Tree)
20+
def toCaptureRef(using Context): CaptureRef = tree.tpe.asInstanceOf[CaptureRef]
21+
1922
extension (cs: CaptureSet)
2023
def toAnnotation(using Context): Annotation =
2124
val refs = cs.elems.toList.map {
@@ -33,7 +36,7 @@ object CaptureOps:
3336
extension (annot: Annotation)
3437
def toCaptureSet(using Context): CaptureSet =
3538
assert(annot.symbol == defn.RetainsAnnot)
36-
CaptureSet(retainedElems(annot.tree).map(_.tpe.asInstanceOf[CaptureRef])*)
39+
CaptureSet(retainedElems(annot.tree).map(_.toCaptureRef)*)
3740
.showing(i"toCaptureSet $annot --> $result", capt)
3841

3942
extension (tp: AnnotatedType)

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ sealed abstract class CaptureSet extends Showable:
105105

106106
/** The smallest superset (via <:<) of this capture set that also contains `ref`.
107107
*/
108-
def + (ref: CaptureRef)(using Context) = ++ (ref.singletonCaptureSet)
108+
def + (ref: CaptureRef)(using Context): CaptureSet =
109+
this ++ ref.singletonCaptureSet
109110

110111
/** The largest capture set (via <:<) that is a subset of both `this` and `that`
111112
*/
@@ -124,6 +125,9 @@ sealed abstract class CaptureSet extends Showable:
124125
case cs1: Const => Const(elems1)
125126
case cs1: Var => Diff(cs1, that)
126127

128+
def - (ref: CaptureRef)(using Context): CaptureSet =
129+
this -- ref.singletonCaptureSet
130+
127131
def filter(p: CaptureRef => Boolean)(using Context): CaptureSet = this match
128132
case cs1: Const => Const(elems.filter(p))
129133
case cs1: Var => Filtered(cs1, p)
@@ -160,7 +164,7 @@ object CaptureSet:
160164
private val emptySet = SimpleIdentitySet.empty
161165
@sharable private var varId = 0
162166

163-
val empty: CaptureSet = Const(emptySet)
167+
val empty: CaptureSet.Const = Const(emptySet)
164168

165169
/** The universal capture set `{*}` */
166170
def universal(using Context): CaptureSet =
@@ -169,7 +173,7 @@ object CaptureSet:
169173
/** Used as a recursion brake */
170174
@sharable private[core] val Pending = Const(SimpleIdentitySet.empty)
171175

172-
def apply(elems: CaptureRef*)(using Context): CaptureSet =
176+
def apply(elems: CaptureRef*)(using Context): CaptureSet.Const =
173177
if elems.isEmpty then empty
174178
else Const(SimpleIdentitySet(elems.map(_.normalizedRef)*))
175179

@@ -270,7 +274,7 @@ object CaptureSet:
270274
addSub(other)
271275

272276
def mapRefs(xs: Refs, f: CaptureRef => CaptureSet)(using Context): CaptureSet =
273-
(empty /: xs)((cs, x) => cs ++ f(x))
277+
((empty: CaptureSet) /: xs)((cs, x) => cs ++ f(x))
274278

275279
type CompareResult = CompareResult.Type
276280

compiler/src/dotty/tools/dotc/core/Types.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,14 +2038,14 @@ object Types {
20382038
trait CaptureRef extends SingletonType:
20392039
private var myCaptureSet: CaptureSet = _
20402040
private var myCaptureSetRunId: Int = NoRunId
2041-
private var mySingletonCaptureSet: CaptureSet = null
2041+
private var mySingletonCaptureSet: CaptureSet.Const = null
20422042

20432043
def canBeTracked(using Context): Boolean
20442044
final def isTracked(using Context): Boolean = canBeTracked && !captureSetOfInfo.isAlwaysEmpty
20452045
def isRootCapability(using Context): Boolean = false
20462046
def normalizedRef(using Context): CaptureRef = this
20472047

2048-
def singletonCaptureSet(using Context): CaptureSet =
2048+
def singletonCaptureSet(using Context): CaptureSet.Const =
20492049
if mySingletonCaptureSet == null then
20502050
mySingletonCaptureSet = CaptureSet(this.normalizedRef)
20512051
mySingletonCaptureSet

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,21 @@ object CheckCaptures:
6060
for ref <- tp.refs.elems do
6161
if (ref.captureSet frozen_<:< CaptureSet.empty) == CompareResult.OK then
6262
report.error(em"$ref cannot be tracked since its capture set is empty", pos)
63+
else if tp.parent.captureSet.accountsFor(ref) then
64+
report.warning(em"redundant capture: ${tp.parent} already accounts for $ref", pos)
6365
case _ =>
6466

67+
def checkWellformedPost(ann: Tree)(using Context): Unit =
68+
def choices(prev: List[Tree], elems: List[Tree]): List[List[Tree]] = elems match
69+
case Nil => Nil
70+
case elem :: elems =>
71+
List(elem :: (prev reverse_::: elems)) ++ choices(elem :: prev, elems)
72+
for case first :: others <- choices(Nil, retainedElems(ann)) do
73+
val firstRef = first.toCaptureRef
74+
val remaining = CaptureSet(others.map(_.toCaptureRef)*)
75+
if remaining.accountsFor(firstRef) then
76+
report.warning(em"redundant capture: $remaining already accounts for $firstRef", ann.srcPos)
77+
6578
private inline val disallowGlobal = true
6679

6780
class CheckCaptures extends Recheck:
@@ -257,7 +270,11 @@ class CheckCaptures extends Recheck:
257270
case tree: TypeTree =>
258271
transformType(tree.tpe, inferred = false).foreachPart(
259272
checkWellformedPost(_, tree.srcPos))
260-
273+
tree.tpe.foreachPart {
274+
case AnnotatedType(_, annot) =>
275+
checkWellformedPost(annot.tree)
276+
case _ =>
277+
}
261278
case tree1 @ TypeApply(fn, args) if disallowGlobal =>
262279
for arg <- args do
263280
//println(i"checking $arg in $tree: ${arg.tpe.captureSet}")

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ class CompilationTests {
180180
compileFile("tests/neg-custom-args/i7314.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")),
181181
compileFile("tests/neg-custom-args/feature-shadowing.scala", defaultOptions.and("-Xfatal-warnings", "-feature")),
182182
compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")),
183+
compileFile("tests/neg-custom-args/capt-wf.scala", defaultOptions.and("-Ycc", "-Xfatal-warnings")),
183184
).checkExpectedErrors()
184185
}
185186

tests/neg-custom-args/captures/capt-wf.scala renamed to tests/neg-custom-args/capt-wf.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ def test(c: Cap, other: String): Unit =
1111
val x3a: () => String = s1
1212
val s2 = () => if x1 == null then "" else "abc"
1313
val x4: {s2} C = ??? // OK
14-
val x5: {c, c} C = ??? // warning: redundant
15-
val x6: {c} {c} C = ??? // warning: redundant
16-
val x7: {c} {*} C = ??? // warning: redundant
14+
val x5: {c, c} C = ??? // error: redundant
15+
val x6: {c} {c} C = ??? // error: redundant
16+
val x7: {c} Cap = ??? // error: redundant
1717
val x8: {*} {c} C = ??? // OK
18+
val x9: {c, *} C = ??? // error: redundant
19+
val x10: {*, c} C = ??? // error: redundant
1820

1921
def even(n: Int): Boolean = if n == 0 then true else odd(n - 1)
2022
def odd(n: Int): Boolean = if n == 1 then true else even(n - 1)

0 commit comments

Comments
 (0)