Skip to content

Commit b4eaf3d

Browse files
committed
Add Mutable classes and ReadOnly capabilities
- Add Mutable trait and mut modifier. - Add dedicated tests `isMutableVar` and `isMutableVarOrAccessor` so that update methods can share the same flag `Mutable` with mutable vars. - Disallow update methods overriding normal methods - Disallow update methods which are not members of classes extending Mutable - Add design document from papers repo to docs/internals - Add readOnly capabilities - Implement raeadOnly access - Check that update methods are only called on references with exclusive capture sets. - Use cap.rd as default capture set of Capability subtypes - Make Mutable a Capability, this means Mutable class references get {cap.rd} as default capture set. - Use {cap} as capture set for creation of types extending Mutable - Narrow retained capture set if expected type is read-only.
1 parent 9aaf684 commit b4eaf3d

Some content is hidden

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

53 files changed

+1095
-210
lines changed

compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ class BTypesFromSymbols[I <: DottyBackendInterface](val int: I, val frontendAcce
285285
// tests/run/serialize.scala and https://github.com/typelevel/cats-effect/pull/2360).
286286
val privateFlag = !sym.isClass && (sym.is(Private) || (sym.isPrimaryConstructor && sym.owner.isTopLevelModuleClass))
287287

288-
val finalFlag = sym.is(Final) && !toDenot(sym).isClassConstructor && !sym.is(Mutable, butNot = Accessor) && !sym.enclosingClass.is(Trait)
288+
val finalFlag = sym.is(Final) && !toDenot(sym).isClassConstructor && !sym.isMutableVar && !sym.enclosingClass.is(Trait)
289289

290290
import asm.Opcodes.*
291291
import GenBCodeOps.addFlagIf

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2231,6 +2231,8 @@ object desugar {
22312231
New(ref(defn.RepeatedAnnot.typeRef), Nil :: Nil))
22322232
else if op.name == nme.CC_REACH then
22332233
Apply(ref(defn.Caps_reachCapability), t :: Nil)
2234+
else if op.name == nme.CC_READONLY then
2235+
Apply(ref(defn.Caps_readOnlyCapability), t :: Nil)
22342236
else
22352237
assert(ctx.mode.isExpr || ctx.reporter.errorsReported || ctx.mode.is(Mode.Interactive), ctx.mode)
22362238
Select(t, op.name)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,7 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
755755
*/
756756
def isVariableOrGetter(tree: Tree)(using Context): Boolean = {
757757
def sym = tree.symbol
758-
def isVar = sym.is(Mutable)
758+
def isVar = sym.isMutableVarOrAccessor
759759
def isGetter =
760760
mayBeVarGetter(sym) && sym.owner.info.member(sym.name.asTermName.setterName).exists
761761

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
206206

207207
case class Var()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Mutable)
208208

209+
case class Mut()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Mutable)
210+
209211
case class Implicit()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Implicit)
210212

211213
case class Given()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Given)
@@ -332,6 +334,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
332334

333335
def isEnumCase: Boolean = isEnum && is(Case)
334336
def isEnumClass: Boolean = isEnum && !is(Case)
337+
def isMutableVar: Boolean = is(Mutable) && mods.exists(_.isInstanceOf[Mod.Var])
335338
}
336339

337340
@sharable val EmptyModifiers: Modifiers = Modifiers()

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

Lines changed: 101 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ extension (tree: Tree)
136136
def toCaptureRefs(using Context): List[CaptureRef] = tree match
137137
case ReachCapabilityApply(arg) =>
138138
arg.toCaptureRefs.map(_.reach)
139+
case ReadOnlyCapabilityApply(arg) =>
140+
arg.toCaptureRefs.map(_.readOnly)
139141
case CapsOfApply(arg) =>
140142
arg.toCaptureRefs
141143
case _ => tree.tpe.dealiasKeepAnnots match
@@ -184,7 +186,7 @@ extension (tp: Type)
184186
case tp: TermRef =>
185187
((tp.prefix eq NoPrefix)
186188
|| tp.symbol.isField && !tp.symbol.isStatic && tp.prefix.isTrackableRef
187-
|| tp.isRootCapability
189+
|| tp.isCap
188190
) && !tp.symbol.isOneOf(UnstableValueFlags)
189191
case tp: TypeRef =>
190192
tp.symbol.isType && tp.derivesFrom(defn.Caps_CapSet)
@@ -193,6 +195,7 @@ extension (tp: Type)
193195
case AnnotatedType(parent, annot) =>
194196
(annot.symbol == defn.ReachCapabilityAnnot
195197
|| annot.symbol == defn.MaybeCapabilityAnnot
198+
|| annot.symbol == defn.ReadOnlyCapabilityAnnot
196199
) && parent.isTrackableRef
197200
case _ =>
198201
false
@@ -222,6 +225,8 @@ extension (tp: Type)
222225
else tp match
223226
case tp @ ReachCapability(_) =>
224227
tp.singletonCaptureSet
228+
case ReadOnlyCapability(ref) =>
229+
ref.deepCaptureSet(includeTypevars)
225230
case tp: SingletonCaptureRef if tp.isTrackableRef =>
226231
tp.reach.singletonCaptureSet
227232
case _ =>
@@ -345,7 +350,8 @@ extension (tp: Type)
345350
def forceBoxStatus(boxed: Boolean)(using Context): Type = tp.widenDealias match
346351
case tp @ CapturingType(parent, refs) if tp.isBoxed != boxed =>
347352
val refs1 = tp match
348-
case ref: CaptureRef if ref.isTracked || ref.isReach => ref.singletonCaptureSet
353+
case ref: CaptureRef if ref.isTracked || ref.isReach || ref.isReadOnly =>
354+
ref.singletonCaptureSet
349355
case _ => refs
350356
CapturingType(parent, refs1, boxed)
351357
case _ =>
@@ -379,23 +385,32 @@ extension (tp: Type)
379385
case _ =>
380386
false
381387

388+
/** Is this a type extending `Mutable` that has update methods? */
389+
def isMutableType(using Context): Boolean =
390+
tp.derivesFrom(defn.Caps_Mutable)
391+
&& tp.membersBasedOnFlags(Mutable | Method, EmptyFlags)
392+
.exists(_.hasAltWith(_.symbol.isUpdateMethod))
393+
382394
/** Tests whether the type derives from `caps.Capability`, which means
383395
* references of this type are maximal capabilities.
384396
*/
385-
def derivesFromCapability(using Context): Boolean = tp.dealias match
397+
def derivesFromCapTrait(cls: ClassSymbol)(using Context): Boolean = tp.dealias match
386398
case tp: (TypeRef | AppliedType) =>
387399
val sym = tp.typeSymbol
388-
if sym.isClass then sym.derivesFrom(defn.Caps_Capability)
389-
else tp.superType.derivesFromCapability
400+
if sym.isClass then sym.derivesFrom(cls)
401+
else tp.superType.derivesFromCapTrait(cls)
390402
case tp: (TypeProxy & ValueType) =>
391-
tp.superType.derivesFromCapability
403+
tp.superType.derivesFromCapTrait(cls)
392404
case tp: AndType =>
393-
tp.tp1.derivesFromCapability || tp.tp2.derivesFromCapability
405+
tp.tp1.derivesFromCapTrait(cls) || tp.tp2.derivesFromCapTrait(cls)
394406
case tp: OrType =>
395-
tp.tp1.derivesFromCapability && tp.tp2.derivesFromCapability
407+
tp.tp1.derivesFromCapTrait(cls) && tp.tp2.derivesFromCapTrait(cls)
396408
case _ =>
397409
false
398410

411+
def derivesFromCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_Capability)
412+
def derivesFromMutable(using Context): Boolean = derivesFromCapTrait(defn.Caps_Mutable)
413+
399414
/** Drop @retains annotations everywhere */
400415
def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling
401416
val tm = new TypeMap:
@@ -406,17 +421,6 @@ extension (tp: Type)
406421
mapOver(t)
407422
tm(tp)
408423

409-
/** If `x` is a capture ref, its reach capability `x*`, represented internally
410-
* as `x @reachCapability`. `x*` stands for all capabilities reachable through `x`".
411-
* We have `{x} <: {x*} <: dcs(x)}` where the deep capture set `dcs(x)` of `x`
412-
* is the union of all capture sets that appear in covariant position in the
413-
* type of `x`. If `x` and `y` are different variables then `{x*}` and `{y*}`
414-
* are unrelated.
415-
*/
416-
def reach(using Context): CaptureRef = tp match
417-
case tp: CaptureRef if tp.isTrackableRef =>
418-
if tp.isReach then tp else ReachCapability(tp)
419-
420424
/** If `x` is a capture ref, its maybe capability `x?`, represented internally
421425
* as `x @maybeCapability`. `x?` stands for a capability `x` that might or might
422426
* not be part of a capture set. We have `{} <: {x?} <: {x}`. Maybe capabilities
@@ -436,52 +440,54 @@ extension (tp: Type)
436440
* but it has fewer issues with type inference.
437441
*/
438442
def maybe(using Context): CaptureRef = tp match
439-
case tp: CaptureRef if tp.isTrackableRef =>
440-
if tp.isMaybe then tp else MaybeCapability(tp)
443+
case tp @ AnnotatedType(_, annot) if annot.symbol == defn.MaybeCapabilityAnnot => tp
444+
case _ => MaybeCapability(tp)
441445

442-
/** If `ref` is a trackable capture ref, and `tp` has only covariant occurrences of a
443-
* universal capture set, replace all these occurrences by `{ref*}`. This implements
444-
* the new aspect of the (Var) rule, which can now be stated as follows:
445-
*
446-
* x: T in E
447-
* -----------
448-
* E |- x: T'
449-
*
450-
* where T' is T with (1) the toplevel capture set replaced by `{x}` and
451-
* (2) all covariant occurrences of cap replaced by `x*`, provided there
452-
* are no occurrences in `T` at other variances. (1) is standard, whereas
453-
* (2) is new.
454-
*
455-
* For (2), multiple-flipped covariant occurrences of cap won't be replaced.
456-
* In other words,
457-
*
458-
* - For xs: List[File^] ==> List[File^{xs*}], the cap is replaced;
459-
* - while f: [R] -> (op: File^ => R) -> R remains unchanged.
460-
*
461-
* Without this restriction, the signature of functions like withFile:
462-
*
463-
* (path: String) -> [R] -> (op: File^ => R) -> R
464-
*
465-
* could be refined to
466-
*
467-
* (path: String) -> [R] -> (op: File^{withFile*} => R) -> R
468-
*
469-
* which is clearly unsound.
470-
*
471-
* Why is this sound? Covariant occurrences of cap must represent capabilities
472-
* that are reachable from `x`, so they are included in the meaning of `{x*}`.
473-
* At the same time, encapsulation is still maintained since no covariant
474-
* occurrences of cap are allowed in instance types of type variables.
446+
/** If `x` is a capture ref, its reach capability `x*`, represented internally
447+
* as `x @reachCapability`. `x*` stands for all capabilities reachable through `x`".
448+
* We have `{x} <: {x*} <: dcs(x)}` where the deep capture set `dcs(x)` of `x`
449+
* is the union of all capture sets that appear in covariant position in the
450+
* type of `x`. If `x` and `y` are different variables then `{x*}` and `{y*}`
451+
* are unrelated.
452+
*/
453+
def reach(using Context): CaptureRef = tp match
454+
case tp @ AnnotatedType(tp1: CaptureRef, annot)
455+
if annot.symbol == defn.MaybeCapabilityAnnot =>
456+
tp.derivedAnnotatedType(tp1.reach, annot)
457+
case tp @ AnnotatedType(tp1: CaptureRef, annot)
458+
if annot.symbol == defn.ReachCapabilityAnnot =>
459+
tp
460+
case _ =>
461+
ReachCapability(tp)
462+
463+
/** If `x` is a capture ref, its read-only capability `x.rd`, represented internally
464+
* as `x @readOnlyCapability`. We have {x.rd} <: {x}. If `x` is a reach capability `y*`,
465+
* then its read-only version is `x.rd*`.
466+
*/
467+
def readOnly(using Context): CaptureRef = tp match
468+
case tp @ AnnotatedType(tp1: CaptureRef, annot)
469+
if annot.symbol == defn.MaybeCapabilityAnnot
470+
|| annot.symbol == defn.ReachCapabilityAnnot =>
471+
tp.derivedAnnotatedType(tp1.readOnly, annot)
472+
case tp @ AnnotatedType(tp1: CaptureRef, annot)
473+
if annot.symbol == defn.ReadOnlyCapabilityAnnot =>
474+
tp
475+
case _ =>
476+
ReadOnlyCapability(tp)
477+
478+
/** If `x` is a capture ref, replacxe all no-flip covariant occurrences of `cap`
479+
* in type `tp` with `x*`.
475480
*/
476481
def withReachCaptures(ref: Type)(using Context): Type =
477482
object narrowCaps extends TypeMap:
478483
var change = false
479484
def apply(t: Type) =
480485
if variance <= 0 then t
481486
else t.dealiasKeepAnnots match
482-
case t @ CapturingType(p, cs) if cs.isUniversal =>
487+
case t @ CapturingType(p, cs) if cs.containsRootCapability =>
483488
change = true
484-
t.derivedCapturingType(apply(p), ref.reach.singletonCaptureSet)
489+
val reachRef = if cs.isReadOnly then ref.reach.readOnly else ref.reach
490+
t.derivedCapturingType(apply(p), reachRef.singletonCaptureSet)
485491
case t @ AnnotatedType(parent, ann) =>
486492
// Don't map annotations, which includes capture sets
487493
t.derivedAnnotatedType(this(parent), ann)
@@ -615,6 +621,16 @@ extension (sym: Symbol)
615621
case c: TypeRef => c.symbol == sym
616622
case _ => false
617623

