Skip to content

Commit f14c19f

Browse files
committed
Give some explanation of a capture set was under-approximated
1 parent 04dba38 commit f14c19f

File tree

7 files changed

+79
-8
lines changed

7 files changed

+79
-8
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,8 @@ extension (tp: Type)
244244
* the two capture sets are combined.
245245
*/
246246
def capturing(cs: CaptureSet)(using Context): Type =
247-
if cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, frozen = true).isOK
247+
if (cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, frozen = true).isOK)
248+
&& !cs.keepAlways
248249
then tp
249250
else tp match
250251
case CapturingType(parent, cs1) => parent.capturing(cs1 ++ cs)

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

+12-3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ sealed abstract class CaptureSet extends Showable:
8888
final def isUnboxable(using Context) =
8989
elems.exists(elem => elem.isRootCapability || Existential.isExistentialVar(elem))
9090

91+
final def keepAlways: Boolean = this.isInstanceOf[EmptyWithProvenance]
92+
9193
/** Try to include an element in this capture set.
9294
* @param elem The element to be added
9395
* @param origin The set that originated the request, or `empty` if the request came from outside.
@@ -219,7 +221,8 @@ sealed abstract class CaptureSet extends Showable:
219221
* `this` and `that`
220222
*/
221223
def ++ (that: CaptureSet)(using Context): CaptureSet =
222-
if this.subCaptures(that, frozen = true).isOK then that
224+
if this.subCaptures(that, frozen = true).isOK then
225+
if that.isAlwaysEmpty && this.keepAlways then this else that
223226
else if that.subCaptures(this, frozen = true).isOK then this
224227
else if this.isConst && that.isConst then Const(this.elems ++ that.elems)
225228
else Union(this, that)
@@ -294,7 +297,7 @@ sealed abstract class CaptureSet extends Showable:
294297
case _ =>
295298
val mapped = mapRefs(elems, tm, tm.variance)
296299
if isConst then
297-
if mapped.isConst && mapped.elems == elems then this
300+
if mapped.isConst && mapped.elems == elems && !mapped.keepAlways then this
298301
else mapped
299302
else Mapped(asVar, tm, tm.variance, mapped)
300303

@@ -398,6 +401,12 @@ object CaptureSet:
398401
override def toString = elems.toString
399402
end Const
400403

404+
case class EmptyWithProvenance(ref: CaptureRef, mapped: Type) extends Const(SimpleIdentitySet.empty):
405+
override def optionalInfo(using Context): String =
406+
if ctx.settings.YccDebug.value
407+
then i" under-approximating the result of mapping $ref to $mapped"
408+
else ""
409+
401410
/** A special capture set that gets added to the types of symbols that were not
402411
* themselves capture checked, in order to admit arbitrary corresponding capture
403412
* sets in subcapturing comparisons. Similar to platform types for explicit
@@ -863,7 +872,7 @@ object CaptureSet:
863872
|| upper.isConst && upper.elems.size == 1 && upper.elems.contains(r1)
864873
|| r.derivesFrom(defn.Caps_CapSet)
865874
if variance > 0 || isExact then upper
866-
else if variance < 0 then CaptureSet.empty
875+
else if variance < 0 then CaptureSet.EmptyWithProvenance(r, r1)
867876
else upper.maybe
868877

869878
/** Apply `f` to each element in `xs`, and join result sets with `++` */

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ object CapturingType:
3333
* boxing status is the same or if A is boxed.
3434
*/
3535
def apply(parent: Type, refs: CaptureSet, boxed: Boolean = false)(using Context): Type =
36-
if refs.isAlwaysEmpty then parent
36+
if refs.isAlwaysEmpty && !refs.keepAlways then parent
3737
else parent match
3838
case parent @ CapturingType(parent1, refs1) if boxed || !parent.isBoxed =>
3939
apply(parent1, refs ++ refs1, boxed)

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

