Skip to content

Commit 38bb363

Browse files
authored
Fixes for mutability (#24205)
Whether something is mutable or not depends on the context: - this of a Mutable type is exclusive in update methods, read-only elsewhere - a selection from a mutable type with read-only capture set is again read-only
2 parents 9ec1f84 + 1f7b608 commit 38bb363

File tree

24 files changed

+1054
-249
lines changed

24 files changed

+1054
-249
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ object desugar {
226226
paramss = (setterParam :: Nil) :: Nil,
227227
tpt = TypeTree(defn.UnitType),
228228
rhs = setterRhs
229-
).withMods((vdef.mods | Accessor) &~ (CaseAccessor | GivenOrImplicit | Lazy))
229+
).withMods((vdef.mods | Accessor) &~ (CaseAccessor | GivenOrImplicit | Lazy | Transparent))
230230
.dropEndMarker() // the end marker should only appear on the getter definition
231231
Thicket(vdef1, setter)
232232
else vdef1

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

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,11 +352,14 @@ extension (tp: Type)
352352
case _ =>
353353
false
354354

355-
/** Is this a type extending `Mutable` that has update methods? */
355+
/** Is this a type extending `Mutable` that has non-private update methods
356+
* or mutable fields?
357+
*/
356358
def isMutableType(using Context): Boolean =
357359
tp.derivesFrom(defn.Caps_Mutable)
358-
&& tp.membersBasedOnFlags(Mutable | Method, EmptyFlags)
359-
.exists(_.hasAltWith(_.symbol.isUpdateMethod))
360+
&& tp.membersBasedOnFlags(Mutable, EmptyFlags).exists: mbr =>
361+
if mbr.symbol.is(Method) then mbr.symbol.isUpdateMethod
362+
else !mbr.symbol.is(Transparent)
360363

361364
/** Is this a reference to caps.cap? Note this is _not_ the GlobalCap capability. */
362365
def isCapRef(using Context): Boolean = tp match
@@ -496,6 +499,23 @@ extension (tp: Type)
496499
def classifier(using Context): ClassSymbol =
497500
tp.classSymbols.map(_.classifier).foldLeft(defn.AnyClass)(leastClassifier)
498501

502+
def exclusivity(using Context): Exclusivity =
503+
if tp.derivesFrom(defn.Caps_Mutable) then
504+
tp match
505+
case tp: Capability if tp.isExclusive => Exclusivity.OK
506+
case _ => tp.captureSet.exclusivity(tp)
507+
else Exclusivity.OK
508+
509+
def exclusivityInContext(using Context): Exclusivity = tp match
510+
case tp: ThisType =>
511+
if tp.derivesFrom(defn.Caps_Mutable)
512+
then ctx.owner.inExclusivePartOf(tp.cls)
513+
else Exclusivity.OK
514+
case tp @ TermRef(prefix: Capability, _) =>
515+
prefix.exclusivityInContext.andAlso(tp.exclusivity)
516+
case _ =>
517+
tp.exclusivity
518+
499519
extension (tp: MethodType)
500520
/** A method marks an existential scope unless it is the prefix of a curried method */
501521
def marksExistentialScope(using Context): Boolean =
@@ -629,8 +649,22 @@ extension (sym: Symbol)
629649
sym.hasAnnotation(defn.ConsumeAnnot)
630650
|| sym.info.hasAnnotation(defn.ConsumeAnnot)
631651

652+
/** An update method is either a method marked with `update` or
653+
* a setter of a non-transparent var.
654+
*/
632655
def isUpdateMethod(using Context): Boolean =
633-
sym.isAllOf(Mutable | Method, butNot = Accessor)
656+
sym.isAllOf(Mutable | Method)
657+
&& (!sym.isSetter || sym.field.is(Transparent))
658+
659+
def inExclusivePartOf(cls: Symbol)(using Context): Exclusivity =
660+
import Exclusivity.*
661+
val encl = sym.enclosingMethodOrClass.skipConstructor
662+
if sym == cls then OK // we are directly in `cls` or in one of its constructors
663+
else if encl.owner == cls then
664+
if encl.isUpdateMethod then OK
665+
else NotInUpdateMethod(encl, cls)
666+
else if encl.isStatic then OutsideClass(cls)
667+
else encl.owner.inExclusivePartOf(cls)
634668

635669
def isReadOnlyMethod(using Context): Boolean =
636670
sym.is(Method, butNot = Mutable | Accessor) && sym.owner.derivesFrom(defn.Caps_Mutable)
@@ -769,3 +803,35 @@ abstract class DeepTypeAccumulator[T](using Context) extends TypeAccumulator[T]:
769803
foldOver(acc, t)
770804
end DeepTypeAccumulator
771805

