Skip to content

Commit 783785f

Browse files
committed
Sharpen range approximation for applied types with capture set ranges
This is a stopgap to avoid approximating the core type to Nothing. It can probably be dropped once we capture set ranges that we can keep as inference results. See pos-customargs/captures/16226.scala for a test case where this matters. The problem is what should be the approximation of the range C[{cs1} A .. {cs2} A] where the type constructor `C` is non-variant? If the variance of the enclosing map is positive, this is `C[? >: {cs1} A <: {cs2} A]`, which is a supertype of both range end points `C[{cs1} A]` and `C[{cs2} A]`. But if the variance is negative, we would normally fall back to `Nothing`, since that is the only known subtype of both range end points. This reasoning seems too strict for capture checking. In a sense, we have already inferred `C[A]` before; now we just need to find out what the set should be. What we are after is a notion of a _capture set range_. I.e. something like C[{cs1}..{cs2} A] with the meaning that the capture set of `C` is an unknown set between `cs1` and `cs2`. We don't have that abstraction yet, so for now we approximate by the bounds, which avoids the failures, even though its soundness status is currently unclear.
1 parent 5bd67bb commit 783785f

File tree

3 files changed

+39
-5
lines changed

3 files changed

+39
-5
lines changed

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5909,6 +5909,10 @@ object Types {
59095909
case Range(tyconLo, tyconHi) =>
59105910
range(derivedAppliedType(tp, tyconLo, args), derivedAppliedType(tp, tyconHi, args))
59115911
case _ =>
5912+
def differsOnlyInCaptureSet(arg: Type) = arg match
5913+
case Range(CapturingType(lo, _), CapturingType(hi, _)) => lo eq hi
5914+
case Range(_, _) => false
5915+
case _ => true
59125916
if args.exists(isRange) then
59135917
if variance > 0 then
59145918
tp.derivedAppliedType(tycon, args.map(rangeToBounds)) match
@@ -5946,6 +5950,24 @@ object Types {
59465950
tp.derivedAppliedType(tycon, hiBuf.toList))
59475951
else if tycon.isLambdaSub || args.exists(isRangeOfNonTermTypes) then
59485952
range(defn.NothingType, defn.AnyType)
5953+
else if ctx.phase == Phases.checkCapturesPhase && args.forall(differsOnlyInCaptureSet) then
5954+
// This is a stopgap to avoid approximating the core type to Nothing. It can probably be
5955+
// dropped once we capture set ranges that we can keep as inference results.
5956+
// See pos-customargs/captures/16226.scala for a test case where this matters.
5957+
// The problem is what should be the approximation of the range
5958+
// C[{cs1} A .. {cs2} A]
5959+
// where the type constructor C is non-variant? If the variance of the enclosing map
5960+
// is positive, this is C[? >: {cs1} A <: {cs2} A], which is a supertype of both range end
5961+
// points C[{cs1} A] and C[{cs2} A]. But if the variance is negative, we would normally
5962+
// fall back to Nothing, since that is the only known subtype of both range end points.
5963+
// This reasoning seems too strict for capture checking. In a sense, we have already
5964+
// inferred C[A] before; now we just need to find out what the set should be. What we are
5965+
// after is a notion of a _capture set range_. I.e. something like
5966+
// C[{cs1}..{cs2} A]
5967+
// with the meaning that the capture set of C is an unknown set between cs1 and cs2.
5968+
// We don't have that abstraction yet, so for now we approximate by the bounds, which
5969+
// avoids the failures, even though its soundness status is currently unclear.
5970+
tp.derivedAppliedType(tycon, args.map(rangeToBounds))
59495971
else
59505972
// See lampepfl/dotty#14152
59515973
range(defn.NothingType, tp.derivedAppliedType(tycon, args.map(rangeToBounds)))
@@ -6037,14 +6059,10 @@ object Types {
60376059
/** A range of possible types between lower bound `lo` and upper bound `hi`.
60386060
* Only used internally in `ApproximatingTypeMap`.
60396061
*/
6040-
case class Range(lo: Type, hi: Type) extends UncachedGroundType {
6062+
case class Range(lo: Type, hi: Type) extends UncachedGroundType:
60416063
assert(!lo.isInstanceOf[Range])
60426064
assert(!hi.isInstanceOf[Range])
60436065

6044-
override def toText(printer: Printer): Text =
6045-
lo.toText(printer) ~ ".." ~ hi.toText(printer)
6046-
}
6047-
60486066
/** Approximate wildcards by their bounds */
60496067
class AvoidWildcardsMap(using Context) extends ApproximatingTypeMap:
60506068
protected def mapWild(t: WildcardType) =

compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,8 @@ class PlainPrinter(_ctx: Context) extends Printer {
278278
case ex: Throwable => Str("...")
279279
}
280280
"LazyRef(" ~ refTxt ~ ")"
281+
case Range(lo, hi) =>
282+
toText(lo) ~ " .. " ~ toText(hi)
281283
case _ =>
282284
tp.fallbackToText(this)
283285
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@annotation.capability class Cap
2+
3+
class LazyRef[T](val elem: () => T):
4+
val get: {elem} () -> T = elem
5+
def map[U](f: T => U): {f, this} LazyRef[U] =
6+
new LazyRef(() => f(elem()))
7+
8+
def map[A, B](ref: {*} LazyRef[A], f: A => B): {f, ref} LazyRef[B] =
9+
new LazyRef(() => f(ref.elem()))
10+
11+
def main(io: Cap) = {
12+
def mapd[A, B]: ({io} LazyRef[A], A => B) => {*} LazyRef[B] =
13+
(ref1, f1) => map[A, B](ref1, f1)
14+
}

0 commit comments

Comments
 (0)