Skip to content

Commit 3c2a206

Browse files
committed
Allow more than one existential per binder.
In a type like () -> (Ref^, Ref^) we need to map the two caps to different existentially bound variables. One could do it with two binders, like this: () -> (ex1: Exists) -> (ex2: Exists) -> (Ref^{ex1}, Ref^{ex2}) But that's impractical since we need to guess the number of binders needed for inferred types where the `cap` occurrences are inferred late. We therefore keep a single binder but use annotated types for the actual references, like this: () -> (ex: Exists) -> (Ref^{ex @Existential}, Ref^{ex @Existential}) Each occurrence of @Existential is a fresh annotation. Therefore, the two capabilities in the previous example count as different. We need to keep the distinction in one-to-one mappings between Fresh.Cap and existentials. TODO: Extend hidden sets and separation checking to existentials. Right now, subtyping identifies existentials on the same level as mutually subsuming references. This is incorrect, we need to be more precise here.
1 parent ffd42da commit 3c2a206

File tree

13 files changed

+212
-50
lines changed

13 files changed

+212
-50
lines changed

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,8 @@ extension (tp: Type)
194194
* - annotated types that represent reach or maybe capabilities
195195
*/
196196
final def isTrackableRef(using Context): Boolean = tp match
197-
case _: (ThisType | TermParamRef) =>
198-
true
197+
case _: ThisType => true
198+
case tp: TermParamRef => !Existential.isBinder(tp)
199199
case tp: TermRef =>
200200
((tp.prefix eq NoPrefix)
201201
|| tp.symbol.isField && !tp.symbol.isStatic && tp.prefix.isTrackableRef
@@ -205,6 +205,7 @@ extension (tp: Type)
205205
tp.symbol.isType && tp.derivesFrom(defn.Caps_CapSet)
206206
case tp: TypeParamRef =>
207207
tp.derivesFrom(defn.Caps_CapSet)
208+
case Existential.Var(_) => true
208209
case AnnotatedType(parent, annot) =>
209210
defn.capabilityWrapperAnnots.contains(annot.symbol) && parent.isTrackableRef
210211
case _ =>

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ trait CaptureRef extends TypeProxy, ValueType:
110110
*/
111111
final def isMaxCapability(using Context): Boolean = this match
112112
case tp: TermRef => tp.isCap || tp.info.derivesFrom(defn.Caps_Exists)
113-
case tp: TermParamRef => tp.underlying.derivesFrom(defn.Caps_Exists)
113+
case Existential.Var(_) => true
114114
case Fresh.Cap(_) => true
115115
case ReadOnlyCapability(tp1) => tp1.isMaxCapability
116116
case _ => false
@@ -228,8 +228,8 @@ trait CaptureRef extends TypeProxy, ValueType:
228228
case _ => false
229229
|| this.match
230230
case ReachCapability(x1) => x1.subsumes(y.stripReach)
231+
case Existential.Var(bv) => subsumesExistentially(bv, y)
231232
case x: TermRef => viaInfo(x.info)(subsumingRefs(_, y))
232-
case x: TermParamRef => subsumesExistentially(x, y)
233233
case x: TypeRef if assumedContainsOf(x).contains(y) => true
234234
case x: TypeRef if x.derivesFrom(defn.Caps_CapSet) =>
235235
x.info match

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

+25-9
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ sealed abstract class CaptureSet extends Showable:
103103
elems.exists(_.stripReadOnly.isCap)
104104

105105
final def isUnboxable(using Context) =
106-
elems.exists(elem => elem.isRootCapability || Existential.isExistentialVar(elem))
106+
elems.exists:
107+
case Existential.Var(_) => true
108+
case elem => elem.isRootCapability
107109

108110
final def isReadOnly(using Context): Boolean =
109111
elems.forall(_.isReadOnly)
@@ -428,7 +430,10 @@ object CaptureSet:
428430

429431
def apply(elems: CaptureRef*)(using Context): CaptureSet.Const =
430432
if elems.isEmpty then empty
431-
else Const(SimpleIdentitySet(elems.map(_.ensuring(_.isTrackableRef))*))
433+
else
434+
for elem <- elems do
435+
assert(elem.isTrackableRef, i"not a trackable ref: $elem")
436+
Const(SimpleIdentitySet(elems*))
432437

433438
def apply(elems: Refs)(using Context): CaptureSet.Const =
434439
if elems.isEmpty then empty else Const(elems)
@@ -563,12 +568,12 @@ object CaptureSet:
563568
private def levelOK(elem: CaptureRef)(using Context): Boolean =
564569
if elem.isRootCapability then
565570
!noUniversal
566-
else if Existential.isExistentialVar(elem) then
567-
!noUniversal
568-
&& !TypeComparer.isOpenedExistential(elem)
569-
// Opened existentials on the left cannot be added to nested capture sets on the right
570-
// of a comparison. Test case is open-existential.scala.
571571
else elem match
572+
case Existential.Var(bv) =>
573+
!noUniversal
574+
&& !TypeComparer.isOpenedExistential(bv)
575+
// Opened existentials on the left cannot be added to nested capture sets on the right
576+
// of a comparison. Test case is open-existential.scala.
572577
case elem: TermRef if level.isDefined =>
573578
elem.prefix match
574579
case prefix: CaptureRef =>
@@ -621,10 +626,13 @@ object CaptureSet:
621626
computingApprox = true
622627
try
623628
val approx = computeApprox(origin).ensuring(_.isConst)
624-
if approx.elems.exists(Existential.isExistentialVar(_)) then
629+
if approx.elems.exists:
630+
case Existential.Var(_) => true
631+
case _ => false
632+
then
625633
ccState.approxWarnings +=
626634
em"""Capture set variable $this gets upper-approximated
627-
|to existential variable from $approx, using {cap} instead."""
635+
|to existential variable from $approx, using {cap} instead."""
628636
universal
629637
else approx
630638
finally computingApprox = false
@@ -1169,6 +1177,8 @@ object CaptureSet:
11691177
try pred finally seen -= ref
11701178
else false
11711179

1180+
override def toString = "open varState"
1181+
11721182
object VarState:
11731183

11741184
/** A class for states that do not allow to record elements or dependent sets.
@@ -1181,6 +1191,7 @@ object CaptureSet:
11811191
override def putElems(v: Var, refs: Refs) = false
11821192
override def putDeps(v: Var, deps: Deps) = false
11831193
override def isOpen = false
1194+
override def toString = "closed varState"
11841195

11851196
/** A closed state that allows a Fresh.Cap instance to subsume a
11861197
* reference `r` only if `r` is already present in the hidden set of the instance.
@@ -1189,6 +1200,7 @@ object CaptureSet:
11891200
@sharable
11901201
object Separate extends Closed:
11911202
override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = false
1203+
override def toString = "separating varState"
11921204

11931205
/** A special state that turns off recording of elements. Used only
11941206
* in `addSub` to prevent cycles in recordings.
@@ -1199,13 +1211,15 @@ object CaptureSet:
11991211
override def putDeps(v: Var, deps: Deps) = true
12001212
override def rollBack(): Unit = ()
12011213
override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = true
1214+
override def toString = "unrecorded varState"
12021215

12031216
/** A closed state that turns off recording of hidden elements (but allows
12041217
* adding them). Used in `mightAccountFor`.
12051218
*/
12061219
@sharable
12071220
private[CaptureSet] object ClosedUnrecorded extends Closed:
12081221
override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = true
1222+
override def toString = "closed unrecorded varState"
12091223

12101224
end VarState
12111225

@@ -1282,6 +1296,8 @@ object CaptureSet:
12821296
case tp: (TypeRef | TypeParamRef) =>
12831297
if tp.derivesFrom(defn.Caps_CapSet) then tp.captureSet
12841298
else empty
1299+
case tp @ Existential.Var(_) =>
1300+
tp.captureSet
12851301
case CapturingType(parent, refs) =>
12861302
recur(parent) ++ refs
12871303
case tp @ AnnotatedType(parent, ann) if ann.hasSymbol(defn.ReachCapabilityAnnot) =>

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

+51-9
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import typer.ErrorReporting.errorType
1111
import Names.TermName
1212
import NameKinds.ExistentialBinderName
1313
import NameOps.isImpureFunction
14+
import CaptureSet.IdempotentCaptRefMap
1415
import reporting.Message
16+
import util.EqHashMap
17+
import util.Spans.NoSpan
1518

1619
/**
1720
@@ -229,6 +232,19 @@ object Existential:
229232
def apply(mk: TermParamRef => Type)(using Context): Type =
230233
exMethodType(mk).toFunctionType(alwaysDependent = true)
231234

235+
/** The (super-) type of existentially bound references */
236+
type Var = AnnotatedType
237+
238+
/** An extractor for existentially bound references of the form ex @existential
239+
* where ex is a TermParamRef of type Exists
240+
*/
241+
object Var:
242+
def apply(boundVar: TermParamRef)(using Context): Var =
243+
AnnotatedType(boundVar, Annotation(defn.ExistentialAnnot, NoSpan))
244+
def unapply(tp: Var)(using Context): Option[TermParamRef] = tp match
245+
case AnnotatedType(bv: TermParamRef, ann) if ann.symbol == defn.ExistentialAnnot => Some(bv)
246+
case _ => None
247+
232248
/** Create existential if bound variable appears in result of `mk` */
233249
def wrap(mk: TermParamRef => Type)(using Context): Type =
234250
val mt = exMethodType(mk)
@@ -242,10 +258,23 @@ object Existential:
242258
case _ =>
243259
core
244260

261+
/** Map existentially bound references referring to `boundVar` one-to-one
262+
* to Fresh.Cap instances
263+
*/
264+
def boundVarToCap(boundVar: TermParamRef, tp: Type)(using Context) =
265+
val subst = new IdempotentCaptRefMap:
266+
val seen = EqHashMap[Annotation, CaptureRef]()
267+
def apply(t: Type): Type = t match
268+
case t @ Var(`boundVar`) =>
269+
seen.getOrElseUpdate(t.annot, Fresh.Cap(NoSymbol))
270+
case _ =>
271+
mapOver(t)
272+
subst(tp)
273+
245274
/** Map top-level existentials to `Fresh.Cap`. */
246275
def toCap(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match
247276
case Existential(boundVar, unpacked) =>
248-
unpacked.substParam(boundVar, Fresh.Cap(NoSymbol))
277+
boundVarToCap(boundVar, unpacked)
249278
case tp1 @ CapturingType(parent, refs) =>
250279
tp1.derivedCapturingType(toCap(parent), refs)
251280
case tp1 @ AnnotatedType(parent, ann) =>
@@ -256,7 +285,7 @@ object Existential:
256285
*/
257286
def toCapDeeply(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match
258287
case Existential(boundVar, unpacked) =>
259-
toCapDeeply(unpacked.substParam(boundVar, Fresh.Cap(NoSymbol)))
288+
toCapDeeply(boundVarToCap(boundVar, unpacked))
260289
case tp1 @ FunctionOrMethod(args, res) =>
261290
val tp2 = tp1.derivedFunctionOrMethod(args, toCapDeeply(res))
262291
if tp2 ne tp1 then tp2 else tp
@@ -293,11 +322,13 @@ object Existential:
293322
super.mapOver(t)
294323

295324
class Wrap(boundVar: TermParamRef) extends CapMap:
325+
private val seen = EqHashMap[CaptureRef, Var]()
326+
296327
def apply(t: Type) = t match
297-
case t: CaptureRef if t.isCapOrFresh => // !!! we should map different fresh refs to different existentials
328+
case t: CaptureRef if t.isCapOrFresh =>
298329
if variance > 0 then
299330
needsWrap = true
300-
boundVar
331+
seen.getOrElseUpdate(t, Var(boundVar))
301332
else
302333
if variance == 0 then
303334
fail(em"""$tp captures the root capability `cap` in invariant position""")
@@ -310,16 +341,27 @@ object Existential:
310341
if variance > 0 then
311342
needsWrap = true
312343
super.mapOver:
313-
defn.FunctionNOf(args, res, contextual).capturing(boundVar.singletonCaptureSet)
344+
defn.FunctionNOf(args, res, contextual)
345+
.capturing(Var(boundVar).singletonCaptureSet)
314346
else mapOver(t)
315347
case _ =>
316348
mapOver(t)
317349
//.showing(i"mapcap $t = $result")
318350

319351
lazy val inverse = new BiTypeMap:
320-
lazy val freshCap = Fresh.Cap(NoSymbol)
321352
def apply(t: Type) = t match
322-
case t: TermParamRef if t eq boundVar => freshCap
353+
case t @ Var(`boundVar`) =>
354+
// do a reverse getOrElseUpdate on `seen` to produce the
355+
// `Fresh.Cap` assosicated with `t`
356+
val it = seen.iterator
357+
var ref: CaptureRef | Null = null
358+
while it.hasNext && ref == null do
359+
val (k, v) = it.next
360+
if v.annot eq t.annot then ref = k
361+
if ref == null then
362+
ref = Fresh.Cap(NoSymbol)
363+
seen(ref) = t
364+
ref
323365
case _ => mapOver(t)
324366
def inverse = Wrap.this
325367
override def toString = "Wrap.inverse"
@@ -359,8 +401,8 @@ object Existential:
359401
case (info: TypeRef) :: rest => info.symbol == defn.Caps_Exists && rest.isEmpty
360402
case _ => false
361403

362-
/** Is `ref` this an existentially bound variable? */
363-
def isExistentialVar(ref: CaptureRef)(using Context) = ref match
404+
/** Is `ref` a TermParamRef representing existentially bound variables? */
405+
def isBinder(ref: CaptureRef)(using Context) = ref match
364406
case ref: TermParamRef => isExistentialMethod(ref.binder)
365407
case _ => false
366408

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
376376
checkSharedOK:
377377
CapturingType(parent2, ann.tree.toCaptureSet)
378378
catch case ex: IllegalCaptureRef =>
379-
report.error(em"Illegal capture reference: ${ex.getMessage.nn}", tptToCheck.srcPos)
379+
if !tptToCheck.isEmpty then
380+
report.error(em"Illegal capture reference: ${ex.getMessage.nn}", tptToCheck.srcPos)
380381
parent2
381382
else if ann.symbol == defn.UncheckedCapturesAnnot then
382383
makeUnchecked(apply(parent))
@@ -917,7 +918,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
917918
var retained = ann.retainedElems.toArray
918919
for i <- 0 until retained.length do
919920
val refTree = retained(i)
920-
for ref <- refTree.toCaptureRefs do
921+
val refs =
922+
try refTree.toCaptureRefs
923+
catch case ex: IllegalCaptureRef =>
924+
report.error(em"Illegal capture reference: ${ex.getMessage.nn}", refTree.srcPos)
925+
Nil
926+
for ref <- refs do
921927
def pos =
922928
if refTree.span.exists then refTree.srcPos
923929
else if ann.span.exists then ann.srcPos

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

+1
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,7 @@ class Definitions {
10711071
@tu lazy val UseAnnot: ClassSymbol = requiredClass("scala.caps.use")
10721072
@tu lazy val ConsumeAnnot: ClassSymbol = requiredClass("scala.caps.consume")
10731073
@tu lazy val RefineOverrideAnnot: ClassSymbol = requiredClass("scala.caps.refineOverride")
1074+
@tu lazy val ExistentialAnnot: ClassSymbol = requiredClass("scala.caps.existential")
10741075
@tu lazy val VolatileAnnot: ClassSymbol = requiredClass("scala.volatile")
10751076
@tu lazy val LanguageFeatureMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.languageFeature")
10761077
@tu lazy val BeanGetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanGetter")

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

+14-7
Original file line numberDiff line numberDiff line change
@@ -2840,14 +2840,21 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
28402840
def canInstantiateWith(assoc: ExAssoc): Boolean = assoc match
28412841
case (bv, bvs) :: assoc1 =>
28422842
if bv == tp1 then
2843-
!Existential.isExistentialVar(tp2)
2844-
|| bvs.contains(tp2)
2845-
|| assoc1.exists(_._1 == tp2)
2843+
tp2 match
2844+
case Existential.Var(bv2) =>
2845+
bvs.contains(bv2) || assoc1.exists(_._1 == bv2)
2846+
case _ =>
2847+
true
28462848
else
28472849
canInstantiateWith(assoc1)
28482850
case Nil =>
28492851
false
2850-
Existential.isExistentialVar(tp1) && canInstantiateWith(assocExistentials)
2852+
tp2 match
2853+
case Existential.Var(bv2) if tp1 eq bv2 =>
2854+
true // for now, existential references referring to the same
2855+
// binder are identified. !!! TODO this needs to be revised
2856+
case _ =>
2857+
canInstantiateWith(assocExistentials)
28512858

28522859
def isOpenedExistential(ref: CaptureRef)(using Context): Boolean =
28532860
openedExistentials.contains(ref)
@@ -2864,7 +2871,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
28642871
.showing(i"existential match not found for $t in $assoc", capt)
28652872

28662873
def apply(t: Type) = t match
2867-
case t: TermParamRef if Existential.isExistentialVar(t) =>
2874+
case t: TermParamRef if Existential.isBinder(t) =>
28682875
// Find outermost existential on the right that can be instantiated to `t`,
28692876
// or `badExistential` if none exists.
28702877
def findMapped(assoc: ExAssoc): CaptureRef = assoc match
@@ -2884,7 +2891,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
28842891
*/
28852892
lazy val inverse = new BiTypeMap:
28862893
def apply(t: Type) = t match
2887-
case t: TermParamRef if Existential.isExistentialVar(t) =>
2894+
case t: TermParamRef if Existential.isBinder(t) =>
28882895
assoc.find(_._1 == t) match
28892896
case Some((_, bvs)) if bvs.nonEmpty => bvs.head
28902897
case _ => bad(t)
@@ -3980,7 +3987,7 @@ class ExplainingTypeComparer(initctx: Context, short: Boolean) extends TypeCompa
39803987
}
39813988

39823989
override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState)(using Context): CaptureSet.CompareResult =
3983-
traceIndented(i"subcaptures $refs1 <:< $refs2, varState = ${vs.toString}") {
3990+
traceIndented(i"subcaptures $refs1 <:< $refs2 in ${vs.toString}") {
39843991
super.subCaptures(refs1, refs2, vs)
39853992
}
39863993

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -6082,7 +6082,7 @@ object Types extends TypeUtils {
60826082
case tp: TypeAlias =>
60836083
ensureTrackable(tp.alias)
60846084
case _ =>
6085-
assert(false, i"not a trackable captureRef ref: $result, ${result.underlyingIterator.toList}")
6085+
assert(false, i"not a trackable CaptureRef: $result with underlying ${result.underlyingIterator.toList}")
60866086
ensureTrackable(result)
60876087

60886088
/** A restriction of the inverse to a function on tracked CaptureRefs */

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

+1
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ class PlainPrinter(_ctx: Context) extends Printer {
441441
case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd"
442442
case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*"
443443
case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?"
444+
case Existential.Var(bv) => toTextRef(bv)
444445
case Fresh.Cap(hidden) =>
445446
val idStr = if showUniqueIds then s"#${hidden.id}" else ""
446447
if printFreshDetailed then s"<cap$idStr hiding " ~ toTextCaptureSet(hidden) ~ ">"

compiler/src/dotty/tools/dotc/transform/Recheck.scala

-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import typer.ErrorReporting.{Addenda, NothingToAdd}
1919
import config.Printers.recheckr
2020
import util.Property
2121
import StdNames.nme
22-
import reporting.trace
2322
import annotation.constructorOnly
2423
import cc.CaptureSet.IdempotentCaptRefMap
2524
import annotation.tailrec

0 commit comments

Comments
 (0)