Skip to content

Commit a1b2ef6

Browse files
committed
Enforce no-exclusive capability restriction only for lazy vals in Mutable types
1 parent 0383ba4 commit a1b2ef6

File tree

5 files changed

+96
-38
lines changed

5 files changed

+96
-38
lines changed

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

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ object CheckCaptures:
8181
end Env
8282

8383
def definesEnv(sym: Symbol)(using Context): Boolean =
84-
sym.is(Method) || sym.isClass || sym.is(Lazy)
84+
sym.isOneOf(MethodOrLazy) || sym.isClass
8585

8686
/** Similar normal substParams, but this is an approximating type map that
8787
* maps parameters in contravariant capture sets to the empty set.
@@ -578,7 +578,7 @@ class CheckCaptures extends Recheck, SymTransformer:
578578
if !isOfNestedMethod(env) then
579579
val nextEnv = nextEnvToCharge(env)
580580
if nextEnv != null && !nextEnv.owner.isStaticOwner then
581-
if env.owner.isReadOnlyMethod && nextEnv.owner != env.owner then
581+
if env.owner.isReadOnlyMethodOrLazyVal && nextEnv.owner != env.owner then
582582
checkReadOnlyMethod(included, env)
583583
recur(included, nextEnv, env)
584584
// Under deferredReaches, don't propagate out of methods inside terms.
@@ -665,7 +665,7 @@ class CheckCaptures extends Recheck, SymTransformer:
665665
*/
666666
override def recheckIdent(tree: Ident, pt: Type)(using Context): Type =
667667
val sym = tree.symbol
668-
if sym.is(Method) || sym.is(Lazy) then
668+
if sym.isOneOf(MethodOrLazy) then
669669
// If ident refers to a parameterless method or lazy val, charge its cv to the environment.
670670
// Lazy vals are like parameterless methods: accessing them may trigger initialization
671671
// that uses captured references.
@@ -690,7 +690,7 @@ class CheckCaptures extends Recheck, SymTransformer:
690690
case pt: PathSelectionProto if ref.isTracked =>
691691
// if `ref` is not tracked then the selection could not give anything new
692692
// class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters.
693-
if pt.select.symbol.isReadOnlyMethod then
693+
if pt.select.symbol.isReadOnlyMethodOrLazyVal then
694694
markFree(ref.readOnly, tree)
695695
else
696696
val sel = ref.select(pt.select.symbol).asInstanceOf[TermRef]
@@ -711,7 +711,7 @@ class CheckCaptures extends Recheck, SymTransformer:
711711
override def selectionProto(tree: Select, pt: Type)(using Context): Type =
712712
val sym = tree.symbol
713713
if !sym.isOneOf(MethodOrLazyOrMutable) && !sym.isStatic
714-
|| sym.isReadOnlyMethod
714+
|| sym.isReadOnlyMethodOrLazyVal
715715
then PathSelectionProto(tree, pt)
716716
else super.selectionProto(tree, pt)
717717

@@ -746,19 +746,6 @@ class CheckCaptures extends Recheck, SymTransformer:
746746
checkUpdate(qualType, tree.srcPos):
747747
i"Cannot call update ${tree.symbol} of ${qualType.showRef}"
748748

749-
// Additionally, lazy val initializers should not call update methods on
750-
// non-local exclusive capabilities (capabilities defined outside the lazy val)
751-
if curEnv.owner.is(Lazy) then
752-
qualType.captureSet.elems.foreach: elem =>
753-
if elem.isExclusive then
754-
val owner = elem.pathOwner
755-
// Allow update methods on local capabilities (owned by the lazy val or nested within it)
756-
if !owner.isContainedIn(curEnv.owner) && owner != curEnv.owner then
757-
report.error(
758-
em"""Lazy val initializer calls update method on non-local exclusive capability $elem;
759-
|lazy val initializers should only access non-local capabilities in a read-only fashion.""",
760-
tree.srcPos)
761-
762749
val origSelType = recheckSelection(tree, qualType, name, disambiguate)
763750
val selType = mapResultRoots(origSelType, tree.symbol)
764751
val selWiden = selType.widen
@@ -1161,12 +1148,10 @@ class CheckCaptures extends Recheck, SymTransformer:
11611148
interpolateIfInferred(tree.tpt, sym)
11621149