624+
def isUpdateMethod(using Context): Boolean =
625+
sym.isAllOf(Mutable | Method, butNot = Accessor)
626+
627+
def isReadOnlyMethod(using Context): Boolean =
628+
sym.is(Method, butNot = Mutable | Accessor) && sym.owner.derivesFrom(defn.Caps_Mutable)
629+
630+
def isInReadOnlyMethod(using Context): Boolean =
631+
if sym.is(Method) && sym.owner.isClass then isReadOnlyMethod
632+
else sym.owner.isInReadOnlyMethod
633+
618634
extension (tp: AnnotatedType)
619635
/** Is this a boxed capturing type? */
620636
def isBoxed(using Context): Boolean = tp.annot match
@@ -637,6 +653,14 @@ object ReachCapabilityApply:
637653
case Apply(reach, arg :: Nil) if reach.symbol == defn.Caps_reachCapability => Some(arg)
638654
case _ => None
639655

656+
/** An extractor for `caps.readOnlyCapability(ref)`, which is used to express a read-only
657+
* capability as a tree in a @retains annotation.
658+
*/
659+
object ReadOnlyCapabilityApply:
660+
def unapply(tree: Apply)(using Context): Option[Tree] = tree match
661+
case Apply(ro, arg :: Nil) if ro.symbol == defn.Caps_readOnlyCapability => Some(arg)
662+
case _ => None
663+
640664
/** An extractor for `caps.capsOf[X]`, which is used to express a generic capture set
641665
* as a tree in a @retains annotation.
642666
*/
@@ -645,22 +669,35 @@ object CapsOfApply:
645669
case TypeApply(capsOf, arg :: Nil) if capsOf.symbol == defn.Caps_capsOf => Some(arg)
646670
case _ => None
647671

648-
class AnnotatedCapability(annot: Context ?=> ClassSymbol):
649-
def apply(tp: Type)(using Context) =
672+
abstract class AnnotatedCapability(annot: Context ?=> ClassSymbol):
673+
def apply(tp: Type)(using Context): AnnotatedType =
674+
assert(tp.isTrackableRef)
675+
tp match
676+
case AnnotatedType(_, annot) => assert(!unwrappable.contains(annot.symbol))
677+
case _ =>
650678
AnnotatedType(tp, Annotation(annot, util.Spans.NoSpan))
651679
def unapply(tree: AnnotatedType)(using Context): Option[CaptureRef] = tree match
652680
case AnnotatedType(parent: CaptureRef, ann) if ann.symbol == annot => Some(parent)
653681
case _ => None
654-
655-
/** An extractor for `ref @annotation.internal.reachCapability`, which is used to express
656-
* the reach capability `ref*` as a type.
657-
*/
658-
object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot)
682+
protected def unwrappable(using Context): Set[Symbol]
659683

660684
/** An extractor for `ref @maybeCapability`, which is used to express
661685
* the maybe capability `ref?` as a type.
662686
*/
663-
object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot)
687+
object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot):
688+
protected def unwrappable(using Context) = Set()
689+
690+
/** An extractor for `ref @readOnlyCapability`, which is used to express
691+
* the rad-only capability `ref.rd` as a type.
692+
*/
693+
object ReadOnlyCapability extends AnnotatedCapability(defn.ReadOnlyCapabilityAnnot):
694+
protected def unwrappable(using Context) = Set(defn.MaybeCapabilityAnnot)
695+
696+
/** An extractor for `ref @annotation.internal.reachCapability`, which is used to express
697+
* the reach capability `ref*` as a type.
698+
*/
699+
object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot):
700+
protected def unwrappable(using Context) = Set(defn.MaybeCapabilityAnnot, defn.ReadOnlyCapabilityAnnot)
664701

665702
/** Offers utility method to be used for type maps that follow aliases */
666703
trait ConservativeFollowAliasMap(using Context) extends TypeMap:

0 commit comments

Comments
 (0)