Skip to content

Commit 5ccea40

Browse files
authored
Changes to Mutable (#24352)
1. Mutable and ExclsuiveCapability are not longer classifiers. This means Mutable classes can capture other capabilities. Read is still a classifier. 2. Update methods in nested classes can access exclusive capabilities external to enclosing classes.
2 parents 115125e + b03d608 commit 5ccea40

File tree

76 files changed

+1085
-307
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+1085
-307
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,10 +434,12 @@ object Capabilities:
434434

435435
/** An exclusive capability is a capability that derives
436436
* indirectly from a maximal capability without going through
437-
* a read-only capability first.
437+
* a read-only capability or a capability classified as SharedCapability first.
438438
*/
439439
final def isExclusive(using Context): Boolean =
440-
!isReadOnly && (isTerminalCapability || captureSetOfInfo.isExclusive)
440+
!isReadOnly
441+
&& !classifier.derivesFrom(defn.Caps_SharedCapability)
442+
&& (isTerminalCapability || captureSetOfInfo.isExclusive)
441443

442444
/** Similar to isExlusive, but also includes capabilties with capture
443445
* set variables in their info whose status is still open.

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import CaptureSet.VarState
1515
import Capabilities.*
1616
import StdNames.nme
1717
import config.Feature
18-
import dotty.tools.dotc.core.NameKinds.TryOwnerName
18+
import NameKinds.TryOwnerName
19+
import typer.ProtoTypes.WildcardSelectionProto
1920

2021
/** Attachment key for capturing type trees */
2122
private val Captures: Key[CaptureSet] = Key()
@@ -635,6 +636,10 @@ extension (tp: AnnotatedType)
635636
case ann: CaptureAnnotation => ann.boxed
636637
case _ => false
637638

639+
/** A prototype that indicates selection */
640+
class PathSelectionProto(val select: Select, val pt: Type) extends typer.ProtoTypes.WildcardSelectionProto:
641+
def selector(using Context): Symbol = select.symbol
642+
638643
/** Drop retains annotations in the inferred type if CC is not enabled
639644
* or transform them into RetainingTypes if CC is enabled.
640645
*/

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ sealed abstract class CaptureSet extends Showable:
6767
* - take mutability from the set's sources (for DerivedVars)
6868
* - compute mutability on demand based on mutability of elements (for Consts)
6969
*/
70-
def associateWithMutable()(using Context): Unit
70+
def associateWithMutable()(using Context): CaptureSet
7171

7272
/** Is this capture set constant (i.e. not an unsolved capture variable)?
7373
* Solved capture variables count as constant.
@@ -297,7 +297,7 @@ sealed abstract class CaptureSet extends Showable:
297297
/** The subcapturing test, using a given VarState */
298298
final def subCaptures(that: CaptureSet)(using ctx: Context, vs: VarState = VarState()): Boolean =
299299
TypeComparer.inNestedLevel:
300-
val this1 = this.adaptMutability(that)
300+
val this1 = if vs.isOpen then this.adaptMutability(that) else this
301301
if this1 == null then false
302302
else if this1 ne this then
303303
capt.println(i"WIDEN ro $this with ${this.mutability} <:< $that with ${that.mutability} to $this1")
@@ -566,9 +566,16 @@ object CaptureSet:
566566
val emptyRefs: Refs = SimpleIdentitySet.empty
567567

568568
/** The empty capture set `{}` */
569-
@sharable // sharable since the set is empty, so setMutable is a no-op
569+
@sharable // sharable since the set is empty, so mutability won't be set
570570
val empty: CaptureSet.Const = Const(emptyRefs)
571571

572+
/** The empty capture set `{}` of a Mutable type, with Reader status */
573+
@sharable // sharable since the set is empty, so mutability won't be set
574+
val emptyOfMutable: CaptureSet.Const =
575+
val cs = Const(emptyRefs)
576+
cs.mutability = Mutability.Reader
577+
cs
578+
572579
/** The universal capture set `{cap}` */
573580
def universal(using Context): Const =
574581
Const(SimpleIdentitySet(GlobalCap))
@@ -623,9 +630,11 @@ object CaptureSet:
623630

624631
private var isComplete = true
625632

626-
def associateWithMutable()(using Context): Unit =
627-
if !elems.isEmpty then
633+
def associateWithMutable()(using Context): CaptureSet =
634+
if elems.isEmpty then emptyOfMutable
635+
else
628636
isComplete = false // delay computation of Mutability status
637+
this
629638

630639
override def mutability(using Context): Mutability =
631640
if !isComplete then
@@ -718,8 +727,9 @@ object CaptureSet:
718727
*/
719728
var deps: Deps = SimpleIdentitySet.empty
720729

721-
def associateWithMutable()(using Context): Unit =
730+
def associateWithMutable()(using Context): CaptureSet =
722731
mutability = Mutable
732+
this
723733

724734
def isConst(using Context) = solved >= ccState.iterationId
725735
def isAlwaysEmpty(using Context) = isConst && elems.isEmpty
@@ -1036,7 +1046,7 @@ object CaptureSet:
10361046
addAsDependentTo(source)
10371047

10381048
/** Mutability is same as in source, except for readOnly */
1039-
override def associateWithMutable()(using Context): Unit = ()
1049+
override def associateWithMutable()(using Context): CaptureSet = this
10401050

10411051
override def mutableToReader(origin: CaptureSet)(using Context): Boolean =
10421052
super.mutableToReader(origin)

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,16 @@ object CapturingType:
3434
*/
3535
def apply(parent: Type, refs: CaptureSet, boxed: Boolean = false)(using Context): Type =
3636
assert(!boxed || !parent.derivesFrom(defn.Caps_CapSet))
37-
if refs.isAlwaysEmpty && !refs.keepAlways then parent
37+
if refs.isAlwaysEmpty && !refs.keepAlways && !parent.derivesFromCapability then
38+
parent
3839
else parent match
3940
case parent @ CapturingType(parent1, refs1) if boxed || !parent.isBoxed =>
4041
apply(parent1, refs ++ refs1, boxed)
4142
case _ =>
42-
if parent.derivesFromMutable then refs.associateWithMutable()
43-
refs.adoptClassifier(parent.inheritedClassifier)
44-
AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot))
43+
val refs1 =
44+
if parent.derivesFromMutable then refs.associateWithMutable() else refs
45+
refs1.adoptClassifier(parent.inheritedClassifier)
46+
AnnotatedType(parent, CaptureAnnotation(refs1, boxed)(defn.RetainsAnnot))
4547

