Skip to content

Commit

Permalink
Add Mutable classes and ReadOnly capabilities
Browse files Browse the repository at this point in the history
 - 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.
  • Loading branch information
odersky committed Jan 10, 2025
1 parent 4ee3154 commit 2d9af0d
Show file tree
Hide file tree
Showing 54 changed files with 1,095 additions and 209 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ class BTypesFromSymbols[I <: DottyBackendInterface](val int: I, val frontendAcce
// tests/run/serialize.scala and https://github.com/typelevel/cats-effect/pull/2360).
val privateFlag = !sym.isClass && (sym.is(Private) || (sym.isPrimaryConstructor && sym.owner.isTopLevelModuleClass))

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

import asm.Opcodes.*
import GenBCodeOps.addFlagIf
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2231,6 +2231,8 @@ object desugar {
New(ref(defn.RepeatedAnnot.typeRef), Nil :: Nil))
else if op.name == nme.CC_REACH then
Apply(ref(defn.Caps_reachCapability), t :: Nil)
else if op.name == nme.CC_READONLY then
Apply(ref(defn.Caps_readOnlyCapability), t :: Nil)
else
assert(ctx.mode.isExpr || ctx.reporter.errorsReported || ctx.mode.is(Mode.Interactive), ctx.mode)
Select(t, op.name)
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
*/
def isVariableOrGetter(tree: Tree)(using Context): Boolean = {
def sym = tree.symbol
def isVar = sym.is(Mutable)
def isVar = sym.isMutableVarOrAccessor
def isGetter =
mayBeVarGetter(sym) && sym.owner.info.member(sym.name.asTermName.setterName).exists

Expand Down
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/untpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {

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

case class Mut()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Mutable)

case class Implicit()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Implicit)

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

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

@sharable val EmptyModifiers: Modifiers = Modifiers()
Expand Down
165 changes: 101 additions & 64 deletions compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ extension (tree: Tree)
def toCaptureRefs(using Context): List[CaptureRef] = tree match
case ReachCapabilityApply(arg) =>
arg.toCaptureRefs.map(_.reach)
case ReadOnlyCapabilityApply(arg) =>
arg.toCaptureRefs.map(_.readOnly)
case CapsOfApply(arg) =>
arg.toCaptureRefs
case _ => tree.tpe.dealiasKeepAnnots match
Expand Down Expand Up @@ -184,7 +186,7 @@ extension (tp: Type)
case tp: TermRef =>
((tp.prefix eq NoPrefix)
|| tp.symbol.isField && !tp.symbol.isStatic && tp.prefix.isTrackableRef
|| tp.isRootCapability
|| tp.isCap
) && !tp.symbol.isOneOf(UnstableValueFlags)
case tp: TypeRef =>
tp.symbol.isType && tp.derivesFrom(defn.Caps_CapSet)
Expand All @@ -193,6 +195,7 @@ extension (tp: Type)
case AnnotatedType(parent, annot) =>
(annot.symbol == defn.ReachCapabilityAnnot
|| annot.symbol == defn.MaybeCapabilityAnnot
|| annot.symbol == defn.ReadOnlyCapabilityAnnot
) && parent.isTrackableRef
case _ =>
false
Expand Down Expand Up @@ -222,6 +225,8 @@ extension (tp: Type)
else tp match
case tp @ ReachCapability(_) =>
tp.singletonCaptureSet
case ReadOnlyCapability(ref) =>
ref.deepCaptureSet(includeTypevars)
case tp: SingletonCaptureRef if tp.isTrackableRef =>
tp.reach.singletonCaptureSet
case _ =>
Expand Down Expand Up @@ -345,7 +350,8 @@ extension (tp: Type)
def forceBoxStatus(boxed: Boolean)(using Context): Type = tp.widenDealias match
case tp @ CapturingType(parent, refs) if tp.isBoxed != boxed =>
val refs1 = tp match
case ref: CaptureRef if ref.isTracked || ref.isReach => ref.singletonCaptureSet
case ref: CaptureRef if ref.isTracked || ref.isReach || ref.isReadOnly =>
ref.singletonCaptureSet
case _ => refs
CapturingType(parent, refs1, boxed)
case _ =>
Expand Down Expand Up @@ -379,23 +385,32 @@ extension (tp: Type)
case _ =>
false

/** Is this a type extending `Mutable` that has update methods? */
def isMutableType(using Context): Boolean =
tp.derivesFrom(defn.Caps_Mutable)
&& tp.membersBasedOnFlags(Mutable | Method, EmptyFlags)
.exists(_.hasAltWith(_.symbol.isUpdateMethod))

/** Tests whether the type derives from `caps.Capability`, which means
* references of this type are maximal capabilities.
*/
def derivesFromCapability(using Context): Boolean = tp.dealias match
def derivesFromCapTrait(cls: ClassSymbol)(using Context): Boolean = tp.dealias match
case tp: (TypeRef | AppliedType) =>
val sym = tp.typeSymbol
if sym.isClass then sym.derivesFrom(defn.Caps_Capability)
else tp.superType.derivesFromCapability
if sym.isClass then sym.derivesFrom(cls)
else tp.superType.derivesFromCapTrait(cls)
case tp: (TypeProxy & ValueType) =>
tp.superType.derivesFromCapability
tp.superType.derivesFromCapTrait(cls)
case tp: AndType =>
tp.tp1.derivesFromCapability || tp.tp2.derivesFromCapability
tp.tp1.derivesFromCapTrait(cls) || tp.tp2.derivesFromCapTrait(cls)
case tp: OrType =>
tp.tp1.derivesFromCapability && tp.tp2.derivesFromCapability
tp.tp1.derivesFromCapTrait(cls) && tp.tp2.derivesFromCapTrait(cls)
case _ =>
false

def derivesFromCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_Capability)
def derivesFromMutable(using Context): Boolean = derivesFromCapTrait(defn.Caps_Mutable)