11631150
def declaredCaptures = tree.tpt.nuType.captureSet
1151+
curEnv = savedEnv
1152+
11641153
if runInConstructor && savedEnv.owner.isClass then
1165-
curEnv = savedEnv
11661154
markFree(declaredCaptures, tree, addUseInfo = false)
1167-
else if sym.is(Lazy) then
1168-
// Restore environment after checking lazy val
1169-
curEnv = savedEnv
11701155

11711156
if sym.owner.isStaticOwner && !declaredCaptures.elems.isEmpty && sym != defn.captureRoot then
11721157
def where =

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,21 @@ object Mutability:
5353
sym.isAllOf(Mutable | Method)
5454
&& (!sym.isSetter || sym.field.is(Transparent))
5555

56-
/** A read-only methid is a real method (not an accessor) in a type extending
57-
* Mutable that is not an update method.
56+
/** A read-only method is a real method (not an accessor) in a type extending
57+
* Mutable that is not an update method. Included are also lazy vals in such types.
5858
*/
59-
def isReadOnlyMethod(using Context): Boolean =
60-
sym.is(Method, butNot = Mutable | Accessor) && sym.owner.derivesFrom(defn.Caps_Mutable)
59+
def isReadOnlyMethodOrLazyVal(using Context): Boolean =
60+
sym.isOneOf(MethodOrLazy, butNot = Mutable | Accessor)
61+
&& sym.owner.derivesFrom(defn.Caps_Mutable)
6162

6263
private def inExclusivePartOf(cls: Symbol)(using Context): Exclusivity =
6364
import Exclusivity.*
64-
val encl = sym.enclosingMethodOrClass.skipConstructor
6565
if sym == cls then OK // we are directly in `cls` or in one of its constructors
66-
else if encl.owner == cls then
67-
if encl.isUpdateMethod then OK
68-
else NotInUpdateMethod(encl, cls)
69-
else if encl.isStatic then OutsideClass(cls)
70-
else encl.owner.inExclusivePartOf(cls)
66+
else if sym.owner == cls then
67+
if sym.isUpdateMethod || sym.isConstructor then OK
68+
else NotInUpdateMethod(sym, cls)
69+
else if sym.isStatic then OutsideClass(cls)
70+
else sym.owner.inExclusivePartOf(cls)
7171