4648
/** An extractor for CapturingTypes. Capturing types are recognized if
4749
* - the annotation is a CaptureAnnotation and we are not past CheckCapturingPhase, or

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

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,6 @@ object CheckCaptures:
103103
override def toString = "SubstParamsMap"
104104
end SubstParamsMap
105105

106-
/** A prototype that indicates selection with an immutable value */
107-
class PathSelectionProto(val select: Select, val pt: Type)(using Context) extends WildcardSelectionProto
108-
109106
/** Check that a @retains annotation only mentions references that can be tracked.
110107
* This check is performed at Typer.
111108
*/
@@ -573,12 +570,12 @@ class CheckCaptures extends Recheck, SymTransformer:
573570
// fresh capabilities. We do check that they hide no parameter reach caps in checkEscapingUses
574571
case _ =>
575572

576-
def checkReadOnlyMethod(included: CaptureSet, env: Env): Unit =
573+
def checkReadOnlyMethod(included: CaptureSet, meth: Symbol): Unit =
577574
included.checkAddedElems: elem =>
578575
if elem.isExclusive then
579576
report.error(
580-
em"""Read-only ${env.owner} accesses exclusive capability $elem;
581-
|${env.owner} should be declared an update method to allow this.""",
577+
em"""Read-only $meth accesses exclusive capability $elem;
578+
|$meth should be declared an update method to allow this.""",
582579
tree.srcPos)
583580

584581
def recur(cs: CaptureSet, env: Env, lastEnv: Env | Null): Unit =
@@ -598,8 +595,11 @@ class CheckCaptures extends Recheck, SymTransformer:
598595
if !isOfNestedMethod(env) then
599596
val nextEnv = nextEnvToCharge(env)
600597
if nextEnv != null && !nextEnv.owner.isStaticOwner then
601-
if env.owner.isReadOnlyMethodOrLazyVal && nextEnv.owner != env.owner then
602-
checkReadOnlyMethod(included, env)
598+
if nextEnv.owner != env.owner
599+
&& env.owner.isReadOnlyMember
600+
&& env.owner.owner.derivesFrom(defn.Caps_Mutable)
601+
then
602+
checkReadOnlyMethod(included, env.owner)
603603
recur(included, nextEnv, env)
604604
// Under deferredReaches, don't propagate out of methods inside terms.
605605
// The use set of these methods will be charged when that method is called.
@@ -702,32 +702,26 @@ class CheckCaptures extends Recheck, SymTransformer:
702702
* type `pt` to `ref`. Expand the marked tree accordingly to take account of
703703
* the added path. Example:
704704
* If we have `x` and the expected type says we select that with `.a.b`
705-
* where `b` is a read-only method, we charge `x.a.b.rd` for tree `x.a.b`
705+
* where `b` is a read-only method, we charge `x.a.rd` for tree `x.a.b`
706706
* instead of just charging `x`.
707707
*/
708-
private def markPathFree(ref: TermRef | ThisType, pt: Type, tree: Tree)(using Context): Unit =
709-
pt match
710-
case pt: PathSelectionProto if ref.isTracked =>
711-
// if `ref` is not tracked then the selection could not give anything new
712-
// class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters.
713-
if pt.select.symbol.isReadOnlyMethodOrLazyVal then
714-
markFree(ref.readOnly, tree)
715-
else
716-
val sel = ref.select(pt.select.symbol).asInstanceOf[TermRef]
717-
markPathFree(sel, pt.pt, pt.select)
718-
case _ =>
719-
markFree(ref.adjustReadOnly(pt), tree)
708+
private def markPathFree(ref: TermRef | ThisType, pt: Type, tree: Tree)(using Context): Unit = pt match
709+
case pt: PathSelectionProto
710+
if ref.isTracked && !pt.selector.isOneOf(MethodOrLazyOrMutable) =>
711+
// if `ref` is not tracked then the selection could not give anything new
712+
// class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters.
713+
val sel = ref.select(pt.selector).asInstanceOf[TermRef]
714+
markPathFree(sel, pt.pt, pt.select)
715+
case _ =>
716+
markFree(ref.adjustReadOnly(pt), tree)
720717

721718
/** The expected type for the qualifier of a selection. If the selection
722719
* could be part of a capability path or is a a read-only method, we return
723720
* a PathSelectionProto.
724721
*/
725722
override def selectionProto(tree: Select, pt: Type)(using Context): Type =
726-
val sym = tree.symbol
727-
if !sym.isOneOf(MethodOrLazyOrMutable) && !sym.isStatic
728-
|| sym.isReadOnlyMethodOrLazyVal
729-
then PathSelectionProto(tree, pt)
730-
else super.selectionProto(tree, pt)
723+
if tree.symbol.isStatic then super.selectionProto(tree, pt)
724+
else PathSelectionProto(tree, pt)
731725

732726
/** A specialized implementation of the selection rule.
733727
*
@@ -977,7 +971,7 @@ class CheckCaptures extends Recheck, SymTransformer:
977971
.getOrElse(cls, cls.info.decls.toList) // pick all symbols in class scope for other classes
978972
.flatMap(classifiersOfFreshInType)
979973
if cls.typeRef.isMutableType then
980-
fieldClassifiers = defn.Caps_Mutable :: fieldClassifiers
974+
fieldClassifiers = cls.classifier :: fieldClassifiers
981975
val parentClassifiers =
982976
cls.parentSyms.map(impliedClassifiers).filter(_.nonEmpty)
983977
if fieldClassifiers.isEmpty && parentClassifiers.isEmpty
@@ -1039,7 +1033,7 @@ class CheckCaptures extends Recheck, SymTransformer:
10391033
recheck(tree.rhs, lhsType.widen)
10401034
lhsType match
10411035
case lhsType @ TermRef(qualType, _)
1042-
if (qualType ne NoPrefix) && !lhsType.symbol.is(Transparent) =>
1036+
if (qualType ne NoPrefix) && !lhsType.symbol.hasAnnotation(defn.UntrackedCapturesAnnot) =>
10431037
checkUpdate(qualType, tree.srcPos)(i"Cannot assign to field ${lhsType.name} of ${qualType.showRef}")
10441038
case _ =>
10451039
defn.UnitType
@@ -1131,21 +1125,30 @@ class CheckCaptures extends Recheck, SymTransformer:
11311125
try
11321126
if sym.is(Module) then sym.info // Modules are checked by checking the module class
11331127
else
1134-
if sym.is(Mutable) && !sym.hasAnnotation(defn.UncheckedCapturesAnnot) then
1135-
val addendum = setup.capturedBy.get(sym) match
1136-
case Some(encl) =>
1137-
val enclStr =
1138-
if encl.isAnonymousFunction then
1139-
val location = setup.anonFunCallee.get(encl) match
1140-
case Some(meth) if meth.exists => i" argument in a call to $meth"
1141-
case _ => ""
1142-
s"an anonymous function$location"
1143-
else encl.show
1144-
i"\n\nNote that $sym does not count as local since it is captured by $enclStr"
1145-
case _ =>
1146-
""
1147-
disallowBadRootsIn(
1148-
tree.tpt.nuType, NoSymbol, i"Mutable $sym", "have type", addendum, sym.srcPos)
1128+
if sym.is(Mutable) then
1129+
if !sym.hasAnnotation(defn.UncheckedCapturesAnnot) then
1130+
val addendum = setup.capturedBy.get(sym) match
1131+
case Some(encl) =>
1132+
val enclStr =
1133+
if encl.isAnonymousFunction then
1134+
val location = setup.anonFunCallee.get(encl) match
1135+
case Some(meth) if meth.exists => i" argument in a call to $meth"
1136+
case _ => ""
1137+
s"an anonymous function$location"
1138+
else encl.show
1139+
i"\n\nNote that $sym does not count as local since it is captured by $enclStr"
1140+
case _ =>
1141+
""
1142+
disallowBadRootsIn(
1143+
tree.tpt.nuType, NoSymbol, i"Mutable $sym", "have type", addendum, sym.srcPos)
1144+
if ccConfig.noUnsafeMutableFields
1145+
&& sym.owner.isClass
1146+
&& !sym.owner.derivesFrom(defn.Caps_Mutable)
1147+
&& !sym.hasAnnotation(defn.UntrackedCapturesAnnot) then
1148+
report.error(
1149+
em"""Mutable $sym is defined in a class that does not extend `Mutable`.
1150+
|The variable needs to be annotated with `untrackedCaptures` to allow this.""",
1151+
tree.namePos)
11491152

11501153
// Lazy vals need their own environment to track captures from their RHS,
11511154
// similar to how methods work
@@ -1481,6 +1484,9 @@ class CheckCaptures extends Recheck, SymTransformer:
14811484
else
14821485
trace.force(i"rechecking $tree with pt = $pt", recheckr, show = true):
14831486
super.recheck(tree, pt)
1487+
catch case ex: AssertionError =>
1488+
println(i"error while rechecking $tree against $pt")
1489+
throw ex
14841490
finally curEnv = saved
14851491
if tree.isTerm && !pt.isBoxedCapturing && pt != LhsProto then
14861492
markFree(res.boxedCaptureSet, tree)
@@ -1793,7 +1799,10 @@ class CheckCaptures extends Recheck, SymTransformer:
17931799

17941800
if needsAdaptation && !insertBox then // we are unboxing
17951801
val criticalSet = // the set with which we unbox
1796-
if covariant then captures // covariant: we box with captures of actual type plus captures leaked by inner adapation
1802+
if covariant then
1803+
if expected.expectsReadOnly && actual.derivesFromMutable
1804+
then captures.readOnly
1805+
else captures
17971806
else expected.captureSet // contravarant: we unbox with captures of epected type
17981807
//debugShowEnvs()
17991808
markFree(criticalSet, tree)

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

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Capabilities.*
88
import util.SrcPos
99
import config.Printers.capt
1010
import ast.tpd.Tree
11+
import typer.ProtoTypes.LhsProto
1112

1213
/** Handling mutability and read-only access
1314
*/
@@ -46,25 +47,28 @@ object Mutability:
4647
end Exclusivity
4748

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

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.
58-
*/
59-
def isReadOnlyMethodOrLazyVal(using Context): Boolean =
60-
sym.isOneOf(MethodOrLazy, butNot = Mutable | Accessor)
61-
&& sym.owner.derivesFrom(defn.Caps_Mutable)
62+
/** A read-only member is a lazy val or a method that is not an update method. */
63+
def isReadOnlyMember(using Context): Boolean =
64+
sym.isOneOf(MethodOrLazy) && !sym.isUpdateMethod
6265

6366
private def inExclusivePartOf(cls: Symbol)(using Context): Exclusivity =
6467
import Exclusivity.*
6568
if sym == cls then OK // we are directly in `cls` or in one of its constructors
69+
else if sym.isUpdateMethod then OK
6670
else if sym.owner == cls then
67-
if sym.isUpdateMethod || sym.isConstructor then OK
71+
if sym.isConstructor then OK
6872
else NotInUpdateMethod(sym, cls)
6973
else if sym.isStatic then OutsideClass(cls)
7074
else sym.owner.inExclusivePartOf(cls)
@@ -77,7 +81,7 @@ object Mutability:
7781
tp.derivesFrom(defn.Caps_Mutable)
7882
&& tp.membersBasedOnFlags(Mutable, EmptyFlags).exists: mbr =>
7983
if mbr.symbol.is(Method) then mbr.symbol.isUpdateMethod
80-
else !mbr.symbol.is(Transparent)
84+
else !mbr.symbol.hasAnnotation(defn.UntrackedCapturesAnnot)
8185

8286
/** OK, except if `tp` extends `Mutable` but `tp`'s capture set is non-exclusive */
8387
private def exclusivity(using Context): Exclusivity =
@@ -98,21 +102,30 @@ object Mutability:
98102
case _ =>
99103
tp.exclusivity
100104

105+
def expectsReadOnly(using Context): Boolean = tp match
106+
case tp: PathSelectionProto =>
107+
tp.selector.isReadOnlyMember || tp.selector.isMutableVar && tp.pt != LhsProto
108+
case _ =>
109+
tp.isValueType
110+
&& (!tp.isMutableType || tp.captureSet.mutability == CaptureSet.Mutability.Reader)
111+
101112
extension (cs: CaptureSet)
102113
private def exclusivity(tp: Type)(using Context): Exclusivity =
103114
if cs.isExclusive then Exclusivity.OK else Exclusivity.ReadOnly(tp)
104115

105116
extension (ref: TermRef | ThisType)
106117
/** Map `ref` to `ref.readOnly` if its type extends Mutble, and one of the
107-
* following is true: it appears in a non-exclusive context, or the expected
108-
* type is a value type that is not a mutable type.
118+
* following is true:
119+
* - it appears in a non-exclusive context,
120+
* - the expected type is a value type that is not a mutable type,
121+
* - the expected type is a read-only selection
109122
*/
110-
def adjustReadOnly(pt: Type)(using Context): Capability =
123+
def adjustReadOnly(pt: Type)(using Context): Capability = {
111124
if ref.derivesFromMutable
112-
&& (pt.isValueType && !pt.isMutableType
113-
|| ref.exclusivityInContext != Exclusivity.OK)
125+
&& (pt.expectsReadOnly || ref.exclusivityInContext != Exclusivity.OK)
114126
then ref.readOnly
115127
else ref
128+
}.showing(i"Adjust RO $ref vs $pt = $result", capt)
116129

117130
/** Check that we can call an update method of `qualType` or perform an assignment
118131
* of a field of `qualType`.
@@ -142,7 +155,6 @@ object Mutability:
142155
&& expected.isValueType
143156
&& (!expected.derivesFromMutable || expected.captureSet.isAlwaysReadOnly)
144157
&& !expected.isSingleton
145-
&& actual.isBoxedCapturing == expected.isBoxedCapturing
146158
then refs.readOnly
147159
else refs
148160
actual.derivedCapturingType(parent1, refs1)

0 commit comments

Comments
 (0)