Skip to content

Commit b6669df

Browse files
committed
Existential capabilities design draft
1 parent 7bdeb0b commit b6669df

File tree

4 files changed

+232
-7
lines changed

4 files changed

+232
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package dotty.tools
2+
package dotc
3+
package cc
4+
5+
import core.*
6+
import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.*
7+
import CaptureSet.IdempotentCaptRefMap
8+
import StdNames.nme
9+
10+
/**
11+
12+
Handling existentials in CC:
13+
14+
In adapt:
15+
16+
- If an EX is toplevel in expected type, replace with a fresh capture set variable
17+
- If an EX is toplevel in actual type, find a trackable replacement `x` as follows:
18+
+ If actual type is a trackable ref, pick that
19+
+ Otherwise, create a fresh skolem val symbol with currently enclosing
20+
method as owner, and use its termRef
21+
Then,
22+
+ If the EX-bound variable appears only at toplevel, replace it with `x`
23+
+ Otherwise, replace it with a fresh reach capability `x*`.
24+
25+
In avoidance of a type T:
26+
27+
- Replace every local variable in T (including locally created EX-skolems)
28+
with a fresh EX-bound variable, and EX-quantify T over all freshly created EX-bound variables.
29+
- Check that no local variable appears under a box.
30+
31+
In cv-computation (markFree):
32+
33+
- Reach capabilities x* of a parameter x cannot appear in the capture set of
34+
the owning method. They have to be widened to dcs(x), or, where this is not
35+
possible, it's an error.
36+
37+
In well-formedness checking of explicitly written type T:
38+
39+
- If T is not the type of a parameter, check that no EX-bound variable appears
40+
under a box.
41+
42+
Subtype rules
43+
44+
- new alphabet: existentially bound variables `a`.
45+
- they can be stored in environments Gamma.
46+
- they are alpha-renable, usual hygiene conditions apply
47+
48+
Gamma |- EX a.T <: U
49+
if Gamma, a |- T <: U
50+
51+
Gamma |- T <: EX a.U
52+
if a in Gamma, T <: U
53+
54+
Representation:
55+
56+
EX a.T[a] is represented as
57+
58+
r @ RecType(T[TermRef[r.recThis, excap]]
59+
60+
where `excap` is a global synthetic symbol.
61+
62+
Subtype checking algorithm, general scheme:
63+
64+
Maintain two structures in TypeComparer:
65+
66+
openExistentials: List[RecThis]
67+
assocExistentials: Map[RecThis, List[RecThis]]
68+
69+
`openExistentials` corresponds to the list of existential variables stored in the environment.
70+
`assocExistentials` maps existential variables bound by existentials appearing on the right
71+
of a subtype judgement to a list of possible associations. Initally this is openExistentials
72+
at the time when the existential on the right was dropped. It can become a single existential
73+
when the existentially bound key variable is unified with one of the variables in the
74+
environment.
75+
76+
Subtype checking algorithm, steps to add for tp1 <:< tp2:
77+
78+
If tp1 is an existential EX a.tp1a:
79+
80+
val saved = openExistentials
81+
openExistentials = tp1.recThis :: openExistentials
82+
try tp1a <:< tp2
83+
finally openExistentials = saved
84+
85+
If tp2 is an existential EX a.tp2a:
86+
87+
val saved = assocExistentials
88+
assocExistentials = assocExistentials + (tp2.recThis -> openExistentials
89+
try tp1 <:< tp2a
90+
finally assocExistentials = saved
91+
92+
If tp1 and tp2 are existentially bound variables `TermRef(pre1/pre2: RecThis, excap)`:
93+
94+
assocExistentials(pre2).contains(pre1) &&
95+
{ assocExistentials(pre2) = List(pre1); true }
96+
97+
Existential source syntax:
98+
99+
Existential types are ususally not written in source, since we still allow the `^`
100+
syntax that can express most of them more concesely (see below for translation rules).
101+
But we should also allow to write existential types explicity, even if it ends up mainly
102+
for debugging. To express them, we add the following trait definition in the caps object:
103+
104+
trait Exists[X]
105+
106+
A typical expression of an existential is then
107+
108+
Exists[(x: Capability) => A ->{x} B]
109+
110+
Existential types are expanded at Typer to the RecType syntax presented above. It is checked
111+
that the type argument is a dependent function type with one argument of type `Capability` and
112+
that this argument is used only in capture sets of the result type.
113+
114+
Existential types can only appear at the top-level of _legal existential scopes_. These are:
115+
116+
- The type of a binding: i.e a type of a parameter or val, a result type of a def, or
117+
a self type of a class.
118+
- The type of a type ascription in an expression or pattern
119+
- The argument and result types of a function.
120+
121+
Expansion of ^:
122+
123+
We drop `cap` as a capture reference, but keep the unqualified `^` syntax.
124+
This now expands to existentials. The translation treats each legal existential scope
125+
separately. If existential scopes nest, the inner ones are translated first.
126+
127+
The expansion algorithm is then defined as follows:
128+
129+
1. In an existential scope, replace every occurrence of ^ with a fresh existentially
130+
bound variable and quantify over all variables such introduced.
131+
132+
2. After this step, type aliases are expanded. If aliases have aliases in arguments,
133+
the outer alias is expanded before the aliases in the arguments. Each time an alias
134+
is expanded that reveals a `^`, apply step (1).
135+
136+
3. The algorithm ends when no more alieases remain to be expanded.
137+
138+
^ captures outside an existential scope or the right hand side of a type alias (e.g. in
139+
a class parent) are not allowed.
140+
141+
Examples:
142+
143+
- `A => B` is an alias type that expands to `(A -> B)^`, which expands to `EX c. A ->{c} B`.
144+
145+
- `Iterator[A => B]` expands to `EX c. Iterator[A ->{c} B]`
146+
147+
- `A -> B^` expands to `A -> EX c.B^{c}`.
148+
149+
- If we define `type Fun[T] = A -> T`, then `Fun[B^]` expands to `EX c.Fun[B^{c}]`, which
150+
dealiases to `EX c.A -> B^{c}`.
151+
152+
- If we define
153+
154+
type F = A -> Fun[B^]
155+
156+
then the type alias expands to
157+
158+
type F = A -> EX c.A -> B^{c}
159+
160+
since the result type of the RHS is a legal existential scope.
161+
*/
162+
object Existential:
163+
164+
/** Pack current type into an existential so that all references to `sym`
165+
* become bound references of the new existential `rt`.
166+
*/
167+
private class PackMap(sym: Symbol, rt: RecType)(using Context) extends DeepTypeMap, IdempotentCaptRefMap:
168+
def apply(tp: Type): Type = tp match
169+
case ref: TermRef if ref.symbol == sym => TermRef(rt.recThis, defn.globalExcapSymbol)
170+
case _ => mapOver(tp)
171+
172+
/** Unpack current type from an existential `rt` so that all references bound by `rt`
173+
* are recplaced by `ref`.
174+
*/
175+
private class OpenMap(rt: RecType, ref: Type)(using Context) extends DeepTypeMap, IdempotentCaptRefMap:
176+
def apply(tp: Type): Type =
177+
if isExBound(tp, rt) then ref else mapOver(tp)
178+
179+
/** Is `tp` a reference to the bound variable of `rt`? */
180+
private def isExBound(tp: Type, rt: Type)(using Context) = tp match
181+
case tp @ TermRef(RecThis(rt1), _) => (rt1 eq rt) && tp.symbol == defn.globalExcapSymbol
182+
case _ => false
183+
184+
/** Open existential, replacing the bund variable by `ref` */
185+
def open(rt: RecType, ref: Type)(using Context): Type = OpenMap(rt, ref)(rt.parent)
186+
187+
/** Create a new existential skolem symbol with given `owner`. Nesting of existential
188+
* skolem symbols is represented by their owner chain.
189+
*/
190+
def newSkolem(owner: Symbol)(using Context): Symbol =
191+
Symbols.newSymbol(owner, nme.EXCAP, Synthetic, defn.Caps_Cap.typeRef)
192+
193+
/** Create an existential type `ex c.<tp>` so that all references to `sym` in `tp`
194+
* become references to the existentially bound variable `c`.
195+
*/
196+
def fromSymbol(tp: Type, sym: Symbol)(using Context): RecType =
197+
RecType(PackMap(sym, _)(tp))
198+
199+
def unapply(rt: RecType)(using Context): Option[Type] =
200+
if isCaptureChecking && rt.parent.existsPart(isExBound(_, rt))
201+
then Some(rt.parent)
202+
else None
203+
204+
end Existential

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import Comments.{Comment, docCtx}
1515
import util.Spans.NoSpan
1616
import config.Feature
1717
import Symbols.requiredModuleRef
18-
import cc.{CaptureSet, RetainingType}
18+
import cc.{CaptureSet, RetainingType, Existential}
1919
import ast.tpd.ref
2020

