Skip to content

Commit db1c784

Browse files
committed
Alternative to sealed: Don't widen to cap under box
1 parent ef97ee2 commit db1c784

File tree

1 file changed

+225
-0
lines changed

1 file changed

+225
-0
lines changed
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
A capture checking variant
2+
==========================
3+
4+
- We use a stricter form of the TOPLAS model where
5+
- we can't unbox and box from and into S^{cap} (as in TOPLAS), and
6+
- we don't allow subtyping to cap under box. I.e.
7+
```
8+
Box S1 ^ C1 <: Box S2 ^ C2
9+
if S1 <: S2, C1 <: C2, C2 != {cap}
10+
```
11+
The aim is to have a system where we detect all leaks on formation
12+
and not some just on access. In the TOPLAS version, we cannot box {cap}
13+
but we can box with some capability {x} and then widen under the box
14+
into {cap}. This was not a problem because we cannot unbox cap so the
15+
data could not be accessed. But now we want to be stricter and already
16+
prevent the formation. This strictness is necessary to ensure soundness
17+
of the `cap -> x*` conversion described below.
18+
19+
- To compensate, and allow capture polymorphism in e.g. function arguments, we allow
20+
some subcapture slack to cap under box when comparing the types of actual and expected types
21+
after rechecking. This has to be done selectively, so that the following are still guaranteed:
22+
23+
- No widening of function results.
24+
That way, library-defined encapsulators like `usingFile` still prevent leaks
25+
on formation.
26+
- No widening in assignments to vars.
27+
That way, we cannot assign local capabilities to global vars.
28+
- No widening in results of trys.
29+
That way, local throws capabilities cannot escape.
30+
31+
- The way to achieve this is to tweak the expected type. Interesting
32+
expected types are created by
33+
34+
1. function parameter types for their arguments,
35+
2. declared types of vals or vars for their right hand sides,
36+
3. declared result types of defs for their right hand sides,
37+
4. declared types of vars for the right hand sides of assignments to them,
38+
5. declared types of seq literals for the elements in that seq literal,
39+
40+
In cases (1) and (2) above we map all covariant occurrences of cap
41+
in the original expected type to a wildcard (i.e. a fluid capture set). This still treats
42+
`try` blocks correctly, since even if the expected type of a try block contains wildcards,
43+
we still need to eliminate capabilities defined in the try through avoidance, and that
44+
will not be possible since we cannot widen to cap via subtyping.
45+
46+
- Technicalities for type comparers and type maps:
47+
48+
1. Subcapturing: We need to thread through the subset propagation logic whether
49+
the elements to add to a capture set come from a boxed set. Maybe it's best
50+
for this if the isBoxed flag was moved from CapturingTypes to CaptureSets?
51+
Or, alternativelty, pass box status to subCaptures as an additional parameter,
52+
but then we also have to deal with uses of subCapturing in, say,
53+
set union or intersection. The knowledge that an element comes from a
54+
boxed set has to be propagated through maps. I.e. if r comes from a boxed
55+
set, we also assume f(r) comes from a boxed set. Then, the `x.subsumes(y)`
56+
test needs to know whether `y` comes from a boxed set. All this looks
57+
rather complicated.
58+
59+
2. Lubs of capture sets can now contain at the same time `cap` and other
60+
references.
61+
62+
3. Avoidance maps can have undefined results. We need to tweak the part
63+
of AvoidMap that widens from a TermRef `ref` to `ref.info`, so that
64+
this is forbidden if the widening appears in a boxed capture set.
65+
This could be achieved by disallowing the root capability in a capture
66+
set that arises from mapping a boxed capture set through avoidance, using
67+
a handler that issues an appropriate error message.
68+
69+
- As in the TOPLAS paper, mutable variables and results of try are implicitly boxed.
70+
71+
- For any immutable variable `x`, introduce a capability `x*` which stands for
72+
"all capabilities reachable through `x`". We have `{x} <: {x*} <: {cap}`.
73+
74+
- We modify the VAR rule as follows:
75+
76+
x: T in E
77+
-----------
78+
E |- x: T'
79+
80+
where T' is T with (1) the toplevel capture set replaced by `{x}` and
81+
(2) all covariant occurrences of cap replaced by `x*`. (1) is standard,
82+
whereas (2) is new.
83+
84+
- Why is this sound? Covariant occurrences of cap must represent capabilities
85+
that are reachable from `x`, so they are included in the meaning of `{x*}`.
86+
87+
88+
Examples:
89+
90+
```scala
91+
class Ref[T](init: T):
92+
private var x: T
93+
def get: T = x
94+
def set(y: T) = { x = y }
95+
```
96+
97+
The following example does not work.
98+
```scala
99+
def runAll(xs: List[Proc]): Unit =
100+
var cur: List[() => Unit] = xs
101+
while cur.nonEmpty do
102+
val next: () => Unit = cur.head // error on unbox
103+
next()
104+
cur = cur.tail
105+
106+
usingFile: f =>
107+
cur = ((() => f.write()): (() ->{f*} Unit)) :: Nil // error since we cannot widen {f*} to {cap} under box
108+
```
109+
Same with refs:
110+
```scala
111+
def runAll(xs: List[Proc]): Unit =
112+
val cur = Ref[List[Proc]](xs: List[() ->{xs*} Unit]) // error on box
113+
while cur.get.nonEmpty do
114+
val next: () => Unit = cur.get.head // error on unbox
115+
next()
116+
cur.set(cur.get.tail: List[Proc])
117+
118+
usingFile: f =>
119+
cur.set:
120+
(() => f.write(): () ->{f*} Unit) :: Nil // error since we cannot widen {f*} to {cap} under box
121+
```
122+
123+
The following variant makes the loop typecheck, but
124+
still rejects the incorrect leakage in `usingFile`.
125+
```scala
126+
def runAll(xs: List[Proc]): Unit =
127+
var cur: List[() ->{xs*} Unit] = xs // OK, by revised VAR
128+
while cur.nonEmpty do
129+
val next: () ->{xs*} Unit = cur.head
130+
next()
131+
cur = cur.tail: List[() ->{xs*} Unit]
132+
133+
usingFile: f =>
134+
cur = (() => f.write(): () ->{f*} Unit) :: Nil // error since {f*} !<: {xs*}
135+
```
136+
137+
Same with refs:
138+
```scala
139+
def runAll(xs: List[Proc]): Unit =
140+
val cur = Ref[List[() ->{xs*} Unit]](xs) // OK, by revised VAR
141+
while cur.get.nonEmpty do
142+
val next: () ->{xs*} Unit = cur.get.head
143+
next()
144+
cur.set(cur.get.tail: List[() ->{xs*} Unit])
145+
146+
usingFile: f =>
147+
cur = (() => f.write(): () ->{f*} Unit) :: Nil // error since {f*} !<: {xs*}
148+
```
149+
150+
The following variant of the while loop is again invalid:
151+
```scala
152+
def runAll(xs: List[Proc]): Unit =
153+
var cur: List[Proc] = xs // error, can't widen under box, xs: List[() ->{xs*} Unit]
154+
while cur.nonEmpty do
155+
val next: () ->{cur*} Unit = cur.head: // error: cur* not wf since cur is not stable
156+
next()
157+
cur = cur.tail
158+
```
159+
More examples. This works:
160+
```scala
161+
def cons(x: Proc, xs: List[Proc]): List[() ->{x, xs*} Unit] =
162+
List.cons[() ->{x, xs*} Unit](x, xs)
163+
```
164+
But this doesn't:
165+
```scala
166+
def addOneProc(xs: List[Proc]) =
167+
def x: Proc = () => write("hello")
168+
val result: List[() ->{x, xs*} Unit] = x :: xs
169+
result // error: can't widen to cap under box in function result
170+
```
171+
And this doesn't either, since `Set` is invariant:
172+
```scala
173+
def cons(x: Proc, xs: Set[Proc]) =
174+
Set.include[Proc](x, xs) // error: can't instantiate type parameter to Proc
175+
```
176+
But this works:
177+
```scala
178+
def cons(x: Proc, xs: Set[Proc]): Set[() ->{x,xs*} Unit] =
179+
Set.include[() ->{x,xs*} Unit](x, xs) // ok
180+
```
181+
182+
This also works:
183+
```scala
184+
def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C =
185+
z => g(f(z))
186+
```
187+
We can also use a widened result type for compose:
188+
```scala
189+
def compose2[A, B, C](f: A => B, g: B => C): A => C =
190+
z => g(f(z))
191+
```
192+
Even this should workL
193+
```scala
194+
def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] =
195+
ps.map(compose1)
196+
// ps: List[(A ->{ps*} A, A ->{ps*} A)]
197+
// Hence compose1's parameters are both of type A ->{ps*} A
198+
// Hence its result type is A ->{ps*} A
199+
// So map's type parameter is A ->{ps*} A
200+
// Expanded typing:
201+
// (ps: List[(A ->{ps*} A, A ->{ps*} A)])
202+
// .map[A ->{ps*} A]: (f: A ->{ps*} A, g: A ->{ps*} A) =>
203+
// compose1[A ->{ps*} A, A ->{ps*} A, A ->{ps*} A](f, g)
204+
// : A -> {f, g} A
205+
// The closure is widened to the non-dependent function type
206+
// (f: A ->{ps*} A, g: A ->{ps*} A) -> A ->{ps*} A
207+
```
208+
209+
But this would not work with `compose2` instead of `compose1`.
210+
211+
Work items:
212+
===========
213+
214+
- Implement x* references.
215+
- internal representation: maybe have a synthetic private member `reach` of
216+
`Any` to which `x*` maps.
217+
- subcapturing: `x <:< x*`.
218+
- Narrowing code: in `adaptBoxed` where `x.type` gets widened to `T^{x}`, also
219+
do the covariant `cap` to `x*` replacement.
220+
- Drop local roots
221+
- Implement restricted subtyping
222+
- Implement adaptation that widens under box
223+
- Drop sealed scheme
224+
225+
def compose(f: A => B, g: B => C): A ->{f, g} C

0 commit comments

Comments
 (0)