806+
/** Either OK, or a reason why capture set cannot be exclusive */
807+
enum Exclusivity:
808+
case OK
809+
810+
/** Enclosing symbol `sym` is a method of class `cls`, but not an update method */
811+
case NotInUpdateMethod(sym: Symbol, cls: Symbol)
812+
813+
/** Access to `this` from outside its class (not sure this can happen) */
814+
case OutsideClass(cls: Symbol)
815+
816+
/** A prefix type `tp` has a read-only capture set */
817+
case ReadOnly(tp: Type)
818+
819+
def isOK: Boolean = this == OK
820+
821+
def andAlso(that: Exclusivity): Exclusivity =
822+
if this == OK then that else this
823+
824+
/** A textual description why `qualType` is not exclusive */
825+
def description(qualType: Type)(using Context): String = this.runtimeChecked match
826+
case Exclusivity.ReadOnly(tp) =>
827+
if qualType eq tp then
828+
i"its capture set ${qualType.captureSet} is read-only"
829+
else
830+
i"the capture set ${tp.captureSet} of its prefix $tp is read-only"
831+
case Exclusivity.NotInUpdateMethod(sym: Symbol, cls: Symbol) =>
832+
i"the access is in $sym, which is not an update method"
833+
case Exclusivity.OutsideClass(cls: Symbol) =>
834+
i"the access from is from ouside $cls"
835+
836+
end Exclusivity
837+

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,15 @@ sealed abstract class CaptureSet extends Showable:
6060
def mutability_=(x: Mutability): Unit =
6161
myMut = x
6262

63-
/** Mark this capture set as belonging to a Mutable type. */
64-
def setMutable()(using Context): Unit
63+
/** Mark this capture set as belonging to a Mutable type. Called when a new
64+
* CapturingType is formed. This is different from the setter `mutability_=`
65+
* in that it be defined with different behaviors:
66+
*
67+
* - set mutability to Mutable (for normal Vars)
68+
* - take mutability from the set's sources (for DerivedVars)
69+
* - compute mutability on demand based on mutability of elements (for Consts)
70+
*/
71+
def associateWithMutable()(using Context): Unit
6572

6673
/** Is this capture set constant (i.e. not an unsolved capture variable)?
6774
* Solved capture variables count as constant.
@@ -145,6 +152,9 @@ sealed abstract class CaptureSet extends Showable:
145152
final def isExclusive(using Context): Boolean =
146153
elems.exists(_.isExclusive)
147154

155+
def exclusivity(tp: Type)(using Context): Exclusivity =
156+
if isExclusive then Exclusivity.OK else Exclusivity.ReadOnly(tp)
157+
148158
/** Similar to isExlusive, but also includes capture set variables
149159
* with unknown status.
150160
*/
@@ -610,7 +620,7 @@ object CaptureSet:
610620

611621
private var isComplete = true
612622

613-
def setMutable()(using Context): Unit =
623+
def associateWithMutable()(using Context): Unit =
614624
if !elems.isEmpty then
615625
isComplete = false // delay computation of Mutability status
616626

@@ -630,9 +640,9 @@ object CaptureSet:
630640
else ""
631641

632642
private def capImpliedByCapability(parent: Type)(using Context): Capability =
633-
if parent.derivesFromExclusive then GlobalCap.readOnly else GlobalCap
643+
if parent.derivesFromMutable then GlobalCap.readOnly else GlobalCap
634644

635-
/* The same as {cap.rd} but generated implicitly for references of Capability subtypes.
645+
/* The same as {cap} but generated implicitly for references of Capability subtypes.
636646
*/
637647
class CSImpliedByCapability(parent: Type)(using @constructorOnly ctx: Context)
638648
extends Const(SimpleIdentitySet(capImpliedByCapability(parent)))
@@ -705,7 +715,7 @@ object CaptureSet:
705715
*/
706716
var deps: Deps = SimpleIdentitySet.empty
707717

708-
def setMutable()(using Context): Unit =
718+
def associateWithMutable()(using Context): Unit =
709719
mutability = Mutable
710720

711721
def isConst(using Context) = solved >= ccState.iterationId
@@ -1028,6 +1038,9 @@ object CaptureSet:
10281038

10291039
addAsDependentTo(source)
10301040

