Skip to content

Commit bc35819

Browse files
committed
Unify existentials when matching function types
1 parent d9418ec commit bc35819

File tree

6 files changed

+72
-10
lines changed

6 files changed

+72
-10
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,10 +258,12 @@ trait CaptureRef extends TypeProxy, ValueType:
258258
vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y)))
259259
|| !y.stripReadOnly.isCap && !yIsExistential && canAddHidden && vs.addHidden(hidden, y)
260260
case x @ root.Result(binder) =>
261-
if y.derivesFromSharedCapability then true
262-
else
261+
val result = y match
262+
case y @ root.Result(_) => vs.unify(x, y)
263+
case _ => y.derivesFromSharedCapability
264+
if !result then
263265
ccState.addNote(CaptureSet.ExistentialSubsumesFailure(x, y))
264-
false
266+
result
265267
case _ =>
266268
y match
267269
case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden)

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,14 @@ object CaptureSet:
11021102
/** A map from captureset variables to their dependent sets at the time of the snapshot. */
11031103
private val depsMap: util.EqHashMap[Var, Deps] = new util.EqHashMap
11041104

1105+
/** A map from root.Result values to other such values. If two result values
1106+
* `a` and `b` are unified, then `eqResultMap(a) = b` and `eqResultMap(b) = a`.
1107+
*/
1108+
private var eqResultMap: util.SimpleIdentityMap[root.Result, root.Result] = util.SimpleIdentityMap.empty
1109+
1110+
/** A snapshot of the `eqResultMap` value at the start of a VarState transaction */
1111+
private var eqResultSnapshot: util.SimpleIdentityMap[root.Result, root.Result] | Null = null
1112+
11051113
/** The recorded elements of `v` (it's required that a recording was made) */
11061114
def elems(v: Var): Refs = elemsMap(v)
11071115

@@ -1141,10 +1149,31 @@ object CaptureSet:
11411149
hidden.add(elem)(using ctx, this)
11421150
true
11431151

1152+
/** If root1 and root2 belong to the same binder but have different originalBinders
1153+
* it means that one of the roots was mapped to the binder of the other by a
1154+
* substBinder when comparing two method types. In that case we can unify
1155+
* the two roots1, provided none of the two roots have already been unified
1156+
* themselves. So unification must be 1-1.
1157+
*/
1158+
def unify(root1: root.Result, root2: root.Result)(using Context): Boolean =
1159+
(root1, root2) match
1160+
case (root1 @ root.Result(binder1), root2 @ root.Result(binder2))
1161+
if (binder1 eq binder2)
1162+
&& (root1.rootAnnot.originalBinder ne root2.rootAnnot.originalBinder)
1163+
&& eqResultMap(root1) == null
1164+
&& eqResultMap(root2) == null
1165+
=>
1166+
if eqResultSnapshot == null then eqResultSnapshot = eqResultMap
1167+
eqResultMap = eqResultMap.updated(root1, root2).updated(root2, root1)
1168+
true
1169+
case _ =>
1170+
false
1171+
11441172
/** Roll back global state to what was recorded in this VarState */
11451173
def rollBack(): Unit =
11461174
elemsMap.keysIterator.foreach(_.resetElems()(using this))
11471175
depsMap.keysIterator.foreach(_.resetDeps()(using this))
1176+
if eqResultSnapshot != null then eqResultMap = eqResultSnapshot.nn
11481177