2121
import scala.annotation.tailrec
@@ -743,6 +743,8 @@ class Definitions {
743743
}
744744
def JavaEnumType = JavaEnumClass.typeRef
745745

746+
@tu lazy val globalExcapSymbol = Existential.newSkolem(NoSymbol)
747+
746748
@tu lazy val MethodHandleClass: ClassSymbol = requiredClass("java.lang.invoke.MethodHandle")
747749
@tu lazy val MethodHandlesLookupClass: ClassSymbol = requiredClass("java.lang.invoke.MethodHandles.Lookup")
748750
@tu lazy val VarHandleClass: ClassSymbol = requiredClass("java.lang.invoke.VarHandle")
@@ -2193,6 +2195,7 @@ class Definitions {
21932195
syntheticCoreMethods
21942196
ScalaValueClasses()
21952197
JavaEnumClass
2198+
globalExcapSymbol
21962199
// end force initialization
21972200
isInitialized = true
21982201
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ object StdNames {
128128
val DOLLAR_NEW: N = "$new"
129129
val EMPTY: N = ""
130130
val EMPTY_PACKAGE: N = "<empty>"
131+
val EXCAP: N = "$excap"
131132
val EXCEPTION_RESULT_PREFIX: N = "exceptionResult"
132133
val EXPAND_SEPARATOR: N = str.EXPAND_SEPARATOR
133134
val IMPORT: N = "<import>"

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

+23-6
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
4646
monitored = false
4747
GADTused = false
4848
opaquesUsed = false
49+
currentExSkolem = c.owner
4950
recCount = 0
5051
needsGc = false
5152
if Config.checkTypeComparerReset then checkReset()
@@ -67,6 +68,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
6768
private var myInstance: TypeComparer = this
6869
def currentInstance: TypeComparer = myInstance
6970

71+
private var currentExSkolem: Symbol = ctx.owner
72+
7073
/** Is a subtype check in progress? In that case we may not
7174
* permanently instantiate type variables, because the corresponding
7275
* constraint might still be retracted and the instantiation should
@@ -327,12 +330,16 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
327330
else
328331
( (tp1.name eq tp2.name)
329332
&& !sym1.is(Private)
330-
&& tp2.isPrefixDependentMemberRef
331-
&& isSubPrefix(tp1.prefix, tp2.prefix)
332-
&& tp1.signature == tp2.signature
333-
&& !(sym1.isClass && sym2.isClass) // class types don't subtype each other
334-
) ||
335-
thirdTryNamed(tp2)
333+
&& (
334+
if tp1.name == nme.EXCAP then
335+
tp2.symbol.ownersIterator.takeWhile(_.name == nme.EXCAP).contains(tp1.symbol)
336+
else
337+
tp2.isPrefixDependentMemberRef
338+
&& isSubPrefix(tp1.prefix, tp2.prefix)
339+
&& tp1.signature == tp2.signature
340+
&& !(sym1.isClass && sym2.isClass) // class types don't subtype each other
341+
))
342+
|| thirdTryNamed(tp2)
336343
case _ =>
337344
secondTry
338345
end compareNamed
@@ -546,6 +553,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
546553
if reduced.exists then
547554
recur(reduced, tp2) && recordGadtUsageIf { MatchType.thatReducesUsingGadt(tp1) }
548555
else thirdTry
556+
case tp1 @ cc.Existential(_) =>
557+
try
558+
currentExSkolem = cc.Existential.newSkolem(currentExSkolem)
559+
val tp1unpacked = cc.Existential.open(tp1, currentExSkolem.termRef)
560+
recur(tp1unpacked, tp2)
561+
finally
562+
currentExSkolem = currentExSkolem.owner
549563
case _: FlexType =>
550564
true
551565
case _ =>
@@ -696,6 +710,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
696710
end compareRefined
697711

698712
compareRefined
713+
case tp2 @ cc.Existential(_) =>
714+
val tp2unpacked = cc.Existential.open(tp2, currentExSkolem.termRef)
715+
recur(tp1, tp2unpacked)
699716
case tp2: RecType =>
700717
def compareRec = tp1.safeDealias match {
701718
case tp1: RecType =>

0 commit comments

Comments
 (0)