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
Skip to content

Commit 48d825e

Browse files
committed
Update scoped-caps.md
1 parent 29ecd8d commit 48d825e

File tree

1 file changed

+94
-30
lines changed

1 file changed

+94
-30
lines changed

docs/_docs/reference/experimental/capture-checking/scoped-caps.md

Lines changed: 94 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ We will discuss three distinct kinds of `cap` in this chapter:
1414

1515
**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.
1616

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

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

2121
So, when writing `T^` (shorthand for `T^{cap}`), `cap` is a way of saying "captures something" without
2222
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
2424
(and other kinds of `cap`) later when discussing [separation checking](separation-checking.md).
2525

2626
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.
2727

28-
## Subcapturing and Levels
28+
## Local Caps
2929

3030
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
3131
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 =
7878
val f: () -> Unit = () => fs.read() // Error: fs cannot flow into {}
7979
```
8080

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

8383
### Visibility and Widening
8484

@@ -90,55 +90,85 @@ def test(fs: FileSystem^): Logger^ =
9090
localLogger // Type widens from Logger^{localLogger} to Logger^{fs}
9191
```
9292

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`.
9494

95-
## Existential Binding in Function Types
95+
### Local Caps of Classes
9696

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`:
98100

99-
### Result Caps
101+
```scala
102+
class Logger(fs: FileSystem^): // local cap₁
103+
// Logger has its own local cap₁, accessed as this.cap₁
104+
val file: File^ = fs.open("log.txt") // File^{this.cap₁}
105+
def log(msg: String): Unit = file.write(msg)
106+
```
100107

101-
Consider this method:
108+
When a class inherits from other classes or traits, the `cap`s of all supertypes in the `extends`
109+
clause are essentially unified with the `cap` of the current class. This unification happens because
110+
all inherited members are accessed through `this`, and hence the local `cap`s will conform through
111+
subtyping with each other:
102112

103113
```scala
104-
def makeLogger(fs: FileSystem^): Logger^ = new Logger(fs)
114+
trait Super: // local cap₁
115+
val doSomething: () => Unit // () ->{cap₁} Unit
116+
117+
class Logger(fs: FileSystem^) extends Supper: // local cap₂
118+
val file: File^ = fs.open("log.txt") // File^{cap₂}
119+
def log(msg: String): Unit = file.write(msg)
120+
val doSomething = () => log("hello") // ok, since {file} <: {this.cap₂} =:= {this.cap₁}
105121
```
106122

107-
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:
108128

109-
In logic, the usual way to achieve this scoping is with an existential binder. We can express the type of `makeLogger` like this:
110129
```scala
111-
makeLogger: (fs: cap₁.FileSystem^{cap₁}): cap₂. Logger^{cap₂}
130+
def test(fs: FileSystem^) =
131+
val logger = Logger(fs) // Fresh logger.cap for this instance, capturing fs
132+
logger
112133
```
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:
114139

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:
116140
```scala
117-
makeLogger: cap₁.(fs: FileSystem^{cap₁}): cap₂. Logger^{cap₂}
141+
class Logger(fs: FileSystem^):
142+
type Cap^
143+
val file: File^{Cap} = ...
144+
// ...
118145
```
119-
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.
120146

121-
### Why Result Caps Don't Subcapture Local Caps
147+
## Parameter and Result Caps in Function Types
122148

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.
150+
151+
### Existential Binding
152+
153+
Consider this method:
124154

125155
```scala
126-
def outer(): () -> File^ =
127-
val localFile: File^ = openFile()
128-
() => localFile // Error!
156+
def makeLogger(fs: FileSystem^): Logger^ = new Logger(fs)
129157
```
130158

131-
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.
134160

161+
In logic, the usual way to achieve this scoping is with an existential binder. We can express the type of `makeLogger` like this:
135162
```scala
136-
def outer(): Unit =
137-
val f: File^ = openFile() // This ^ is outer's local cap
138-
val g: () => Unit = () => f.read() // OK: closure's local cap subcaptures outer's local cap
163+
makeLogger: (fs: cap₁.FileSystem^{cap₁}): cap₂. Logger^{cap₂}
139164
```
165+
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₂`.
140166

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:
168+
```scala
169+
makeLogger: cap₁.(fs: FileSystem^{cap₁}): cap₂. Logger^{cap₂}
170+
```
171+
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.
142172

143173
### Expansion Rules for Function Types
144174

@@ -182,6 +212,40 @@ To summarize:
182212

183213
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.
184214

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+
def process(x: File^/* parameter {cap₁} */): Unit = /* local cap₂ */
221+
val y: File^/*{cap₂}*/ = x // OK: x's cap is at process's level, same as process's local cap
222+
val f: () =>/*{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+
def outer(): () -> File^ =
233+
val localFile: 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+
def outer(): Unit =
243+
val f: File^ = openFile() // This ^ is outer's local cap
244+
val g: () => 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+
185249
## Comparison with Rust Lifetimes
186250

187251
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

Comments
 (0)