11491178
private var seen: util.EqHashSet[CaptureRef] = new util.EqHashSet
11501179

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1255,8 +1255,11 @@ class CheckCaptures extends Recheck, SymTransformer:
12551255
i"""reference ${ref}$levelStr
12561256
|cannot be included in outer capture set $cs"""
12571257
case ExistentialSubsumesFailure(ex, other) =>
1258+
def since =
1259+
if other.isRootCapability then ""
1260+
else " since that capability is not a SharedCapability"
12581261
i"""the existential capture root in ${ex.rootAnnot.originalBinder.resType}
1259-
|cannot subsume the capability $other"""
1262+
|cannot subsume the capability $other$since"""
12601263
i"""
12611264
|
12621265
|Note that ${msg.toString}"""

tests/neg-custom-args/captures/erased-methods2.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
| Required: (erased x$1: CT[Ex3]^) ?->{fresh} (erased x$2: CT[Ex2]^) ?->{localcap} Unit
66
|
77
| Note that the existential capture root in (erased x$2: CT[Ex2]^) ?=> Unit
8-
| cannot subsume the capability x$1.type
8+
| cannot subsume the capability x$1.type since that capability is not a SharedCapability
99
21 | ?=> (x$2: CT[Ex2]^)
1010
22 | ?=>
1111
23 | //given (CT[Ex3]^) = x$1
@@ -19,7 +19,7 @@
1919
|Required: (erased x$1: CT[Ex3]^) ?->{fresh} (erased x$1: CT[Ex2]^) ?->{localcap} (erased x$2: CT[Ex1]^) ?->{localcap} Unit
2020
|
2121
|Note that the existential capture root in (erased x$1: CT[Ex2]^) ?=> (erased x$2: CT[Ex1]^) ?->{localcap} Unit
22-
|cannot subsume the capability x$1.type
22+
|cannot subsume the capability x$1.type since that capability is not a SharedCapability
2323
32 | ?=> (erased x$2: CT[Ex2]^)
2424
33 | ?=> (erased x$3: CT[Ex1]^)
2525
34 | ?=> Throw(new Ex3)

tests/neg-custom-args/captures/reaches.check

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@
5656
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:67:32 --------------------------------------
5757
67 | val id: (x: File^) -> File^ = x => x // error
5858
| ^^^^^^
59-
| Found: (x: File^) ->? File^{x}
60-
| Required: (x: File^) -> File^{localcap}
59+
| Found: (x: File^) ->? File^{x}
60+
| Required: (x: File^) -> File^{localcap}
6161
|
62-
| Note that the existential capture root in File^
63-
| cannot subsume the capability x.type
62+
| Note that the existential capture root in File^
63+
| cannot subsume the capability x.type since that capability is not a SharedCapability
6464
|
6565
| longer explanation available when compiling with `-explain`
6666
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:70:38 --------------------------------------
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
class A
2+
class B
3+
class S extends caps.SharedCapability
4+
5+
def test(io: Object^): Unit =
6+
val f: (x: A^) -> B^ = ???
7+
val g: A^ -> B^ = f // error
8+
val _: (y: A^) -> B^ = f
9+
val _: (x: A^) -> B^ = g // error
10+
val _: A^ -> B^ = f // error
11+
val _: A^ -> B^ = g
12+
val _: A^ -> B^ = x => g(x) // should be error, since g is pure, g(x): B^{x} , which does not match B^{fresh}
13+
val _: (x: A^) -> B^ = x => f(x) // error: existential in B cannot subsume `x` since `x` is not shared
14+
15+
val h: S -> B^ = ???
16+
val _: (x: S) -> B^ = h // error: direct conversion fails
17+
val _: (x: S) -> B^ = x => h(x) // but eta expansion succeeds (for SharedCapabilities)
18+
19+
val j: (x: S) -> B^ = ???
20+
val _: (x: S) -> B^ = j
21+
val _: (x: S) -> B^ = x => j(x)
22+
val _: S -> B^ = j // error
23+
val _: S -> B^ = x => j(x) // should be error
24+
25+
val g2: A^ => B^ = ???
26+
val _: A^ => B^ = x => g2(x) // should be error: g2(x): B^{g2, x}, and the `x` cannot be subsumed by fresh
27+
val g3: A^ => B^ = ???
28+
val _: A^{io} => B^ = x => g3(x) // ok, now g3(x): B^{g3, x}, which is widened to B^{g3, io}

0 commit comments

Comments
 (0)