Skip to content

Conversation

@bracevac
Copy link
Contributor

@bracevac bracevac commented Dec 5, 2025

Continuation of #24626

[skip ci]

@bracevac bracevac force-pushed the ob-cc-docs2 branch 2 times, most recently from 8f6cf6a to d73dd15 Compare December 9, 2025 12:24
@natsukagami natsukagami self-requested a review December 10, 2025 16:54
@bracevac bracevac force-pushed the ob-cc-docs2 branch 2 times, most recently from b3da99d to e8699e3 Compare December 15, 2025 14:07
@bracevac bracevac changed the title Update CC Docs 2 Update CC Docs Until scoped-caps.md Dec 17, 2025
@bracevac bracevac marked this pull request as ready for review December 17, 2025 17:46
signature expresses that the cumulative capture set of the input futures is preserved in the
resulting stream:
```scala
def collect[T, C^](fs: Set[Future[T]]^{C})(using Async^): Stream[Future[T]^{C}] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def collect[T, C^](fs: Set[Future[T]]^{C})(using Async^): Stream[Future[T]^{C}] =
def collect[T, C^](fs: Set[Future[T]^{C}])(using Async^): Stream[Future[T]^{C}] =

Comment on lines +115 to +125
#### Tracking the evolution of mutable objects
A common use case for explicit capture parameters is when a mutable object’s reachable capabilities
_grow_ due to mutation. For example, concatenating effectful iterators:
```scala
class ConcatIterator[A, C^](var iterators: mutable.List[IterableOnce[A]^{C}]):
def concat(it: IterableOnce[A]^): ConcatIterator[A, {C, it}]^{this, it} =
iterators ++= it // ^
this // track contents of `it` in the result
```
In such a scenario, we also should ensure that any pre-existing alias of a `ConcatIterator` object should become
inaccessible after invoking its `concat` method. This is achieved with [mutation and separation tracking](separation-checking.md) which are currently in development.
In such cases, the type system must ensure that any existing aliases of the iterator become invalid
after mutation. This is handled by [mutation tracking](mutability.md) and [separation tracking](separation-checking.md), which are currently under development.
Copy link
Contributor

@natsukagami natsukagami Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a poor example, as it instantly leads the reader into writing this (very buggy, doesn't guarantee anything until separation checking) code.

The problem with mutable collections with non-type-parameter element types is that you cannot use implicit capture polymorphism: they get instantiated with the elements at the construction of the collection.

class IteratorList(private var iterators: mutable.List[Iterator[A]^]):
  //                                            ^ this cap will be instantiated with whatever is in the initial list
  // ...
  def +=(it: Iterator[A]^) = iterators += it
  
val xs = IteratorList(mutable.List.empty) // forcefully instantiated to {}

(also we don't have a name to refer to the capture set of the elements, when we write the += method)

Therefore, we need a capture set variable, to allow the user/inference to specify a capture set suitable for the whole usage of the collection.

class IteratorList[C^](private var iterators: mutable.List[Iterator[A]^{C}]):
  // ...
  def +=(it: Iterator[A]^{C}) = iterators += it
  
val xs = IteratorList(mutable.List.empty)
xs += Iterator(async)
xs += Iterator(io)
// inference will (probably) find out that xs: IteratorList[{async, io}] 

Note that this capture set will not change, it's part of the type: it means you have to be able to name all the captures of all the elements at the point of creating the collection. If you want a growing capture set, it's not sound until separation checking.

- **Methods** (but not accessors or constructors)

Local values like `f1`, `f2`, `ref`, etc., don't define their own levels. They inherit the level of their enclosing method or class. For example, this means:
- `f1` is at `Outer`'s level, i.e., `f` subcaptures local `cap₁`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `f1` is at `Outer`'s level, i.e., `f` subcaptures local `cap₁`.
- `f1` is at `Outer`'s level, i.e., `f1` subcaptures local `cap₁`.

...
```

Each capability has a _level_ corresponding to the local `cap` of its defining scope. 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 (which would mean a capability lives longer than its lexical lifetime). The compiler computes a capability's level by walking up the ownership chain until reaching a symbol that represents a level boundary. Level boundaries are:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think flow is a vague word. Maybe "The level determines where a capability can be part of: it can be a part of caps at the same level or more deeply nested..." or something like "assigned to"?

Update: I think we used in a lot of places, so maybe it's fine?

Local values like `f1`, `f2`, `ref`, etc., don't define their own levels. They inherit the level of their enclosing method or class. For example, this means:
- `f1` is at `Outer`'s level, i.e., `f` subcaptures local `cap₁`.
- `f2` and `ref` are both at `method`'s level, i.e., both subcapture local `cap₂`.
- By lexical lifetime, `{cap₂} <: {cap₃}` holds, but it does **not** hold that `{cap₃} <: {cap₂}`. Hence,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- By lexical lifetime, `{cap₂} <: {cap₃}` holds, but it does **not** hold that `{cap₃} <: {cap₂}`. Hence,
- By lexical nesting, `{cap₂} <: {cap₃}` holds, but it does **not** hold that `{cap₃} <: {cap₂}`. Hence,

Lifetimes? Are we Rust? 🦀

```

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should say something about the withFile[T] pattern here?


**Examples:**

- `A => B` is an alias type that expands to `A ->{cap} B`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should A => B => C be extended to? I think it's quite unclear here


## Comparison with Rust Lifetimes

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
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 of a reference then acts as an upper bound of the reference itself: it only lives as long as all the capabilities it contains are visible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants