Skip to content

Commit 747f2de

Browse files
committed
Use @untrackedCaptures instead of transparent
1 parent 74d6490 commit 747f2de

File tree

5 files changed

+30
-20
lines changed

5 files changed

+30
-20
lines changed

compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1039,7 +1039,7 @@ class CheckCaptures extends Recheck, SymTransformer:
10391039
recheck(tree.rhs, lhsType.widen)
10401040
lhsType match
10411041
case lhsType @ TermRef(qualType, _)
1042-
if (qualType ne NoPrefix) && !lhsType.symbol.is(Transparent) =>
1042+
if (qualType ne NoPrefix) && !lhsType.symbol.hasAnnotation(defn.UntrackedCapturesAnnot) =>
10431043
checkUpdate(qualType, tree.srcPos)(i"Cannot assign to field ${lhsType.name} of ${qualType.showRef}")
10441044
case _ =>
10451045
defn.UnitType

compiler/src/dotty/tools/dotc/cc/Mutability.scala

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,17 @@ object Mutability:
4646
end Exclusivity
4747

4848
extension (sym: Symbol)
49-
/** An update method is either a method marked with `update` or
50-
* a setter of a non-transparent var. `update` is implicit for `consume` methods
51-
* of Mutable classes.
49+
/** An update method is either a method marked with `update` or a setter
50+
* of a field of a Mutable class that's not annotated with @uncheckedCaptures.
51+
* `update` is implicit for `consume` methods of Mutable classes.
5252
*/
5353
def isUpdateMethod(using Context): Boolean =
5454
sym.isAllOf(Mutable | Method)
55-
&& (!sym.isSetter || sym.field.is(Transparent))
55+
&& (if sym.isSetter then
56+
sym.owner.derivesFrom(defn.Caps_Mutable)
57+
&& !sym.field.hasAnnotation(defn.UntrackedCapturesAnnot)
58+
else true
59+
)
5660

5761
/** A read-only method is a real method (not an accessor) in a type extending
5862
* Mutable that is not an update method. Included are also lazy vals in such types.
@@ -79,7 +83,7 @@ object Mutability:
7983
tp.derivesFrom(defn.Caps_Mutable)
8084
&& tp.membersBasedOnFlags(Mutable, EmptyFlags).exists: mbr =>
8185
if mbr.symbol.is(Method) then mbr.symbol.isUpdateMethod
82-
else !mbr.symbol.is(Transparent)
86+
else !mbr.symbol.hasAnnotation(defn.UntrackedCapturesAnnot)
8387

8488
/** OK, except if `tp` extends `Mutable` but `tp`'s capture set is non-exclusive */
8589
private def exclusivity(using Context): Exclusivity =

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -283,31 +283,34 @@ ro.set(22) // disallowed, since `ro` is read-only access
283283
```
284284

285285

286-
## Transparent Vars
286+
## Untracked Vars
287287

288288
Sometimes, disallowing assignments to mutable fields from normal methods is too restrictive. For instance:
289289
```scala
290+
import caps.unsafe.untrackedCaptures
291+
290292
class Cache[T](eval: () -> T):
291-
private transparent var x: T = compiletime.uninitialized
292-
private transparent var known = false
293+
@untrackedCaptures private var x: T = compiletime.uninitialized
294+
@untrackedCaptures private var known = false
293295
def force: T =
294296
if !known then
295297
x = eval()
296298
known = true
297299
x
298300
```
299-
Here, the mutable field `x` is used to store the result of a pure function `eval`. This is equivalent to just calling `eval()` directly but can be more efficient since the cached value is
300-
evaluated at most once. So from a semantic standpoint, it should not be necessary to make `force` an update method, even though it does assign to `x`.
301+
Note that, even though `Cache` has mutable variables, it is not declared as a `Mutable` class. In this case, the mutable field `x` is used to store the result of a pure function `eval` and field `known` reflects whether `eval` was called. This is equivalent to just calling `eval()` directly but can be more efficient since the cached value is evaluated at most once. So from a semantic standpoint, it should not be necessary to make `force` an update method, even though it does assign to `x`.
302+
303+
We can avoid the need for update methods by annotating mutable fields with `@untrackedCaptures`. Assignments to untracked mutable field are then not checked for read-only restrictions. The `@untrackedCaptures` annotation can be imported from the `scala.caps.unsafe` object. It is up to the developer
304+
to use `@untrackedCaptures` responsibly so that it does not hide visible side effects on mutable state.
301305

302-
We can avoid the need for update methods by declaring mutable fields `transparent`. Assignments to `transparent` mutable field are not checked for read-only restrictions. It is up to the developer
303-
to use `transparent` responsibly so that it does not hide visible side effects on mutable state.
306+
Note that at the moment an assignment to a variable is restricted _only_ if the variable is a field of a `Mutable` class. Fields of other classes and local variables are currently not checked. So the `Cache` class above would in fact
307+
currently compile without the addition of `@untrackedCaptures`.
304308

305-
Note that an assignment to a variable is restricted only if the variable is a field of a `Mutable` class. Fields of other classes and local variables are currently not checked.
309+
But is planned to tighten the rules in the future so that mutable fields that are not annotated with `@untrackedCaptures` can be declared only in classes extending `Mutable`. This means that all assignments to mutable fields would be checked with the read-only restriction, and `@untrackedCapture`s would become essential as an escape hatch.
306310

307-
It is planned to tighten the rules in the future so that non-transparent mutable fields can be declared only in classes extending `Mutable`. This means that all assignments to mutable fields would be checked with the read-only restriction, and `transparent` would become essential as
308-
an escape hatch.
311+
By contrast, it is not planned to check assignments to local mutable variables, which are not fields of some class. So `@untrackedCaptures` is disallowed for such local variables.
309312

310-
By contrast, it is not planned to check assignments to local mutable variables, which are not fields of some class. So `transparent` is disallowed for such local variables.
313+
The `untrackedCaptures` annotation can also be used in some other contexts unrelated to mutable variables. These are described in its [doc comment](https://www.scala-lang.org/api/current/scala/caps/unsafe$$untrackedCaptures.html).
311314

312315
## Read-Only Capsets
313316

tests/pos-custom-args/captures/lazyref-mutvar.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import compiletime.uninitialized
2+
import caps.unsafe.untrackedCaptures
3+
24
class LazyRef[T](val mkElem: () => T):
3-
transparent var elem: T = uninitialized
4-
transparent var evaluated = false
5+
@untrackedCaptures var elem: T = uninitialized
6+
@untrackedCaptures var evaluated = false
57
def get: T =
68
if !evaluated then
79
elem = mkElem()

tests/pos-custom-args/captures/transparent-mutvars.scala renamed to tests/pos-custom-args/captures/untracked-mutvars.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import caps.unsafe.untrackedCaptures
12
class Ref[T](init: T) extends caps.Mutable:
2-
transparent var fld: T = init
3+
@untrackedCaptures var fld: T = init
34
def hide(x: T) = this.fld = x // ok
45
update def hide2(x: T) = this.fld = x // ok
56

0 commit comments

Comments
 (0)