|
| 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