7272
extension (tp: Type)
7373
/** Is this a type extending `Mutable` that has non-private update methods

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ val b2: Ref^{cap.rd} = a
217217

218218
## Lazy Vals and Read-Only Restrictions
219219

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.
220+
Lazy val initializers in `Mutable` classes are subject to read-only restrictions similar to those for normal methods. Specifically, a lazy val initializer in a `Mutable` class cannot call update methods or refer to non-local exclusive capabilities—capabilities defined outside the lazy val's scope.
221221

222222
### Read-Only Initialization
223223

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
-- Error: tests/neg-custom-args/captures/lazyvals-sep.scala:30:6 -------------------------------------------------------
2+
30 | r.set(current * 100) // error - exclusive access in initializer
3+
| ^^^^^
4+
| Cannot call update method set of TestClass.this.r
5+
| since the access is in lazy value lazyVal2, which is not an update method.
6+
-- Error: tests/neg-custom-args/captures/lazyvals-sep.scala:36:12 ------------------------------------------------------
7+
36 | () => r.set(current); r.get() // error, even though exclusive access is in closure, not initializer
8+
| ^^^^^
9+
| Cannot call update method set of TestClass.this.r
10+
| since the access is in lazy value lazyVal3, which is not an update method.
11+
-- Error: tests/neg-custom-args/captures/lazyvals-sep.scala:42:10 ------------------------------------------------------
12+
42 | r.set(100) // error
13+
| ^^^^^
14+
| Cannot call update method set of TestClass.this.r
15+
| since the access is in lazy value lazyVal4, which is not an update method.
16+
-- Error: tests/neg-custom-args/captures/lazyvals-sep.scala:61:6 -------------------------------------------------------
17+
61 | r.set(200) // error
18+
| ^^^^^
19+
| Cannot call update method set of TestClass.this.r
20+
| since the access is in lazy value lazyVal6, which is not an update method.
21+
-- Error: tests/neg-custom-args/captures/lazyvals-sep.scala:71:6 -------------------------------------------------------
22+
71 | r.set(200) // error
23+
| ^^^^^
24+
| Cannot call update method set of TestClass.this.r
25+
| since the access is in lazy value lazyVal8, which is not an update method.
26+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyvals-sep.scala:72:12 ---------------------------------
27+
72 | Wrapper(r) // error
28+
| ^
29+
|Found: Ref^{TestClass.this.r.rd}
30+
|Required: Ref^
31+
|
32+
|Note that capability TestClass.this.r.rd is not included in capture set {}.
33+
|
34+
|Note that {cap} is an exclusive capture set of the mutable type Ref^,
35+
|it cannot subsume a read-only capture set of the mutable type Ref^{TestClass.this.r.rd}.
36+
|
37+
|where: ^ and cap refer to a fresh root capability classified as Mutable created in lazy value lazyVal8 when checking argument to parameter ref of constructor Wrapper
38+
|
39+
| longer explanation available when compiling with `-explain`
40+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyvals-sep.scala:77:12 ---------------------------------
41+
77 | Wrapper(r) // error
42+
| ^
43+
|Found: Ref^{TestClass.this.r.rd}
44+
|Required: Ref^
45+
|
46+
|Note that capability TestClass.this.r.rd is not included in capture set {}.
47+
|
48+
|Note that {cap} is an exclusive capture set of the mutable type Ref^,
49+
|it cannot subsume a read-only capture set of the mutable type Ref^{TestClass.this.r.rd}.
50+
|
51+
|where: ^ and cap refer to a fresh root capability classified as Mutable created in lazy value lazyVal9 when checking argument to parameter ref of constructor Wrapper
52+
|
53+
| longer explanation available when compiling with `-explain`
54+
-- Error: tests/neg-custom-args/captures/lazyvals-sep.scala:82:8 -------------------------------------------------------
55+
82 | r.set(0) // error exclusive access in conditional
56+
| ^^^^^
57+
| Cannot call update method set of TestClass.this.r
58+
| since the access is in lazy value lazyVal10, which is not an update method.
59+
-- Error: tests/neg-custom-args/captures/lazyvals-sep.scala:90:8 -------------------------------------------------------
60+
90 | r.set(42) // error
61+
| ^^^^^
62+
| Cannot call update method set of TestClass.this.r
63+
| since the access is in lazy value lazyVal11, which is not an update method.

tests/neg-custom-args/captures/lazyvals-sep.scala

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class Wrapper(val ref: Ref^) extends Mutable:
1515
class WrapperRd(val ref: Ref^{cap.rd}):
1616
def compute(): Int = ref.get()
1717

18-
@main def run =
18+
class TestClass extends Mutable:
1919
val r: Ref^ = Ref(0)
2020
val r2: Ref^ = Ref(42)
2121

@@ -33,7 +33,7 @@ class WrapperRd(val ref: Ref^{cap.rd}):
3333
// Test case 3: Exclusive access in returned closure - should be OK
3434
lazy val lazyVal3: () ->{r} Int =
3535
val current = r2.get()
36-
() => r.set(current); r.get() // exclusive access in closure, not initializer
36+
() => r.set(current); r.get() // error, even though exclusive access is in closure, not initializer
3737

3838
// Test case 4: Multiple nested blocks with exclusive access - should error
3939
lazy val lazyVal4: () ->{r.rd} Int =
@@ -69,12 +69,12 @@ class WrapperRd(val ref: Ref^{cap.rd}):
6969
// Test case 8: Wrapper type - exclusive access in initializer - should error
7070
lazy val lazyVal8: Wrapper^{cap, r} =
7171
r.set(200) // error
72-
Wrapper(r)
72+
Wrapper(r) // error
7373

7474
// Test case 9: Wrapper type - non-exclusive access in initializer - should be ok
7575
lazy val lazyVal9: Wrapper^{cap, r} =
7676
r.get()
77-
Wrapper(r)
77+
Wrapper(r) // error
7878

7979
// Test case 10: Conditional with exclusive access - should error
8080
lazy val lazyVal10: WrapperRd^{r} =
@@ -110,4 +110,14 @@ class WrapperRd(val ref: Ref^{cap.rd}):
110110
lazy val lazyVal14: () => Ref^ =
111111
val r3: Ref^ = Ref(10)
112112
r3.set(100) // ok
113-
() => r3
113+
() => r3
114+
115+
def test =
116+
val r: Ref^ = Ref(0)
117+
val r2: Ref^ = Ref(42)
118+
119+
lazy val lazyVal2: () ->{r.rd} Int =
120+
val current = r2.get()
121+
r.set(current * 100) // ok, lazy vals outside Mutable can access exclusive capabilities
122+
() => r.get()
123+

0 commit comments

Comments
 (0)