/** Drop @retains annotations everywhere */
def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling
val tm = new TypeMap:
Expand All @@ -406,17 +421,6 @@ extension (tp: Type)
mapOver(t)
tm(tp)

/** If `x` is a capture ref, its reach capability `x*`, represented internally
* as `x @reachCapability`. `x*` stands for all capabilities reachable through `x`".
* We have `{x} <: {x*} <: dcs(x)}` where the deep capture set `dcs(x)` of `x`
* is the union of all capture sets that appear in covariant position in the
* type of `x`. If `x` and `y` are different variables then `{x*}` and `{y*}`
* are unrelated.
*/
def reach(using Context): CaptureRef = tp match
case tp: CaptureRef if tp.isTrackableRef =>
if tp.isReach then tp else ReachCapability(tp)

/** If `x` is a capture ref, its maybe capability `x?`, represented internally
* as `x @maybeCapability`. `x?` stands for a capability `x` that might or might
* not be part of a capture set. We have `{} <: {x?} <: {x}`. Maybe capabilities
Expand All @@ -436,52 +440,54 @@ extension (tp: Type)
* but it has fewer issues with type inference.
*/
def maybe(using Context): CaptureRef = tp match
case tp: CaptureRef if tp.isTrackableRef =>
if tp.isMaybe then tp else MaybeCapability(tp)
case tp @ AnnotatedType(_, annot) if annot.symbol == defn.MaybeCapabilityAnnot => tp
case _ => MaybeCapability(tp)

/** If `ref` is a trackable capture ref, and `tp` has only covariant occurrences of a
* universal capture set, replace all these occurrences by `{ref*}`. This implements
* the new aspect of the (Var) rule, which can now be stated as follows:
*
* x: T in E
* -----------
* E |- x: T'
*
* where T' is T with (1) the toplevel capture set replaced by `{x}` and
* (2) all covariant occurrences of cap replaced by `x*`, provided there
* are no occurrences in `T` at other variances. (1) is standard, whereas
* (2) is new.
*
* For (2), multiple-flipped covariant occurrences of cap won't be replaced.
* In other words,
*
* - For xs: List[File^] ==> List[File^{xs*}], the cap is replaced;
* - while f: [R] -> (op: File^ => R) -> R remains unchanged.
*
* Without this restriction, the signature of functions like withFile:
*
* (path: String) -> [R] -> (op: File^ => R) -> R
*
* could be refined to
*
* (path: String) -> [R] -> (op: File^{withFile*} => R) -> R
*
* which is clearly unsound.
*
* Why is this sound? Covariant occurrences of cap must represent capabilities
* that are reachable from `x`, so they are included in the meaning of `{x*}`.
* At the same time, encapsulation is still maintained since no covariant
* occurrences of cap are allowed in instance types of type variables.
/** If `x` is a capture ref, its reach capability `x*`, represented internally
* as `x @reachCapability`. `x*` stands for all capabilities reachable through `x`".
* We have `{x} <: {x*} <: dcs(x)}` where the deep capture set `dcs(x)` of `x`
* is the union of all capture sets that appear in covariant position in the
* type of `x`. If `x` and `y` are different variables then `{x*}` and `{y*}`
* are unrelated.
*/
def reach(using Context): CaptureRef = tp match
case tp @ AnnotatedType(tp1: CaptureRef, annot)
if annot.symbol == defn.MaybeCapabilityAnnot =>
tp.derivedAnnotatedType(tp1.reach, annot)
case tp @ AnnotatedType(tp1: CaptureRef, annot)
if annot.symbol == defn.ReachCapabilityAnnot =>
tp
case _ =>
ReachCapability(tp)

/** If `x` is a capture ref, its read-only capability `x.rd`, represented internally
* as `x @readOnlyCapability`. We have {x.rd} <: {x}. If `x` is a reach capability `y*`,
* then its read-only version is `x.rd*`.
*/
def readOnly(using Context): CaptureRef = tp match
case tp @ AnnotatedType(tp1: CaptureRef, annot)
if annot.symbol == defn.MaybeCapabilityAnnot
|| annot.symbol == defn.ReachCapabilityAnnot =>
tp.derivedAnnotatedType(tp1.readOnly, annot)
case tp @ AnnotatedType(tp1: CaptureRef, annot)
if annot.symbol == defn.ReadOnlyCapabilityAnnot =>
tp
case _ =>
ReadOnlyCapability(tp)

/** If `x` is a capture ref, replacxe all no-flip covariant occurrences of `cap`
* in type `tp` with `x*`.
*/
def withReachCaptures(ref: Type)(using Context): Type =
object narrowCaps extends TypeMap:
var change = false
def apply(t: Type) =
if variance <= 0 then t
else t.dealiasKeepAnnots match
case t @ CapturingType(p, cs) if cs.isUniversal =>
case t @ CapturingType(p, cs) if cs.containsRootCapability =>
change = true
t.derivedCapturingType(apply(p), ref.reach.singletonCaptureSet)
val reachRef = if cs.isReadOnly then ref.reach.readOnly else ref.reach
t.derivedCapturingType(apply(p), reachRef.singletonCaptureSet)
case t @ AnnotatedType(parent, ann) =>
// Don't map annotations, which includes capture sets
t.derivedAnnotatedType(this(parent), ann)
Expand Down Expand Up @@ -615,6 +621,16 @@ extension (sym: Symbol)
case c: TypeRef => c.symbol == sym
case _ => false

def isUpdateMethod(using Context): Boolean =
sym.isAllOf(Mutable | Method, butNot = Accessor)

def isReadOnlyMethod(using Context): Boolean =
sym.is(Method, butNot = Mutable | Accessor) && sym.owner.derivesFrom(defn.Caps_Mutable)

def isInReadOnlyMethod(using Context): Boolean =
if sym.is(Method) && sym.owner.isClass then isReadOnlyMethod
else sym.owner.isInReadOnlyMethod

extension (tp: AnnotatedType)
/** Is this a boxed capturing type? */
def isBoxed(using Context): Boolean = tp.annot match
Expand Down Expand Up @@ -650,6 +666,14 @@ object ReachCapabilityApply:
case Apply(reach, arg :: Nil) if reach.symbol == defn.Caps_reachCapability => Some(arg)
case _ => None

/** An extractor for `caps.readOnlyCapability(ref)`, which is used to express a read-only
* capability as a tree in a @retains annotation.
*/
object ReadOnlyCapabilityApply:
def unapply(tree: Apply)(using Context): Option[Tree] = tree match
case Apply(ro, arg :: Nil) if ro.symbol == defn.Caps_readOnlyCapability => Some(arg)
case _ => None

/** An extractor for `caps.capsOf[X]`, which is used to express a generic capture set
* as a tree in a @retains annotation.
*/
Expand All @@ -658,22 +682,35 @@ object CapsOfApply:
case TypeApply(capsOf, arg :: Nil) if capsOf.symbol == defn.Caps_capsOf => Some(arg)
case _ => None

class AnnotatedCapability(annot: Context ?=> ClassSymbol):
def apply(tp: Type)(using Context) =
abstract class AnnotatedCapability(annot: Context ?=> ClassSymbol):
def apply(tp: Type)(using Context): AnnotatedType =
assert(tp.isTrackableRef)
tp match
case AnnotatedType(_, annot) => assert(!unwrappable.contains(annot.symbol))
case _ =>
AnnotatedType(tp, Annotation(annot, util.Spans.NoSpan))
def unapply(tree: AnnotatedType)(using Context): Option[CaptureRef] = tree match
case AnnotatedType(parent: CaptureRef, ann) if ann.symbol == annot => Some(parent)
case _ => None

/** An extractor for `ref @annotation.internal.reachCapability`, which is used to express
* the reach capability `ref*` as a type.
*/
object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot)
protected def unwrappable(using Context): Set[Symbol]

/** An extractor for `ref @maybeCapability`, which is used to express
* the maybe capability `ref?` as a type.
*/
object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot)
object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot):
protected def unwrappable(using Context) = Set()

/** An extractor for `ref @readOnlyCapability`, which is used to express
* the rad-only capability `ref.rd` as a type.
*/
object ReadOnlyCapability extends AnnotatedCapability(defn.ReadOnlyCapabilityAnnot):
protected def unwrappable(using Context) = Set(defn.MaybeCapabilityAnnot)

/** An extractor for `ref @annotation.internal.reachCapability`, which is used to express
* the reach capability `ref*` as a type.
*/
object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot):
protected def unwrappable(using Context) = Set(defn.MaybeCapabilityAnnot, defn.ReadOnlyCapabilityAnnot)

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

0 comments on commit 2d9af0d

Please sign in to comment.