1041+
/** Mutability is same as in source, except for readOnly */
1042+
override def associateWithMutable()(using Context): Unit = ()
1043+
10311044
override def mutableToReader(origin: CaptureSet)(using Context): Boolean =
10321045
super.mutableToReader(origin)
10331046
&& ((origin eq source) || source.mutableToReader(this))
@@ -1364,7 +1377,7 @@ object CaptureSet:
13641377
def description(using Context): String =
13651378
def ofType(tp: Type) = if tp.exists then i"of the mutable type $tp" else "of a mutable type"
13661379
i"""$cs is an exclusive capture set ${ofType(hi)},
1367-
|it cannot subsume a read-only capture set ${ofType(lo)}."""
1380+
|it cannot subsume a read-only capture set ${ofType(lo)}"""
13681381

13691382
/** A VarState serves as a snapshot mechanism that can undo
13701383
* additions of elements or super sets if an operation fails

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ object CapturingType:
3939
case parent @ CapturingType(parent1, refs1) if boxed || !parent.isBoxed =>
4040
apply(parent1, refs ++ refs1, boxed)
4141
case _ =>
42-
if parent.derivesFromMutable then refs.setMutable()
42+
if parent.derivesFromMutable then refs.associateWithMutable()
4343
refs.adoptClassifier(parent.classifier)
4444
AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot))
4545

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

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -680,12 +680,20 @@ class CheckCaptures extends Recheck, SymTransformer:
680680
if pt.select.symbol.isReadOnlyMethod then
681681
markFree(ref.readOnly, tree)
682682
else
683-
markPathFree(ref.select(pt.select.symbol).asInstanceOf[TermRef], pt.pt, pt.select)
683+
val sel = ref.select(pt.select.symbol).asInstanceOf[TermRef]
684+
sel.recomputeDenot()
685+
// We need to do a recomputeDenot here since we have not yet properly
686+
// computed the type of the full path. This means that we erroneously
687+
// think the denotation is the same as in the previous phase so no
688+
// member computation is performed. A test case where this matters is
689+
// read-only-use.scala, where the error on r3 goes unreported.
690+
markPathFree(sel, pt.pt, pt.select)
684691
case _ =>
685-
if ref.derivesFromMutable && pt.isValueType && !pt.isMutableType then
686-
markFree(ref.readOnly, tree)
687-
else
688-
markFree(ref, tree)
692+
if ref.derivesFromMutable then
693+
if pt.isValueType && !pt.isMutableType || ref.exclusivityInContext != Exclusivity.OK
694+
then markFree(ref.readOnly, tree)
695+
else markFree(ref, tree)
696+
else markFree(ref, tree)
689697

690698
/** The expected type for the qualifier of a selection. If the selection
691699
* could be part of a capability path or is a a read-only method, we return
@@ -724,13 +732,10 @@ class CheckCaptures extends Recheck, SymTransformer:
724732
case _ => denot
725733

726734
// Don't allow update methods to be called unless the qualifier captures
727-
// an exclusive reference. TODO This should probably rolled into
728-
// qualifier logic once we have it.
729-
if tree.symbol.isUpdateMethod && !qualType.captureSet.isExclusive then
730-
report.error(
731-
em"""cannot call update ${tree.symbol} from $qualType,
732-
|since its capture set ${qualType.captureSet} is read-only""",
733-
tree.srcPos)
735+
// an exclusive reference.
736+
if tree.symbol.isUpdateMethod then
737+
checkUpdate(qualType, tree.srcPos):
738+
i"Cannot call update ${tree.symbol} of ${qualType.showRef}"
734739

735740
val origSelType = recheckSelection(tree, qualType, name, disambiguate)
736741
val selType = mapResultRoots(origSelType, tree.symbol)
@@ -768,6 +773,12 @@ class CheckCaptures extends Recheck, SymTransformer:
768773
selType
769774
}//.showing(i"recheck sel $tree, $qualType = $result")
770775

776+
def checkUpdate(qualType: Type, pos: SrcPos)(msg: => String)(using Context): Unit =
777+
qualType.exclusivityInContext match
778+
case Exclusivity.OK =>
779+
case err =>
780+
report.error(em"$msg\nsince ${err.description(qualType)}.", pos)
781+
771782
/** Recheck applications, with special handling of unsafeAssumePure.
772783
* More work is done in `recheckApplication`, `recheckArg` and `instantiate` below.
773784
*/
@@ -997,6 +1008,16 @@ class CheckCaptures extends Recheck, SymTransformer:
9971008
report.error(em"$refArg is not a tracked capability", refArg.srcPos)
9981009
case _ =>
9991010

1011+
override def recheckAssign(tree: Assign)(using Context): Type =
1012+
val lhsType = recheck(tree.lhs, LhsProto)
1013+
recheck(tree.rhs, lhsType.widen)
1014+
lhsType match
1015+
case lhsType @ TermRef(qualType, _)
1016+
if (qualType ne NoPrefix) && !lhsType.symbol.is(Transparent) =>
1017+
checkUpdate(qualType, tree.srcPos)(i"Cannot assign to field ${lhsType.name} of ${qualType.showRef}")
1018+
case _ =>
1019+
defn.UnitType
1020+
10001021
/** Recheck Closure node: add the captured vars of the anonymoys function
10011022
* to the result type. See also `recheckClosureBlock` which rechecks the
10021023
* block containing the anonymous function and the Closure node.
@@ -1836,6 +1857,17 @@ class CheckCaptures extends Recheck, SymTransformer:
18361857
actual
18371858
end improveReadOnly
18381859

1860+
def adaptReadOnly(improved: Type, original: Type, expected: Type, tree: Tree)(using Context): Type = improved match
1861+
case improved @ CapturingType(parent, refs)
1862+
if parent.derivesFrom(defn.Caps_Mutable)
1863+
&& expected.isValueType
1864+
&& refs.isExclusive
1865+
&& !original.exclusivityInContext.isOK =>
1866+
improved.derivedCapturingType(parent, refs.readOnly)
1867+
.showing(i"Adapted readonly $improved for $tree with original = $original in ${ctx.owner} --> $result", capt)
1868+
case _ =>
1869+
improved
1870+
18391871
/* Currently not needed since it forms part of `adapt`
18401872
private def improve(actual: Type, prefix: Type)(using Context): Type =
18411873
val widened = actual.widen.dealiasKeepAnnots
@@ -1873,10 +1905,11 @@ class CheckCaptures extends Recheck, SymTransformer:
18731905
val widened = actual.widen.dealiasKeepAnnots.dropUseAndConsumeAnnots
18741906
val improvedVAR = improveCaptures(widened, actual)
18751907
val improved = improveReadOnly(improvedVAR, expected)
1908+
val adaptedReadOnly = adaptReadOnly(improved, actual, expected, tree)
18761909
val adapted = adaptBoxed(
1877-
improved.withReachCaptures(actual), expected, tree,
1910+
adaptedReadOnly.withReachCaptures(actual), expected, tree,
18781911
covariant = true, alwaysConst = false)
1879-
if adapted eq improvedVAR // no .rd improvement, no box-adaptation
1912+
if adapted eq improvedVAR // no .rd improvement or adaptation, no box-adaptation
18801913
then actual // might as well use actual instead of improved widened
18811914
else adapted.showing(i"adapt $actual vs $expected = $adapted", capt)
18821915
end adapt

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -689,9 +689,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
689689
// neither pure, nor are publicily extensible with an unconstrained self type.
690690
val cs = CaptureSet.ProperVar(cls, CaptureSet.emptyRefs, nestedOK = false, isRefining = false)
691691
if cls.derivesFrom(defn.Caps_Capability) then
692-
// If cls is a capability class, we need to add a fresh readonly capability to
693-
// ensure we cannot treat the class as pure.
694-
CaptureSet.fresh(cls, cls.thisType, Origin.InDecl(cls)).readOnly.subCaptures(cs)
692+
// If cls is a capability class, we need to add a fresh capability to ensure
693+
// we cannot treat the class as pure.
694+
CaptureSet.fresh(cls, cls.thisType, Origin.InDecl(cls)).subCaptures(cs)
695695
CapturingType(cinfo.selfType, cs)
696696

697697
// Compute new parent types

compiler/src/dotty/tools/dotc/typer/Checking.scala

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -630,8 +630,13 @@ object Checking {
630630
if sym.is(Transparent) then
631631
if sym.isType then
632632
if !sym.isExtensibleClass then fail(em"`transparent` can only be used for extensible classes and traits")
633+
else if sym.isMutableVar && sym.owner.isClass && Feature.ccEnabled
634+
|| sym.isInlineMethod
635+
then
636+
() // ok
633637
else
634-
if !sym.isInlineMethod then fail(em"`transparent` can only be used for inline methods")
638+
def ccAdd = if Feature.ccEnabled then i" and mutable fields" else ""
639+
fail(em"`transparent` can only be used for inline methods$ccAdd")
635640
if (!sym.isClass && sym.is(Abstract))
636641
fail(OnlyClassesCanBeAbstract(sym))
637642
// note: this is not covered by the next test since terms can be abstract (which is a dual-mode flag)
@@ -691,7 +696,7 @@ object Checking {
691696
if sym.isWrappedToplevelDef && !sym.isType && sym.flags.is(Infix, butNot = Extension) then
692697
fail(ModifierNotAllowedForDefinition(Flags.Infix, s"A top-level ${sym.showKind} cannot be infix."))
693698
if sym.isUpdateMethod && !sym.owner.derivesFrom(defn.Caps_Mutable) then
694-
fail(em"Update methods can only be used as members of classes extending the `Mutable` trait")
699+
fail(em"Update method ${sym.name} must be declared in a class extending the `Mutable` trait.")
695700
if sym.is(Erased) then checkErasedOK(sym)
696701
checkCombination(Final, Open)
697702
checkCombination(Sealed, Open)

0 commit comments

Comments
 (0)