diff --git a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala index 97934935f352..817d0be54d26 100644 --- a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala +++ b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala @@ -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 diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 67e1885b511f..3eb186786be5 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -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) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index e0fe17755257..6ea6c27331dd 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -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 diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 2acfc4cf86e3..e89dc2c1cdb5 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -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) @@ -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() diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala index f0018cc93d7e..8977d146a639 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -55,11 +55,6 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte if (this.refs eq refs) && (this.boxed == boxed) then this else CaptureAnnotation(refs, boxed)(cls) - override def sameAnnotation(that: Annotation)(using Context): Boolean = that match - case CaptureAnnotation(refs, boxed) => - this.refs == refs && this.boxed == boxed && this.symbol == that.symbol - case _ => false - override def mapWith(tm: TypeMap)(using Context) = val elems = refs.elems.toList val elems1 = elems.mapConserve(tm) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index bc4eb92234eb..7a9e7d558580 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -16,9 +16,14 @@ import config.Feature import collection.mutable import CCState.* import reporting.Message +import CaptureSet.Frozen +/** Attachment key for capturing type trees */ private val Captures: Key[CaptureSet] = Key() +/** Context property to print Fresh.Cap as "fresh" instead of "cap" */ +val PrintFresh: Key[Unit] = Key() + object ccConfig: /** If true, allow mapping capture set variables under captureChecking with maps that are neither @@ -46,8 +51,13 @@ object ccConfig: */ def useSealed(using Context) = Feature.sourceVersion.stable != SourceVersion.`3.5` -end ccConfig + def useFresh(using Context): Boolean = + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.6`) + + def followAliases(using Context): Boolean = + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.6`) +end ccConfig /** Are we at checkCaptures phase? */ def isCaptureChecking(using Context): Boolean = @@ -136,6 +146,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 @@ -184,16 +196,14 @@ 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) case tp: TypeParamRef => tp.derivesFrom(defn.Caps_CapSet) case AnnotatedType(parent, annot) => - (annot.symbol == defn.ReachCapabilityAnnot - || annot.symbol == defn.MaybeCapabilityAnnot - ) && parent.isTrackableRef + defn.capabilityWrapperAnnots.contains(annot.symbol) && parent.isTrackableRef case _ => false @@ -222,6 +232,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 _ => @@ -239,7 +251,7 @@ extension (tp: Type) * the two capture sets are combined. */ def capturing(cs: CaptureSet)(using Context): Type = - if (cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, frozen = true).isOK) + if (cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, Frozen.All).isOK) && !cs.keepAlways then tp else tp match @@ -345,7 +357,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 _ => @@ -379,23 +392,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: @@ -406,16 +428,9 @@ 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) + def hasUseAnnot(using Context): Boolean = tp match + case AnnotatedType(_, ann) => ann.symbol == defn.UseAnnot + case _ => false /** 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 @@ -436,42 +451,43 @@ 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: @@ -479,9 +495,10 @@ extension (tp: Type) 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) @@ -506,6 +523,24 @@ extension (tp: Type) tp case _ => tp + end withReachCaptures + + /** Does this type contain no-flip covariant occurrences of `cap`? */ + def containsCap(using Context): Boolean = + val acc = new TypeAccumulator[Boolean]: + def apply(x: Boolean, t: Type) = + x + || variance > 0 && t.dealiasKeepAnnots.match + case t @ CapturingType(p, cs) if cs.containsCap => + true + case t @ AnnotatedType(parent, ann) => + // Don't traverse annotations, which includes capture sets + this(x, parent) + case Existential(_, _) => + false + case _ => + foldOver(x, t) + acc(false, tp) def level(using Context): Level = tp match @@ -615,6 +650,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 @@ -629,6 +674,19 @@ class CleanupRetains(using Context) extends TypeMap: RetainingType(tp, Nil, byName = annot.symbol == defn.RetainsByNameAnnot) case _ => mapOver(tp) +/** A typemap that follows aliases and keeps their transformed results if + * there is a change. + */ +trait FollowAliasesMap(using Context) extends TypeMap: + var follow = true // Used for debugging so that we can compare results with and w/o following. + def mapFollowingAliases(t: Type): Type = + val t1 = t.dealiasKeepAnnots + if follow && (t1 ne t) then + val t2 = apply(t1) + if t2 ne t1 then t2 + else t + else mapOver(t) + /** An extractor for `caps.reachCapability(ref)`, which is used to express a reach * capability as a tree in a @retains annotation. */ @@ -637,6 +695,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. */ @@ -645,22 +711,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: diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 9bda9102cbb8..e4fbe5770888 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -15,7 +15,9 @@ import compiletime.uninitialized import StdNames.nme /** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs, - * as well as two kinds of AnnotatedTypes representing reach and maybe capabilities. + * as well as three kinds of AnnotatedTypes representing readOnly, reach, and maybe capabilities. + * If there are several annotations they come with an orderL + * `*` first, `.rd` next, `?` last. */ trait CaptureRef extends TypeProxy, ValueType: private var myCaptureSet: CaptureSet | Null = uninitialized @@ -28,39 +30,78 @@ trait CaptureRef extends TypeProxy, ValueType: final def isTracked(using Context): Boolean = this.isTrackableRef && (isMaxCapability || !captureSetOfInfo.isAlwaysEmpty) - /** Is this a reach reference of the form `x*`? */ - final def isReach(using Context): Boolean = this match - case AnnotatedType(_, annot) => annot.symbol == defn.ReachCapabilityAnnot - case _ => false - /** Is this a maybe reference of the form `x?`? */ - final def isMaybe(using Context): Boolean = this match - case AnnotatedType(_, annot) => annot.symbol == defn.MaybeCapabilityAnnot - case _ => false + final def isMaybe(using Context): Boolean = this ne stripMaybe - final def stripReach(using Context): CaptureRef = - if isReach then - val AnnotatedType(parent: CaptureRef, _) = this: @unchecked - parent - else this + /** Is this a read-only reference of the form `x.rd` or a capture set variable + * with only read-ony references in its upper bound? + */ + final def isReadOnly(using Context): Boolean = this match + case tp: TypeRef => tp.captureSetOfInfo.isReadOnly + case _ => this ne stripReadOnly - final def stripMaybe(using Context): CaptureRef = - if isMaybe then - val AnnotatedType(parent: CaptureRef, _) = this: @unchecked - parent - else this + /** Is this a reach reference of the form `x*`? */ + final def isReach(using Context): Boolean = this ne stripReach + + final def stripMaybe(using Context): CaptureRef = this match + case AnnotatedType(tp1: CaptureRef, annot) if annot.symbol == defn.MaybeCapabilityAnnot => + tp1 + case _ => + this + + final def stripReadOnly(using Context): CaptureRef = this match + case tp @ AnnotatedType(tp1: CaptureRef, annot) => + val sym = annot.symbol + if sym == defn.ReadOnlyCapabilityAnnot then + tp1 + else if sym == defn.MaybeCapabilityAnnot then + tp.derivedAnnotatedType(tp1.stripReadOnly, annot) + else + this + case _ => + this + + final def stripReach(using Context): CaptureRef = this match + case tp @ AnnotatedType(tp1: CaptureRef, annot) => + val sym = annot.symbol + if sym == defn.ReachCapabilityAnnot then + tp1 + else if sym == defn.ReadOnlyCapabilityAnnot || sym == defn.MaybeCapabilityAnnot then + tp.derivedAnnotatedType(tp1.stripReach, annot) + else + this + case _ => + this /** Is this reference the generic root capability `cap` ? */ - final def isRootCapability(using Context): Boolean = this match + final def isCap(using Context): Boolean = this match case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot case _ => false + /** Is this reference a Fresh.Cap instance? */ + final def isFresh(using Context): Boolean = this match + case Fresh.Cap(_) => true + case _ => false + + /** Is this reference the generic root capability `cap` or a Fresh.Cap instance? */ + final def isCapOrFresh(using Context): Boolean = isCap || isFresh + + /** Is this reference one the generic root capabilities `cap` or `cap.rd` ? */ + final def isRootCapability(using Context): Boolean = this match + case ReadOnlyCapability(tp1) => tp1.isCapOrFresh + case _ => isCapOrFresh + /** Is this reference capability that does not derive from another capability ? */ final def isMaxCapability(using Context): Boolean = this match - case tp: TermRef => tp.isRootCapability || tp.info.derivesFrom(defn.Caps_Exists) + case tp: TermRef => tp.isCap || tp.info.derivesFrom(defn.Caps_Exists) case tp: TermParamRef => tp.underlying.derivesFrom(defn.Caps_Exists) + case Fresh.Cap(_) => true + case ReadOnlyCapability(tp1) => tp1.isMaxCapability case _ => false + final def isExclusive(using Context): Boolean = + !isReadOnly && (isMaxCapability || captureSetOfInfo.isExclusive) + // With the support of pathes, we don't need to normalize the `TermRef`s anymore. // /** Normalize reference so that it can be compared with `eq` for equality */ // final def normalizedRef(using Context): CaptureRef = this match @@ -83,7 +124,7 @@ trait CaptureRef extends TypeProxy, ValueType: else myCaptureSet = CaptureSet.Pending val computed = CaptureSet.ofInfo(this) - if !isCaptureChecking || underlying.isProvisional then + if !isCaptureChecking || ctx.mode.is(Mode.IgnoreCaptures) || underlying.isProvisional then myCaptureSet = null else myCaptureSet = computed @@ -93,6 +134,8 @@ trait CaptureRef extends TypeProxy, ValueType: final def invalidateCaches() = myCaptureSetRunId = NoRunId + import CaptureSet.{VarState, FrozenAllState} + /** x subsumes x * x =:= y ==> x subsumes y * x subsumes y ==> x subsumes y.f @@ -105,9 +148,9 @@ trait CaptureRef extends TypeProxy, ValueType: * Y: CapSet^c1...CapSet^c2, x subsumes (CapSet^c2) ==> x subsumes Y * Contains[X, y] ==> X subsumes y * - * TODO: Document cases with more comments. + * TODO: Move to CaptureSet */ - final def subsumes(y: CaptureRef)(using Context): Boolean = + final def subsumes(y: CaptureRef)(using ctx: Context, vs: VarState = FrozenAllState): Boolean = def subsumingRefs(x: Type, y: Type): Boolean = x match case x: CaptureRef => y match @@ -122,9 +165,9 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false (this eq y) - || this.isRootCapability + || maxSubsumes(y, canAddHidden = vs.frozen != CaptureSet.Frozen.None) || y.match - case y: TermRef => + case y: TermRef if !y.isCap => y.prefix.match case ypre: CaptureRef => this.subsumes(ypre) @@ -142,6 +185,7 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false || viaInfo(y.info)(subsumingRefs(this, _)) case MaybeCapability(y1) => this.stripMaybe.subsumes(y1) + case ReadOnlyCapability(y1) => this.stripReadOnly.subsumes(y1) case y: TypeRef if y.derivesFrom(defn.Caps_CapSet) => // The upper and lower bounds don't have to be in the form of `CapSet^{...}`. // They can be other capture set variables, which are bounded by `CapSet`, @@ -168,6 +212,33 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false end subsumes + /** This is a maximal capabaility that subsumes `y` in given context and VarState. + * @param canAddHidden If true we allow maximal capabilties to subsume all other capabilities. + * We add those capabilities to the hidden set if this is Fresh.Cap + * If false we only accept `y` elements that are already in the + * hidden set of this Fresh.Cap. The idea is that in a VarState that + * accepts additions we first run `maxSubsumes` with `canAddHidden = false` + * so that new variables get added to the sets. If that fails, we run + * the test again with canAddHidden = true as a last effort before we + * fail a comparison. + */ + def maxSubsumes(y: CaptureRef, canAddHidden: Boolean)(using ctx: Context, vs: VarState = FrozenAllState): Boolean = + this.match + case Fresh.Cap(hidden) => + if vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) then true + else if !y.stripReadOnly.isCap && hidden.recordElemsState() then + if canAddHidden then + hidden.elems += y + true + else + false + else false + case _ => + this.isCap && canAddHidden + || y.match + case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden) + case _ => false + def assumedContainsOf(x: TypeRef)(using Context): SimpleIdentitySet[CaptureRef] = CaptureSet.assumedContains.getOrElse(x, SimpleIdentitySet.empty) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 1750e98f708a..ba4a209354f0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -14,7 +14,6 @@ import printing.{Showable, Printer} import printing.Texts.* import util.{SimpleIdentitySet, Property} import typer.ErrorReporting.Addenda -import TypeComparer.subsumesExistentially import util.common.alwaysTrue import scala.collection.{mutable, immutable} import CCState.* @@ -83,11 +82,28 @@ sealed abstract class CaptureSet extends Showable: /** Does this capture set contain the root reference `cap` as element? */ final def isUniversal(using Context) = + elems.exists(_.isCap) + + /** Does this capture set contain the root reference `cap` as element? */ + final def isUniversalOrFresh(using Context) = + elems.exists(_.isCapOrFresh) + + /** Does this capture set contain a root reference `cap` or `cap.rd` as element? */ + final def containsRootCapability(using Context) = elems.exists(_.isRootCapability) + final def containsCap(using Context) = + elems.exists(_.stripReadOnly.isCap) + final def isUnboxable(using Context) = elems.exists(elem => elem.isRootCapability || Existential.isExistentialVar(elem)) + final def isReadOnly(using Context): Boolean = + elems.forall(_.isReadOnly) + + final def isExclusive(using Context): Boolean = + elems.exists(_.isExclusive) + final def keepAlways: Boolean = this.isInstanceOf[EmptyWithProvenance] /** Try to include an element in this capture set. @@ -126,7 +142,7 @@ sealed abstract class CaptureSet extends Showable: * capture set. */ protected final def addNewElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if elem.isMaxCapability || summon[VarState] == FrozenState then + if elem.isMaxCapability || summon[VarState].isInstanceOf[FrozenVarState] then addThisElem(elem) else addThisElem(elem).orElse: @@ -146,6 +162,11 @@ sealed abstract class CaptureSet extends Showable: */ protected def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult + protected def addHiddenElem(elem: CaptureRef)(using ctx: Context, vs: VarState): CompareResult = + if elems.exists(_.maxSubsumes(elem, canAddHidden = true)) + then CompareResult.OK + else CompareResult.Fail(this :: Nil) + /** If this is a variable, add `cs` as a dependent set */ protected def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult @@ -157,16 +178,30 @@ sealed abstract class CaptureSet extends Showable: /** {x} <:< this where <:< is subcapturing, but treating all variables * as frozen. */ - def accountsFor(x: CaptureRef)(using Context): Boolean = + def accountsFor(x: CaptureRef)(using ctx: Context, vs: VarState = FrozenAllState): Boolean = + + /** Like `refs.exists(p)`, but testing fresh cap instances in refs last */ + def existsElem(refs: SimpleIdentitySet[CaptureRef], p: CaptureRef => Boolean): Boolean = + refs.exists: + case Fresh.Cap(_) => false + case elem => p(elem) + || + refs.exists: + case elem @ Fresh.Cap(_) => p(elem) + case elem => false + def debugInfo(using Context) = i"$this accountsFor $x, which has capture set ${x.captureSetOfInfo}" + def test(using Context) = reporting.trace(debugInfo): - elems.exists(_.subsumes(x)) + existsElem(elems, _.subsumes(x)) || !x.isMaxCapability && !x.derivesFrom(defn.Caps_CapSet) - && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK + && x.captureSetOfInfo.subCaptures(this, Frozen.All).isOK + comparer match case comparer: ExplainingTypeComparer => comparer.traceIndented(debugInfo)(test) case _ => test + end accountsFor /** A more optimistic version of accountsFor, which does not take variable supersets * of the `x` reference into account. A set might account for `x` if it accounts @@ -176,14 +211,13 @@ sealed abstract class CaptureSet extends Showable: * root capability `cap`. */ def mightAccountFor(x: CaptureRef)(using Context): Boolean = - reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true) { - elems.exists(_.subsumes(x)) + reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true): + elems.exists(_.subsumes(x)(using ctx, FrozenVarState())) || !x.isMaxCapability && { val elems = x.captureSetOfInfo.elems !elems.isEmpty && elems.forall(mightAccountFor) } - } /** A more optimistic version of subCaptures used to choose one of two typing rules * for selections and applications. `cs1 mightSubcapture cs2` if `cs2` might account for @@ -199,8 +233,12 @@ sealed abstract class CaptureSet extends Showable: * be added when making this test. An attempt to add either * will result in failure. */ - final def subCaptures(that: CaptureSet, frozen: Boolean)(using Context): CompareResult = - subCaptures(that)(using ctx, if frozen then FrozenState else VarState()) + final def subCaptures(that: CaptureSet, frozen: Frozen)(using Context): CompareResult = + val state = frozen match + case Frozen.None => VarState() + case Frozen.Vars => FrozenVarState() + case Frozen.All => FrozenAllState + subCaptures(that)(using ctx, state) /** The subcapturing test, using a given VarState */ private def subCaptures(that: CaptureSet)(using Context, VarState): CompareResult = @@ -217,16 +255,16 @@ sealed abstract class CaptureSet extends Showable: * in a frozen state. */ def =:= (that: CaptureSet)(using Context): Boolean = - this.subCaptures(that, frozen = true).isOK - && that.subCaptures(this, frozen = true).isOK + this.subCaptures(that, Frozen.All).isOK + && that.subCaptures(this, Frozen.All).isOK /** The smallest capture set (via <:<) that is a superset of both * `this` and `that` */ def ++ (that: CaptureSet)(using Context): CaptureSet = - if this.subCaptures(that, frozen = true).isOK then + if this.subCaptures(that, Frozen.All).isOK then if that.isAlwaysEmpty && this.keepAlways then this else that - else if that.subCaptures(this, frozen = true).isOK then this + else if that.subCaptures(this, Frozen.All).isOK then this else if this.isConst && that.isConst then Const(this.elems ++ that.elems) else Union(this, that) @@ -238,8 +276,8 @@ sealed abstract class CaptureSet extends Showable: /** The largest capture set (via <:<) that is a subset of both `this` and `that` */ def **(that: CaptureSet)(using Context): CaptureSet = - if this.subCaptures(that, frozen = true).isOK then this - else if that.subCaptures(this, frozen = true).isOK then that + if this.subCaptures(that, Frozen.Vars).isOK then this + else if that.subCaptures(this, Frozen.Vars).isOK then that else if this.isConst && that.isConst then Const(elemIntersection(this, that)) else Intersection(this, that) @@ -310,6 +348,8 @@ sealed abstract class CaptureSet extends Showable: def maybe(using Context): CaptureSet = map(MaybeMap()) + def readOnly(using Context): CaptureSet = map(ReadOnlyMap()) + /** Invoke handler if this set has (or later aquires) the root capability `cap` */ def disallowRootCapability(handler: () => Context ?=> Unit)(using Context): this.type = if isUnboxable then handler() @@ -354,6 +394,11 @@ sealed abstract class CaptureSet extends Showable: override def toText(printer: Printer): Text = printer.toTextCaptureSet(this) ~~ description + /** Apply function `f` to the elements. Typcially used for printing. + * Overridden in HiddenSet so that we don't run into infinite recursions + */ + def processElems[T](f: Refs => T): T = f(elems) + object CaptureSet: type Refs = SimpleIdentitySet[CaptureRef] type Vars = SimpleIdentitySet[Var] @@ -364,7 +409,7 @@ object CaptureSet: /** If set to `true`, capture stack traces that tell us where sets are created */ private final val debugSets = false - private val emptySet = SimpleIdentitySet.empty + val emptySet = SimpleIdentitySet.empty /** The empty capture set `{}` */ val empty: CaptureSet.Const = Const(emptySet) @@ -373,6 +418,13 @@ object CaptureSet: def universal(using Context): CaptureSet = defn.captureRoot.termRef.singletonCaptureSet + def fresh(owner: Symbol = NoSymbol)(using Context): CaptureSet = + Fresh.Cap(owner).singletonCaptureSet + + /** The shared capture set `{cap.rd}` */ + def shared(using Context): CaptureSet = + defn.captureRoot.termRef.readOnly.singletonCaptureSet + /** Used as a recursion brake */ @sharable private[dotc] val Pending = Const(SimpleIdentitySet.empty) @@ -389,7 +441,7 @@ object CaptureSet: def isAlwaysEmpty = elems.isEmpty def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = - CompareResult.Fail(this :: Nil) + addHiddenElem(elem) def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK @@ -419,7 +471,7 @@ object CaptureSet: object Fluid extends Const(emptySet): override def isAlwaysEmpty = false override def addThisElem(elem: CaptureRef)(using Context, VarState) = CompareResult.OK - override def accountsFor(x: CaptureRef)(using Context): Boolean = true + override def accountsFor(x: CaptureRef)(using Context, VarState): Boolean = true override def mightAccountFor(x: CaptureRef)(using Context): Boolean = true override def toString = "" end Fluid @@ -463,7 +515,7 @@ object CaptureSet: /** Record current elements in given VarState provided it does not yet * contain an entry for this variable. */ - private def recordElemsState()(using VarState): Boolean = + protected[CaptureSet] def recordElemsState()(using VarState): Boolean = varState.getElems(this) match case None => varState.putElems(this, elems) case _ => true @@ -485,16 +537,16 @@ object CaptureSet: deps = state.deps(this) final def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if isConst // Fail if variable is solved, - || !recordElemsState() // or given VarState is frozen, - || Existential.isBadExistential(elem) // or `elem` is an out-of-scope existential, - then + if isConst || !recordElemsState() then // Fail if variable is solved or given VarState is frozen + addHiddenElem(elem) + else if Existential.isBadExistential(elem) then // Fail if `elem` is an out-of-scope existential CompareResult.Fail(this :: Nil) else if !levelOK(elem) then CompareResult.LevelError(this, elem) // or `elem` is not visible at the level of the set. else - //if id == 34 then assert(!elem.isUniversalRootCapability) + // id == 108 then assert(false, i"trying to add $elem to $this") assert(elem.isTrackableRef, elem) + assert(!this.isInstanceOf[HiddenSet] || summon[VarState] == FrozenAllState, summon[VarState]) elems += elem if elem.isRootCapability then rootAddedHandler() @@ -508,8 +560,13 @@ object CaptureSet: res.addToTrace(this) private def levelOK(elem: CaptureRef)(using Context): Boolean = - if elem.isRootCapability || Existential.isExistentialVar(elem) then + if elem.isRootCapability then !noUniversal + else if Existential.isExistentialVar(elem) then + !noUniversal + && !TypeComparer.isOpenedExistential(elem) + // Opened existentials on the left cannot be added to nested capture sets on the right + // of a comparison. Test case is open-existential.scala. else elem match case elem: TermRef if level.isDefined => elem.prefix match @@ -521,6 +578,8 @@ object CaptureSet: elem.cls.ccLevel.nextInner <= level case ReachCapability(elem1) => levelOK(elem1) + case ReadOnlyCapability(elem1) => + levelOK(elem1) case MaybeCapability(elem1) => levelOK(elem1) case _ => @@ -553,8 +612,10 @@ object CaptureSet: final def upperApprox(origin: CaptureSet)(using Context): CaptureSet = if isConst then this - else if elems.exists(_.isRootCapability) || computingApprox then + else if isUniversal || computingApprox then universal + else if containsCap && isReadOnly then + shared else computingApprox = true try @@ -577,7 +638,7 @@ object CaptureSet: */ def solve()(using Context): Unit = if !isConst then - val approx = upperApprox(empty) + val approx = upperApprox(empty).map(Fresh.FromCap(NoSymbol).inverse) .showing(i"solve $this = $result", capt) //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") val newElems = approx.elems -- elems @@ -865,6 +926,25 @@ object CaptureSet: def elemIntersection(cs1: CaptureSet, cs2: CaptureSet)(using Context): Refs = cs1.elems.filter(cs2.mightAccountFor) ++ cs2.elems.filter(cs1.mightAccountFor) + /** A capture set variable used to record the references hidden by a Fresh.Cap instance */ + class HiddenSet(initialHidden: Refs = emptySet)(using @constructorOnly ictx: Context) + extends Var(initialElems = initialHidden): + override def recordElemsState()(using VarState): Boolean = + varState.getElems(this) match + case None => varState.putHidden(this, elems) + case _ => true + + /** Apply function `f` to `elems` while setting `elems` to empty for the + * duration. This is used to escape infinite recursions if two Frash.Caps + * refer to each other in their hidden sets. + */ + override def processElems[T](f: Refs => T): T = + val savedElems = elems + elems = emptySet + try f(savedElems) + finally elems = savedElems + end HiddenSet + /** Extrapolate tm(r) according to `variance`. Let r1 be the result of tm(r). * - If r1 is a tracked CaptureRef, return {r1} * - If r1 has an empty capture set, return {} @@ -958,13 +1038,21 @@ object CaptureSet: case _ => this end CompareResult + /** An enum indicating a Frozen degree for subCapturing tests */ + enum Frozen: + case None // operations are performed in a regular VarState + case Vars // operations are performed in a FrozenVarState + case All // operations are performed in FrozenAllState + /** A VarState serves as a snapshot mechanism that can undo * additions of elements or super sets if an operation fails */ class VarState: + def frozen: Frozen = Frozen.None + /** A map from captureset variables to their elements at the time of the snapshot. */ - private val elemsMap: util.EqHashMap[Var, Refs] = new util.EqHashMap + protected val elemsMap: util.EqHashMap[Var, Refs] = new util.EqHashMap /** A map from captureset variables to their dependent sets at the time of the snapshot. */ private val depsMap: util.EqHashMap[Var, Deps] = new util.EqHashMap @@ -976,8 +1064,7 @@ object CaptureSet: def getElems(v: Var): Option[Refs] = elemsMap.get(v) /** Record elements, return whether this was allowed. - * By default, recording is allowed but the special state FrozenState - * overrides this. + * By default, recording is allowed in regular both not in frozen states. */ def putElems(v: Var, elems: Refs): Boolean = { elemsMap(v) = elems; true } @@ -988,26 +1075,50 @@ object CaptureSet: def getDeps(v: Var): Option[Deps] = depsMap.get(v) /** Record dependent sets, return whether this was allowed. - * By default, recording is allowed but the special state FrozenState - * overrides this. + * By default, recording is allowed in regular both not in frozen states. */ def putDeps(v: Var, deps: Deps): Boolean = { depsMap(v) = deps; true } + /** Record hidden elements in elemsMap of hidden set `v`, + * return whether this was allowed. By default, recording is allowed + * but the special state FrozenAllState overrides this. + */ + def putHidden(v: HiddenSet, elems: Refs): Boolean = { elemsMap(v) = elems; true } + /** Roll back global state to what was recorded in this VarState */ def rollBack(): Unit = elemsMap.keysIterator.foreach(_.resetElems()(using this)) depsMap.keysIterator.foreach(_.resetDeps()(using this)) + + private var seen: util.EqHashSet[CaptureRef] = new util.EqHashSet + + /** Run test `pred` unless `ref` was seen in an enclosing `ifNotSeen` operation */ + def ifNotSeen(ref: CaptureRef)(pred: => Boolean): Boolean = + if seen.add(ref) then + try pred finally seen -= ref + else false end VarState - /** A special state that does not allow to record elements or dependent sets. + /** A class for states that do not allow to record elements or dependent sets. * In effect this means that no new elements or dependent sets can be added - * in this state (since the previous state cannot be recorded in a snapshot) + * in these states (since the previous state cannot be recorded in a snapshot) + * On the other hand, these states do allow by default Fresh.Cap instances to + * subsume arbitary types, which are then recorded in their hidden sets. */ - @sharable - object FrozenState extends VarState: + class FrozenVarState extends VarState: + override def frozen = Frozen.Vars override def putElems(v: Var, refs: Refs) = false override def putDeps(v: Var, deps: Deps) = false - override def rollBack(): Unit = () + override def putHidden(v: HiddenSet, elems: Refs): Boolean = { elemsMap(v) = elems; true } + + @sharable + /** A frozen state that allows a Fresh.Cap instancce to subsume a + * reference `r` only if `r` is already present in the hidden set of the instance. + * No new references can be added. + */ + object FrozenAllState extends FrozenVarState: + override def frozen = Frozen.All + override def putHidden(v: HiddenSet, elems: Refs): Boolean = false @sharable /** A special state that turns off recording of elements. Used only @@ -1021,25 +1132,29 @@ object CaptureSet: /** The current VarState, as passed by the implicit context */ def varState(using state: VarState): VarState = state - /** Maps `x` to `x?` */ - private class MaybeMap(using Context) extends BiTypeMap: + /** A template for maps on capabilities where f(c) <: c and f(f(c)) = c */ + private abstract class NarrowingCapabilityMap(using Context) extends BiTypeMap: + def mapRef(ref: CaptureRef): CaptureRef def apply(t: Type) = t match - case t: CaptureRef if t.isTrackableRef => t.maybe + case t: CaptureRef if t.isTrackableRef => mapRef(t) case _ => mapOver(t) - override def toString = "Maybe" - lazy val inverse = new BiTypeMap: + def apply(t: Type) = t // since f(c) <: c, this is the best inverse + def inverse = NarrowingCapabilityMap.this + override def toString = NarrowingCapabilityMap.this.toString ++ ".inverse" + end NarrowingCapabilityMap - def apply(t: Type) = t match - case t: CaptureRef if t.isMaybe => t.stripMaybe - case t => mapOver(t) - - def inverse = MaybeMap.this + /** Maps `x` to `x?` */ + private class MaybeMap(using Context) extends NarrowingCapabilityMap: + def mapRef(ref: CaptureRef): CaptureRef = ref.maybe + override def toString = "Maybe" - override def toString = "Maybe.inverse" - end MaybeMap + /** Maps `x` to `x.rd` */ + private class ReadOnlyMap(using Context) extends NarrowingCapabilityMap: + def mapRef(ref: CaptureRef): CaptureRef = ref.readOnly + override def toString = "ReadOnly" /* Not needed: def ofClass(cinfo: ClassInfo, argTypes: List[Type])(using Context): CaptureSet = @@ -1065,13 +1180,14 @@ object CaptureSet: /** The capture set of the type underlying CaptureRef */ def ofInfo(ref: CaptureRef)(using Context): CaptureSet = ref match - case ref: (TermRef | TermParamRef) if ref.isMaxCapability => - if ref.isTrackableRef then ref.singletonCaptureSet - else CaptureSet.universal case ReachCapability(ref1) => ref1.widen.deepCaptureSet(includeTypevars = true) .showing(i"Deep capture set of $ref: ${ref1.widen} = ${result}", capt) - case _ => ofType(ref.underlying, followResult = true) + case ReadOnlyCapability(ref1) => + ref1.captureSetOfInfo.map(ReadOnlyMap()) + case _ => + if ref.isMaxCapability then ref.singletonCaptureSet + else ofType(ref.underlying, followResult = true) /** Capture set of a type */ def ofType(tp: Type, followResult: Boolean)(using Context): CaptureSet = @@ -1138,7 +1254,7 @@ object CaptureSet: case t: TypeRef if t.symbol.isAbstractOrParamType && !seen.contains(t.symbol) => seen += t.symbol val upper = t.info.bounds.hi - if includeTypevars && upper.isExactlyAny then CaptureSet.universal + if includeTypevars && upper.isExactlyAny then CaptureSet.fresh(t.symbol) else this(cs, upper) case t @ FunctionOrMethod(args, res @ Existential(_, _)) if args.forall(_.isAlwaysPure) => @@ -1193,9 +1309,10 @@ object CaptureSet: for CompareResult.LevelError(cs, ref) <- ccState.levelError.toList yield ccState.levelError = None if ref.isRootCapability then + def capStr = if ref.isReadOnly then "cap.rd" else "cap" i""" | - |Note that the universal capability `cap` + |Note that the universal capability `$capStr` |cannot be included in capture set $cs""" else val levelStr = ref match diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 830d9ad0a4d4..50b2fd94d1d7 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -18,11 +18,12 @@ import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property} import transform.{Recheck, PreRecheck, CapturedVars} import Recheck.* import scala.collection.mutable -import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult} +import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult, Frozen} import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} import reporting.{trace, Message, OverrideError} +import Existential.derivedExistentialType /** The capture checker */ object CheckCaptures: @@ -150,6 +151,7 @@ object CheckCaptures: |is must be a type parameter or abstract type with a caps.CapSet upper bound.""", elem.srcPos) case ReachCapabilityApply(arg) => check(arg, elem.srcPos) + case ReadOnlyCapabilityApply(arg) => check(arg, elem.srcPos) case _ => check(elem, elem.srcPos) /** Under the sealed policy, report an error if some part of `tp` contains the @@ -237,6 +239,19 @@ object CheckCaptures: /** Was a new type installed for this tree? */ def hasNuType: Boolean + + /** Is this tree passed to a parameter or assigned to a value with a type + * that contains cap in no-flip covariant position, which will necessite + * a separation check? + */ + def needsSepCheck: Boolean + + /** If a tree is an argument for which needsSepCheck is true, + * the actual type of the argument before it was widened to formal. + * The nuType of this argument is the formal parameter type in this case, + * but for error diagnosis it's important to know what the actual type was. + */ + def actualType: Type end CheckerAPI class CheckCaptures extends Recheck, SymTransformer: @@ -277,6 +292,15 @@ class CheckCaptures extends Recheck, SymTransformer: */ private val todoAtPostCheck = new mutable.ListBuffer[() => Unit] + /** Maps trees that will need a separation check because they contain cap + * to the actual, non-widened type. + */ + private val sepCheckable = util.EqHashMap[Tree, Type]() + + extension [T <: Tree](tree: T) + def needsSepCheck: Boolean = sepCheckable.contains(tree) + def actualType: Type = sepCheckable.getOrElse(tree, NoType) + /** Instantiate capture set variables appearing contra-variantly to their * upper approximation. */ @@ -306,32 +330,33 @@ class CheckCaptures extends Recheck, SymTransformer: /** Assert subcapturing `cs1 <: cs2` (available for debugging, otherwise unused) */ def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = - assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2") + assert(cs1.subCaptures(cs2, Frozen.None).isOK, i"$cs1 is not a subset of $cs2") /** If `res` is not CompareResult.OK, report an error */ - def checkOK(res: CompareResult, prefix: => String, pos: SrcPos, provenance: => String = "")(using Context): Unit = + def checkOK(res: CompareResult, prefix: => String, added: CaptureRef | CaptureSet, pos: SrcPos, provenance: => String = "")(using Context): Unit = if !res.isOK then - def toAdd: String = CaptureSet.levelErrors.toAdd.mkString - def descr: String = - val d = res.blocking.description - if d.isEmpty then provenance else "" - report.error(em"$prefix included in the allowed capture set ${res.blocking}$descr$toAdd", pos) + inContext(Fresh.printContext(added, res.blocking)): + def toAdd: String = CaptureSet.levelErrors.toAdd.mkString + def descr: String = + val d = res.blocking.description + if d.isEmpty then provenance else "" + report.error(em"$prefix included in the allowed capture set ${res.blocking}$descr$toAdd", pos) /** Check subcapturing `{elem} <: cs`, report error on failure */ def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos, provenance: => String = "")(using Context) = checkOK( - elem.singletonCaptureSet.subCaptures(cs, frozen = false), + elem.singletonCaptureSet.subCaptures(cs, Frozen.None), i"$elem cannot be referenced here; it is not", - pos, provenance) + elem, pos, provenance) /** Check subcapturing `cs1 <: cs2`, report error on failure */ def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos, provenance: => String = "", cs1description: String = "")(using Context) = checkOK( - cs1.subCaptures(cs2, frozen = false), + cs1.subCaptures(cs2, Frozen.None), if cs1.elems.size == 1 then i"reference ${cs1.elems.toList.head}$cs1description is not" else i"references $cs1$cs1description are not all", - pos, provenance) + cs1, pos, provenance) /** If `sym` is a class or method nested inside a term, a capture set variable representing * the captured variables of the environment associated with `sym`. @@ -381,7 +406,7 @@ class CheckCaptures extends Recheck, SymTransformer: def markFree(sym: Symbol, pos: SrcPos)(using Context): Unit = markFree(sym, sym.termRef, pos) - def markFree(sym: Symbol, ref: TermRef, pos: SrcPos)(using Context): Unit = + def markFree(sym: Symbol, ref: CaptureRef, pos: SrcPos)(using Context): Unit = if sym.exists && ref.isTracked then markFree(ref.captureSet, pos) /** Make sure the (projected) `cs` is a subset of the capture sets of all enclosing @@ -484,7 +509,8 @@ class CheckCaptures extends Recheck, SymTransformer: def includeCallCaptures(sym: Symbol, resType: Type, pos: SrcPos)(using Context): Unit = resType match case _: MethodOrPoly => // wait until method is fully applied case _ => - if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos) + if sym.exists then + if curEnv.isOpen then markFree(capturedVars(sym), pos) /** Under the sealed policy, disallow the root capability in type arguments. * Type arguments come either from a TypeApply node or from an AppliedType @@ -530,13 +556,18 @@ class CheckCaptures extends Recheck, SymTransformer: // expected type `pt`. // Example: If we have `x` and the expected type says we select that with `.a.b`, // we charge `x.a.b` instead of `x`. - def addSelects(ref: TermRef, pt: Type): TermRef = pt match + def addSelects(ref: TermRef, pt: Type): CaptureRef = pt match case pt: PathSelectionProto if ref.isTracked => - // if `ref` is not tracked then the selection could not give anything new - // class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters. - addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt) + if pt.sym.isReadOnlyMethod then + ref.readOnly + else + // if `ref` is not tracked then the selection could not give anything new + // class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters. + addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt) case _ => ref - val pathRef = addSelects(sym.termRef, pt) + var pathRef: CaptureRef = addSelects(sym.termRef, pt) + if pathRef.derivesFrom(defn.Caps_Mutable) && pt.isValueType && !pt.isMutableType then + pathRef = pathRef.readOnly markFree(sym, pathRef, tree.srcPos) super.recheckIdent(tree, pt) @@ -545,7 +576,9 @@ class CheckCaptures extends Recheck, SymTransformer: */ override def selectionProto(tree: Select, pt: Type)(using Context): Type = val sym = tree.symbol - if !sym.isOneOf(UnstableValueFlags) && !sym.isStatic then PathSelectionProto(sym, pt) + if !sym.isOneOf(UnstableValueFlags) && !sym.isStatic + || sym.isReadOnlyMethod + then PathSelectionProto(sym, pt) else super.selectionProto(tree, pt) /** A specialized implementation of the selection rule. @@ -573,6 +606,12 @@ class CheckCaptures extends Recheck, SymTransformer: } case _ => denot + if tree.symbol.isUpdateMethod && !qualType.captureSet.isExclusive then + report.error( + em"""cannot call update ${tree.symbol} from $qualType, + |since its capture set ${qualType.captureSet} is read-only""", + tree.srcPos) + val selType = recheckSelection(tree, qualType, name, disambiguate) val selWiden = selType.widen @@ -620,11 +659,11 @@ class CheckCaptures extends Recheck, SymTransformer: val meth = tree.fun.symbol if meth == defn.Caps_unsafeAssumePure then val arg :: Nil = tree.args: @unchecked - val argType0 = recheck(arg, pt.capturing(CaptureSet.universal)) + val argType0 = recheck(arg, pt.stripCapturing.capturing(CaptureSet.universal)) val argType = if argType0.captureSet.isAlwaysEmpty then argType0 else argType0.widen.stripCapturing - capt.println(i"rechecking $arg with $pt: $argType") + capt.println(i"rechecking unsafeAssumePure of $arg with $pt: $argType") super.recheckFinish(argType, tree, pt) else val res = super.recheckApply(tree, pt) @@ -635,13 +674,16 @@ class CheckCaptures extends Recheck, SymTransformer: * charge the deep capture set of the actual argument to the environment. */ protected override def recheckArg(arg: Tree, formal: Type)(using Context): Type = - val argType = recheck(arg, formal) - formal match - case AnnotatedType(formal1, ann) if ann.symbol == defn.UseAnnot => - // The UseAnnot is added to `formal` by `prepareFunction` - capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") - markFree(argType.deepCaptureSet, arg.srcPos) - case _ => + val freshenedFormal = Fresh.fromCap(formal) + val argType = recheck(arg, freshenedFormal) + .showing(i"recheck arg $arg vs $freshenedFormal", capt) + if formal.hasUseAnnot then + // The @use annotation is added to `formal` by `prepareFunction` + capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") + markFree(argType.deepCaptureSet, arg.srcPos) + if formal.containsCap then + arg.updNuType(freshenedFormal) + sepCheckable(arg) = argType argType /** Map existential captures in result to `cap` and implement the following @@ -671,9 +713,7 @@ class CheckCaptures extends Recheck, SymTransformer: val qualCaptures = qualType.captureSet val argCaptures = for (argType, formal) <- argTypes.lazyZip(funType.paramInfos) yield - formal match - case AnnotatedType(_, ann) if ann.symbol == defn.UseAnnot => argType.deepCaptureSet - case _ => argType.captureSet + if formal.hasUseAnnot then argType.deepCaptureSet else argType.captureSet appType match case appType @ CapturingType(appType1, refs) if qualType.exists @@ -731,7 +771,9 @@ class CheckCaptures extends Recheck, SymTransformer: def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = var refined: Type = core var allCaptures: CaptureSet = - if core.derivesFromCapability then defn.universalCSImpliedByCapability else initCs + if core.derivesFromMutable then CaptureSet.fresh() + else if core.derivesFromCapability then initCs ++ Fresh.Cap().readOnly.singletonCaptureSet + else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol if !getter.is(Private) && getter.hasTrackedParts then @@ -751,6 +793,8 @@ class CheckCaptures extends Recheck, SymTransformer: // can happen for curried constructors if instantiate of a previous step // added capture set to result. augmentConstructorType(parent, initCs ++ refs) + case core @ Existential(boundVar, core1) => + core.derivedExistentialType(augmentConstructorType(core1, initCs)) case _ => val (refined, cs) = addParamArgRefinements(core, initCs) refined.capturing(cs) @@ -1105,6 +1149,7 @@ class CheckCaptures extends Recheck, SymTransformer: if tree.isTerm && !pt.isBoxedCapturing && pt != LhsProto then markFree(res.boxedCaptureSet, tree.srcPos) res + end recheck /** Under the old unsealed policy: check that cap is ot unboxed */ override def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = @@ -1183,10 +1228,11 @@ class CheckCaptures extends Recheck, SymTransformer: actualBoxed else capt.println(i"conforms failed for ${tree}: $actual vs $expected") - err.typeMismatch(tree.withType(actualBoxed), expected1, - addApproxAddenda( - addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors), - expected1)) + inContext(Fresh.printContext(actualBoxed, expected1)): + err.typeMismatch(tree.withType(actualBoxed), expected1, + addApproxAddenda( + addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors), + expected1)) actual end checkConformsExpr @@ -1352,7 +1398,7 @@ class CheckCaptures extends Recheck, SymTransformer: val cs = actual.captureSet if covariant then cs ++ leaked else - if !leaked.subCaptures(cs, frozen = false).isOK then + if !leaked.subCaptures(cs, Frozen.None).isOK then report.error( em"""$expected cannot be box-converted to ${actual.capturing(leaked)} |since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", pos) @@ -1427,6 +1473,25 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => widened case _ => widened + /** If actual is a capturing type T^C extending Mutable, and expected is an + * unboxed non-singleton value type not extending mutable, narrow the capture + * set `C` to `ro(C)`. + * The unboxed condition ensures that the expected is not a type variable + * that's upper bounded by a read-only type. In this case it would not be sound + * to narrow to the read-only set, since that set can be propagated + * by the type variable instantiatiin. + */ + private def improveReadOnly(actual: Type, expected: Type)(using Context): Type = actual match + case actual @ CapturingType(parent, refs) + if parent.derivesFrom(defn.Caps_Mutable) + && expected.isValueType + && !expected.isMutableType + && !expected.isSingleton + && !expected.isBoxedCapturing => + actual.derivedCapturingType(parent, refs.readOnly) + case _ => + actual + /** Adapt `actual` type to `expected` type. This involves: * - narrow toplevel captures of `x`'s underlying type to `{x}` according to CC's VAR rule * - narrow nested captures of `x`'s underlying type to `{x*}` @@ -1436,12 +1501,14 @@ class CheckCaptures extends Recheck, SymTransformer: if expected == LhsProto || expected.isSingleton && actual.isSingleton then actual else - val widened = improveCaptures(actual.widen.dealiasKeepAnnots, actual) + val improvedVAR = improveCaptures(actual.widen.dealiasKeepAnnots, actual) + val improvedRO = improveReadOnly(improvedVAR, expected) val adapted = adaptBoxed( - widened.withReachCaptures(actual), expected, pos, + improvedRO.withReachCaptures(actual), expected, pos, covariant = true, alwaysConst = false, boxErrors) - if adapted eq widened then actual - else adapted.showing(i"adapt boxed $actual vs $expected = $adapted", capt) + if adapted eq improvedVAR // no .rd improvement, no box-adaptation + then actual // might as well use actual instead of improved widened + else adapted.showing(i"adapt $actual vs $expected = $adapted", capt) end adapt // ---- Unit-level rechecking ------------------------------------------- @@ -1484,18 +1551,16 @@ class CheckCaptures extends Recheck, SymTransformer: /** Check that overrides don't change the @use status of their parameters */ override def additionalChecks(member: Symbol, other: Symbol)(using Context): Unit = + def fail(msg: String) = + report.error( + OverrideError(msg, self, member, other, self.memberInfo(member), self.memberInfo(other)), + if member.owner == clazz then member.srcPos else clazz.srcPos) for (params1, params2) <- member.rawParamss.lazyZip(other.rawParamss) (param1, param2) <- params1.lazyZip(params2) do if param1.hasAnnotation(defn.UseAnnot) != param2.hasAnnotation(defn.UseAnnot) then - report.error( - OverrideError( - i"has a parameter ${param1.name} with different @use status than the corresponding parameter in the overridden definition", - self, member, other, self.memberInfo(member), self.memberInfo(other) - ), - if member.owner == clazz then member.srcPos else clazz.srcPos - ) + fail(i"has a parameter ${param1.name} with different @use status than the corresponding parameter in the overridden definition") end OverridingPairsCheckerCC def traverse(t: Tree)(using Context) = @@ -1526,7 +1591,7 @@ class CheckCaptures extends Recheck, SymTransformer: def traverse(tree: Tree)(using Context) = tree match case id: Ident => val sym = id.symbol - if sym.is(Mutable, butNot = Method) && sym.owner.isTerm then + if sym.isMutableVar && sym.owner.isTerm then val enclMeth = ctx.owner.enclosingMethod if sym.enclosingMethod != enclMeth then capturedBy(sym) = enclMeth @@ -1601,7 +1666,7 @@ class CheckCaptures extends Recheck, SymTransformer: selfType match case CapturingType(_, refs: CaptureSet.Var) if !root.isEffectivelySealed - && !refs.elems.exists(_.isRootCapability) + && !refs.isUniversal && !root.matchesExplicitRefsInBaseClass(refs) => // Forbid inferred self types unless they are already implied by an explicit @@ -1656,7 +1721,7 @@ class CheckCaptures extends Recheck, SymTransformer: val widened = ref.captureSetOfInfo val added = widened.filter(isAllowed(_)) capt.println(i"heal $ref in $cs by widening to $added") - if !added.subCaptures(cs, frozen = false).isOK then + if !added.subCaptures(cs, Frozen.None).isOK then val location = if meth.exists then i" of ${meth.showLocated}" else "" val paramInfo = if ref.paramName.info.kind.isInstanceOf[UniqueNameKind] @@ -1743,6 +1808,7 @@ class CheckCaptures extends Recheck, SymTransformer: end checker checker.traverse(unit)(using ctx.withOwner(defn.RootClass)) + if ccConfig.useFresh then SepChecker(this).traverse(unit) if !ctx.reporter.errorsReported then // We dont report errors here if previous errors were reported, because other // errors often result in bad applied types, but flagging these bad types gives diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index ea979e0b9f7f..39f6fcf14fd9 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -4,7 +4,6 @@ package cc import core.* import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* -import CaptureSet.IdempotentCaptRefMap import StdNames.nme import ast.tpd.* import Decorators.* @@ -243,18 +242,10 @@ object Existential: case _ => core - /** Map top-level existentials to `cap`. Do the same for existentials - * in function results if all preceding arguments are known to be always pure. - */ + /** Map top-level existentials to `Fresh.Cap`. */ def toCap(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => - val transformed = unpacked.substParam(boundVar, defn.captureRoot.termRef) - transformed match - case FunctionOrMethod(args, res @ Existential(_, _)) - if args.forall(_.isAlwaysPure) => - transformed.derivedFunctionOrMethod(args, toCap(res)) - case _ => - transformed + unpacked.substParam(boundVar, Fresh.Cap()) case tp1 @ CapturingType(parent, refs) => tp1.derivedCapturingType(toCap(parent), refs) case tp1 @ AnnotatedType(parent, ann) => @@ -265,7 +256,7 @@ object Existential: */ def toCapDeeply(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => - toCapDeeply(unpacked.substParam(boundVar, defn.captureRoot.termRef)) + toCapDeeply(unpacked.substParam(boundVar, Fresh.Cap())) case tp1 @ FunctionOrMethod(args, res) => val tp2 = tp1.derivedFunctionOrMethod(args, toCapDeeply(res)) if tp2 ne tp1 then tp2 else tp @@ -282,7 +273,7 @@ object Existential: case AppliedType(tycon, _) => !defn.isFunctionSymbol(tycon.typeSymbol) case _ => false - /** Replace all occurrences of `cap` in parts of this type by an existentially bound + /** Replace all occurrences of `cap` (or fresh) in parts of this type by an existentially bound * variable. If there are such occurrences, or there might be in the future due to embedded * capture set variables, create an existential with the variable wrapping the type. * Stop at function or method types since these have been mapped before. @@ -303,7 +294,7 @@ object Existential: class Wrap(boundVar: TermParamRef) extends CapMap: def apply(t: Type) = t match - case t: TermRef if t.isRootCapability => + case t: CaptureRef if t.isCapOrFresh => // !!! we should map different fresh refs to different existentials if variance > 0 then needsWrap = true boundVar @@ -326,8 +317,9 @@ object Existential: //.showing(i"mapcap $t = $result") lazy val inverse = new BiTypeMap: + lazy val freshCap = Fresh.Cap() def apply(t: Type) = t match - case t: TermParamRef if t eq boundVar => defn.captureRoot.termRef + case t: TermParamRef if t eq boundVar => freshCap case _ => mapOver(t) def inverse = Wrap.this override def toString = "Wrap.inverse" diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala new file mode 100644 index 000000000000..14c4c03e4115 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -0,0 +1,139 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* +import StdNames.nme +import ast.tpd.* +import Decorators.* +import typer.ErrorReporting.errorType +import Names.TermName +import NameKinds.ExistentialBinderName +import NameOps.isImpureFunction +import reporting.Message +import util.SimpleIdentitySet.empty +import CaptureSet.{Refs, emptySet, NarrowingCapabilityMap} +import dotty.tools.dotc.util.SimpleIdentitySet + +/** Handling fresh in CC: + +*/ +object Fresh: + + case class Annot(hidden: CaptureSet.HiddenSet) extends Annotation: + override def symbol(using Context) = defn.FreshCapabilityAnnot + override def tree(using Context) = New(symbol.typeRef, Nil) + override def derivedAnnotation(tree: Tree)(using Context): Annotation = this + + override def hash: Int = hidden.hashCode + override def eql(that: Annotation) = that match + case Annot(hidden) => this.hidden eq hidden + case _ => false + end Annot + + private def ownerToHidden(owner: Symbol, reach: Boolean)(using Context): Refs = + val ref = owner.termRef + if reach then + if ref.isTrackableRef then SimpleIdentitySet(ref.reach) else emptySet + else + if ref.isTracked then SimpleIdentitySet(ref) else emptySet + + object Cap: + + def apply(initialHidden: Refs = emptySet)(using Context): CaptureRef = + if ccConfig.useFresh then + AnnotatedType(defn.captureRoot.termRef, Annot(CaptureSet.HiddenSet(initialHidden))) + else + defn.captureRoot.termRef + + def apply(owner: Symbol, reach: Boolean)(using Context): CaptureRef = + apply(ownerToHidden(owner, reach)) + + def apply(owner: Symbol)(using Context): CaptureRef = + apply(ownerToHidden(owner, reach = false)) + + def unapply(tp: AnnotatedType)(using Context): Option[CaptureSet.HiddenSet] = tp.annot match + case Annot(hidden) => Some(hidden) + case _ => None + end Cap + + class FromCap(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap: + thisMap => + + var reach = false + + private def initHidden = + val ref = owner.termRef + if reach then + if ref.isTrackableRef then SimpleIdentitySet(ref.reach) else emptySet + else + if ref.isTracked then SimpleIdentitySet(ref) else emptySet + + override def apply(t: Type) = + if variance <= 0 then t + else t match + case t: CaptureRef if t.isCap => + Cap(initHidden) + case t @ CapturingType(_, refs) => + val savedReach = reach + if t.isBoxed then reach = true + try mapOver(t) finally reach = savedReach + case t @ AnnotatedType(parent, ann) => + val parent1 = this(parent) + if ann.symbol.isRetains && ann.tree.toCaptureSet.containsCap then + this(CapturingType(parent1, ann.tree.toCaptureSet)) + else + t.derivedAnnotatedType(parent1, ann) + case _ => + mapFollowingAliases(t) + + override def toString = "CapToFresh" + + lazy val inverse: BiTypeMap & FollowAliasesMap = new BiTypeMap with FollowAliasesMap: + def apply(t: Type): Type = t match + case t @ Cap(_) => defn.captureRoot.termRef + case t @ CapturingType(_, refs) => mapOver(t) + case _ => mapFollowingAliases(t) + + def inverse = thisMap + override def toString = thisMap.toString + ".inverse" + + end FromCap + + /** Maps cap to fresh */ + def fromCap(tp: Type, owner: Symbol = NoSymbol)(using Context): Type = + if ccConfig.useFresh then FromCap(owner)(tp) else tp + + /** Maps fresh to cap */ + def toCap(tp: Type)(using Context): Type = + if ccConfig.useFresh then FromCap(NoSymbol).inverse(tp) else tp + + /** If `refs` contains an occurrence of `cap` or `cap.rd`, the current context + * with an added property PrintFresh. This addition causes all occurrences of + * `Fresh.Cap` to be printed as `fresh` instead of `cap`, so that one avoids + * confusion in error messages. + */ + def printContext(refs: (Type | CaptureSet)*)(using Context): Context = + def hasCap = new TypeAccumulator[Boolean]: + def apply(x: Boolean, t: Type) = + x || t.dealiasKeepAnnots.match + case Fresh.Cap(_) => false + case t: TermRef => t.isCap || this(x, t.widen) + case x: ThisType => false + case _ => foldOver(x, t) + def containsFresh(x: Type | CaptureSet): Boolean = x match + case tp: Type => + hasCap(false, tp) + case refs: CaptureSet => + refs.elems.exists(_.stripReadOnly.isCap) + + if refs.exists(containsFresh) then ctx.withProperty(PrintFresh, Some(())) + else ctx + end printContext +end Fresh + + + + + diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala new file mode 100644 index 000000000000..e0a53e3390ed --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -0,0 +1,113 @@ +package dotty.tools +package dotc +package cc +import ast.tpd +import collection.mutable + +import core.* +import Symbols.*, Types.* +import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* +import CaptureSet.{Refs, emptySet} +import config.Printers.capt +import StdNames.nme + +class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: + import tpd.* + import checker.* + + extension (cs: CaptureSet) + def footprint(using Context): CaptureSet = + def recur(elems: CaptureSet.Refs, newElems: List[CaptureRef]): CaptureSet.Refs = newElems match + case newElem :: newElems1 => + val superElems = newElem.captureSetOfInfo.elems.filter: superElem => + !superElem.isMaxCapability && !elems.contains(superElem) + recur(superElems ++ elems, superElems.toList ++ newElems1) + case Nil => elems + val elems: CaptureSet.Refs = cs.elems.filter(!_.isMaxCapability) + CaptureSet(recur(elems, elems.toList)) + + def overlapWith(other: CaptureSet)(using Context): CaptureSet.Refs = + val refs1 = cs.elems + val refs2 = other.elems + def common(refs1: CaptureSet.Refs, refs2: CaptureSet.Refs) = + refs1.filter: ref => + ref.isExclusive && refs2.exists(_.stripReadOnly eq ref) + common(refs1, refs2) ++ common(refs2, refs1) + + private def hidden(elem: CaptureRef)(using Context): CaptureSet.Refs = elem match + case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ hidden(hcs) + case ReadOnlyCapability(ref) => hidden(ref).map(_.readOnly) + case _ => emptySet + + private def hidden(cs: CaptureSet)(using Context): CaptureSet.Refs = + val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet + + def hiddenByElem(elem: CaptureRef): CaptureSet.Refs = + if seen.add(elem) then elem match + case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs) + case ReadOnlyCapability(ref) => hiddenByElem(ref).map(_.readOnly) + case _ => emptySet + else emptySet + + def recur(cs: CaptureSet): CaptureSet.Refs = + (emptySet /: cs.elems): (elems, elem) => + elems ++ hiddenByElem(elem) + + recur(cs) + end hidden + + private def checkApply(fn: Tree, args: List[Tree])(using Context): Unit = + val fnCaptures = fn.nuType.deepCaptureSet + + def captures(arg: Tree) = + val argType = arg.nuType + if argType.hasUseAnnot then argType.deepCaptureSet else argType.captureSet + + val argCaptures = args.map(captures) + capt.println(i"check separate $fn($args), fnCaptures = $fnCaptures, argCaptures = $argCaptures") + var footprint = argCaptures.foldLeft(fnCaptures.footprint): (fp, ac) => + fp ++ ac.footprint + val paramNames = fn.nuType.widen match + case MethodType(pnames) => pnames + case _ => args.indices.map(nme.syntheticParamName(_)) + for (arg, ac, pname) <- args.lazyZip(argCaptures).lazyZip(paramNames) do + if arg.needsSepCheck then + val hiddenInArg = CaptureSet(hidden(ac)) + //println(i"check sep $arg / $footprint / $hiddenInArg") + val overlap = hiddenInArg.footprint.overlapWith(footprint) + if !overlap.isEmpty then + def formalName = if pname.toString.contains('$') then "" else i"$pname " + def whatStr = if overlap.size == 1 then "this capability" else "these capabilities" + def funStr = + if fn.symbol.exists then i"${fn.symbol}" + else "the function" + report.error( + em"""Separation failure: argument of type ${arg.actualType} to capture-polymorphic parameter + |${formalName}of type ${arg.nuType} captures ${CaptureSet(overlap)}, and $whatStr is also passed separately to $funStr.""", + arg.srcPos) + footprint ++= hiddenInArg + + private def traverseApply(tree: Tree, argss: List[List[Tree]])(using Context): Unit = tree match + case Apply(fn, args) => traverseApply(fn, args :: argss) + case TypeApply(fn, args) => traverseApply(fn, argss) // skip type arguments + case _ => + if argss.nestedExists(_.needsSepCheck) then + checkApply(tree, argss.flatten) + + def traverse(tree: Tree)(using Context): Unit = + tree match + case tree: GenericApply => + if tree.symbol != defn.Caps_unsafeAssumeSeparate then + tree.tpe match + case _: MethodOrPoly => + case _ => traverseApply(tree, Nil) + traverseChildren(tree) + case _ => + traverseChildren(tree) +end SepChecker + + + + + + diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index ebe128d7776c..ede036e6a875 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -13,7 +13,7 @@ import ast.tpd, tpd.* import transform.{PreRecheck, Recheck}, Recheck.* import CaptureSet.{IdentityCaptRefMap, IdempotentCaptRefMap} import Synthetics.isExcluded -import util.{Property, SimpleIdentitySet} +import util.SimpleIdentitySet import reporting.Message import printing.{Printer, Texts}, Texts.{Text, Str} import collection.mutable @@ -40,7 +40,7 @@ trait SetupAPI: object Setup: - val name: String = "ccSetup" + val name: String = "setupCC" val description: String = "prepare compilation unit for capture checking" /** Recognizer for `res $throws exc`, returning `(res, exc)` in case of success */ @@ -132,7 +132,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def mappedInfo = if toBeUpdated.contains(sym) then symd.info // don't transform symbols that will anyway be updated - else transformExplicitType(symd.info) + else Fresh.fromCap(transformExplicitType(symd.info), sym) if Synthetics.needsTransform(symd) then Synthetics.transform(symd, mappedInfo) else if isPreCC(sym) then @@ -192,11 +192,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * 3. Refine other class types C by adding capture set variables to their parameter getters * (see addCaptureRefinements), provided `refine` is true. * 4. Add capture set variables to all types that can be tracked + * 5. Perform normalizeCaptures * * Polytype bounds are only cleaned using step 1, but not otherwise transformed. */ private def transformInferredType(tp: Type)(using Context): Type = - def mapInferred(refine: Boolean): TypeMap = new TypeMap: + def mapInferred(refine: Boolean): TypeMap = new TypeMap with FollowAliasesMap: override def toString = "map inferred" /** Refine a possibly applied class type C where the class has tracked parameters @@ -277,7 +278,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: paramInfos = tp.paramInfos.mapConserve(_.dropAllRetains.bounds), resType = this(tp.resType)) case _ => - mapOver(tp) + if ccConfig.followAliases then mapFollowingAliases(tp) else mapOver(tp) addVar(addCaptureRefinements(normalizeCaptures(tp1)), ctx.owner) end apply end mapInferred @@ -299,9 +300,10 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * 3. Add universal capture sets to types deriving from Capability * 4. Map `cap` in function result types to existentially bound variables. * 5. Schedule deferred well-formed tests for types with retains annotations. + * 6. Perform normalizeCaptures */ private def transformExplicitType(tp: Type, tptToCheck: Tree = EmptyTree)(using Context): Type = - val toCapturing = new DeepTypeMap: + val toCapturing = new DeepTypeMap with FollowAliasesMap: override def toString = "expand aliases" /** Expand $throws aliases. This is hard-coded here since $throws aliases in stdlib @@ -337,7 +339,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tp @ CapturingType(parent, refs) if (refs eq defn.universalCSImpliedByCapability) && !tp.isBoxedCapturing => parent - case tp @ CapturingType(parent, refs) => tp case _ => tp def apply(t: Type) = @@ -355,6 +356,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: catch case ex: IllegalCaptureRef => report.error(em"Illegal capture reference: ${ex.getMessage.nn}", tptToCheck.srcPos) parent2 + else if ann.symbol == defn.UncheckedCapturesAnnot then + makeUnchecked(apply(parent)) else t.derivedAnnotatedType(parent1, ann) case throwsAlias(res, exc) => @@ -363,13 +366,22 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // Map references to capability classes C to C^ if t.derivesFromCapability && !t.isSingleton && t.typeSymbol != defn.Caps_Exists then CapturingType(t, defn.universalCSImpliedByCapability, boxed = false) - else normalizeCaptures(mapOver(t)) + else normalizeCaptures( + if ccConfig.followAliases then mapFollowingAliases(t) else mapOver(t)) end toCapturing def fail(msg: Message) = if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos) val tp1 = toCapturing(tp) + if false then + val tp1alt = + toCapturing.follow = false + toCapturing(tp) + if tp1 ne tp1alt then + println(i"""DIFF mapping $tp + |WAS $tp1alt + |NOW $tp1""") val tp2 = Existential.mapCapInResults(fail)(tp1) if tp2 ne tp then capt.println(i"expanded explicit in ${ctx.owner}: $tp --> $tp1 --> $tp2") tp2 @@ -427,26 +439,36 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def setupTraverser(checker: CheckerAPI) = new TreeTraverserWithPreciseImportContexts: import checker.* - /** Transform type of tree, and remember the transformed type as the type the tree */ - private def transformTT(tree: TypeTree, boxed: Boolean)(using Context): Unit = + private val paramSigChange = util.EqHashSet[Tree]() + + /** Transform type of tree, and remember the transformed type as the type the tree + * @pre !(boxed && sym.exists) + */ + private def transformTT(tree: TypeTree, sym: Symbol, boxed: Boolean)(using Context): Unit = if !tree.hasNuType then - val transformed = + var transformed = if tree.isInferred then transformInferredType(tree.tpe) else transformExplicitType(tree.tpe, tptToCheck = tree) - tree.setNuType(if boxed then box(transformed) else transformed) + if boxed then transformed = box(transformed) + if sym.is(Param) && (transformed ne tree.tpe) then + paramSigChange += tree + tree.setNuType( + if boxed then transformed + else if sym.hasAnnotation(defn.UncheckedCapturesAnnot) then makeUnchecked(transformed) + else Fresh.fromCap(transformed, sym)) /** Transform the type of a val or var or the result type of a def */ def transformResultType(tpt: TypeTree, sym: Symbol)(using Context): Unit = // First step: Transform the type and record it as knownType of tpt. try - transformTT(tpt, + transformTT(tpt, sym, boxed = - sym.is(Mutable, butNot = Method) + sym.isMutableVar && !ccConfig.useSealed && !sym.hasAnnotation(defn.UncheckedCapturesAnnot), // Under the sealed policy, we disallow root capabilities in the type of mutable - // variables, no need to box them here. + // variables, no need to box them here ) catch case ex: IllegalCaptureRef => capt.println(i"fail while transforming result type $tpt of $sym") @@ -489,9 +511,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tree @ TypeApply(fn, args) => traverse(fn) - if !defn.isTypeTestOrCast(fn.symbol) then - for case arg: TypeTree <- args do - transformTT(arg, boxed = true) // type arguments in type applications are boxed + for case arg: TypeTree <- args do + if defn.isTypeTestOrCast(fn.symbol) then + arg.setNuType(Fresh.fromCap(arg.tpe)) + else + transformTT(arg, NoSymbol, boxed = true) // type arguments in type applications are boxed case tree: TypeDef if tree.symbol.isClass => val sym = tree.symbol @@ -500,6 +524,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: inContext(ctx.withOwner(sym)) traverseChildren(tree) + case tree @ TypeDef(_, rhs: TypeTree) => + transformTT(rhs, tree.symbol, boxed = false) + case tree @ SeqLiteral(elems, tpt: TypeTree) => traverse(elems) tpt.setNuType(box(transformInferredType(tpt.tpe))) @@ -516,7 +543,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** Processing done on node `tree` after its children are traversed */ def postProcess(tree: Tree)(using Context): Unit = tree match case tree: TypeTree => - transformTT(tree, boxed = false) + transformTT(tree, NoSymbol, boxed = false) case tree: ValOrDefDef => // Make sure denotation of tree's symbol is correct val sym = tree.symbol @@ -543,8 +570,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def paramSignatureChanges = tree.match case tree: DefDef => tree.paramss.nestedExists: - case param: ValDef => param.tpt.hasNuType - case param: TypeDef => param.rhs.hasNuType + case param: ValDef => paramSigChange.contains(param.tpt) + case param: TypeDef => paramSigChange.contains(param.rhs) case _ => false // A symbol's signature changes if some of its parameter types or its result type @@ -579,7 +606,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: mt.paramInfos else val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) - psyms.map(psym => adaptedInfo(psym, subst(psym.nextInfo).asInstanceOf[mt.PInfo])), + psyms.map(psym => adaptedInfo(psym, subst(Fresh.toCap(psym.nextInfo)).asInstanceOf[mt.PInfo])), mt1 => integrateRT(mt.resType, psymss.tail, resType, psyms :: prevPsymss, mt1 :: prevLambdas) ) @@ -734,7 +761,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case RetainingType(parent, refs) => needsVariable(parent) && !refs.tpes.exists: - case ref: TermRef => ref.isRootCapability + case ref: TermRef => ref.isCap case _ => false case AnnotatedType(parent, _) => needsVariable(parent) @@ -797,6 +824,16 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if variance > 0 then t1 else decorate(t1, Function.const(CaptureSet.Fluid)) + /** Replace all universal capture sets in this type by */ + private def makeUnchecked(using Context): TypeMap = new TypeMap with FollowAliasesMap: + def apply(t: Type) = t match + case t @ CapturingType(parent, refs) => + val parent1 = this(parent) + if refs.isUniversal then t.derivedCapturingType(parent1, CaptureSet.Fluid) + else t + case Existential(_) => t + case _ => mapFollowingAliases(t) + /** Pull out an embedded capture set from a part of `tp` */ def normalizeCaptures(tp: Type)(using Context): Type = tp match case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) => @@ -819,7 +856,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed) case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) - case tp @ AppliedType(tycon, args) if !defn.isFunctionClass(tp.dealias.typeSymbol) => + case tp @ AppliedType(tycon, args) + if !defn.isFunctionClass(tp.dealias.typeSymbol) + && (!ccConfig.followAliases || (tp.dealias eq tp)) => tp.derivedAppliedType(tycon, args.mapConserve(box)) case tp: RealTypeBounds => tp.derivedTypeBounds(tp.lo, box(tp.hi)) diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 1372ebafe82f..9e2729eb7f31 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -116,7 +116,7 @@ object Synthetics: def transformUnapplyCaptures(info: Type)(using Context): Type = info match case info: MethodType => val paramInfo :: Nil = info.paramInfos: @unchecked - val newParamInfo = CapturingType(paramInfo, CaptureSet.universal) + val newParamInfo = CapturingType(paramInfo, CaptureSet.fresh()) val trackedParam = info.paramRefs.head def newResult(tp: Type): Type = tp match case tp: MethodOrPoly => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 2890bdf306be..7381eda18dcf 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -15,7 +15,7 @@ import Comments.{Comment, docCtx} import util.Spans.NoSpan import config.Feature import Symbols.requiredModuleRef -import cc.{CaptureSet, RetainingType, Existential} +import cc.{CaptureSet, RetainingType, Existential, readOnly} import ast.tpd.ref import scala.annotation.tailrec @@ -992,18 +992,21 @@ class Definitions { @tu lazy val CapsModule: Symbol = requiredModule("scala.caps") @tu lazy val captureRoot: TermSymbol = CapsModule.requiredValue("cap") - @tu lazy val Caps_Capability: TypeSymbol = CapsModule.requiredType("Capability") + @tu lazy val Caps_Capability: ClassSymbol = requiredClass("scala.caps.Capability") @tu lazy val Caps_CapSet: ClassSymbol = requiredClass("scala.caps.CapSet") @tu lazy val Caps_reachCapability: TermSymbol = CapsModule.requiredMethod("reachCapability") + @tu lazy val Caps_readOnlyCapability: TermSymbol = CapsModule.requiredMethod("readOnlyCapability") @tu lazy val Caps_capsOf: TermSymbol = CapsModule.requiredMethod("capsOf") @tu lazy val Caps_Exists: ClassSymbol = requiredClass("scala.caps.Exists") @tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe") @tu lazy val Caps_unsafeAssumePure: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumePure") + @tu lazy val Caps_unsafeAssumeSeparate: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumeSeparate") @tu lazy val Caps_ContainsTrait: TypeSymbol = CapsModule.requiredType("Contains") @tu lazy val Caps_containsImpl: TermSymbol = CapsModule.requiredMethod("containsImpl") + @tu lazy val Caps_Mutable: ClassSymbol = requiredClass("scala.caps.Mutable") /** The same as CaptureSet.universal but generated implicitly for references of Capability subtypes */ - @tu lazy val universalCSImpliedByCapability = CaptureSet(captureRoot.termRef) + @tu lazy val universalCSImpliedByCapability = CaptureSet(captureRoot.termRef.readOnly) @tu lazy val PureClass: Symbol = requiredClass("scala.Pure") @@ -1074,6 +1077,8 @@ class Definitions { @tu lazy val TargetNameAnnot: ClassSymbol = requiredClass("scala.annotation.targetName") @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") @tu lazy val ReachCapabilityAnnot = requiredClass("scala.annotation.internal.reachCapability") + @tu lazy val FreshCapabilityAnnot = requiredClass("scala.annotation.internal.freshCapability") + @tu lazy val ReadOnlyCapabilityAnnot = requiredClass("scala.annotation.internal.readOnlyCapability") @tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability") @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains") @tu lazy val RetainsCapAnnot: ClassSymbol = requiredClass("scala.annotation.retainsCap") @@ -1522,6 +1527,9 @@ class Definitions { @tu lazy val pureSimpleClasses = Set(StringClass, NothingClass, NullClass) ++ ScalaValueClasses() + @tu lazy val capabilityWrapperAnnots: Set[Symbol] = + Set(ReachCapabilityAnnot, ReadOnlyCapabilityAnnot, MaybeCapabilityAnnot, FreshCapabilityAnnot) + @tu lazy val AbstractFunctionType: Array[TypeRef] = mkArityArray("scala.runtime.AbstractFunction", MaxImplementedFunctionArity, 0).asInstanceOf[Array[TypeRef]] val AbstractFunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun(AbstractFunctionType.map(_.symbol.asClass)) def AbstractFunctionClass(n: Int)(using Context): Symbol = AbstractFunctionClassPerRun()(using ctx)(n) diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 0775b3caaf0c..57bf870c6b64 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -597,7 +597,6 @@ object Flags { val JavaInterface: FlagSet = JavaDefined | NoInits | Trait val JavaProtected: FlagSet = JavaDefined | Protected val MethodOrLazy: FlagSet = Lazy | Method - val MutableOrLazy: FlagSet = Lazy | Mutable val MethodOrLazyOrMutable: FlagSet = Lazy | Method | Mutable val LiftedMethod: FlagSet = Lifted | Method val LocalParam: FlagSet = Local | Param diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 56d71c7fb57e..dc30ae2be7fb 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -121,6 +121,7 @@ object StdNames { val BITMAP_CHECKINIT: N = s"${BITMAP_PREFIX}init$$" // initialization bitmap for checkinit values val BITMAP_CHECKINIT_TRANSIENT: N = s"${BITMAP_PREFIX}inittrans$$" // initialization bitmap for transient checkinit values val CC_REACH: N = "$reach" + val CC_READONLY: N = "$readOnly" val DEFAULT_GETTER: N = str.DEFAULT_GETTER val DEFAULT_GETTER_INIT: N = "$lessinit$greater" val DO_WHILE_PREFIX: N = "doWhile$" @@ -553,6 +554,7 @@ object StdNames { val materializeTypeTag: N = "materializeTypeTag" val mirror : N = "mirror" val moduleClass : N = "moduleClass" + val mut: N = "mut" val name: N = "name" val nameDollar: N = "$name" val ne: N = "ne" @@ -587,6 +589,7 @@ object StdNames { val productPrefix: N = "productPrefix" val quotes : N = "quotes" val raw_ : N = "raw" + val rd: N = "rd" val refl: N = "refl" val reflect: N = "reflect" val reflectiveSelectable: N = "reflectiveSelectable" diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index be651842d9b0..07506749ff07 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -806,6 +806,13 @@ object SymDenotations { final def isRealMethod(using Context): Boolean = this.is(Method, butNot = Accessor) && !isAnonymousFunction + /** A mutable variable (not a getter or setter for it) */ + final def isMutableVar(using Context): Boolean = is(Mutable, butNot = Method) + + /** A mutable variable or its getter or setter */ + final def isMutableVarOrAccessor(using Context): Boolean = + is(Mutable) && (!is(Method) || is(Accessor)) + /** Is this a getter? */ final def isGetter(using Context): Boolean = this.is(Accessor) && !originalName.isSetterName && !(originalName.isScala2LocalSuffix && symbol.owner.is(Scala2x)) diff --git a/compiler/src/dotty/tools/dotc/core/SymUtils.scala b/compiler/src/dotty/tools/dotc/core/SymUtils.scala index 1a762737d52f..baaeb025c6d5 100644 --- a/compiler/src/dotty/tools/dotc/core/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/SymUtils.scala @@ -287,7 +287,7 @@ class SymUtils: */ def isConstExprFinalVal(using Context): Boolean = atPhaseNoLater(erasurePhase) { - self.is(Final, butNot = Mutable) && self.info.resultType.isInstanceOf[ConstantType] + self.is(Final) && !self.isMutableVarOrAccessor && self.info.resultType.isInstanceOf[ConstantType] } && !self.sjsNeedsField /** The `ConstantType` of a val known to be `isConstrExprFinalVal`. diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 8414c3795f49..22cf1036786d 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2170,7 +2170,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling val info2 = tp2.refinedInfo val isExpr2 = info2.isInstanceOf[ExprType] var info1 = m.info match - case info1: ValueType if isExpr2 || m.symbol.is(Mutable) => + case info1: ValueType if isExpr2 || m.symbol.isMutableVarOrAccessor => // OK: { val x: T } <: { def x: T } // OK: { var x: T } <: { def x: T } // NO: { var x: T } <: { val x: T } @@ -2845,6 +2845,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false Existential.isExistentialVar(tp1) && canInstantiateWith(assocExistentials) + def isOpenedExistential(ref: CaptureRef)(using Context): Boolean = + openedExistentials.contains(ref) + /** bi-map taking existentials to the left of a comparison to matching * existentials on the right. This is not a bijection. However * we have `forwards(backwards(bv)) == bv` for an existentially bound `bv`. @@ -2889,10 +2892,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling end inverse end MapExistentials + protected def frozenDegree(frozen: Boolean) = + if frozen then CaptureSet.Frozen.Vars else CaptureSet.Frozen.None + protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = try if assocExistentials.isEmpty then - refs1.subCaptures(refs2, frozen) + refs1.subCaptures(refs2, frozenDegree(frozen)) else val mapped = refs1.map(MapExistentials(assocExistentials)) if mapped.elems.exists(Existential.isBadExistential) @@ -2903,7 +2909,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling throw ex protected def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - refs1.subCaptures(refs2, frozen) + refs1.subCaptures(refs2, frozenDegree(frozen)) /** Is the boxing status of tp1 and tp2 the same, or alternatively, is * the capture sets `refs1` of `tp1` a subcapture of the empty set? @@ -2911,7 +2917,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling */ protected def sameBoxed(tp1: Type, tp2: Type, refs1: CaptureSet)(using Context): Boolean = (tp1.isBoxedCapturing == tp2.isBoxedCapturing) - || refs1.subCaptures(CaptureSet.empty, frozenConstraint).isOK + || refs1.subCaptures(CaptureSet.empty, frozenDegree(frozenConstraint)).isOK // ----------- Diagnostics -------------------------------------------------- @@ -3476,6 +3482,9 @@ object TypeComparer { def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context) = comparing(_.subsumesExistentially(tp1, tp2)) + + def isOpenedExistential(ref: CaptureRef)(using Context) = + comparing(_.isOpenedExistential(ref)) } object MatchReducer: diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 7ae790c62a2c..6b7d804f28c4 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -19,7 +19,7 @@ import typer.Inferencing.* import typer.IfBottom import reporting.TestingReporter import cc.{CapturingType, derivedCapturingType, CaptureSet, captureSet, isBoxed, isBoxedCapturing} -import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} +import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap, Frozen} import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -161,7 +161,7 @@ object TypeOps: TypeComparer.lub(simplify(l, theMap), simplify(r, theMap), isSoft = tp.isSoft) case tp @ CapturingType(parent, refs) => if !ctx.mode.is(Mode.Type) - && refs.subCaptures(parent.captureSet, frozen = true).isOK + && refs.subCaptures(parent.captureSet, Frozen.All).isOK && (tp.isBoxed || !parent.isBoxedCapturing) // fuse types with same boxed status and outer boxed with any type then diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index c5937074f4bc..857714ffa940 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4178,7 +4178,7 @@ object Types extends TypeUtils { tl => params.map(p => tl.integrate(params, adaptParamInfo(p))), tl => tl.integrate(params, resultType)) - /** Adapt info of parameter symbol to be integhrated into corresponding MethodType + /** Adapt info of parameter symbol to be integrated into corresponding MethodType * using the scheme described in `fromSymbols`. */ def adaptParamInfo(param: Symbol, pinfo: Type)(using Context): Type = diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 7933cbbea12f..fca810cf0efe 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1588,22 +1588,36 @@ object Parsers { case _ => None } - /** CaptureRef ::= { SimpleRef `.` } SimpleRef [`*`] + /** CaptureRef ::= { SimpleRef `.` } SimpleRef [`*`] [`.` rd] * | [ { SimpleRef `.` } SimpleRef `.` ] id `^` */ def captureRef(): Tree = - val ref = dotSelectors(simpleRef()) - if isIdent(nme.raw.STAR) then - in.nextToken() - atSpan(startOffset(ref)): - PostfixOp(ref, Ident(nme.CC_REACH)) - else if isIdent(nme.UPARROW) then + + def derived(ref: Tree, name: TermName) = in.nextToken() - atSpan(startOffset(ref)): - convertToTypeId(ref) match - case ref: RefTree => makeCapsOf(ref) - case ref => ref - else ref + atSpan(startOffset(ref)) { PostfixOp(ref, Ident(name)) } + + def recur(ref: Tree): Tree = + if in.token == DOT then + in.nextToken() + if in.isIdent(nme.rd) then derived(ref, nme.CC_READONLY) + else recur(selector(ref)) + else if in.isIdent(nme.raw.STAR) then + val reachRef = derived(ref, nme.CC_REACH) + if in.token == DOT && in.lookahead.isIdent(nme.rd) then + in.nextToken() + derived(reachRef, nme.CC_READONLY) + else reachRef + else if isIdent(nme.UPARROW) then + in.nextToken() + atSpan(startOffset(ref)): + convertToTypeId(ref) match + case ref: RefTree => makeCapsOf(ref) + case ref => ref + else ref + + recur(simpleRef()) + end captureRef /** CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` -- under captureChecking */ @@ -3285,13 +3299,14 @@ object Parsers { case SEALED => Mod.Sealed() case IDENTIFIER => name match { - case nme.erased if in.erasedEnabled => Mod.Erased() case nme.inline => Mod.Inline() case nme.opaque => Mod.Opaque() case nme.open => Mod.Open() case nme.transparent => Mod.Transparent() case nme.infix => Mod.Infix() case nme.tracked => Mod.Tracked() + case nme.erased if in.erasedEnabled => Mod.Erased() + case nme.mut if Feature.ccEnabled => Mod.Mut() } } @@ -4672,7 +4687,8 @@ object Parsers { syntaxError(msg, tree.span) Nil tree match - case tree: MemberDef if !(tree.mods.flags & (ModifierFlags &~ Mutable)).isEmpty => + case tree: MemberDef + if !(tree.mods.flags & ModifierFlags).isEmpty && !tree.mods.isMutableVar => // vars are OK, mut defs are not fail(em"refinement cannot be ${(tree.mods.flags & ModifierFlags).flagStrings().mkString("`", "`, `", "`")}") case tree: DefDef if tree.termParamss.nestedExists(!_.rhs.isEmpty) => fail(em"refinement cannot have default arguments") diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 2007b633a7c5..e007e3e689b3 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -1196,7 +1196,10 @@ object Scanners { def isSoftModifier: Boolean = token == IDENTIFIER - && (softModifierNames.contains(name) || name == nme.erased && erasedEnabled || name == nme.tracked && trackedEnabled) + && (softModifierNames.contains(name) + || name == nme.erased && erasedEnabled + || name == nme.tracked && trackedEnabled + || name == nme.mut && Feature.ccEnabled) def isSoftModifierInModifierPosition: Boolean = isSoftModifier && inModifierPosition() diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index cac82eb0c4bd..eb6b5bcf569a 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -15,7 +15,7 @@ import util.SourcePosition import scala.util.control.NonFatal import scala.annotation.switch import config.{Config, Feature} -import cc.{CapturingType, RetainingType, CaptureSet, ReachCapability, MaybeCapability, isBoxed, retainedElems, isRetainsLike} +import cc.* class PlainPrinter(_ctx: Context) extends Printer { @@ -27,6 +27,12 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def printDebug = ctx.settings.YprintDebug.value + /** Print Fresh.Cap instances as */ + protected def printFreshDetailed = printDebug + + /** Print Fresh.Cap instances as "fresh" */ + protected def printFresh = printFreshDetailed || ctx.property(PrintFresh).isDefined + private var openRecs: List[RecType] = Nil protected def maxToTextRecursions: Int = 100 @@ -158,7 +164,7 @@ class PlainPrinter(_ctx: Context) extends Printer { else val core: Text = if !cs.isConst && cs.elems.isEmpty then "?" - else "{" ~ Text(cs.elems.toList.map(toTextCaptureRef), ", ") ~ "}" + else "{" ~ Text(cs.processElems(_.toList.map(toTextCaptureRef)), ", ") ~ "}" // ~ Str("?").provided(!cs.isConst) core ~ cs.optionalInfo @@ -167,8 +173,9 @@ class PlainPrinter(_ctx: Context) extends Printer { toTextCaptureRef(ref.typeOpt) case TypeApply(fn, arg :: Nil) if fn.symbol == defn.Caps_capsOf => toTextRetainedElem(arg) - case _ => - toText(ref) + case ReachCapabilityApply(ref1) => toTextRetainedElem(ref1) ~ "*" + case ReadOnlyCapabilityApply(ref1) => toTextRetainedElem(ref1) ~ ".rd" + case _ => toText(ref) private def toTextRetainedElems[T <: Untyped](refs: List[Tree[T]]): Text = "{" ~ Text(refs.map(ref => toTextRetainedElem(ref)), ", ") ~ "}" @@ -178,8 +185,7 @@ class PlainPrinter(_ctx: Context) extends Printer { */ protected def toTextCapturing(parent: Type, refsText: Text, boxText: Text): Text = changePrec(InfixPrec): - boxText ~ toTextLocal(parent) ~ "^" - ~ (refsText provided refsText != rootSetText) + boxText ~ toTextLocal(parent) ~ "^" ~ (refsText provided refsText != rootSetText) final protected def rootSetText = Str("{cap}") // TODO Use disambiguation @@ -190,7 +196,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp: TermRef if !tp.denotationIsCurrent && !homogenizedView // always print underlying when testing picklers - && !tp.isRootCapability + && !tp.isCap || tp.symbol.is(Module) || tp.symbol.name == nme.IMPORT => toTextRef(tp) ~ ".type" @@ -202,14 +208,14 @@ class PlainPrinter(_ctx: Context) extends Printer { else toTextPrefixOf(tp) ~ selectionString(tp) case tp: TermParamRef => - ParamRefNameString(tp) ~ lambdaHash(tp.binder) ~ ".type" + ParamRefNameString(tp) ~ hashStr(tp.binder) ~ ".type" case tp: TypeParamRef => val suffix = if showNestingLevel then val tvar = ctx.typerState.constraint.typeVarOfParam(tp) if tvar.exists then s"#${tvar.asInstanceOf[TypeVar].nestingLevel.toString}" else "" else "" - ParamRefNameString(tp) ~ lambdaHash(tp.binder) ~ suffix + ParamRefNameString(tp) ~ hashStr(tp.binder) ~ suffix case tp: SingletonType => toTextSingleton(tp) case AppliedType(tycon, args) => @@ -242,9 +248,20 @@ class PlainPrinter(_ctx: Context) extends Printer { }.close case tp @ CapturingType(parent, refs) => val boxText: Text = Str("box ") provided tp.isBoxed //&& ctx.settings.YccDebug.value - val showAsCap = refs.isUniversal && (refs.elems.size == 1 || !printDebug) - val refsText = if showAsCap then rootSetText else toTextCaptureSet(refs) - toTextCapturing(parent, refsText, boxText) + if parent.derivesFrom(defn.Caps_Capability) + && refs.containsRootCapability && refs.isReadOnly && !printDebug + then + toText(parent) + else + val refsText = + if refs.isUniversal then + if refs.elems.size == 1 || !printDebug then rootSetText + else toTextCaptureSet(refs) + else if !refs.elems.isEmpty && refs.elems.forall(_.isCapOrFresh) && !printFresh then + rootSetText + else + toTextCaptureSet(refs) + toTextCapturing(parent, refsText, boxText) case tp @ RetainingType(parent, refs) => if Feature.ccEnabledSomewhere then val refsText = refs match @@ -275,19 +292,19 @@ class PlainPrinter(_ctx: Context) extends Printer { case ExprType(restp) => def arrowText: Text = restp match case AnnotatedType(parent, ann) if ann.symbol == defn.RetainsByNameAnnot => - val refs = ann.tree.retainedElems - if refs.exists(_.symbol == defn.captureRoot) then Str("=>") - else Str("->") ~ toTextRetainedElems(refs) + ann.tree.retainedElems match + case ref :: Nil if ref.symbol == defn.captureRoot => Str("=>") + case refs => Str("->") ~ toTextRetainedElems(refs) case _ => if Feature.pureFunsEnabled then "->" else "=>" changePrec(GlobalPrec)(arrowText ~ " " ~ toText(restp)) case tp: HKTypeLambda => changePrec(GlobalPrec) { - "[" ~ paramsText(tp) ~ "]" ~ lambdaHash(tp) ~ Str(" =>> ") ~ toTextGlobal(tp.resultType) + "[" ~ paramsText(tp) ~ "]" ~ hashStr(tp) ~ Str(" =>> ") ~ toTextGlobal(tp.resultType) } case tp: PolyType => changePrec(GlobalPrec) { - "[" ~ paramsText(tp) ~ "]" ~ lambdaHash(tp) ~ + "[" ~ paramsText(tp) ~ "]" ~ hashStr(tp) ~ (Str(": ") provided !tp.resultType.isInstanceOf[MethodOrPoly]) ~ toTextGlobal(tp.resultType) } @@ -297,7 +314,10 @@ class PlainPrinter(_ctx: Context) extends Printer { else if (annot.symbol == defn.IntoAnnot || annot.symbol == defn.IntoParamAnnot) && !printDebug then atPrec(GlobalPrec)( Str("into ") ~ toText(tpe) ) - else toTextLocal(tpe) ~ " " ~ toText(annot) + else if annot.isInstanceOf[CaptureAnnotation] then + toTextLocal(tpe) ~ "^" ~ toText(annot) + else + toTextLocal(tpe) ~ " " ~ toText(annot) case FlexibleType(_, tpe) => "(" ~ toText(tpe) ~ ")?" case tp: TypeVar => @@ -335,7 +355,7 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def paramsText(lam: LambdaType): Text = { def paramText(ref: ParamRef) = val erased = ref.underlying.hasAnnotation(defn.ErasedParamAnnot) - keywordText("erased ").provided(erased) ~ ParamRefNameString(ref) ~ lambdaHash(lam) ~ toTextRHS(ref.underlying, isParameter = true) + keywordText("erased ").provided(erased) ~ ParamRefNameString(ref) ~ hashStr(lam) ~ toTextRHS(ref.underlying, isParameter = true) Text(lam.paramRefs.map(paramText), ", ") } @@ -347,11 +367,11 @@ class PlainPrinter(_ctx: Context) extends Printer { /** The name of the symbol without a unique id. */ protected def simpleNameString(sym: Symbol): String = nameString(sym.name) - /** If -uniqid is set, the hashcode of the lambda type, after a # */ - protected def lambdaHash(pt: LambdaType): Text = - if (showUniqueIds) - try "#" + pt.hashCode - catch { case ex: NullPointerException => "" } + /** If -uniqid is set, the hashcode of the type, after a # */ + protected def hashStr(tp: Type): String = + if showUniqueIds then + try "#" + tp.hashCode + catch case ex: NullPointerException => "" else "" /** A string to append to a symbol composed of: @@ -400,7 +420,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp @ ConstantType(value) => toText(value) case pref: TermParamRef => - ParamRefNameString(pref) ~ lambdaHash(pref.binder) + ParamRefNameString(pref) ~ hashStr(pref.binder) case tp: RecThis => val idx = openRecs.reverse.indexOf(tp.binder) if (idx >= 0) selfRecName(idx + 1) @@ -414,11 +434,16 @@ class PlainPrinter(_ctx: Context) extends Printer { def toTextCaptureRef(tp: Type): Text = homogenize(tp) match - case tp: TermRef if tp.symbol == defn.captureRoot => Str("cap") + case tp: TermRef if tp.symbol == defn.captureRoot => "cap" case tp: SingletonType => toTextRef(tp) case tp: (TypeRef | TypeParamRef) => toText(tp) ~ "^" + case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd" case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" + case Fresh.Cap(hidden) => + if printFreshDetailed then s"" ~ "//" + else if printFresh then "fresh" + else "cap" case tp => toText(tp) protected def isOmittablePrefix(sym: Symbol): Boolean = @@ -533,7 +558,7 @@ class PlainPrinter(_ctx: Context) extends Printer { else if sym.is(Param) then "parameter" else if sym.is(Given) then "given instance" else if (flags.is(Lazy)) "lazy value" - else if (flags.is(Mutable)) "variable" + else if (sym.isMutableVar) "variable" else if (sym.isClassConstructor && sym.isPrimaryConstructor) "primary constructor" else if (sym.isClassConstructor) "constructor" else if (sym.is(Method)) "method" @@ -549,7 +574,7 @@ class PlainPrinter(_ctx: Context) extends Printer { else if (flags.is(Module)) "object" else if (sym.isClass) "class" else if (sym.isType) "type" - else if (flags.is(Mutable)) "var" + else if (sym.isMutableVarOrAccessor) "var" else if (flags.is(Package)) "package" else if (sym.is(Method)) "def" else if (sym.isTerm && !flags.is(Param)) "val" diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index b7f2eef8c8f9..2c7f970908ba 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -336,7 +336,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { "?" ~ (("(ignored: " ~ toText(ignored) ~ ")") provided printDebug) case tp @ PolyProto(targs, resType) => "[applied to [" ~ toTextGlobal(targs, ", ") ~ "] returning " ~ toText(resType) - case ReachCapability(_) | MaybeCapability(_) => + case tp: AnnotatedType if tp.isTrackableRef => toTextCaptureRef(tp) case _ => super.toText(tp) @@ -742,6 +742,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case PostfixOp(l, op) => if op.name == nme.CC_REACH then changePrec(DotPrec) { toText(l) ~ "*" } + else if op.name == nme.CC_READONLY then + changePrec(DotPrec) { toText(l) ~ ".rd" } else changePrec(InfixPrec) { toText(l) ~ " " ~ toText(op) } case PrefixOp(op, r) => diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 28a2b5757a93..0c2e524a4733 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -1694,7 +1694,7 @@ class OnlyClassesCanHaveDeclaredButUndefinedMembers(sym: Symbol)( def msg(using Context) = i"""Declaration of $sym not allowed here: only classes can have declared but undefined members""" def explain(using Context) = - if sym.is(Mutable) then "Note that variables need to be initialized to be defined." + if sym.isMutableVarOrAccessor then "Note that variables need to be initialized to be defined." else "" } diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index 75e859111932..5dd69ebc3386 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -418,7 +418,7 @@ private class ExtractAPICollector(nonLocalClassSymbols: mutable.HashSet[Symbol]) apiClass(sym.asClass) } else if (sym.isType) { apiTypeMember(sym.asType) - } else if (sym.is(Mutable, butNot = Accessor)) { + } else if (sym.isMutableVar) { api.Var.of(sym.name.toString, apiAccess(sym), apiModifiers(sym), apiAnnotations(sym, inlineOrigin).toArray, apiType(sym.info)) } else if (sym.isStableMember && !sym.isRealMethod) { diff --git a/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala b/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala index c1725cbd0255..7263bce0478c 100644 --- a/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala +++ b/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala @@ -120,7 +120,7 @@ object CapturedVars: def traverse(tree: Tree)(using Context) = tree match case id: Ident => val sym = id.symbol - if sym.is(Mutable, butNot = Method) && sym.owner.isTerm then + if sym.isMutableVar && sym.owner.isTerm then val enclMeth = ctx.owner.enclosingMethod if sym.enclosingMethod != enclMeth then report.log(i"capturing $sym in ${sym.enclosingMethod}, referenced from $enclMeth") diff --git a/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala b/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala index e8a402068bfc..5f52ac82879a 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala @@ -65,7 +65,7 @@ class CheckReentrant extends MiniPhase { scanning(cls) { for (sym <- cls.classInfo.decls) if (sym.isTerm && !sym.isSetter && !isIgnored(sym)) - if (sym.is(Mutable)) { + if (sym.isMutableVarOrAccessor) { report.error( em"""possible data race involving globally reachable ${sym.showLocated}: ${sym.info} | use -Ylog:checkReentrant+ to find out more about why the variable is reachable.""") diff --git a/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala b/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala index 6c74f302b65d..957fd78e9c2c 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala @@ -52,7 +52,7 @@ class CheckStatic extends MiniPhase { report.error(MissingCompanionForStatic(defn.symbol), defn.srcPos) else if (clashes.exists) report.error(MemberWithSameNameAsStatic(), defn.srcPos) - else if (defn.symbol.is(Flags.Mutable) && companion.is(Flags.Trait)) + else if (defn.symbol.isMutableVarOrAccessor && companion.is(Flags.Trait)) report.error(TraitCompanionWithMutableStatic(), defn.srcPos) else if (defn.symbol.is(Flags.Lazy)) report.error(LazyStaticField(), defn.srcPos) diff --git a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala index d647d50560d3..818fb3fc029d 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala @@ -803,7 +803,7 @@ object CheckUnused: private def isUnsetVarDef(using Context): Boolean = val sym = memDef.symbol - sym.is(Mutable) && !setVars(sym) + sym.isMutableVarOrAccessor && !setVars(sym) extension (imp: tpd.Import) /** Enum generate an import for its cases (but outside them), which should be ignored */ diff --git a/compiler/src/dotty/tools/dotc/transform/Constructors.scala b/compiler/src/dotty/tools/dotc/transform/Constructors.scala index 9a0df830c6d7..b373565489f0 100644 --- a/compiler/src/dotty/tools/dotc/transform/Constructors.scala +++ b/compiler/src/dotty/tools/dotc/transform/Constructors.scala @@ -155,7 +155,7 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase = case Ident(_) | Select(This(_), _) => var sym = tree.symbol def isOverridableSelect = tree.isInstanceOf[Select] && !sym.isEffectivelyFinal - def switchOutsideSupercall = !sym.is(Mutable) && !isOverridableSelect + def switchOutsideSupercall = !sym.isMutableVarOrAccessor && !isOverridableSelect // If true, switch to constructor parameters also in the constructor body // that follows the super call. // Variables need to go through the getter since they might have been updated. diff --git a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala index e2712a7d6302..2fd777f715d9 100644 --- a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala +++ b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala @@ -255,7 +255,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { def transformMemberDefThreadUnsafe(x: ValOrDefDef)(using Context): Thicket = { val claz = x.symbol.owner.asClass val tpe = x.tpe.widen.resultType.widen - assert(!(x.symbol is Mutable)) + assert(!x.symbol.isMutableVarOrAccessor) val containerName = LazyLocalName.fresh(x.name.asTermName) val containerSymbol = newSymbol(claz, containerName, x.symbol.flags &~ containerFlagsMask | containerFlags | Private, @@ -447,7 +447,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { } def transformMemberDefThreadSafe(x: ValOrDefDef)(using Context): Thicket = { - assert(!(x.symbol is Mutable)) + assert(!x.symbol.isMutableVarOrAccessor) if ctx.settings.YlegacyLazyVals.value then transformMemberDefThreadSafeLegacy(x) else diff --git a/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala b/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala index 95975ad9e6b8..b3ec05501b5b 100644 --- a/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala +++ b/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala @@ -28,7 +28,7 @@ class MoveStatics extends MiniPhase with SymTransformer { def transformSym(sym: SymDenotation)(using Context): SymDenotation = if (sym.hasAnnotation(defn.ScalaStaticAnnot) && sym.owner.is(Flags.Module) && sym.owner.companionClass.exists && - (sym.is(Flags.Method) || !(sym.is(Flags.Mutable) && sym.owner.companionClass.is(Flags.Trait)))) { + (sym.is(Flags.Method) || !(sym.isMutableVarOrAccessor && sym.owner.companionClass.is(Flags.Trait)))) { sym.owner.asClass.delete(sym.symbol) sym.owner.companionClass.asClass.enter(sym.symbol) sym.copySymDenotation(owner = sym.owner.companionClass) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 172ae337d6e6..e8227f759ad4 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -167,7 +167,11 @@ abstract class Recheck extends Phase, SymTransformer: * from the current type. */ def setNuType(tpe: Type): Unit = - if nuTypes.lookup(tree) == null && (tpe ne tree.tpe) then nuTypes(tree) = tpe + if nuTypes.lookup(tree) == null then updNuType(tpe) + + /** Set new type of the tree unconditionally. */ + def updNuType(tpe: Type): Unit = + if tpe ne tree.tpe then nuTypes(tree) = tpe /** The new type of the tree, or if none was installed, the original type */ def nuType(using Context): Type = @@ -255,7 +259,10 @@ abstract class Recheck extends Phase, SymTransformer: def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type = val resType = recheck(tree.tpt) - if tree.rhs.isEmpty then resType + def isUninitWildcard = tree.rhs match + case Ident(nme.WILDCARD) => tree.symbol.is(Mutable) + case _ => false + if tree.rhs.isEmpty || isUninitWildcard then resType else recheck(tree.rhs, resType) def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Type = diff --git a/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala b/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala index f22fc53e9b6e..7531b6e41c19 100644 --- a/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala +++ b/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala @@ -33,7 +33,7 @@ class UninitializedDefs extends MiniPhase: def recur(rhs: Tree): Boolean = rhs match case rhs: RefTree => rhs.symbol == defn.Compiletime_uninitialized - && tree.symbol.is(Mutable) && tree.symbol.owner.isClass + && tree.symbol.isMutableVarOrAccessor && tree.symbol.owner.isClass case closureDef(ddef) if defn.isContextFunctionType(tree.tpt.tpe.dealias) => recur(ddef.rhs) case _ => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 52760cf8b6c7..115037d1930a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -829,7 +829,7 @@ class Objects(using Context @constructorOnly): Bottom else if target.exists then def isNextFieldOfColonColon: Boolean = ref.klass == defn.ConsClass && target.name.toString == "next" - if target.isOneOf(Flags.Mutable) && !isNextFieldOfColonColon then + if target.isMutableVarOrAccessor && !isNextFieldOfColonColon then if ref.hasVar(target) then val addr = ref.varAddr(target) if addr.owner == State.currentObject then diff --git a/compiler/src/dotty/tools/dotc/transform/init/Util.scala b/compiler/src/dotty/tools/dotc/transform/init/Util.scala index e11d0e1e21a5..ca30e2d32a4d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Util.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Util.scala @@ -112,5 +112,5 @@ object Util: /** Whether the class or its super class/trait contains any mutable fields? */ def isMutable(cls: ClassSymbol)(using Context): Boolean = - cls.classInfo.decls.exists(_.is(Flags.Mutable)) || + cls.classInfo.decls.exists(_.isMutableVarOrAccessor) || cls.parentSyms.exists(parentCls => isMutable(parentCls.asClass)) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index e870ffd0fc90..ca12b4f64ff7 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -37,7 +37,7 @@ import config.Feature, Feature.{sourceVersion, modularity} import config.SourceVersion.* import config.MigrationVersion import printing.Formatting.hlAsKeyword -import cc.{isCaptureChecking, isRetainsLike} +import cc.{isCaptureChecking, isRetainsLike, isUpdateMethod} import collection.mutable import reporting.* @@ -578,7 +578,7 @@ object Checking { if (sym.isConstructor && !sym.isPrimaryConstructor && sym.owner.is(Trait, butNot = JavaDefined)) val addendum = if ctx.settings.Ydebug.value then s" ${sym.owner.flagsString}" else "" fail(em"Traits cannot have secondary constructors$addendum") - checkApplicable(Inline, sym.isTerm && !sym.isOneOf(Mutable | Module)) + checkApplicable(Inline, sym.isTerm && !sym.is(Module) && !sym.isMutableVarOrAccessor) checkApplicable(Lazy, !sym.isOneOf(Method | Mutable)) if (sym.isType && !sym.isOneOf(Deferred | JavaDefined)) for (cls <- sym.allOverriddenSymbols.filter(_.isClass)) { @@ -587,8 +587,12 @@ object Checking { } if sym.isWrappedToplevelDef && !sym.isType && sym.flags.is(Infix, butNot = Extension) then fail(ModifierNotAllowedForDefinition(Flags.Infix, s"A top-level ${sym.showKind} cannot be infix.")) + if sym.isUpdateMethod && !sym.owner.derivesFrom(defn.Caps_Mutable) then + fail(em"Update methods can only be used as members of classes deriving from the `Mutable` trait") checkApplicable(Erased, - !sym.isOneOf(MutableOrLazy, butNot = Given) && !sym.isType || sym.isClass) + !sym.is(Lazy, butNot = Given) + && !sym.isMutableVarOrAccessor + && (!sym.isType || sym.isClass)) checkCombination(Final, Open) checkCombination(Sealed, Open) checkCombination(Final, Sealed) diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index 13e75be75838..58119981dfc4 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -85,7 +85,7 @@ object ErrorReporting { /** An explanatory note to be added to error messages * when there's a problem with abstract var defs */ def abstractVarMessage(sym: Symbol): String = - if (sym.underlyingSymbol.is(Mutable)) + if sym.underlyingSymbol.isMutableVarOrAccessor then "\n(Note that variables need to be initialized to be defined)" else "" diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 310ca999f4c5..86b9a337e69a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -253,7 +253,7 @@ object Nullables: val mutables = infos.foldLeft(Set[TermRef]()): (ms, info) => ms.union( if info.asserted == null then Set.empty - else info.asserted.filter(_.symbol.is(Mutable))) + else info.asserted.filter(_.symbol.isMutableVarOrAccessor)) infos.extendWith(NotNullInfo(Set(), mutables)) end extension @@ -307,7 +307,7 @@ object Nullables: || s.isClass // not in a class || recur(s.owner)) - refSym.is(Mutable) // if it is immutable, we don't need to check the rest conditions + refSym.isMutableVarOrAccessor // if it is immutable, we don't need to check the rest conditions && refOwner.isTerm && recur(ctx.owner) end extension @@ -574,7 +574,7 @@ object Nullables: object dropNotNull extends TreeMap: var dropped: Boolean = false override def transform(t: Tree)(using Context) = t match - case AssertNotNull(t0) if t0.symbol.is(Mutable) => + case AssertNotNull(t0) if t0.symbol.isMutableVarOrAccessor => nullables.println(i"dropping $t") dropped = true transform(t0) diff --git a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala index 59993a69797d..4e7c4336b852 100644 --- a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala +++ b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala @@ -130,7 +130,7 @@ trait QuotesAndSplices { report.error("Open pattern expected an identifier", arg.srcPos) EmptyTree } - for arg <- typedArgs if arg.symbol.is(Mutable) do // TODO support these patterns. Possibly using scala.quoted.util.Var + for arg <- typedArgs if arg.symbol.isMutableVarOrAccessor do // TODO support these patterns. Possibly using scala.quoted.util.Var report.error("References to `var`s cannot be used in higher-order pattern", arg.srcPos) val argTypes = typedArgs.map(_.tpe.widenTermRefExpr) val patType = (tree.typeargs.isEmpty, tree.args.isEmpty) match diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 7e53b38b5f98..a10815815a7b 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -21,7 +21,7 @@ import config.MigrationVersion import config.Printers.refcheck import reporting.* import Constants.Constant -import cc.stripCapturing +import cc.{stripCapturing, isUpdateMethod} object RefChecks { import tpd.* @@ -594,7 +594,7 @@ object RefChecks { overrideError("needs `override` modifier") else if (other.is(AbsOverride) && other.isIncompleteIn(clazz) && !member.is(AbsOverride)) overrideError("needs `abstract override` modifiers") - else if member.is(Override) && other.is(Mutable) then + else if member.is(Override) && other.isMutableVarOrAccessor then overrideError("cannot override a mutable variable") else if (member.isAnyOverride && !(member.owner.thisType.baseClasses exists (_ isSubClass other.owner)) && @@ -615,6 +615,8 @@ object RefChecks { overrideError("is erased, cannot override non-erased member") else if (other.is(Erased) && !member.isOneOf(Erased | Inline)) // (1.9) overrideError("is not erased, cannot override erased member") + else if member.isUpdateMethod && !other.is(Mutable) then + overrideError(i"is an update method, cannot override a read-only method") else if other.is(Inline) && !member.is(Inline) then // (1.10) overrideError("is not inline, cannot implement an inline method") else if (other.isScala2Macro && !member.isScala2Macro) // (1.11) @@ -772,7 +774,7 @@ object RefChecks { // Give a specific error message for abstract vars based on why it fails: // It could be unimplemented, have only one accessor, or be uninitialized. - if (underlying.is(Mutable)) { + if underlying.isMutableVarOrAccessor then val isMultiple = grouped.getOrElse(underlying.name, Nil).size > 1 // If both getter and setter are missing, squelch the setter error. @@ -781,7 +783,6 @@ object RefChecks { if (member.isSetter) "\n(Note that an abstract var requires a setter in addition to the getter)" else if (member.isGetter && !isMultiple) "\n(Note that an abstract var requires a getter in addition to the setter)" else err.abstractVarMessage(member)) - } else if (underlying.is(Method)) { // If there is a concrete method whose name matches the unimplemented // abstract method, and a cursory examination of the difference reveals diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index c941ffe74e18..ddcd0f55e3ae 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1348,7 +1348,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer cpy.Assign(tree)(lhsCore, typed(tree.rhs, lhs1.tpe.widen)).withType(defn.UnitType) def canAssign(sym: Symbol) = - sym.is(Mutable, butNot = Accessor) || + sym.isMutableVar || ctx.owner.isPrimaryConstructor && !sym.is(Method) && sym.maybeOwner == ctx.owner.owner || // allow assignments from the primary constructor to class fields ctx.owner.name.is(TraitSetterName) || ctx.owner.isStaticConstructor diff --git a/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala b/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala index 3699ca80d011..0c2929283ee3 100644 --- a/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala +++ b/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala @@ -157,7 +157,7 @@ class VarianceChecker(using Context) { def isLocal = base.isAllOf(PrivateLocal) || base.is(Private) && !base.hasAnnotation(defn.AssignedNonLocallyAnnot) - if base.is(Mutable, butNot = Method) && !isLocal then + if base.isMutableVar && !isLocal then base.removeAnnotation(defn.AssignedNonLocallyAnnot) variance = 0 try checkInfo(base.info) diff --git a/docs/_docs/internals/exclusive-capabilities.md b/docs/_docs/internals/exclusive-capabilities.md new file mode 100644 index 000000000000..97c6592ac693 --- /dev/null +++ b/docs/_docs/internals/exclusive-capabilities.md @@ -0,0 +1,551 @@ +# Exclusive Capabilities + +Language design draft + + +## Capability Kinds + +A capability is called + - _exclusive_ if it is `cap` or it has an exclusive capability in its capture set. + - _shared_ otherwise. + +There is a new top capability `shared` which can be used as a capability for deriving shared capture sets. Other shared capabilities are created as read-only versions of exclusive capabilities. + +## Update Methods + +We introduce a new trait +```scala +trait Mutable +``` +It is used as a base trait for types that define _update methods_ using +a new modifier `mut`. + +`mut` can only be used in classes or objects extending `Mutable`. An update method is allowed to access exclusive capabilities in the method's environment. By contrast, a normal method in a type extending `Mutable` may access exclusive capabilities only if they are defined locally or passed to it in parameters. + +**Example:** +```scala +class Ref(init: Int) extends Mutable: + private var current = init + def get: Int = current + mut def put(x: Int): Unit = current = x +``` +Here, `put` needs to be declared as an update method since it accesses the exclusive write capability of the variable `current` in its environment. +`mut` can also be used on an inner class of a class or object extending `Mutable`. It gives all code in the class the right +to access exclusive capabilities in the class environment. Normal classes +can only access exclusive capabilities defined in the class or passed to it in parameters. + +```scala +object Registry extends Mutable: + var count = 0 + mut class Counter: + mut def next: Int = + count += 1 + count +``` +Normal method members of `Mutable` classes cannot call update methods. This is indicated since accesses in the callee are recorded in the caller. So if the callee captures exclusive capabilities so does the caller. + +An update method cannot implement or override a normal method, whereas normal methods may implement or override update methods. Since methods such as `toString` or `==` inherited from Object are normal methods, it follows that none of these methods may be implemented as an update method. + +The `apply` method of a function type is also a normal method, hence `Mutable` classes may not implement a function type with an update method as the `apply` method. + +## Mutable Types + +A type is called a _mutable_ if it extends `Mutable` and it has an update method or an update class as non-private member or constructor. + +When we create an instance of a mutable type we always add `cap` to its capture set. For instance, if class `Ref` is declared as shown previously then `new Ref(1)` has type `Ref[Int]^{cap}`. + +**Restriction:** A non-mutable type cannot be downcast by a pattern match to a mutable type. + +**Definition:** A class is _read_only_ if the following conditions are met: + + 1. It does not extend any exclusive capabilities from its environment. + 2. It does not take parameters with exclusive capabilities. + 3. It does not contain mutable fields, or fields that take exclusive capabilities. + +**Restriction:** If a class or trait extends `Mutable` all its parent classes or traits must either extend `Mutable` or be read-only. + +The idea is that when we upcast a reference to a type extending `Mutable` to a type that does not extend `Mutable`, we cannot possibly call a method on this reference that uses an exclusive capability. Indeed, by the previous restriction this class must be a read-only class, which means that none of the code implemented +in the class can access exclusive capabilities on its own. And we +also cannot override any of the methods of this class with a method +accessing exclusive capabilities, since such a method would have +to be an update method and update methods are not allowed to override regular methods. + + + +**Example:** + +Consider trait `IterableOnce` from the standard library. + +```scala +trait IterableOnce[+T] extends Mutable: + def iterator: Iterator[T]^{this} + mut def foreach(op: T => Unit): Unit + mut def exists(op: T => Boolean): Boolean + ... +``` +The trait is a mutable type with many update methods, among them `foreach` and `exists`. These need to be classified as `mut` because their implementation in the subtrait `Iterator` uses the update method `next`. +```scala +trait Iterator[T] extends IterableOnce[T]: + def iterator = this + def hasNext: Boolean + mut def next(): T + mut def foreach(op: T => Unit): Unit = ... + mut def exists(op; T => Boolean): Boolean = ... + ... +``` +But there are other implementations of `IterableOnce` that are not mutable types (even though they do indirectly extend the `Mutable` trait). Notably, collection classes implement `IterableOnce` by creating a fresh +`iterator` each time one is required. The mutation via `next()` is then restricted to the state of that iterator, whereas the underlying collection is unaffected. These implementations would implement each `mut` method in `IterableOnce` by a normal method without the `mut` modifier. + +```scala +trait Iterable[T] extends IterableOnce[T]: + def iterator = new Iterator[T] { ... } + def foreach(op: T => Unit) = iterator.foreach(op) + def exists(op: T => Boolean) = iterator.exists(op) +``` +Here, `Iterable` is not a mutable type since it has no update method as member. +All inherited update methods are (re-)implemented by normal methods. + +**Note:** One might think that we don't need a base trait `Mutable` since in any case +a mutable type is defined by the presence of update methods, not by what it extends. In fact the importance of `Mutable` is that it defines _the other methods_ as read-only methods that _cannot_ access exclusive capabilities. For types not extending `Mutable`, this is not the case. For instance, the `apply` method of a function type is not an update method and the type itself does not extend `Mutable`. But `apply` may well be implemented by +a method that accesses exclusive capabilities. + + + +## Read-only Capabilities + +If `x` is an exclusive capability of a type extending `Mutable`, `x.rd` is its associated, shared _read-only_ capability. + +`shared` can be understood as the read-only capability corresponding to `cap`. +```scala + shared = cap.rd +``` + +A _top capability_ is either `cap` or `shared`. + + +## Shorthands + +**Meaning of `^`:** + +The meaning of `^` and `=>` is the same as before: + + - `C^` means `C^{cap}`. + - `A => B` means `(A -> B)^{cap}`. + +**Implicitly added capture sets** + +A reference to a type extending any of the traits `Capability` or `Mutable` gets an implicit capture set `{shared}` in case no explicit capture set is given. + +For instance, a matrix multiplication method can be expressed as follows: + +```scala +class Matrix(nrows: Int, ncols: Int) extends Mutable: + mut def update(i: Int, j: Int, x: Double): Unit = ... + def apply(i: Int, j: Int): Double = ... + +def mul(a: Matrix, b: Matrix, c: Matrix^): Unit = + // multiply a and b, storing the result in c +``` +Here, `a` and `b` are implicitly read-only, and `c`'s type has capture set `cap`. I.e. with explicit capture sets this would read: +```scala +def mul(a: Matrix^{shared}, b: Matrix^{shared}, c: Matrix^{cap}): Unit +``` +Separation checking will then make sure that `a` and `b` must be different from `c`. + + +## Capture Sets + +As the previous example showed, we would like to use a `Mutable` type such as `Array` or `Matrix` in two permission levels: read-only and unrestricted. A standard technique is to invent a type qualifier such as "read-only" or "mutable" to indicate access permissions. What we would like to do instead is to combine the qualifier with the capture set of a type. So we +distinguish two kinds of capture sets: regular and read-only. Read-only sets can contain only shared capabilities. + +Internally, in the discussion that follows we use a label after the set to indicate its mode. `{...}_` is regular and `{...}rd` is read-only. We could envisage source language to specify read-only sets, e.g. something like + +```scala +{io, async}.rd +``` + +But in almost all cases we don't need an explicit mode in source code to indicate the kind of capture set, since the contents of the set itself tell us what kind it is. A capture set is assumed to be read-only if it is on a +type extending `Mutable` and it contains only shared capabilities, otherwise it is assumed to be regular. + +The read-only function `ro` maps capture sets to read-only capture sets. It is defined pointwise on capabilities as follows: + + - `ro ({ x1, ..., xn } _) = { ro(x1), ..., ro(xn) }` + - `ro(x) = x` if `x` is shared + - `ro(x) = x.rd` if `x` is exclusive + + + +## Subcapturing + +Subcapturing has to take the mode of capture sets into account. We let `m` stand for arbitrary modes. + +1. Rule (sc-var) comes in two variants. If `x` is defined as `S^C` then + + - `{x, xs} m <: (C u {xs}) m` + - `{x.rd, xs} m <: (ro(C) u {xs}) m` + +3. The subset rule works only between sets of the same kind: + + - `C _ <: C _ u {x}` + - `C rd <: C rd u {x}` if `x` is a shared capability. + +4. We can map regular capture sets to read-only sets: + + - `C _ <: ro(C) rd` + +5. Read-only capabilities in regular capture sets can be widened to exclusive capabilities: + + - `{x.rd, xs} _ <: {x, xs} _` + +One case where an explicit capture set mode would be useful concerns +refinements of type variable bounds, as in the following example. +```scala +class A: + type T <: Object^{x.rd, y} +class B extends A: + type T <: Object^{x.rd} +class C extends B: + type T = Matrix^{x.rd} +``` +We assume that `x` and `y` are exclusive capabilities. +The capture set of type `T` in class `C` is a read-only set since `Matrix` extends `Mutable`. But the capture sets of the occurrences of +`T` in `A` and `B` are regular. This leads to an error in bounds checking +the definition of `T` in `C` against the one in `B` +since read-only sets do not subcapture regular sets. We can fix the +problem by declaring the capture set in class `B` as read-only: +```scala +class B extends A: + type T <: Object^{x.rd}.rd +``` +But now a different problem arises since the capture set of `T` in `B` is +read-only but the capture set of `T` and `A` is regular. The capture set of +`T` in `A` cannot be made read-only since it contains an exclusive capability `y`. So we'd have to drop `y` and declare class `A` like this: +```scala +class A: + type T <: Object^{x.rd}.rd +``` + + + +## Accesses to Mutable Types + +A _read-only access_ is a reference `x` to a type extending `Mutable` with a regular capture set if the expected type is one of the following: + + - a value type that is not a mutable type, or + - a select prototype with a member that is a normal method or class (not an update method or class). + +A read-only access contributes the read-only capability `x.rd` to its environment (as formalized by _cv_). Other accesses contribute the full capability `x`. + +A reference `p.m` to an update method or class `m` of a mutable type is allowed only if `p`'s capture set is regular. + +If `e` is an expression of a type `T^cs` extending `Mutable` and the expected type is a value type that is not a mutable type, then the type of `e` is mapped to `T^ro(cs)`. + + +## Expression Typing + +An expression's type should never contain a top capability in its deep capture set. This is achieved by the following rules: + + - On var access `x`: + + - replace all direct capture sets with `x` + - replace all boxed caps with `x*` + + _Variant_: If the type of the typevar corresponding to a boxed cap can be uniquely reached by a path `this.p`, replace the `cap` with `x.p*`. + + - On select `t.foo` where `C` is the capture set of `t`: apply the SELECT rule, which amounts to: + + - replace all direct caps with `C` + - replace all boxed caps with `C*` + + - On applications: `t(args)`, `new C(args)` if the result type `T` contains `cap` (deeply): + + - create a fresh skolem `val sk: T` + - set result type to `sk.type` + + Skolem symbols are eliminated before they reach the type of the enclosing val or def. + + - When avoiding a variable in a local block, as in: + ```scala + { val x: T^ = ...; ... r: List[T^{x}] } + ``` + where the capture set of `x` contains a top capability, + replace `x` by a fresh skolem `val sk: T`. Alternatively: keep it as is, but don't widen it. + + +## Post Processing Right Hand Sides + +The type of the right hand sides of `val`s or `def`s is post-processed before it becomes the inferred type or is compared with the declared type. Post processing +means that all local skolems in the type are avoided, which might mean `cap` can now occur in the the type. + +However, if a local skolem `sk` has `cap` as underlying type, but is only used +in its read-only form `sk.rd` in the result type, we can drop the skolem instead of widening to `shared`. + +**Example:** + +```scala + def f(x: Int): Double = ... + + def precomputed(n: Int)(f: Int -> Double): Int -> Double = + val a: Array[Double]^ = Array.tabulate(n)(f) + a(_) +``` +Here, `Array.tabulate(n)(f)` returns a value of type `Array[Double]^{cap}`. +The last expression `a(_)` expands to the closure `idx => a(idx)`, which +has type `Int ->{a.rd} Double`, since `a` appears only in the context of a +selection with the `apply` method of `Array`, which is not an update method. The type of the enclosing block then has type `Int ->{sk.rd} Double` for a fresh skolem `sk`, +since `a` is no longer visible. After post processing, this type becomes +`Int -> Double`. + +This pattern allows to use mutation in the construction of a local data structure, returning a pure result when the construction is done. Such +data structures are said to have _transient mutability_. + +## Separation checking + +Separation checking checks that we don't have hidden aliases. A hidden alias arises when we have two definitions `x` and `y` with overlapping transitive capture sets that are not manifest in the types of `x` and `y` because one of these types has widened the alias to a top capability. + +Since expression types can't mention cap, widening happens only + - when passing an argument to a parameter + - when widening to a declared (result) type of a val or def + +**Definitions:** + + - The _transitive capture set_ `tcs(c)` of a capability `c` with underlying capture set `C` is `c` itself, plus the transitive capture set of `C`, but excluding `cap` or `shared`. + + - The _transitive capture set_ `tcs(C)` of a capture set C is the union + of `tcs(c)` for all elements `c` of `C`. + + - Two capture sets _interfere_ if one contains an exclusive capability `x` and the other + also contains `x` or contains the read-only capability `x.rd`. + + - If `C1 <: C2` and `C2` contains a top capability, then let `C2a` be `C2` without top capabilities. The hidden set `hidden(C1, C2)` of `C1` relative to `C2` is the smallest subset `C1h` of `C1` such that `C1 \ C1h <: C2a`. + + - If `T1 <: T2` then let the hidden set `hidden(T1, T2)` of `T1` relative to `T2` be the + union of all hidden sets of corresponding capture sets in `T1` and `T2`. + + +**Algorithm outline:** + + - Associate _shadowed sets_ with blocks, template statement sequences, applications, and val symbols. The idea is that a shadowed set gets populated when a capture reference is widened to cap. In that case the original references that were widened get added to the set. + + - After processing a `val x: T2 = t` with `t: T1` after post-processing: + + - If `T2` is declared, add `tcs(hidden(T1, T2))` to the shadowed set + of the enclosing statement sequence and remember it as `shadowed(x)`. + - If`T2` is inferred, add `tcs(T1)` to the shadowed set + of the enclosing statement sequence and remember it as `shadowed(x)`. + + - When processing the right hand side of a `def f(params): T2 = t` with `t: T1` after post-processing + + - If `T2` is declared, check that `shadowed*(hidden(T1, T2))` contains only local values (including skolems). + - If `T2` is inferred, check that `shadowed*(tcs(T1))` contains only local values (including skolems). + + Here, `shadowed*` is the transitive closure of `shadowed`. + + - When processing an application `p.f(arg1, ..., arg_n)`, after processing `p`, add its transitive capture set to the shadowed set of the call. Then, in sequence, process each argument by adding `tcs(hidden(T1, T2))` to the shadowed set of the call, where `T1` is the argument type and `T2` is the type of the formal parameter. + + - When adding a reference `r` or capture set `C` in `markFree` to enclosing environments, check that `tcs(r)` (respectively, `tcs(C)`) does not interfere with an enclosing shadowed set. + + +This requires, first, a linear processing of the program in evaluation order, and, second, that all capture sets are known. Normal rechecking violates both of these requirements. First, definitions +without declared result types are lazily rechecked using completers. Second, capture sets are constructed +incrementally. So we probably need a second scan after rechecking proper. In order not to duplicate work, we need to record during rechecking all additions to environments via `markFree`. + +**Notes:** + + - Mutable variables are not allowed to have top capabilities in their deep capture sets, so separation checking is not needed for checking var definitions or assignments. + + - A lazy val can be thought of conceptually as a value with possibly a capturing type and as a method computing that value. A reference to a lazy val is interpreted as a call to that method. It's use set is the reference to the lazy val itself as well as the use set of the called method. + + - + +## Escape Checking + +The rules for separation checking also check that capabilities do not escape. Separate +rules for explicitly preventing cap to be boxed or unboxed are not needed anymore. Consider the canonical `withFile` example: +```scala +def withFile[T](body: File^ => T): T = + ... + +withFile: f => + () => f.write("too late") +``` +Here, the argument to `withFile` has the dependent function type +```scala +(f: File^) -> () ->{f} Unit +``` +A non-dependent type is required so the expected result type of the closure is +``` +() ->{cap} Unit +``` +When typing a closure, we type an anonymous function. The result type of that function is determined by type inference. That means the generated closure looks like this +```scala +{ def $anon(f: File^): () ->{cap} Unit = + () => f.write("too late") + $anon +} +``` +By the rules of separation checking the hidden set of the body of $anon is `f`, which refers +to a value outside the rhs of `$anon`. This is illegal according to separation checking. + +In the last example, `f: File^` was an exclusive capability. But it could equally have been a shared capability, i.e. `withFile` could be formulated as follows: +```scala +def withFile[T](body: File^{shared} => T): T = +``` +The same reasoning as before would enforce that there are no leaks. + + +## Mutable Variables + +Local mutable variables are tracked by default. It is essentially as if a mutable variable `x` was decomposed into a new private field of class `Ref` together with a getter and setter. I.e. instead of +```scala +var x: T = init +``` +we'd deal with +```scala +val x$ = Ref[T](init) +def x = x$.get +mut def x_=(y: T) = x$.put(y) +``` + +There should be a way to exclude a mutable variable or field from tracking. Maybe an annotation or modifier such as `transparent` or `untracked`? + +The expansion outlined above justifies the following rules for handling mutable variables directly: + + - A type with non-private tracked mutable fields is classified as mutable. + It has to extend the `Mutable` class. + - A read access to a local mutable variable `x` charges the capability `x.rd` to the environment. + - An assignment to a local mutable variable `x` charges the capability `x` to the environment. + - A read access to a mutable field `this.x` charges the capability `this.rd` to the environment. + - A write access to a mutable field `this.x` charges the capability `this` to the environment. + +Mutable Scopes +============== + +We sometimes want to make separation checking coarser. For instance when constructing a doubly linked list we want to create `Mutable` objects and +store them in mutable variables. Since a variable's type cannot contain `cap`, +we must know beforehand what mutable objects it can be refer to. This is impossible if the other objects are created later. + +Mutable scopes provide a solution to this they permit to derive a set of variables from a common exclusive reference. We define a new class +```scala +class MutableScope extends Mutable +``` +To make mutable scopes useful, we need a small tweak +of the rule governing `new` in the _Mutable Types_ section. The previous rule was: + +> When we create an instance of a mutable type we always add `cap` to its capture set. + +The new rule is: + +> When we create an instance of a mutable type we search for a given value of type `MutableScope`. If such a value is found (say it is `ms`) then we use +`ms` as the capture set of the created instance. Otherwise we use `cap`. + +We could envisage using mutable scopes like this: +``` +object enclave: + private given ms: MutableScope() + + ... +``` +Within `enclave` all mutable objects have `ms` as their capture set. So they can contain variables that also have `ms` as their capture set of their values. + +Mutable scopes should count as mutable types (this can be done either by decree or by adding an update method to `MutableScope`). Hence, mutable scopes can themselves be nested inside other mutable scopes. + +## Consumed Capabilities + +We allow `consume` as a modifier on parameters and methods. Example: + +```scala +class C extends Capability + +class Channel[T]: + def send(consume x: T) + + + +class Buffer[+T] extends Mutable: + consume def append(x: T): Buffer[T]^ + +b.append(x) +b1.append(y) + +def concat[T](consume buf1: Buffer[T]^, buf2: Buffer[T]): Buffer[T]^ + +A ->{x.consume} B + + +A + + C , Gamma, x: S |- t; T + --------------------------- + , Gamma |- (x -> t): S ->C T + + + C, Gamma |- let x = s in t: T + + +class Iterator[T]: + consume def filter(p: T => Boolean): Iterator[T]^ + consume def exists(p: T => Boolean): Boolean +``` + +As a parameter, `consume` implies `^` as capture set of the parameter type. The `^` can be given, but is redundant. + +When a method with a `consume` parameter of type `T2^` is called with an argument of type `T1`, we add the elements of `tcs(hidden(T1, T2^))` not just to the enclosing shadowed set but to all enclosing shadowed sets where elements are visible. This makes these elements permanently inaccessible. + + + +val f = Future { ... } +val g = Future { ... } + + +A parameter is implicitly @unbox if it contains a boxed cap. Example: + +def apply[T](f: Box[T => T], y: T): T = + xs.head(y) + +def compose[T](fs: @unbox List[T => T]) = + xs.foldRight(identity)((f: T => T, g: T => T) => x => g(f(x))) + + + +compose(List(f, g)) + +f :: g :: Nil + +def compose[T](fs: List[Unbox[T => T]], x: T) = + val combined = (xs.foldRight(identity)((f: T => T, g: T => T) => x => g(f(x)))): T->{fs*} T + combined(x) + + +With explicit diff --git a/library/src/scala/annotation/internal/freshCapability.scala b/library/src/scala/annotation/internal/freshCapability.scala new file mode 100644 index 000000000000..a25eee4f4c6d --- /dev/null +++ b/library/src/scala/annotation/internal/freshCapability.scala @@ -0,0 +1,7 @@ +package scala.annotation +package internal + +/** An annotation used internally for fresh capability wrappers of `cap` + */ +class freshCapability extends StaticAnnotation + diff --git a/library/src/scala/annotation/internal/readOnlyCapability.scala b/library/src/scala/annotation/internal/readOnlyCapability.scala new file mode 100644 index 000000000000..8e939aea6bb9 --- /dev/null +++ b/library/src/scala/annotation/internal/readOnlyCapability.scala @@ -0,0 +1,7 @@ +package scala.annotation +package internal + +/** An annotation that marks a capture ref as a read-only capability. + * `x.rd` is encoded as `x.type @readOnlyCapability` + */ +class readOnlyCapability extends StaticAnnotation diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index c35b3b55e813..9d0a8883cde9 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -16,6 +16,8 @@ import annotation.{experimental, compileTimeOnly, retainsCap} @deprecated("Use `Capability` instead") type Cap = Capability + trait Mutable extends Capability + /** Carrier trait for capture set type parameters */ trait CapSet extends Any @@ -41,6 +43,12 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ extension (x: Any) def reachCapability: Any = x + /** Unique capabilities x! which appear as terms in @retains annotations are encoded + * as `caps.uniqueCapability(x)`. When converted to CaptureRef types in capture sets + * they are represented as `x.type @annotation.internal.uniqueCapability`. + */ + extension (x: Any) def readOnlyCapability: Any = x + /** A trait to allow expressing existential types such as * * (x: Exists) => A ->{x} B @@ -52,7 +60,12 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ final class untrackedCaptures extends annotation.StaticAnnotation - /** This should go into annotations. For now it is here, so that we + /** An annotation on parameters `x` stating that the method's body makes + * use of the reach capability `x*`. Consequently, when calling the method + * we need to charge the deep capture set of the actual argiment to the + * environment. + * + * Note: This should go into annotations. For now it is here, so that we * can experiment with it quickly between minor releases */ final class use extends annotation.StaticAnnotation @@ -66,4 +79,9 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ def unsafeAssumePure: T = x + /** A wrapper around code for which separation checks are suppressed. + */ + def unsafeAssumeSeparate[T](op: T): T = op + end unsafe +end caps \ No newline at end of file diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 00e7153bcb83..4723fd745d6a 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -13,6 +13,8 @@ object MiMaFilters { ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.quotedPatternsWithPolymorphicFunctions"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$quotedPatternsWithPolymorphicFunctions$"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.runtime.Patterns.higherOrderHoleWithTypes"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.freshCapability"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.readOnlyCapability"), ), // Additions since last LTS diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index 28ce8da104aa..853f3f9328a1 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -25,6 +25,7 @@ import scala.runtime.Statics import language.experimental.captureChecking import annotation.unchecked.uncheckedCaptures import caps.untrackedCaptures +import caps.unsafe.unsafeAssumeSeparate /** This class implements an immutable linked list. We call it "lazy" * because it computes its elements only when they are needed. @@ -682,7 +683,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz remaining -= 1 scout = scout.tail } - dropRightState(scout) + caps.unsafe.unsafeAssumeSeparate(dropRightState(scout)) } } @@ -879,6 +880,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz if (!cursor.stateDefined) b.append(sep).append("") } else { @inline def same(a: LazyListIterable[A]^, b: LazyListIterable[A]^): Boolean = (a eq b) || (a.state eq b.state) + // !!!CC with qualifiers, same should have cap.rd parameters // Cycle. // If we have a prefix of length P followed by a cycle of length C, // the scout will be at position (P%C) in the cycle when the cursor @@ -890,7 +892,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz // the start of the loop. var runner = this var k = 0 - while (!same(runner, scout)) { + while (!caps.unsafe.unsafeAssumeSeparate(same(runner, scout))) { runner = runner.tail scout = scout.tail k += 1 @@ -900,11 +902,11 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz // everything once. If cursor is already at beginning, we'd better // advance one first unless runner didn't go anywhere (in which case // we've already looped once). - if (same(cursor, scout) && (k > 0)) { + if (caps.unsafe.unsafeAssumeSeparate(same(cursor, scout)) && (k > 0)) { appendCursorElement() cursor = cursor.tail } - while (!same(cursor, scout)) { + while (!caps.unsafe.unsafeAssumeSeparate(same(cursor, scout))) { appendCursorElement() cursor = cursor.tail } @@ -1052,7 +1054,9 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { val head = it.next() rest = rest.tail restRef = rest // restRef.elem = rest - sCons(head, newLL(stateFromIteratorConcatSuffix(it)(flatMapImpl(rest, f).state))) + sCons(head, newLL( + caps.unsafe.unsafeAssumeSeparate( + stateFromIteratorConcatSuffix(it)(flatMapImpl(rest, f).state)))) } else State.Empty } } @@ -1181,7 +1185,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { def iterate[A](start: => A)(f: A => A): LazyListIterable[A]^{start, f} = newLL { val head = start - sCons(head, iterate(f(head))(f)) + sCons(head, unsafeAssumeSeparate(iterate(f(head))(f))) } /** diff --git a/tests/neg-custom-args/captures/boundschecks2.scala b/tests/neg-custom-args/captures/boundschecks2.scala index 923758d722f9..99366a8e7aff 100644 --- a/tests/neg-custom-args/captures/boundschecks2.scala +++ b/tests/neg-custom-args/captures/boundschecks2.scala @@ -8,6 +8,6 @@ object test { val foo: C[Tree^] = ??? // error type T = C[Tree^] // error - val bar: T -> T = ??? + //val bar: T -> T = ??? // --> boundschecks3.scala for what happens if we uncomment val baz: C[Tree^] -> Unit = ??? // error } diff --git a/tests/neg-custom-args/captures/boundschecks3.check b/tests/neg-custom-args/captures/boundschecks3.check new file mode 100644 index 000000000000..36e1336e8f05 --- /dev/null +++ b/tests/neg-custom-args/captures/boundschecks3.check @@ -0,0 +1,4 @@ +-- Error: tests/neg-custom-args/captures/boundschecks3.scala:11:13 ----------------------------------------------------- +11 | val bar: T -> T = ??? // error, since `T` is expanded here. But the msg is not very good. + | ^^^^^^ + | test.C[box test.Tree^] captures the root capability `cap` in invariant position diff --git a/tests/neg-custom-args/captures/boundschecks3.scala b/tests/neg-custom-args/captures/boundschecks3.scala new file mode 100644 index 000000000000..f5e9652c0913 --- /dev/null +++ b/tests/neg-custom-args/captures/boundschecks3.scala @@ -0,0 +1,13 @@ +object test { + + class Tree + + def f[X <: Tree](x: X): Unit = () + + class C[X <: Tree](x: X) + + val foo: C[Tree^] = ??? // hidden error + type T = C[Tree^] // hidden error + val bar: T -> T = ??? // error, since `T` is expanded here. But the msg is not very good. + val baz: C[Tree^] -> Unit = ??? // hidden error +} diff --git a/tests/neg-custom-args/captures/box-adapt-cases.check b/tests/neg-custom-args/captures/box-adapt-cases.check new file mode 100644 index 000000000000..dba556dd98a9 --- /dev/null +++ b/tests/neg-custom-args/captures/box-adapt-cases.check @@ -0,0 +1,21 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:7:10 ------------------------------- +7 | x.value(cap => cap.use()) // error, was OK + | ^^^^^^^^^^^^^^^^ + | Found: (cap: box Cap^?) => Int + | Required: (cap: box Cap^) ->{fresh} Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:14:10 ------------------------------ +14 | x.value(cap => cap.use()) // error + | ^^^^^^^^^^^^^^^^ + | Found: (cap: box Cap^?) ->{io} Int + | Required: (cap: box Cap^{io}) -> Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:28:10 ------------------------------ +28 | x.value(cap => cap.use()) // error + | ^^^^^^^^^^^^^^^^ + | Found: (cap: box Cap^?) ->{io, fs} Int + | Required: (cap: box Cap^{io, fs}) ->{io} Int + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/box-adapt-cases.scala b/tests/neg-custom-args/captures/box-adapt-cases.scala index d9ec0f80a548..150b3cc2c3e7 100644 --- a/tests/neg-custom-args/captures/box-adapt-cases.scala +++ b/tests/neg-custom-args/captures/box-adapt-cases.scala @@ -1,29 +1,29 @@ trait Cap { def use(): Int } def test1(): Unit = { - type Id[X] = [T] -> (op: X => T) -> T + class Id[X](val value: [T] -> (op: X => T) -> T) val x: Id[Cap^] = ??? - x(cap => cap.use()) + x.value(cap => cap.use()) // error, was OK } def test2(io: Cap^): Unit = { - type Id[X] = [T] -> (op: X -> T) -> T + class Id[X](val value: [T] -> (op: X -> T) -> T) val x: Id[Cap^{io}] = ??? - x(cap => cap.use()) // error + x.value(cap => cap.use()) // error } def test3(io: Cap^): Unit = { - type Id[X] = [T] -> (op: X ->{io} T) -> T + class Id[X](val value: [T] -> (op: X ->{io} T) -> T) val x: Id[Cap^{io}] = ??? - x(cap => cap.use()) // ok + x.value(cap => cap.use()) // ok } def test4(io: Cap^, fs: Cap^): Unit = { - type Id[X] = [T] -> (op: X ->{io} T) -> T + class Id[X](val value: [T] -> (op: X ->{io} T) -> T) val x: Id[Cap^{io, fs}] = ??? - x(cap => cap.use()) // error + x.value(cap => cap.use()) // error } diff --git a/tests/neg-custom-args/captures/box-adapt-cov.scala b/tests/neg-custom-args/captures/box-adapt-cov.scala index 2c1f15a5c77f..e1e6dd4cec1a 100644 --- a/tests/neg-custom-args/captures/box-adapt-cov.scala +++ b/tests/neg-custom-args/captures/box-adapt-cov.scala @@ -1,14 +1,14 @@ trait Cap def test1(io: Cap^) = { - type Op[X] = [T] -> Unit -> X + class Op[+X](val value: [T] -> Unit -> X) val f: Op[Cap^{io}] = ??? - val x: [T] -> Unit -> Cap^{io} = f // error + val x: [T] -> Unit -> Cap^{io} = f.value // error } def test2(io: Cap^) = { - type Op[X] = [T] -> Unit -> X^{io} + class Op[+X](val value: [T] -> Unit -> X^{io}) val f: Op[Cap^{io}] = ??? - val x: Unit -> Cap^{io} = f[Unit] // error - val x1: Unit ->{io} Cap^{io} = f[Unit] // ok + val x: Unit -> Cap^{io} = f.value[Unit] // error + val x1: Unit ->{io} Cap^{io} = f.value[Unit] // ok } diff --git a/tests/neg-custom-args/captures/box-adapt-depfun.scala b/tests/neg-custom-args/captures/box-adapt-depfun.scala index d1c1c73f8207..f673c657f753 100644 --- a/tests/neg-custom-args/captures/box-adapt-depfun.scala +++ b/tests/neg-custom-args/captures/box-adapt-depfun.scala @@ -1,23 +1,23 @@ trait Cap { def use(): Int } def test1(io: Cap^): Unit = { - type Id[X] = [T] -> (op: X ->{io} T) -> T + class Id[X](val value: [T] -> (op: X ->{io} T) -> T) val x: Id[Cap]^{io} = ??? - x(cap => cap.use()) // ok + x.value(cap => cap.use()) // ok } def test2(io: Cap^): Unit = { - type Id[X] = [T] -> (op: (x: X) ->{io} T) -> T + class Id[X](val value: [T] -> (op: (x: X) ->{io} T) -> T) val x: Id[Cap^{io}] = ??? - x(cap => cap.use()) + x.value(cap => cap.use()) // should work when the expected type is a dependent function } def test3(io: Cap^): Unit = { - type Id[X] = [T] -> (op: (x: X) ->{} T) -> T + class Id[X](val value: [T] -> (op: (x: X) ->{} T) -> T) val x: Id[Cap^{io}] = ??? - x(cap => cap.use()) // error + x.value(cap => cap.use()) // error } diff --git a/tests/neg-custom-args/captures/box-adapt-typefun.scala b/tests/neg-custom-args/captures/box-adapt-typefun.scala index 175acdda1c8f..76da047f42a9 100644 --- a/tests/neg-custom-args/captures/box-adapt-typefun.scala +++ b/tests/neg-custom-args/captures/box-adapt-typefun.scala @@ -1,13 +1,13 @@ trait Cap { def use(): Int } def test1(io: Cap^): Unit = { - type Op[X] = [T] -> X -> Unit + class Op[X](val value: [T] -> X -> Unit) val f: [T] -> (Cap^{io}) -> Unit = ??? - val op: Op[Cap^{io}] = f // error + val op: Op[Cap^{io}] = Op(f) // was error, now ok } def test2(io: Cap^): Unit = { - type Lazy[X] = [T] -> Unit -> X + class Lazy[X](val value: [T] -> Unit -> X) val f: Lazy[Cap^{io}] = ??? - val test: [T] -> Unit -> (Cap^{io}) = f // error + val test: [T] -> Unit -> (Cap^{io}) = f.value // error } diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index 1c113591922d..de2078ddf30a 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -8,10 +8,10 @@ -- Error: tests/neg-custom-args/captures/byname.scala:19:5 ------------------------------------------------------------- 19 | h(g()) // error | ^^^ - | reference (cap2 : Cap^) is not included in the allowed capture set {cap1} + | reference (cap2 : Cap) is not included in the allowed capture set {cap1} | of an enclosing function literal with expected type () ?->{cap1} I -- Error: tests/neg-custom-args/captures/byname.scala:22:12 ------------------------------------------------------------ 22 | h2(() => g())() // error | ^^^ - | reference (cap2 : Cap^) is not included in the allowed capture set {cap1} + | reference (cap2 : Cap) is not included in the allowed capture set {cap1} | of an enclosing function literal with expected type () ->{cap1} I diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index f63c55ca48c4..acf8faa7a969 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -36,15 +36,15 @@ -- Error: tests/neg-custom-args/captures/capt1.scala:34:16 ------------------------------------------------------------- 34 | val z2 = h[() -> Cap](() => x) // error // error | ^^^^^^^^^ - | Type variable X of method h cannot be instantiated to () -> box C^ since - | the part box C^ of that type captures the root capability `cap`. + | Type variable X of method h cannot be instantiated to () -> (ex$15: caps.Exists) -> C^{ex$15} since + | the part C^{ex$15} of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/capt1.scala:34:30 ------------------------------------------------------------- 34 | val z2 = h[() -> Cap](() => x) // error // error | ^ - | reference (x : C^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> box C^ + | reference (x : C^) is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> (ex$15: caps.Exists) -> C^{ex$15} -- Error: tests/neg-custom-args/captures/capt1.scala:36:13 ------------------------------------------------------------- 36 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error | ^^^^^^^^^^^^^^^^^^^^^^^ - | Type variable X of method h cannot be instantiated to box () ->{x} Cap since - | the part Cap of that type captures the root capability `cap`. + | Type variable X of method h cannot be instantiated to box () ->{x} (ex$20: caps.Exists) -> C^{ex$20} since + | the part C^{ex$20} of that type captures the root capability `cap`. diff --git a/tests/neg-custom-args/captures/cc-dep-param.check b/tests/neg-custom-args/captures/cc-dep-param.check new file mode 100644 index 000000000000..69b1e9f33412 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-dep-param.check @@ -0,0 +1,5 @@ +-- Error: tests/neg-custom-args/captures/cc-dep-param.scala:8:6 -------------------------------------------------------- +8 | foo(a, useA) // error: separation failure + | ^ + | Separation failure: argument of type (a : Foo[Int]^) to capture-polymorphic parameter + | of type Foo[Int]^ captures {a}, and this capability is also passed separately to method foo. diff --git a/tests/neg-custom-args/captures/cc-dep-param.scala b/tests/neg-custom-args/captures/cc-dep-param.scala new file mode 100644 index 000000000000..5fcb722a1a6c --- /dev/null +++ b/tests/neg-custom-args/captures/cc-dep-param.scala @@ -0,0 +1,8 @@ +import language.experimental.captureChecking + +trait Foo[T] +def test(): Unit = + val a: Foo[Int]^ = ??? + val useA: () ->{a} Unit = ??? + def foo[X](x: Foo[X]^, op: () ->{x} Unit): Unit = ??? + foo(a, useA) // error: separation failure diff --git a/tests/neg-custom-args/captures/cc-subst-param-exact.scala b/tests/neg-custom-args/captures/cc-subst-param-exact.scala index 35e4acb95fdc..2482bb18c727 100644 --- a/tests/neg-custom-args/captures/cc-subst-param-exact.scala +++ b/tests/neg-custom-args/captures/cc-subst-param-exact.scala @@ -5,13 +5,13 @@ trait Ref[T] { def set(x: T): T } def test() = { def swap[T](x: Ref[T]^)(y: Ref[T]^{x}): Unit = ??? - def foo[T](x: Ref[T]^): Unit = + def foo[T](x: Ref[T]^{cap.rd}): Unit = swap(x)(x) - def bar[T](x: () => Ref[T]^)(y: Ref[T]^{x}): Unit = + def bar[T](x: () => Ref[T]^{cap.rd})(y: Ref[T]^{x}): Unit = swap(x())(y) // error - def baz[T](x: Ref[T]^)(y: Ref[T]^{x}): Unit = + def baz[T](x: Ref[T]^{cap.rd})(y: Ref[T]^{x}): Unit = swap(x)(y) } @@ -19,15 +19,15 @@ trait IO type Op = () -> Unit def test2(c: IO^, f: Op^{c}) = { def run(io: IO^)(op: Op^{io}): Unit = op() - run(c)(f) + run(c)(f) // error: separation failure def bad(getIO: () => IO^, g: Op^{getIO}): Unit = - run(getIO())(g) // error + run(getIO())(g) // error // error: separation failure } def test3() = { def run(io: IO^)(op: Op^{io}): Unit = ??? val myIO: IO^ = ??? val myOp: Op^{myIO} = ??? - run(myIO)(myOp) + run(myIO)(myOp) // error: separation failure } diff --git a/tests/neg-custom-args/captures/cc-this5.check b/tests/neg-custom-args/captures/cc-this5.check index 21b5b36e0574..a69c482300f8 100644 --- a/tests/neg-custom-args/captures/cc-this5.check +++ b/tests/neg-custom-args/captures/cc-this5.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/cc-this5.scala:16:20 ---------------------------------------------------------- 16 | def f = println(c) // error | ^ - | reference (c : Cap^) is not included in the allowed capture set {} + | reference (c : Cap) is not included in the allowed capture set {} | of the enclosing class A -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-this5.scala:21:15 ------------------------------------- 21 | val x: A = this // error diff --git a/tests/neg-custom-args/captures/depfun-reach.check b/tests/neg-custom-args/captures/depfun-reach.check index c1d7d05dc8d6..676ca7c5104f 100644 --- a/tests/neg-custom-args/captures/depfun-reach.check +++ b/tests/neg-custom-args/captures/depfun-reach.check @@ -2,13 +2,13 @@ 13 | op // error | ^^ | Found: (xs: List[(X, box () ->{io} Unit)]) ->{op} List[box () ->{xs*} Unit] - | Required: (xs: List[(X, box () ->{io} Unit)]) => List[() -> Unit] + | Required: (xs: List[(X, box () ->{io} Unit)]) ->{fresh} List[() -> Unit] | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/depfun-reach.scala:20:60 --------------------------------- 20 | val b: (xs: List[() ->{io} Unit]) => List[() ->{} Unit] = a // error | ^ | Found: (xs: List[box () ->{io} Unit]) ->{a} List[box () ->{xs*} Unit] - | Required: (xs: List[box () ->{io} Unit]) => List[() -> Unit] + | Required: (xs: List[box () ->{io} Unit]) ->{fresh} List[() -> Unit] | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/effect-swaps.check b/tests/neg-custom-args/captures/effect-swaps.check index b74c165fd6b6..48dc46c09821 100644 --- a/tests/neg-custom-args/captures/effect-swaps.check +++ b/tests/neg-custom-args/captures/effect-swaps.check @@ -25,5 +25,5 @@ -- Error: tests/neg-custom-args/captures/effect-swaps.scala:66:15 ------------------------------------------------------ 66 | Result.make: // error: local reference leaks | ^^^^^^^^^^^ - |local reference contextual$9 from (using contextual$9: boundary.Label[Result[box Future[box T^?]^{fr, contextual$9}, box E^?]]^): + |local reference contextual$9 from (using contextual$9: boundary.Label[Result[box Future[box T^?]^{fr, contextual$9}, box E^?]]): | box Future[box T^?]^{fr, contextual$9} leaks into outer capture set of type parameter T of method make in object Result diff --git a/tests/neg-custom-args/captures/explain-under-approx.check b/tests/neg-custom-args/captures/explain-under-approx.check deleted file mode 100644 index c186fc6adb11..000000000000 --- a/tests/neg-custom-args/captures/explain-under-approx.check +++ /dev/null @@ -1,14 +0,0 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:12:10 ------------------------- -12 | col.add(Future(() => 25)) // error - | ^^^^^^^^^^^^^^^^ - | Found: Future[Int]{val a: (async : Async^)}^{async} - | Required: Future[Int]^{col.futs*} - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:15:11 ------------------------- -15 | col1.add(Future(() => 25)) // error - | ^^^^^^^^^^^^^^^^ - | Found: Future[Int]{val a: (async : Async^)}^{async} - | Required: Future[Int]^{col1.futs*} - | - | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/extending-cap-classes.check b/tests/neg-custom-args/captures/extending-cap-classes.check index 0936f48576e5..4a77a638a4d8 100644 --- a/tests/neg-custom-args/captures/extending-cap-classes.check +++ b/tests/neg-custom-args/captures/extending-cap-classes.check @@ -1,21 +1,21 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:7:15 ------------------------- 7 | val x2: C1 = new C2 // error | ^^^^^^ - | Found: C2^ + | Found: C2 | Required: C1 | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:8:15 ------------------------- 8 | val x3: C1 = new C3 // error | ^^^^^^ - | Found: C3^ + | Found: C3 | Required: C1 | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:13:15 ------------------------ 13 | val z2: C1 = y2 // error | ^^ - | Found: (y2 : C2^) + | Found: (y2 : C2) | Required: C1 | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/filevar-expanded.check b/tests/neg-custom-args/captures/filevar-expanded.check new file mode 100644 index 000000000000..13df5fc0c9c4 --- /dev/null +++ b/tests/neg-custom-args/captures/filevar-expanded.check @@ -0,0 +1,9 @@ +-- Error: tests/neg-custom-args/captures/filevar-expanded.scala:34:19 -------------------------------------------------- +34 | withFile(io3): f => // error: separation failure + | ^ + |Separation failure: argument of type (f: test2.File^{io3}) ->{io3} Unit to capture-polymorphic parameter + |of type (f: test2.File^{io3}) => Unit captures {io3}, and this capability is also passed separately to method withFile. +35 | val o = Service(io3) +36 | o.file = f // this is a bit dubious. It's legal since we treat class refinements +37 | // as capture set variables that can be made to include refs coming from outside. +38 | o.log diff --git a/tests/pos-custom-args/captures/filevar-expanded.scala b/tests/neg-custom-args/captures/filevar-expanded.scala similarity index 94% rename from tests/pos-custom-args/captures/filevar-expanded.scala rename to tests/neg-custom-args/captures/filevar-expanded.scala index 58e7a0e67e0a..461a617bde0d 100644 --- a/tests/pos-custom-args/captures/filevar-expanded.scala +++ b/tests/neg-custom-args/captures/filevar-expanded.scala @@ -31,7 +31,7 @@ object test2: op(new File) def test(io3: IO^) = - withFile(io3): f => + withFile(io3): f => // error: separation failure val o = Service(io3) o.file = f // this is a bit dubious. It's legal since we treat class refinements // as capture set variables that can be made to include refs coming from outside. diff --git a/tests/neg-custom-args/captures/function-combinators.check b/tests/neg-custom-args/captures/function-combinators.check new file mode 100644 index 000000000000..e8dfcf79112c --- /dev/null +++ b/tests/neg-custom-args/captures/function-combinators.check @@ -0,0 +1,10 @@ +-- Error: tests/neg-custom-args/captures/function-combinators.scala:10:21 ---------------------------------------------- +10 | val a1 = f.andThen(f); // error: separation failure + | ^ + | Separation failure: argument of type (f : Int => Int) to capture-polymorphic parameter + | of type Int => Int captures {f}, and this capability is also passed separately to method andThen. +-- Error: tests/neg-custom-args/captures/function-combinators.scala:16:22 ---------------------------------------------- +16 | val b2 = g1.andThen(g1); // error: separation failure + | ^^ + | Separation failure: argument of type (g1 : Int ->{ctx1} Int) to capture-polymorphic parameter + | of type Int => Int captures {ctx1}, and this capability is also passed separately to method andThen. diff --git a/tests/neg-custom-args/captures/function-combinators.scala b/tests/neg-custom-args/captures/function-combinators.scala new file mode 100644 index 000000000000..a96ab54416ae --- /dev/null +++ b/tests/neg-custom-args/captures/function-combinators.scala @@ -0,0 +1,31 @@ +class ContextClass +type Context = ContextClass^ +import caps.unsafe.unsafeAssumePure + +def Test(using ctx1: Context, ctx2: Context) = + val f: Int => Int = identity + val g1: Int ->{ctx1} Int = identity + val g2: Int ->{ctx2} Int = identity + val h: Int -> Int = identity + val a1 = f.andThen(f); // error: separation failure + val _: Int ->{f} Int = a1 + val a2 = f.andThen(g1); val _: Int ->{f, g1} Int = a2 + val a3 = f.andThen(g2); val _: Int ->{f, g2} Int = a3 + val a4 = f.andThen(h); val _: Int ->{f} Int = a4 + val b1 = g1.andThen(f); val _: Int ->{f, g1} Int = b1 + val b2 = g1.andThen(g1); // error: separation failure + val _: Int ->{g1} Int = b2 + val b3 = g1.andThen(g2); val _: Int ->{g1, g2} Int = b3 + val b4 = g1.andThen(h); val _: Int ->{g1} Int = b4 + val c1 = h.andThen(f); val _: Int ->{f} Int = c1 + val c2 = h.andThen(g1); val _: Int ->{g1} Int = c2 + val c3 = h.andThen(g2); val _: Int ->{g2} Int = c3 + val c4 = h.andThen(h); val _: Int -> Int = c4 + + val f2: (Int, Int) => Int = _ + _ + val f2c = f2.curried; val _: Int -> Int ->{f2} Int = f2c + val f2t = f2.tupled; val _: ((Int, Int)) ->{f2} Int = f2t + + val f3: (Int, Int, Int) => Int = ??? + val f3c = f3.curried; val _: Int -> Int -> Int ->{f3} Int = f3c + val f3t = f3.tupled; val _: ((Int, Int, Int)) ->{f3} Int = f3t diff --git a/tests/neg-custom-args/captures/i15922.scala b/tests/neg-custom-args/captures/i15922.scala index 848a22fe5341..b8bcc346ef81 100644 --- a/tests/neg-custom-args/captures/i15922.scala +++ b/tests/neg-custom-args/captures/i15922.scala @@ -1,8 +1,8 @@ trait Cap { def use(): Int } -type Id[X] = [T] -> (op: X => T) -> T -def mkId[X](x: X): Id[X] = [T] => (op: X => T) => op(x) +class Id[+X](val value: [T] -> (op: X => T) -> T) +def mkId[X](x: X): Id[X] = Id([T] => (op: X => T) => op(x)) def withCap[X](op: (Cap^) => X): X = { val cap: Cap^ = new Cap { def use() = { println("cap is used"); 0 } } diff --git a/tests/neg-custom-args/captures/i16725.scala b/tests/neg-custom-args/captures/i16725.scala index 1accf197c626..96cf44e72f3c 100644 --- a/tests/neg-custom-args/captures/i16725.scala +++ b/tests/neg-custom-args/captures/i16725.scala @@ -3,12 +3,12 @@ class IO extends caps.Capability: def brewCoffee(): Unit = ??? def usingIO[T](op: IO => T): T = ??? -type Wrapper[T] = [R] -> (f: T => R) -> R -def mk[T](x: T): Wrapper[T] = [R] => f => f(x) +class Wrapper[T](val value: [R] -> (f: T => R) -> R) +def mk[T](x: T): Wrapper[T] = Wrapper([R] => f => f(x)) def useWrappedIO(wrapper: Wrapper[IO]): () -> Unit = () => - wrapper: io => // error + wrapper.value: io => // error io.brewCoffee() def main(): Unit = - val escaped = usingIO(io => useWrappedIO(mk(io))) + val escaped = usingIO(io => useWrappedIO(mk(io))) // error escaped() // boom diff --git a/tests/neg-custom-args/captures/i19330.check b/tests/neg-custom-args/captures/i19330.check new file mode 100644 index 000000000000..78219e0316ee --- /dev/null +++ b/tests/neg-custom-args/captures/i19330.check @@ -0,0 +1,12 @@ +-- Error: tests/neg-custom-args/captures/i19330.scala:15:29 ------------------------------------------------------------ +15 | val leaked = usingLogger[x.T]: l => // error + | ^^^ + | Type variable T of method usingLogger cannot be instantiated to x.T since + | the part () => Logger^ of that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i19330.scala:22:22 --------------------------------------- +22 | val bad: bar.T = foo(bar) // error + | ^^^^^^^^ + | Found: () => Logger^ + | Required: () ->{fresh} (ex$9: caps.Exists) -> Logger^{ex$9} + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i19330.scala b/tests/neg-custom-args/captures/i19330.scala index 715b670860cd..3e6131fc51b3 100644 --- a/tests/neg-custom-args/captures/i19330.scala +++ b/tests/neg-custom-args/captures/i19330.scala @@ -19,5 +19,5 @@ def foo(x: Foo): x.T = def test(): Unit = val bar = new Bar - val bad: bar.T = foo(bar) + val bad: bar.T = foo(bar) // error val leaked: Logger^ = bad() // leaked scoped capability! diff --git a/tests/neg-custom-args/captures/i21401.check b/tests/neg-custom-args/captures/i21401.check index 679c451949bd..e7483e10bfa6 100644 --- a/tests/neg-custom-args/captures/i21401.check +++ b/tests/neg-custom-args/captures/i21401.check @@ -11,8 +11,8 @@ -- Error: tests/neg-custom-args/captures/i21401.scala:16:66 ------------------------------------------------------------ 16 | val leaked: [R, X <: Boxed[IO^] -> R] -> (op: X) -> R = usingIO[Res](mkRes) // error | ^^^ - | Type variable R of method usingIO cannot be instantiated to Res since - | the part box IO^ of that type captures the root capability `cap`. + | Type variable R of method usingIO cannot be instantiated to [R, X <: Boxed[box IO^] -> R] => (op: X) -> R since + | the part box IO^ of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/i21401.scala:17:29 ------------------------------------------------------------ 17 | val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) // error // error | ^^^^^^^^^^ @@ -21,8 +21,8 @@ -- Error: tests/neg-custom-args/captures/i21401.scala:17:52 ------------------------------------------------------------ 17 | val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) // error // error | ^^^^^^^^^^^^^^^^^^^^^^^^ - |Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> (ex$18: caps.Exists) -> Boxed[box IO^{ex$18}] since - |the part box IO^{ex$18} of that type captures the root capability `cap`. + |Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> (ex$20: caps.Exists) -> Boxed[box IO^{ex$20}] since + |the part box IO^{ex$20} of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/i21401.scala:18:21 ------------------------------------------------------------ 18 | val y: IO^{x*} = x.unbox // error | ^^^^^^^ diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index f4967253455f..109283eae01f 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -1,17 +1,20 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:12:33 --------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:12:12 --------------------------------------- 12 | files.map((f: F) => new Logger(f)) // error, Q: can we make this pass (see #19076)? - | ^ - | Found: (f : F^) - | Required: File^ + | ^^^^^^^^^^^^^^^^^^^^^^^ + | Found: (f: F) ->{files.rd*} box Logger{val f²: File^?}^? + | Required: (f: box F^{files.rd*}) ->{fresh} box Logger{val f²: File^?}^? + | + | where: f is a reference to a value parameter + | f² is a value in class Logger | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:15:12 --------------------------------------- 15 | files.map(new Logger(_)) // error, Q: can we improve the error message? | ^^^^^^^^^^^^^ - | Found: (_$1: box File^{files*}) ->{files*} (ex$16: caps.Exists) -> box Logger{val f: File^{_$1}}^{ex$16} - | Required: (_$1: box File^{files*}) => box Logger{val f: File^?}^? + |Found: (_$1: box File^{files*}) ->{files*} (ex$16: caps.Exists) -> box Logger{val f: File^{_$1}}^{ex$16.rd, _$1} + |Required: (_$1: box File^{files*}) => box Logger{val f: File^?}^? | - | Note that the universal capability `cap` - | cannot be included in capture set ? + |Note that reference ex$16.rd + |cannot be included in outer capture set ? | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check index 111719a81f07..bdd053910ac8 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.check +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/lazylists-exceptions.scala:36:2 ----------------------------------------------- 36 | try // error | ^ - | The result of `try` cannot have type LazyList[Int]^ since + | The result of `try` cannot have type LazyList[Int]^{cap.rd} since | that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 37 | tabulate(10) { i => diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index 8683615c07d8..634b77d47c91 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -26,3 +26,8 @@ | Required: LazyRef[Int]^{cap1} | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/lazyref.scala:24:55 ----------------------------------------------------------- +24 | val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) // error: separation failure + | ^ + | Separation failure: argument of type (x: Int) ->{cap2} Int to capture-polymorphic parameter + | of type Int => Int captures {cap2}, and this capability is also passed separately to method map. diff --git a/tests/neg-custom-args/captures/lazyref.scala b/tests/neg-custom-args/captures/lazyref.scala index 99aa10d5d2b2..9772b10abf50 100644 --- a/tests/neg-custom-args/captures/lazyref.scala +++ b/tests/neg-custom-args/captures/lazyref.scala @@ -21,5 +21,5 @@ def test(cap1: Cap, cap2: Cap) = val ref2c: LazyRef[Int]^{cap2} = ref2 // error val ref3 = ref1.map(g) val ref3c: LazyRef[Int]^{ref1} = ref3 // error - val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) + val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) // error: separation failure val ref4c: LazyRef[Int]^{cap1} = ref4 // error diff --git a/tests/neg-custom-args/captures/mut-outside-mutable.check b/tests/neg-custom-args/captures/mut-outside-mutable.check new file mode 100644 index 000000000000..0407f35745b9 --- /dev/null +++ b/tests/neg-custom-args/captures/mut-outside-mutable.check @@ -0,0 +1,8 @@ +-- Error: tests/neg-custom-args/captures/mut-outside-mutable.scala:5:10 ------------------------------------------------ +5 | mut def foreach(op: T => Unit): Unit // error + | ^ + | Update methods can only be used as members of classes deriving from the `Mutable` trait +-- Error: tests/neg-custom-args/captures/mut-outside-mutable.scala:9:12 ------------------------------------------------ +9 | mut def baz() = 1 // error + | ^ + | Update methods can only be used as members of classes deriving from the `Mutable` trait diff --git a/tests/neg-custom-args/captures/mut-outside-mutable.scala b/tests/neg-custom-args/captures/mut-outside-mutable.scala new file mode 100644 index 000000000000..18c0e59c5bd8 --- /dev/null +++ b/tests/neg-custom-args/captures/mut-outside-mutable.scala @@ -0,0 +1,10 @@ +import caps.Mutable + +trait IterableOnce[T]: + def iterator: Iterator[T]^{this} + mut def foreach(op: T => Unit): Unit // error + +trait Foo extends Mutable: + def bar = + mut def baz() = 1 // error + baz() diff --git a/tests/neg-custom-args/captures/mut-override.scala b/tests/neg-custom-args/captures/mut-override.scala new file mode 100644 index 000000000000..848e4d880223 --- /dev/null +++ b/tests/neg-custom-args/captures/mut-override.scala @@ -0,0 +1,19 @@ +import caps.Mutable + +trait IterableOnce[T] extends Mutable: + def iterator: Iterator[T]^{this} + mut def foreach(op: T => Unit): Unit + +trait Iterator[T] extends IterableOnce[T]: + def iterator = this + def hasNext: Boolean + mut def next(): T + mut def foreach(op: T => Unit): Unit = ??? + override mut def toString = ??? // error + +trait Iterable[T] extends IterableOnce[T]: + def iterator: Iterator[T] = ??? + def foreach(op: T => Unit) = iterator.foreach(op) + +trait BadIterator[T] extends Iterator[T]: + override mut def hasNext: Boolean // error diff --git a/tests/neg-custom-args/captures/outer-var.check b/tests/neg-custom-args/captures/outer-var.check index 72af842728a1..a5b077cd8d32 100644 --- a/tests/neg-custom-args/captures/outer-var.check +++ b/tests/neg-custom-args/captures/outer-var.check @@ -1,7 +1,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:11:8 ------------------------------------- 11 | x = q // error | ^ - | Found: (q : Proc) + | Found: (q : () => Unit) | Required: () ->{p, q²} Unit | | where: q is a parameter in method inner @@ -11,14 +11,14 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:12:9 ------------------------------------- 12 | x = (q: Proc) // error | ^^^^^^^ - | Found: Proc + | Found: () => Unit | Required: () ->{p, q} Unit | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:13:9 ------------------------------------- 13 | y = (q: Proc) // error | ^^^^^^^ - | Found: Proc + | Found: () => Unit | Required: () ->{p} Unit | | Note that the universal capability `cap` @@ -28,11 +28,11 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:14:8 ------------------------------------- 14 | y = q // error, was OK under unsealed | ^ - | Found: (q : Proc) + | Found: (q : () => Unit) | Required: () ->{p} Unit | - | Note that reference (q : Proc), defined in method inner - | cannot be included in outer capture set {p} of variable y + | Note that reference (q : () => Unit), defined in method inner + | cannot be included in outer capture set {p} | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/outer-var.scala:16:57 --------------------------------------------------------- diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 7c00fa7299fe..ef755ebfcbd2 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -25,6 +25,20 @@ | ^^^^^^^^^^ | Type variable T of constructor Ref cannot be instantiated to List[box () => Unit] since | the part box () => Unit of that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:45:35 -------------------------------------- +45 | val next: () => Unit = cur.get.head // error + | ^^^^^^^^^^^^ + | Found: () => Unit + | Required: () ->{fresh} Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:47:20 -------------------------------------- +47 | cur.set(cur.get.tail: List[Proc]) // error + | ^^^^^^^^^^^^ + | Found: List[box () => Unit] + | Required: List[box () ->{fresh} Unit] + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/reaches.scala:53:51 ----------------------------------------------------------- 53 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error | ^ diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index a9773b76f445..fcb64355b6cd 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -42,9 +42,9 @@ def runAll2(xs: List[Proc]): Unit = def runAll3(xs: List[Proc]): Unit = val cur = Ref[List[Proc]](xs) // error while cur.get.nonEmpty do - val next: () => Unit = cur.get.head + val next: () => Unit = cur.get.head // error next() - cur.set(cur.get.tail: List[Proc]) + cur.set(cur.get.tail: List[Proc]) // error class Id[-A, +B >: A](): def apply(a: A): B = a diff --git a/tests/neg-custom-args/captures/readOnly.check b/tests/neg-custom-args/captures/readOnly.check new file mode 100644 index 000000000000..e1aed07657e5 --- /dev/null +++ b/tests/neg-custom-args/captures/readOnly.check @@ -0,0 +1,19 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/readOnly.scala:14:21 ------------------------------------- +14 | val _: () -> Int = getA // error + | ^^^^ + | Found: (getA : () ->{a.rd} Int) + | Required: () -> Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/readOnly.scala:17:23 ------------------------------------- +17 | val _: Int -> Unit = putA // error + | ^^^^ + | Found: (putA : (x$0: Int) ->{a} Unit) + | Required: Int -> Unit + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/readOnly.scala:20:23 ---------------------------------------------------------- +20 | val doit = () => z.put(x.get max y.get) // error + | ^^^^^ + | cannot call update method put from (z : Ref), + | since its capture set {z} is read-only diff --git a/tests/neg-custom-args/captures/readOnly.scala b/tests/neg-custom-args/captures/readOnly.scala new file mode 100644 index 000000000000..4edea6638980 --- /dev/null +++ b/tests/neg-custom-args/captures/readOnly.scala @@ -0,0 +1,22 @@ +import caps.Mutable +import caps.cap + +class Ref(init: Int) extends Mutable: + private var current = init + def get: Int = current + mut def put(x: Int): Unit = current = x + +def Test(c: Object^) = + val a: Ref^ = Ref(1) + val b: Ref^ = Ref(2) + + val getA = () => a.get + val _: () -> Int = getA // error + + val putA = (x: Int) => a.put(x) + val _: Int -> Unit = putA // error + + def setMax(x: Ref^{cap.rd}, y: Ref^{cap.rd}, z: Ref^{cap.rd}) = + val doit = () => z.put(x.get max y.get) // error + val _: () ->{x.rd, y.rd, z} Unit = doit + doit() diff --git a/tests/neg-custom-args/captures/real-try.check b/tests/neg-custom-args/captures/real-try.check index 7a4b12ac08f6..6b478b48515a 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -7,7 +7,7 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:14:2 ----------------------------------------------------------- 14 | try // error | ^ - | The result of `try` cannot have type () => Unit since + | The result of `try` cannot have type () ->{cap.rd} Unit since | that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 15 | () => foo(1) @@ -17,7 +17,7 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:20:10 ---------------------------------------------------------- 20 | val x = try // error | ^ - | The result of `try` cannot have type () => Unit since + | The result of `try` cannot have type () ->{cap.rd} Unit since | that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 21 | () => foo(1) @@ -27,7 +27,7 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:26:10 ---------------------------------------------------------- 26 | val y = try // error | ^ - | The result of `try` cannot have type () => Cell[Unit]^? since + | The result of `try` cannot have type () ->{cap.rd} Cell[Unit]^? since | that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 27 | () => Cell(foo(1)) @@ -37,8 +37,8 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:32:10 ---------------------------------------------------------- 32 | val b = try // error | ^ - | The result of `try` cannot have type Cell[box () => Unit]^? since - | the part box () => Unit of that type captures the root capability `cap`. + | The result of `try` cannot have type Cell[box () ->{cap.rd} Unit]^? since + | the part box () ->{cap.rd} Unit of that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 33 | Cell(() => foo(1)) 34 | catch diff --git a/tests/neg-custom-args/captures/sepchecks.scala b/tests/neg-custom-args/captures/sepchecks.scala new file mode 100644 index 000000000000..67e4cadaf992 --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks.scala @@ -0,0 +1,61 @@ +import caps.Mutable +import caps.cap + +trait Rdr[T]: + def get: T + +class Ref[T](init: T) extends Rdr[T], Mutable: + private var current = init + def get: T = current + mut def put(x: T): Unit = current = x + +def Test(c: Object^): Unit = + val a: Ref[Int]^ = Ref(1) + val b: Ref[Int]^ = Ref(2) + def aa = a + + val getA = () => a.get + val _: () ->{a.rd} Int = getA + + val putA = (x: Int) => a.put(x) + val _: Int ->{a} Unit = putA + + def setMax(x: Ref[Int]^{cap.rd}, y: Ref[Int]^{cap.rd}, z: Ref[Int]^{cap}) = + val doit = () => z.put(x.get max y.get) + val _: () ->{x.rd, y.rd, z} Unit = doit + doit() + + def setMax2(x: Rdr[Int]^{cap.rd}, y: Rdr[Int]^{cap.rd}, z: Ref[Int]^{cap}) = ??? + + setMax2(aa, aa, b) + setMax2(a, aa, b) + setMax2(a, b, b) // error + setMax2(b, b, b) // error + + abstract class IMatrix: + def apply(i: Int, j: Int): Double + + class Matrix(nrows: Int, ncols: Int) extends IMatrix, Mutable: + val arr = Array.fill(nrows, ncols)(0.0) + def apply(i: Int, j: Int): Double = arr(i)(j) + mut def update(i: Int, j: Int, x: Double): Unit = arr(i)(j) = x + + def mul(x: IMatrix^{cap.rd}, y: IMatrix^{cap.rd}, z: Matrix^): Matrix^ = ??? + + val m1 = Matrix(10, 10) + val m2 = Matrix(10, 10) + mul(m1, m2, m2) // error: will fail separation checking + mul(m1, m1, m2) // ok + + def move(get: () => Int, set: Int => Unit) = + set(get()) + + val geta = () => a.get + + def get2(x: () => Int, y: () => Int): (Int, Int) = + (x(), y()) + + move(geta, b.put(_)) // ok + move(geta, a.put(_)) // error + get2(geta, geta) // ok + get2(geta, () => a.get) // ok \ No newline at end of file diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 72604451472c..23c1b056c659 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -6,8 +6,8 @@ -- Error: tests/neg-custom-args/captures/try.scala:30:65 --------------------------------------------------------------- 30 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error | ^ - | reference (x : CanThrow[Exception]) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () ->? Nothing + | reference (x : CT[Exception]^) is not included in the allowed capture set {} + | of an enclosing function literal with expected type () ->? Nothing -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:52:2 ------------------------------------------- 47 |val global: () -> Int = handle { 48 | (x: CanThrow[Exception]) => diff --git a/tests/neg-custom-args/captures/unsound-reach-2.scala b/tests/neg-custom-args/captures/unsound-reach-2.scala index c7dfa117a2fe..f0b163a687e6 100644 --- a/tests/neg-custom-args/captures/unsound-reach-2.scala +++ b/tests/neg-custom-args/captures/unsound-reach-2.scala @@ -13,7 +13,7 @@ class Bar extends Foo[File^]: // error def use(x: File^)(op: Consumer[File^]): Unit = op.apply(x) def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/unsound-reach-3.scala b/tests/neg-custom-args/captures/unsound-reach-3.scala index c5cdfca9d87a..32feb5f73e76 100644 --- a/tests/neg-custom-args/captures/unsound-reach-3.scala +++ b/tests/neg-custom-args/captures/unsound-reach-3.scala @@ -12,7 +12,7 @@ class Bar extends Foo[File^]: // error def use(x: File^): File^ = x def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/unsound-reach-4.check b/tests/neg-custom-args/captures/unsound-reach-4.check index ca95bf42ba59..2d00eb0364e0 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.check +++ b/tests/neg-custom-args/captures/unsound-reach-4.check @@ -3,6 +3,13 @@ | ^^^^^^^^^^ | Type variable X of trait Foo cannot be instantiated to File^ since | that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach-4.scala:17:29 ------------------------------ +17 | val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). + | ^^^^^^^ + | Found: Bar^? + | Required: Foo[box File^] + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:22:22 --------------------------------------------------- 22 | escaped = boom.use(f) // error | ^^^^^^^^^^^ diff --git a/tests/neg-custom-args/captures/unsound-reach-4.scala b/tests/neg-custom-args/captures/unsound-reach-4.scala index 88fbc2f5c1de..efda110d1989 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.scala +++ b/tests/neg-custom-args/captures/unsound-reach-4.scala @@ -14,7 +14,7 @@ class Bar extends Foo[File^]: // error def use(x: F): File^ = x def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/unsound-reach.check b/tests/neg-custom-args/captures/unsound-reach.check index 69794f569edb..17d4a4420833 100644 --- a/tests/neg-custom-args/captures/unsound-reach.check +++ b/tests/neg-custom-args/captures/unsound-reach.check @@ -8,6 +8,13 @@ | ^ | Type variable X of constructor Foo2 cannot be instantiated to box File^ since | that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach.scala:18:31 -------------------------------- +18 | val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). + | ^^^^^^^ + | Found: Bar^? + | Required: Foo[box File^] + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/unsound-reach.scala:23:21 ----------------------------------------------------- 23 | boom.use(f): (f1: File^{backdoor*}) => // error | ^ diff --git a/tests/neg-custom-args/captures/unsound-reach.scala b/tests/neg-custom-args/captures/unsound-reach.scala index 3fb666c7c1fc..b8bf1d5f694e 100644 --- a/tests/neg-custom-args/captures/unsound-reach.scala +++ b/tests/neg-custom-args/captures/unsound-reach.scala @@ -15,7 +15,7 @@ class Bar2 extends Foo2[File^]: // error def use(x: File^)(op: File^ => Unit): Unit = op(x) // OK using sealed checking def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/update-call.scala b/tests/neg-custom-args/captures/update-call.scala new file mode 100644 index 000000000000..848e4d880223 --- /dev/null +++ b/tests/neg-custom-args/captures/update-call.scala @@ -0,0 +1,19 @@ +import caps.Mutable + +trait IterableOnce[T] extends Mutable: + def iterator: Iterator[T]^{this} + mut def foreach(op: T => Unit): Unit + +trait Iterator[T] extends IterableOnce[T]: + def iterator = this + def hasNext: Boolean + mut def next(): T + mut def foreach(op: T => Unit): Unit = ??? + override mut def toString = ??? // error + +trait Iterable[T] extends IterableOnce[T]: + def iterator: Iterator[T] = ??? + def foreach(op: T => Unit) = iterator.foreach(op) + +trait BadIterator[T] extends Iterator[T]: + override mut def hasNext: Boolean // error diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index e4b1e71a2000..4fe4163aa433 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -1,18 +1,19 @@ -- Error: tests/neg-custom-args/captures/vars.scala:24:14 -------------------------------------------------------------- 24 | a = x => g(x) // error | ^^^^ - | reference (cap3 : Cap) is not included in the allowed capture set {cap1} of variable a + | reference (cap3 : CC^) is not included in the allowed capture set {cap1} + | of an enclosing function literal with expected type (x$0: String) ->{cap1} String | - | Note that reference (cap3 : Cap), defined in method scope - | cannot be included in outer capture set {cap1} of variable a + | Note that reference (cap3 : CC^), defined in method scope + | cannot be included in outer capture set {cap1} -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:25:8 ------------------------------------------ 25 | a = g // error | ^ | Found: (x: String) ->{cap3} String | Required: (x$0: String) ->{cap1} String | - | Note that reference (cap3 : Cap), defined in method scope - | cannot be included in outer capture set {cap1} of variable a + | Note that reference (cap3 : CC^), defined in method scope + | cannot be included in outer capture set {cap1} | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:27:12 ----------------------------------------- diff --git a/tests/neg/cc-ex-conformance.scala b/tests/neg/cc-ex-conformance.scala index a953466daa9a..16e13376c5b3 100644 --- a/tests/neg/cc-ex-conformance.scala +++ b/tests/neg/cc-ex-conformance.scala @@ -21,5 +21,5 @@ def Test = val ex3: EX3 = ??? val ex4: EX4 = ??? val _: EX4 = ex3 // ok - val _: EX4 = ex4 + val _: EX4 = ex4 // error (???) Probably since we also introduce existentials on expansion val _: EX3 = ex4 // error diff --git a/tests/neg/existential-mapping.check b/tests/neg/existential-mapping.check index edfce67f6eef..e739d6db993c 100644 --- a/tests/neg/existential-mapping.check +++ b/tests/neg/existential-mapping.check @@ -33,56 +33,56 @@ -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:21:30 ----------------------------------------------- 21 | val _: A^ -> (x: C^) -> C = x5 // error | ^^ - | Found: (x5 : A^ -> (ex$27: caps.Exists) -> Fun[C^{ex$27}]) + | Found: (x5 : A^ -> (x: C^) -> (ex$27: caps.Exists) -> C^{ex$27}) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:24:30 ----------------------------------------------- 24 | val _: A^ -> (x: C^) => C = x6 // error | ^^ - | Found: (x6 : A^ -> (ex$33: caps.Exists) -> IFun[C^{ex$33}]) - | Required: A^ -> (ex$36: caps.Exists) -> (x: C^) ->{ex$36} C + | Found: (x6 : A^ -> (ex$36: caps.Exists) -> (x: C^) ->{ex$36} (ex$35: caps.Exists) -> C^{ex$35}) + | Required: A^ -> (ex$39: caps.Exists) -> (x: C^) ->{ex$39} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:27:25 ----------------------------------------------- 27 | val _: (x: C^) => C = y1 // error | ^^ - | Found: (y1 : (x: C^) => (ex$38: caps.Exists) -> C^{ex$38}) - | Required: (x: C^) => C + | Found: (y1 : (x: C^) ->{fresh} (ex$41: caps.Exists) -> C^{ex$41}) + | Required: (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:30:20 ----------------------------------------------- 30 | val _: C^ => C = y2 // error | ^^ - | Found: (y2 : C^ => (ex$42: caps.Exists) -> C^{ex$42}) - | Required: C^ => C + | Found: (y2 : C^ ->{fresh} (ex$45: caps.Exists) -> C^{ex$45}) + | Required: C^ ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:33:30 ----------------------------------------------- 33 | val _: A^ => (x: C^) => C = y3 // error | ^^ - | Found: (y3 : A^ => (ex$47: caps.Exists) -> (x: C^) ->{ex$47} (ex$46: caps.Exists) -> C^{ex$46}) - | Required: A^ => (ex$50: caps.Exists) -> (x: C^) ->{ex$50} C + | Found: (y3 : A^ ->{fresh} (ex$50: caps.Exists) -> (x: C^) ->{ex$50} (ex$49: caps.Exists) -> C^{ex$49}) + | Required: A^ ->{fresh} (ex$53: caps.Exists) -> (x: C^) ->{ex$53} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:36:25 ----------------------------------------------- 36 | val _: A^ => C^ => C = y4 // error | ^^ - | Found: (y4 : A^ => (ex$53: caps.Exists) -> C^ ->{ex$53} (ex$52: caps.Exists) -> C^{ex$52}) - | Required: A^ => (ex$56: caps.Exists) -> C^ ->{ex$56} C + | Found: (y4 : A^ ->{fresh} (ex$56: caps.Exists) -> C^ ->{ex$56} (ex$55: caps.Exists) -> C^{ex$55}) + | Required: A^ ->{fresh} (ex$59: caps.Exists) -> C^ ->{ex$59} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:39:30 ----------------------------------------------- 39 | val _: A^ => (x: C^) -> C = y5 // error | ^^ - | Found: (y5 : A^ => (ex$58: caps.Exists) -> Fun[C^{ex$58}]) - | Required: A^ => (x: C^) -> C + | Found: (y5 : A^ ->{fresh} (x: C^) -> (ex$61: caps.Exists) -> C^{ex$61}) + | Required: A^ ->{fresh} (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:42:30 ----------------------------------------------- 42 | val _: A^ => (x: C^) => C = y6 // error | ^^ - | Found: (y6 : A^ => (ex$64: caps.Exists) -> IFun[C^{ex$64}]) - | Required: A^ => (ex$67: caps.Exists) -> (x: C^) ->{ex$67} C + | Found: (y6 : A^ ->{fresh} (ex$70: caps.Exists) -> (x: C^) ->{ex$70} (ex$69: caps.Exists) -> C^{ex$69}) + | Required: A^ ->{fresh} (ex$73: caps.Exists) -> (x: C^) ->{ex$73} C | | longer explanation available when compiling with `-explain` diff --git a/tests/pos-custom-args/captures/boxmap-paper.scala b/tests/pos-custom-args/captures/boxmap-paper.scala index 9d5bb49af25d..436132280d40 100644 --- a/tests/pos-custom-args/captures/boxmap-paper.scala +++ b/tests/pos-custom-args/captures/boxmap-paper.scala @@ -1,7 +1,14 @@ +import caps.cap -type Cell[+T] = [K] -> (T => K) -> K +type Cell_orig[+T] = [K] -> (T => K) -> K -def cell[T](x: T): Cell[T] = +def cell_orig[T](x: T): Cell_orig[T] = + [K] => (k: T => K) => k(x) + +class Cell[+T](val value: [K] -> (T => K) -> K): + def apply[K]: (T => K) -> K = value[K] + +def cell[T](x: T): Cell[T] = Cell: [K] => (k: T => K) => k(x) def get[T](c: Cell[T]): T = c[T](identity) @@ -12,16 +19,20 @@ def map[A, B](c: Cell[A])(f: A => B): Cell[B] def pureMap[A, B](c: Cell[A])(f: A -> B): Cell[B] = c[Cell[B]]((x: A) => cell(f(x))) -def lazyMap[A, B](c: Cell[A])(f: A => B): () ->{f} Cell[B] +def lazyMap[A, B](c: Cell[A])(f: A ->{cap.rd} B): () ->{f} Cell[B] = () => c[Cell[B]]((x: A) => cell(f(x))) trait IO: def print(s: String): Unit -def test(io: IO^) = +def test(io: IO^{cap.rd}) = val loggedOne: () ->{io} Int = () => { io.print("1"); 1 } + // We have a leakage of io because type arguments to alias type `Cell` are not boxed. + val c_orig: Cell[() ->{io} Int]^{io} + = cell[() ->{io} Int](loggedOne) + val c: Cell[() ->{io} Int] = cell[() ->{io} Int](loggedOne) diff --git a/tests/pos-custom-args/captures/cc-cast.scala b/tests/pos-custom-args/captures/cc-cast.scala new file mode 100644 index 000000000000..cfd96d63bee7 --- /dev/null +++ b/tests/pos-custom-args/captures/cc-cast.scala @@ -0,0 +1,12 @@ +import annotation.unchecked.uncheckedCaptures +import compiletime.uninitialized + +def foo(x: Int => Int) = () + + +object Test: + def test(x: Object) = + foo(x.asInstanceOf[Int => Int]) + + @uncheckedCaptures var x1: Object^ = uninitialized + @uncheckedCaptures var x2: Object^ = _ diff --git a/tests/pos-custom-args/captures/cc-dep-param.scala b/tests/pos-custom-args/captures/cc-dep-param.scala index 1440cd4d7d40..5fd634de9040 100644 --- a/tests/pos-custom-args/captures/cc-dep-param.scala +++ b/tests/pos-custom-args/captures/cc-dep-param.scala @@ -1,8 +1,9 @@ import language.experimental.captureChecking +import caps.cap trait Foo[T] def test(): Unit = - val a: Foo[Int]^ = ??? + val a: Foo[Int]^{cap.rd} = ??? val useA: () ->{a} Unit = ??? def foo[X](x: Foo[X]^, op: () ->{x} Unit): Unit = ??? foo(a, useA) diff --git a/tests/pos/cc-ex-unpack.scala b/tests/pos-custom-args/captures/ex-fun-aliases.scala.disabled similarity index 79% rename from tests/pos/cc-ex-unpack.scala rename to tests/pos-custom-args/captures/ex-fun-aliases.scala.disabled index ae9b4ea5d805..ff86927b874c 100644 --- a/tests/pos/cc-ex-unpack.scala +++ b/tests/pos-custom-args/captures/ex-fun-aliases.scala.disabled @@ -11,8 +11,12 @@ type EX3 = () -> (c: Exists) -> () -> C^{c} type EX4 = () -> () -> (c: Exists) -> C^{c} +type FUN1 = (c: C^) -> (C^{c}, C^{c}) + def Test = def f = val ex1: EX1 = ??? val c1 = ex1 + val fun1: FUN1 = c => (c, c) + val fun2 = fun1 c1 diff --git a/tests/pos-custom-args/captures/foreach2.scala b/tests/pos-custom-args/captures/foreach2.scala new file mode 100644 index 000000000000..318bcb9cddfc --- /dev/null +++ b/tests/pos-custom-args/captures/foreach2.scala @@ -0,0 +1,7 @@ +import annotation.unchecked.uncheckedCaptures + +class ArrayBuffer[T]: + def foreach(op: T => Unit): Unit = ??? +def test = + val tasks = new ArrayBuffer[(() => Unit) @uncheckedCaptures] + val _: Unit = tasks.foreach(((task: () => Unit) => task())) diff --git a/tests/pos-custom-args/captures/function-combinators.scala b/tests/pos-custom-args/captures/function-combinators.scala index 4354af4c7636..07233d296b68 100644 --- a/tests/pos-custom-args/captures/function-combinators.scala +++ b/tests/pos-custom-args/captures/function-combinators.scala @@ -1,17 +1,18 @@ class ContextClass type Context = ContextClass^ +import caps.unsafe.unsafeAssumeSeparate def Test(using ctx1: Context, ctx2: Context) = val f: Int => Int = identity val g1: Int ->{ctx1} Int = identity val g2: Int ->{ctx2} Int = identity val h: Int -> Int = identity - val a1 = f.andThen(f); val _: Int ->{f} Int = a1 + val a1 = unsafeAssumeSeparate(f.andThen(f)); val _: Int ->{f} Int = a1 val a2 = f.andThen(g1); val _: Int ->{f, g1} Int = a2 val a3 = f.andThen(g2); val _: Int ->{f, g2} Int = a3 val a4 = f.andThen(h); val _: Int ->{f} Int = a4 val b1 = g1.andThen(f); val _: Int ->{f, g1} Int = b1 - val b2 = g1.andThen(g1); val _: Int ->{g1} Int = b2 + val b2 = unsafeAssumeSeparate(g1.andThen(g1)); val _: Int ->{g1} Int = b2 val b3 = g1.andThen(g2); val _: Int ->{g1, g2} Int = b3 val b4 = g1.andThen(h); val _: Int ->{g1} Int = b4 val c1 = h.andThen(f); val _: Int ->{f} Int = c1 diff --git a/tests/neg-custom-args/captures/i15749a.scala b/tests/pos-custom-args/captures/i15749a.scala similarity index 51% rename from tests/neg-custom-args/captures/i15749a.scala rename to tests/pos-custom-args/captures/i15749a.scala index d3c1fce13322..184f980d6d70 100644 --- a/tests/neg-custom-args/captures/i15749a.scala +++ b/tests/pos-custom-args/captures/i15749a.scala @@ -6,19 +6,17 @@ object u extends Unit type Top = Any^ -type Wrapper[+T] = [X] -> (op: T ->{cap} X) -> X +class Wrapper[+T](val value: [X] -> (op: T ->{cap} X) -> X) def test = - def wrapper[T](x: T): Wrapper[T] = + def wrapper[T](x: T): Wrapper[T] = Wrapper: [X] => (op: T ->{cap} X) => op(x) def strictMap[A <: Top, B <: Top](mx: Wrapper[A])(f: A ->{cap} B): Wrapper[B] = - mx((x: A) => wrapper(f(x))) + mx.value((x: A) => wrapper(f(x))) def force[A](thunk: Unit ->{cap} A): A = thunk(u) def forceWrapper[A](@use mx: Wrapper[Unit ->{cap} A]): Wrapper[A] = - // Γ ⊢ mx: Wrapper[□ {cap} Unit => A] - // `force` should be typed as ∀(□ {cap} Unit -> A) A, but it can not - strictMap[Unit ->{mx*} A, A](mx)(t => force[A](t)) // error // should work + strictMap[Unit ->{mx*} A, A](mx)(t => force[A](t)) // was error when Wrapper was an alias type diff --git a/tests/pos-custom-args/captures/i15923-cases.scala b/tests/pos-custom-args/captures/i15923-cases.scala.disabled similarity index 100% rename from tests/pos-custom-args/captures/i15923-cases.scala rename to tests/pos-custom-args/captures/i15923-cases.scala.disabled diff --git a/tests/pos-custom-args/captures/i15925.scala b/tests/pos-custom-args/captures/i15925.scala.disabled similarity index 100% rename from tests/pos-custom-args/captures/i15925.scala rename to tests/pos-custom-args/captures/i15925.scala.disabled diff --git a/tests/pos-custom-args/captures/i20237-explicit.scala b/tests/pos-custom-args/captures/i20237-explicit.scala new file mode 100644 index 000000000000..0999d4acd50e --- /dev/null +++ b/tests/pos-custom-args/captures/i20237-explicit.scala @@ -0,0 +1,15 @@ +import language.experimental.captureChecking + +class Cap extends caps.Capability: + def use[T](body: Cap => T) = body(this) + +class Box[T](body: Cap => T): + def open(cap: Cap) = cap.use(body) + +object Box: + def make[T](body: Cap => T)(cap: Cap): Box[T]^{body} = Box(x => body(x)) + +def main = + val givenCap: Cap = new Cap + val xx: Cap => Int = y => 1 + val box = Box.make[Int](xx)(givenCap).open \ No newline at end of file diff --git a/tests/pos/i20237.scala b/tests/pos-custom-args/captures/i20237.scala similarity index 100% rename from tests/pos/i20237.scala rename to tests/pos-custom-args/captures/i20237.scala diff --git a/tests/pos-custom-args/captures/mutRef.scala b/tests/pos-custom-args/captures/mutRef.scala new file mode 100644 index 000000000000..5fe82c9b987a --- /dev/null +++ b/tests/pos-custom-args/captures/mutRef.scala @@ -0,0 +1,5 @@ +import caps.Mutable +class Ref(init: Int) extends Mutable: + private var current = init + def get: Int = current + mut def put(x: Int): Unit = current = x diff --git a/tests/pos-custom-args/captures/nested-classes-2.scala b/tests/pos-custom-args/captures/nested-classes-2.scala index 744635ee949b..7290ed4a12ea 100644 --- a/tests/pos-custom-args/captures/nested-classes-2.scala +++ b/tests/pos-custom-args/captures/nested-classes-2.scala @@ -1,21 +1,7 @@ - -def f(x: (() => Unit)): (() => Unit) => (() => Unit) = - def g(y: (() => Unit)): (() => Unit) = x - g - -def test1(x: (() => Unit)): Unit = - def test2(y: (() => Unit)) = - val a: (() => Unit) => (() => Unit) = f(y) - a(x) // OK, but should be error - test2(() => ()) - def test2(x1: (() => Unit), x2: (() => Unit) => Unit) = class C1(x1: (() => Unit), xx2: (() => Unit) => Unit): - def c2(y1: (() => Unit), y2: (() => Unit) => Unit): C2^ = C2(y1, y2) - class C2(y1: (() => Unit), y2: (() => Unit) => Unit): - val a: (() => Unit) => (() => Unit) = f(y1) - a(x1) //OK, but should be error - C2(() => (), x => ()) + def c2(y1: (() => Unit), y2: (() => Unit) => Unit): C2^ = ??? + class C2(y1: (() => Unit), y2: (() => Unit) => Unit) def test3(y1: (() => Unit), y2: (() => Unit) => Unit) = val cc1: C1^{y1, y2} = C1(y1, y2) diff --git a/tests/pos-custom-args/captures/open-existential.scala b/tests/pos-custom-args/captures/open-existential.scala new file mode 100644 index 000000000000..8b43f27a051c --- /dev/null +++ b/tests/pos-custom-args/captures/open-existential.scala @@ -0,0 +1,15 @@ +trait Async extends caps.Capability + +class Future[+T](x: () => T)(using val a: Async) + +class Collector[T](val futs: Seq[Future[T]^]): + def add(fut: Future[T]^{futs*}) = ??? + +def main() = + given async: Async = ??? + val futs = (1 to 20).map(x => Future(() => x)) + val col = Collector(futs) + val col1: Collector[Int] { val futs: Seq[Future[Int]^{async}] } + = Collector(futs) + + diff --git a/tests/pos-custom-args/captures/simple-apply.scala b/tests/pos-custom-args/captures/simple-apply.scala new file mode 100644 index 000000000000..1e2a6715dd79 --- /dev/null +++ b/tests/pos-custom-args/captures/simple-apply.scala @@ -0,0 +1,6 @@ +object Test: + + def foo(x: Object^, ys: List[Object^]) = ??? + def test(io: Object^, async: Object^): Unit = + val v: Object^{io} = ??? + foo(v, List(async)) diff --git a/tests/pos-custom-args/captures/skolems2.scala b/tests/pos-custom-args/captures/skolems2.scala new file mode 100644 index 000000000000..dd6417042339 --- /dev/null +++ b/tests/pos-custom-args/captures/skolems2.scala @@ -0,0 +1,15 @@ +def Test(c: Object^, f: Object^ => Object^) = + def cc: Object^ = c + val x1 = + { f(cc) } + val x2 = + f(cc) + val x3: Object^ = + f(cc) + val x4: Object^ = + { f(cc) } + + + + + diff --git a/tests/pos-special/stdlib/Test2.scala b/tests/pos-special/stdlib/Test2.scala index cab9440c17db..e0d9a1491516 100644 --- a/tests/pos-special/stdlib/Test2.scala +++ b/tests/pos-special/stdlib/Test2.scala @@ -2,6 +2,7 @@ import scala.reflect.ClassTag import language.experimental.captureChecking import collection.{View, Seq} import collection.mutable.{ArrayBuffer, ListBuffer} +import caps.unsafe.unsafeAssumeSeparate object Test { @@ -87,7 +88,7 @@ object Test { val ys9: Iterator[Boolean]^{xs9} = xs9 val xs10 = xs.flatMap(flips) val ys10: Iterator[Int]^{xs10} = xs10 - val xs11 = xs ++ xs + val xs11 = unsafeAssumeSeparate(xs ++ xs) val ys11: Iterator[Int]^{xs11} = xs11 val xs12 = xs ++ Nil val ys12: Iterator[Int]^{xs12} = xs12 @@ -95,7 +96,7 @@ object Test { val ys13: List[Int] = xs13 val xs14 = xs ++ ("a" :: Nil) val ys14: Iterator[Any]^{xs14} = xs14 - val xs15 = xs.zip(xs9) + val xs15 = unsafeAssumeSeparate(xs.zip(xs9)) val ys15: Iterator[(Int, Boolean)]^{xs15} = xs15 println("-------") println(x1) @@ -141,7 +142,7 @@ object Test { val ys9: View[Boolean]^{xs9} = xs9 val xs10 = xs.flatMap(flips) val ys10: View[Int]^{xs10} = xs10 - val xs11 = xs ++ xs + val xs11 = unsafeAssumeSeparate(xs ++ xs) val ys11: View[Int]^{xs11} = xs11 val xs12 = xs ++ Nil val ys12: View[Int]^{xs12} = xs12 @@ -149,7 +150,7 @@ object Test { val ys13: List[Int] = xs13 val xs14 = xs ++ ("a" :: Nil) val ys14: View[Any]^{xs14} = xs14 - val xs15 = xs.zip(xs9) + val xs15 = unsafeAssumeSeparate(xs.zip(xs9)) val ys15: View[(Int, Boolean)]^{xs15} = xs15 println("-------") println(x1) diff --git a/tests/pos/boxmap-paper.scala b/tests/pos/boxmap-paper.scala deleted file mode 100644 index aa983114ed8a..000000000000 --- a/tests/pos/boxmap-paper.scala +++ /dev/null @@ -1,38 +0,0 @@ -import language.experimental.captureChecking - -type Cell[+T] = [K] -> (T => K) -> K - -def cell[T](x: T): Cell[T] = - [K] => (k: T => K) => k(x) - -def get[T](c: Cell[T]): T = c[T](identity) - -def map[A, B](c: Cell[A])(f: A => B): Cell[B] - = c[Cell[B]]((x: A) => cell(f(x))) - -def pureMap[A, B](c: Cell[A])(f: A -> B): Cell[B] - = c[Cell[B]]((x: A) => cell(f(x))) - -def lazyMap[A, B](c: Cell[A])(f: A => B): () ->{f} Cell[B] - = () => c[Cell[B]]((x: A) => cell(f(x))) - -trait IO: - def print(s: String): Unit - -def test(io: IO^) = - - val loggedOne: () ->{io} Int = () => { io.print("1"); 1 } - - val c: Cell[() ->{io} Int] - = cell[() ->{io} Int](loggedOne) - - val g = (f: () ->{io} Int) => - val x = f(); io.print(" + ") - val y = f(); io.print(s" = ${x + y}") - - val r = lazyMap[() ->{io} Int, Unit](c)(f => g(f)) - val r2 = lazyMap[() ->{io} Int, Unit](c)(g) - val r3 = lazyMap(c)(g) - val _ = r() - val _ = r2() - val _ = r3() diff --git a/tests/run-custom-args/captures/colltest5/Test_2.scala b/tests/run-custom-args/captures/colltest5/Test_2.scala index f6f47b536541..2bde8cb5a885 100644 --- a/tests/run-custom-args/captures/colltest5/Test_2.scala +++ b/tests/run-custom-args/captures/colltest5/Test_2.scala @@ -1,5 +1,6 @@ import Predef.{augmentString as _, wrapString as _, *} import scala.reflect.ClassTag +import caps.unsafe.unsafeAssumeSeparate object Test { import colltest5.strawman.collections.* @@ -89,7 +90,7 @@ object Test { val ys9: View[Boolean]^{xs9} = xs9 val xs10 = xs.flatMap(flips) val ys10: View[Int]^{xs10} = xs10 - val xs11 = xs ++ xs + val xs11 = unsafeAssumeSeparate(xs ++ xs) val ys11: View[Int]^{xs11} = xs11 val xs12 = xs ++ Nil val ys12: View[Int]^{xs12} = xs12 @@ -97,7 +98,7 @@ object Test { val ys13: List[Int] = xs13 val xs14 = xs ++ Cons("a", Nil) val ys14: View[Any]^{xs14} = xs14 - val xs15 = xs.zip(xs9) + val xs15 = unsafeAssumeSeparate(xs.zip(xs9)) val ys15: View[(Int, Boolean)]^{xs15} = xs15 println("-------") println(x1)