+22-1
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,25 @@ class CheckCaptures extends Recheck, SymTransformer:
990990
|
991991
|Note that ${msg.toString}"""
992992

993+
private def addApproxAddenda(using Context) =
994+
new TypeAccumulator[Addenda]:
995+
def apply(add: Addenda, t: Type) = t match
996+
case CapturingType(t, CaptureSet.EmptyWithProvenance(ref, mapped)) =>
997+
/* val (origCore, kind) = original match
998+
case tp @ AnnotatedType(parent, ann) if ann.hasSymbol(defn.ReachCapabilityAnnot) =>
999+
(parent, " deep")
1000+
case _ =>
1001+
(original, "")*/
1002+
add ++ new Addenda:
1003+
override def toAdd(using Context): List[String] =
1004+
i"""
1005+
|
1006+
|Note that a capability $ref in a capture set appearing in contravariant position
1007+
|was mapped to $mapped which is not a capability. Therefore, it was under-approximated to the empty set."""
1008+
:: Nil
1009+
case _ =>
1010+
foldOver(add, t)
1011+
9931012
/** Massage `actual` and `expected` types before checking conformance.
9941013
* Massaging is done by the methods following this one:
9951014
* - align dependent function types and add outer references in the expected type
@@ -1015,7 +1034,9 @@ class CheckCaptures extends Recheck, SymTransformer:
10151034
else
10161035
capt.println(i"conforms failed for ${tree}: $actual vs $expected")
10171036
err.typeMismatch(tree.withType(actualBoxed), expected1,
1018-
addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors))
1037+
addApproxAddenda(
1038+
addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors),
1039+
expected1))
10191040
actual
10201041
end checkConformsExpr
10211042

Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/class-contra.scala:12:39 ---------------------------------
22
12 | def fun(x: K{val f: T^{a}}) = x.setf(a) // error
33
| ^
4-
| Found: (a : T^{x, y})
5-
| Required: T
4+
| Found: (a : T^{x, y})
5+
| Required: T^{}
6+
|
7+
| Note that a capability (K.this.f : T^) in a capture set appearing in contravariant position
8+
| was mapped to (x.f : T^{a}) which is not a capability. Therefore, it was under-approximated to the empty set.
69
|
710
| longer explanation available when compiling with `-explain`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:12:10 -------------------------
2+
12 | col.add(Future(() => 25)) // error
3+
| ^^^^^^^^^^^^^^^^
4+
| Found: Future[Int]{val a: (async : Async^)}^{async}
5+
| Required: Future[Int]^{}
6+
|
7+
| Note that a capability Collector.this.futs* in a capture set appearing in contravariant position
8+
| was mapped to col.futs* which is not a capability. Therefore, it was under-approximated to the empty set.
9+
|
10+
| longer explanation available when compiling with `-explain`
11+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:15:11 -------------------------
12+
15 | col1.add(Future(() => 25)) // error
13+
| ^^^^^^^^^^^^^^^^
14+
| Found: Future[Int]{val a: (async : Async^)}^{async}
15+
| Required: Future[Int]^{}
16+
|
17+
| Note that a capability Collector.this.futs* in a capture set appearing in contravariant position
18+
| was mapped to col1.futs* which is not a capability. Therefore, it was under-approximated to the empty set.
19+
|
20+
| longer explanation available when compiling with `-explain`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
trait Async extends caps.Capability
2+
3+
class Future[+T](x: () => T)(using val a: Async)
4+
5+
class Collector[T](val futs: Seq[Future[T]^]):
6+
def add(fut: Future[T]^{futs*}) = ???
7+
8+
def main() =
9+
given async: Async = ???
10+
val futs = (1 to 20).map(x => Future(() => x))
11+
val col = Collector(futs)
12+
col.add(Future(() => 25)) // error
13+
val col1: Collector[Int] { val futs: Seq[Future[Int]^{async}] }
14+
= Collector(futs)
15+
col1.add(Future(() => 25)) // error
16+
17+

0 commit comments

Comments
 (0)