You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When discussing escape checking, we referred to a scoping discipline. That is, capture sets can contain only capabilities that are visible at the point where the set is defined. But that raises the question: where is a universal capability `cap` defined? In fact, what is written as the top type `cap` can mean different capabilities, depending on scope. Usually a `cap` refers to a universal capability defined in the scope where the `cap` appears.
10
10
11
+
A useful mental model is to think of `cap` as a "container" that can _absorb_ concrete capabilities. When you write `T^` (shorthand for `T^{cap}`), you're saying "this value may capture some capabilities that will flow into this `cap`." Different `cap` instances in different scopes are different containers: a capability that flows into one doesn't automatically flow into another.
12
+
11
13
### Existential Binding
12
14
13
15
Special rules apply to `cap`s in method and function parameters and results. For example, take this method:
@@ -16,7 +18,7 @@ Special rules apply to `cap`s in method and function parameters and results. For
This creates a `Logger` that captures `fs`. We could have been more specific in specifying `Logger^{fs}` as the return type, but the current definition is also valid, and might be preferable if we want to hide details of what the returned logger captures. If we write it as above then certainly the implied `cap` in the return type should be able to subsume the capability `fs`. This means that this `cap` has to be defined in a scope in which `fs` is visible.
21
+
This creates a `Logger` that captures `fs`. We could have been more specific in specifying `Logger^{fs}` as the return type, but the current definition is also valid, and might be preferable if we want to hide details of what the returned logger captures. If we write it as above then certainly the implied `cap` in the return type should be able to absorb the capability `fs`. This means that this `cap` has to be defined in a scope in which `fs` is visible.
20
22
21
23
In logic, the usual way to achieve this scoping is with an existential binder. We can express the type of `makeLogger` like this:
22
24
```scala
@@ -32,15 +34,15 @@ There's a connection with [capture polymorphism](polymorphism.md) here. `cap`s i
32
34
33
35
### Function Types
34
36
35
-
The conventions for method types carry over to function types. A function type
37
+
The conventions for method types carry over to function types. A dependent function type
36
38
```scala
37
39
(x: T) ->U^
38
40
```
39
41
is interpreted as having an existentially bound `cap` in the result, like this:
40
42
```scala
41
43
(x: T) ->∃cap.U^{cap}
42
44
```
43
-
The same rules hold for the other kinds of function arrows, `=>`, `?->`, and `?=>`. So `cap` can in this case subsume the function parameter `x` since it is locally bound in the function result.
45
+
The same rules hold for the other kinds of function arrows, `=>`, `?->`, and `?=>`. So `cap` can in this case absorb the function parameter `x` since `x` is locally bound in the function result.
44
46
45
47
However, the expansion of `cap` into an existentially bound variable only applies to functions that use the dependent function style syntax, with explicitly named parameters. Parametric functions such as `A => B^` or `(A₁, ..., Aₖ) -> B^` don't bind their result cap in an existential quantifier. For instance, the function
46
48
```scala
@@ -54,10 +56,11 @@ In other words, existential quantifiers are only inserted in results of function
54
56
55
57
**Examples:**
56
58
57
-
-`A => B` is an alias type that expands to `A ->{cap} B`, therefore
58
-
`(x: T) -> A => B` expands to `(x: T) -> ∃cap.(A ->{cap} B)`.
59
+
-`A => B` is an alias type that expands to `A ->{cap} B`.
60
+
- Therefore
61
+
`(x: T) -> A => B` expands to `(x: T) -> ∃c.(A ->{c} B)`.
- Occurrences of `cap` elsewhere are not translated. They can be seen as representing an existential in the
70
73
scope of the definition in which they appear.
71
74
72
-
### Fresh Capabilities vs Result Capabilities
73
-
74
-
Internally, the compiler represents scoped `cap` instances using two different mechanisms:
75
-
76
-
-**Fresh capabilities** are used for most `cap` instances. They track a _hidden set_ of concrete capabilities they subsume. When you pass a `FileSystem^` to a function expecting `T^`, the fresh capability in the parameter learns that it subsumes your specific `FileSystem`. Fresh capabilities participate in subcapturing: if `{fs} <: {cap}`, the fresh capability records `fs` in its hidden set.
77
-
78
-
-**Result capabilities** are used for `cap` in dependent function results. They are _rigid_—they don't accumulate a hidden set. Instead, two result capabilities can only be related through _unification_, which makes them equivalent. This prevents the result's capture set from being "polluted" by unrelated capabilities.
79
-
80
-
The distinction matters when checking function subtyping:
81
-
82
-
```scala
83
-
valf: (x: FileSystem^) ->Logger^=???
84
-
valg: (x: FileSystem^) ->Logger^{x} = f // OK
85
-
```
86
-
87
-
Here, the result `cap` in `f`'s type is a result capability. When checking if `f` can be assigned to `g`, the checker unifies `f`'s result capability with `{x}`. This works because unification is symmetric—we're just saying "these represent the same capability."
88
-
89
-
In contrast:
90
-
91
-
```scala
92
-
valleaky:Logger^=???
93
-
valf: (x: FileSystem^) ->Logger^= x => leaky // Error
94
-
```
95
-
96
-
This fails because `leaky`'s capture set cannot flow into the result capability—result capabilities don't accept arbitrary capabilities through subsumption, only through unification with other result capabilities tied to the same function.
97
-
98
75
## Levels and Escape Prevention
99
76
100
-
Each capability has a _level_ corresponding to where it was defined. A capability can only be captured by scopes at the same level or nested more deeply.
77
+
Each capability has a _level_ corresponding to where it was defined. The level determines where a capability can flow: it can flow into `cap`s at the same level or more deeply nested, but not outward to enclosing scopes. Later sections on [capability classifiers](classifiers.md) will add a controlled
@@ -129,12 +107,12 @@ Local values like `x`, `y`, and `z` don't define their own levels. They inherit
129
107
130
108
### The Level Check
131
109
132
-
A capability can flow into a capture set only if the capture set's scope is _contained in_ the capability's level owner. In the example above, `ref.set(f)` fails because:
133
-
-`ref`'s type parameter was instantiated at `outer`'s level
134
-
-`f` captures `cap2`, which is at `inner`'s level
135
-
-`outer` is not contained in `inner`, so `cap2` cannot flow into `ref`
110
+
A capability can flow into a `cap`only if that `cap`'s scope is _contained in_ the capability's level owner. In the example above, `ref.set(f)` fails because:
111
+
-`ref`'s type parameter has a `cap` that was instantiated at `outer`'s level
112
+
-`f` captures `c2`, which is at `inner`'s level
113
+
-`outer` is not contained in `inner`, so `c2` cannot flow into `ref`'s `cap`
136
114
137
-
This ensures capabilities can only flow "inward" to more nested scopes, never "outward" to enclosing ones.
115
+
This ensures capabilities flow "inward" to more nested scopes, never "outward" to enclosing ones.
138
116
139
117
### Comparison with Rust Lifetimes
140
118
@@ -149,10 +127,13 @@ fn bad<'a>() -> &'a i32 {
149
127
```
150
128
151
129
```scala
152
-
// Scala CC: rejected because cap would escape its level
153
-
defbad(): () ->Unit=
154
-
valcap=CC()
155
-
() => cap.use()
130
+
// Scala CC: rejected because c escapes inner's level to outer's level
131
+
defouter() =
132
+
varescape: () =>Unit= () => ()
133
+
definner(c: Cap^) =
134
+
escape = () => c.use() // Error: c at inner's level cannot escape to outer
-**Explicit vs. implicit**: Rust lifetimes are often written explicitly (`&'a T`). Scala CC levels are computed automatically from the program structure.
166
-
-**Granularity**: Rust lifetimes can distinguish different fields of a struct. Scala CC levels are coarser, tied to method and class boundaries.
146
+
-**Explicit vs. implicit**: Rust lifetimes are often written explicitly (`&'a T`). Scala capture checking levels are computed automatically from the program structure.
167
147
168
148
## Charging Captures to Enclosing Scopes
169
149
170
-
When a capability is used, the capture checker must verify that all enclosing scopes properly account for it. This process is called _charging_ the capability to the environment.
150
+
When a capability is used, it must flow into the `cap`s of all enclosing scopes. This process is
151
+
called _charging_ the capability to the environment.
171
152
172
153
```scala
173
-
defouter(cap1: FileSystem^):Unit=
174
-
definner(): () ->{cap1} Unit=
175
-
() =>cap1.read() // cap1 is used here
154
+
defouter(fs: FileSystem^):Unit=
155
+
definner(): () ->{fs} Unit=
156
+
() =>fs.read() // cap1 is used here
176
157
inner()
177
158
```
178
159
179
-
When the capture checker sees `cap1.read()`, it verifies that:
180
-
1. The immediately enclosing closure `() => cap1.read()`declares `cap1` in its capture set
181
-
2. The enclosing method `inner`accounts for `cap1` (it does, via its result type)
182
-
3. The enclosing method `outer`accounts for `cap1` (it does, via its parameter)
160
+
When the capture checker sees `fs.read()`, it verifies that`fs` can flow into each enclosing scope:
161
+
1. The immediately enclosing closure `() => fs.read()`must have `fs` in its capture set ✓
162
+
2. The enclosing method `inner`must account for `fs` (it does, via its result type) ✓
163
+
3. The enclosing method `outer`must account for `fs` (it does, via its parameter) ✓
183
164
184
-
At each level, the checker verifies that the used capabilities are a _subcapture_ of what the scope declares:
165
+
If any scope refuses to absorb the capability, capture checking fails:
185
166
186
167
```scala
187
168
defprocess(fs: FileSystem^):Unit=
188
-
valf: () ->Unit= () => fs.read() // Error: {fs} is not a subset of {}
The closure is declared pure (`() -> Unit`), but uses `fs`. Since `{fs}` is not a subset of the empty capture set, capture checking fails.
172
+
The closure is declared pure (`() -> Unit`), meaning its `cap` is the empty set. The capability `fs` cannot flow into an empty set, so this is rejected.
192
173
193
174
## Visibility and Widening
194
175
195
-
As capabilities are charged to outer scopes, they are _filtered_ to include only those visible at each level. When a local capability cannot appear in a type (because it's not visible), the capture set is _widened_ to the smallest visible superset:
176
+
When capabilities flow outward to enclosing scopes, they must remain visible. A local capability cannot appear in a type outside its defining scope. In such cases, the capture set is _widened_ to the smallest visible superset:
196
177
197
178
```scala
198
-
deftest(fs: FileSystem^):Logger^=
179
+
deftest(fs: FileSystem^):Logger^{cap}=
199
180
vallocalLogger=Logger(fs)
200
-
localLogger // Type is widened from Logger^{localLogger} to Logger^{fs}
181
+
localLogger // Type widens from Logger^{localLogger} to Logger^{fs}
201
182
```
202
183
203
-
Here, `localLogger` cannot appear in the result type because it's a local variable. The capture set `{localLogger}`is widened to `{fs}`, which covers it (since `localLogger` captures `fs`) and is visible outside `test`.
184
+
Here, `localLogger` cannot appear in the result type because it's a local variable. The capture set `{localLogger}`widens to `{fs}`, which covers it (since `localLogger` captures `fs`) and is visible outside `test`. In effect, `fs` flows into the result's `cap` instead of `localLogger`.
204
185
205
-
However, widening cannot always succeed:
186
+
However, widening cannot always find a valid target:
206
187
207
188
```scala
208
189
deftest(fs: FileSystem^): () ->Unit=
209
190
vallocalLogger=Logger(fs)
210
191
() => localLogger.log("hello") // Error: cannot widen to empty set
211
192
```
212
193
213
-
The closure's capture set `{localLogger}` would need to be widened to fit the return type `() -> Unit`, but there's no visible capability that covers `localLogger` and fits in the empty set. This is an error.
194
+
The closure captures `localLogger`, but the return type `() -> Unit` has an empty capture set. There's no visible capability that covers `localLogger` and can flow into an empty set, so the checker rejects this code.
0 commit comments