WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
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
Copy file name to clipboardExpand all lines: docs/_docs/reference/experimental/capture-checking/scoped-caps.md
+94-30Lines changed: 94 additions & 30 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -14,18 +14,18 @@ We will discuss three distinct kinds of `cap` in this chapter:
14
14
15
15
**Local caps**: Every class, method body, and block has its own local `cap`. It abstracts over the capabilities used inside that scope, representing them by a single name to the outside world. Local caps form a subcapturing hierarchy based on lexical nesting.
16
16
17
-
**Parameter caps**: When `cap` appears in a function parameter type (e.g., `def foo(x: T^)`), it gets its own cap scoped to that parameter. At call sites, parameter caps are instantiated to the actual capabilities passed in.
17
+
**Parameter caps**: When `cap` appears in a function parameter type (e.g., `def foo(x: T^)`), it gets its own `cap` scoped to that parameter. At call sites, parameter caps are instantiated to the actual capabilities passed in.
18
18
19
-
**Result caps**: When `cap` appears in a function result type (e.g., `def foo(x: T): U^`), it becomes an existentially-bound cap that describes what the caller receives. Unlike local caps, result caps do _not_ subsume the enclosing scope's `cap`.
19
+
**Result caps**: When `cap` appears in a function result type (e.g., `def foo(x: T): U^`), it becomes an existentially-bound `cap` that describes what the caller receives.
20
20
21
21
So, when writing `T^` (shorthand for `T^{cap}`), `cap` is a way of saying "captures something" without
22
22
naming what it is precisely, and depending of the context occurrence of such `cap`s, the capture checker imposes
23
-
restrictions on which capabilities are allowed to flow into them. We will further expand on this idea
23
+
restrictions on which capabilities are allowed to flow into them by means of subcapturing. We will further expand on this idea
24
24
(and other kinds of `cap`) later when discussing [separation checking](separation-checking.md).
25
25
26
26
Another analogy for the different `cap`s is that they are some form of implicitly named existential or abstract self-capture set attached to elements of the program structure, e.g., scopes, parameters, or return values.
27
27
28
-
## Subcapturing and Levels
28
+
## Local Caps
29
29
30
30
Local `cap`s form a subcapturing hierarchy based on lexical nesting: a nested scope's local `cap` subsumes its enclosing scope's local `cap`. This makes sense because the inner scope can use any capability available in the outer scope
31
31
as well as locally defined ones. At the top level, there is a true universal `cap` — the local `cap` of the global scope — which all other local `cap`s ultimately subsume:
@@ -78,7 +78,7 @@ def process(fs: FileSystem^): Unit =
The closure is declared pure (`() -> Unit`), meaning its local cap is the empty set. The capability `fs` cannot flow into an empty set, so the checker rejects this.
81
+
The closure is declared pure (`() -> Unit`), meaning its local `cap` is the empty set. The capability `fs` cannot flow into an empty set, so the checker rejects this.
localLogger // Type widens from Logger^{localLogger} to Logger^{fs}
91
91
```
92
92
93
-
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`.
93
+
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`.
94
94
95
-
##Existential Binding in Function Types
95
+
### Local Caps of Classes
96
96
97
-
So far we've discussed local `cap`s that follow the lexical nesting hierarchy. But `cap` can also appear in function parameter and result types, where special binding rules apply.
97
+
A class receives its own local `cap` for the scope of its body. This `cap` serves as a template for
98
+
a fresh `cap` that will be attached to each instance of the class. Inside the class body, references
99
+
to the class's `cap` are implicitly prefixed by the path `this`:
98
100
99
-
### Result Caps
101
+
```scala
102
+
classLogger(fs: FileSystem^):// local cap₁
103
+
// Logger has its own local cap₁, accessed as this.cap₁
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.
123
+
As explained in [Capture Checking of Classes](classes.md), the capture checker infers and verifies
124
+
constraints on the contents of a class's `cap` through its self-type, reporting any inconsistencies.
125
+
126
+
When creating an instance, the class's template `cap` is substituted with a fresh `cap` specific to
127
+
the new object:
108
128
109
-
In logic, the usual way to achieve this scoping is with an existential binder. We can express the type of `makeLogger` like this:
vallogger=Logger(fs) // Fresh logger.cap for this instance, capturing fs
132
+
logger
112
133
```
113
-
In words: `makeLogger` takes a parameter `fs` of type `Filesystem` capturing _some_ universal capability `cap₁` and returns a `Logger` capturing some other (possibly different) universal `cap₂`.
134
+
Note that the `cap` of attached to `logger` subcaptures the local `cap` of method `test` in accordance
135
+
to the rules outlined earlier.
136
+
137
+
Conceptually, a class' local `cap` behaves like an implicit [capture-set member](polymorphism.md#capability-members)
138
+
present in the class and all its supertypes:
114
139
115
-
We can also turn the existential in the function parameter to a universal "forall" in the function itself. In that alternative notation, the type of makeLogger would read like this:
There's a connection with [capture polymorphism](polymorphism.md) here. `cap`s in function parameters behave like additional capture parameters that can be instantiated at the call site to arbitrary capabilities.
120
146
121
-
### Why Result Caps Don't Subcapture Local Caps
147
+
##Parameter and Result Caps in Function Types
122
148
123
-
Result `cap`s do _not_ subsume the enclosing scope's local `cap`. Result `cap`s are bound at the function boundary, not within the function body:
149
+
So far we've discussed local `cap`s that follow the lexical nesting hierarchy. But `cap` can also appear in function parameter and result types, where special binding rules apply.
The return type `() -> File^` contains an existentially-bound result cap. If this result cap subcaptured `outer`'s local cap, then `localFile` could flow into it, and the local file would escape. The whole point of the existential is to describe what the _caller_ receives — it must not allow capabilities from the callee's scope to leak out.
132
-
133
-
In contrast, a local cap inside a function body _does_ subcapture the enclosing local cap:
159
+
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.
134
160
161
+
In logic, the usual way to achieve this scoping is with an existential binder. We can express the type of `makeLogger` like this:
135
162
```scala
136
-
defouter():Unit=
137
-
valf:File^= openFile() // This ^ is outer's local cap
138
-
valg: () =>Unit= () => f.read() // OK: closure's local cap subcaptures outer's local cap
In words: `makeLogger` takes a parameter `fs` of type `Filesystem` capturing _some_ universal capability `cap₁` and returns a `Logger` capturing some other (possibly different) universal `cap₂`.
140
166
141
-
Here the closure's local cap can absorb `f` because both are nested within `outer`.
167
+
We can also turn the existential in the function parameter to a universal "forall" in the function itself. In that alternative notation, the type of `makeLogger` would read like this:
There's a connection with [capture polymorphism](polymorphism.md) here. `cap`s in function parameters behave like additional capture parameters that can be instantiated at the call site to arbitrary capabilities.
142
172
143
173
### Expansion Rules for Function Types
144
174
@@ -182,6 +212,40 @@ To summarize:
182
212
183
213
Later sections on [capability classifiers](classifiers.md) will add a controlled mechanism that permits capabilities to escape their level for situations where this would be desirable.
184
214
215
+
### Parameter Caps and Local Caps
216
+
217
+
Inside the function body, parameter caps are at the **same level** as the function's local `cap`. This means the function's local `cap` can subsume capabilities from parameters:
218
+
219
+
```scala
220
+
defprocess(x: File^/* parameter {cap₁} */):Unit=/* local cap₂ */
221
+
valy:File^/*{cap₂}*/= x // OK: x's cap is at process's level, same as process's local cap
222
+
valf: () =>/*{cap₂}*/Unit= () => x.read() // OK: closure's local cap subsumes x
223
+
```
224
+
225
+
The parameter `x` has a capability at `process`'s level. The local `cap` of `process` (and any nested closures) can subsume it because they're at the same level or more deeply nested.
226
+
227
+
### Result Caps Don't Subsume Local Caps
228
+
229
+
Result `cap`s do _not_ subsume the enclosing scope's local `cap`. Result `cap`s are bound at the function boundary, not within the function body:
230
+
231
+
```scala
232
+
defouter(): () ->File^=
233
+
vallocalFile:File^= openFile()
234
+
() => localFile // Error!
235
+
```
236
+
237
+
The return type `() -> File^` contains an existentially-bound result `cap`. If this result `cap` subsumed `outer`'s local `cap`, then `localFile` could flow into it, and the local file would escape. The whole point of the existential is to describe what the _caller_ receives — it must not allow capabilities from the callee's scope to leak out.
238
+
239
+
In contrast, a local `cap` inside a function body _does_ subsume the enclosing local `cap`:
240
+
241
+
```scala
242
+
defouter():Unit=
243
+
valf:File^= openFile() // This ^ is outer's local cap
244
+
valg: () =>Unit= () => f.read() // OK: closure's local cap subsumes outer's local cap
245
+
```
246
+
247
+
Here the closure's local `cap` can absorb `f` because both are nested within `outer`.
248
+
185
249
## Comparison with Rust Lifetimes
186
250
187
251
Readers familiar with Rust may notice similarities to lifetime checking. Both systems prevent references from escaping their valid scope. In Rust, a reference type `&'a T` carries an explicit lifetime parameter `'a`. In Scala's capture checking, the lifetime is folded into the capability name itself: `T^{x}` says "a `T` capturing `x`," and `x`'s level implicitly determines how long this reference is valid. A capture set then acts as an upper bound on the lifetimes of all the capabilities it contains.
0 commit comments