Skip to content

Commit 186df5e

Browse files
committed
Update CC language reference with lazy vals
1 parent 36b9ced commit 186df5e

File tree

2 files changed

+118
-0
lines changed

2 files changed

+118
-0
lines changed

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,63 @@ def f(x: ->{c} Int): Int
172172
```
173173
Here, the actual argument to `f` is allowed to use the `c` capability but no others.
174174

175+
## Lazy Vals
176+
177+
Lazy vals receive special treatment under capture checking, similar to parameterless methods. A lazy val has two distinct capture sets:
178+
179+
1. **The initializer's capture set**: What capabilities the initialization code uses
180+
2. **The result's capture set**: What capabilities the lazy val's value captures
181+
182+
### Initializer Captures
183+
184+
When a lazy val is declared, its initializer is checked in its own environment (like a method body). The initializer can capture capabilities, and these are tracked separately:
185+
186+
```scala
187+
def example(console: Console^) =
188+
lazy val x: () -> String =
189+
console.println("Computing x") // console captured by initializer
190+
() => "Hello, World!" // result doesn't capture console
191+
192+
val fun: () ->{console} String = () => x() // ok: accessing x uses console
193+
val fun2: () -> String = () => x() // error: x captures console
194+
```
195+
196+
Here, the initializer of `x` uses `console` (to print a message), so accessing `x` for the first time will use the `console` capability. However, the **result** of `x` is a pure function `() -> String` that doesn't capture any capabilities.
197+
198+
The type system tracks that accessing `x` requires the `console` capability, even though the resulting value doesn't. This is reflected in the function types: `fun` must declare `{console}` in its capture set because it accesses `x`.
199+
200+
### Lazy Val Member Selection
201+
202+
When accessing a lazy val member through a qualifier, the qualifier is charged to the current capture set, just like calling a parameterless method:
203+
204+
```scala
205+
trait Container:
206+
lazy val lazyMember: String
207+
208+
def client(c: Container^): Unit =
209+
val f1: () -> String = () => c.lazyMember // error
210+
val f2: () ->{c} String = () => c.lazyMember // ok
211+
```
212+
213+
Accessing `c.lazyMember` can trigger initialization, which may use capabilities from `c`. Therefore, the capture set must include `c`.
214+
215+
### Equivalence with Methods
216+
217+
For capture checking purposes, lazy vals behave identically to parameterless methods:
218+
219+
```scala
220+
trait T:
221+
def methodMember(): String
222+
lazy val lazyMember: String
223+
224+
def test(t: T^): Unit =
225+
// Both require {t} in the capture set
226+
val m: () ->{t} String = () => t.methodMember()
227+
val l: () ->{t} String = () => t.lazyMember
228+
```
229+
230+
This equivalence reflects that both can trigger computation using capabilities from their enclosing object.
231+
175232
## Subtyping and Subcapturing
176233

177234
Capturing influences subtyping. As usual we write `T₁ <: T₂` to express that the type

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,67 @@ val b1: Ref^{a.rd} = a
215215
val b2: Ref^{cap.rd} = a
216216
```
217217

218+
## Lazy Vals and Read-Only Restrictions
219+
220+
Lazy val initializers are subject to read-only restrictions similar to those for normal methods in `Mutable` classes. Specifically, a lazy val initializer cannot call update methods on non-local exclusive capabilities—capabilities defined outside the lazy val's scope.
221+
222+
### Read-Only Initialization
223+
224+
When a lazy val is declared, its initializer may use capabilities from the surrounding environment. However, it can only access non-local exclusive capabilities in a read-only fashion:
225+
226+
```scala
227+
def example(r: Ref^) =
228+
lazy val badInit: () ->{r.rd} Int =
229+
r.set(100) // error: cannot call update method
230+
() => r.get()
231+
232+
lazy val goodInit: () ->{r.rd} Int =
233+
val i = r.get() // ok: read-only access
234+
() => r.get() + i
235+
```
236+
237+
The initializer of `badInit` attempts to call `r.set(100)`, an update method on the non-local exclusive capability `r`. This is rejected because initializers should not perform mutations on external state.
238+
239+
### Local Capabilities
240+
241+
The restriction applies only to **non-local** capabilities. A lazy val can freely call update methods on capabilities it creates locally within its initializer:
242+
243+
```scala
244+
def example =
245+
lazy val localMutation: () => Int =
246+
val local: Ref^ = Ref(10) // created in initializer
247+
local.set(100) // ok: local capability
248+
() => local.get()
249+
```
250+
251+
Here, `local` is created within the lazy val's initializer, so it counts as a local capability. The initializer can call update methods on it.
252+
253+
### Closures vs. Initializer Code
254+
255+
The read-only restriction applies to code that runs during initialization, not to closures created by the initializer:
256+
257+
```scala
258+
def example(r: Ref^) =
259+
lazy val closure: () ->{r} Int =
260+
val i = r.get()
261+
() => r.set(i); r.get() // ok: closure runs later, not during init
262+
```
263+
264+
Closures (like `() => ...`) run at a later time than the initializer, so their update method calls are permitted.
265+
266+
### Rationale
267+
268+
Since lazy val initialization happens unpredictably (on first access), allowing initializers to mutate non-local state could cause subtle bugs. The read-only restriction ensures:
269+
270+
1. **Predictable initialization**: Accessing a lazy val for the first time doesn't have surprising side effects on external mutable state
271+
2. **Thread safety**: Lazy vals can be safely used in concurrent contexts without worrying about initialization side effects
272+
3. **Referential transparency**: The timing of first access doesn't affect program correctness
273+
274+
This makes lazy vals behave like normal methods in `Mutable` classes: they can read from their environment but cannot update it unless explicitly marked.
275+
There's currently no `update` modifier for lazy vals, so their initialization is always read-only with respect to non-local capabilities. A future version of capture checking might
276+
support `update lazy val` if there are compelling use cases and there is sufficient
277+
community demand.
278+
218279
## Update Restrictions
219280

220281
If a capability `r` is a read-only access, then one cannot use `r` to call an update method of `r` or to assign to a field of `r`. E.g. `r.set(22)` and `r.current = 22` are both disallowed.

0 commit comments

Comments
 (0)