Skip to content

Commit 47c5fa3

Browse files
committed
Sync
1 parent fe4f394 commit 47c5fa3

File tree

3 files changed

+38
-7
lines changed

3 files changed

+38
-7
lines changed

docs/_docs/reference/experimental/capture-checking/basics.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@ loophole()
347347
But this will not compile either, since the capture set of the mutable variable `loophole` cannot refer to variable `f`, which is not visible
348348
where `loophole` is defined.
349349

350+
### Monotonicity Rule
351+
350352
Looking at object graphs, we observe a monotonicity property: The capture set of an object `x` covers the capture sets of all objects reachable through `x`. This property is reflected in the type system by the following _monotonicity rule_:
351353

352354
- In a class `C` with a field `f`, the capture set `{this}` covers the capture set `{this.f}` as well as the capture set of any application of `this.f` to pure arguments.

docs/_docs/reference/experimental/capture-checking/classes.md

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ Here class `Super` has local capability `a`, which gets inherited by class
4545
`Sub` and is combined with `Sub`'s own local capability `b`. Class `Sub` also has an argument capability corresponding to its parameter `x`. This capability gets instantiated to `c` in the final constructor call `Sub(c)`. Hence,
4646
the capture set of that call is `{a, b, c}`.
4747

48-
The capture set of the type of `this` of a class is inferred by the capture checker, unless the type is explicitly declared with a self type annotation like this one:
48+
## Capture Set of This
49+
50+
The capture set of the type of `this` of a class is inferred by the capture checker, unless the type is explicitly declared with a self-type annotation like this one:
4951
```scala
5052
class C:
5153
self: D^{a, b} => ...
@@ -73,7 +75,34 @@ we know that the type of `this` must be pure, since `this` is the right hand sid
7375
| of the enclosing class A
7476
```
7577

76-
## Capture Tunnelling
78+
### Traits and Open Classes
79+
80+
The self-type inference behaves differently depending on whether all subclasses of a class are known. For a regular (non-open, non-abstract) class, all subclasses are known at compile time, so the capture checker can precisely infer the self-type. However, for traits, abstract classes, and [`open`](../other-new-features/open-classes.md) classes, arbitrary subclasses may exist, so the capture checker conservatively assumes that `this` may capture capabilities.
81+
82+
For example:
83+
```scala
84+
class A:
85+
def fn: A = this // ok
86+
87+
trait B:
88+
def fn: B = this // error
89+
def fn2: B^ = this // ok
90+
91+
abstract class C:
92+
def fn: C = this // error
93+
def fn2: C^ = this // ok
94+
95+
sealed abstract class D:
96+
def fn: D = this // ok
97+
98+
object D0 extends D
99+
100+
open class E:
101+
def fn: E = this // error
102+
def fn2: E^ = this // ok
103+
```
104+
105+
## Capture Tunneling
77106

78107
Consider the following simple definition of a `Pair` class:
79108
```scala
@@ -93,7 +122,7 @@ def p: Pair[Int ->{ct} String, Logger^{fs}] = Pair(x, y)
93122
```
94123
This might seem surprising. The `Pair(x, y)` value does capture capabilities `ct` and `fs`. Why don't they show up in its type at the outside?
95124

96-
The answer is capture tunnelling. Once a type variable is instantiated to a capturing type, the
125+
The answer is capture tunneling. Once a type variable is instantiated to a capturing type, the
97126
capture is not propagated beyond this point. On the other hand, if the type variable is instantiated
98127
again on access, the capture information "pops out" again. For instance, even though `p` is technically pure because its capture set is empty, writing `p.fst` would record a reference to the captured capability `ct`. So if this access was put in a closure, the capability would again form part of the outer capture set. E.g.
99128
```scala
@@ -144,7 +173,7 @@ end LzyCons
144173
The `LzyCons` class takes two parameters: A head `hd` and a tail `tl`, which is a function
145174
returning a `LzyList`. Both the function and its result can capture arbitrary capabilities.
146175
The result of applying the function is memoized after the first dereference of `tail` in
147-
the private mutable field `cache`. Note that the typing of the assignment `cache = tl()` relies on the monotonicity rule for `{this}` capture sets.
176+
the private mutable field `cache`. Note that the typing of the assignment `cache = tl()` relies on the [monotonicity rule](basics.md#monotonicity-rule) for `{this}` capture sets.
148177

149178
Here is an extension method to define an infix cons operator `#:` for lazy lists. It is analogous
150179
to `::` but instead of a strict list it produces a lazy list without evaluating its right operand.
@@ -204,7 +233,7 @@ Their capture annotations are all as one would expect:
204233
- Filtering a lazy list produces a lazy list that captures the original list as well
205234
as the (possibly impure) filtering predicate.
206235
- Concatenating two lazy lists produces a lazy list that captures both arguments.
207-
- Dropping elements from a lazy list gives a safe approximation where the original list is captured in the result. In fact, it's only some suffix of the list that is retained at run time, but our modelling identifies lazy lists and their suffixes, so this additional knowledge would not be useful.
236+
- Dropping elements from a lazy list gives a safe approximation where the original list is captured in the result. In fact, it's only some suffix of the list that is retained at run time, but our modeling identifies lazy lists and their suffixes, so this additional knowledge would not be useful.
208237

209238
Of course the function passed to `map` or `filter` could also be pure. After all, `A -> B` is a subtype of `(A -> B)^{cap}` which is the same as `A => B`. In that case, the pure function
210239
argument will _not_ show up in the result type of `map` or `filter`. For instance:
@@ -218,7 +247,7 @@ argument does not show up since it is pure. Likewise, if the lazy list
218247
This demonstrates that capability-based
219248
effect systems with capture checking are naturally _effect polymorphic_.
220249

221-
This concludes our example. It's worth mentioning that an equivalent program defining and using standard, strict lists would require no capture annotations whatsoever. It would compile exactly as written now in standard Scala 3, yet one gets the capture checking for free. Essentially, `=>` already means "can capture anything" and since in a strict list side effecting operations are not retained in the result, there are no additional captures to record. A strict list could of course capture side-effecting closures in its elements but then tunnelling applies, since
250+
This concludes our example. It's worth mentioning that an equivalent program defining and using standard, strict lists would require no capture annotations whatsoever. It would compile exactly as written now in standard Scala 3, yet one gets the capture checking for free. Essentially, `=>` already means "can capture anything" and since in a strict list side effecting operations are not retained in the result, there are no additional captures to record. A strict list could of course capture side-effecting closures in its elements but then tunneling applies, since
222251
these elements are represented by a type variable. This means we don't need to annotate anything there either.
223252

224253
Another possibility would be a variant of lazy lists that requires all functions passed to `map`, `filter` and other operations like it to be pure. E.g. `map` on such a list would be defined like this:

docs/_docs/reference/experimental/capture-checking/internals.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ that gets propagated and resolved further out.
3434

3535
When a mapping `m` is performed on a capture set variable `C`, a new variable `Cm` is created that contains the mapped elements and that is linked with `C`. If `C` subsequently acquires further elements through propagation, these are also propagated to `Cm` after being transformed by the `m` mapping. `Cm` also gets the same supersets as `C`, mapped again using `m`.
3636

37-
One interesting aspect of the capture checker concerns the implementation of capture tunnelling. The [foundational theory](https://infoscience.epfl.ch/record/290885) on which capture checking is based makes tunnelling explicit through so-called _box_ and
37+
One interesting aspect of the capture checker concerns the implementation of capture tunneling. The [foundational theory](https://infoscience.epfl.ch/record/290885) on which capture checking is based makes tunneling explicit through so-called _box_ and
3838
_unbox_ operations. Boxing hides a capture set and unboxing recovers it. The capture checker inserts virtual box and unbox operations based on actual and expected types similar to the way the type checker inserts implicit conversions. When capture set variables are first introduced, any capture set in a capturing type that is an instance of a type parameter instance is marked as "boxed". A boxing operation is
3939
inserted if the expected type of an expression is a capturing type with
4040
a boxed capture set variable. The effect of the insertion is that any references

0 commit comments

Comments
 (0)