diff --git a/.jvmopts b/.jvmopts index a50abf36aa42..4df4f826d1db 100644 --- a/.jvmopts +++ b/.jvmopts @@ -1,5 +1,5 @@ -Xss1m --Xms512m --Xmx4096m +-Xms1024m +-Xmx8192m -XX:MaxInlineLevel=35 -XX:ReservedCodeCacheSize=512m 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 c235143e97f1..471a9953c4f0 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -2262,6 +2262,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 32ab8378ae16..45e17794ec96 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -759,7 +759,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..2be492ed6189 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -42,7 +42,8 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte case cr: TermRef => ref(cr) case cr: TermParamRef => untpd.Ident(cr.paramName).withType(cr) case cr: ThisType => This(cr.cls) - // TODO: Will crash if the type is an annotated type, for example `cap?` + case root(_) => ref(root.cap) + // TODO: Will crash if the type is an annotated type, for example `cap.rd` } val arg = repeated(elems, TypeTree(defn.AnyType)) New(symbol.typeRef, arg :: Nil) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 92cd40a65d5a..908e3174bfce 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.{VarState, CompareResult, CompareFailure} +/** Attachment key for capturing type trees */ private val Captures: Key[CaptureSet] = Key() +/** Context property to print root.Fresh(...) 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 @@ -38,14 +43,13 @@ object ccConfig: */ inline val deferredReaches = false - /** If true, use "sealed" as encapsulation mechanism, meaning that we - * check that type variable instantiations don't have `cap` in any of - * their capture sets. This is an alternative of the original restriction - * that `cap` can't be boxed or unboxed. It is dropped in 3.5 but used - * again in 3.6. - */ - def useSealed(using Context) = - Feature.sourceVersion.stable != SourceVersion.`3.5` + /** If true, turn on separation checking */ + def useSepChecks(using Context): Boolean = + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`) + + /** Not used currently. Handy for trying out new features */ + def newScheme(using Context): Boolean = + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`) end ccConfig @@ -72,13 +76,29 @@ def depFun(args: List[Type], resultType: Type, isContextual: Boolean, paramNames /** An exception thrown if a @retains argument is not syntactically a CaptureRef */ class IllegalCaptureRef(tpe: Type)(using Context) extends Exception(tpe.show) +/** A base trait for data producing addenda to error messages */ +trait ErrorNote + /** Capture checking state, which is known to other capture checking components */ class CCState: - /** The last pair of capture reference and capture set where - * the reference could not be added to the set due to a level conflict. - */ - var levelError: Option[CaptureSet.CompareResult.LevelError] = None + /** Error reprting notes produces since the last call to `test` */ + var notes: List[ErrorNote] = Nil + + def addNote(note: ErrorNote): Unit = + if !notes.exists(_.getClass == note.getClass) then + notes = note :: notes + + def test(op: => CompareResult): CompareResult = + val saved = notes + notes = Nil + try op match + case res: CompareFailure => res.withNotes(notes) + case res => res + finally notes = saved + + def testOK(op: => Boolean): CompareResult = + test(if op then CompareResult.OK else CompareResult.Fail(Nil)) /** Warnings relating to upper approximations of capture sets with * existentially bound variables. @@ -88,6 +108,10 @@ class CCState: private var curLevel: Level = outermostLevel private val symLevel: mutable.Map[Symbol, Int] = mutable.Map() + private var openExistentialScopes: List[MethodType] = Nil + + private var capIsRoot: Boolean = false + object CCState: opaque type Level = Int @@ -101,18 +125,55 @@ object CCState: */ def currentLevel(using Context): Level = ccState.curLevel + /** Perform `op` in the next inner level + * @pre We are currently in capture checking or setup + */ inline def inNestedLevel[T](inline op: T)(using Context): T = val ccs = ccState val saved = ccs.curLevel ccs.curLevel = ccs.curLevel.nextInner try op finally ccs.curLevel = saved + /** Perform `op` in the next inner level unless `p` holds. + * @pre We are currently in capture checking or setup + */ inline def inNestedLevelUnless[T](inline p: Boolean)(inline op: T)(using Context): T = val ccs = ccState val saved = ccs.curLevel if !p then ccs.curLevel = ccs.curLevel.nextInner try op finally ccs.curLevel = saved + /** If we are currently in capture checking or setup, and `mt` is a method + * type that is not a prefix of a curried method, perform `op` assuming + * a fresh enclosing existential scope `mt`, otherwise perform `op` directly. + */ + inline def inNewExistentialScope[T](mt: MethodType)(op: => T)(using Context): T = + if isCaptureCheckingOrSetup then + val ccs = ccState + val saved = ccs.openExistentialScopes + if mt.marksExistentialScope then ccs.openExistentialScopes = mt :: ccs.openExistentialScopes + try op finally ccs.openExistentialScopes = saved + else + op + + /** Run `op` under the assumption that `cap` can subsume all other capabilties + * except Result capabilities. Every use of this method should be scrutinized + * for whether it introduces an unsoundness hole. + */ + inline def withCapAsRoot[T](op: => T)(using Context): T = + if isCaptureCheckingOrSetup then + val ccs = ccState + val saved = ccs.capIsRoot + ccs.capIsRoot = true + try op finally ccs.capIsRoot = saved + else op + + /** Is `caps.cap` a root capability that is allowed to subsume other capabilities? */ + def capIsRoot(using Context): Boolean = ccState.capIsRoot + + /** The currently opened existential scopes */ + def openExistentialScopes(using Context): List[MethodType] = ccState.openExistentialScopes + extension (x: Level) def isDefined: Boolean = x >= 0 def <= (y: Level) = (x: Int) <= y @@ -124,7 +185,7 @@ object CCState: end CCState /** The currently valid CCState */ -def ccState(using Context) = +def ccState(using Context): CCState = Phases.checkCapturesPhase.asInstanceOf[CheckCaptures].ccState1 extension (tree: Tree) @@ -136,6 +197,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 @@ -165,7 +228,7 @@ extension (tree: Tree) elems case _ => if tree.symbol.maybeOwner == defn.RetainsCapAnnot - then ref(defn.captureRoot.termRef) :: Nil + then ref(root.cap) :: Nil else Nil extension (tp: Type) @@ -179,21 +242,19 @@ extension (tp: Type) * - annotated types that represent reach or maybe capabilities */ final def isTrackableRef(using Context): Boolean = tp match - case _: (ThisType | TermParamRef) => - true + case _: (ThisType | TermParamRef) => true 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 root.Result(_) => true 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 +283,8 @@ extension (tp: Type) else tp match case tp @ ReachCapability(_) => tp.singletonCaptureSet + case ReadOnlyCapability(ref) => + ref.deepCaptureSet(includeTypevars).readOnly case tp: SingletonCaptureRef if tp.isTrackableRef => tp.reach.singletonCaptureSet case _ => @@ -239,7 +302,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, VarState.Separate).isOK) && !cs.keepAlways then tp else tp match @@ -268,9 +331,12 @@ extension (tp: Type) case _ => tp - /** The first element of this path type */ + /** The first element of this path type. Note that class parameter references + * are of the form this.C but their pathroot is still this.C, not this. + */ final def pathRoot(using Context): Type = tp.dealias match - case tp1: NamedType if tp1.symbol.owner.isClass => tp1.prefix.pathRoot + case tp1: NamedType if tp1.symbol.maybeOwner.isClass && !tp1.symbol.is(TypeParam) => + tp1.prefix.pathRoot case tp1 => tp1 /** If this part starts with `C.this`, the class `C`. @@ -306,18 +372,27 @@ extension (tp: Type) /** The capture set consisting of all top-level captures of `tp` that appear under a box. * Unlike for `boxed` this also considers parents of capture types, unions and * intersections, and type proxies other than abstract types. + * Furthermore, if the original type is a capture ref `x`, it replaces boxed universal sets + * on the fly with x*. */ def boxedCaptureSet(using Context): CaptureSet = - def getBoxed(tp: Type): CaptureSet = tp match + def getBoxed(tp: Type, pre: Type): CaptureSet = tp match case tp @ CapturingType(parent, refs) => - val pcs = getBoxed(parent) - if tp.isBoxed then refs ++ pcs else pcs + val pcs = getBoxed(parent, pre) + if !tp.isBoxed then + pcs + else if pre.exists && refs.containsRootCapability then + val reachRef = if refs.isReadOnly then pre.reach.readOnly else pre.reach + pcs ++ reachRef.singletonCaptureSet + else + pcs ++ refs + case ref: CaptureRef if ref.isTracked && !pre.exists => getBoxed(ref, ref) case tp: TypeRef if tp.symbol.isAbstractOrParamType => CaptureSet.empty - case tp: TypeProxy => getBoxed(tp.superType) - case tp: AndType => getBoxed(tp.tp1) ** getBoxed(tp.tp2) - case tp: OrType => getBoxed(tp.tp1) ++ getBoxed(tp.tp2) + case tp: TypeProxy => getBoxed(tp.superType, pre) + case tp: AndType => getBoxed(tp.tp1, pre) ** getBoxed(tp.tp2, pre) + case tp: OrType => getBoxed(tp.tp1, pre) ++ getBoxed(tp.tp2, pre) case _ => CaptureSet.empty - getBoxed(tp) + getBoxed(tp, NoType) /** Is the boxedCaptureSet of this type nonempty? */ def isBoxedCapturing(using Context): Boolean = @@ -345,7 +420,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 +455,58 @@ extension (tp: Type) case _ => false - /** Tests whether the type derives from `caps.Capability`, which means - * references of this type are maximal capabilities. + /** 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)) + + /** Knowing that `tp` is a function type, is it an alias to a function other + * than `=>`? + */ + def isAliasFun(using Context): Boolean = tp match + case AppliedType(tycon, _) => !defn.isFunctionSymbol(tycon.typeSymbol) + case _ => false + + /** Tests whether all CapturingType parts of the type that are traversed for + * dcs computation satisfy at least one of two conditions: + * 1. They decorate classes that extend the given capability class `cls`, or + * 2. Their capture set is constant and consists only of capabilities + * the derive from `cls` in the sense of `derivesFromCapTrait`. */ - def derivesFromCapability(using Context): Boolean = tp.dealias match + def derivesFromCapTraitDeeply(cls: ClassSymbol)(using Context): Boolean = + val accumulate = new DeepTypeAccumulator[Boolean]: + def capturingCase(acc: Boolean, parent: Type, refs: CaptureSet) = + this(acc, parent) + && (parent.derivesFromCapTrait(cls) + || refs.isConst && refs.elems.forall(_.derivesFromCapTrait(cls))) + def abstractTypeCase(acc: Boolean, t: TypeRef, upperBound: Type) = + this(acc, upperBound) + accumulate(true, tp) + + /** Tests whether the type derives from capability class `cls`. */ + def derivesFromCapTrait(cls: ClassSymbol)(using Context): Boolean = tp.dealiasKeepAnnots 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 ReachCapability(tp1) => + tp1.widen.derivesFromCapTraitDeeply(cls) + case ReadOnlyCapability(tp1) => + tp1.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) + def derivesFromSharedCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_SharedCapability) + /** Drop @retains annotations everywhere */ def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling val tm = new TypeMap: @@ -406,17 +517,6 @@ extension (tp: Type) mapOver(t) tm(tp) - /** If `x` is a capture ref, its reach capability `x*`, represented internally - * as `x @reachCapability`. `x*` stands for all capabilities reachable through `x`". - * We have `{x} <: {x*} <: dcs(x)}` where the deep capture set `dcs(x)` of `x` - * is the union of all capture sets that appear in covariant position in the - * type of `x`. If `x` and `y` are different variables then `{x*}` and `{y*}` - * are unrelated. - */ - def reach(using Context): CaptureRef = tp match - case tp: CaptureRef if tp.isTrackableRef => - if tp.isReach then tp else ReachCapability(tp) - /** If `x` is a capture ref, its maybe capability `x?`, represented internally * as `x @maybeCapability`. `x?` stands for a capability `x` that might or might * not be part of a capture set. We have `{} <: {x?} <: {x}`. Maybe capabilities @@ -436,62 +536,75 @@ 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 + /** 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. * - * which is clearly unsound. + * Reach capabilities cannot wrap read-only capabilities or maybe capabilities. + * We have + * (x.rd).reach = x*.rd + * (x.rd)? = (x*)? + */ + def reach(using Context): CaptureRef = tp match + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.MaybeCapabilityAnnot => + tp1.reach.maybe + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.ReadOnlyCapabilityAnnot => + tp1.reach.readOnly + 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*`. * - * 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. + * Read-only capabilities cannot wrap maybe capabilities + * but they can wrap reach capabilities. We have + * (x?).readOnly = (x.rd)? + */ + def readOnly(using Context): CaptureRef = tp match + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.MaybeCapabilityAnnot => + tp1.readOnly.maybe + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.ReadOnlyCapabilityAnnot => + tp + case _ => + ReadOnlyCapability(tp) + + /** If `x` is a capture ref, replace all no-flip covariant occurrences of `cap` + * in type `tp` with `x*`. */ def withReachCaptures(ref: Type)(using Context): Type = object narrowCaps extends TypeMap: var change = false def apply(t: Type) = if variance <= 0 then t - else t.dealiasKeepAnnots match - case t @ CapturingType(p, cs) if cs.isUniversal => + else t.dealias match + 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) - case t @ FunctionOrMethod(args, res @ Existential(_, _)) - if args.forall(_.isAlwaysPure) => - // Also map existentials in results to reach capabilities if all - // preceding arguments are known to be always pure - apply(t.derivedFunctionOrMethod(args, Existential.toCap(res))) - case Existential(_, _) => - t + case t @ FunctionOrMethod(args, res) => + if args.forall(_.isAlwaysPure) then + // Also map existentials in results to reach capabilities if all + // preceding arguments are known to be always pure + t.derivedFunctionOrMethod(args, apply(root.resultToFresh(res))) + else + t case _ => mapOver(t) end narrowCaps @@ -506,6 +619,22 @@ 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 _ => + foldOver(x, t) + acc(false, tp) def level(using Context): Level = tp match @@ -513,6 +642,11 @@ extension (tp: Type) case tp: ThisType => tp.cls.ccLevel.nextInner case _ => undefinedLevel +extension (tp: MethodType) + /** A method marks an existential scope unless it is the prefix of a curried method */ + def marksExistentialScope(using Context): Boolean = + !tp.resType.isInstanceOf[MethodOrPoly] + extension (cls: ClassSymbol) def pureBaseClass(using Context): Option[Symbol] = @@ -615,12 +749,25 @@ 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 case ann: CaptureAnnotation => ann.boxed case _ => false + def rootAnnot: root.Annot = (tp.annot: @unchecked) match + case ann: root.Annot => ann + /** Drop retains annotations in the type. */ class CleanupRetains(using Context) extends TypeMap: def apply(tp: Type): Type = @@ -650,6 +797,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. */ @@ -658,49 +813,41 @@ 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) = - AnnotatedType(tp, Annotation(annot, util.Spans.NoSpan)) +abstract class AnnotatedCapability(annotCls: Context ?=> ClassSymbol): + def apply(tp: Type)(using Context): AnnotatedType = + assert(tp.isTrackableRef, i"not a trackable ref: $tp") + tp match + case AnnotatedType(_, annot) => + assert(!unwrappable.contains(annot.symbol), i"illegal combination of derived capabilities: $annotCls over ${annot.symbol}") + case _ => + tp match + case tp: CaptureRef => tp.derivedRef(annotCls) + case _ => AnnotatedType(tp, Annotation(annotCls, 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 AnnotatedType(parent: CaptureRef, ann) if ann.hasSymbol(annotCls) => 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] +end AnnotatedCapability /** An extractor for `ref @maybeCapability`, which is used to express * the maybe capability `ref?` as a type. */ -object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot) - -/** Offers utility method to be used for type maps that follow aliases */ -trait ConservativeFollowAliasMap(using Context) extends TypeMap: - - /** If `mapped` is a type alias, apply the map to the alias, while keeping - * annotations. If the result is different, return it, otherwise return `mapped`. - * Furthermore, if `original` is a LazyRef or TypeVar and the mapped result is - * the same as the underlying type, keep `original`. This avoids spurious differences - * which would lead to spurious dealiasing in the result - */ - protected def applyToAlias(original: Type, mapped: Type) = - val mapped1 = mapped match - case t: (TypeRef | AppliedType) => - val t1 = t.dealiasKeepAnnots - if t1 eq t then t - else - // If we see a type alias, map the alias type and keep it if it's different - val t2 = apply(t1) - if t2 ne t1 then t2 else t - case _ => - mapped - original match - case original: (LazyRef | TypeVar) if mapped1 eq original.underlying => - original - case _ => - mapped1 -end ConservativeFollowAliasMap +object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot): + protected def unwrappable(using Context) = Set() + +/** An extractor for `ref @readOnlyCapability`, which is used to express + * the read-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) /** An extractor for all kinds of function types as well as method and poly types. * It includes aliases of function types such as `=>`. TODO: Can we do without? @@ -755,3 +902,36 @@ object ContainsParam: if tycon.typeSymbol == defn.Caps_ContainsTrait && cs.typeSymbol.isAbstractOrParamType => Some((cs, ref)) case _ => None + +/** A class encapsulating the assumulator logic needed for `CaptureSet.ofTypeDeeply` + * and `derivesFromCapTraitDeeply`. + * NOTE: The traversal logic needs to be in sync with narrowCaps in CaptureOps, which + * replaces caps with reach capabilties. There are two exceptions, however. + * - First, invariant arguments. These have to be included to be conservative + * in dcs but must be excluded in narrowCaps. + * - Second, unconstrained type variables are handled specially in `ofTypeDeeply`. + */ +abstract class DeepTypeAccumulator[T](using Context) extends TypeAccumulator[T]: + val seen = util.HashSet[Symbol]() + + protected def capturingCase(acc: T, parent: Type, refs: CaptureSet): T + + protected def abstractTypeCase(acc: T, t: TypeRef, upperBound: Type): T + + def apply(acc: T, t: Type) = + if variance < 0 then acc + else t.dealias match + case t @ CapturingType(p, cs1) => + capturingCase(acc, p, cs1) + case t: TypeRef if t.symbol.isAbstractOrParamType && !seen.contains(t.symbol) => + seen += t.symbol + abstractTypeCase(acc, t, t.info.bounds.hi) + case AnnotatedType(parent, _) => + this(acc, parent) + case t @ FunctionOrMethod(args, res) => + if args.forall(_.isAlwaysPure) then this(acc, root.resultToFresh(res)) + else acc + case _ => + foldOver(acc, t) +end DeepTypeAccumulator + diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 2caba4cf7d89..0b810218c07c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -6,62 +6,114 @@ import core.* import Types.*, Symbols.*, Contexts.*, Decorators.* import util.{SimpleIdentitySet, Property} import typer.ErrorReporting.Addenda -import TypeComparer.subsumesExistentially import util.common.alwaysTrue import scala.collection.mutable import CCState.* import Periods.NoRunId import compiletime.uninitialized import StdNames.nme +import CaptureSet.VarState +import Annotations.Annotation +import config.Printers.capt /** 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 order: + * `*` first, `.rd` next, `?` last. */ trait CaptureRef extends TypeProxy, ValueType: private var myCaptureSet: CaptureSet | Null = uninitialized private var myCaptureSetRunId: Int = NoRunId private var mySingletonCaptureSet: CaptureSet.Const | Null = null + private var myDerivedRefs: List[AnnotatedType] = Nil + + /** A derived reach, readOnly or maybe reference. Derived references are cached. */ + def derivedRef(annotCls: ClassSymbol)(using Context): AnnotatedType = + def recur(refs: List[AnnotatedType]): AnnotatedType = refs match + case ref :: refs1 => + if ref.annot.symbol == annotCls then ref else recur(refs1) + case Nil => + val derived = AnnotatedType(this, Annotation(annotCls, util.Spans.NoSpan)) + myDerivedRefs = derived :: myDerivedRefs + derived + recur(myDerivedRefs) /** Is the reference tracked? This is true if it can be tracked and the capture * set of the underlying type is not always empty. */ final def isTracked(using Context): Boolean = - this.isTrackableRef && (isMaxCapability || !captureSetOfInfo.isAlwaysEmpty) + this.isTrackableRef && (isRootCapability || !captureSetOfInfo.isAlwaysEmpty) + + /** Is this a maybe reference of the form `x?`? */ + final def isMaybe(using Context): Boolean = this ne stripMaybe + + /** 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 /** 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 + final def isReach(using Context): Boolean = this ne stripReach - /** 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 stripMaybe(using Context): CaptureRef = this match + case AnnotatedType(tp1: CaptureRef, annot) if annot.symbol == defn.MaybeCapabilityAnnot => + tp1 + case _ => + this - final def stripReach(using Context): CaptureRef = - if isReach then - val AnnotatedType(parent: CaptureRef, _) = this: @unchecked - parent - else 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 stripMaybe(using Context): CaptureRef = - if isMaybe then - val AnnotatedType(parent: CaptureRef, _) = this: @unchecked - parent - else 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 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: TermParamRef => tp.underlying.derivesFrom(defn.Caps_Exists) + /** Is this reference a Fresh instance? */ + final def isFresh(using Context): Boolean = this match + case root.Fresh(_) => true case _ => false - // With the support of pathes, we don't need to normalize the `TermRef`s anymore. + /** Is this reference the generic root capability `cap` or a Fresh instance? */ + final def isCapOrFresh(using Context): Boolean = isCap || isFresh + + /** Is this reference one of the generic root capabilities `cap` or `cap.rd` ? */ + final def isRootCapability(using Context): Boolean = this match + case ReadOnlyCapability(tp1) => tp1.isRootCapability + case root(_) => true + case _ => isCap + + /** An exclusive capability is a capability that derives + * indirectly from a maximal capability without going through + * a read-only capability first. + */ + final def isExclusive(using Context): Boolean = + !isReadOnly && (isRootCapability || captureSetOfInfo.isExclusive) + + // With the support of paths, 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 // case tp @ AnnotatedType(parent: CaptureRef, annot) if tp.isTrackableRef => @@ -104,35 +156,35 @@ trait CaptureRef extends TypeProxy, ValueType: * X: CapSet^c1...CapSet^c2, (CapSet^c1) subsumes y ==> X subsumes y * Y: CapSet^c1...CapSet^c2, x subsumes (CapSet^c2) ==> x subsumes Y * Contains[X, y] ==> X subsumes y - * - * TODO: Document cases with more comments. */ - final def subsumes(y: CaptureRef)(using Context): Boolean = + final def subsumes(y: CaptureRef)(using ctx: Context, vs: VarState = VarState.Separate): Boolean = + def subsumingRefs(x: Type, y: Type): Boolean = x match case x: CaptureRef => y match case y: CaptureRef => x.subsumes(y) case _ => false case _ => false - def viaInfo(info: Type)(test: Type => Boolean): Boolean = info.match + def viaInfo(info: Type)(test: Type => Boolean): Boolean = info.dealias match case info: SingletonCaptureRef => test(info) + case CapturingType(parent, _) => + if this.derivesFrom(defn.Caps_CapSet) then test(info) + /* + If `this` is a capture set variable `C^`, then it is possible that it can be + reached from term variables in a reachability chain through the context. + For instance, in `def test[C^](src: Foo^{C^}) = { val x: Foo^{src} = src; val y: Foo^{x} = x; y }` + we expect that `C^` subsumes `x` and `y` in the body of the method + (cf. test case cc-poly-varargs.scala for a more involved example). + */ + else viaInfo(parent)(test) case info: AndType => viaInfo(info.tp1)(test) || viaInfo(info.tp2)(test) case info: OrType => viaInfo(info.tp1)(test) && viaInfo(info.tp2)(test) - case info @ CapturingType(_,_) if this.derivesFrom(defn.Caps_CapSet) => - /* - If `this` is a capture set variable `C^`, then it is possible that it can be - reached from term variables in a reachability chain through the context. - For instance, in `def test[C^](src: Foo^{C^}) = { val x: Foo^{src} = src; val y: Foo^{x} = x; y }` - we expect that `C^` subsumes `x` and `y` in the body of the method - (cf. test case cc-poly-varargs.scala for a more involved example). - */ - test(info) case _ => false (this eq y) - || this.isRootCapability + || maxSubsumes(y, canAddHidden = !vs.isOpen) || y.match - case y: TermRef if !y.isRootCapability => + case y: TermRef if !y.isCap => y.prefix.match case ypre: CaptureRef => this.subsumes(ypre) @@ -150,6 +202,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`, @@ -167,7 +220,6 @@ trait CaptureRef extends TypeProxy, ValueType: || this.match case ReachCapability(x1) => x1.subsumes(y.stripReach) case x: TermRef => viaInfo(x.info)(subsumingRefs(_, y)) - case x: TermParamRef => subsumesExistentially(x, y) case x: TypeRef if assumedContainsOf(x).contains(y) => true case x: TypeRef if x.derivesFrom(defn.Caps_CapSet) => x.info match @@ -180,6 +232,76 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false end subsumes + /** This is a maximal capability that subsumes `y` in given context and VarState. + * @param canAddHidden If true we allow maximal capabilities to subsume all other capabilities. + * We add those capabilities to the hidden set if this is a Fresh instance. + * If false we only accept `y` elements that are already in the + * hidden set of this Fresh instance. 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 = VarState.Separate): Boolean = + def yIsExistential = y.stripReadOnly match + case root.Result(_) => + capt.println(i"failed existential $this >: $y") + true + case _ => false + (this eq y) + || this.match + case root.Fresh(hidden) => + vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) + || !y.stripReadOnly.isCap && !yIsExistential && canAddHidden && vs.addHidden(hidden, y) + case x @ root.Result(binder) => + if y.derivesFromSharedCapability then true + else + ccState.addNote(CaptureSet.ExistentialSubsumesFailure(x, y)) + false + case _ => + y match + case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden) + case _ if this.isCap => + y.isCap + || y.derivesFromSharedCapability + || !yIsExistential + && canAddHidden + && vs != VarState.HardSeparate + && (CCState.capIsRoot + // || { println(i"no longer $this maxSubsumes $y, ${y.isCap}"); false } // debug + ) + || false + case _ => + false + + /** `x covers y` if we should retain `y` when computing the overlap of + * two footprints which have `x` respectively `y` as elements. + * We assume that .rd have already been stripped on both sides. + * We have: + * + * x covers x + * x covers y ==> x covers y.f + * x covers y ==> x* covers y*, x? covers y? + * TODO what other clauses from subsumes do we need to port here? + */ + final def covers(y: CaptureRef)(using Context): Boolean = + (this eq y) + || y.match + case y @ TermRef(ypre: CaptureRef, _) if !y.isCap => + this.covers(ypre) + case ReachCapability(y1) => + this match + case ReachCapability(x1) => x1.covers(y1) + case _ => false + case MaybeCapability(y1) => + this match + case MaybeCapability(x1) => x1.covers(y1) + case _ => false + case root.Fresh(hidden) => + hidden.superCaps.exists(this covers _) + 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 39c41c369864..688605dcc32d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -14,10 +14,11 @@ 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.* +import TypeOps.AvoidMap +import compiletime.uninitialized /** A class for capture sets. Capture sets can be constants or variables. * Capture sets support inclusion constraints <:< where <:< is subcapturing. @@ -81,12 +82,27 @@ sealed abstract class CaptureSet extends Showable: assert(!isConst) asInstanceOf[Var] + /** Convert to Const with current elements unconditionally */ + def toConst: Const = this match + case c: Const => c + case v: Var => Const(v.elems) + /** Does this capture set contain the root reference `cap` as element? */ final def isUniversal(using Context) = + elems.exists(_.isCap) + + /** Does this capture set contain a root reference `cap` or `cap.rd` as element? */ + final def containsRootCapability(using Context) = elems.exists(_.isRootCapability) - final def isUnboxable(using Context) = - elems.exists(elem => elem.isRootCapability || Existential.isExistentialVar(elem)) + final def containsCap(using Context) = + elems.exists(_.stripReadOnly.isCap) + + 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] @@ -125,8 +141,8 @@ sealed abstract class CaptureSet extends Showable: * element is not the root capability, try instead to include its underlying * capture set. */ - protected final def addNewElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if elem.isMaxCapability || summon[VarState] == FrozenState then + protected final def addNewElem(elem: CaptureRef)(using ctx: Context, vs: VarState): CompareResult = + if elem.isRootCapability || !vs.isOpen then addThisElem(elem) else addThisElem(elem).orElse: @@ -146,27 +162,40 @@ sealed abstract class CaptureSet extends Showable: */ protected def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult + protected def addIfHiddenOrFail(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 /** If `cs` is a variable, add this capture set as one of its dependent sets */ protected def addAsDependentTo(cs: CaptureSet)(using Context): this.type = - cs.addDependent(this)(using ctx, UnrecordedState) + cs.addDependent(this)(using ctx, VarState.Unrecorded) this /** {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 = VarState.Separate): Boolean = + 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)) - || !x.isMaxCapability + || // Even though subsumes already follows captureSetOfInfo, this is not enough. + // For instance x: C^{y, z}. Then neither y nor z subsumes x but {y, z} accounts for x. + !x.isRootCapability && !x.derivesFrom(defn.Caps_CapSet) - && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK + && !(vs.isSeparating && x.captureSetOfInfo.containsRootCapability) + // in VarState.Separate, don't try to widen to cap since that might succeed with {cap} <: {cap} + && x.captureSetOfInfo.subCaptures(this, VarState.Separate).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 +205,14 @@ 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)) - || !x.isMaxCapability + reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true): + CCState.withCapAsRoot: // OK here since we opportunistically choose an alternative which gets checked later + elems.exists(_.subsumes(x)(using ctx, VarState.ClosedUnrecorded)) + || !x.isRootCapability && { 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 @@ -194,21 +223,17 @@ sealed abstract class CaptureSet extends Showable: elems.forall(that.mightAccountFor) && !that.elems.forall(this.mightAccountFor) - /** The subcapturing test. - * @param frozen if true, no new variables or dependent sets are allowed to - * 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()) + /** The subcapturing test, taking an explicit VarState. */ + final def subCaptures(that: CaptureSet, vs: VarState)(using Context): CompareResult = + subCaptures(that)(using ctx, vs) /** The subcapturing test, using a given VarState */ - private def subCaptures(that: CaptureSet)(using Context, VarState): CompareResult = + final def subCaptures(that: CaptureSet)(using ctx: Context, vs: VarState = VarState()): CompareResult = val result = that.tryInclude(elems, this) if result.isOK then addDependent(that) else - ccState.levelError = ccState.levelError.orElse(result.levelError) + result.levelError.foreach(ccState.addNote) varState.rollBack() result //.showing(i"subcaptures $this <:< $that = ${result.show}", capt) @@ -217,19 +242,22 @@ 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, VarState.Separate).isOK + && that.subCaptures(this, VarState.Separate).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, VarState.HardSeparate).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, VarState.HardSeparate).isOK then this else if this.isConst && that.isConst then Const(this.elems ++ that.elems) else Union(this, that) + def ++ (that: CaptureSet.Const)(using Context): CaptureSet.Const = + Const(this.elems ++ that.elems) + /** The smallest superset (via <:<) of this capture set that also contains `ref`. */ def + (ref: CaptureRef)(using Context): CaptureSet = @@ -238,8 +266,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, VarState.Closed()).isOK then this + else if that.subCaptures(this, VarState.Closed()).isOK then that else if this.isConst && that.isConst then Const(elemIntersection(this, that)) else Intersection(this, that) @@ -297,6 +325,8 @@ sealed abstract class CaptureSet extends Showable: else BiMapped(asVar, tm, mappedElems) case tm: IdentityCaptRefMap => this + case tm: AvoidMap if this.isInstanceOf[HiddenSet] => + this case _ => val mapped = mapRefs(elems, tm, tm.variance) if isConst then @@ -310,9 +340,11 @@ 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() + if containsRootCapability then handler() this /** Invoke handler on the elements to ensure wellformedness of the capture set. @@ -354,6 +386,11 @@ sealed abstract class CaptureSet extends Showable: override def toText(printer: Printer): Text = printer.toTextCaptureSet(this) ~~ description + /** Apply function `f` to the elements. Typically 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,21 +401,37 @@ 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 emptyRefs: Refs = SimpleIdentitySet.empty /** The empty capture set `{}` */ - val empty: CaptureSet.Const = Const(emptySet) + val empty: CaptureSet.Const = Const(emptyRefs) /** The universal capture set `{cap}` */ def universal(using Context): CaptureSet = - defn.captureRoot.termRef.singletonCaptureSet + root.cap.singletonCaptureSet + + /** The same as CaptureSet.universal but generated implicitly for + * references of Capability subtypes + */ + def universalImpliedByCapability(using Context) = + defn.universalCSImpliedByCapability + + def fresh(owner: Symbol = NoSymbol)(using Context): CaptureSet = + root.Fresh.withOwner(owner).singletonCaptureSet + + /** The shared capture set `{cap.rd}` */ + def shared(using Context): CaptureSet = + root.cap.readOnly.singletonCaptureSet /** Used as a recursion brake */ @sharable private[dotc] val Pending = Const(SimpleIdentitySet.empty) def apply(elems: CaptureRef*)(using Context): CaptureSet.Const = if elems.isEmpty then empty - else Const(SimpleIdentitySet(elems.map(_.ensuring(_.isTrackableRef))*)) + else + for elem <- elems do + assert(elem.isTrackableRef, i"not a trackable ref: $elem") + Const(SimpleIdentitySet(elems*)) def apply(elems: Refs)(using Context): CaptureSet.Const = if elems.isEmpty then empty else Const(elems) @@ -389,7 +442,7 @@ object CaptureSet: def isAlwaysEmpty = elems.isEmpty def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = - CompareResult.Fail(this :: Nil) + addIfHiddenOrFail(elem) def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK @@ -416,16 +469,18 @@ object CaptureSet: * nulls, this provides more lenient checking against compilation units that * were not yet compiled with capture checking on. */ - object Fluid extends Const(emptySet): + object Fluid extends Const(emptyRefs): 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 /** The subclass of captureset variables with given initial elements */ - class Var(override val owner: Symbol = NoSymbol, initialElems: Refs = emptySet, val level: Level = undefinedLevel, underBox: Boolean = false)(using @constructorOnly ictx: Context) extends CaptureSet: + class Var(initialOwner: Symbol = NoSymbol, initialElems: Refs = emptyRefs, val level: Level = undefinedLevel, underBox: Boolean = false)(using @constructorOnly ictx: Context) extends CaptureSet: + + override def owner = initialOwner /** A unique identification number for diagnostics */ val id = @@ -438,12 +493,15 @@ object CaptureSet: private var isSolved: Boolean = false /** The elements currently known to be in the set */ - var elems: Refs = initialElems + protected var myElems: Refs = initialElems + + def elems: Refs = myElems + def elems_=(refs: Refs): Unit = myElems = refs /** The sets currently known to be dependent sets (i.e. new additions to this set * are propagated to these dependent sets.) */ - var deps: Deps = emptySet + var deps: Deps = SimpleIdentitySet.empty def isConst = isSolved def isAlwaysEmpty = isSolved && elems.isEmpty @@ -485,16 +543,14 @@ 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 - CompareResult.Fail(this :: Nil) + if isConst || !recordElemsState() then // Fail if variable is solved or given VarState is frozen + addIfHiddenOrFail(elem) 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].isSeparating, summon[VarState]) elems += elem if elem.isRootCapability then rootAddedHandler() @@ -507,15 +563,16 @@ object CaptureSet: elems -= elem res.addToTrace(this) + // TODO: Also track allowable TermParamRefs and root.Results in capture sets private def levelOK(elem: CaptureRef)(using Context): Boolean = 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 @ root.Result(mt) => + !noUniversal + && !CCState.openExistentialScopes.contains(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. case elem: TermRef if level.isDefined => elem.prefix match case prefix: CaptureRef => @@ -526,6 +583,8 @@ object CaptureSet: elem.cls.ccLevel.nextInner <= level case ReachCapability(elem1) => levelOK(elem1) + case ReadOnlyCapability(elem1) => + levelOK(elem1) case MaybeCapability(elem1) => levelOK(elem1) case _ => @@ -558,16 +617,21 @@ 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 val approx = computeApprox(origin).ensuring(_.isConst) - if approx.elems.exists(Existential.isExistentialVar(_)) then + if approx.elems.exists: + case root.Result(_) => true + case _ => false + then ccState.approxWarnings += em"""Capture set variable $this gets upper-approximated - |to existential variable from $approx, using {cap} instead.""" + |to existential variable from $approx, using {cap} instead.""" universal else approx finally computingApprox = false @@ -582,12 +646,15 @@ object CaptureSet: */ def solve()(using Context): Unit = if !isConst then - val approx = upperApprox(empty) - .showing(i"solve $this = $result", capt) - //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") - val newElems = approx.elems -- elems - if tryInclude(newElems, empty)(using ctx, VarState()).isOK then - markSolved() + CCState.withCapAsRoot: // // OK here since we infer parameter types that get checked later + val approx = upperApprox(empty) + .map(root.CapToFresh(NoSymbol).inverse) // Fresh --> cap + .showing(i"solve $this = $result", capt) + //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") + val newElems = approx.elems -- elems + given VarState() + if tryInclude(newElems, empty).isOK then + markSolved() /** Mark set as solved and propagate this info to all dependent sets */ def markSolved()(using Context): Unit = @@ -868,7 +935,97 @@ object CaptureSet: end Intersection def elemIntersection(cs1: CaptureSet, cs2: CaptureSet)(using Context): Refs = - cs1.elems.filter(cs2.mightAccountFor) ++ cs2.elems.filter(cs1.mightAccountFor) + cs1.elems.filter(cs2.accountsFor) ++ cs2.elems.filter(cs1.accountsFor) + + /** A capture set variable used to record the references hidden by a Fresh instance, + * The elems and deps members are repurposed as follows: + * elems: Set of hidden references + * deps : Set of hidden sets for which the Fresh instance owning this set + * is a hidden element. + * Hidden sets may become aliases of other hidden sets, which means that + * reads and writes of elems go to the alias. + * If H is an alias of R.hidden for some Fresh instance R then: + * H.elems == {R} + * H.deps = {R.hidden} + * This encoding was chosen because it relies only on the elems and deps fields + * which are already subject through snapshotting and rollbacks in VarState. + * It's advantageous if we don't need to deal with other pieces of state there. + */ + class HiddenSet(initialOwner: Symbol)(using @constructorOnly ictx: Context) + extends Var(initialOwner): + var owningCap: AnnotatedType = uninitialized + var givenOwner: Symbol = initialOwner + + override def owner = givenOwner + + // assert(id != 34, i"$initialHidden") + + private def aliasRef: AnnotatedType | Null = + if myElems.size == 1 then + myElems.nth(0) match + case al @ root.Fresh(hidden) if deps.contains(hidden) => al + case _ => null + else null + + private def aliasSet: HiddenSet = + if myElems.size == 1 then + myElems.nth(0) match + case root.Fresh(hidden) if deps.contains(hidden) => hidden + case _ => this + else this + + def superCaps: List[AnnotatedType] = + deps.toList.map(_.asInstanceOf[HiddenSet].owningCap) + + override def elems: Refs = + val al = aliasSet + if al eq this then super.elems else al.elems + + override def elems_=(refs: Refs) = + val al = aliasSet + if al eq this then super.elems_=(refs) else al.elems_=(refs) + + /** Add element to hidden set. Also add it to all supersets (as indicated by + * deps of this set). Follow aliases on both hidden set and added element + * before adding. If the added element is also a Fresh instance with + * hidden set H which is a superset of this set, then make this set an + * alias of H. + */ + def add(elem: CaptureRef)(using ctx: Context, vs: VarState): Unit = + val alias = aliasSet + if alias ne this then alias.add(elem) + else + def addToElems() = + elems += elem + deps.foreach: dep => + assert(dep != this) + vs.addHidden(dep.asInstanceOf[HiddenSet], elem) + elem match + case root.Fresh(hidden) => + if this ne hidden then + val alias = hidden.aliasRef + if alias != null then + add(alias) + else if deps.contains(hidden) then // make this an alias of elem + capt.println(i"Alias $this to $hidden") + elems = SimpleIdentitySet(elem) + deps = SimpleIdentitySet(hidden) + else + addToElems() + hidden.deps += this + case _ => + addToElems() + + /** Apply function `f` to `elems` while setting `elems` to empty for the + * duration. This is used to escape infinite recursions if two Freshs + * refer to each other in their hidden sets. + */ + override def processElems[T](f: Refs => T): T = + val savedElems = elems + elems = emptyRefs + 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} @@ -905,7 +1062,7 @@ object CaptureSet: */ def subCapturesRange(arg1: TypeBounds, arg2: Type)(using Context): Boolean = arg1 match case TypeBounds(CapturingType(lo, loRefs), CapturingType(hi, hiRefs)) if lo =:= hi => - given VarState = VarState() + given VarState() val cs2 = arg2.captureSet hiRefs.subCaptures(cs2).isOK && cs2.subCaptures(loRefs).isOK case _ => @@ -920,10 +1077,23 @@ object CaptureSet: /** A TypeMap that is the identity on capture references */ trait IdentityCaptRefMap extends TypeMap + /** A value of this class is produced and added as a note to ccState + * when a subsumes check decides that an existential variable `ex` cannot be + * instantiated to the other capability `other`. + */ + case class ExistentialSubsumesFailure(val ex: root.Result, val other: CaptureRef) extends ErrorNote + + trait CompareFailure: + private var myErrorNotes: List[ErrorNote] = Nil + def errorNotes: List[ErrorNote] = myErrorNotes + def withNotes(notes: List[ErrorNote]): this.type = + myErrorNotes = notes + this + enum CompareResult extends Showable: case OK - case Fail(trace: List[CaptureSet]) - case LevelError(cs: CaptureSet, elem: CaptureRef) + case Fail(trace: List[CaptureSet]) extends CompareResult, CompareFailure + case LevelError(cs: CaptureSet, elem: CaptureRef) extends CompareResult, CompareFailure, ErrorNote override def toText(printer: Printer): Text = inContext(printer.printerContext): @@ -981,8 +1151,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 but not in frozen states. */ def putElems(v: Var, elems: Refs): Boolean = { elemsMap(v) = elems; true } @@ -993,58 +1162,127 @@ 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 but not in frozen states. */ def putDeps(v: Var, deps: Deps): Boolean = { depsMap(v) = deps; true } + /** Does this state allow additions of elements to capture set variables? */ + def isOpen = true + def isSeparating = false + + /** Add element to hidden set, recording it in elemsMap, + * return whether this was allowed. By default, recording is allowed + * but the special state VarState.Separate overrides this. + */ + def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = + elemsMap.get(hidden) match + case None => + elemsMap(hidden) = hidden.elems + depsMap(hidden) = hidden.deps + case _ => + hidden.add(elem)(using ctx, this) + 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)) - end VarState - /** A special state that does 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) - */ - @sharable - object FrozenState extends VarState: - override def putElems(v: Var, refs: Refs) = false - override def putDeps(v: Var, deps: Deps) = false - override def rollBack(): Unit = () + private var seen: util.EqHashSet[CaptureRef] = new util.EqHashSet - @sharable - /** A special state that turns off recording of elements. Used only - * in `addSub` to prevent cycles in recordings. - */ - private object UnrecordedState extends VarState: - override def putElems(v: Var, refs: Refs) = true - override def putDeps(v: Var, deps: Deps) = true - override def rollBack(): Unit = () + /** 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 + + override def toString = "open varState" + + object VarState: + + /** 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 these states (since the previous state cannot be recorded in a snapshot) + * On the other hand, these states do allow by default Fresh instances to + * subsume arbitary types, which are then recorded in their hidden sets. + */ + class Closed extends VarState: + override def putElems(v: Var, refs: Refs) = false + override def putDeps(v: Var, deps: Deps) = false + override def isOpen = false + override def toString = "closed varState" + + /** A closed state that allows a Fresh instance to subsume a + * reference `r` only if `r` is already present in the hidden set of the instance. + * No new references can be added. + */ + class Separating extends Closed: + override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = false + override def toString = "separating varState" + override def isSeparating = true + + /** A closed state that allows a Fresh instance to subsume a + * reference `r` only if `r` is already present in the hidden set of the instance. + * No new references can be added. + */ + @sharable + object Separate extends Separating + + /** Like Separate but in addition we assume that `cap` never subsumes anything else. + * Used in `++` to not lose track of dependencies between function parameters. + */ + @sharable + object HardSeparate extends Separating + + /** A special state that turns off recording of elements. Used only + * in `addSub` to prevent cycles in recordings. + */ + @sharable + private[CaptureSet] object Unrecorded extends VarState: + override def putElems(v: Var, refs: Refs) = true + override def putDeps(v: Var, deps: Deps) = true + override def rollBack(): Unit = () + override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = true + override def toString = "unrecorded varState" + + /** A closed state that turns off recording of hidden elements (but allows + * adding them). Used in `mightAccountFor`. + */ + @sharable + private[CaptureSet] object ClosedUnrecorded extends Closed: + override def addHidden(hidden: HiddenSet, elem: CaptureRef)(using Context): Boolean = true + override def toString = "closed unrecorded varState" + + end VarState + @sharable /** 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 = @@ -1073,11 +1311,19 @@ object CaptureSet: case ReachCapability(ref1) => ref1.widen.deepCaptureSet(includeTypevars = true) .showing(i"Deep capture set of $ref: ${ref1.widen} = ${result}", capt) + 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 */ + if ref.isRootCapability then ref.singletonCaptureSet + else ofType(ref.underlying, followResult = false) + + /** Capture set of a type + * @param followResult If true, also include capture sets of function results. + * This mode is currently not used. It could be interesting + * when we change the system so that the capture set of a function + * is the union of the capture sets if its span. + * In this case we should use `followResult = true` in the call in ofInfo above. + */ def ofType(tp: Type, followResult: Boolean)(using Context): CaptureSet = def recur(tp: Type): CaptureSet = trace(i"ofType $tp, ${tp.getClass} $followResult", show = true): tp.dealiasKeepAnnots match @@ -1088,9 +1334,14 @@ object CaptureSet: case tp: (TypeRef | TypeParamRef) => if tp.derivesFrom(defn.Caps_CapSet) then tp.captureSet else empty + case tp @ root.Result(_) => + tp.captureSet case CapturingType(parent, refs) => recur(parent) ++ refs case tp @ AnnotatedType(parent, ann) if ann.hasSymbol(defn.ReachCapabilityAnnot) => + // Note: we don't use the `ReachCapability(parent)` extractor here since that + // only works if `parent` is a CaptureRef, but in illegal programs it might not be. + // And then we do not want to fall back to empty. parent match case parent: SingletonCaptureRef if parent.isTrackableRef => tp.singletonCaptureSet @@ -1098,8 +1349,11 @@ object CaptureSet: CaptureSet.ofTypeDeeply(parent.widen) case tpd @ defn.RefinedFunctionOf(rinfo: MethodType) if followResult => ofType(tpd.parent, followResult = false) // pick up capture set from parent type - ++ (recur(rinfo.resType) // add capture set of result - -- CaptureSet(rinfo.paramRefs.filter(_.isTracked)*)) // but disregard bound parameters + ++ recur(rinfo.resType) // add capture set of result + .filter: + case TermParamRef(binder, _) => binder ne rinfo + case root.Result(binder) => binder ne rinfo + case _ => true case tpd @ AppliedType(tycon, args) => if followResult && defn.isNonRefinedFunction(tpd) then recur(args.last) @@ -1123,33 +1377,14 @@ object CaptureSet: /** The deep capture set of a type is the union of all covariant occurrences of * capture sets. Nested existential sets are approximated with `cap`. - * NOTE: The traversal logic needs to be in sync with narrowCaps in CaptureOps, which - * replaces caps with reach capabilties. The one exception to this is invariant - * arguments. This have to be included to be conservative in dcs but must be - * excluded in narrowCaps. */ def ofTypeDeeply(tp: Type, includeTypevars: Boolean = false)(using Context): CaptureSet = - val collect = new TypeAccumulator[CaptureSet]: - val seen = util.HashSet[Symbol]() - def apply(cs: CaptureSet, t: Type) = - if variance < 0 then cs - else t.dealias match - case t @ CapturingType(p, cs1) => - this(cs, p) ++ cs1 - case t @ AnnotatedType(parent, ann) => - this(cs, parent) - 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 - else this(cs, upper) - case t @ FunctionOrMethod(args, res @ Existential(_, _)) - if args.forall(_.isAlwaysPure) => - this(cs, Existential.toCap(res)) - case t @ Existential(_, _) => - cs - case _ => - foldOver(cs, t) + val collect = new DeepTypeAccumulator[CaptureSet]: + def capturingCase(acc: CaptureSet, parent: Type, refs: CaptureSet) = + this(acc, parent) ++ refs + def abstractTypeCase(acc: CaptureSet, t: TypeRef, upperBound: Type) = + if includeTypevars && upperBound.isExactlyAny then CaptureSet.fresh(t.symbol) + else this(acc, upperBound) collect(CaptureSet.empty, tp) type AssumedContains = immutable.Map[TypeRef, SimpleIdentitySet[CaptureRef]] @@ -1190,23 +1425,4 @@ object CaptureSet: println(i" ${cv.show.padTo(20, ' ')} :: ${cv.deps.toList}%, %") } else op - - def levelErrors: Addenda = new Addenda: - override def toAdd(using Context) = - for CompareResult.LevelError(cs, ref) <- ccState.levelError.toList yield - ccState.levelError = None - if ref.isRootCapability then - i""" - | - |Note that the universal capability `cap` - |cannot be included in capture set $cs""" - else - val levelStr = ref match - case ref: TermRef => i", defined in ${ref.symbol.maybeOwner}" - case _ => "" - i""" - | - |Note that reference ${ref}$levelStr - |cannot be included in outer capture set $cs""" - end CaptureSet diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 830d9ad0a4d4..8b6ce6550cc3 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, CompareFailure, ExistentialSubsumesFailure} import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} import reporting.{trace, Message, OverrideError} +import Annotations.Annotation /** The capture checker */ object CheckCaptures: @@ -88,6 +89,7 @@ object CheckCaptures: tp case _ => mapOver(tp) + override def toString = "SubstParamsMap" end SubstParamsMap /** Used for substituting parameters in a special case: when all actual arguments @@ -107,6 +109,7 @@ object CheckCaptures: tp case _ => mapOver(tp) + override def toString = "SubstParamsBiMap" lazy val inverse = new BiTypeMap: def apply(tp: Type): Type = tp match @@ -123,6 +126,7 @@ object CheckCaptures: tp case _ => mapOver(tp) + override def toString = "SubstParamsBiMap.inverse" def inverse = thisMap end SubstParamsBiMap @@ -150,6 +154,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 @@ -161,6 +166,10 @@ object CheckCaptures: private val seen = new EqHashSet[TypeRef] + // We keep track of open existential scopes here so that we can set these scopes + // in ccState when printing a part of the offending type. + var openExistentialScopes: List[MethodType] = Nil + def traverse(t: Type) = t.dealiasKeepAnnots match case t: TypeRef => @@ -178,51 +187,37 @@ object CheckCaptures: () case CapturingType(parent, refs) => if variance >= 0 then + val openScopes = openExistentialScopes refs.disallowRootCapability: () => - def part = if t eq tp then "" else i"the part $t of " + def part = + if t eq tp then "" + else + // Show in context of all enclosing traversed existential scopes. + def showInOpenedFreshBinders(mts: List[MethodType]): String = mts match + case Nil => i"the part $t of " + case mt :: mts1 => + CCState.inNewExistentialScope(mt): + showInOpenedFreshBinders(mts1) + showInOpenedFreshBinders(openScopes.reverse) report.error( em"""$what cannot $have $tp since |${part}that type captures the root capability `cap`.$addendum""", pos) traverse(parent) + case defn.RefinedFunctionOf(mt) => + traverse(mt) + case t: MethodType if t.marksExistentialScope => + atVariance(-variance): + t.paramInfos.foreach(traverse) + val saved = openExistentialScopes + openExistentialScopes = t :: openExistentialScopes + try traverse(t.resType) + finally openExistentialScopes = saved case t => traverseChildren(t) - if ccConfig.useSealed then check.traverse(tp) + check.traverse(tp) end disallowRootCapabilitiesIn - /** If we are not under the sealed policy, and a tree is an application that unboxes - * its result or is a try, check that the tree's type does not have covariant universal - * capabilities. - */ - private def checkNotUniversalInUnboxedResult(tpe: Type, tree: Tree)(using Context): Unit = - def needsUniversalCheck = tree match - case _: RefTree | _: Apply | _: TypeApply => tree.symbol.unboxesResult - case _: Try => true - case _ => false - - object checkNotUniversal extends TypeTraverser: - def traverse(tp: Type) = - tp.dealias match - case wtp @ CapturingType(parent, refs) => - if variance > 0 then - refs.disallowRootCapability: () => - def part = if wtp eq tpe.widen then "" else i" in its part $wtp" - report.error( - em"""The expression's type ${tpe.widen} is not allowed to capture the root capability `cap`$part. - |This usually means that a capability persists longer than its allowed lifetime.""", - tree.srcPos) - if !wtp.isBoxed then traverse(parent) - case tp => - traverseChildren(tp) - - if !ccConfig.useSealed - && !tpe.hasAnnotation(defn.UncheckedCapturesAnnot) - && needsUniversalCheck - && tpe.widen.isValueType - then - checkNotUniversal.traverse(tpe.widen) - end checkNotUniversalInUnboxedResult - trait CheckerAPI: /** Complete symbol info of a val or a def */ def completeDef(tree: ValOrDefDef, sym: Symbol)(using Context): Type @@ -237,6 +232,21 @@ 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 type of the formal paremeter corresponding to the argument. + */ + def formalType: Type + + /** The "use set", i.e. the capture set marked as free at this node. */ + def markedFree: CaptureSet + end CheckerAPI class CheckCaptures extends Recheck, SymTransformer: @@ -277,6 +287,18 @@ class CheckCaptures extends Recheck, SymTransformer: */ private val todoAtPostCheck = new mutable.ListBuffer[() => Unit] + /** Maps trees that need a separation check because they are arguments to + * polymorphic parameters. The trees are mapped to the formal parameter type. + */ + private val sepCheckFormals = util.EqHashMap[Tree, Type]() + + private val usedSet = util.EqHashMap[Tree, CaptureSet]() + + extension [T <: Tree](tree: T) + def needsSepCheck: Boolean = sepCheckFormals.contains(tree) + def formalType: Type = sepCheckFormals.getOrElse(tree, NoType) + def markedFree = usedSet.getOrElse(tree, CaptureSet.empty) + /** Instantiate capture set variables appearing contra-variantly to their * upper approximation. */ @@ -293,45 +315,68 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => traverseChildren(t) + /* Also set any previously unset owners of toplevel Fresh instances to improve + * error diagnostics in separation checking. + */ + private def anchorCaps(sym: Symbol)(using Context) = new TypeTraverser: + override def traverse(t: Type) = + if variance > 0 then + t match + case t @ CapturingType(parent, refs) => + for ref <- refs.elems do + ref match + case root.Fresh(hidden) if !hidden.givenOwner.exists => + hidden.givenOwner = sym + case _ => + traverse(parent) + case t @ defn.RefinedFunctionOf(rinfo) => + traverse(rinfo) + case _ => + traverseChildren(t) + /** If `tpt` is an inferred type, interpolate capture set variables appearing contra- - * variantly in it. + * variantly in it. Also anchor Fresh instances with anchorCaps. */ - private def interpolateVarsIn(tpt: Tree)(using Context): Unit = + private def interpolateVarsIn(tpt: Tree, sym: Symbol)(using Context): Unit = if tpt.isInstanceOf[InferredTypeTree] then interpolator().traverse(tpt.nuType) .showing(i"solved vars in ${tpt.nuType}", capt) + anchorCaps(sym).traverse(tpt.nuType) for msg <- ccState.approxWarnings do report.warning(msg, tpt.srcPos) ccState.approxWarnings.clear() /** 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).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 = - 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) + def checkOK(res: CompareResult, prefix: => String, added: CaptureRef | CaptureSet, pos: SrcPos, provenance: => String = "")(using Context): Unit = + res match + case res: CompareFailure => + inContext(root.printContext(added, res.blocking)): + def toAdd: String = errorNotes(res.errorNotes).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) + case _ => /** 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), + ccState.test(elem.singletonCaptureSet.subCaptures(cs)), 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), + ccState.test(cs1.subCaptures(cs2)), 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`. @@ -378,17 +423,17 @@ class CheckCaptures extends Recheck, SymTransformer: /** Include `sym` in the capture sets of all enclosing environments nested in the * the environment in which `sym` is defined. */ - def markFree(sym: Symbol, pos: SrcPos)(using Context): Unit = - markFree(sym, sym.termRef, pos) + def markFree(sym: Symbol, tree: Tree)(using Context): Unit = + markFree(sym, sym.termRef, tree) - def markFree(sym: Symbol, ref: TermRef, pos: SrcPos)(using Context): Unit = - if sym.exists && ref.isTracked then markFree(ref.captureSet, pos) + def markFree(sym: Symbol, ref: CaptureRef, tree: Tree)(using Context): Unit = + if sym.exists && ref.isTracked then markFree(ref.captureSet, tree) /** Make sure the (projected) `cs` is a subset of the capture sets of all enclosing * environments. At each stage, only include references from `cs` that are outside * the environment's owner */ - def markFree(cs: CaptureSet, pos: SrcPos)(using Context): Unit = + def markFree(cs: CaptureSet, tree: Tree)(using Context): Unit = // A captured reference with the symbol `sym` is visible from the environment // if `sym` is not defined inside the owner of the environment. inline def isVisibleFromEnv(sym: Symbol, env: Env) = @@ -410,7 +455,7 @@ class CheckCaptures extends Recheck, SymTransformer: val what = if ref.isType then "Capture set parameter" else "Local reach capability" report.error( em"""$what $c leaks into capture scope of ${env.ownerString}. - |To allow this, the ${ref.symbol} should be declared with a @use annotation""", pos) + |To allow this, the ${ref.symbol} should be declared with a @use annotation""", tree.srcPos) case _ => /** Avoid locally defined capability by charging the underlying type @@ -430,7 +475,7 @@ class CheckCaptures extends Recheck, SymTransformer: CaptureSet.ofType(c.widen, followResult = false) capt.println(i"Widen reach $c to $underlying in ${env.owner}") underlying.disallowRootCapability: () => - report.error(em"Local capability $c in ${env.ownerString} cannot have `cap` as underlying capture set", pos) + report.error(em"Local capability $c in ${env.ownerString} cannot have `cap` as underlying capture set", tree.srcPos) recur(underlying, env, lastEnv) /** Avoid locally defined capability if it is a reach capability or capture set @@ -452,9 +497,15 @@ class CheckCaptures extends Recheck, SymTransformer: // The path-use.scala neg test contains an example. val underlying = CaptureSet.ofTypeDeeply(c1.widen) capt.println(i"Widen reach $c to $underlying in ${env.owner}") - underlying.disallowRootCapability: () => - report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", pos) - recur(underlying, env, null) + if ccConfig.useSepChecks then + recur(underlying.filter(!_.isRootCapability), env, null) + // we don't want to disallow underlying Fresh instances, since these are typically locally created + // fresh capabilities. We don't need to also follow the hidden set since separation + // checking makes ure that locally hidden references need to go to @consume parameters. + else + underlying.disallowRootCapability: () => + report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", tree.srcPos) + recur(underlying, env, null) case c: TypeRef if c.isParamPath => checkUseDeclared(c, env, null) case _ => @@ -470,7 +521,7 @@ class CheckCaptures extends Recheck, SymTransformer: then avoidLocalCapability(c, env, lastEnv) else avoidLocalReachCapability(c, env) isVisible - checkSubset(included, env.captured, pos, provenance(env)) + checkSubset(included, env.captured, tree.srcPos, provenance(env)) capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}") if !isOfNestedMethod(env) then recur(included, nextEnvToCharge(env, !_.owner.isStaticOwner), env) @@ -478,13 +529,18 @@ class CheckCaptures extends Recheck, SymTransformer: // will be charged when that method is called. recur(cs, curEnv, null) + usedSet(tree) = tree.markedFree ++ cs end markFree /** Include references captured by the called method in the current environment stack */ - def includeCallCaptures(sym: Symbol, resType: Type, pos: SrcPos)(using Context): Unit = resType match + def includeCallCaptures(sym: Symbol, resType: Type, tree: Tree)(using Context): Unit = resType match case _: MethodOrPoly => // wait until method is fully applied case _ => - if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos) + def isRetained(ref: CaptureRef): Boolean = ref.pathRoot match + case root: ThisType => ctx.owner.isContainedIn(root.cls) + case _ => true + if sym.exists && curEnv.isOpen then + markFree(capturedVars(sym).filter(isRetained), tree) /** Under the sealed policy, disallow the root capability in type arguments. * Type arguments come either from a TypeApply node or from an AppliedType @@ -497,7 +553,7 @@ class CheckCaptures extends Recheck, SymTransformer: */ def disallowCapInTypeArgs(fn: Tree, sym: Symbol, args: List[Tree])(using Context): Unit = def isExempt = sym.isTypeTestOrCast || sym == defn.Compiletime_erasedValue - if ccConfig.useSealed && !isExempt then + if !isExempt then val paramNames = atPhase(thisPhase.prev): fn.tpe.widenDealias match case tl: TypeLambda => tl.paramNames @@ -508,44 +564,57 @@ class CheckCaptures extends Recheck, SymTransformer: for case (arg: TypeTree, pname) <- args.lazyZip(paramNames) do def where = if sym.exists then i" in an argument of $sym" else "" - val (addendum, pos) = + val (addendum, errTree) = if arg.isInferred - then ("\nThis is often caused by a local capability$where\nleaking as part of its result.", fn.srcPos) - else if arg.span.exists then ("", arg.srcPos) - else ("", fn.srcPos) + then ("\nThis is often caused by a local capability$where\nleaking as part of its result.", fn) + else if arg.span.exists then ("", arg) + else ("", fn) disallowRootCapabilitiesIn(arg.nuType, NoSymbol, - i"Type variable $pname of $sym", "be instantiated to", addendum, pos) + i"Type variable $pname of $sym", "be instantiated to", addendum, errTree.srcPos) val param = fn.symbol.paramNamed(pname) - if param.isUseParam then markFree(arg.nuType.deepCaptureSet, pos) + if param.isUseParam then markFree(arg.nuType.deepCaptureSet, errTree) end disallowCapInTypeArgs + /** Rechecking idents involves: + * - adding call captures for idents referring to methods + * - marking as free the identifier with any selections or .rd + * modifiers implied by the expected type + */ override def recheckIdent(tree: Ident, pt: Type)(using Context): Type = val sym = tree.symbol if sym.is(Method) then // If ident refers to a parameterless method, charge its cv to the environment - includeCallCaptures(sym, sym.info, tree.srcPos) + includeCallCaptures(sym, sym.info, tree) else if !sym.isStatic then - // Otherwise charge its symbol, but add all selections implied by the e - // 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 + // Otherwise charge its symbol, but add all selections and also any `.rd` + // modifier implied by the expected type `pt`. + // Example: If we have `x` and the expected type says we select that with `.a.b` + // where `b` is a read-only method, we charge `x.a.b.rd` instead of `x`. + 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) - markFree(sym, pathRef, tree.srcPos) + 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) super.recheckIdent(tree, pt) /** The expected type for the qualifier of a selection. If the selection - * could be part of a capabaility path, we return a PathSelectionProto. + * could be part of a capability path or is a a read-only method, we return + * a PathSelectionProto. */ 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 +642,15 @@ class CheckCaptures extends Recheck, SymTransformer: } case _ => denot + // Don't allow update methods to be called unless the qualifier captures + // an exclusive reference. TODO This should probably rolled into + // qualifier logic once we have it. + 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 @@ -580,7 +658,7 @@ class CheckCaptures extends Recheck, SymTransformer: // - on the LHS of assignments, or // - if the qualifier or selection type is boxed, or // - the selection is either a trackable capture ref or a pure type - if pt == LhsProto + if noWiden(selType, pt) || qualType.isBoxedCapturing || selWiden.isBoxedCapturing || selType.isTrackableRef @@ -602,15 +680,17 @@ class CheckCaptures extends Recheck, SymTransformer: selType }//.showing(i"recheck sel $tree, $qualType = $result") - /** Hook for massaging a function before it is applied. Copies all @use annotations - * on method parameter symbols to the corresponding paramInfo types. + /** Hook for massaging a function before it is applied. Copies all @use and @consume + * annotations on method parameter symbols to the corresponding paramInfo types. */ override def prepareFunction(funtpe: MethodType, meth: Symbol)(using Context): MethodType = - val paramInfosWithUses = funtpe.paramInfos.zipWithConserve(funtpe.paramNames): (formal, pname) => - val param = meth.paramNamed(pname) - param.getAnnotation(defn.UseAnnot) match - case Some(ann) => AnnotatedType(formal, ann) - case _ => formal + val paramInfosWithUses = + funtpe.paramInfos.zipWithConserve(funtpe.paramNames): (formal, pname) => + val param = meth.paramNamed(pname) + def copyAnnot(tp: Type, cls: ClassSymbol) = param.getAnnotation(cls) match + case Some(ann) => AnnotatedType(tp, ann) + case _ => tp + copyAnnot(copyAnnot(formal, defn.UseAnnot), defn.ConsumeAnnot) funtpe.derivedLambdaType(paramInfos = paramInfosWithUses) /** Recheck applications, with special handling of unsafeAssumePure. @@ -620,28 +700,31 @@ 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(root.Fresh())) 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) - includeCallCaptures(meth, res, tree.srcPos) + includeCallCaptures(meth, res, tree) res - /** Recheck argument, and, if formal parameter carries a `@use`, + /** Recheck argument against a "freshened" version of `formal` where toplevel `cap` + * occurrences are replaced by `Fresh` instances. Also, if formal parameter carries a `@use`, * 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 = root.capToFresh(formal) + val argType = recheck(arg, freshenedFormal) + .showing(i"recheck arg $arg vs $freshenedFormal", capt) + if formal.hasAnnotation(defn.UseAnnot) || formal.hasAnnotation(defn.ConsumeAnnot) then + // The @use and/or @consume annotation is added to `formal` by `prepareFunction` + capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") + markFree(argType.deepCaptureSet, arg) + if formal.containsCap then + sepCheckFormals(arg) = freshenedFormal argType /** Map existential captures in result to `cap` and implement the following @@ -667,13 +750,11 @@ class CheckCaptures extends Recheck, SymTransformer: */ protected override def recheckApplication(tree: Apply, qualType: Type, funType: MethodType, argTypes: List[Type])(using Context): Type = - val appType = Existential.toCap(super.recheckApplication(tree, qualType, funType, argTypes)) + val appType = root.resultToFresh(super.recheckApplication(tree, qualType, funType, argTypes)) 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.hasAnnotation(defn.UseAnnot) then argType.deepCaptureSet else argType.captureSet appType match case appType @ CapturingType(appType1, refs) if qualType.exists @@ -722,20 +803,28 @@ class CheckCaptures extends Recheck, SymTransformer: /** First half of result pair: * Refine the type of a constructor call `new C(t_1, ..., t_n)` - * to C{val x_1: T_1, ..., x_m: T_m} where x_1, ..., x_m are the tracked - * parameters of C and T_1, ..., T_m are the types of the corresponding arguments. + * to C{val x_1: @refineOverride T_1, ..., x_m: @refineOverride T_m} + * where x_1, ..., x_m are the tracked parameters of C and + * T_1, ..., T_m are the types of the corresponding arguments. The @refineOveride + * annotations avoid problematic intersections of capture sets when those + * parameters are selected. * * Second half: union of initial capture set and all capture sets of arguments - * to tracked parameters. + * to tracked parameters. The initial capture set `initCs` is augmented with + * - root.Fresh(...) if `core` extends Mutable + * - root.Fresh(...).rd if `core` extends Capability */ 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 initCs ++ CaptureSet.fresh() + else if core.derivesFromCapability then initCs ++ root.Fresh.withOwner(core.classSymbol).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 - refined = RefinedType(refined, getterName, argType.unboxed) // Yichen you might want to check this + refined = RefinedType(refined, getterName, + AnnotatedType(argType.unboxed, Annotation(defn.RefineOverrideAnnot, util.Spans.NoSpan))) // Yichen you might want to check this allCaptures ++= argType.captureSet (refined, allCaptures) @@ -770,8 +859,8 @@ class CheckCaptures extends Recheck, SymTransformer: case fun @ Select(qual, nme.apply) => qual.symbol.orElse(fun.symbol) case fun => fun.symbol disallowCapInTypeArgs(tree.fun, meth, tree.args) - val res = Existential.toCap(super.recheckTypeApply(tree, pt)) - includeCallCaptures(tree.symbol, res, tree.srcPos) + val res = root.resultToFresh(super.recheckTypeApply(tree, pt)) + includeCallCaptures(tree.symbol, res, tree) checkContains(tree) res end recheckTypeApply @@ -822,8 +911,8 @@ class CheckCaptures extends Recheck, SymTransformer: // which are less intelligible. An example is the line `a = x` in // neg-custom-args/captures/vars.scala. That's why this code is conditioned. // to apply only to closures that are not eta expansions. - val res1 = Existential.toCapDeeply(res) - val pt1 = Existential.toCapDeeply(pt) + val res1 = root.resultToFresh(res) // TODO: why deep = true? + val pt1 = root.resultToFresh(pt) // We need to open existentials here in order not to get vars mixed up in them // We do the proper check with existentials when we are finished with the closure block. capt.println(i"pre-check closure $expr of type $res1 against $pt1") @@ -872,7 +961,7 @@ class CheckCaptures extends Recheck, SymTransformer: // for more info from the context, so we cannot interpolate. Note that we cannot // expect to have all necessary info available at the point where the anonymous // function is compiled since we do not propagate expected types into blocks. - interpolateVarsIn(tree.tpt) + interpolateVarsIn(tree.tpt, sym) /** Recheck method definitions: * - check body in a nested environment that tracks uses, in a nested level, @@ -918,7 +1007,7 @@ class CheckCaptures extends Recheck, SymTransformer: if !sym.isAnonymousFunction then // Anonymous functions propagate their type to the enclosing environment // so it is not in general sound to interpolate their types. - interpolateVarsIn(tree.tpt) + interpolateVarsIn(tree.tpt, sym) curEnv = saved end recheckDefDef @@ -1018,7 +1107,8 @@ class CheckCaptures extends Recheck, SymTransformer: for param <- cls.paramGetters do if !param.hasAnnotation(defn.ConstructorOnlyAnnot) && !param.hasAnnotation(defn.UntrackedCapturesAnnot) then - checkSubset(param.termRef.captureSet, thisSet, param.srcPos) // (3) + CCState.withCapAsRoot: // OK? We need this here since self types use `cap` instead of `fresh` + checkSubset(param.termRef.captureSet, thisSet, param.srcPos) // (3) for pureBase <- cls.pureBaseClass do // (4) def selfTypeTree = impl.body .collect: @@ -1048,7 +1138,7 @@ class CheckCaptures extends Recheck, SymTransformer: case AnnotatedType(_, annot) if annot.symbol == defn.RequiresCapabilityAnnot => annot.tree match case Apply(_, cap :: Nil) => - markFree(cap.symbol, tree.srcPos) + markFree(cap.symbol, tree) case _ => case _ => super.recheckTyped(tree) @@ -1058,7 +1148,7 @@ class CheckCaptures extends Recheck, SymTransformer: */ override def recheckTry(tree: Try, pt: Type)(using Context): Type = val tp = super.recheckTry(tree, pt) - if ccConfig.useSealed && Feature.enabled(Feature.saferExceptions) then + if Feature.enabled(Feature.saferExceptions) then disallowRootCapabilitiesIn(tp, ctx.owner, "The result of `try`", "have type", "\nThis is often caused by a locally generated exception capability leaking as part of its result.", @@ -1103,14 +1193,9 @@ class CheckCaptures extends Recheck, SymTransformer: super.recheck(tree, pt) finally curEnv = saved if tree.isTerm && !pt.isBoxedCapturing && pt != LhsProto then - markFree(res.boxedCaptureSet, tree.srcPos) + markFree(res.boxedCaptureSet, tree) res - - /** Under the old unsealed policy: check that cap is ot unboxed */ - override def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = - checkNotUniversalInUnboxedResult(tpe, tree) - super.recheckFinish(tpe, tree, pt) - end recheckFinish + end recheck // ------------------ Adaptation ------------------------------------- // @@ -1127,14 +1212,29 @@ class CheckCaptures extends Recheck, SymTransformer: type BoxErrors = mutable.ListBuffer[Message] | Null - private def boxErrorAddenda(boxErrors: BoxErrors) = - if boxErrors == null then NothingToAdd + private def errorNotes(notes: List[ErrorNote])(using Context): Addenda = + if notes.isEmpty then NothingToAdd else new Addenda: - override def toAdd(using Context): List[String] = - boxErrors.toList.map: msg => - i""" - | - |Note that ${msg.toString}""" + override def toAdd(using Context) = notes.map: note => + val msg = note match + case CompareResult.LevelError(cs, ref) => + if ref.stripReadOnly.isCapOrFresh then + def capStr = if ref.isReadOnly then "cap.rd" else "cap" + i"""the universal capability `$capStr` + |cannot be included in capture set $cs""" + else + val levelStr = ref match + case ref: TermRef => i", defined in ${ref.symbol.maybeOwner}" + case _ => "" + i"""reference ${ref}$levelStr + |cannot be included in outer capture set $cs""" + case ExistentialSubsumesFailure(ex, other) => + i"""the existential capture root in ${ex.rootAnnot.originalBinder.resType} + |cannot subsume the capability $other""" + i""" + | + |Note that ${msg.toString}""" + /** Addendas for error messages that show where we have under-approximated by * mapping a a capture ref in contravariant position to the empty set because @@ -1168,26 +1268,27 @@ class CheckCaptures extends Recheck, SymTransformer: */ override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type = var expected1 = alignDependentFunction(expected, actual.stripCapturing) - val boxErrors = new mutable.ListBuffer[Message] - val actualBoxed = adapt(actual, expected1, tree.srcPos, boxErrors) + val actualBoxed = adapt(actual, expected1, tree) //println(i"check conforms $actualBoxed <<< $expected1") if actualBoxed eq actual then // Only `addOuterRefs` when there is no box adaptation expected1 = addOuterRefs(expected1, actual, tree.srcPos) - if isCompatible(actualBoxed, expected1) then - if debugSuccesses then tree match - case Ident(_) => - println(i"SUCCESS $tree:\n${TypeComparer.explained(_.isSubType(actual, expected))}") - case _ => - 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)) - actual + ccState.testOK(isCompatible(actualBoxed, expected1)) match + case CompareResult.OK => + if debugSuccesses then tree match + case Ident(_) => + println(i"SUCCESS $tree for $actual <:< $expected:\n${TypeComparer.explained(_.isSubType(actualBoxed, expected1))}") + case _ => + actualBoxed + case fail: CompareFailure => + capt.println(i"conforms failed for ${tree}: $actual vs $expected") + inContext(root.printContext(actualBoxed, expected1)): + err.typeMismatch(tree.withType(actualBoxed), expected1, + addApproxAddenda( + addenda ++ errorNotes(fail.errorNotes), + expected1)) + actual end checkConformsExpr /** Turn `expected` into a dependent function when `actual` is dependent. */ @@ -1203,6 +1304,14 @@ class CheckCaptures extends Recheck, SymTransformer: case defn.RefinedFunctionOf(rinfo: MethodType) => depFun(args, resultType, isContextual, rinfo.paramNames) case _ => expected + case expected @ defn.RefinedFunctionOf(einfo: MethodType) + if einfo.allParamNamesSynthetic => + actual match + case defn.RefinedFunctionOf(ainfo: MethodType) + if !ainfo.allParamNamesSynthetic && ainfo.paramNames.hasSameLengthAs(einfo.paramNames) => + einfo.derivedLambdaType(paramNames = ainfo.paramNames) + .toFunctionType(alwaysDependent = true) + case _ => expected case _ => expected recur(expected) @@ -1288,7 +1397,7 @@ class CheckCaptures extends Recheck, SymTransformer: * * @param alwaysConst always make capture set variables constant after adaptation */ - def adaptBoxed(actual: Type, expected: Type, pos: SrcPos, covariant: Boolean, alwaysConst: Boolean, boxErrors: BoxErrors)(using Context): Type = + def adaptBoxed(actual: Type, expected: Type, tree: Tree, covariant: Boolean, alwaysConst: Boolean)(using Context): Type = def recur(actual: Type, expected: Type, covariant: Boolean): Type = @@ -1320,17 +1429,9 @@ class CheckCaptures extends Recheck, SymTransformer: def adaptStr = i"adapting $actual ${if covariant then "~~>" else "<~~"} $expected" - // Get existentials and wildcards out of the way - actual match - case actual @ Existential(_, actualUnpacked) => - return Existential.derivedExistentialType(actual): - recur(actualUnpacked, expected, covariant) - case _ => + // Get wildcards out of the way expected match - case expected @ Existential(_, expectedUnpacked) => - return recur(actual, expectedUnpacked, covariant) - case _: WildcardType => - return actual + case _: WildcardType => return actual case _ => trace(adaptStr, capt, show = true) { @@ -1352,10 +1453,12 @@ class CheckCaptures extends Recheck, SymTransformer: val cs = actual.captureSet if covariant then cs ++ leaked else - if !leaked.subCaptures(cs, frozen = false).isOK then + if // CCState.withCapAsRoot: // Not sure withCapAsRoot is OK here, actually + !leaked.subCaptures(cs).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) + |since the additional capture set $leaked resulting from box conversion is not allowed in $actual""", tree.srcPos) cs def adaptedType(resultBoxed: Boolean) = @@ -1365,34 +1468,12 @@ class CheckCaptures extends Recheck, SymTransformer: .capturing(if alwaysConst then CaptureSet(captures.elems) else captures) .forceBoxStatus(resultBoxed) - if needsAdaptation then - val criticalSet = // the set with which we box or unbox + if needsAdaptation && !insertBox then // we are unboxing + val criticalSet = // the set with which we unbox if covariant then captures // covariant: we box with captures of actual type plus captures leaked by inner adapation else expected.captureSet // contravarant: we unbox with captures of epected type - def msg = em"""$actual cannot be box-converted to $expected - |since at least one of their capture sets contains the root capability `cap`""" - def allowUniversalInBoxed = - ccConfig.useSealed - || expected.hasAnnotation(defn.UncheckedCapturesAnnot) - || actual.widen.hasAnnotation(defn.UncheckedCapturesAnnot) - if !allowUniversalInBoxed then - if criticalSet.isUnboxable && expected.isValueType then - // We can't box/unbox the universal capability. Leave `actual` as it is - // so we get an error in checkConforms. Add the error message generated - // from boxing as an addendum. This tends to give better error - // messages than disallowing the root capability in `criticalSet`. - if boxErrors != null then boxErrors += msg - if ctx.settings.YccDebug.value then - println(i"cannot box/unbox $actual vs $expected") - return actual - // Disallow future addition of `cap` to `criticalSet`. - criticalSet.disallowRootCapability: () => - report.error(msg, pos) - - if !insertBox then // we are unboxing //debugShowEnvs() - markFree(criticalSet, pos) - end if + markFree(criticalSet, tree) // Compute the adapted type. // The result is boxed if actual is boxed and we don't need to adapt, @@ -1418,30 +1499,70 @@ class CheckCaptures extends Recheck, SymTransformer: * Then * foo: Foo { def a: C^{foo}; def b: C^{foo} }^{foo} */ - private def improveCaptures(widened: Type, actual: Type)(using Context): Type = actual match + private def improveCaptures(widened: Type, prefix: Type)(using Context): Type = prefix match case ref: CaptureRef if ref.isTracked => widened match - case CapturingType(p, refs) if ref.singletonCaptureSet.mightSubcapture(refs) => - widened.derivedCapturingType(p, ref.singletonCaptureSet) + case widened @ CapturingType(p, refs) if ref.singletonCaptureSet.mightSubcapture(refs) => + val improvedCs = + if widened.isBoxed then ref.reach.singletonCaptureSet + else ref.singletonCaptureSet + widened.derivedCapturingType(p, improvedCs) .showing(i"improve $widened to $result", capt) 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 type 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 instantiation. + */ + 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 + + /* Currently not needed since it forms part of `adapt` + private def improve(actual: Type, prefix: Type)(using Context): Type = + val widened = actual.widen.dealiasKeepAnnots + val improved = improveCaptures(widened, prefix).withReachCaptures(prefix) + if improved eq widened then actual else improved + */ + + /** An actual singleton type should not be widened if the expected type is a + * LhsProto, or a singleton type, or a path selection with a stable value + */ + private def noWiden(actual: Type, expected: Type)(using Context): Boolean = + actual.isSingleton + && expected.match + case expected: PathSelectionProto => !expected.sym.isOneOf(UnstableValueFlags) + case _ => expected.isSingleton || expected == LhsProto + /** 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*}` * - do box adaptation */ - def adapt(actual: Type, expected: Type, pos: SrcPos, boxErrors: BoxErrors)(using Context): Type = - if expected == LhsProto || expected.isSingleton && actual.isSingleton then + def adapt(actual: Type, expected: Type, tree: Tree)(using Context): Type = + if noWiden(actual, expected) then actual else - val widened = improveCaptures(actual.widen.dealiasKeepAnnots, actual) + val improvedVAR = improveCaptures(actual.widen.dealiasKeepAnnots, actual) + val improved = improveReadOnly(improvedVAR, expected) val adapted = adaptBoxed( - widened.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) + improved.withReachCaptures(actual), expected, tree, + covariant = true, alwaysConst = false) + 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 ------------------------------------------- @@ -1452,19 +1573,19 @@ class CheckCaptures extends Recheck, SymTransformer: * But maybe we can then elide the check during the RefChecks phase under captureChecking? */ def checkOverrides = new TreeTraverser: - class OverridingPairsCheckerCC(clazz: ClassSymbol, self: Type, srcPos: SrcPos)(using Context) extends OverridingPairsChecker(clazz, self): + class OverridingPairsCheckerCC(clazz: ClassSymbol, self: Type, tree: Tree)(using Context) extends OverridingPairsChecker(clazz, self): /** Check subtype with box adaptation. * This function is passed to RefChecks to check the compatibility of overriding pairs. * @param sym symbol of the field definition that is being checked */ override def checkSubType(actual: Type, expected: Type)(using Context): Boolean = - val expected1 = alignDependentFunction(addOuterRefs(expected, actual, srcPos), actual.stripCapturing) + val expected1 = alignDependentFunction(addOuterRefs(expected, actual, tree.srcPos), actual.stripCapturing) val actual1 = val saved = curEnv try curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv) val adapted = - adaptBoxed(actual, expected1, srcPos, covariant = true, alwaysConst = true, null) + adaptBoxed(actual, expected1, tree, covariant = true, alwaysConst = true) actual match case _: MethodType => // We remove the capture set resulted from box adaptation for method types, @@ -1482,20 +1603,23 @@ class CheckCaptures extends Recheck, SymTransformer: override def checkInheritedTraitParameters: Boolean = false - /** Check that overrides don't change the @use status of their parameters */ + /** Check that overrides don't change the @use or @consume status of their parameters */ override def additionalChecks(member: Symbol, other: Symbol)(using Context): Unit = 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 - ) + def checkAnnot(cls: ClassSymbol) = + if param1.hasAnnotation(cls) != param2.hasAnnotation(cls) then + report.error( + OverrideError( + i"has a parameter ${param1.name} with different @${cls.name} 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) + + checkAnnot(defn.UseAnnot) + checkAnnot(defn.ConsumeAnnot) end OverridingPairsCheckerCC def traverse(t: Tree)(using Context) = @@ -1526,7 +1650,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 +1725,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 @@ -1650,13 +1774,13 @@ class CheckCaptures extends Recheck, SymTransformer: case ref: TermParamRef if !allowed.contains(ref) && !seen.contains(ref) => seen += ref - if ref.isMaxCapability then + if ref.isRootCapability then report.error(i"escaping local reference $ref", tree.srcPos) else 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).isOK then val location = if meth.exists then i" of ${meth.showLocated}" else "" val paramInfo = if ref.paramName.info.kind.isInstanceOf[UniqueNameKind] @@ -1733,16 +1857,16 @@ class CheckCaptures extends Recheck, SymTransformer: val normArgs = args.lazyZip(tl.paramInfos).map: (arg, bounds) => arg.withType(arg.nuType.forceBoxStatus( bounds.hi.isBoxedCapturing | bounds.lo.isBoxedCapturing)) - checkBounds(normArgs, tl) + CCState.withCapAsRoot: // OK? We need this since bounds use `cap` instead of `fresh` + checkBounds(normArgs, tl) args.lazyZip(tl.paramNames).foreach(healTypeParam(_, _, fun.symbol)) case _ => - case tree: TypeTree if !ccConfig.useSealed => - checkArraysAreSealedIn(tree.tpe, tree.srcPos) case _ => end check end checker checker.traverse(unit)(using ctx.withOwner(defn.RootClass)) + if ccConfig.useSepChecks then SepCheck(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 deleted file mode 100644 index ea979e0b9f7f..000000000000 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ /dev/null @@ -1,385 +0,0 @@ -package dotty.tools -package dotc -package cc - -import core.* -import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* -import CaptureSet.IdempotentCaptRefMap -import StdNames.nme -import ast.tpd.* -import Decorators.* -import typer.ErrorReporting.errorType -import Names.TermName -import NameKinds.ExistentialBinderName -import NameOps.isImpureFunction -import reporting.Message - -/** - -Handling existentials in CC: - - - We generally use existentials only in function and method result types - - All occurrences of an EX-bound variable appear co-variantly in the bound type - -In Setup: - - - Convert occurrences of `cap` in function results to existentials. Precise rules below. - - Conversions are done in two places: - - + As part of mapping from local types of parameters and results to infos of methods. - The local types just use `cap`, whereas the result type in the info uses EX-bound variables. - + When converting functions or methods appearing in explicitly declared types. - Here again, we only replace cap's in fucntion results. - - - Conversion is done with a BiTypeMap in `Existential.mapCap`. - -In reckeckApply and recheckTypeApply: - - - If an EX is toplevel in the result type, replace its bound variable - occurrences with `cap`. - -Level checking and avoidance: - - - Environments, capture refs, and capture set variables carry levels - - + levels start at 0 - + The level of a block or template statement sequence is one higher than the level of - its environment - + The level of a TermRef is the level of the environment where its symbol is defined. - + The level of a ThisType is the level of the statements of the class to which it beloongs. - + The level of a TermParamRef is currently -1 (i.e. TermParamRefs are not yet checked using this system) - + The level of a capture set variable is the level of the environment where it is created. - - - Variables also carry info whether they accept `cap` or not. Variables introduced under a box - don't, the others do. - - - Capture set variables do not accept elements of level higher than the variable's level - - - We use avoidance to heal such cases: If the level-incorrect ref appears - + covariantly: widen to underlying capture set, reject if that is cap and the variable does not allow it - + contravariantly: narrow to {} - + invarianty: reject with error - -In cv-computation (markFree): - - - Reach capabilities x* of a parameter x cannot appear in the capture set of - the owning method. They have to be widened to dcs(x), or, where this is not - possible, it's an error. - -In box adaptation: - - - Check that existential variables are not boxed or unboxed. - -Subtype rules - - - new alphabet: existentially bound variables `a`. - - they can be stored in environments Gamma. - - they are alpha-renable, usual hygiene conditions apply - - Gamma |- EX a.T <: U - if Gamma, a |- T <: U - - Gamma |- T <: EX a.U - if exists capture set C consisting of capture refs and ex-bound variables - bound in Gamma such that Gamma |- T <: [a := C]U - -Representation: - - EX a.T[a] is represented as a dependent function type - - (a: Exists) => T[a]] - - where Exists is defined in caps like this: - - sealed trait Exists extends Capability - - The defn.RefinedFunctionOf extractor will exclude existential types from - its results, so only normal refined functions match. - - Let `boundvar(ex)` be the TermParamRef defined by the existential type `ex`. - -Subtype checking algorithm, general scheme: - - Maintain two structures in TypeComparer: - - openExistentials: List[TermParamRef] - assocExistentials: Map[TermParamRef, List[TermParamRef]] - - `openExistentials` corresponds to the list of existential variables stored in the environment. - `assocExistentials` maps existential variables bound by existentials appearing on the right - to the value of `openExistentials` at the time when the existential on the right was dropped. - -Subtype checking algorithm, steps to add for tp1 <:< tp2: - - If tp1 is an existential EX a.tp1a: - - val saved = openExistentials - openExistentials = boundvar(tp1) :: openExistentials - try tp1a <:< tp2 - finally openExistentials = saved - - If tp2 is an existential EX a.tp2a: - - val saved = assocExistentials - assocExistentials = assocExistentials + (boundvar(tp2) -> openExistentials) - try tp1 <:< tp2a - finally assocExistentials = saved - - If tp2 is an existentially bound variable: - assocExistentials(tp2).isDefined - && (assocExistentials(tp2).contains(tp1) || tp1 is not existentially bound) - -Subtype checking algorithm, comparing two capture sets CS1 <:< CS2: - - We need to map the (possibly to-be-added) existentials in CS1 to existentials - in CS2 so that we can compare them. We use `assocExistentals` for that: - To map an EX-variable V1 in CS1, pick the last (i.e. outermost, leading to the smallest - type) EX-variable in `assocExistentials` that has V1 in its possible instances. - To go the other way (and therby produce a BiTypeMap), map an EX-variable - V2 in CS2 to the first (i.e. innermost) EX-variable it can be instantiated to. - If either direction is not defined, we choose a special "bad-existetal" value - that represents and out-of-scope existential. This leads to failure - of the comparison. - -Existential source syntax: - - Existential types are ususally not written in source, since we still allow the `^` - syntax that can express most of them more concesely (see below for translation rules). - But we should also allow to write existential types explicity, even if it ends up mainly - for debugging. To express them, we use the encoding with `Exists`, so a typical - expression of an existential would be - - (x: Exists) => A ->{x} B - - Existential types can only at the top level of the result type - of a function or method. - -Restrictions on Existential Types: (to be implemented if we want to -keep the source syntax for users). - - - An existential capture ref must be the only member of its set. This is - intended to model the idea that existential variables effectibely range - over capture sets, not capture references. But so far our calculus - and implementation does not yet acoommodate first-class capture sets. - - Existential capture refs must appear co-variantly in their bound type - - So the following would all be illegal: - - EX x.C^{x, io} // error: multiple members - EX x.() => EX y.C^{x, y} // error: multiple members - EX x.C^{x} ->{x} D // error: contra-variant occurrence - EX x.Set[C^{x}] // error: invariant occurrence - -Expansion of ^: - - We expand all occurrences of `cap` in the result types of functions or methods - to existentially quantified types. Nested scopes are expanded before outer ones. - - The expansion algorithm is then defined as follows: - - 1. In a result type, replace every occurrence of ^ with a fresh existentially - bound variable and quantify over all variables such introduced. - - 2. After this step, type aliases are expanded. If aliases have aliases in arguments, - the outer alias is expanded before the aliases in the arguments. Each time an alias - is expanded that reveals a `^`, apply step (1). - - 3. The algorithm ends when no more alieases remain to be expanded. - - Examples: - - - `A => B` is an alias type that expands to `(A -> B)^`, therefore - `() -> A => B` expands to `() -> EX c. A ->{c} B`. - - - `() => Iterator[A => B]` expands to `() => EX c. Iterator[A ->{c} B]` - - - `A -> B^` expands to `A -> EX c.B^{c}`. - - - If we define `type Fun[T] = A -> T`, then `() -> Fun[B^]` expands to `() -> EX c.Fun[B^{c}]`, which - dealiases to `() -> EX c.A -> B^{c}`. - - - If we define - - type F = A -> Fun[B^] - - then the type alias expands to - - type F = A -> EX c.A -> B^{c} -*/ -object Existential: - - type Carrier = RefinedType - - def unapply(tp: Carrier)(using Context): Option[(TermParamRef, Type)] = - tp.refinedInfo match - case mt: MethodType - if isExistentialMethod(mt) && defn.isNonRefinedFunction(tp.parent) => - Some(mt.paramRefs.head, mt.resultType) - case _ => None - - /** Create method type in the refinement of an existential type */ - private def exMethodType(using Context)( - mk: TermParamRef => Type, - boundName: TermName = ExistentialBinderName.fresh() - ): MethodType = - MethodType(boundName :: Nil)( - mt => defn.Caps_Exists.typeRef :: Nil, - mt => mk(mt.paramRefs.head)) - - /** Create existential */ - def apply(mk: TermParamRef => Type)(using Context): Type = - exMethodType(mk).toFunctionType(alwaysDependent = true) - - /** Create existential if bound variable appears in result of `mk` */ - def wrap(mk: TermParamRef => Type)(using Context): Type = - val mt = exMethodType(mk) - if mt.isResultDependent then mt.toFunctionType() else mt.resType - - extension (tp: Carrier) - def derivedExistentialType(core: Type)(using Context): Type = tp match - case Existential(boundVar, unpacked) => - if core eq unpacked then tp - else apply(bv => core.substParam(boundVar, bv)) - 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. - */ - 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 - case tp1 @ CapturingType(parent, refs) => - tp1.derivedCapturingType(toCap(parent), refs) - case tp1 @ AnnotatedType(parent, ann) => - tp1.derivedAnnotatedType(toCap(parent), ann) - case _ => tp - - /** Map existentials at the top-level and in all nested result types to `cap` - */ - def toCapDeeply(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match - case Existential(boundVar, unpacked) => - toCapDeeply(unpacked.substParam(boundVar, defn.captureRoot.termRef)) - case tp1 @ FunctionOrMethod(args, res) => - val tp2 = tp1.derivedFunctionOrMethod(args, toCapDeeply(res)) - if tp2 ne tp1 then tp2 else tp - case tp1 @ CapturingType(parent, refs) => - tp1.derivedCapturingType(toCapDeeply(parent), refs) - case tp1 @ AnnotatedType(parent, ann) => - tp1.derivedAnnotatedType(toCapDeeply(parent), ann) - case _ => tp - - /** Knowing that `tp` is a function type, is an alias to a function other - * than `=>`? - */ - private def isAliasFun(tp: Type)(using Context) = tp match - case AppliedType(tycon, _) => !defn.isFunctionSymbol(tycon.typeSymbol) - case _ => false - - /** Replace all occurrences of `cap` 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. - */ - def mapCap(tp: Type, fail: Message => Unit)(using Context): Type = - var needsWrap = false - - abstract class CapMap extends BiTypeMap: - override def mapOver(t: Type): Type = t match - case t @ FunctionOrMethod(args, res) if variance > 0 && !isAliasFun(t) => - t // `t` should be mapped in this case by a different call to `mapCap`. - case Existential(_, _) => - t - case t: (LazyRef | TypeVar) => - mapConserveSuper(t) - case _ => - super.mapOver(t) - - class Wrap(boundVar: TermParamRef) extends CapMap: - def apply(t: Type) = t match - case t: TermRef if t.isRootCapability => - if variance > 0 then - needsWrap = true - boundVar - else - if variance == 0 then - fail(em"""$tp captures the root capability `cap` in invariant position""") - // we accept variance < 0, and leave the cap as it is - super.mapOver(t) - case t @ CapturingType(parent, refs: CaptureSet.Var) => - if variance > 0 then needsWrap = true - super.mapOver(t) - case defn.FunctionNOf(args, res, contextual) if t.typeSymbol.name.isImpureFunction => - if variance > 0 then - needsWrap = true - super.mapOver: - defn.FunctionNOf(args, res, contextual).capturing(boundVar.singletonCaptureSet) - else mapOver(t) - case _ => - mapOver(t) - //.showing(i"mapcap $t = $result") - - lazy val inverse = new BiTypeMap: - def apply(t: Type) = t match - case t: TermParamRef if t eq boundVar => defn.captureRoot.termRef - case _ => mapOver(t) - def inverse = Wrap.this - override def toString = "Wrap.inverse" - end Wrap - - val wrapped = apply(Wrap(_)(tp)) - if needsWrap then wrapped else tp - end mapCap - - /** Map `cap` in function results to fresh existentials */ - def mapCapInResults(fail: Message => Unit)(using Context): TypeMap = new: - - def mapFunOrMethod(tp: Type, args: List[Type], res: Type): Type = - val args1 = atVariance(-variance)(args.map(this)) - val res1 = res match - case res: MethodType => mapFunOrMethod(res, res.paramInfos, res.resType) - case res: PolyType => mapFunOrMethod(res, Nil, res.resType) // TODO: Also map bounds of PolyTypes - case _ => mapCap(apply(res), fail) - //.showing(i"map cap res $res / ${apply(res)} of $tp = $result") - tp.derivedFunctionOrMethod(args1, res1) - - def apply(t: Type): Type = t match - case FunctionOrMethod(args, res) if variance > 0 && !isAliasFun(t) => - mapFunOrMethod(t, args, res) - case CapturingType(parent, refs) => - t.derivedCapturingType(this(parent), refs) - case Existential(_, _) => - t - case t: (LazyRef | TypeVar) => - mapConserveSuper(t) - case _ => - mapOver(t) - end mapCapInResults - - /** Is `mt` a method represnting an existential type when used in a refinement? */ - def isExistentialMethod(mt: TermLambda)(using Context): Boolean = mt.paramInfos match - case (info: TypeRef) :: rest => info.symbol == defn.Caps_Exists && rest.isEmpty - case _ => false - - /** Is `ref` this an existentially bound variable? */ - def isExistentialVar(ref: CaptureRef)(using Context) = ref match - case ref: TermParamRef => isExistentialMethod(ref.binder) - case _ => false - - /** An value signalling an out-of-scope existential that should - * lead to a compare failure. - */ - def badExistential(using Context): TermParamRef = - exMethodType(identity, nme.OOS_EXISTENTIAL).paramRefs.head - - def isBadExistential(ref: CaptureRef) = ref match - case ref: TermParamRef => ref.paramName == nme.OOS_EXISTENTIAL - case _ => false - -end Existential 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..c8ab2ccbe81a --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -0,0 +1,959 @@ +package dotty.tools +package dotc +package cc +import ast.tpd +import collection.mutable + +import core.* +import Symbols.*, Types.*, Flags.* +import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* +import CaptureSet.{Refs, emptyRefs, HiddenSet} +import config.Printers.capt +import StdNames.nme +import util.{SimpleIdentitySet, EqHashMap, SrcPos} +import tpd.* +import reflect.ClassTag +import reporting.trace + +/** The separation checker is a tree traverser that is run after capture checking. + * It checks tree nodes for various separation conditions, explained in the + * methods below. Rough summary: + * + * - Hidden sets of arguments must not be referred to in the same application + * - Hidden sets of (result-) types must not be referred to alter in the same scope. + * - Returned hidden sets can only refer to @consume parameters. + * - If returned hidden sets refer to an encloding this, the reference must be + * from a @consume method. + * - Consumed entities cannot be used subsequently. + * - Entitites cannot be consumed in a loop. + */ +object SepCheck: + + /** Enumerates kinds of captures encountered so far */ + enum Captures: + case None + case Explicit // one or more explicitly declared captures + case Hidden // exacttly one hidden captures + case NeedsCheck // one hidden capture and one other capture (hidden or declared) + + def add(that: Captures): Captures = + if this == None then that + else if that == None then this + else if this == Explicit && that == Explicit then Explicit + else NeedsCheck + end Captures + + /** The role in which a checked type appears, used for composing error messages */ + enum TypeRole: + case Result(sym: Symbol, inferred: Boolean) + case Argument(arg: Tree) + case Qualifier(qual: Tree, meth: Symbol) + + /** If this is a Result tole, the associated symbol, otherwise NoSymbol */ + def dclSym = this match + case Result(sym, _) => sym + case _ => NoSymbol + + /** A textual description of this role */ + def description(using Context): String = this match + case Result(sym, inferred) => + def inferredStr = if inferred then " inferred" else "" + def resultStr = if sym.info.isInstanceOf[MethodicType] then " result" else "" + i"$sym's$inferredStr$resultStr type" + case TypeRole.Argument(_) => + "the argument's adapted type" + case TypeRole.Qualifier(_, meth) => + i"the type of the prefix to a call of $meth" + end TypeRole + + /** A class for segmented sets of consumed references. + * References are associated with the source positions where they first appeared. + * References are compared with `eq`. + */ + abstract class ConsumedSet: + /** The references in the set. The array should be treated as immutable in client code */ + def refs: Array[CaptureRef] + + /** The associated source positoons. The array should be treated as immutable in client code */ + def locs: Array[SrcPos] + + /** The number of references in the set */ + def size: Int + + def toMap: Map[CaptureRef, SrcPos] = refs.take(size).zip(locs).toMap + + def show(using Context) = + s"[${toMap.map((ref, loc) => i"$ref -> $loc").toList}]" + end ConsumedSet + + /** A fixed consumed set consisting of the given references `refs` and + * associated source positions `locs` + */ + class ConstConsumedSet(val refs: Array[CaptureRef], val locs: Array[SrcPos]) extends ConsumedSet: + def size = refs.size + + /** A mutable consumed set, which is initially empty */ + class MutConsumedSet extends ConsumedSet: + var refs: Array[CaptureRef] = new Array(4) + var locs: Array[SrcPos] = new Array(4) + var size = 0 + var peaks: Refs = emptyRefs + + private def double[T <: AnyRef : ClassTag](xs: Array[T]): Array[T] = + val xs1 = new Array[T](xs.length * 2) + xs.copyToArray(xs1) + xs1 + + private def ensureCapacity(added: Int): Unit = + if size + added > refs.length then + refs = double(refs) + locs = double(locs) + + /** If `ref` is in the set, its associated source position, otherwise `null` */ + def get(ref: CaptureRef): SrcPos | Null = + var i = 0 + while i < size && (refs(i) ne ref) do i += 1 + if i < size then locs(i) else null + + def clashing(ref: CaptureRef)(using Context): SrcPos | Null = + val refPeaks = ref.peaks + if !peaks.sharedWith(refPeaks).isEmpty then + var i = 0 + while i < size && refs(i).peaks.sharedWith(refPeaks).isEmpty do + i += 1 + assert(i < size) + locs(i) + else null + + /** If `ref` is not yet in the set, add it with given source position */ + def put(ref: CaptureRef, loc: SrcPos)(using Context): Unit = + if get(ref) == null then + ensureCapacity(1) + refs(size) = ref + locs(size) = loc + size += 1 + peaks = peaks ++ ref.peaks + + /** Add all references with their associated positions from `that` which + * are not yet in the set. + */ + def ++= (that: ConsumedSet)(using Context): Unit = + for i <- 0 until that.size do put(that.refs(i), that.locs(i)) + + /** Run `op` and return any new references it created in a separate `ConsumedSet`. + * The current mutable set is reset to its state before `op` was run. + */ + def segment(op: => Unit): ConsumedSet = + val start = size + val savedPeaks = peaks + try + op + if size == start then EmptyConsumedSet + else ConstConsumedSet(refs.slice(start, size), locs.slice(start, size)) + finally + size = start + peaks = savedPeaks + end MutConsumedSet + + val EmptyConsumedSet = ConstConsumedSet(Array(), Array()) + + case class PeaksPair(actual: Refs, hidden: Refs) + + case class DefInfo(tree: ValOrDefDef, symbol: Symbol, hidden: Refs, hiddenPeaks: Refs) + + extension (refs: Refs) + + /** The footprint of a set of references `refs` the smallest set `F` such that + * 1. if includeMax is false then no maximal capability is in `F` + * 2. all capabilities in `refs` satisfying (1) are in `F` + * 3. if `f in F` then the footprint of `f`'s info is also in `F`. + */ + private def footprint(includeMax: Boolean = false)(using Context): Refs = + def retain(ref: CaptureRef) = includeMax || !ref.isRootCapability + def recur(elems: Refs, newElems: List[CaptureRef]): Refs = newElems match + case newElem :: newElems1 => + val superElems = newElem.captureSetOfInfo.elems.filter: superElem => + retain(superElem) && !elems.contains(superElem) + recur(elems ++ superElems, newElems1 ++ superElems.toList) + case Nil => elems + val elems: Refs = refs.filter(retain) + recur(elems, elems.toList) + + private def peaks(using Context): Refs = + def recur(seen: Refs, acc: Refs, newElems: List[CaptureRef]): Refs = trace(i"peaks $acc, $newElems = "): + newElems match + case newElem :: newElems1 => + if seen.contains(newElem) then + recur(seen, acc, newElems1) + else newElem.stripReadOnly match + case root.Fresh(hidden) => + if hidden.deps.isEmpty then recur(seen + newElem, acc + newElem, newElems1) + else + val superCaps = + if newElem.isReadOnly then hidden.superCaps.map(_.readOnly) + else hidden.superCaps + recur(seen + newElem, acc, superCaps ++ newElems) + case _ => + if newElem.isRootCapability + //|| newElem.isInstanceOf[TypeRef | TypeParamRef] + then recur(seen + newElem, acc, newElems1) + else recur(seen + newElem, acc, newElem.captureSetOfInfo.elems.toList ++ newElems1) + case Nil => acc + recur(emptyRefs, emptyRefs, refs.toList) + + /** The shared peaks between `refs` and `other` */ + private def sharedWith(other: Refs)(using Context): Refs = + def common(refs1: Refs, refs2: Refs) = + refs1.filter: ref => + !ref.isReadOnly && refs2.exists(_.stripReadOnly eq ref) + common(refs, other) ++ common(other, refs) + + /** The overlap of two footprint sets F1 and F2. This contains all exclusive references `r` + * such that one of the following is true: + * 1. + * - one of the sets contains `r` + * - the other contains a capability `s` or `s.rd` where `s` _covers_ `r` + * 2. + * - one of the sets contains `r.rd` + * - the other contains a capability `s` where `s` _covers_ `r` + * + * A capability `s` covers `r` if `r` can be seen as a path extension of `s`. E.g. + * if `s = x.a` and `r = x.a.b.c` then `s` covers `a`. + */ + private def overlapWith(other: Refs)(using Context): Refs = + val refs1 = refs + val refs2 = other + + /** Exclusive capabilities in refs1 that are covered by exclusive or + * stripped read-only capabilties in refs2 + * + stripped read-only capabilities in refs1 that are covered by an + * exclusive capability in refs2. + */ + def common(refs1: Refs, refs2: Refs) = + refs1.filter: ref => + ref.isExclusive && refs2.exists(_.stripReadOnly.covers(ref)) + ++ + refs1 + .filter: + case ReadOnlyCapability(ref @ TermRef(prefix: CaptureRef, _)) => + // We can get away testing only references with at least one field selection + // here since stripped readOnly references that equal a reference in refs2 + // are added by the first clause of the symmetric call to common. + !ref.isCap && refs2.exists(_.covers(prefix)) + case _ => + false + .map(_.stripReadOnly) + + common(refs, other) ++ common(other, refs) + end overlapWith + + /** The non-maximal elements hidden directly or indirectly by a maximal + * capability in `refs`. E g. if `R = {x, >}` then + * its hidden set is `{y, z}`. + */ + private def hiddenSet(using Context): Refs = + val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet + + def hiddenByElem(elem: CaptureRef): Refs = elem match + case root.Fresh(hcs) => hcs.elems ++ recur(hcs.elems) + case ReadOnlyCapability(ref1) => hiddenByElem(ref1).map(_.readOnly) + case _ => emptyRefs + + def recur(refs: Refs): Refs = + (emptyRefs /: refs): (elems, elem) => + if seen.add(elem) then elems ++ hiddenByElem(elem) else elems + + recur(refs) + end hiddenSet + + /** Subtract all elements that are covered by some element in `others` from this set. */ + private def deduct(others: Refs)(using Context): Refs = + refs.filter: ref => + !others.exists(_.covers(ref)) + + /** Deduct `sym` and `sym*` from `refs` */ + private def deductSymRefs(sym: Symbol)(using Context): Refs = + val ref = sym.termRef + if ref.isTrackableRef then refs.deduct(SimpleIdentitySet(ref, ref.reach)) + else refs + + end extension + + extension (ref: CaptureRef) + def peaks(using Context): Refs = SimpleIdentitySet(ref).peaks + +class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: + import checker.* + import SepCheck.* + + /** The set of capabilities that are hidden by a polymorphic result type + * of some previous definition. + */ + private var defsShadow: Refs = emptyRefs + + /** The previous val or def definitions encountered during separation checking + * in reverse order. These all enclose and precede the current traversal node. + */ + private var previousDefs: List[DefInfo] = Nil + + /** The set of references that were consumed so far in the current method */ + private var consumed: MutConsumedSet = MutConsumedSet() + + /** Infos aboput Labeled expressions enclosing the current traversal point. + * For each labeled expression, it's label name, and a list buffer containing + * all consumed sets of return expressions referring to that label. + */ + private var openLabeled: List[(Name, mutable.ListBuffer[ConsumedSet])] = Nil + + /** The deep capture set of an argument or prefix widened to the formal parameter, if + * the latter contains a cap. + */ + private def formalCaptures(arg: Tree)(using Context): Refs = + arg.formalType.orElse(arg.nuType).deepCaptureSet.elems + + /** The deep capture set if the type of `tree` */ + private def captures(tree: Tree)(using Context): Refs = + tree.nuType.deepCaptureSet.elems + + // ---- Error reporting TODO Once these are stabilized, move to messages -----" + + + def sharedPeaksStr(shared: Refs)(using Context): String = + shared.nth(0) match + case fresh @ root.Fresh(hidden) => + if hidden.owner.exists then i"$fresh of ${hidden.owner}" else i"$fresh" + case other => + i"$other" + + def overlapStr(hiddenSet: Refs, clashSet: Refs)(using Context): String = + val hiddenFootprint = hiddenSet.footprint() + val clashFootprint = clashSet.footprint() + // The overlap of footprints, or, of this empty the set of shared peaks. + // We prefer footprint overlap since it tends to be more informative. + val overlap = hiddenFootprint.overlapWith(clashFootprint) + if !overlap.isEmpty then i"${CaptureSet(overlap)}" + else + val sharedPeaks = hiddenSet.footprint(includeMax = true).sharedWith: + clashSet.footprint(includeMax = true) + assert(!sharedPeaks.isEmpty, i"no overlap for $hiddenSet vs $clashSet") + sharedPeaksStr(sharedPeaks) + + /** Report a separation failure in an application `fn(args)` + * @param fn the function + * @param parts the function prefix followed by the flattened argument list + * @param polyArg the clashing argument to a polymorphic formal + * @param clashing the argument, function prefix, or entire function application result with + * which it clashes, + * + */ + def sepApplyError(fn: Tree, parts: List[Tree], polyArg: Tree, clashing: Tree)(using Context): Unit = + val polyArgIdx = parts.indexOf(polyArg).ensuring(_ >= 0) - 1 + val clashIdx = parts.indexOf(clashing) // -1 means entire function application + def paramName(mt: Type, idx: Int): Option[Name] = mt match + case mt @ MethodType(pnames) => + if idx < pnames.length then Some(pnames(idx)) else paramName(mt.resType, idx - pnames.length) + case mt: PolyType => paramName(mt.resType, idx) + case _ => None + def formalName = paramName(fn.nuType.widen, polyArgIdx) match + case Some(pname) => i"$pname " + case _ => "" + def qualifier = methPart(fn) match + case Select(qual, _) => qual + case _ => EmptyTree + def isShowableMethod = fn.symbol.exists && !defn.isFunctionSymbol(fn.symbol.maybeOwner) + def funType = + if fn.symbol.exists && !qualifier.isEmpty then qualifier.nuType else fn.nuType + def funStr = + if isShowableMethod then i"${fn.symbol}: ${fn.symbol.info}" + else i"a function of type ${funType.widen}" + def clashArgStr = clashIdx match + case -1 => "function result" + case 0 => "function prefix" + case 1 => "first argument " + case 2 => "second argument" + case 3 => "third argument " + case n => s"${n}th argument " + def clashTypeStr = + if clashIdx == 0 && !isShowableMethod then "" // we already mentioned the type in `funStr` + else i" with type ${clashing.nuType}" + val hiddenSet = formalCaptures(polyArg).hiddenSet + val clashSet = captures(clashing) + report.error( + em"""Separation failure: argument of type ${polyArg.nuType} + |to $funStr + |corresponds to capture-polymorphic formal parameter ${formalName}of type ${polyArg.formalType} + |and hides capabilities ${CaptureSet(hiddenSet)}. + |Some of these overlap with the captures of the ${clashArgStr.trim}$clashTypeStr. + | + | Hidden set of current argument : ${CaptureSet(hiddenSet)} + | Hidden footprint of current argument : ${CaptureSet(hiddenSet.footprint())} + | Capture set of $clashArgStr : ${CaptureSet(clashSet)} + | Footprint set of $clashArgStr : ${CaptureSet(clashSet.footprint())} + | The two sets overlap at : ${overlapStr(hiddenSet, clashSet)}""", + polyArg.srcPos) + + /** Report a use/definition failure, where a previously hidden capability is + * used again. + * @param tree the tree where the capability is used + * @param clashing the tree where the capability is previously hidden, + * or emptyTree if none exists + * @param used the uses of `tree` + * @param hidden the hidden set of the clashing def, + * or the global hidden set if no clashing def exists + */ + def sepUseError(tree: Tree, clashingDef: ValOrDefDef | Null, used: Refs, hidden: Refs)(using Context): Unit = + if clashingDef != null then + def resultStr = if clashingDef.isInstanceOf[DefDef] then " result" else "" + report.error( + em"""Separation failure: Illegal access to ${overlapStr(hidden, used)} which is hidden by the previous definition + |of ${clashingDef.symbol} with$resultStr type ${clashingDef.tpt.nuType}. + |This type hides capabilities ${CaptureSet(hidden)}""", + tree.srcPos) + else + report.error( + em"""Separation failure: illegal access to ${overlapStr(hidden, used)} which is hidden by some previous definitions + |No clashing definitions were found. This might point to an internal error.""", + tree.srcPos) + + /** Report a failure where a previously consumed capability is used again, + * @param ref the capability that is used after being consumed + * @param loc the position where the capability was consumed + * @param pos the position where the capability was used again + */ + def consumeError(ref: CaptureRef, loc: SrcPos, pos: SrcPos)(using Context): Unit = + report.error( + em"""Separation failure: Illegal access to $ref, which was passed to a + |@consume parameter or was used as a prefix to a @consume method on line ${loc.line + 1} + |and therefore is no longer available.""", + pos) + + /** Report a failure where a capability is consumed in a loop. + * @param ref the capability + * @param loc the position where the capability was consumed + */ + def consumeInLoopError(ref: CaptureRef, pos: SrcPos)(using Context): Unit = + report.error( + em"""Separation failure: $ref appears in a loop, therefore it cannot + |be passed to a @consume parameter or be used as a prefix of a @consume method call.""", + pos) + + // ------------ Checks ----------------------------------------------------- + + /** Check separation between different arguments and between function + * prefix and arguments. A capability cannot be hidden by one of these arguments + * and also be either explicitly referenced or hidden by the prefix or another + * argument. "Hidden" means: the capability is in the deep capture set of the + * argument and appears in the hidden set of the corresponding (capture-polymorphic) + * formal parameter. Howeber, we do allow explicit references to a hidden + * capability in later arguments, if the corresponding formal parameter mentions + * the parameter where the capability was hidden. For instance in + * + * def seq(x: () => Unit; y ->{cap, x} Unit): Unit + * def f: () ->{io} Unit + * + * we do allow `seq(f, f)` even though `{f, io}` is in the hidden set of the + * first parameter `x`, since the second parameter explicitly mentions `x` in + * its capture set. + * + * Also check separation via checkType within individual arguments widened to their + * formal paramater types. + * + * @param fn the applied function + * @param args the flattened argument lists + * @param app the entire application tree + * @param deps cross argument dependencies: maps argument trees to + * those other arguments that where mentioned by coorresponding + * formal parameters. + */ + private def checkApply(fn: Tree, args: List[Tree], app: Tree, deps: collection.Map[Tree, List[Tree]])(using Context): Unit = + val (qual, fnCaptures) = methPart(fn) match + case Select(qual, _) => (qual, qual.nuType.captureSet) + case _ => (fn, CaptureSet.empty) + var currentPeaks = PeaksPair(fnCaptures.elems.peaks, emptyRefs) + val partsWithPeaks = mutable.ListBuffer[(Tree, PeaksPair)]() += (qual -> currentPeaks) + + capt.println( + i"""check separate $fn($args), fnCaptures = $fnCaptures, + | formalCaptures = ${args.map(arg => CaptureSet(formalCaptures(arg)))}, + | actualCaptures = ${args.map(arg => CaptureSet(captures(arg)))}, + | deps = ${deps.toList}""") + val parts = qual :: args + + for arg <- args do + val argPeaks = PeaksPair( + captures(arg).peaks, + if arg.needsSepCheck then formalCaptures(arg).hiddenSet.peaks else emptyRefs) + val argDeps = deps(arg) + + def clashingPart(argPeaks: Refs, selector: PeaksPair => Refs): Tree = + partsWithPeaks.find: (prev, prevPeaks) => + !argDeps.contains(prev) + && !selector(prevPeaks).sharedWith(argPeaks).isEmpty + match + case Some(prev, _) => prev + case None => EmptyTree + + // 1. test argPeaks.actual against previously captured hidden sets + if !argPeaks.actual.sharedWith(currentPeaks.hidden).isEmpty then + val clashing = clashingPart(argPeaks.actual, _.hidden) + if !clashing.isEmpty then sepApplyError(fn, parts, clashing, arg) + else assert(!argDeps.isEmpty) + + if arg.needsSepCheck then + //println(i"testing $arg, formal = ${arg.formalType}, peaks = ${argPeaks.actual}/${argPeaks.hidden} against ${currentPeaks.actual}") + checkType(arg.formalType, arg.srcPos, TypeRole.Argument(arg)) + // 2. test argPeaks.hidden against previously captured actuals + if !argPeaks.hidden.sharedWith(currentPeaks.actual).isEmpty then + val clashing = clashingPart(argPeaks.hidden, _.actual) + if !clashing.isEmpty then + if !clashing.needsSepCheck then + // if clashing needs a separation check then we already got an erro + // in (1) at position of clashing. No need to report it twice. + //println(i"CLASH $arg / ${argPeaks.formal} vs $clashing / ${peaksOfTree(clashing).actual} / ${captures(clashing).peaks}") + sepApplyError(fn, parts, arg, clashing) + else assert(!argDeps.isEmpty) + + partsWithPeaks += (arg -> argPeaks) + currentPeaks = PeaksPair( + currentPeaks.actual ++ argPeaks.actual, + currentPeaks.hidden ++ argPeaks.hidden) + end for + + def collectRefs(args: List[Type], res: Type) = + args.foldLeft(argCaptures(res)): (refs, arg) => + refs ++ arg.deepCaptureSet.elems + + /** The deep capture sets of all parameters of this type (if it is a function type) */ + def argCaptures(tpe: Type): Refs = tpe match + case defn.FunctionOf(args, resultType, isContextual) => + collectRefs(args, resultType) + case defn.RefinedFunctionOf(mt) => + collectRefs(mt.paramInfos, mt.resType) + case CapturingType(parent, _) => + argCaptures(parent) + case _ => + emptyRefs + + if !deps(app).isEmpty then + lazy val appPeaks = argCaptures(app.nuType).peaks + lazy val partPeaks = partsWithPeaks.toMap + for arg <- deps(app) do + if arg.needsSepCheck && !partPeaks(arg).hidden.sharedWith(appPeaks).isEmpty then + sepApplyError(fn, parts, arg, app) + end checkApply + + /** 1. Check that the capabilities used at `tree` don't overlap with + * capabilities hidden by a previous definition. + * 2. Also check that none of the used capabilities was consumed before. + */ + def checkUse(tree: Tree)(using Context): Unit = + val used = tree.markedFree.elems + if !used.isEmpty then + capt.println(i"check use $tree: $used") + val usedPeaks = used.peaks + val overlap = defsShadow.peaks.sharedWith(usedPeaks) + if !defsShadow.peaks.sharedWith(usedPeaks).isEmpty then + val sym = tree.symbol + + def findClashing(prevDefs: List[DefInfo]): Option[DefInfo] = prevDefs match + case prevDef :: prevDefs1 => + if prevDef.symbol == sym then Some(prevDef) + else if !prevDef.hiddenPeaks.sharedWith(usedPeaks).isEmpty then Some(prevDef) + else findClashing(prevDefs1) + case Nil => + None + + findClashing(previousDefs) match + case Some(clashing) => + if clashing.symbol != sym then + sepUseError(tree, clashing.tree, used, clashing.hidden) + case None => + sepUseError(tree, null, used, defsShadow) + + for ref <- used do + val pos = consumed.clashing(ref) + if pos != null then consumeError(ref, pos, tree.srcPos) + end checkUse + + /** If `tp` denotes some version of a singleton capture ref `x.type` the set `{x, x*}` + * otherwise the empty set. + */ + def explicitRefs(tp: Type)(using Context): Refs = tp match + case tp: (TermRef | ThisType) if tp.isTrackableRef => SimpleIdentitySet(tp, tp.reach) + case AnnotatedType(parent, _) => explicitRefs(parent) + case AndType(tp1, tp2) => explicitRefs(tp1) ++ explicitRefs(tp2) + case OrType(tp1, tp2) => explicitRefs(tp1) ** explicitRefs(tp2) + case _ => emptyRefs + + /** Check validity of consumed references `refsToCheck`. The references are consumed + * because they are hidden in a Fresh result type or they are referred + * to in an argument to a @consume parameter or in a prefix of a @consume method -- + * which one applies is determined by the role parameter. + * + * This entails the following checks: + * - The reference must be defined in the same as method or class as + * the access. + * - If the reference is to a term parameter, that parameter must be + * marked as @consume as well. + * - If the reference is to a this type of the enclosing class, the + * access must be in a @consume method. + * + * References that extend SharedCapability are excluded from checking. + * As a side effect, add all checked references with the given position `pos` + * to the global `consumed` map. + * + * @param refsToCheck the referencves to check + * @param tpe the type containing those references + * @param role the role in which the type apears + * @param descr a textual description of the type and its relationship with the checked reference + * @param pos position for error reporting + */ + def checkConsumedRefs(refsToCheck: Refs, tpe: Type, role: TypeRole, descr: => String, pos: SrcPos)(using Context) = + val badParams = mutable.ListBuffer[Symbol]() + def currentOwner = role.dclSym.orElse(ctx.owner) + for hiddenRef <- refsToCheck.deductSymRefs(role.dclSym).deduct(explicitRefs(tpe)) do + if !hiddenRef.derivesFromSharedCapability then + hiddenRef.pathRoot match + case ref: TermRef => + val refSym = ref.symbol + if currentOwner.enclosingMethodOrClass.isProperlyContainedIn(refSym.maybeOwner.enclosingMethodOrClass) then + report.error(em"""Separation failure: $descr non-local $refSym""", pos) + else if refSym.is(TermParam) + && !refSym.hasAnnotation(defn.ConsumeAnnot) + && currentOwner.isContainedIn(refSym.owner) + then + badParams += refSym + case ref: ThisType => + val encl = currentOwner.enclosingMethodOrClass + if encl.isProperlyContainedIn(ref.cls) + && !encl.is(Synthetic) + && !encl.hasAnnotation(defn.ConsumeAnnot) + then + report.error( + em"""Separation failure: $descr non-local this of class ${ref.cls}. + |The access must be in a @consume method to allow this.""", + pos) + case _ => + + if badParams.nonEmpty then + def paramsStr(params: List[Symbol]): String = (params: @unchecked) match + case p :: Nil => i"${p.name}" + case p :: p2 :: Nil => i"${p.name} and ${p2.name}" + case p :: ps => i"${p.name}, ${paramsStr(ps)}" + val (pluralS, singleS) = if badParams.tail.isEmpty then ("", "s") else ("s", "") + report.error( + em"""Separation failure: $descr parameter$pluralS ${paramsStr(badParams.toList)}. + |The parameter$pluralS need$singleS to be annotated with @consume to allow this.""", + pos) + + role match + case _: TypeRole.Argument | _: TypeRole.Qualifier => + for ref <- refsToCheck do + if !ref.derivesFromSharedCapability then + consumed.put(ref, pos) + case _ => + end checkConsumedRefs + + /** Check separation conditions of type `tpe` that appears in `role`. + * 1. Check that the parts of type `tpe` are mutually separated, as defined in + * `checkParts` below. + * 2. Check that validity of all references consumed by the type as defined in + * `checkLegalRefs` below + */ + def checkType(tpe: Type, pos: SrcPos, role: TypeRole)(using Context): Unit = + + /** Deduct some elements from `refs` according to the role of the checked type `tpe`: + * - If the the type apears as a (result-) type of a definition of `x`, deduct + * `x` and `x*`. + * - If the checked type (or, for arguments, the actual type of the argument) + * is morally a singleton type `y.type` deduct `y` and `y*` as well. + */ + extension (refs: Refs) def pruned = + val deductedType = role match + case TypeRole.Argument(arg) => arg.tpe + case _ => tpe + refs.deductSymRefs(role.dclSym).deduct(explicitRefs(deductedType)) + + def sepTypeError(parts: List[Type], genPart: Type, otherPart: Type): Unit = + val captured = genPart.deepCaptureSet.elems + val hiddenSet = captured.hiddenSet.pruned + val clashSet = otherPart.deepCaptureSet.elems + val deepClashSet = (clashSet.footprint() ++ clashSet.hiddenSet).pruned + report.error( + em"""Separation failure in ${role.description} $tpe. + |One part, $genPart, hides capabilities ${CaptureSet(hiddenSet)}. + |Another part, $otherPart, captures capabilities ${CaptureSet(deepClashSet)}. + |The two sets overlap at ${overlapStr(hiddenSet, deepClashSet)}.""", + pos) + + /** Check that the parts of type `tpe` are mutually separated. + * This means that references hidden in some part of the type may not + * be explicitly referenced or hidden in some other part. + */ + def checkParts(parts: List[Type]): Unit = + var currentPeaks = PeaksPair(emptyRefs, emptyRefs) + val partsWithPeaks = mutable.ListBuffer[(Type, PeaksPair)]() + + for part <- parts do + val captured = part.deepCaptureSet.elems.pruned + val hidden = captured.hiddenSet.pruned + val actual = captured ++ hidden + val partPeaks = PeaksPair(actual.peaks, hidden.peaks) + /* + println(i"""check parts $parts + |current = ${currentPeaks.actual}/${currentPeaks.hidden} + |new = $captured/${captured.hiddenSet.pruned} + |new = ${captured.peaks}/${captured.hiddenSet.pruned.peaks}""") + */ + + def clashingPart(argPeaks: Refs, selector: PeaksPair => Refs): Type = + partsWithPeaks.find: (prev, prevPeaks) => + !selector(prevPeaks).sharedWith(argPeaks).isEmpty + match + case Some(prev, _) => prev + case None => NoType + + if !partPeaks.actual.sharedWith(currentPeaks.hidden).isEmpty then + //println(i"CLASH ${partPeaks.actual} with ${currentPeaks.hidden}") + val clashing = clashingPart(partPeaks.actual, _.hidden) + //println(i"CLASH ${partPeaks.actual} with ${currentPeaks.hidden}") + if clashing.exists then sepTypeError(parts, clashing, part) + + if !partPeaks.hidden.sharedWith(currentPeaks.actual).isEmpty then + val clashing = clashingPart(partPeaks.hidden, _.actual) + if clashing.exists then sepTypeError(parts, part, clashing) + + partsWithPeaks += (part -> partPeaks) + currentPeaks = PeaksPair( + currentPeaks.actual ++ partPeaks.actual, + currentPeaks.hidden ++ partPeaks.hidden) + end checkParts + + /** A traverser that collects part lists to check for separation conditions. + * The accumulator of type `Captures` indicates what kind of captures were + * encountered in previous parts. + */ + object traverse extends TypeAccumulator[Captures]: + + /** A stack of part lists to check. We maintain this since immediately + * checking parts when traversing the type would check innermost to outermost. + * But we want to check outermost parts first since this prioritizes errors + * that are more obvious. + */ + var toCheck: List[List[Type]] = Nil + + private val seen = util.HashSet[Symbol]() + + def apply(c: Captures, t: Type) = + if variance < 0 then c + else + val t1 = t.dealias + t1 match + case t @ AppliedType(tycon, args) => + val c1 = foldOver(Captures.None, t) + if c1 == Captures.NeedsCheck then + toCheck = (tycon :: args) :: toCheck + c.add(c1) + case t @ CapturingType(parent, cs) => + val c1 = this(c, parent) + if cs.elems.exists(_.stripReadOnly.isFresh) then c1.add(Captures.Hidden) + else if !cs.elems.isEmpty then c1.add(Captures.Explicit) + else c1 + case t: TypeRef if t.symbol.isAbstractOrParamType => + if seen.contains(t.symbol) then c + else + seen += t.symbol + apply(apply(c, t.prefix), t.info.bounds.hi) + case t => + foldOver(c, t) + + /** If `tpe` appears as a (result-) type of a definition, treat its + * hidden set minus its explicitly declared footprint as consumed. + * If `tpe` appears as an argument to a @consume parameter, treat + * its footprint as consumed. + */ + def checkLegalRefs() = role match + case TypeRole.Result(sym, _) => + if !sym.isAnonymousFunction // we don't check return types of anonymous functions + && !sym.is(Case) // We don't check so far binders in patterns since they + // have inferred universal types. TODO come back to this; + // either infer more precise types for such binders or + // "see through them" when we look at hidden sets. + then + val refs = tpe.deepCaptureSet.elems + val toCheck = refs.hiddenSet.footprint().deduct(refs.footprint()) + checkConsumedRefs(toCheck, tpe, role, i"${role.description} $tpe hides", pos) + case TypeRole.Argument(arg) => + if tpe.hasAnnotation(defn.ConsumeAnnot) then + val capts = captures(arg).footprint() + checkConsumedRefs(capts, tpe, role, i"argument to @consume parameter with type ${arg.nuType} refers to", pos) + case _ => + + if !tpe.hasAnnotation(defn.UntrackedCapturesAnnot) then + traverse(Captures.None, tpe) + traverse.toCheck.foreach(checkParts) + checkLegalRefs() + end checkType + + /** Check the (result-) type of a definition of symbol `sym` */ + def checkType(tpt: Tree, sym: Symbol)(using Context): Unit = + checkType(tpt.nuType, tpt.srcPos, + TypeRole.Result(sym, inferred = tpt.isInstanceOf[InferredTypeTree])) + + /** The list of all individual method types making up some potentially + * curried method type. + */ + private def collectMethodTypes(tp: Type): List[TermLambda] = tp match + case tp: MethodType => tp :: collectMethodTypes(tp.resType) + case tp: PolyType => collectMethodTypes(tp.resType) + case _ => Nil + + /** The inter-parameter dependencies of the function reference `fn` applied + * to the argument lists `argss`. For instance, if `f` has type + * + * f(x: A, y: B^{cap, x}, z: C^{x, y}): D + * + * then the dependencies of an application `f(a, b, c)` of type C^{y} is the map + * + * [ b -> [a] + * , c -> [a, b] + * , f(a, b, c) -> [b]] + */ + private def dependencies(fn: Tree, argss: List[List[Tree]], app: Tree)(using Context): collection.Map[Tree, List[Tree]] = + def isFunApply(sym: Symbol) = + sym.name == nme.apply && defn.isFunctionClass(sym.owner) + val mtpe = + if fn.symbol.exists && !isFunApply(fn.symbol) then fn.symbol.info + else fn.nuType.widen + val mtps = collectMethodTypes(mtpe) + assert(mtps.hasSameLengthAs(argss), i"diff for $fn: ${fn.symbol} /// $mtps /// $argss") + val mtpsWithArgs = mtps.zip(argss) + val argMap = mtpsWithArgs.toMap + val deps = mutable.HashMap[Tree, List[Tree]]().withDefaultValue(Nil) + + def recordDeps(formal: Type, actual: Tree) = + for dep <- formal.captureSet.elems.toList do + val referred = dep.stripReach match + case dep: TermParamRef => + argMap(dep.binder)(dep.paramNum) :: Nil + case dep: ThisType if dep.cls == fn.symbol.owner => + val Select(qual, _) = fn: @unchecked // TODO can we use fn instead? + qual :: Nil + case _ => + Nil + deps(actual) ++= referred + + for (mt, args) <- mtpsWithArgs; (formal, arg) <- mt.paramInfos.zip(args) do + recordDeps(formal, arg) + recordDeps(mtpe.finalResultType, app) + capt.println(i"deps for $app = ${deps.toList}") + deps + + + /** Decompose an application into a function prefix and a list of argument lists. + * If some of the arguments need a separation check because they are capture polymorphic, + * perform a separation check with `checkApply` + */ + private def traverseApply(app: Tree)(using Context): Unit = + def recur(tree: Tree, argss: List[List[Tree]]): Unit = tree match + case Apply(fn, args) => recur(fn, args :: argss) + case TypeApply(fn, args) => recur(fn, argss) // skip type arguments + case _ => + if argss.nestedExists(_.needsSepCheck) then + checkApply(tree, argss.flatten, app, dependencies(tree, argss, app)) + recur(app, Nil) + + /** Is `tree` an application of `caps.unsafe.unsafeAssumeSeparate`? */ + def isUnsafeAssumeSeparate(tree: Tree)(using Context): Boolean = tree match + case tree: Apply => tree.symbol == defn.Caps_unsafeAssumeSeparate + case _ => false + + def pushDef(tree: ValOrDefDef, hiddenByDef: Refs)(using Context): Unit = + defsShadow ++= hiddenByDef + previousDefs = DefInfo(tree, tree.symbol, hiddenByDef, hiddenByDef.peaks) :: previousDefs + + /** Check (result-) type of `tree` for separation conditions using `checkType`. + * Excluded are parameters and definitions that have an =unsafeAssumeSeparate + * application as right hand sides. + * Hidden sets of checked definitions are added to `defsShadow`. + */ + def checkValOrDefDef(tree: ValOrDefDef)(using Context): Unit = + if !tree.symbol.isOneOf(TermParamOrAccessor) && !isUnsafeAssumeSeparate(tree.rhs) then + checkType(tree.tpt, tree.symbol) + capt.println(i"sep check def ${tree.symbol}: ${tree.tpt} with ${captures(tree.tpt).hiddenSet.footprint()}") + pushDef(tree, captures(tree.tpt).hiddenSet.deductSymRefs(tree.symbol)) + + def inSection[T](op: => T)(using Context): T = + val savedDefsShadow = defsShadow + val savedPrevionsDefs = previousDefs + try op + finally + previousDefs = savedPrevionsDefs + defsShadow = savedDefsShadow + + def traverseSection[T](tree: Tree)(using Context) = inSection(traverseChildren(tree)) + + /** Traverse `tree` and perform separation checks everywhere */ + def traverse(tree: Tree)(using Context): Unit = + if !isUnsafeAssumeSeparate(tree) then trace(i"checking separate $tree"): + checkUse(tree) + tree match + case tree @ Select(qual, _) if tree.symbol.is(Method) && tree.symbol.hasAnnotation(defn.ConsumeAnnot) => + traverseChildren(tree) + checkConsumedRefs( + captures(qual).footprint(), qual.nuType, + TypeRole.Qualifier(qual, tree.symbol), + i"call prefix of @consume ${tree.symbol} refers to", qual.srcPos) + case tree: GenericApply => + traverseChildren(tree) + tree.tpe match + case _: MethodOrPoly => + case _ => traverseApply(tree) + case _: Block | _: Template => + traverseSection(tree) + case tree: ValDef => + traverseChildren(tree) + checkValOrDefDef(tree) + case tree: DefDef => + inSection: + consumed.segment: + for params <- tree.paramss; case param: ValDef <- params do + pushDef(param, emptyRefs) + traverseChildren(tree) + checkValOrDefDef(tree) + case If(cond, thenp, elsep) => + traverse(cond) + val thenConsumed = consumed.segment(traverse(thenp)) + val elseConsumed = consumed.segment(traverse(elsep)) + consumed ++= thenConsumed + consumed ++= elseConsumed + case tree @ Labeled(bind, expr) => + val consumedBuf = mutable.ListBuffer[ConsumedSet]() + openLabeled = (bind.name, consumedBuf) :: openLabeled + traverse(expr) + for cs <- consumedBuf do consumed ++= cs + openLabeled = openLabeled.tail + case Return(expr, from) => + val retConsumed = consumed.segment(traverse(expr)) + from match + case Ident(name) => + for (lbl, consumedBuf) <- openLabeled do + if lbl == name then + consumedBuf += retConsumed + case _ => + case Match(sel, cases) => + // Matches without returns might still be kept after pattern matching to + // encode table switches. + traverse(sel) + val caseConsumed = for cas <- cases yield consumed.segment(traverse(cas)) + caseConsumed.foreach(consumed ++= _) + case tree: TypeDef if tree.symbol.isClass => + consumed.segment: + traverseChildren(tree) + case tree: WhileDo => + val loopConsumed = consumed.segment(traverseChildren(tree)) + if loopConsumed.size != 0 then + val (ref, pos) = loopConsumed.toMap.head + consumeInLoopError(ref, pos) + case _ => + traverseChildren(tree) +end SepCheck \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index a5e96f1f9ce2..6d52ad94613b 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -14,6 +14,7 @@ import transform.{PreRecheck, Recheck}, Recheck.* import CaptureSet.{IdentityCaptRefMap, IdempotentCaptRefMap} import Synthetics.isExcluded import util.SimpleIdentitySet +import util.chaining.* import reporting.Message import printing.{Printer, Texts}, Texts.{Text, Str} import collection.mutable @@ -85,7 +86,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** Drops `private` from the flags of `symd` provided it is * a parameter accessor that's not `constructorOnly` or `uncheckedCaptured` * and that contains at least one @retains in co- or in-variant position. - * The @retains mught be implicit for a type deriving from `Capability`. + * The @retains might be implicit for a type deriving from `Capability`. */ private def newFlagsFor(symd: SymDenotation)(using Context): FlagSet = @@ -132,7 +133,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 transformExplicitType(symd.info, sym, freshen = true) if Synthetics.needsTransform(symd) then Synthetics.transform(symd, mappedInfo) else if isPreCC(sym) then @@ -180,6 +181,75 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tp: MethodOrPoly => tp // don't box results of methods outside refinements case _ => recur(tp) + private trait SetupTypeMap extends FollowAliasesMap: + private var isTopLevel = true + + protected def innerApply(tp: Type): Type + + final def apply(tp: Type) = + val saved = isTopLevel + if variance < 0 then isTopLevel = false + try tp match + case defn.RefinedFunctionOf(rinfo: MethodType) => + val rinfo1 = apply(rinfo) + if rinfo1 ne rinfo then rinfo1.toFunctionType(alwaysDependent = true) + else tp + case _ => + innerApply(tp) + finally isTopLevel = saved + + /** Map parametric functions with results that have a capture set somewhere + * to dependent functions. + */ + protected def normalizeFunctions(tp: Type, original: Type, expandAlways: Boolean = false)(using Context): Type = + tp match + case AppliedType(tycon, args) + if defn.isNonRefinedFunction(tp) && isTopLevel => + // Expand if we have an applied type that underwent some addition of capture sets + val expand = expandAlways || original.match + case AppliedType(`tycon`, args0) => args0.last ne args.last + case _ => false + if expand then + depFun(args.init, args.last, + isContextual = defn.isContextFunctionClass(tycon.classSymbol)) + .showing(i"add function refinement $tp ($tycon, ${args.init}, ${args.last}) --> $result", capt) + else tp + case _ => tp + + /** 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) => + CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, parent.isBoxed) + case tp: RecType => + tp.parent match + case parent @ CapturingType(parent1, refs) => + CapturingType(tp.derivedRecType(parent1), refs, parent.isBoxed) + case _ => + tp // can return `tp` here since unlike RefinedTypes, RecTypes are never created + // by `mapInferred`. Hence if the underlying type admits capture variables + // a variable was already added, and the first case above would apply. + case AndType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => + assert(tp1.isBoxed == tp2.isBoxed) + CapturingType(AndType(parent1, parent2), refs1 ** refs2, tp1.isBoxed) + case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => + assert(tp1.isBoxed == tp2.isBoxed) + CapturingType(OrType(parent1, parent2, tp.isSoft), refs1 ++ refs2, tp1.isBoxed) + case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2) => + 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) && (tp.dealias eq tp) => + tp.derivedAppliedType(tycon, args.mapConserve(box)) + case tp: RealTypeBounds => + tp.derivedTypeBounds(tp.lo, box(tp.hi)) + case tp: LazyRef => + normalizeCaptures(tp.ref) + case _ => + tp + + end SetupTypeMap + /** Transform the type of an InferredTypeTree by performing the following transformation * steps everywhere in the type: * 1. Drop retains annotations @@ -197,7 +267,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * 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 with FollowAliasesMap: + def mapInferred(refine: Boolean): TypeMap = new TypeMap with SetupTypeMap: override def toString = "map inferred" /** Refine a possibly applied class type C where the class has tracked parameters @@ -225,53 +295,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => tp case _ => tp - private var isTopLevel = true - - private def mapNested(ts: List[Type]): List[Type] = - val saved = isTopLevel - isTopLevel = false - try ts.mapConserve(this) - finally isTopLevel = saved - - def apply(tp: Type) = + def innerApply(tp: Type) = val tp1 = tp match case AnnotatedType(parent, annot) if annot.symbol.isRetains => // Drop explicit retains annotations apply(parent) - case tp @ AppliedType(tycon, args) => - val tycon1 = this(tycon) - if defn.isNonRefinedFunction(tp) then - // Convert toplevel generic function types to dependent functions - if !defn.isFunctionSymbol(tp.typeSymbol) && (tp.dealias ne tp) then - // This type is a function after dealiasing, so we dealias and recurse. - // See #15925. - this(tp.dealias) - else - val args0 = args.init - var res0 = args.last - val args1 = mapNested(args0) - val res1 = this(res0) - if isTopLevel then - depFun(args1, res1, - isContextual = defn.isContextFunctionClass(tycon1.classSymbol)) - .showing(i"add function refinement $tp ($tycon1, $args1, $res1) (${tp.dealias}) --> $result", capt) - else if (tycon1 eq tycon) && (args1 eq args0) && (res1 eq res0) then - tp - else - tp.derivedAppliedType(tycon1, args1 :+ res1) - else - tp.derivedAppliedType(tycon1, args.mapConserve(arg => box(this(arg)))) - case defn.RefinedFunctionOf(rinfo: MethodType) => - val rinfo1 = apply(rinfo) - if rinfo1 ne rinfo then rinfo1.toFunctionType(alwaysDependent = true) - else tp - case Existential(_, unpacked) => - // drop the existential, the bound variables will be replaced by capture set variables - apply(unpacked) - case tp: MethodType => - tp.derivedLambdaType( - paramInfos = mapNested(tp.paramInfos), - resType = this(tp.resType)) case tp: TypeLambda => // Don't recurse into parameter bounds, just cleanup any stray retains annotations tp.derivedLambdaType( @@ -279,13 +307,14 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: resType = this(tp.resType)) case _ => mapFollowingAliases(tp) - addVar(addCaptureRefinements(normalizeCaptures(tp1)), ctx.owner) - end apply + addVar( + addCaptureRefinements(normalizeCaptures(normalizeFunctions(tp1, tp))), + ctx.owner) end mapInferred try val tp1 = mapInferred(refine = true)(tp) - val tp2 = Existential.mapCapInResults(_ => assert(false))(tp1) + val tp2 = root.toResultInResults(_ => assert(false))(tp1) if tp2 ne tp then capt.println(i"expanded inferred in ${ctx.owner}: $tp --> $tp1 --> $tp2") tp2 catch case ex: AssertionError => @@ -300,11 +329,18 @@ 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 + * 6. Perform normalizeCaptures */ - private def transformExplicitType(tp: Type, tptToCheck: Tree = EmptyTree)(using Context): Type = - val toCapturing = new DeepTypeMap with FollowAliasesMap: - override def toString = "expand aliases" + private def transformExplicitType(tp: Type, sym: Symbol, freshen: Boolean, tptToCheck: Tree = EmptyTree)(using Context): Type = + + def fail(msg: Message) = + if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos) + + object toCapturing extends DeepTypeMap, SetupTypeMap: + override def toString = "transformExplicitType" + + var keepFunAliases = true + var keptFunAliases = false /** Expand $throws aliases. This is hard-coded here since $throws aliases in stdlib * are defined with `?=>` rather than `?->`. @@ -332,19 +368,47 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else fntpe /** If C derives from Capability and we have a C^cs in source, we leave it as is - * instead of expanding it to C^{cap}^cs. We do this by stripping capability-generated + * instead of expanding it to C^{cap.rd}^cs. We do this by stripping capability-generated * universal capture sets from the parent of a CapturingType. */ def stripImpliedCaptureSet(tp: Type): Type = tp match case tp @ CapturingType(parent, refs) - if (refs eq defn.universalCSImpliedByCapability) && !tp.isBoxedCapturing => + if (refs eq CaptureSet.universalImpliedByCapability) && !tp.isBoxedCapturing => parent case _ => tp - def apply(t: Type) = + /** Check that types extending SharedCapability don't have a `cap` in their capture set. + * TODO This is not enough. + * We need to also track that we cannot get exclusive capabilities in paths + * where some prefix derives from SharedCapability. Also, can we just + * exclude `cap`, or do we have to extend this to all exclusive capabilties? + * The problem is that we know what is exclusive in general only after capture + * checking, not before. + */ + def checkSharedOK(tp: Type): tp.type = + tp match + case CapturingType(parent, refs) + if refs.isUniversal && parent.derivesFromSharedCapability => + fail(em"$tp extends SharedCapability, so it cannot capture `cap`") + case _ => + tp + + /** Map references to capability classes C to C^, + * normalize captures and map to dependent functions. + */ + def defaultApply(t: Type) = + if t.derivesFromCapability + && !t.isSingleton + && (!sym.isConstructor || (t ne tp.finalResultType)) + // Don't add ^ to result types of class constructors deriving from Capability + then CapturingType(t, CaptureSet.universalImpliedByCapability, boxed = false) + else normalizeCaptures(mapFollowingAliases(t)) + + def innerApply(t: Type) = t match case t @ CapturingType(parent, refs) => - t.derivedCapturingType(stripImpliedCaptureSet(this(parent)), refs) + checkSharedOK: + t.derivedCapturingType(stripImpliedCaptureSet(this(parent)), refs) case t @ AnnotatedType(parent, ann) => val parent1 = this(parent) if ann.symbol.isRetains then @@ -352,28 +416,48 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if !tptToCheck.isEmpty then checkWellformedLater(parent2, ann.tree, tptToCheck) try - CapturingType(parent2, ann.tree.toCaptureSet) + checkSharedOK: + CapturingType(parent2, ann.tree.toCaptureSet) catch case ex: IllegalCaptureRef => - report.error(em"Illegal capture reference: ${ex.getMessage.nn}", tptToCheck.srcPos) + if !tptToCheck.isEmpty then + 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) => this(expandThrowsAlias(res, exc, Nil)) + case t @ AppliedType(tycon, args) + if defn.isNonRefinedFunction(t) + && !defn.isFunctionSymbol(t.typeSymbol) && (t.dealias ne tp) => + if keepFunAliases then + // Hold off with dealising and expand in a second pass. + // This is necessary to bind existentialFresh instances to the right method binder. + keptFunAliases = true + mapOver(t) + else + // In the second pass, map the alias + apply(t.dealias) case t => - // 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(mapFollowingAliases(t)) + defaultApply(t) end toCapturing - def fail(msg: Message) = - if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos) + def transform(tp: Type): Type = + val tp1 = toCapturing(tp) + val tp2 = root.toResultInResults(fail, toCapturing.keepFunAliases)(tp1) + val snd = if toCapturing.keepFunAliases then "" else " 2nd time" + if tp2 ne tp then capt.println(i"expanded explicit$snd in ${ctx.owner}: $tp --> $tp1 --> $tp2") + tp2 - val tp1 = toCapturing(tp) - val tp2 = Existential.mapCapInResults(fail)(tp1) - if tp2 ne tp then capt.println(i"expanded explicit in ${ctx.owner}: $tp --> $tp1 --> $tp2") - tp2 + val tp1 = transform(tp) + val tp2 = + if toCapturing.keptFunAliases then + toCapturing.keepFunAliases = false + transform(tp1) + else tp1 + if freshen then root.capToFresh(tp2).tap(addOwnerAsHidden(_, sym)) + else tp2 end transformExplicitType /** Substitute parameter symbols in `from` to paramRefs in corresponding @@ -424,31 +508,49 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: extension (sym: Symbol) def nextInfo(using Context): Type = atPhase(thisPhase.next)(sym.info) + private def addOwnerAsHidden(tp: Type, owner: Symbol)(using Context): Unit = + val ref = owner.termRef + def add = new TypeTraverser: + var reach = false + def traverse(t: Type): Unit = t match + case root.Fresh(hidden) => + if reach then hidden.elems += ref.reach + else if ref.isTracked then hidden.elems += ref + case t @ CapturingType(_, _) if t.isBoxed && !reach => + reach = true + try traverseChildren(t) finally reach = false + case _ => + traverseChildren(t) + if ref.isTrackableRef then add.traverse(tp) + end addOwnerAsHidden + /** A traverser that adds knownTypes and updates symbol infos */ 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 of 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) + else transformExplicitType(tree.tpe, sym, freshen = !boxed, tptToCheck = tree) + if boxed then transformed = box(transformed) + if sym.is(Param) && (transformed ne tree.tpe) then + paramSigChange += tree + tree.setNuType( + if sym.hasAnnotation(defn.UncheckedCapturesAnnot) then makeUnchecked(transformed) + else transformed) /** 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, - boxed = - sym.is(Mutable, butNot = Method) - && !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. - ) + transformTT(tpt, sym, boxed = false) catch case ex: IllegalCaptureRef => capt.println(i"fail while transforming result type $tpt of $sym") throw ex @@ -490,9 +592,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(root.capToFresh(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 @@ -501,6 +605,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))) @@ -511,13 +618,13 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => traverseChildren(tree) postProcess(tree) - checkProperUse(tree) + checkProperUseOrConsume(tree) end traverse /** 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 @@ -544,8 +651,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 @@ -580,7 +687,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(root.freshToCap(psym.nextInfo)).asInstanceOf[mt.PInfo])), mt1 => integrateRT(mt.resType, psymss.tail, resType, psyms :: prevPsymss, mt1 :: prevLambdas) ) @@ -594,7 +701,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // If there's a change in the signature, update the info of `sym` if sym.exists && signatureChanges then val newInfo = - Existential.mapCapInResults(report.error(_, tree.srcPos)): + root.toResultInResults(report.error(_, tree.srcPos)): integrateRT(sym.info, sym.paramSymss, localReturnType, Nil, Nil) .showing(i"update info $sym: ${sym.info} = $result", capt) if newInfo ne sym.info then @@ -648,7 +755,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // Compute new parent types val ps1 = inContext(ctx.withOwner(cls)): - ps.mapConserve(transformExplicitType(_)) + ps.mapConserve(transformExplicitType(_, NoSymbol, freshen = false)) // Install new types and if it is a module class also update module object if (selfInfo1 ne selfInfo) || (ps1 ne ps) then @@ -665,16 +772,31 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => end postProcess - /** Check that @use annotations only appear on parameters and not on anonymous function parameters */ - def checkProperUse(tree: Tree)(using Context): Unit = tree match + /** Check that @use and @consume annotations only appear on parameters and not on + * anonymous function parameters + */ + def checkProperUseOrConsume(tree: Tree)(using Context): Unit = tree match case tree: MemberDef => - def useAllowed(sym: Symbol) = - (sym.is(Param) || sym.is(ParamAccessor)) && !sym.owner.isAnonymousFunction + val sym = tree.symbol + def isMethodParam = (sym.is(Param) || sym.is(ParamAccessor)) + && !sym.owner.isAnonymousFunction for ann <- tree.symbol.annotations do - if ann.symbol == defn.UseAnnot && !useAllowed(tree.symbol) then - report.error(i"Only parameters of methods can have @use annotations", tree.srcPos) + val annotCls = ann.symbol + if annotCls == defn.ConsumeAnnot then + if !(isMethodParam && sym.isTerm) + && !(sym.is(Method) && sym.owner.isClass) + then + report.error( + em"""@consume cannot be used here. Only memeber methods and their term parameters + |can have @consume annotations.""", + tree.srcPos) + else if annotCls == defn.UseAnnot then + if !isMethodParam then + report.error( + em"@use cannot be used here. Only method parameters can have @use annotations.", + tree.srcPos) case _ => - end checkProperUse + end checkProperUseOrConsume end setupTraverser // --------------- Adding capture set variables ---------------------------------- @@ -735,7 +857,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) @@ -798,37 +920,14 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if variance > 0 then t1 else decorate(t1, Function.const(CaptureSet.Fluid)) - /** 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) => - CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, parent.isBoxed) - case tp: RecType => - tp.parent match - case parent @ CapturingType(parent1, refs) => - CapturingType(tp.derivedRecType(parent1), refs, parent.isBoxed) - case _ => - tp // can return `tp` here since unlike RefinedTypes, RecTypes are never created - // by `mapInferred`. Hence if the underlying type admits capture variables - // a variable was already added, and the first case above would apply. - case AndType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => - assert(tp1.isBoxed == tp2.isBoxed) - CapturingType(AndType(parent1, parent2), refs1 ** refs2, tp1.isBoxed) - case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => - assert(tp1.isBoxed == tp2.isBoxed) - CapturingType(OrType(parent1, parent2, tp.isSoft), refs1 ++ refs2, tp1.isBoxed) - case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2) => - 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) && (tp.dealias eq tp) => - tp.derivedAppliedType(tycon, args.mapConserve(box)) - case tp: RealTypeBounds => - tp.derivedTypeBounds(tp.lo, box(tp.hi)) - case tp: LazyRef => - normalizeCaptures(tp.ref) - case _ => - tp + /** 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.containsRootCapability then t.derivedCapturingType(parent1, CaptureSet.Fluid) + else t + case _ => mapFollowingAliases(t) /** Run setup on a compilation unit with given `tree`. * @param recheckDef the function to run for completing a val or def @@ -856,7 +955,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: var retained = ann.retainedElems.toArray for i <- 0 until retained.length do val refTree = retained(i) - for ref <- refTree.toCaptureRefs do + val refs = + try refTree.toCaptureRefs + catch case ex: IllegalCaptureRef => + report.error(em"Illegal capture reference: ${ex.getMessage.nn}", refTree.srcPos) + Nil + for ref <- refs do def pos = if refTree.span.exists then refTree.srcPos else if ann.span.exists then ann.srcPos @@ -877,6 +981,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: for j <- 0 until retained.length if j != i r <- retained(j).toCaptureRefs + if !r.isRootCapability yield r val remaining = CaptureSet(others*) check(remaining, remaining) diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 1372ebafe82f..cdc3afff8a2d 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 => @@ -132,23 +132,26 @@ object Synthetics: val (pt: PolyType) = info: @unchecked val (mt: MethodType) = pt.resType: @unchecked val (enclThis: ThisType) = owner.thisType: @unchecked + val paramCaptures = CaptureSet(enclThis, root.cap) pt.derivedLambdaType(resType = MethodType(mt.paramNames)( - mt1 => mt.paramInfos.map(_.capturing(CaptureSet.universal)), + mt1 => mt.paramInfos.map(_.capturing(paramCaptures)), mt1 => CapturingType(mt.resType, CaptureSet(enclThis, mt1.paramRefs.head)))) def transformCurriedTupledCaptures(info: Type, owner: Symbol) = val (et: ExprType) = info: @unchecked val (enclThis: ThisType) = owner.thisType: @unchecked - def mapFinalResult(tp: Type, f: Type => Type): Type = - val defn.FunctionNOf(args, res, isContextual) = tp: @unchecked - if defn.isFunctionNType(res) then - defn.FunctionNOf(args, mapFinalResult(res, f), isContextual) - else + def mapFinalResult(tp: Type, f: Type => Type): Type = tp match + case FunctionOrMethod(args, res) => + tp.derivedFunctionOrMethod(args, mapFinalResult(res, f)) + case _ => f(tp) ExprType(mapFinalResult(et.resType, CapturingType(_, CaptureSet(enclThis)))) def transformCompareCaptures = - MethodType(defn.ObjectType.capturing(CaptureSet.universal) :: Nil, defn.BooleanType) + val (enclThis: ThisType) = symd.owner.thisType: @unchecked + MethodType( + defn.ObjectType.capturing(CaptureSet(root.cap, enclThis)) :: Nil, + defn.BooleanType) symd.copySymDenotation(info = symd.name match case DefaultGetterName(nme.copy, n) => diff --git a/compiler/src/dotty/tools/dotc/cc/root.scala b/compiler/src/dotty/tools/dotc/cc/root.scala new file mode 100644 index 000000000000..da4ee330ca3c --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/root.scala @@ -0,0 +1,333 @@ +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 CaptureSet.IdempotentCaptRefMap +import reporting.Message +import util.{SimpleIdentitySet, EqHashMap} +import util.Spans.NoSpan +import annotation.internal.sharable + +/** A module defining three kinds of root capabilities + * - `cap` of kind `Global`: This is the global root capability. Among others it is + * used in the types of formal parameters, in type bounds, and in self types. + * `cap` does not subsume other capabilities, except in arguments of + * `withCapAsRoot` calls. + * - Instances of Fresh(hidden), of kind Fresh. These do subsume other capabilties in scope. + * They track with hidden sets which other capabilities were subsumed. + * Hidden sets are inspected by separation checking. + * - Instances of Result(binder), of kind Result. These are existentials associated with + * the result types of dependent methods. They don't subsume other capabilties. + * + * Representation: + * + * - `cap` is just the TermRef `scala.caps.cap` defined in the `caps` module + * - `Fresh` and `Result` instances are annotated types of `scala.caps.cap` + * with a special `root.Annot` annotation. The symbol of the annotation is + * `annotation.internal.rootCapability`. The annotation carries a kind, which provides + * a hidden set for Fresh instances and a binder method type for Result instances. + * + * Setup: + * + * In the setup phase, `cap` instances in the result of a dependent function type + * or method type such as `(x: T): C^{cap}` are converted to `Result(binder)` instances, + * where `binder` refers to the method type. Most other cap instances are mapped to + * Fresh instances instead. For example the `cap` in the result of `T => C^{cap}` + * is mapped to a Fresh instance. + * + * If one needs to use a dependent function type yet one still want to map `cap` to + * a fresh instance instead an existential root, one can achieve that by the use + * of a type alias. For instance, the following type creates an existential for `^`: + * + * (x: A) => (C^{x}, D^) + * + * By contrast, this variant creates a fresh instance instead: + * + * type F[X] = (x: A) => (C^{x}, X) + * F[D^] + * + * The trick is that the argument D^ is mapped to D^{fresh} before the `F` alias + * is expanded. + */ +object root: + + enum Kind: + case Result(binder: MethodType) + case Fresh(hidden: CaptureSet.HiddenSet) + case Global + + override def equals(other: Any): Boolean = + (this eq other.asInstanceOf[AnyRef]) || this.match + case Kind.Result(b1) => other match + case Kind.Result(b2) => b1 eq b2 + case _ => false + case Kind.Fresh(h1) => other match + case Kind.Fresh(h2) => h1 eq h2 + case _ => false + case Kind.Global => false + end Kind + + @sharable private var rootId = 0 + + /** The annotation of a root instance */ + case class Annot(kind: Kind) extends Annotation: + + /** id printed under -uniqid, for debugging */ + val id = + rootId += 1 + rootId + + override def symbol(using Context) = defn.RootCapabilityAnnot + override def tree(using Context) = New(symbol.typeRef, Nil) + override def derivedAnnotation(tree: Tree)(using Context): Annotation = this + + private var myOriginalKind = kind + def originalBinder: MethodType = myOriginalKind.asInstanceOf[Kind.Result].binder + + def derivedAnnotation(binder: MethodType)(using Context): Annotation = kind match + case Kind.Result(b) if b ne binder => + val ann = Annot(Kind.Result(binder)) + ann.myOriginalKind = myOriginalKind + ann + case _ => + this + + override def hash: Int = kind.hashCode + override def eql(that: Annotation) = that match + case Annot(kind) => this.kind eq kind + case _ => false + + /** Special treatment of `SubstBindingMaps` which can change the binder of a + * Result instances + */ + override def mapWith(tm: TypeMap)(using Context) = kind match + case Kind.Result(binder) => tm match + case tm: Substituters.SubstBindingMap[MethodType] @unchecked if tm.from eq binder => + derivedAnnotation(tm.to) + case _ => this + case _ => this + end Annot + + def cap(using Context): TermRef = defn.captureRoot.termRef + + /** The type of fresh references */ + type Fresh = AnnotatedType + + object Fresh: + /** Constructor and extractor methods for "fresh" capabilities */ + private def make(owner: Symbol)(using Context): CaptureRef = + if ccConfig.useSepChecks then + val hiddenSet = CaptureSet.HiddenSet(owner) + val res = AnnotatedType(cap, Annot(Kind.Fresh(hiddenSet))) + hiddenSet.owningCap = res + //assert(hiddenSet.id != 3) + res + else + cap + + def withOwner(owner: Symbol)(using Context): CaptureRef = make(owner) + def apply()(using Context): CaptureRef = make(NoSymbol) + + def unapply(tp: AnnotatedType): Option[CaptureSet.HiddenSet] = tp.annot match + case Annot(Kind.Fresh(hidden)) => Some(hidden) + case _ => None + end Fresh + + /** The type of existentially bound references */ + type Result = AnnotatedType + + object Result: + def apply(binder: MethodType)(using Context): Result = + val hiddenSet = CaptureSet.HiddenSet(NoSymbol) + val res = AnnotatedType(cap, Annot(Kind.Result(binder))) + hiddenSet.owningCap = res + res + + def unapply(tp: Result)(using Context): Option[MethodType] = tp.annot match + case Annot(Kind.Result(binder)) => Some(binder) + case _ => None + end Result + + def unapply(root: CaptureRef)(using Context): Option[Kind] = root match + case root @ AnnotatedType(_, ann: Annot) => Some(ann.kind) + case _ if root.isCap => Some(Kind.Global) + case _ => None + + /** Map each occurrence of cap to a different Sep.Cap instance */ + class CapToFresh(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap: + thisMap => + + override def apply(t: Type) = + if variance <= 0 then t + else t match + case t: CaptureRef if t.isCap => + Fresh.withOwner(owner) + case t @ CapturingType(_, _) => + mapOver(t) + 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 @ Fresh(_) => cap + case t @ CapturingType(_, refs) => mapOver(t) + case _ => mapFollowingAliases(t) + + def inverse = thisMap + override def toString = thisMap.toString + ".inverse" + + end CapToFresh + + /** Maps cap to fresh */ + def capToFresh(tp: Type, owner: Symbol = NoSymbol)(using Context): Type = + if ccConfig.useSepChecks then CapToFresh(owner)(tp) else tp + + /** Maps fresh to cap */ + def freshToCap(tp: Type)(using Context): Type = + if ccConfig.useSepChecks then CapToFresh(NoSymbol).inverse(tp) else tp + + /** Map top-level free existential variables one-to-one to Fresh instances */ + def resultToFresh(tp: Type)(using Context): Type = + val subst = new IdempotentCaptRefMap: + val seen = EqHashMap[Annotation, CaptureRef]() + var localBinders: SimpleIdentitySet[MethodType] = SimpleIdentitySet.empty + + def apply(t: Type): Type = t match + case t @ Result(binder) => + if localBinders.contains(binder) then t // keep bound references + else seen.getOrElseUpdate(t.annot, Fresh()) // map free references to Fresh() + case t: MethodType => + // skip parameters + val saved = localBinders + if t.marksExistentialScope then localBinders = localBinders + t + try t.derivedLambdaType(resType = this(t.resType)) + finally localBinders = saved + case t: PolyType => + // skip parameters + t.derivedLambdaType(resType = this(t.resType)) + case _ => + mapOver(t) + + subst(tp) + end resultToFresh + + /** Replace all occurrences of `cap` (or fresh) in parts of this type by an existentially bound + * variable bound by `mt`. + * Stop at function or method types since these have been mapped before. + */ + def toResult(tp: Type, mt: MethodType, fail: Message => Unit)(using Context): Type = + + abstract class CapMap extends BiTypeMap: + override def mapOver(t: Type): Type = t match + case t @ FunctionOrMethod(args, res) if variance > 0 && !t.isAliasFun => + t // `t` should be mapped in this case by a different call to `mapCap`. + case t: (LazyRef | TypeVar) => + mapConserveSuper(t) + case _ => + super.mapOver(t) + + object toVar extends CapMap: + private val seen = EqHashMap[CaptureRef, Result]() + + def apply(t: Type) = t match + case t: CaptureRef if t.isCapOrFresh => + if variance > 0 then + seen.getOrElseUpdate(t, Result(mt)) + else + if variance == 0 then + fail(em"""$tp captures the root capability `cap` in invariant position. + |This capability cannot be converted to an existential in the result type of a function.""") + // we accept variance < 0, and leave the cap as it is + super.mapOver(t) + case defn.FunctionNOf(args, res, contextual) if t.typeSymbol.name.isImpureFunction => + if variance > 0 then + super.mapOver: + defn.FunctionNOf(args, res, contextual) + .capturing(Result(mt).singletonCaptureSet) + else mapOver(t) + case _ => + mapOver(t) + //.showing(i"mapcap $t = $result") + override def toString = "toVar" + + object inverse extends BiTypeMap: + def apply(t: Type) = t match + case t @ Result(`mt`) => + // do a reverse getOrElseUpdate on `seen` to produce the + // `Fresh` assosicated with `t` + val it = seen.iterator + var ref: CaptureRef | Null = null + while it.hasNext && ref == null do + val (k, v) = it.next + if v.annot eq t.annot then ref = k + if ref == null then + ref = Fresh() + seen(ref) = t + ref + case _ => mapOver(t) + def inverse = toVar.this + override def toString = "toVar.inverse" + end toVar + + toVar(tp) + end toResult + + /** Map global roots in function results to result roots */ + def toResultInResults(fail: Message => Unit, keepAliases: Boolean = false)(using Context): TypeMap = new TypeMap with FollowAliasesMap: + def apply(t: Type): Type = t match + case defn.RefinedFunctionOf(mt) => + val mt1 = apply(mt) + if mt1 ne mt then mt1.toFunctionType(alwaysDependent = true) + else t + case t: MethodType if variance > 0 && t.marksExistentialScope => + val t1 = mapOver(t).asInstanceOf[MethodType] + t1.derivedLambdaType(resType = toResult(t1.resType, t1, fail)) + case CapturingType(parent, refs) => + t.derivedCapturingType(this(parent), refs) + case t: (LazyRef | TypeVar) => + mapConserveSuper(t) + case _ => + if keepAliases then mapOver(t) else mapFollowingAliases(t) + end toResultInResults + + /** 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` 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(_) => false + case t: TermRef => t.isCap || this(x, t.widen) + case x: ThisType => false + case _ => foldOver(x, t) + + def containsCap(x: Type | CaptureSet): Boolean = x match + case tp: Type => + hasCap(false, tp) + case refs: CaptureSet => + refs.elems.exists(_.stripReadOnly.isCap) + + if refs.exists(containsCap) then ctx.withProperty(PrintFresh, Some(())) + else ctx + end printContext +end root \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index c6c0ab47de52..d2d716769990 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -452,6 +452,7 @@ private sealed trait YSettings: val YccDebug: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-debug", "Used in conjunction with captureChecking language import, debug info for captured references.") val YccNew: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-new", "Used in conjunction with captureChecking language import, try out new variants (debug option)") val YccLog: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-log", "Used in conjunction with captureChecking language import, print tracing and debug info") + val YccVerbose: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-verbose", "Print root capabilities with more details") val YccPrintSetup: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-print-setup", "Used in conjunction with captureChecking language import, print trees after cc.Setup phase") /** Area-specific debug output */ diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index f89bc8691e2d..cf2795648484 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, readOnly} import ast.tpd.ref import scala.annotation.tailrec @@ -998,18 +998,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") + @tu lazy val Caps_SharedCapability: ClassSymbol = requiredClass("scala.caps.SharedCapability") /** 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") @@ -1068,6 +1071,8 @@ class Definitions { @tu lazy val UncheckedCapturesAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedCaptures") @tu lazy val UntrackedCapturesAnnot: ClassSymbol = requiredClass("scala.caps.untrackedCaptures") @tu lazy val UseAnnot: ClassSymbol = requiredClass("scala.caps.use") + @tu lazy val ConsumeAnnot: ClassSymbol = requiredClass("scala.caps.consume") + @tu lazy val RefineOverrideAnnot: ClassSymbol = requiredClass("scala.caps.refineOverride") @tu lazy val VolatileAnnot: ClassSymbol = requiredClass("scala.volatile") @tu lazy val LanguageFeatureMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.languageFeature") @tu lazy val BeanGetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanGetter") @@ -1083,6 +1088,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 RootCapabilityAnnot = requiredClass("scala.caps.rootCapability") + @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") @@ -1106,6 +1113,10 @@ class Definitions { @tu lazy val MetaAnnots: Set[Symbol] = NonBeanMetaAnnots + BeanGetterMetaAnnot + BeanSetterMetaAnnot + // Set of annotations that are not printed in types except under -Yprint-debug + @tu lazy val SilentAnnots: Set[Symbol] = + Set(InlineParamAnnot, ErasedParamAnnot, RefineOverrideAnnot) + // A list of annotations that are commonly used to indicate that a field/method argument or return // type is not null. These annotations are used by the nullification logic in JavaNullInterop to // improve the precision of type nullification. @@ -1212,13 +1223,8 @@ class Definitions { */ def unapply(tpe: RefinedType)(using Context): Option[MethodOrPoly] = tpe.refinedInfo match - case mt: MethodType - if tpe.refinedName == nme.apply - && isFunctionType(tpe.parent) - && !Existential.isExistentialMethod(mt) => Some(mt) - case mt: PolyType - if tpe.refinedName == nme.apply - && isFunctionType(tpe.parent) => Some(mt) + case mt: MethodOrPoly + if tpe.refinedName == nme.apply && isFunctionType(tpe.parent) => Some(mt) case _ => None end RefinedFunctionOf @@ -1553,6 +1559,9 @@ class Definitions { @tu lazy val pureSimpleClasses = Set(StringClass, NothingClass, NullClass) ++ ScalaValueClasses() + @tu lazy val capabilityWrapperAnnots: Set[Symbol] = + Set(ReachCapabilityAnnot, ReadOnlyCapabilityAnnot, MaybeCapabilityAnnot, RootCapabilityAnnot) + @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 90e5544f19af..c33c795571e6 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$" @@ -554,6 +555,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" @@ -588,6 +590,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/Substituters.scala b/compiler/src/dotty/tools/dotc/core/Substituters.scala index 96da91293d91..be474215fbb8 100644 --- a/compiler/src/dotty/tools/dotc/core/Substituters.scala +++ b/compiler/src/dotty/tools/dotc/core/Substituters.scala @@ -9,7 +9,7 @@ import cc.CaptureSet.IdempotentCaptRefMap */ object Substituters: - final def subst(tp: Type, from: BindingType, to: BindingType, theMap: SubstBindingMap | Null)(using Context): Type = + final def subst[BT <: BindingType](tp: Type, from: BT, to: BT, theMap: SubstBindingMap[BT] | Null)(using Context): Type = tp match { case tp: BoundType => if (tp.binder eq from) tp.copyBoundType(to.asInstanceOf[tp.BT]) else tp @@ -163,7 +163,7 @@ object Substituters: .mapOver(tp) } - final class SubstBindingMap(from: BindingType, to: BindingType)(using Context) extends DeepTypeMap, BiTypeMap { + final class SubstBindingMap[BT <: BindingType](val from: BT, val to: BT)(using Context) extends DeepTypeMap, BiTypeMap { def apply(tp: Type): Type = subst(tp, from, to, this)(using mapCtx) def inverse = SubstBindingMap(to, from) } diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 54e18bf1ea1b..e7eda037117c 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 54ba0e3bdd06..1b83014e5735 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 cc0471d40213..f8b0675bd204 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -47,8 +47,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling monitored = false GADTused = false opaquesUsed = false - openedExistentials = Nil - assocExistentials = Nil recCount = 0 needsGc = false if Config.checkTypeComparerReset then checkReset() @@ -67,18 +65,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling /** Indicates whether the subtype check used opaque types */ private var opaquesUsed: Boolean = false - /** In capture checking: The existential types that are open because they - * appear in an existential type on the left in an enclosing comparison. - */ - private var openedExistentials: List[TermParamRef] = Nil - - /** In capture checking: A map from existential types that are appear - * in an existential type on the right in an enclosing comparison. - * Each existential gets mapped to the opened existentials to which it - * may resolve at this point. - */ - private var assocExistentials: ExAssoc = Nil - private var myInstance: TypeComparer = this def currentInstance: TypeComparer = myInstance @@ -335,10 +321,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // This is safe because X$ self-type is X.type sym1 = sym1.companionModule if (sym1 ne NoSymbol) && (sym1 eq sym2) then - ctx.erasedTypes || - sym1.isStaticOwner || - isSubPrefix(tp1.prefix, tp2.prefix) || - thirdTryNamed(tp2) + ctx.erasedTypes + || sym1.isStaticOwner + || isSubPrefix(tp1.prefix, tp2.prefix) + || thirdTryNamed(tp2) else (tp1.name eq tp2.name) && !sym1.is(Private) @@ -440,7 +426,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling if (tp1.prefix.isStable) return tryLiftedToThis1 case _ => if isCaptureVarComparison then - return subCaptures(tp1.captureSet, tp2.captureSet, frozenConstraint).isOK + return subCaptures(tp1.captureSet, tp2.captureSet).isOK if (tp1 eq NothingType) || isBottom(tp1) then return true } @@ -548,7 +534,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1 @ CapturingType(parent1, refs1) => def compareCapturing = if tp2.isAny then true - else if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK && sameBoxed(tp1, tp2, refs1) + else if subCaptures(refs1, tp2.captureSet).isOK && sameBoxed(tp1, tp2, refs1) || !ctx.mode.is(Mode.CheckBoundsOrSelfType) && tp1.isAlwaysPure then val tp2a = @@ -565,8 +551,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling if reduced.exists then recur(reduced, tp2) && recordGadtUsageIf { MatchType.thatReducesUsingGadt(tp1) } else thirdTry - case Existential(boundVar, tp1unpacked) => - compareExistentialLeft(boundVar, tp1unpacked, tp2) case _: FlexType => true case _ => @@ -591,7 +575,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling && (isBottom(tp1) || GADTusage(tp2.symbol)) if isCaptureVarComparison then - return subCaptures(tp1.captureSet, tp2.captureSet, frozenConstraint).isOK + return subCaptures(tp1.captureSet, tp2.captureSet).isOK isSubApproxHi(tp1, info2.lo) && (trustBounds || isSubApproxHi(tp1, info2.hi)) || compareGADT @@ -651,8 +635,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling thirdTryNamed(tp2) case tp2: TypeParamRef => compareTypeParamRef(tp2) - case Existential(boundVar, tp2unpacked) => - compareExistentialRight(tp1, boundVar, tp2unpacked) case tp2: RefinedType => def compareRefinedSlow: Boolean = val name2 = tp2.refinedName @@ -678,12 +660,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) case (info1 @ CapturingType(parent1, refs1), info2: Type) if info2.stripCapturing.isInstanceOf[MethodOrPoly] => - subCaptures(refs1, info2.captureSet, frozenConstraint).isOK && sameBoxed(info1, info2, refs1) + subCaptures(refs1, info2.captureSet).isOK && sameBoxed(info1, info2, refs1) && isSubInfo(parent1, info2) case (info1: Type, CapturingType(parent2, refs2)) if info1.stripCapturing.isInstanceOf[MethodOrPoly] => val refs1 = info1.captureSet - (refs1.isAlwaysEmpty || subCaptures(refs1, refs2, frozenConstraint).isOK) && sameBoxed(info1, info2, refs1) + (refs1.isAlwaysEmpty || subCaptures(refs1, refs2).isOK) && sameBoxed(info1, info2, refs1) && isSubInfo(info1, parent2) case _ => isSubType(info1, info2) @@ -822,7 +804,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling (tp1.signature consistentParams tp2.signature) && matchingMethodParams(tp1, tp2) && (!tp2.isImplicitMethod || tp1.isImplicitMethod) && - isSubType(tp1.resultType, tp2.resultType.subst(tp2, tp1)) + CCState.inNewExistentialScope(tp2): + isSubType(tp1.resultType, tp2.resultType.subst(tp2, tp1)) case _ => false } compareMethod @@ -877,12 +860,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // capt-capibility.scala and function-combinators.scala val singletonOK = tp1 match case tp1: SingletonType - if subCaptures(tp1.underlying.captureSet, refs2, frozen = true).isOK => + if subCaptures(tp1.underlying.captureSet, refs2, CaptureSet.VarState.Separate).isOK => recur(tp1.widen, tp2) case _ => false singletonOK - || subCaptures(refs1, refs2, frozenConstraint).isOK + || subCaptures(refs1, refs2).isOK && sameBoxed(tp1, tp2, refs1) && (recur(tp1.widen.stripCapturing, parent2) || tp1.isInstanceOf[SingletonType] && recur(tp1, parent2) @@ -2174,7 +2157,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 } @@ -2806,119 +2789,24 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // ----------- Capture checking ----------------------------------------------- - /** A type associating instantiatable existentials on the right of a comparison - * with the existentials they can be instantiated with. - */ - type ExAssoc = List[(TermParamRef, List[TermParamRef])] - - private def compareExistentialLeft(boundVar: TermParamRef, tp1unpacked: Type, tp2: Type)(using Context): Boolean = - val saved = openedExistentials - try - openedExistentials = boundVar :: openedExistentials - recur(tp1unpacked, tp2) - finally - openedExistentials = saved - - private def compareExistentialRight(tp1: Type, boundVar: TermParamRef, tp2unpacked: Type)(using Context): Boolean = - val saved = assocExistentials - try - assocExistentials = (boundVar, openedExistentials) :: assocExistentials - recur(tp1, tp2unpacked) - finally - assocExistentials = saved - - /** Is `tp1` an existential var that subsumes `tp2`? This is the case if `tp1` is - * instantiatable (i.e. it's a key in `assocExistentials`) and one of the - * following is true: - * - `tp2` is not an existential var, - * - `tp1` is associated via `assocExistentials` with `tp2`, - * - `tp2` appears as key in `assocExistentials` further out than `tp1`. - * The third condition allows to instantiate c2 to c1 in - * EX c1: A -> Ex c2. B - */ - def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context): Boolean = - def canInstantiateWith(assoc: ExAssoc): Boolean = assoc match - case (bv, bvs) :: assoc1 => - if bv == tp1 then - !Existential.isExistentialVar(tp2) - || bvs.contains(tp2) - || assoc1.exists(_._1 == tp2) - else - canInstantiateWith(assoc1) - case Nil => - 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`. - * That's enough to qualify as a BiTypeMap. - */ - private class MapExistentials(assoc: ExAssoc)(using Context) extends BiTypeMap: - - private def bad(t: Type) = - Existential.badExistential - .showing(i"existential match not found for $t in $assoc", capt) - - def apply(t: Type) = t match - case t: TermParamRef if Existential.isExistentialVar(t) => - // Find outermost existential on the right that can be instantiated to `t`, - // or `badExistential` if none exists. - def findMapped(assoc: ExAssoc): CaptureRef = assoc match - case (bv, assocBvs) :: assoc1 => - val outer = findMapped(assoc1) - if !Existential.isBadExistential(outer) then outer - else if assocBvs.contains(t) then bv - else bad(t) - case Nil => - bad(t) - findMapped(assoc) - case _ => - mapOver(t) - - /** The inverse takes existentials on the right to the innermost existential - * on the left to which they can be instantiated. - */ - lazy val inverse = new BiTypeMap: - def apply(t: Type) = t match - case t: TermParamRef if Existential.isExistentialVar(t) => - assoc.find(_._1 == t) match - case Some((_, bvs)) if bvs.nonEmpty => bvs.head - case _ => bad(t) - case _ => - mapOver(t) - - def inverse = MapExistentials.this - override def toString = "MapExistentials.inverse" - end inverse - end MapExistentials + protected def makeVarState() = + if frozenConstraint then CaptureSet.VarState.Closed() else CaptureSet.VarState() - protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = + protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, + vs: CaptureSet.VarState = makeVarState())(using Context): CaptureSet.CompareResult = try - if assocExistentials.isEmpty then - refs1.subCaptures(refs2, frozen) - else - val mapped = refs1.map(MapExistentials(assocExistentials)) - if mapped.elems.exists(Existential.isBadExistential) - then CaptureSet.CompareResult.Fail(refs2 :: Nil) - else subCapturesMapped(mapped, refs2, frozen) + refs1.subCaptures(refs2, vs) catch case ex: AssertionError => println(i"fail while subCaptures $refs1 <:< $refs2") throw ex - protected def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - refs1.subCaptures(refs2, 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? * In the latter case, boxing status does not matter. */ 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, makeVarState()).isOK // ----------- Diagnostics -------------------------------------------------- @@ -3496,14 +3384,8 @@ object TypeComparer { def reduceMatchWith[T](op: MatchReducer => T)(using Context): T = comparing(_.reduceMatchWith(op)) - def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - comparing(_.subCaptures(refs1, refs2, frozen)) - - def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context) = - comparing(_.subsumesExistentially(tp1, tp2)) - - def isOpenedExistential(ref: CaptureRef)(using Context) = - comparing(_.isOpenedExistential(ref)) + def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState)(using Context): CaptureSet.CompareResult = + comparing(_.subCaptures(refs1, refs2, vs)) } object MatchReducer: @@ -3978,14 +3860,9 @@ class ExplainingTypeComparer(initctx: Context, short: Boolean) extends TypeCompa super.gadtAddBound(sym, b, isUpper) } - override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - traceIndented(i"subcaptures $refs1 <:< $refs2 ${if frozen then "frozen" else ""}") { - super.subCaptures(refs1, refs2, frozen) - } - - override def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - traceIndented(i"subcaptures mapped $refs1 <:< $refs2 ${if frozen then "frozen" else ""}") { - super.subCapturesMapped(refs1, refs2, frozen) + override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState)(using Context): CaptureSet.CompareResult = + traceIndented(i"subcaptures $refs1 <:< $refs2 in ${vs.toString}") { + super.subCaptures(refs1, refs2, vs) } def lastTrace(header: String): String = header + { try b.toString finally b.clear() } diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index a7f41a71d7ce..0b758061febd 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, VarState} 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, VarState.Separate).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 7c0c89da97ee..df7700c73a17 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -861,20 +861,27 @@ object Types extends TypeUtils { pdenot.asSingleDenotation.derivedSingleDenotation(pdenot.symbol, jointInfo) } else - val isRefinedMethod = rinfo.isInstanceOf[MethodOrPoly] - val joint = pdenot.meet( - new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId), pre, isRefinedMethod), - pre, - safeIntersection = ctx.base.pendingMemberSearches.contains(name)) - joint match - case joint: SingleDenotation - if isRefinedMethod - && (rinfo <:< joint.info - || name == nme.apply && defn.isFunctionType(tp.parent)) => - // use `rinfo` to keep the right parameter names for named args. See i8516.scala. - joint.derivedSingleDenotation(joint.symbol, rinfo, pre, isRefinedMethod) - case _ => - joint + val overridingRefinement = rinfo match + case AnnotatedType(rinfo1, ann) if ann.symbol == defn.RefineOverrideAnnot => rinfo1 + case _ if pdenot.symbol.is(Tracked) => rinfo + case _ => NoType + if overridingRefinement.exists then + pdenot.asSingleDenotation.derivedSingleDenotation(pdenot.symbol, overridingRefinement) + else + val isRefinedMethod = rinfo.isInstanceOf[MethodOrPoly] + val joint = pdenot.meet( + new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId), pre, isRefinedMethod), + pre, + safeIntersection = ctx.base.pendingMemberSearches.contains(name)) + joint match + case joint: SingleDenotation + if isRefinedMethod + && (rinfo <:< joint.info + || name == nme.apply && defn.isFunctionType(tp.parent)) => + // use `rinfo` to keep the right parameter names for named args. See i8516.scala. + joint.derivedSingleDenotation(joint.symbol, rinfo, pre, isRefinedMethod) + case _ => + joint } def goApplied(tp: AppliedType, tycon: HKTypeLambda) = @@ -1121,7 +1128,7 @@ object Types extends TypeUtils { TypeComparer.topLevelSubType(this, that) } - /** Is this type a subtype of that type? */ + /** Is this type a subtype of that type without adding to the constraint? */ final def frozen_<:<(that: Type)(using Context): Boolean = { record("frozen_<:<") TypeComparer.isSubTypeWhenFrozen(this, that) @@ -3949,7 +3956,7 @@ object Types extends TypeUtils { def apply(tp: Type) = tp match { case tp @ TypeRef(pre, _) => tp.info match { - case TypeAlias(alias) if depStatus(NoDeps, pre) == TrueDeps => apply(alias) + case TypeAlias(alias) if depStatus(NoDeps, pre, forParams = false) == TrueDeps => apply(alias) case _ => mapOver(tp) } case _ => @@ -3963,7 +3970,7 @@ object Types extends TypeUtils { private var myDependencyStatus: DependencyStatus = Unknown private var myParamDependencyStatus: DependencyStatus = Unknown - private def depStatus(initial: DependencyStatus, tp: Type)(using Context): DependencyStatus = + private def depStatus(initial: DependencyStatus, tp: Type, forParams: Boolean)(using Context): DependencyStatus = class DepAcc extends TypeAccumulator[DependencyStatus]: def apply(status: DependencyStatus, tp: Type) = compute(status, tp, this) def combine(x: DependencyStatus, y: DependencyStatus) = @@ -3992,11 +3999,13 @@ object Types extends TypeUtils { case tp: AnnotatedType => tp match case CapturingType(parent, refs) => - (compute(status, parent, theAcc) /: refs.elems) { + val status1 = (compute(status, parent, theAcc) /: refs.elems): (s, ref) => ref.stripReach match - case tp: TermParamRef if tp.binder eq thisLambdaType => combine(s, CaptureDeps) + case tp: TermParamRef if tp.binder eq thisLambdaType => combine(s, TrueDeps) case tp => combine(s, compute(status, tp, theAcc)) - } + if refs.isConst || forParams // We assume capture set variables in parameters don't generate param dependencies + then status1 + else combine(status1, Provisional) case _ => if tp.annot.refersToParamOf(thisLambdaType) then TrueDeps else compute(status, tp.parent, theAcc) @@ -4020,7 +4029,7 @@ object Types extends TypeUtils { private def dependencyStatus(using Context): DependencyStatus = if (myDependencyStatus != Unknown) myDependencyStatus else { - val result = depStatus(NoDeps, resType) + val result = depStatus(NoDeps, resType, forParams = false) if ((result & Provisional) == 0) myDependencyStatus = result (result & StatusMask).toByte } @@ -4033,7 +4042,7 @@ object Types extends TypeUtils { else { val result = if (paramInfos.isEmpty) NoDeps - else paramInfos.tail.foldLeft(NoDeps)(depStatus(_, _)) + else paramInfos.tail.foldLeft(NoDeps)(depStatus(_, _, forParams = true)) if ((result & Provisional) == 0) myParamDependencyStatus = result (result & StatusMask).toByte } @@ -4042,18 +4051,20 @@ object Types extends TypeUtils { * which cannot be eliminated by de-aliasing? */ def isResultDependent(using Context): Boolean = - dependencyStatus == TrueDeps || dependencyStatus == CaptureDeps + dependencyStatus == TrueDeps /** Does one of the parameter types contain references to earlier parameters * of this method type which cannot be eliminated by de-aliasing? */ def isParamDependent(using Context): Boolean = - paramDependencyStatus == TrueDeps || paramDependencyStatus == CaptureDeps + paramDependencyStatus == TrueDeps - /** Is there a dependency involving a reference in a capture set, but - * otherwise no true result dependency? + /** Like resultDependent || paramDependent, but without attempt to eliminate + * dependencies with de-aliasing */ - def isCaptureDependent(using Context) = dependencyStatus == CaptureDeps + def looksDependent(using Context): Boolean = + (dependencyStatus & StatusMask) != NoDeps + || (paramDependencyStatus & StatusMask) != NoDeps def newParamRef(n: Int): TermParamRef = new TermParamRefImpl(this, n) @@ -4069,7 +4080,7 @@ object Types extends TypeUtils { case ReachCapability(tp1) => apply(tp1) match case tp1a: CaptureRef if tp1a.isTrackableRef => tp1a.reach - case _ => defn.captureRoot.termRef + case _ => root.cap case AnnotatedType(parent, ann) if ann.refersToParamOf(thisLambdaType) => val parent1 = mapOver(parent) if ann.symbol.isRetainsLike then @@ -4083,6 +4094,10 @@ object Types extends TypeUtils { } dropDependencies(resultType) else resultType + + /** Are all parameter names synthetic? */ + def allParamNamesSynthetic = paramNames.zipWithIndex.forall: (name, i) => + name == nme.syntheticParamName(i) } abstract case class MethodType(paramNames: List[TermName])( @@ -4112,7 +4127,6 @@ object Types extends TypeUtils { def nonErasedParamCount(using Context): Int = paramInfos.count(p => !p.hasAnnotation(defn.ErasedParamAnnot)) - protected def prefixString: String = companion.prefixString } @@ -4175,7 +4189,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 = @@ -4461,8 +4475,7 @@ object Types extends TypeUtils { final val Unknown: DependencyStatus = 0 // not yet computed final val NoDeps: DependencyStatus = 1 // no dependent parameters found final val FalseDeps: DependencyStatus = 2 // all dependent parameters are prefixes of non-depended alias types - final val CaptureDeps: DependencyStatus = 3 // dependencies in capture sets under captureChecking, otherwise only false dependencoes - final val TrueDeps: DependencyStatus = 4 // some truly dependent parameters exist + final val TrueDeps: DependencyStatus = 3 // some truly dependent parameters exist final val StatusMask: DependencyStatus = 7 // the bits indicating actual dependency status final val Provisional: DependencyStatus = 8 // set if dependency status can still change due to type variable instantiations } @@ -6082,7 +6095,7 @@ object Types extends TypeUtils { case tp: TypeAlias => ensureTrackable(tp.alias) case _ => - assert(false, i"not a trackable captureRef ref: $result, ${result.underlyingIterator.toList}") + assert(false, i"not a trackable CaptureRef: $result with underlying ${result.underlyingIterator.toList}") ensureTrackable(result) /** A restriction of the inverse to a function on tracked CaptureRefs */ diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 20eb6e9b33fa..c24dbce1b6ac 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1600,22 +1600,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 */ @@ -1624,7 +1638,7 @@ object Parsers { } def capturesAndResult(core: () => Tree): Tree = - if Feature.ccEnabled && in.token == LBRACE && in.offset == in.lastOffset + if Feature.ccEnabled && in.token == LBRACE && canStartCaptureSetContentsTokens.contains(in.lookahead.token) then CapturesAndResult(captureSet(), core()) else core() @@ -3304,13 +3318,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() } } @@ -3378,7 +3393,8 @@ object Parsers { * | override * | opaque * LocalModifier ::= abstract | final | sealed | open | implicit | lazy | erased | - * inline | transparent | infix + * inline | transparent | infix | + * mut -- under cc */ def modifiers(allowed: BitSet = modifierTokens, start: Modifiers = Modifiers()): Modifiers = { @tailrec @@ -4691,7 +4707,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 ed20c189796b..f7050cec41fd 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -1209,7 +1209,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/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index c78a336ecdf5..bc55371ec96a 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -295,6 +295,8 @@ object Tokens extends TokensCommon { final val colonEOLPredecessors = BitSet(RPAREN, RBRACKET, BACKQUOTED_IDENT, THIS, SUPER, NEW) + final val canStartCaptureSetContentsTokens = BitSet(IDENTIFIER, BACKQUOTED_IDENT, THIS, RBRACE) + final val closingParens = BitSet(RPAREN, RBRACKET, RBRACE) final val softModifierNames = Set(nme.inline, nme.opaque, nme.open, nme.transparent, nme.infix) diff --git a/compiler/src/dotty/tools/dotc/printing/Formatting.scala b/compiler/src/dotty/tools/dotc/printing/Formatting.scala index ccd7b4e4e282..741b997d9926 100644 --- a/compiler/src/dotty/tools/dotc/printing/Formatting.scala +++ b/compiler/src/dotty/tools/dotc/printing/Formatting.scala @@ -8,7 +8,7 @@ import core.* import Texts.*, Types.*, Flags.*, Symbols.*, Contexts.* import Decorators.* import reporting.Message -import util.DiffUtil +import util.{DiffUtil, SimpleIdentitySet} import Highlighting.* object Formatting { @@ -87,6 +87,9 @@ object Formatting { def show(x: H *: T) = CtxShow(toStr(x.head) *: toShown(x.tail).asInstanceOf[Tuple]) + given [X <: AnyRef: Show]: Show[SimpleIdentitySet[X]] with + def show(x: SimpleIdentitySet[X]) = summon[Show[List[X]]].show(x.toList) + given Show[FlagSet] with def show(x: FlagSet) = x.flagsString diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index e90aeb217362..41278ca27159 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -27,6 +27,12 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def printDebug = ctx.settings.YprintDebug.value + /** Print Fresh instances as */ + protected def ccVerbose = ctx.settings.YccVerbose.value + + /** Print Fresh instances as "fresh" */ + protected def printFresh = ccVerbose || ctx.property(PrintFresh).isDefined + private var openRecs: List[RecType] = Nil protected def maxToTextRecursions: Int = 100 @@ -153,12 +159,14 @@ class PlainPrinter(_ctx: Context) extends Printer { + defn.FromJavaObjectSymbol def toTextCaptureSet(cs: CaptureSet): Text = - if printDebug && ctx.settings.YccDebug.value && !cs.isConst then cs.toString + if printDebug && ctx.settings.YccDebug.value + && !cs.isConst && !cs.isInstanceOf[CaptureSet.HiddenSet] //HiddenSets can be cyclic + then cs.toString else if cs == CaptureSet.Fluid then "" 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 +175,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 +187,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 +198,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 +210,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 +250,32 @@ 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 + // The set if universal if it consists only of caps.cap or + // only of an existential Fresh that is bound to the immediately enclosing method. + def isUniversal = + refs.elems.size == 1 + && (refs.isUniversal + || !printDebug && !printFresh && !showUniqueIds && refs.elems.nth(0).match + case root.Result(binder) => + CCState.openExistentialScopes match + case b :: _ => binder eq b + case _ => false + case _ => + false + ) + val refsText = + if isUniversal then + rootSetText + 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 @@ -270,30 +301,30 @@ class PlainPrinter(_ctx: Context) extends Printer { ~ paramsText(tp) ~ ")" ~ (Str(": ") provided !tp.resultType.isInstanceOf[MethodOrPoly]) - ~ toText(tp.resultType) + ~ CCState.inNewExistentialScope(tp)(toText(tp.resultType)) } 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) } case AnnotatedType(tpe, annot) => - if annot.symbol == defn.InlineParamAnnot || annot.symbol == defn.ErasedParamAnnot - then toText(tpe) + if defn.SilentAnnots.contains(annot.symbol) && !printDebug then + toText(tpe) else if (annot.symbol == defn.IntoAnnot || annot.symbol == defn.IntoParamAnnot) && !printDebug then atPrec(GlobalPrec)( Str("into ") ~ toText(tpe) ) @@ -338,7 +369,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), ", ") } @@ -350,11 +381,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: @@ -403,7 +434,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) @@ -417,11 +448,25 @@ 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 tp @ root.Result(binder) => + val idStr = s"##${tp.rootAnnot.id}" + // TODO: Better printing? USe a mode where we print more detailed + val vbleText: Text = CCState.openExistentialScopes.indexOf(binder) match + case -1 => + "" + case n => "outer_" * n ++ (if printFresh then "localcap" else "cap") + vbleText ~ hashStr(binder) ~ Str(idStr).provided(showUniqueIds) + case tp @ root.Fresh(hidden) => + val idStr = if showUniqueIds then s"#${tp.rootAnnot.id}" else "" + if ccVerbose then s"" + else if printFresh then "fresh" + else "cap" case tp => toText(tp) protected def isOmittablePrefix(sym: Symbol): Boolean = @@ -536,7 +581,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" @@ -552,7 +597,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 32115e6bc087..5839a7a817c8 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -161,48 +161,59 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { private def toTextFunction(tp: AppliedType, refs: Text = Str("")): Text = val AppliedType(tycon, args) = (tp: @unchecked) val tsym = tycon.typeSymbol - val isGiven = tsym.name.isContextFunction + val isContextual = tsym.name.isContextFunction val capturesRoot = refs == rootSetText val isPure = Feature.pureFunsEnabled && !tsym.name.isImpureFunction && !capturesRoot - changePrec(GlobalPrec) { - val argStr: Text = - if args.length == 2 - && !defn.isDirectTupleNType(args.head) - && !isGiven - then - atPrec(InfixPrec) { argText(args.head) } - else + toTextFunction(args.init, args.last, tp, refs.provided(!capturesRoot), isContextual, isPure) + + private def toTextFunction(args: List[Type], res: Type, fn: MethodType | AppliedType, refs: Text, + isContextual: Boolean, isPure: Boolean): Text = + changePrec(GlobalPrec): + val argStr: Text = args match + case arg :: Nil if !defn.isDirectTupleNType(arg) && !isContextual => + atPrec(InfixPrec): + argText(arg) + case _=> "(" - ~ argsText(args.init) + ~ argsText(args) ~ ")" - argStr - ~ " " ~ arrow(isGiven, isPure) - ~ (refs provided !capturesRoot) - ~ " " ~ argText(args.last) - } - - protected def toTextMethodAsFunction(info: Type, isPure: Boolean, refs: Text = Str("")): Text = info match - case info: MethodType => - val capturesRoot = refs == rootSetText - changePrec(GlobalPrec) { - "(" - ~ paramsText(info) - ~ ") " - ~ arrow(info.isImplicitMethod, isPure && !capturesRoot) - ~ (refs provided !capturesRoot) - ~ " " - ~ toTextMethodAsFunction(info.resultType, isPure) - } - case info: PolyType => - changePrec(GlobalPrec) { - "[" - ~ paramsText(info) - ~ "] => " - ~ toTextMethodAsFunction(info.resultType, isPure) - } - case _ => - toText(info) + argStr ~ " " ~ arrow(isContextual, isPure) ~ refs ~ " " + ~ fn.match + case fn: MethodType => CCState.inNewExistentialScope(fn)(argText(res)) + case _ => argText(res) + + protected def toTextMethodAsFunction(info: Type, isPure: Boolean, refs: Text = Str("")): Text = + def recur(tp: Type, enclInfo: MethodType | Null): Text = tp match + case tp: MethodType => + val isContextual = tp.isImplicitMethod + val capturesRoot = refs == rootSetText + if cc.isCaptureCheckingOrSetup + && tp.allParamNamesSynthetic && !tp.looksDependent + && !showUniqueIds && !printDebug && !printFresh + then + // cc.Setup converts all functions to dependent functions. Undo that when printing. + toTextFunction(tp.paramInfos, tp.resType, tp, refs.provided(!capturesRoot), isContextual, isPure && !capturesRoot) + else + changePrec(GlobalPrec): + "(" + ~ paramsText(tp) + ~ ") " + ~ arrow(isContextual, isPure && !capturesRoot) + ~ refs.provided(!capturesRoot) + ~ " " + ~ recur(tp.resultType, tp) + case tp: PolyType => + changePrec(GlobalPrec) { + "[" + ~ paramsText(tp) + ~ "] => " + ~ recur(tp.resultType, enclInfo) + } + case _ => + if enclInfo != null then CCState.inNewExistentialScope(enclInfo)(toText(tp)) + else toText(tp) + recur(info, null) override def toText(tp: Type): Text = controlled { def toTextTuple(args: List[Type]): Text = @@ -286,9 +297,6 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { if !printDebug && appliedText(tp.asInstanceOf[HKLambda].resType).isEmpty => // don't eta contract if the application would be printed specially toText(tycon) - case Existential(boundVar, unpacked) - if !printDebug && !ctx.settings.YccDebug.value && !unpacked.existsPart(_ == boundVar) => - toText(unpacked) case tp: RefinedType if defn.isFunctionType(tp) && !printDebug => toTextMethodAsFunction(tp.refinedInfo, isPure = Feature.pureFunsEnabled && !tp.typeSymbol.name.isImpureFunction, @@ -337,7 +345,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) @@ -744,6 +752,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) => @@ -938,7 +948,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { tree.hasType && tree.symbol.exists && ctx.settings.YprintSyms.value protected def nameIdText[T <: Untyped](tree: NameTree[T]): Text = - if (tree.hasType && tree.symbol.exists) { + if (tree.hasType && tree.symbol.exists && tree.symbol.isType == tree.name.isTypeName) { val str = nameString(tree.symbol) tree match { case tree: RefTree => withPos(str, tree.sourcePos) diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index b5d67f0808b2..dcd7ed10987b 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 c303c40485ce..4d915b57df1b 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/CheckShadowing.scala b/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala index 3adb3ab0ce7d..e6fe64fe7b62 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala @@ -18,7 +18,6 @@ import dotty.tools.dotc.core.Types.NoType import dotty.tools.dotc.core.Types.Type import dotty.tools.dotc.core.Types import dotty.tools.dotc.semanticdb.TypeOps -import dotty.tools.dotc.cc.boxedCaptureSet import dotty.tools.dotc.core.Symbols.{NoSymbol, isParamOrAccessor} import scala.collection.mutable import dotty.tools.dotc.core.Scopes.Scope 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/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 8936c460de81..60c36fdbbbb7 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -19,7 +19,6 @@ import typer.ErrorReporting.{Addenda, NothingToAdd} import config.Printers.recheckr import util.Property import StdNames.nme -import reporting.trace import annotation.constructorOnly import cc.CaptureSet.IdempotentCaptRefMap import annotation.tailrec @@ -167,7 +166,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 = 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 328446a02e23..226e8dbf3fb4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -1032,7 +1032,7 @@ class Objects(using Context @constructorOnly): UnknownValue 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 ec07fefc64ab..cd90d9f2397a 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.* @@ -596,7 +596,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)) { @@ -605,8 +605,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 extending 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 a015348e90a7..17e475e50a49 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, CCState} object RefChecks { import tpd.* @@ -107,7 +107,9 @@ object RefChecks { def checkSelfConforms(other: ClassSymbol) = var otherSelf = other.declaredSelfTypeAsSeenFrom(cls.thisType) if otherSelf.exists then - if !(cinfo.selfType <:< otherSelf) then + if !CCState.withCapAsRoot: // OK? We need this here since self types use `cap` instead of `fresh` + cinfo.selfType <:< otherSelf + then report.error(DoesNotConformToSelfType("illegal inheritance", cinfo.selfType, cls, otherSelf, "parent", other), cls.srcPos) @@ -595,7 +597,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)) && @@ -616,6 +618,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) @@ -775,7 +779,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. @@ -784,7 +788,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 be3186720fa1..03782a423fc7 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1393,7 +1393,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/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala index b243145c9e5f..714e3a5fc0d6 100644 --- a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala +++ b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala @@ -18,12 +18,18 @@ abstract class SimpleIdentitySet[+Elem <: AnyRef] { var acc: SimpleIdentitySet[B] = SimpleIdentitySet.empty foreach(x => acc += f(x)) acc + def flatMap[B <: AnyRef](f: Elem => SimpleIdentitySet[B]): SimpleIdentitySet[B] = + var acc: SimpleIdentitySet[B] = SimpleIdentitySet.empty + foreach(x => acc ++= f(x)) + acc def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A def toList: List[Elem] - def iterator: Iterator[Elem] + def nth(n: Int): Elem final def isEmpty: Boolean = size == 0 + final def iterator: Iterator[Elem] = Iterator.tabulate(size)(nth) + def forall[E >: Elem <: AnyRef](p: E => Boolean): Boolean = !exists(!p(_)) def filter(p: Elem => Boolean): SimpleIdentitySet[Elem] = @@ -42,6 +48,11 @@ abstract class SimpleIdentitySet[+Elem <: AnyRef] { if (that.contains(x)) s else s + x } + def ** [E >: Elem <: AnyRef](that: SimpleIdentitySet[E]): SimpleIdentitySet[E] = + if this.size == 0 then this + else if that.size == 0 then that + else this.filter(that.contains) + def == [E >: Elem <: AnyRef](that: SimpleIdentitySet[E]): Boolean = this.size == that.size && forall(that.contains) @@ -69,7 +80,7 @@ object SimpleIdentitySet { override def map[B <: AnyRef](f: Nothing => B): SimpleIdentitySet[B] = empty def /: [A, E <: AnyRef](z: A)(f: (A, E) => A): A = z def toList = Nil - def iterator = Iterator.empty + def nth(n: Int): Nothing = throw new IndexOutOfBoundsException(n.toString) } private class Set1[+Elem <: AnyRef](x0: AnyRef) extends SimpleIdentitySet[Elem] { @@ -87,7 +98,9 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(z, x0.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: Nil - def iterator = Iterator.single(x0.asInstanceOf[Elem]) + def nth(n: Int) = + if n == 0 then x0.asInstanceOf[Elem] + else throw new IndexOutOfBoundsException(n.toString) } private class Set2[+Elem <: AnyRef](x0: AnyRef, x1: AnyRef) extends SimpleIdentitySet[Elem] { @@ -109,10 +122,10 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(f(z, x0.asInstanceOf[E]), x1.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: x1.asInstanceOf[Elem] :: Nil - def iterator = Iterator.tabulate(2) { + def nth(n: Int) = n match case 0 => x0.asInstanceOf[Elem] case 1 => x1.asInstanceOf[Elem] - } + case _ => throw new IndexOutOfBoundsException(n.toString) } private class Set3[+Elem <: AnyRef](x0: AnyRef, x1: AnyRef, x2: AnyRef) extends SimpleIdentitySet[Elem] { @@ -149,11 +162,11 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(f(f(z, x0.asInstanceOf[E]), x1.asInstanceOf[E]), x2.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: x1.asInstanceOf[Elem] :: x2.asInstanceOf[Elem] :: Nil - def iterator = Iterator.tabulate(3) { + def nth(n: Int) = n match case 0 => x0.asInstanceOf[Elem] case 1 => x1.asInstanceOf[Elem] case 2 => x2.asInstanceOf[Elem] - } + case _ => throw new IndexOutOfBoundsException(n.toString) } private class SetN[+Elem <: AnyRef](val xs: Array[AnyRef]) extends SimpleIdentitySet[Elem] { @@ -200,7 +213,9 @@ object SimpleIdentitySet { foreach(buf += _) buf.toList } - def iterator = xs.iterator.asInstanceOf[Iterator[Elem]] + def nth(n: Int) = + if 0 <= n && n < size then xs(n).asInstanceOf[Elem] + else throw new IndexOutOfBoundsException(n.toString) override def ++ [E >: Elem <: AnyRef](that: SimpleIdentitySet[E]): SimpleIdentitySet[E] = that match { case that: SetN[?] => diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index e62c80d7bff7..47ed2aa6564d 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -37,7 +37,6 @@ class CompilationTests { compileFilesInDir("tests/pos-special/sourcepath/outer", defaultOptions.and("-sourcepath", "tests/pos-special/sourcepath")), compileFile("tests/pos-special/sourcepath/outer/nested/Test4.scala", defaultOptions.and("-sourcepath", "tests/pos-special/sourcepath")), compileFilesInDir("tests/pos-scala2", defaultOptions.and("-source", "3.0-migration")), - compileFilesInDir("tests/pos-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking")), compileFile("tests/pos-special/utf8encoded.scala", defaultOptions.and("-encoding", "UTF8")), compileFile("tests/pos-special/utf16encoded.scala", defaultOptions.and("-encoding", "UTF16")), compileDir("tests/pos-special/i18589", defaultOptions.and("-Wsafe-init").without("-Ycheck:all")), @@ -56,6 +55,12 @@ class CompilationTests { aggregateTests(tests*).checkCompile() } + @Test def posCC: Unit = + given TestGroup = TestGroup("compilePosCC") + aggregateTests( + compileFilesInDir("tests/pos-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking")), + ).checkCompile() + @Test def rewrites: Unit = { implicit val testGroup: TestGroup = TestGroup("rewrites") 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/CanThrow.scala b/library/src/scala/CanThrow.scala index 91c94229c43c..485dcecb37df 100644 --- a/library/src/scala/CanThrow.scala +++ b/library/src/scala/CanThrow.scala @@ -8,7 +8,7 @@ import annotation.{implicitNotFound, experimental, capability} */ @experimental @implicitNotFound("The capability to throw exception ${E} is missing.\nThe capability can be provided by one of the following:\n - Adding a using clause `(using CanThrow[${E}])` to the definition of the enclosing method\n - Adding `throws ${E}` clause after the result type of the enclosing method\n - Wrapping this piece of code with a `try` block that catches ${E}") -erased class CanThrow[-E <: Exception] extends caps.Capability +erased class CanThrow[-E <: Exception] extends caps.SharedCapability @experimental object unsafeExceptions: 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..d53a9b128e5b 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -16,6 +16,10 @@ import annotation.{experimental, compileTimeOnly, retainsCap} @deprecated("Use `Capability` instead") type Cap = Capability + trait Mutable extends Capability + + trait SharedCapability extends Capability + /** Carrier trait for capture set type parameters */ trait CapSet extends Any @@ -41,10 +45,16 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ extension (x: Any) def reachCapability: Any = x - /** A trait to allow expressing existential types such as - * - * (x: Exists) => A ->{x} B + /** Read-only capabilities x.rd which appear as terms in @retains annotations are encoded + * as `caps.readOnlyCapability(x)`. When converted to CaptureRef types in capture sets + * they are represented as `x.type @annotation.internal.readOnlyCapability`. */ + extension (x: Any) def readOnlyCapability: Any = x + + /** A trait that used to allow expressing existential types. Replaced by + * root.Result instances. + */ + @deprecated sealed trait Exists extends Capability /** This should go into annotations. For now it is here, so that we @@ -52,11 +62,41 @@ 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 + /** An annotations on parameters and update methods. + * On a parameter it states that any capabilties passed in the argument + * are no longer available afterwards, unless they are of class `SharableCapabilitty`. + * On an update method, it states that the `this` of the enclosing class is + * consumed, which means that any capabilities of the method prefix are + * no longer available afterwards. + */ + final class consume extends annotation.StaticAnnotation + + /** An internal annotation placed on a refinement created by capture checking. + * Refinements with this annotation unconditionally override any + * info from the parent type, so no intersection needs to be formed. + * This could be useful for tracked parameters as well. + */ + final class refineOverride extends annotation.StaticAnnotation + + /** An annotation used internally for root capability wrappers of `cap` that + * represent either Fresh or Result capabilities. + * A capability is encoded as `caps.cap @rootCapability(...)` where + * `rootCapability(...)` is a special kind of annotation of type `root.Annot` + * that contains either a hidden set for Fresh instances or a method type binder + * for Result instances. + */ + final class rootCapability extends annotation.StaticAnnotation + object unsafe: extension [T](x: T) @@ -66,4 +106,9 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ def unsafeAssumePure: T = x + /** A wrapper around code for which separation checks are suppressed. + */ + def unsafeAssumeSeparate(op: Any): op.type = op + end unsafe +end caps \ No newline at end of file diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index e4473b9869ca..964acf50089f 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -8,6 +8,7 @@ object MiMaFilters { val ForwardsBreakingChanges: Map[String, Seq[ProblemFilter]] = Map( // Additions that require a new minor version of the library Build.mimaPreviousDottyVersion -> Seq( + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.readOnlyCapability"), ), // Additions since last LTS diff --git a/scala2-library-cc/src/scala/collection/IterableOnce.scala b/scala2-library-cc/src/scala/collection/IterableOnce.scala index 7e8555421c53..7ea62a9e1a65 100644 --- a/scala2-library-cc/src/scala/collection/IterableOnce.scala +++ b/scala2-library-cc/src/scala/collection/IterableOnce.scala @@ -805,7 +805,7 @@ trait IterableOnceOps[+A, +CC[_], +C] extends Any { this: IterableOnce[A]^ => case _ => Some(reduceLeft(op)) } private final def reduceLeftOptionIterator[B >: A](op: (B, A) => B): Option[B] = reduceOptionIterator[A, B](iterator)(op) - private final def reduceOptionIterator[X >: A, B >: X](it: Iterator[X]^)(op: (B, X) => B): Option[B] = { + private final def reduceOptionIterator[X >: A, B >: X](it: Iterator[X]^{this, caps.cap})(op: (B, X) => B): Option[B] = { if (it.hasNext) { var acc: B = it.next() while (it.hasNext) diff --git a/scala2-library-cc/src/scala/collection/View.scala b/scala2-library-cc/src/scala/collection/View.scala index 132934dbe3bd..72a073836e77 100644 --- a/scala2-library-cc/src/scala/collection/View.scala +++ b/scala2-library-cc/src/scala/collection/View.scala @@ -16,6 +16,7 @@ import scala.annotation.{nowarn, tailrec} import scala.collection.mutable.{ArrayBuffer, Builder} import scala.collection.immutable.LazyList import language.experimental.captureChecking +import caps.unsafe.unsafeAssumeSeparate /** Views are collections whose transformation operations are non strict: the resulting elements * are evaluated only when the view is effectively traversed (e.g. using `foreach` or `foldLeft`), @@ -154,6 +155,12 @@ object View extends IterableFactory[View] { new Filter(filter.underlying, a => filter.p(a) && p(a), isFlipped) .asInstanceOf[Filter[A]^{underlying, p}] // !!! asInstanceOf needed once paths were added, see path-patmat-should-be-pos.scala for minimization + //case filter: Filter[A]^{underlying} if filter.isFlipped == isFlipped => + // unsafeAssumeSeparate: + // See filter-iterable.scala for a test where a variant of Filter + // works without the unsafeAssumeSeparate. But it requires significant + // changes compared to the version here. See also Filter in colltest5.CollectionStrawManCC5_1. + // new Filter(filter.underlying, a => filter.p(a) && p(a), isFlipped) case _ => new Filter(underlying, p, isFlipped) } } diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index 28ce8da104aa..e9383f6e4c8c 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -24,7 +24,8 @@ import scala.language.implicitConversions import scala.runtime.Statics import language.experimental.captureChecking import annotation.unchecked.uncheckedCaptures -import caps.untrackedCaptures +import caps.{cap, 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. @@ -386,12 +387,30 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz */ def lazyAppendedAll[B >: A](suffix: => collection.IterableOnce[B]^): LazyListIterable[B]^{this, suffix} = newLL { - if (isEmpty) suffix match { + {if (isEmpty) suffix match { case lazyList: LazyListIterable[B] => lazyList.state // don't recompute the LazyListIterable case coll if coll.knownSize == 0 => State.Empty case coll => stateFromIterator(coll.iterator) } else sCons(head, tail lazyAppendedAll suffix) + }.asInstanceOf + /* TODO: Without the asInstanceOf, we get + [error] 390 | {if (isEmpty) suffix match { + [error] | ^y-cc / Compile / compileIncremental 10s + [error] |Found: () ?->{suffix} + [error] | scala.collection.immutable.LazyListIterable.State[box B^?]^{unknown.localcap} + [error] |Required: () ?->{fresh} + [error] | scala.collection.immutable.LazyListIterable.State[box B^?]^{localcap} + [error] 391 | case lazyList: LazyListIterable[B] => lazyList.state // don't recompute the LazyListIterable + [error] 392 | case coll if coll.knownSize == 0 => State.Empty + [error] 393 | case coll => stateFromIterator(coll.iterator) + [error] 394 | } + [error] 395 | else sCons(head, tail lazyAppendedAll suffix) + [error] 396 | }//.asInstanceOf + [error] | + + Figure out why we found a result with capture {unknown.localcap}. + */ } /** @inheritdoc @@ -462,8 +481,11 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz * $preservesLaziness */ override def partitionMap[A1, A2](f: A => Either[A1, A2]): (LazyListIterable[A1]^{this, f}, LazyListIterable[A2]^{this, f}) = { - val (left, right) = map(f).partition(_.isLeft) - (left.map(_.asInstanceOf[Left[A1, _]].value), right.map(_.asInstanceOf[Right[_, A2]].value)) + unsafeAssumeSeparate: + val part = map(f).partition(_.isLeft) + val left = part._1 + val right = part._2 + (left.map(_.asInstanceOf[Left[A1, _]].value), right.map(_.asInstanceOf[Right[_, A2]].value)) } /** @inheritdoc @@ -674,7 +696,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz override def dropRight(n: Int): LazyListIterable[A]^{this} = { if (n <= 0) this else if (knownIsEmpty) LazyListIterable.empty - else newLL { + else unsafeAssumeSeparate { newLL { var scout = this var remaining = n // advance scout n elements ahead (or until empty) @@ -683,7 +705,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz scout = scout.tail } dropRightState(scout) - } + }} } private def dropRightState(scout: LazyListIterable[_]^): State[A]^{this, scout} = @@ -878,7 +900,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz // if cursor (eq scout) has state defined, it is empty; else unknown state 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) + @inline def same(a: LazyListIterable[A]^, b: LazyListIterable[A]^{cap, a}): Boolean = (a eq b) || (a.state eq b.state) // 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 @@ -1052,7 +1074,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( + unsafeAssumeSeparate( + stateFromIteratorConcatSuffix(it)(flatMapImpl(rest, f).state)))) } else State.Empty } } @@ -1178,7 +1202,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { * @param f the function that's repeatedly applied * @return the LazyListIterable returning the infinite sequence of values `start, f(start), f(f(start)), ...` */ - def iterate[A](start: => A)(f: A => A): LazyListIterable[A]^{start, f} = + def iterate[A](start: => A)(f: A ->{cap, start} A): LazyListIterable[A]^{start, f} = newLL { val head = start sCons(head, iterate(f(head))(f)) diff --git a/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala b/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala index 9ce0399e0662..1c3f669f5358 100644 --- a/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala +++ b/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala @@ -14,10 +14,11 @@ package scala package collection package mutable import language.experimental.captureChecking +import caps.cap private[mutable] trait CheckedIndexedSeqView[+A] extends IndexedSeqView[A] { - protected val mutationCount: () => Int + protected val mutationCount: () ->{cap.rd} Int override def iterator: Iterator[A]^{this} = new CheckedIndexedSeqView.CheckedIterator(this, mutationCount()) override def reverseIterator: Iterator[A]^{this} = new CheckedIndexedSeqView.CheckedReverseIterator(this, mutationCount()) @@ -42,7 +43,7 @@ private[mutable] object CheckedIndexedSeqView { import IndexedSeqView.SomeIndexedSeqOps @SerialVersionUID(3L) - private[mutable] class CheckedIterator[A](self: IndexedSeqView[A]^, mutationCount: => Int) + private[mutable] class CheckedIterator[A](self: IndexedSeqView[A]^, mutationCount: ->{cap.rd} Int) extends IndexedSeqView.IndexedSeqViewIterator[A](self) { private[this] val expectedCount = mutationCount override def hasNext: Boolean = { @@ -52,7 +53,7 @@ private[mutable] object CheckedIndexedSeqView { } @SerialVersionUID(3L) - private[mutable] class CheckedReverseIterator[A](self: IndexedSeqView[A]^, mutationCount: => Int) + private[mutable] class CheckedReverseIterator[A](self: IndexedSeqView[A]^, mutationCount: ->{cap.rd} Int) extends IndexedSeqView.IndexedSeqViewReverseIterator[A](self) { private[this] val expectedCount = mutationCount override def hasNext: Boolean = { @@ -62,43 +63,43 @@ private[mutable] object CheckedIndexedSeqView { } @SerialVersionUID(3L) - class Id[+A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int) + class Id[+A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Id(underlying) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Appended[+A](underlying: SomeIndexedSeqOps[A]^, elem: A)(protected val mutationCount: () => Int) + class Appended[+A](underlying: SomeIndexedSeqOps[A]^, elem: A)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Appended(underlying, elem) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Prepended[+A](elem: A, underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int) + class Prepended[+A](elem: A, underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Prepended(elem, underlying) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Concat[A](prefix: SomeIndexedSeqOps[A]^, suffix: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int) + class Concat[A](prefix: SomeIndexedSeqOps[A]^, suffix: SomeIndexedSeqOps[A]^)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Concat[A](prefix, suffix) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Take[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int) + class Take[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Take(underlying, n) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class TakeRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int) + class TakeRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.TakeRight(underlying, n) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Drop[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int) + class Drop[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Drop[A](underlying, n) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class DropRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int) + class DropRight[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.DropRight[A](underlying, n) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) - class Map[A, B](underlying: SomeIndexedSeqOps[A]^, f: A => B)(protected val mutationCount: () => Int) + class Map[A, B](underlying: SomeIndexedSeqOps[A]^, f: A => B)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Map(underlying, f) with CheckedIndexedSeqView[B] @SerialVersionUID(3L) - class Reverse[A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int) + class Reverse[A](underlying: SomeIndexedSeqOps[A]^)(protected val mutationCount: () ->{cap.rd} Int) extends IndexedSeqView.Reverse[A](underlying) with CheckedIndexedSeqView[A] { override def reverse: IndexedSeqView[A] = underlying match { case x: IndexedSeqView[A] => x @@ -107,7 +108,7 @@ private[mutable] object CheckedIndexedSeqView { } @SerialVersionUID(3L) - class Slice[A](underlying: SomeIndexedSeqOps[A]^, from: Int, until: Int)(protected val mutationCount: () => Int) + class Slice[A](underlying: SomeIndexedSeqOps[A]^, from: Int, until: Int)(protected val mutationCount: () ->{cap.rd} Int) extends AbstractIndexedSeqView[A] with CheckedIndexedSeqView[A] { protected val lo = from max 0 protected val hi = (until max 0) min underlying.length diff --git a/tests/neg-custom-args/captures/bad-uses-2.scala b/tests/neg-custom-args/captures/bad-uses-2.scala index 8dd121b2b134..2b4d6eebb2f0 100644 --- a/tests/neg-custom-args/captures/bad-uses-2.scala +++ b/tests/neg-custom-args/captures/bad-uses-2.scala @@ -1,7 +1,13 @@ -import caps.use -class Test: +import caps.{use, consume} +class TestUse: @use def F = ??? // error @use val x = ??? // error @use type T // error def foo[@use T](@use c: T): Unit = ??? // OK +class TestConsume: + @consume def F = ??? // ok + @consume val x = ??? // error + @consume type T // error + def foo[@consume T](@use c: T): Unit = ??? // error + diff --git a/tests/neg-custom-args/captures/boundschecks3.check b/tests/neg-custom-args/captures/boundschecks3.check index 36e1336e8f05..035d327e3d71 100644 --- a/tests/neg-custom-args/captures/boundschecks3.check +++ b/tests/neg-custom-args/captures/boundschecks3.check @@ -1,4 +1,24 @@ --- 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 +-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/boundschecks3.scala:9:11 --------------------------------- +9 | val foo: C[Tree^] = ??? // error + | ^ + | Type argument box test.Tree^ does not conform to upper bound test.Tree in inferred type test.C[box test.Tree^] + | + | longer explanation available when compiling with `-explain` +-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/boundschecks3.scala:10:11 -------------------------------- +10 | type T = C[Tree^] // error + | ^ + | Type argument box test.Tree^ does not conform to upper bound test.Tree in inferred type test.C[box test.Tree^] + | + | longer explanation available when compiling with `-explain` +-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/boundschecks3.scala:11:11 -------------------------------- +11 | val bar: T -> T = ??? // error + | ^ + |Type argument box test.Tree^ does not conform to upper bound test.Tree in subpart test.C[box test.Tree^] of inferred type test.C[box test.Tree^] -> test.C[box test.Tree^] + | + | longer explanation available when compiling with `-explain` +-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/boundschecks3.scala:12:11 -------------------------------- +12 | val baz: C[Tree^] -> Unit = ??? // error + | ^ + |Type argument box test.Tree^ does not conform to upper bound test.Tree in subpart test.C[box test.Tree^] of inferred type test.C[box test.Tree^] -> Unit + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/boundschecks3.scala b/tests/neg-custom-args/captures/boundschecks3.scala index f5e9652c0913..72a562e3628b 100644 --- a/tests/neg-custom-args/captures/boundschecks3.scala +++ b/tests/neg-custom-args/captures/boundschecks3.scala @@ -6,8 +6,8 @@ object test { 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 + val foo: C[Tree^] = ??? // error + type T = C[Tree^] // error + val bar: T -> T = ??? // error + val baz: C[Tree^] -> Unit = ??? // error } diff --git a/tests/neg-custom-args/captures/box-adapt-cases.check b/tests/neg-custom-args/captures/box-adapt-cases.check index 7ff185c499a5..e5cadb051ac1 100644 --- a/tests/neg-custom-args/captures/box-adapt-cases.check +++ b/tests/neg-custom-args/captures/box-adapt-cases.check @@ -1,12 +1,19 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:14:10 ------------------------------ -14 | x.value(cap => cap.use()) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:8:10 ------------------------------- +8 | 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:15:10 ------------------------------ +15 | 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 +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:29:10 ------------------------------ +29 | x.value(cap => cap.use()) // error | ^^^^^^^^^^^^^^^^ | Found: (cap: box Cap^?) ->{io, fs} Int | Required: (cap: box Cap^{io, fs}) ->{io} Int diff --git a/tests/neg-custom-args/captures/box-adapt-cases.scala b/tests/neg-custom-args/captures/box-adapt-cases.scala index 8f7d7a0a6667..7db58318ed05 100644 --- a/tests/neg-custom-args/captures/box-adapt-cases.scala +++ b/tests/neg-custom-args/captures/box-adapt-cases.scala @@ -1,10 +1,11 @@ + trait Cap { def use(): Int } def test1(): Unit = { class Id[X](val value: [T] -> (op: X => T) -> T) val x: Id[Cap^] = ??? - x.value(cap => cap.use()) + x.value(cap => cap.use()) // error, was OK } def test2(io: Cap^): Unit = { diff --git a/tests/neg-custom-args/captures/box-adapt-contra.check b/tests/neg-custom-args/captures/box-adapt-contra.check new file mode 100644 index 000000000000..fd36f23aa999 --- /dev/null +++ b/tests/neg-custom-args/captures/box-adapt-contra.check @@ -0,0 +1,15 @@ +-- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:9:52 --------------------------------------------------- +9 | val f: (Cap^{c} -> Unit) -> Unit = useCap[Cap^{c}](c) // error + | ^^^^^^^^^^^^^^^^^^ + | Cap^{c} -> Unit cannot be box-converted to box Cap^{c} ->{c} Unit + | since the additional capture set {c} resulting from box conversion is not allowed in box Cap^{c} -> Unit +-- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:13:57 -------------------------------------------------- +13 | val f1: (Cap^{c} => Unit) ->{c} Unit = useCap1[Cap^{c}](c) // error, was ok when cap was a root + | ^^^^^^^^^^^^^^^^^^^ + | Cap^{c} => Unit cannot be box-converted to box Cap^{c} ->{cap, c} Unit + | since the additional capture set {c} resulting from box conversion is not allowed in box Cap^{c} => Unit +-- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:19:54 -------------------------------------------------- +19 | val f3: (Cap^{c} -> Unit) => Unit = useCap3[Cap^{c}](c) // error + | ^^^^^^^^^^^^^^^^^^^ + | Cap^{c} -> Unit cannot be box-converted to box Cap^{c} ->{d, c} Unit + | since the additional capture set {c} resulting from box conversion is not allowed in box Cap^{c} ->{d} Unit diff --git a/tests/neg-custom-args/captures/box-adapt-contra.scala b/tests/neg-custom-args/captures/box-adapt-contra.scala index 2dc79a66d932..9c18a9a2c50a 100644 --- a/tests/neg-custom-args/captures/box-adapt-contra.scala +++ b/tests/neg-custom-args/captures/box-adapt-contra.scala @@ -1,4 +1,5 @@ -import language.experimental.captureChecking + +import caps.consume trait Cap @@ -7,9 +8,9 @@ def useCap[X](x: X): (X -> Unit) -> Unit = ??? def test1(c: Cap^): Unit = val f: (Cap^{c} -> Unit) -> Unit = useCap[Cap^{c}](c) // error -def test2(c: Cap^, d: Cap^): Unit = +def test2(@consume c: Cap^, d: Cap^): Unit = def useCap1[X](x: X): (X => Unit) -> Unit = ??? - val f1: (Cap^{c} => Unit) ->{c} Unit = useCap1[Cap^{c}](c) // ok + val f1: (Cap^{c} => Unit) ->{c} Unit = useCap1[Cap^{c}](c) // error, was ok when cap was a root def useCap2[X](x: X): (X ->{c} Unit) -> Unit = ??? val f2: (Cap^{c} -> Unit) ->{c} Unit = useCap2[Cap^{c}](c) // ok diff --git a/tests/neg-custom-args/captures/box-adapt-cs.scala b/tests/neg-custom-args/captures/box-adapt-cs.scala index a2a9232fb264..13b81dbd9b5d 100644 --- a/tests/neg-custom-args/captures/box-adapt-cs.scala +++ b/tests/neg-custom-args/captures/box-adapt-cs.scala @@ -5,7 +5,10 @@ def test1(io: Cap^): Unit = { val x: Id[Cap^{io}] = ??? val f: (Cap^) -> Unit = ??? - x(f) // ok + x(f) // error + + val g: (Cap^{io}) -> Unit = ??? + x(g) // ok } def test2(io: Cap^): Unit = { diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index 1c113591922d..0e1a016442ed 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -1,17 +1,17 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:10:6 ---------------------------------------- 10 | h(f2()) // error | ^^^^ - | Found: (x$0: Int) ->{cap1} Int - | Required: (x$0: Int) ->? Int + | Found: Int ->{cap1} Int + | Required: Int ->? Int | | longer explanation available when compiling with `-explain` -- 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/capt-depfun.check b/tests/neg-custom-args/captures/capt-depfun.check new file mode 100644 index 000000000000..7cd838d72dc0 --- /dev/null +++ b/tests/neg-custom-args/captures/capt-depfun.check @@ -0,0 +1,12 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt-depfun.scala:11:43 ---------------------------------- +11 | val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error // error: separatioon + | ^^^^^^^ + | Found: Str^{} ->{ac, y, z} Str^{y, z} + | Required: Str^{y, z} ->{fresh} Str^{y, z} + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/capt-depfun.scala:11:24 ------------------------------------------------------- +11 | val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error // error: separatioon + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Separation failure: value dc's type Str^{y, z} => Str^{y, z} hides parameters y and z. + | The parameters need to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/capt-depfun.scala b/tests/neg-custom-args/captures/capt-depfun.scala index 20226b239198..384c403bdd27 100644 --- a/tests/neg-custom-args/captures/capt-depfun.scala +++ b/tests/neg-custom-args/captures/capt-depfun.scala @@ -1,4 +1,6 @@ import annotation.retains + + class C type Cap = C @retains(caps.cap) class Str @@ -6,4 +8,4 @@ class Str def f(y: Cap, z: Cap) = def g(): C @retains(y, z) = ??? val ac: ((x: Cap) => Str @retains(x) => Str @retains(x)) = ??? - val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error + val dc: ((Str^{y, z}) => Str^{y, z}) = ac(g()) // error // error: separatioon diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index acf8faa7a969..804e18072752 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -1,50 +1,68 @@ --- Error: tests/neg-custom-args/captures/capt1.scala:6:11 -------------------------------------------------------------- -6 | () => if x == null then y else y // error +-- Error: tests/neg-custom-args/captures/capt1.scala:5:11 -------------------------------------------------------------- +5 | () => if x == null then y else y // error | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> C --- Error: tests/neg-custom-args/captures/capt1.scala:9:11 -------------------------------------------------------------- -9 | () => if x == null then y else y // error +-- Error: tests/neg-custom-args/captures/capt1.scala:8:11 -------------------------------------------------------------- +8 | () => if x == null then y else y // error | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type Matchable --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:16:2 ----------------------------------------- -16 | def f(y: Int) = if x == null then y else y // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:15:2 ----------------------------------------- +15 | def f(y: Int) = if x == null then y else y // error | ^ | Found: (y: Int) ->{x} Int | Required: Matchable -17 | f +16 | f | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:23:2 ----------------------------------------- -23 | class F(y: Int) extends A: // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:22:2 ----------------------------------------- +22 | class F(y: Int) extends A: // error | ^ | Found: A^{x} | Required: A -24 | def m() = if x == null then y else y -25 | F(22) +23 | def m() = if x == null then y else y +24 | F(22) | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:28:2 ----------------------------------------- -28 | new A: // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:27:2 ----------------------------------------- +27 | new A: // error | ^ | Found: A^{x} | Required: A -29 | def m() = if x == null then y else y +28 | def m() = if x == null then y else y | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/capt1.scala:34:16 ------------------------------------------------------------- -34 | val z2 = h[() -> Cap](() => x) // error // error +-- Error: tests/neg-custom-args/captures/capt1.scala:36:16 ------------------------------------------------------------- +36 | val z2 = h[() -> Cap](() => x) // error // error | ^^^^^^^^^ - | 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 + | 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`. +-- Error: tests/neg-custom-args/captures/capt1.scala:36:30 ------------------------------------------------------------- +36 | 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 () -> (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 + | reference (x : C^) is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> box C^ +-- Error: tests/neg-custom-args/captures/capt1.scala:38:13 ------------------------------------------------------------- +38 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error | ^^^^^^^^^^^^^^^^^^^^^^^ - | 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`. + | Type variable X of method h cannot be instantiated to box () ->{x} C^ since + | the part C^ of that type captures the root capability `cap`. +-- Error: tests/neg-custom-args/captures/capt1.scala:43:7 -------------------------------------------------------------- +43 | if x == null then // error: separation + | ^ + | Separation failure: Illegal access to {x} which is hidden by the previous definition + | of value z1 with type () => C^. + | This type hides capabilities {x} +-- Error: tests/neg-custom-args/captures/capt1.scala:44:12 ------------------------------------------------------------- +44 | () => x // error: separation + | ^ + | Separation failure: Illegal access to {x} which is hidden by the previous definition + | of value z1 with type () => C^. + | This type hides capabilities {x} +-- Error: tests/neg-custom-args/captures/capt1.scala:47:2 -------------------------------------------------------------- +47 | x // error: separation + | ^ + | Separation failure: Illegal access to {x} which is hidden by the previous definition + | of value z1 with type () => C^. + | This type hides capabilities {x} diff --git a/tests/neg-custom-args/captures/capt1.scala b/tests/neg-custom-args/captures/capt1.scala index 8da7e633ca51..48778dbd6716 100644 --- a/tests/neg-custom-args/captures/capt1.scala +++ b/tests/neg-custom-args/captures/capt1.scala @@ -1,5 +1,4 @@ - import annotation.retains class C def f(x: C @retains(caps.cap), y: C): () -> C = @@ -28,10 +27,21 @@ def h4(x: Cap, y: Int): A = new A: // error def m() = if x == null then y else y +def f1(c: Cap): () ->{c} c.type = () => c // ok + def foo() = val x: C @retains(caps.cap) = ??? def h[X](a: X)(b: X) = a + val z2 = h[() -> Cap](() => x) // error // error (() => C()) val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error + val z1: () => Cap = f1(x) + + val z4 = + if x == null then // error: separation + () => x // error: separation + else + () => C() + x // error: separation diff --git a/tests/neg-custom-args/captures/caseclass/Test_2.scala b/tests/neg-custom-args/captures/caseclass/Test_2.scala index e54ab1774202..8c13a0d831ef 100644 --- a/tests/neg-custom-args/captures/caseclass/Test_2.scala +++ b/tests/neg-custom-args/captures/caseclass/Test_2.scala @@ -5,7 +5,7 @@ def test(c: C) = val mixed: () ->{c} Unit = pure val x = Ref(impure) val _: Ref = x // error - val y = x.copy() + val y = caps.unsafe.unsafeAssumeSeparate(x.copy()) // TODO remove val yc: Ref = y // error val y0 = x.copy(pure) val yc0: Ref = y0 diff --git a/tests/neg-custom-args/captures/cc-ex-conformance.check b/tests/neg-custom-args/captures/cc-ex-conformance.check new file mode 100644 index 000000000000..1840fb478689 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-ex-conformance.check @@ -0,0 +1,69 @@ +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:12:14 ------------------------------------------------- +12 |type EX1 = () => (c: Exists) => (C^{c}, C^{c}) // error: illegal capture ref + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:14:14 ------------------------------------------------- +14 |type EX2 = () => (c1: Exists) => (c2: Exists) => (C^{c1}, C^{c2}) // error: illegal capture ref + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Illegal capture reference: c2.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:16:14 ------------------------------------------------- +16 |type EX3 = () => (c: Exists) => (x: Object^) => C^{c} // error: illegal capture ref + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:18:14 ------------------------------------------------- +18 |type EX4 = () => (x: Object^) => (c: Exists) => C^{c} // error: illegal capture ref + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:21:11 ------------------------------------------------- +21 | val ex1: EX1 = ??? // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:22:11 ------------------------------------------------- +22 | val ex2: EX2 = ??? // error: illegal capture ref + | ^^^ + | Illegal capture reference: c2.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:23:9 -------------------------------------------------- +23 | val _: EX1 = ex1 // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:24:9 -------------------------------------------------- +24 | val _: EX2 = ex1 // error separation // error: illegal capture ref + | ^^^ + | Illegal capture reference: c2.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:25:9 -------------------------------------------------- +25 | val _: EX1 = ex2 // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:27:11 ------------------------------------------------- +27 | val ex3: EX3 = ??? // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:28:11 ------------------------------------------------- +28 | val ex4: EX4 = ??? // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:29:9 -------------------------------------------------- +29 | val _: EX4 = ex3 // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:30:9 -------------------------------------------------- +30 | val _: EX4 = ex4 // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:31:9 -------------------------------------------------- +31 | val _: EX3 = ex4 // error: type mismatch // error: illegal capture ref + | ^^^ + | Illegal capture reference: c.type +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:31:15 ---------------------------- +31 | val _: EX3 = ex4 // error: type mismatch // error: illegal capture ref + | ^^^ + | Found: () ->{ex4} (x: Object^) ->{ex4*} (ex$18: caps.Exists) -> (c: caps.Exists) ->{ex$18} C + | Required: () ->{fresh} (ex$27: caps.Exists) -> (c: caps.Exists) ->{ex$27} (x: Object^) ->{fresh} C + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/cc-ex-conformance.scala:24:15 ------------------------------------------------- +24 | val _: EX2 = ex1 // error separation // error: illegal capture ref + | ^^^ + | Separation failure: Illegal access to {ex1} which is hidden by the previous definition + | of value _$1 with type () => (ex$10: caps.Exists) -> (c: caps.Exists) ->{ex$10} (C, C). + | This type hides capabilities {ex1} diff --git a/tests/neg-custom-args/captures/cc-ex-conformance.scala b/tests/neg-custom-args/captures/cc-ex-conformance.scala deleted file mode 100644 index 16e13376c5b3..000000000000 --- a/tests/neg-custom-args/captures/cc-ex-conformance.scala +++ /dev/null @@ -1,25 +0,0 @@ -import language.experimental.captureChecking -import caps.{Exists, Capability} - -class C - -type EX1 = () => (c: Exists) => (C^{c}, C^{c}) - -type EX2 = () => (c1: Exists) => (c2: Exists) => (C^{c1}, C^{c2}) - -type EX3 = () => (c: Exists) => (x: Object^) => C^{c} - -type EX4 = () => (x: Object^) => (c: Exists) => C^{c} - -def Test = - val ex1: EX1 = ??? - val ex2: EX2 = ??? - val _: EX1 = ex1 - val _: EX2 = ex1 // ok - val _: EX1 = ex2 // ok - - val ex3: EX3 = ??? - val ex4: EX4 = ??? - val _: EX4 = ex3 // ok - val _: EX4 = ex4 // error (???) Probably since we also introduce existentials on expansion - val _: EX3 = ex4 // error diff --git a/tests/neg-custom-args/captures/cc-existential-conformance.check b/tests/neg-custom-args/captures/cc-existential-conformance.check new file mode 100644 index 000000000000..bc94b97fc327 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-existential-conformance.check @@ -0,0 +1,40 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-existential-conformance.scala:8:24 -------------------- +8 | val y: A -> Fun[B^] = x // error + | ^ + | Found: (x : A -> (x²: A) -> B^{localcap}) + | Required: A -> A -> B^{fresh} + | + | where: x is a value in method test + | x² is a reference to a value parameter + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-existential-conformance.scala:9:29 -------------------- +9 | val z: A -> (x: A) -> B^ = y // error + | ^ + | Found: (y : A -> A -> B^) + | Required: A -> (x: A) -> B^ + | + | Note that the existential capture root in B^ + | cannot subsume the capability cap + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-existential-conformance.scala:13:19 ------------------- +13 | val y: Fun[B^] = x // error + | ^ + | Found: (x : (x²: A) -> B^{localcap}) + | Required: A -> B^{fresh} + | + | where: x is a value in method test2 + | x² is a reference to a value parameter + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-existential-conformance.scala:14:24 ------------------- +14 | val z: (x: A) -> B^ = y // error + | ^ + | Found: (y : A -> B^) + | Required: (x: A) -> B^ + | + | Note that the existential capture root in B^ + | cannot subsume the capability cap + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-existential-conformance.scala b/tests/neg-custom-args/captures/cc-existential-conformance.scala new file mode 100644 index 000000000000..107570cc5019 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-existential-conformance.scala @@ -0,0 +1,14 @@ +class A +class B + +type Fun[T] = A -> T + +def test() = + val x: A -> (x: A) -> B^ = ??? + val y: A -> Fun[B^] = x // error + val z: A -> (x: A) -> B^ = y // error + +def test2() = + val x: (x: A) -> B^ = ??? + val y: Fun[B^] = x // error + val z: (x: A) -> B^ = y // error diff --git a/tests/pos-custom-args/captures/cc-poly-source.scala b/tests/neg-custom-args/captures/cc-poly-source.scala similarity index 76% rename from tests/pos-custom-args/captures/cc-poly-source.scala rename to tests/neg-custom-args/captures/cc-poly-source.scala index 2de5c6d67340..e08ea36a6fc9 100644 --- a/tests/pos-custom-args/captures/cc-poly-source.scala +++ b/tests/neg-custom-args/captures/cc-poly-source.scala @@ -27,7 +27,10 @@ import caps.use def test2(@use lbls: List[Label^]) = def makeListener(lbl: Label^): Listener^{lbl} = ??? - val listeners = lbls.map(makeListener) + val listeners = lbls.map(makeListener) // error + // we get an error here because we no longer allow contravariant cap + // to subsume other capabilities. The problem can be solved by declaring + // Label a SharedCapability, see cc-poly-source-capability.scala val src = Source[CapSet^{lbls*}] for l <- listeners do src.register(l) 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..08a3efaaffdf 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) } 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/consume-overrides.scala b/tests/neg-custom-args/captures/consume-overrides.scala new file mode 100644 index 000000000000..78f013349a31 --- /dev/null +++ b/tests/neg-custom-args/captures/consume-overrides.scala @@ -0,0 +1,15 @@ +import caps.consume + +trait A[X]: + def foo(@consume x: X): X + def bar(x: X): X + +trait B extends A[C]: + def foo(x: C): C // error + def bar(@consume x: C): C // error + +trait B2: + def foo(x: C): C + def bar(@consume x: C): C + +abstract class C extends A[C], B2 // error diff --git a/tests/neg-custom-args/captures/contracap.check b/tests/neg-custom-args/captures/contracap.check new file mode 100644 index 000000000000..31b58acb80d5 --- /dev/null +++ b/tests/neg-custom-args/captures/contracap.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/contracap.scala:15:48 ------------------------------------ +15 | val g: (Ref[Int]^{a}, Ref[Int]^{a}) -> Unit = f // error + | ^ + | Found: (f : (Ref[Int]^, Ref[Int]^) -> Unit) + | Required: (Ref[Int]^{a}, Ref[Int]^{a}) -> Unit + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/contracap.scala b/tests/neg-custom-args/captures/contracap.scala new file mode 100644 index 000000000000..0ba1507eb11f --- /dev/null +++ b/tests/neg-custom-args/captures/contracap.scala @@ -0,0 +1,16 @@ +import language.experimental.captureChecking +import caps.* + +class Ref[T](init: T) extends Mutable: + private var value: T = init + def get: T = value + mut def set(newValue: T): Unit = value = newValue + +// a library function that assumes that a and b MUST BE separate +def swap[T](a: Ref[Int]^, b: Ref[Int]^): Unit = ??? + +def test2(): Unit = + val a: Ref[Int]^ = Ref(0) + val f: (Ref[Int]^, Ref[Int]^) -> Unit = swap + val g: (Ref[Int]^{a}, Ref[Int]^{a}) -> Unit = f // error + g(a, a) // OH NO \ No newline at end of file diff --git a/tests/neg-custom-args/captures/delayedRunops.check b/tests/neg-custom-args/captures/delayedRunops.check index 68da4672acf5..14ecbcffd8dd 100644 --- a/tests/neg-custom-args/captures/delayedRunops.check +++ b/tests/neg-custom-args/captures/delayedRunops.check @@ -1,14 +1,14 @@ --- Error: tests/neg-custom-args/captures/delayedRunops.scala:16:13 ----------------------------------------------------- -16 | runOps(ops1) // error +-- Error: tests/neg-custom-args/captures/delayedRunops.scala:17:13 ----------------------------------------------------- +17 | runOps(ops1) // error | ^^^^ | reference ops* is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Unit --- Error: tests/neg-custom-args/captures/delayedRunops.scala:22:13 ----------------------------------------------------- -22 | runOps(ops1) // error - | ^^^^ - | Local reach capability ops1* leaks into capture scope of enclosing function --- Error: tests/neg-custom-args/captures/delayedRunops.scala:28:13 ----------------------------------------------------- -28 | runOps(ops1) // error +-- Error: tests/neg-custom-args/captures/delayedRunops.scala:29:13 ----------------------------------------------------- +29 | runOps(ops1) // error | ^^^^ | reference ops* is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Unit +-- Error: tests/neg-custom-args/captures/delayedRunops.scala:22:16 ----------------------------------------------------- +22 | val ops1: List[() => Unit] = ops // error + | ^^^^^^^^^^^^^^^^ + | Separation failure: value ops1's type List[box () => Unit] hides non-local parameter ops diff --git a/tests/neg-custom-args/captures/delayedRunops.scala b/tests/neg-custom-args/captures/delayedRunops.scala index 191118fa19c9..946d4324ddeb 100644 --- a/tests/neg-custom-args/captures/delayedRunops.scala +++ b/tests/neg-custom-args/captures/delayedRunops.scala @@ -1,5 +1,6 @@ import language.experimental.captureChecking -import caps.use + +import caps.{use, consume} // ok def runOps(@use ops: List[() => Unit]): Unit = @@ -16,10 +17,10 @@ import caps.use runOps(ops1) // error // unsound: impure operation pretended pure - def delayedRunOps2(ops: List[() => Unit]): () ->{} Unit = + def delayedRunOps2(@consume ops: List[() => Unit]): () ->{} Unit = () => - val ops1: List[() => Unit] = ops - runOps(ops1) // error + val ops1: List[() => Unit] = ops // error + runOps(ops1) // was error // unsound: impure operation pretended pure def delayedRunOps3(ops: List[() => Unit]): () ->{} Unit = diff --git a/tests/neg-custom-args/captures/depfun-reach.check b/tests/neg-custom-args/captures/depfun-reach.check index c1d7d05dc8d6..5ffb0873d752 100644 --- a/tests/neg-custom-args/captures/depfun-reach.check +++ b/tests/neg-custom-args/captures/depfun-reach.check @@ -1,14 +1,19 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/depfun-reach.scala:13:4 ---------------------------------- -13 | op // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/depfun-reach.scala:19:4 ---------------------------------- +19 | op // error | ^^ - | Found: (xs: List[(X, box () ->{io} Unit)]) ->{op} List[box () ->{xs*} Unit] + | Found: (op : (xs: List[(X, box () ->{io} Unit)]) => List[box () ->{xs*} Unit]) | Required: (xs: List[(X, box () ->{io} Unit)]) => 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 +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/depfun-reach.scala:26:60 --------------------------------- +26 | 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] + | Found: (a : (xs: List[box () ->{io} Unit]) => List[box () ->{xs*} Unit]) + | Required: (xs: List[box () ->{io} Unit]) => List[() -> Unit] | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/depfun-reach.scala:18:17 ------------------------------------------------------ +18 | : (xs: List[(X, () ->{io} Unit)]) => List[() ->{} Unit] = // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + |Separation failure: method foo's result type (xs: List[(X, box () ->{io} Unit)]) => List[() -> Unit] hides parameter op. + |The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/depfun-reach.scala b/tests/neg-custom-args/captures/depfun-reach.scala index 94b10f7dbcdb..5e8c298df637 100644 --- a/tests/neg-custom-args/captures/depfun-reach.scala +++ b/tests/neg-custom-args/captures/depfun-reach.scala @@ -1,6 +1,12 @@ import language.experimental.captureChecking import caps.cap +trait List[+T]: + def foreach(op: T => Unit): Unit = ??? + +object List: + def apply[T](elem: T): List[T] = ??? + def test(io: Object^, async: Object^) = def compose(op: List[(() ->{cap} Unit, () ->{cap} Unit)]): List[() ->{op*} Unit] = List(() => op.foreach((f,g) => { f(); g() })) @@ -9,7 +15,7 @@ def test(io: Object^, async: Object^) = compose(op) def foo[X](op: (xs: List[(X, () ->{io} Unit)]) => List[() ->{xs*} Unit]) - : (xs: List[(X, () ->{io} Unit)]) => List[() ->{} Unit] = + : (xs: List[(X, () ->{io} Unit)]) => List[() ->{} Unit] = // error op // error def boom(op: List[(() ->{async} Unit, () ->{io} Unit)]): List[() ->{} Unit] = diff --git a/tests/neg-custom-args/captures/effect-swaps-explicit.scala b/tests/neg-custom-args/captures/effect-swaps-explicit.scala index e440271ccf88..b3756056abbd 100644 --- a/tests/neg-custom-args/captures/effect-swaps-explicit.scala +++ b/tests/neg-custom-args/captures/effect-swaps-explicit.scala @@ -14,7 +14,7 @@ end boundary import boundary.{Label, break} -trait Async extends caps.Capability +trait Async extends caps.SharedCapability object Async: def blocking[T](body: Async ?=> T): T = ??? diff --git a/tests/neg-custom-args/captures/effect-swaps.check b/tests/neg-custom-args/captures/effect-swaps.check index b74c165fd6b6..28611959d905 100644 --- a/tests/neg-custom-args/captures/effect-swaps.check +++ b/tests/neg-custom-args/captures/effect-swaps.check @@ -1,29 +1,29 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:62:8 ---------------------------------- -61 | Result: -62 | Future: // error, type mismatch +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:64:8 ---------------------------------- +63 | Result: +64 | Future: // error, type mismatch | ^ | Found: Result.Ok[box Future[box T^?]^{fr, contextual$1}] | Required: Result[Future[T], Nothing] -63 | fr.await.ok +65 | fr.await.ok |-------------------------------------------------------------------------------------------------------------------- |Inline stack trace |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |This location contains code that was inlined from effect-swaps.scala:39 -39 | boundary(Ok(body)) + |This location contains code that was inlined from effect-swaps.scala:41 +41 | boundary(Ok(body)) | ^^^^^^^^ -------------------------------------------------------------------------------------------------------------------- | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:72:10 --------------------------------- -72 | Future: fut ?=> // error: type mismatch +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:74:10 --------------------------------- +74 | Future: fut ?=> // error: type mismatch | ^ | Found: Future[box T^?]^{fr, lbl} | Required: Future[box T^?]^? -73 | fr.await.ok +75 | fr.await.ok | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/effect-swaps.scala:66:15 ------------------------------------------------------ -66 | Result.make: // error: local reference leaks +-- Error: tests/neg-custom-args/captures/effect-swaps.scala:68:15 ------------------------------------------------------ +68 | 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/effect-swaps.scala b/tests/neg-custom-args/captures/effect-swaps.scala index 99c781b963c5..3f0cc25fbb25 100644 --- a/tests/neg-custom-args/captures/effect-swaps.scala +++ b/tests/neg-custom-args/captures/effect-swaps.scala @@ -1,3 +1,5 @@ + + object boundary: final class Label[-T] extends caps.Capability @@ -12,7 +14,7 @@ end boundary import boundary.{Label, break} -trait Async extends caps.Capability +trait Async extends caps.SharedCapability object Async: def blocking[T](body: Async ?=> T): T = ??? diff --git a/tests/neg-custom-args/captures/erased-methods2.check b/tests/neg-custom-args/captures/erased-methods2.check new file mode 100644 index 000000000000..832d9a6c4a10 --- /dev/null +++ b/tests/neg-custom-args/captures/erased-methods2.check @@ -0,0 +1,29 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/erased-methods2.scala:21:9 ------------------------------- +21 | ?=> (x$2: CT[Ex2]^) // error + | ^ + | Found: (erased x$2: CT[Ex2]^) ?->{x$1} Unit + | Required: (erased x$2: CT[Ex2]^) ?->? Unit + | + | Note that the existential capture root in (erased x$2: CT[Ex2]^) ?=> Unit + | cannot subsume the capability x$1.type +22 | ?=> +23 | //given (CT[Ex3]^) = x$1 +24 | Throw(new Ex3) + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/erased-methods2.scala:32:9 ------------------------------- +32 | ?=> (erased x$2: CT[Ex2]^) // error + | ^ + | Found: (erased x$2: CT[Ex2]^) ?->{x$1} (erased x$2: CT[Ex1]^) ?->{x$1} Unit + | Required: (erased x$1²: CT[Ex2]^) ?->? (erased x$2: CT[Ex1]^) ?->? Unit + | + | where: x$1 is a parameter in an anonymous function in method foo10a + | x$1² is a reference to a value parameter + | + | + | Note that the existential capture root in (erased x$1: CT[Ex2]^) ?=> (erased x$2: CT[Ex1]^) ?->{localcap} Unit + | cannot subsume the capability x$1.type +33 | ?=> (erased x$3: CT[Ex1]^) +34 | ?=> Throw(new Ex3) + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/erased-methods2.scala b/tests/neg-custom-args/captures/erased-methods2.scala new file mode 100644 index 000000000000..0b59f741323a --- /dev/null +++ b/tests/neg-custom-args/captures/erased-methods2.scala @@ -0,0 +1,34 @@ +import language.experimental.saferExceptions +import language.experimental.erasedDefinitions +import language.experimental.captureChecking + +class Ex1 extends Exception("Ex1") +class Ex2 extends Exception("Ex2") +class Ex3 extends Exception("Ex3") + +erased class CT[-E <: Exception] extends caps.Capability + +def Throw[Ex <: Exception](ex: Ex)(using CT[Ex]^): Nothing = ??? + +def foo8a(i: Int) = + (erased xx1: CT[Ex2]^) ?=> Throw(new Ex2) + +def foo9a(i: Int) + : (x$1: CT[Ex3]^) + ?=> (x$2: CT[Ex2]^) + ?=> Unit + = (x$1: CT[Ex3]^) + ?=> (x$2: CT[Ex2]^) // error + ?=> + //given (CT[Ex3]^) = x$1 + Throw(new Ex3) + +def foo10a(i: Int) + : (erased x$0: CT[Ex3]^) + ?=> (erased x$1: CT[Ex2]^) + ?=> (erased x$2: CT[Ex1]^) + ?=> Unit + = (erased x$1: CT[Ex3]^) + ?=> (erased x$2: CT[Ex2]^) // error + ?=> (erased x$3: CT[Ex1]^) + ?=> Throw(new Ex3) diff --git a/tests/neg-custom-args/captures/existential-mapping.check b/tests/neg-custom-args/captures/existential-mapping.check index 30836bc427cf..974c5c38f74c 100644 --- a/tests/neg-custom-args/captures/existential-mapping.check +++ b/tests/neg-custom-args/captures/existential-mapping.check @@ -1,88 +1,89 @@ --- Error: tests/neg-custom-args/captures/existential-mapping.scala:44:13 ----------------------------------------------- -44 | val z1: A^ => Array[C^] = ??? // error - | ^^^^^^^^^^^^^^^ - | Array[box C^] captures the root capability `cap` in invariant position +-- Error: tests/neg-custom-args/captures/existential-mapping.scala:46:10 ----------------------------------------------- +46 | val z2: (x: A^) => Array[C^] = ??? // error + | ^^^^^^^^^^^^^^^^^^^^ + | Array[box C^] captures the root capability `cap` in invariant position. + | This capability cannot be converted to an existential in the result type of a function. -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:9:25 --------------------------- 9 | val _: (x: C^) -> C = x1 // error | ^^ - | Found: (x1 : (x: C^) -> (ex$3: caps.Exists) -> C^{ex$3}) + | Found: (x1 : (x: C^) -> C^{localcap}) | Required: (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:12:20 -------------------------- 12 | val _: C^ -> C = x2 // error | ^^ - | Found: (x2 : C^ -> (ex$7: caps.Exists) -> C^{ex$7}) + | Found: (x2 : C^ -> C^{fresh}) | Required: C^ -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:15:30 -------------------------- 15 | val _: A^ -> (x: C^) -> C = x3 // error | ^^ - | Found: (x3 : A^ -> (x: C^) -> (ex$11: caps.Exists) -> C^{ex$11}) + | Found: (x3 : A^ -> (x: C^) -> C^{localcap}) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:18:25 -------------------------- 18 | val _: A^ -> C^ -> C = x4 // error | ^^ - | Found: (x4 : A^ -> C^ -> (ex$19: caps.Exists) -> C^{ex$19}) + | Found: (x4 : A^ -> C^ -> C^{fresh}) | Required: A^ -> C^ -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:21:30 -------------------------- 21 | val _: A^ -> (x: C^) -> C = x5 // error | ^^ - | Found: (x5 : A^ -> (x: C^) -> (ex$27: caps.Exists) -> C^{ex$27}) + | Found: (x5 : A^ -> (x: C^) -> C^{localcap}) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:24:30 -------------------------- 24 | val _: A^ -> (x: C^) => C = x6 // error | ^^ - | 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 + | Found: (x6 : A^ -> (x: C^) ->{fresh} C^{localcap}) + | Required: A^ -> (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:27:25 -------------------------- 27 | val _: (x: C^) => C = y1 // error | ^^ - | Found: (y1 : (x: C^) => (ex$41: caps.Exists) -> C^{ex$41}) - | Required: (x: C^) => C + | Found: (y1 : (x: C^) ->{fresh} C^{localcap}) + | Required: (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:30:20 -------------------------- 30 | val _: C^ => C = y2 // error | ^^ - | Found: (y2 : C^ => (ex$45: caps.Exists) -> C^{ex$45}) - | Required: C^ => C + | Found: (y2 : C^ ->{fresh} C^{fresh}) + | Required: C^ ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:33:30 -------------------------- 33 | val _: A^ => (x: C^) => C = y3 // error | ^^ - | Found: (y3 : A^ => (ex$50: caps.Exists) -> (x: C^) ->{ex$50} (ex$49: caps.Exists) -> C^{ex$49}) - | Required: A^ => (ex$53: caps.Exists) -> (x: C^) ->{ex$53} C + | Found: (y3 : A^ ->{fresh} (x: C^) ->{fresh} C^{localcap}) + | Required: A^ ->{fresh} (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:36:25 -------------------------- 36 | val _: A^ => C^ => C = y4 // error | ^^ - | Found: (y4 : A^ => (ex$56: caps.Exists) -> C^ ->{ex$56} (ex$55: caps.Exists) -> C^{ex$55}) - | Required: A^ => (ex$59: caps.Exists) -> C^ ->{ex$59} C + | Found: (y4 : A^ ->{fresh} C^ ->{fresh} C^{fresh}) + | Required: A^ ->{fresh} C^ ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:39:30 -------------------------- 39 | val _: A^ => (x: C^) -> C = y5 // error | ^^ - | Found: (y5 : A^ => (x: C^) -> (ex$61: caps.Exists) -> C^{ex$61}) - | Required: A^ => (x: C^) -> C + | Found: (y5 : A^ ->{fresh} (x: C^) -> C^{localcap}) + | Required: A^ ->{fresh} (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:42:30 -------------------------- 42 | val _: A^ => (x: C^) => C = y6 // error | ^^ - | Found: (y6 : A^ => (ex$70: caps.Exists) -> (x: C^) ->{ex$70} (ex$69: caps.Exists) -> C^{ex$69}) - | Required: A^ => (ex$73: caps.Exists) -> (x: C^) ->{ex$73} C + | Found: (y6 : A^ ->{fresh} (x: C^) ->{fresh} C^{localcap}) + | Required: A^ ->{fresh} (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/existential-mapping.scala b/tests/neg-custom-args/captures/existential-mapping.scala index 290f7dc767a6..3543ef27037f 100644 --- a/tests/neg-custom-args/captures/existential-mapping.scala +++ b/tests/neg-custom-args/captures/existential-mapping.scala @@ -41,6 +41,7 @@ def Test = val y6: A^ => IFun[C^] = ??? val _: A^ => (x: C^) => C = y6 // error - val z1: A^ => Array[C^] = ??? // error + val z1: A^ => Array[C^] = ??? // ok + val z2: (x: A^) => Array[C^] = ??? // error 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..f48736984a36 --- /dev/null +++ b/tests/neg-custom-args/captures/filevar-expanded.check @@ -0,0 +1,14 @@ +-- Error: tests/neg-custom-args/captures/filevar-expanded.scala:34:13 -------------------------------------------------- +34 | withFile(io3): f => // error: separation failure + | ^^^ + | Separation failure: argument of type (io3 : test2.IO^) + | to method withFile: [T](io2: test2.IO^)(op: (f: test2.File^{io2}) => T): T + | corresponds to capture-polymorphic formal parameter io2 of type test2.IO^ + | and hides capabilities {io3}. + | Some of these overlap with the captures of the second argument with type (f: test2.File^{io3}) ->{io3} Unit. + | + | Hidden set of current argument : {io3} + | Hidden footprint of current argument : {io3} + | Capture set of second argument : {io3} + | Footprint set of second argument : {io3} + | The two sets overlap at : {io3} 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/filevar.check b/tests/neg-custom-args/captures/filevar.check new file mode 100644 index 000000000000..22efd36053b4 --- /dev/null +++ b/tests/neg-custom-args/captures/filevar.check @@ -0,0 +1,9 @@ +-- Error: tests/neg-custom-args/captures/filevar.scala:8:6 ------------------------------------------------------------- +8 | var file: File^ = uninitialized // error, was OK under unsealed + | ^ + | Mutable variable file cannot have type File^ since + | that type captures the root capability `cap`. +-- Warning: tests/neg-custom-args/captures/filevar.scala:11:55 --------------------------------------------------------- +11 |def withFile[T](op: (l: caps.Capability) ?-> (f: File^{l}) => T): T = + | ^ + | redundant capture: File already accounts for l.type diff --git a/tests/neg-custom-args/captures/heal-tparam-cs.check b/tests/neg-custom-args/captures/heal-tparam-cs.check new file mode 100644 index 000000000000..6367452db7f7 --- /dev/null +++ b/tests/neg-custom-args/captures/heal-tparam-cs.check @@ -0,0 +1,39 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/heal-tparam-cs.scala:15:13 ------------------------------- +15 | localCap { c => // error + | ^ + | Found: (x$0: Capp^) ->? () ->{x$0} Unit + | Required: (c: Capp^) -> () ->{localcap} Unit + | + | Note that the existential capture root in () => Unit + | cannot subsume the capability x$0.type +16 | (c1: Capp^) => () => { c1.use() } +17 | } + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/heal-tparam-cs.scala:25:13 ------------------------------- +25 | localCap { c => // error + | ^ + | Found: (x$0: Capp^{io}) ->? () ->{x$0} Unit + | Required: (c: Capp^{io}) -> () ->{net} Unit +26 | (c1: Capp^{io}) => () => { c1.use() } +27 | } + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/heal-tparam-cs.scala:41:10 ------------------------------- +41 | io => () => io.use() // error + | ^^^^^^^^^^^^^^ + | Found: () ->{io} Unit + | Required: () ->? Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/heal-tparam-cs.scala:44:10 ------------------------------- +44 | io => () => io.use() // error + | ^^^^^^^^^^^^^^ + | Found: () ->{io} Unit + | Required: () ->? Unit + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/heal-tparam-cs.scala:10:14 ---------------------------------------------------- +10 | val test1 = localCap { c => // error + | ^^^^^^^^ + | local reference c leaks into outer capture set of type parameter T of method localCap diff --git a/tests/neg-custom-args/captures/heal-tparam-cs.scala b/tests/neg-custom-args/captures/heal-tparam-cs.scala index fde4b93e196c..6d0b838613f8 100644 --- a/tests/neg-custom-args/captures/heal-tparam-cs.scala +++ b/tests/neg-custom-args/captures/heal-tparam-cs.scala @@ -1,4 +1,5 @@ import language.experimental.captureChecking +//import language.`3.8` trait Capp { def use(): Unit } @@ -16,7 +17,7 @@ def main(io: Capp^, net: Capp^): Unit = { } val test3: (c: Capp^{io}) -> () ->{io} Unit = - localCap { c => // error + localCap { c => // ok (c1: Capp^{io}) => () => { c1.use() } } @@ -31,4 +32,14 @@ def main(io: Capp^, net: Capp^): Unit = { localCap2 { c => // ok () => { c.use() } } + } + +// Original issue from PR #16264 +def main2() = { + val f: (io: Capp^) -> () -> Unit = + io => () => io.use() // error + + val g: (Capp^) -> () -> Unit = + io => () => io.use() // error +} \ No newline at end of file diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index 67685d5663b8..b867636a64cd 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -1,29 +1,25 @@ --- Error: tests/neg-custom-args/captures/i15772.scala:19:26 ------------------------------------------------------------ -19 | val c : C^{x} = new C(x) // error +-- Error: tests/neg-custom-args/captures/i15772.scala:21:26 ------------------------------------------------------------ +21 | val c : C^{x} = new C(x) // error | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Int --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:20:46 --------------------------------------- -20 | val boxed1 : ((C^) => Unit) -> Unit = box1(c) // error +-- Error: tests/neg-custom-args/captures/i15772.scala:22:46 ------------------------------------------------------------ +22 | val boxed1 : ((C^) => Unit) -> Unit = box1(c) // error | ^^^^^^^ - | Found: (C{val arg: C^}^{c} => Unit) ->{c} Unit - | Required: (C^ => Unit) -> Unit - | - | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/i15772.scala:26:26 ------------------------------------------------------------ -26 | val c : C^{x} = new C(x) // error + |C^ => Unit cannot be box-converted to box C{val arg: C^}^{c} ->{cap, c} Unit + |since the additional capture set {c} resulting from box conversion is not allowed in box C{val arg: C^}^{c} => Unit +-- Error: tests/neg-custom-args/captures/i15772.scala:28:26 ------------------------------------------------------------ +28 | val c : C^{x} = new C(x) // error | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Int --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:27:35 --------------------------------------- -27 | val boxed2 : Observe[C^] = box2(c) // error +-- Error: tests/neg-custom-args/captures/i15772.scala:29:35 ------------------------------------------------------------ +29 | val boxed2 : Observe[C^] = box2(c) // error | ^^^^^^^ - | Found: (C{val arg: C^}^{c} => Unit) ->{c} Unit - | Required: (C^ => Unit) -> Unit - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:33:34 --------------------------------------- -33 | val boxed2 : Observe[C]^ = box2(c) // error + |C^ => Unit cannot be box-converted to box C{val arg: C^}^{c} ->{cap, c} Unit + |since the additional capture set {c} resulting from box conversion is not allowed in box C{val arg: C^}^{c} => Unit +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:35:34 --------------------------------------- +35 | val boxed2 : Observe[C]^ = box2(c) // error | ^ | Found: box C^ | Required: box C{val arg: C^?}^? @@ -32,10 +28,14 @@ | cannot be included in capture set ? | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:44:2 ---------------------------------------- -44 | x: (() -> Unit) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:46:2 ---------------------------------------- +46 | x: (() -> Unit) // error | ^ | Found: (x : () ->{filesList, sayHello} Unit) | Required: () -> Unit | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/i15772.scala:34:10 ------------------------------------------------------------ +34 | def c : C^ = new C(x) // error separation + | ^^ + | Separation failure: method c's result type C^ hides non-local parameter x diff --git a/tests/neg-custom-args/captures/i15772.scala b/tests/neg-custom-args/captures/i15772.scala index a054eac835c1..face1e8a0ff5 100644 --- a/tests/neg-custom-args/captures/i15772.scala +++ b/tests/neg-custom-args/captures/i15772.scala @@ -1,3 +1,5 @@ + + type Observe[T] = (T => Unit) -> Unit def unsafe(cap: C^) = cap.bad() @@ -29,7 +31,7 @@ def main2(x: C^) : () -> Int = 0 def main3(x: C^) = - def c : C^ = new C(x) + def c : C^ = new C(x) // error separation val boxed2 : Observe[C]^ = box2(c) // error boxed2((cap: C^) => unsafe(c)) 0 diff --git a/tests/neg-custom-args/captures/i19330-alt2.scala b/tests/neg-custom-args/captures/i19330-alt2.scala index 3e52e3c65634..017ae2c486b3 100644 --- a/tests/neg-custom-args/captures/i19330-alt2.scala +++ b/tests/neg-custom-args/captures/i19330-alt2.scala @@ -10,6 +10,6 @@ trait Foo: def foo: this.T = val leaked = usingLogger[T]: l => // error - val t: () => Logger^ = () => l - t: T + val t: () => Logger^ = () => l // error separation + t: T // error leaked diff --git a/tests/neg-custom-args/captures/i19330.check b/tests/neg-custom-args/captures/i19330.check index a8925b117611..06a074c40a10 100644 --- a/tests/neg-custom-args/captures/i19330.check +++ b/tests/neg-custom-args/captures/i19330.check @@ -3,3 +3,22 @@ | ^^^ | 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:17:4 ---------------------------------------- +17 | t: x.T // error + | ^ + | Found: () ->{t} Logger^{t*} + | Required: x.T + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i19330.scala:22:22 --------------------------------------- +22 | val bad: bar.T = foo(bar) // error + | ^^^^^^^^ + | Found: bar.T + | Required: () => Logger^ + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/i19330.scala:16:14 ------------------------------------------------------------ +16 | val t: () => Logger^ = () => l // error + | ^^^^^^^^^^^^^ + | Separation failure: value t's type () => Logger^ hides parameter l. + | The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/i19330.scala b/tests/neg-custom-args/captures/i19330.scala index 715b670860cd..cdc0367bc7d3 100644 --- a/tests/neg-custom-args/captures/i19330.scala +++ b/tests/neg-custom-args/captures/i19330.scala @@ -1,7 +1,7 @@ - import language.experimental.captureChecking + trait Logger def usingLogger[T](op: Logger^ => T): T = ??? @@ -13,11 +13,11 @@ class Bar extends Foo: def foo(x: Foo): x.T = val leaked = usingLogger[x.T]: l => // error - val t: () => Logger^ = () => l - t: x.T + val t: () => Logger^ = () => l // error + t: x.T // error leaked 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 e7483e10bfa6..cb1400ebc420 100644 --- a/tests/neg-custom-args/captures/i21401.check +++ b/tests/neg-custom-args/captures/i21401.check @@ -21,9 +21,5 @@ -- 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$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 - | ^^^^^^^ - | Local reach capability x* leaks into capture scope of method test2 + | Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> Boxed[box IO^] since + | the part box IO^ of that type captures the root capability `cap`. diff --git a/tests/neg-custom-args/captures/i21401.scala b/tests/neg-custom-args/captures/i21401.scala index 0b5479376a0a..f6071e2a47d5 100644 --- a/tests/neg-custom-args/captures/i21401.scala +++ b/tests/neg-custom-args/captures/i21401.scala @@ -15,5 +15,5 @@ def test2() = val a = usingIO[IO^](x => x) // error val leaked: [R, X <: Boxed[IO^] -> R] -> (op: X) -> R = usingIO[Res](mkRes) // error val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) // error // error - val y: IO^{x*} = x.unbox // error + val y: IO^{x*} = x.unbox // was error y.println("boom") diff --git a/tests/neg-custom-args/captures/i21442.check b/tests/neg-custom-args/captures/i21442.check index 30becfea0215..66bc0727f412 100644 --- a/tests/neg-custom-args/captures/i21442.check +++ b/tests/neg-custom-args/captures/i21442.check @@ -1,9 +1,10 @@ --- Error: tests/neg-custom-args/captures/i21442.scala:9:13 ------------------------------------------------------------- -9 | val io = x.unbox // error: local reach capability {x*} leaks - | ^^^^^^^ - | Local reach capability x* leaks into capture scope of method foo. - | To allow this, the parameter x should be declared with a @use annotation --- Error: tests/neg-custom-args/captures/i21442.scala:17:14 ------------------------------------------------------------ -17 | val io = x1.unbox // error - | ^^^^^^^^ - | Local reach capability x1* leaks into capture scope of method bar +-- Error: tests/neg-custom-args/captures/i21442.scala:10:13 ------------------------------------------------------------ +10 | val io = x.unbox // error: local reach capability {x*} leaks + | ^^^^^^^ + | Local reach capability x.unbox* leaks into capture scope of method foo. + | To allow this, the parameter x should be declared with a @use annotation +-- Error: tests/neg-custom-args/captures/i21442.scala:17:10 ------------------------------------------------------------ +17 | val x1: Boxed[IO^] = x // error + | ^^^^^^^^^^ + | Separation failure: value x1's type Boxed[box IO^] hides parameter x. + | The parameter needs to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/i21442.scala b/tests/neg-custom-args/captures/i21442.scala index c9fa7d152fae..16d32c5218cb 100644 --- a/tests/neg-custom-args/captures/i21442.scala +++ b/tests/neg-custom-args/captures/i21442.scala @@ -1,4 +1,5 @@ import language.experimental.captureChecking + trait IO: def use(): Unit case class Boxed[+T](unbox: T) @@ -13,6 +14,6 @@ def foo(x: Boxed[IO^]): Unit = // slightly different way. // But, no type error reported. def bar(x: Boxed[IO^]): Unit = - val x1: Boxed[IO^] = x - val io = x1.unbox // error + val x1: Boxed[IO^] = x // error + val io = x1.unbox // was error io.use() diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index f4967253455f..382768b73bc6 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*} box Logger{val f: File^{_$1}}^{localcap.rd, _$1} + | Required: (_$1: box File^{files*}) ->{fresh} box Logger{val f: File^?}^? | - | Note that the universal capability `cap` - | cannot be included in capture set ? + | Note that reference .rd + | cannot be included in outer capture set ? | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i21920.check b/tests/neg-custom-args/captures/i21920.check index 8efa24426d01..b022d71b8418 100644 --- a/tests/neg-custom-args/captures/i21920.check +++ b/tests/neg-custom-args/captures/i21920.check @@ -1,7 +1,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21920.scala:34:34 --------------------------------------- 34 | val cell: Cell[File] = File.open(f => Cell(Seq(f))) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: Cell[box File^{f, f²}]{val head: () ?->? IterableOnce[box File^{f, f²}]^?}^? + | Found: Cell[box File^{f, f²}]{val head: () ?->? IterableOnce[box File^{f²}]^?}^? | Required: Cell[File] | | where: f is a reference to a value parameter diff --git a/tests/neg-custom-args/captures/i22005.scala b/tests/neg-custom-args/captures/i22005.scala index a9dca999e42b..689246d6f835 100644 --- a/tests/neg-custom-args/captures/i22005.scala +++ b/tests/neg-custom-args/captures/i22005.scala @@ -1,3 +1,4 @@ + import caps.* class IO @@ -5,4 +6,4 @@ class File(io: IO^) class Handler[C^]: def f(file: File^): File^{C^} = file // error - def g(file: File^{C^}): File^ = file // ok + def g(@consume file: File^{C^}): File^ = file // ok 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..8db8791d1123 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -1,28 +1,70 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:19:28 -------------------------------------- -19 | val ref1c: LazyRef[Int] = ref1 // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:20:28 -------------------------------------- +20 | val ref1c: LazyRef[Int] = ref1 // error | ^^^^ | Found: (ref1 : LazyRef[Int]{val elem: () ->{cap1} Int}^{cap1}) | Required: LazyRef[Int] | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:21:35 -------------------------------------- -21 | val ref2c: LazyRef[Int]^{cap2} = ref2 // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:22:35 -------------------------------------- +22 | val ref2c: LazyRef[Int]^{cap2} = ref2 // error | ^^^^ | Found: LazyRef[Int]{val elem: () ->{ref2*} Int}^{ref2} | Required: LazyRef[Int]^{cap2} | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:23:35 -------------------------------------- -23 | val ref3c: LazyRef[Int]^{ref1} = ref3 // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:24:35 -------------------------------------- +24 | val ref3c: LazyRef[Int]^{ref1} = ref3 // error | ^^^^ | Found: LazyRef[Int]{val elem: () ->{ref3*} Int}^{ref3} | Required: LazyRef[Int]^{ref1} | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:25:35 -------------------------------------- -25 | val ref4c: LazyRef[Int]^{cap1} = ref4 // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:30:35 -------------------------------------- +30 | val ref4c: LazyRef[Int]^{cap1} = ref4 // error | ^^^^ | Found: LazyRef[Int]{val elem: () ->{ref4*} Int}^{ref4} | Required: LazyRef[Int]^{cap1} | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/lazyref.scala:8:24 ------------------------------------------------------------ +8 | new LazyRef(() => f(elem())) // error: separation failure + | ^^^^ + | Separation failure: Illegal access to {LazyRef.this.elem} which is hidden by the previous definition + | of value get with type () => T. + | This type hides capabilities {LazyRef.this.elem} +-- Error: tests/neg-custom-args/captures/lazyref.scala:23:13 ----------------------------------------------------------- +23 | val ref3 = ref1.map(g) // error: separation failure + | ^^^^ + | Separation failure: Illegal access to {cap1} which is hidden by the previous definition + | of value ref2 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {cap1} +-- Error: tests/neg-custom-args/captures/lazyref.scala:26:9 ------------------------------------------------------------ +26 | if cap1 == cap2 // error: separation failure // error: separation failure + | ^^^^ + | Separation failure: Illegal access to {cap1} which is hidden by the previous definition + | of value ref3 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1} +-- Error: tests/neg-custom-args/captures/lazyref.scala:26:17 ----------------------------------------------------------- +26 | if cap1 == cap2 // error: separation failure // error: separation failure + | ^^^^ + | Separation failure: Illegal access to {cap2} which is hidden by the previous definition + | of value ref3 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1} +-- Error: tests/neg-custom-args/captures/lazyref.scala:27:11 ----------------------------------------------------------- +27 | then ref1 // error: separation failure + | ^^^^ + | Separation failure: Illegal access to {cap1, ref1} which is hidden by the previous definition + | of value ref3 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1} +-- Error: tests/neg-custom-args/captures/lazyref.scala:28:11 ----------------------------------------------------------- +28 | else ref2) // error: separation failure + | ^^^^ + | Separation failure: Illegal access to {cap1, cap2, ref1} which is hidden by the previous definition + | of value ref3 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1} +-- Error: tests/neg-custom-args/captures/lazyref.scala:29:9 ------------------------------------------------------------ +29 | .map(g) // error: separation failure + | ^ + | Separation failure: Illegal access to {cap2} which is hidden by the previous definition + | of value ref3 with type LazyRef[Int]{val elem: () => Int}^{cap2, ref1}. + | This type hides capabilities {ref2*, cap1} diff --git a/tests/neg-custom-args/captures/lazyref.scala b/tests/neg-custom-args/captures/lazyref.scala index 99aa10d5d2b2..396d9470ea17 100644 --- a/tests/neg-custom-args/captures/lazyref.scala +++ b/tests/neg-custom-args/captures/lazyref.scala @@ -1,10 +1,11 @@ + class CC type Cap = CC^ class LazyRef[T](val elem: () => T): val get: () => T = elem def map[U](f: T => U): LazyRef[U]^{f, this} = - new LazyRef(() => f(elem())) + new LazyRef(() => f(elem())) // error: separation failure def map[A, B](ref: LazyRef[A]^, f: A => B): LazyRef[B]^{f, ref} = new LazyRef(() => f(ref.elem())) @@ -19,7 +20,11 @@ def test(cap1: Cap, cap2: Cap) = val ref1c: LazyRef[Int] = ref1 // error val ref2 = map(ref1, g) val ref2c: LazyRef[Int]^{cap2} = ref2 // error - val ref3 = ref1.map(g) + val ref3 = ref1.map(g) // error: separation failure val ref3c: LazyRef[Int]^{ref1} = ref3 // error - val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) + val ref4 = ( + if cap1 == cap2 // error: separation failure // error: separation failure + then ref1 // error: separation failure + else ref2) // error: separation failure + .map(g) // error: separation failure val ref4c: LazyRef[Int]^{cap1} = ref4 // error diff --git a/tests/neg-custom-args/captures/leak-problem.scala b/tests/neg-custom-args/captures/leak-problem.scala index c842280c0587..9a9534052d30 100644 --- a/tests/neg-custom-args/captures/leak-problem.scala +++ b/tests/neg-custom-args/captures/leak-problem.scala @@ -25,7 +25,7 @@ def test(): Unit = def useBoxedAsync1(@use x: Box[Async^]): Unit = x.get.read() val xs: Box[Async^] = ??? - val xsLambda = () => useBoxedAsync(xs) // error + val xsLambda = () => useBoxedAsync(xs) // was error now ok val _: () ->{xs*} Unit = xsLambda val _: () -> Unit = xsLambda // error diff --git a/tests/neg-custom-args/captures/leaked-curried.check b/tests/neg-custom-args/captures/leaked-curried.check index be11aedd74ae..9199d468b55a 100644 --- a/tests/neg-custom-args/captures/leaked-curried.check +++ b/tests/neg-custom-args/captures/leaked-curried.check @@ -1,10 +1,18 @@ -- Error: tests/neg-custom-args/captures/leaked-curried.scala:14:20 ---------------------------------------------------- 14 | () => () => io // error | ^^ - | reference (io : Cap^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> () ->{io} (ex$7: caps.Exists) -> Cap^{ex$7} + | reference (io : Cap^) is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> () ->{io} Cap^ -- Error: tests/neg-custom-args/captures/leaked-curried.scala:17:20 ---------------------------------------------------- 17 | () => () => io // error | ^^ - | reference (io : Cap^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> () ->{io} (ex$15: caps.Exists) -> Cap^{ex$15} + | reference (io : Cap^) is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> () ->{io} Cap^ +-- Error: tests/neg-custom-args/captures/leaked-curried.scala:13:15 ---------------------------------------------------- +13 | val get: () ->{} () ->{io} Cap^ = // error: separation + | ^^^^^^^^^^^^^^^^^^^^^^ + | Separation failure: value get's type () -> () ->{io} Cap^ hides non-local parameter io +-- Error: tests/neg-custom-args/captures/leaked-curried.scala:16:15 ---------------------------------------------------- +16 | val get: () ->{} () ->{io} Cap^ = // error: separation + | ^^^^^^^^^^^^^^^^^^^^^^ + | Separation failure: value get's type () -> () ->{io} Cap^ hides non-local parameter io diff --git a/tests/neg-custom-args/captures/leaked-curried.scala b/tests/neg-custom-args/captures/leaked-curried.scala index d765955ee6ce..576c9d8a5db9 100644 --- a/tests/neg-custom-args/captures/leaked-curried.scala +++ b/tests/neg-custom-args/captures/leaked-curried.scala @@ -10,10 +10,11 @@ def main(): Unit = val leaked = withCap: (io: Cap^) => class Fuzz extends Box, Pure: self => - val get: () ->{} () ->{io} Cap^ = + val get: () ->{} () ->{io} Cap^ = // error: separation () => () => io // error class Foo extends Box, Pure: - val get: () ->{} () ->{io} Cap^ = + val get: () ->{} () ->{io} Cap^ = // error: separation () => () => io // error new Foo val bad = leaked.get()().use() // using a leaked capability + diff --git a/tests/neg-custom-args/captures/levels.check b/tests/neg-custom-args/captures/levels.check index b99adefd4b2f..96512055e119 100644 --- a/tests/neg-custom-args/captures/levels.check +++ b/tests/neg-custom-args/captures/levels.check @@ -7,7 +7,7 @@ 22 | r.setV(g) // error | ^ | Found: box (x: String) ->{cap3} String - | Required: box (x$0: String) ->? String + | Required: box (x: String) ->? String | | Note that reference (cap3 : CC^), defined in method scope | cannot be included in outer capture set ? of value r diff --git a/tests/neg-custom-args/captures/linear-buffer-2.check b/tests/neg-custom-args/captures/linear-buffer-2.check new file mode 100644 index 000000000000..3d64c432d116 --- /dev/null +++ b/tests/neg-custom-args/captures/linear-buffer-2.check @@ -0,0 +1,29 @@ +-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:13:13 --------------------------------------------------- +13 | val buf3 = buf.append(3) // error + | ^^^ + | Separation failure: Illegal access to {buf} which is hidden by the previous definition + | of value buf1 with type Buffer[Int]^. + | This type hides capabilities {buf} +-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:20:13 --------------------------------------------------- +20 | val buf3 = buf1.append(4) // error + | ^^^^ + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 18 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:28:13 --------------------------------------------------- +28 | val buf3 = buf1.append(4) // error + | ^^^^ + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 25 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:38:13 --------------------------------------------------- +38 | val buf3 = buf1.append(4) // error + | ^^^^ + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 33 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer-2.scala:42:4 ---------------------------------------------------- +42 | buf.append(1) // error + | ^^^ + | Separation failure: (buf : Buffer[Int]^) appears in a loop, therefore it cannot + | be passed to a @consume parameter or be used as a prefix of a @consume method call. diff --git a/tests/neg-custom-args/captures/linear-buffer-2.scala b/tests/neg-custom-args/captures/linear-buffer-2.scala new file mode 100644 index 000000000000..428171c3fab8 --- /dev/null +++ b/tests/neg-custom-args/captures/linear-buffer-2.scala @@ -0,0 +1,42 @@ +import caps.{cap, consume, Mutable} +import language.experimental.captureChecking + +class Buffer[T] extends Mutable: + @consume mut def append(x: T): Buffer[T]^ = this // ok + +def app[T](@consume buf: Buffer[T]^, elem: T): Buffer[T]^ = + buf.append(elem) + +def Test(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = buf.append(1) + val buf2 = buf1.append(2) // OK + val buf3 = buf.append(3) // error + +def Test2(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = buf.append(1) + val buf2 = + if ??? then buf1.append(2) // OK + else buf1.append(3) // OK + val buf3 = buf1.append(4) // error + +def Test3(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = buf.append(1) + val buf2 = (??? : Int) match + case 1 => buf1.append(2) // OK + case 2 => buf1.append(2) + case _ => buf1.append(3) + val buf3 = buf1.append(4) // error + +def Test4(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = buf.append(1) + val buf2 = (??? : Int) match + case 1 => buf1.append(2) // OK + case 2 => buf1.append(2) + case 3 => buf1.append(3) + case 4 => buf1.append(4) + case 5 => buf1.append(5) + val buf3 = buf1.append(4) // error + +def Test5(@consume buf: Buffer[Int]^) = + while true do + buf.append(1) // error diff --git a/tests/neg-custom-args/captures/linear-buffer.check b/tests/neg-custom-args/captures/linear-buffer.check new file mode 100644 index 000000000000..16ba3bd096a2 --- /dev/null +++ b/tests/neg-custom-args/captures/linear-buffer.check @@ -0,0 +1,44 @@ +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:5:24 ------------------------------------------------------ +5 | mut def append(x: T): BadBuffer[T]^ = this // error + | ^^^^^^^^^^^^^ + | Separation failure: method append's result type BadBuffer[T]^ hides non-local this of class class BadBuffer. + | The access must be in a @consume method to allow this. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:7:13 ------------------------------------------------------ +7 | def bar: BadBuffer[T]^ = this // error + | ^^^^^^^^^^^^^ + | Separation failure: method bar's result type BadBuffer[T]^ hides non-local this of class class BadBuffer. + | The access must be in a @consume method to allow this. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:6:9 ------------------------------------------------------- +6 | def foo = // error + | ^ + |Separation failure: method foo's inferred result type BadBuffer[box T^?]^ hides non-local this of class class BadBuffer. + |The access must be in a @consume method to allow this. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:19:17 ----------------------------------------------------- +19 | val buf3 = app(buf, 3) // error + | ^^^ + | Separation failure: Illegal access to (buf : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 17 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:26:17 ----------------------------------------------------- +26 | val buf3 = app(buf1, 4) // error + | ^^^^ + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 24 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:34:17 ----------------------------------------------------- +34 | val buf3 = app(buf1, 4) // error + | ^^^^ + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 31 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:44:17 ----------------------------------------------------- +44 | val buf3 = app(buf1, 4) // error + | ^^^^ + | Separation failure: Illegal access to (buf1 : Buffer[Int]^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 39 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/linear-buffer.scala:48:8 ------------------------------------------------------ +48 | app(buf, 1) // error + | ^^^ + | Separation failure: (buf : Buffer[Int]^) appears in a loop, therefore it cannot + | be passed to a @consume parameter or be used as a prefix of a @consume method call. diff --git a/tests/neg-custom-args/captures/linear-buffer.scala b/tests/neg-custom-args/captures/linear-buffer.scala new file mode 100644 index 000000000000..97315c1aa0fb --- /dev/null +++ b/tests/neg-custom-args/captures/linear-buffer.scala @@ -0,0 +1,48 @@ +import caps.{cap, consume, Mutable} +import language.experimental.captureChecking + +class BadBuffer[T] extends Mutable: + mut def append(x: T): BadBuffer[T]^ = this // error + def foo = // error + def bar: BadBuffer[T]^ = this // error + bar + +class Buffer[T] extends Mutable: + @consume mut def append(x: T): Buffer[T]^ = this // ok + +def app[T](@consume buf: Buffer[T]^, elem: T): Buffer[T]^ = + buf.append(elem) + +def Test(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = app(buf, 1) + val buf2 = app(buf1, 2) // OK + val buf3 = app(buf, 3) // error + +def Test2(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = app(buf, 1) + val buf2 = + if ??? then app(buf1, 2) // OK + else app(buf1, 3) // OK + val buf3 = app(buf1, 4) // error + +def Test3(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = app(buf, 1) + val buf2 = (??? : Int) match + case 1 => app(buf1, 2) // OK + case 2 => app(buf1, 2) + case _ => app(buf1, 3) + val buf3 = app(buf1, 4) // error + +def Test4(@consume buf: Buffer[Int]^) = + val buf1: Buffer[Int]^ = app(buf, 1) + val buf2 = (??? : Int) match + case 1 => app(buf1, 2) // OK + case 2 => app(buf1, 2) + case 3 => app(buf1, 3) + case 4 => app(buf1, 4) + case 5 => app(buf1, 5) + val buf3 = app(buf1, 4) // error + +def Test5(@consume buf: Buffer[Int]^) = + while true do + app(buf, 1) // 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..bfc1b5161f0a --- /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 extending 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 extending 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/non-local-consume.scala b/tests/neg-custom-args/captures/non-local-consume.scala new file mode 100644 index 000000000000..0e46146eb7e8 --- /dev/null +++ b/tests/neg-custom-args/captures/non-local-consume.scala @@ -0,0 +1,29 @@ +import caps.{cap, consume, Mutable} +import language.experimental.captureChecking + +class Buffer extends Mutable + +def f1(@consume buf: Buffer^): Buffer^ = + val buf1: Buffer^ = buf // OK + buf1 + +def f2(@consume buf: Buffer^): Buffer^ = + def g(): Buffer^ = buf // error + g() + +def f3(@consume buf: Buffer^): Buffer^ = + val buf1 = buf + def g(): Buffer^ = buf1 // error + g() + +def f4(@consume buf: Buffer^): Buffer^ = + val buf1: Buffer^ = buf + def g(): Buffer^ = buf1 // error + g() + +def f5(@consume buf: Buffer^): Unit = + val buf1: Buffer^ = buf + def g(): Unit = cc(buf1) // error + g() + +def cc(@consume buf: Buffer^): Unit = () diff --git a/tests/neg-custom-args/captures/outer-var.check b/tests/neg-custom-args/captures/outer-var.check index b24579b7a69f..a55abfaaf98d 100644 --- a/tests/neg-custom-args/captures/outer-var.check +++ b/tests/neg-custom-args/captures/outer-var.check @@ -1,5 +1,5 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:11:8 ------------------------------------- -11 | x = q // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:12:8 ------------------------------------- +12 | x = q // error | ^ | Found: (q : () => Unit) | Required: () ->{p, q²} Unit @@ -8,15 +8,15 @@ | q² is a parameter in method test | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:12:9 ------------------------------------- -12 | x = (q: Proc) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:13:9 ------------------------------------- +13 | x = (q: Proc) // error | ^^^^^^^ | 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 +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:14:9 ------------------------------------- +14 | y = (q: Proc) // error | ^^^^^^^ | Found: () => Unit | Required: () ->{p} Unit @@ -25,8 +25,8 @@ | cannot be included in capture set {p} of variable y | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:14:8 ------------------------------------- -14 | y = q // error, was OK under unsealed +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:15:8 ------------------------------------- +15 | y = q // error, was OK under unsealed | ^ | Found: (q : () => Unit) | Required: () ->{p} Unit @@ -35,8 +35,8 @@ | cannot be included in outer capture set {p} of variable y | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/outer-var.scala:16:57 --------------------------------------------------------- -16 | var finalizeActions = collection.mutable.ListBuffer[() => Unit]() // error, was OK under unsealed +-- Error: tests/neg-custom-args/captures/outer-var.scala:17:57 --------------------------------------------------------- +17 | var finalizeActions = collection.mutable.ListBuffer[() => Unit]() // error, was OK under unsealed | ^^^^^^^^^^ | Type variable A of object ListBuffer cannot be instantiated to box () => Unit since | that type captures the root capability `cap`. diff --git a/tests/neg-custom-args/captures/outer-var.scala b/tests/neg-custom-args/captures/outer-var.scala index f869bfbfc387..eb82312d4b37 100644 --- a/tests/neg-custom-args/captures/outer-var.scala +++ b/tests/neg-custom-args/captures/outer-var.scala @@ -1,3 +1,4 @@ + class CC type Cap = CC^ diff --git a/tests/neg-custom-args/captures/path-patmat-should-be-pos.scala b/tests/neg-custom-args/captures/path-patmat-should-be-pos.scala index aca6102204a3..5f434a21cc8b 100644 --- a/tests/neg-custom-args/captures/path-patmat-should-be-pos.scala +++ b/tests/neg-custom-args/captures/path-patmat-should-be-pos.scala @@ -1,6 +1,8 @@ +import caps.cap + class It[A] -class Filter[A](val underlying: It[A]^, val p: A => Boolean) extends It[A] +class Filter[A](val underlying: It[A]^, val p: A ->{cap, underlying} Boolean) extends It[A] object Filter: def apply[A](underlying: It[A]^, p: A => Boolean): Filter[A]^{underlying, p} = underlying match diff --git a/tests/neg-custom-args/captures/path-use.check b/tests/neg-custom-args/captures/path-use.check deleted file mode 100644 index e09ee232dd17..000000000000 --- a/tests/neg-custom-args/captures/path-use.check +++ /dev/null @@ -1,4 +0,0 @@ --- Error: tests/neg-custom-args/captures/path-use.scala:18:32 ---------------------------------------------------------- -18 | val g = () => println(c.procs.head) // error, local reach capability c.procs* leaks - | ^^^^^^^^^^^^ - | Local reach capability c.procs* leaks into capture scope of method test diff --git a/tests/neg-custom-args/captures/path-use.scala b/tests/neg-custom-args/captures/path-use.scala deleted file mode 100644 index 31feb4c0adf4..000000000000 --- a/tests/neg-custom-args/captures/path-use.scala +++ /dev/null @@ -1,25 +0,0 @@ -import language.experimental.namedTuples - -class IO - -class C(val f: IO^): - val procs: List[Proc] = ??? - -type Proc = () => Unit - -def test(io: IO^) = - val c = C(io) - val f = () => println(c.f) - val _: () ->{c.f} Unit = f - - val x = c.procs - val _: List[() ->{c.procs*} Unit] = x - - val g = () => println(c.procs.head) // error, local reach capability c.procs* leaks - val _: () ->{c.procs*} Unit = g - - val cc: C { val f: IO^{io}; val procs: List[() ->{io} Unit] }^{io} = - ??? - - val gg = () => println(cc.procs.head) // OK, since cc.procs* has {io} as underlying capture set - val _: () ->{io} Unit = gg diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 7c00fa7299fe..b1e46c300ef7 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -1,12 +1,12 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:22:11 -------------------------------------- -22 | cur = (() => f.write()) :: Nil // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:23:11 -------------------------------------- +23 | cur = (() => f.write()) :: Nil // error | ^^^^^^^^^^^^^^^^^^^^^^^ | Found: List[box () ->{f} Unit] | Required: List[box () ->{xs*} Unit] | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:33:7 --------------------------------------- -33 | (() => f.write()) :: Nil // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:34:7 --------------------------------------- +34 | (() => f.write()) :: Nil // error | ^^^^^^^^^^^^^^^^^^^^^^^ | Found: List[box () ->{f} Unit] | Required: box List[box () ->{xs*} Unit]^? @@ -15,47 +15,110 @@ | cannot be included in outer capture set {xs*} of value cur | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/reaches.scala:36:6 ------------------------------------------------------------ -36 | var cur: List[Proc] = xs // error +-- Error: tests/neg-custom-args/captures/reaches.scala:37:6 ------------------------------------------------------------ +37 | var cur: List[Proc] = xs // error | ^ | Mutable variable cur cannot have type List[box () => Unit] since | the part box () => Unit of that type captures the root capability `cap`. --- Error: tests/neg-custom-args/captures/reaches.scala:43:16 ----------------------------------------------------------- -43 | val cur = Ref[List[Proc]](xs) // error +-- Error: tests/neg-custom-args/captures/reaches.scala:44:16 ----------------------------------------------------------- +44 | val cur = Ref[List[Proc]](xs) // error | ^^^^^^^^^^ | 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`. --- Error: tests/neg-custom-args/captures/reaches.scala:53:51 ----------------------------------------------------------- -53 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:46:35 -------------------------------------- +46 | 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:48:20 -------------------------------------- +48 | 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:54:51 ----------------------------------------------------------- +54 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error | ^ | Type variable A of constructor Id cannot be instantiated to box () => Unit since | that type captures the root capability `cap`. --- Error: tests/neg-custom-args/captures/reaches.scala:55:6 ------------------------------------------------------------ -55 | id(() => f.write()) // error - | ^^^^^^^^^^^^^^^^^^^ - | Local reach capability id* leaks into capture scope of method test --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:62:27 -------------------------------------- -62 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:63:27 -------------------------------------- +63 | val f1: File^{id*} = id(f) // error // error + | ^^^^^ + | Found: File^{f} + | Required: File^{id*} + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:67:37 -------------------------------------- +67 | val id: (x: File^) -> File^ = x => x // error + | ^ + | Found: (x : File^) + | Required: File^? + | + | Note that the existential capture root in File^ + | cannot subsume the capability x.type + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:71:27 -------------------------------------- +71 | val f1: File^{id*} = id(f) // error // error | ^^^^^ | Found: File^{f} | Required: File^{id*} | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/reaches.scala:79:10 ----------------------------------------------------------- -79 | ps.map((x, y) => compose1(x, y)) // error // error +-- Error: tests/neg-custom-args/captures/reaches.scala:88:10 ----------------------------------------------------------- +88 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck | ^ | Local reach capability ps* leaks into capture scope of method mapCompose. | To allow this, the parameter ps should be declared with a @use annotation --- Error: tests/neg-custom-args/captures/reaches.scala:79:13 ----------------------------------------------------------- -79 | ps.map((x, y) => compose1(x, y)) // error // error +-- Error: tests/neg-custom-args/captures/reaches.scala:88:13 ----------------------------------------------------------- +88 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck | ^ | Local reach capability ps* leaks into capture scope of method mapCompose. | To allow this, the parameter ps should be declared with a @use annotation --- Error: tests/neg-custom-args/captures/reaches.scala:61:31 ----------------------------------------------------------- -61 | val leaked = usingFile[File^{id*}]: f => // error +-- Error: tests/neg-custom-args/captures/reaches.scala:88:28 ----------------------------------------------------------- +88 | ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck + | ^ + | Separation failure: argument of type A ->{x} box A^? + | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C + | corresponds to capture-polymorphic formal parameter f of type box A^? => box A^? + | and hides capabilities {x}. + | Some of these overlap with the captures of the second argument with type A ->{y} box A^?. + | + | Hidden set of current argument : {x} + | Hidden footprint of current argument : {x, ps*} + | Capture set of second argument : {y} + | Footprint set of second argument : {y, ps*} + | The two sets overlap at : {ps*} +-- Error: tests/neg-custom-args/captures/reaches.scala:91:28 ----------------------------------------------------------- +91 | ps.map((x, y) => compose1(x, y)) // error sepcheck + | ^ + | Separation failure: argument of type A ->{x} box A^? + | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C + | corresponds to capture-polymorphic formal parameter f of type box A^? => box A^? + | and hides capabilities {x}. + | Some of these overlap with the captures of the second argument with type A ->{y} box A^?. + | + | Hidden set of current argument : {x} + | Hidden footprint of current argument : {x, ps*} + | Capture set of second argument : {y} + | Footprint set of second argument : {y, ps*} + | The two sets overlap at : {ps*} +-- Error: tests/neg-custom-args/captures/reaches.scala:62:31 ----------------------------------------------------------- +62 | val leaked = usingFile[File^{id*}]: f => // error + | ^^^ + | id* cannot be tracked since its deep capture set is empty +-- Error: tests/neg-custom-args/captures/reaches.scala:63:18 ----------------------------------------------------------- +63 | val f1: File^{id*} = id(f) // error // error + | ^^^ + | id* cannot be tracked since its deep capture set is empty +-- Error: tests/neg-custom-args/captures/reaches.scala:70:31 ----------------------------------------------------------- +70 | val leaked = usingFile[File^{id*}]: f => // error | ^^^ | id* cannot be tracked since its deep capture set is empty --- Error: tests/neg-custom-args/captures/reaches.scala:62:18 ----------------------------------------------------------- -62 | val f1: File^{id*} = id(f) // error, since now id(f): File^ // error +-- Error: tests/neg-custom-args/captures/reaches.scala:71:18 ----------------------------------------------------------- +71 | val f1: File^{id*} = id(f) // error // error | ^^^ | id* cannot be tracked since its deep capture set is empty diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index a9773b76f445..32afd4066333 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -1,4 +1,5 @@ -import caps.use +import caps.use; +import caps.consume class File: def write(): Unit = ??? @@ -32,7 +33,7 @@ def runAll1(@use xs: List[Proc]): Unit = cur.set: (() => f.write()) :: Nil // error -def runAll2(xs: List[Proc]): Unit = +def runAll2(@consume xs: List[Proc]): Unit = var cur: List[Proc] = xs // error while cur.nonEmpty do val next: () => Unit = cur.head @@ -42,9 +43,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 @@ -52,14 +53,22 @@ class Id[-A, +B >: A](): def test = val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error usingFile: f => - id(() => f.write()) // error + id(() => f.write()) // was error def attack2 = val id: File^ -> File^ = x => x + // val id: File^ -> File^{fresh} + + val leaked = usingFile[File^{id*}]: f => // error + val f1: File^{id*} = id(f) // error // error + f1 + +def attack3 = + val id: (x: File^) -> File^ = x => x // error // val id: File^ -> EX C.File^C val leaked = usingFile[File^{id*}]: f => // error - val f1: File^{id*} = id(f) // error, since now id(f): File^ // error + val f1: File^{id*} = id(f) // error // error f1 class List[+A]: @@ -76,7 +85,7 @@ def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C = z => g(f(z)) def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] = - ps.map((x, y) => compose1(x, y)) // error // error + ps.map((x, y) => compose1(x, y)) // error // error // error sepcheck def mapCompose2[A](@use ps: List[(A => A, A => A)]): List[A ->{ps*} A] = - ps.map((x, y) => compose1(x, y)) + ps.map((x, y) => compose1(x, y)) // error sepcheck diff --git a/tests/neg-custom-args/captures/reaches2.check b/tests/neg-custom-args/captures/reaches2.check index 1e921ee92072..926e6772bd8f 100644 --- a/tests/neg-custom-args/captures/reaches2.check +++ b/tests/neg-custom-args/captures/reaches2.check @@ -1,10 +1,24 @@ --- Error: tests/neg-custom-args/captures/reaches2.scala:8:10 ----------------------------------------------------------- -8 | ps.map((x, y) => compose1(x, y)) // error // error - | ^ - |reference ps* is not included in the allowed capture set {} - |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^? --- Error: tests/neg-custom-args/captures/reaches2.scala:8:13 ----------------------------------------------------------- -8 | ps.map((x, y) => compose1(x, y)) // error // error - | ^ - |reference ps* is not included in the allowed capture set {} - |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^? +-- Error: tests/neg-custom-args/captures/reaches2.scala:10:10 ---------------------------------------------------------- +10 | ps.map((x, y) => compose1(x, y)) // error // error // error + | ^ + | reference ps* is not included in the allowed capture set {} + | of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box A^? ->? A^? +-- Error: tests/neg-custom-args/captures/reaches2.scala:10:13 ---------------------------------------------------------- +10 | ps.map((x, y) => compose1(x, y)) // error // error // error + | ^ + | reference ps* is not included in the allowed capture set {} + | of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box A^? ->? A^? +-- Error: tests/neg-custom-args/captures/reaches2.scala:10:28 ---------------------------------------------------------- +10 | ps.map((x, y) => compose1(x, y)) // error // error // error + | ^ + | Separation failure: argument of type A ->{x} box A^? + | to method compose1: [A, B, C](f: A => B, g: B => C): A ->{f, g} C + | corresponds to capture-polymorphic formal parameter f of type box A^? => box A^? + | and hides capabilities {x}. + | Some of these overlap with the captures of the second argument with type A ->{y} box A^?. + | + | Hidden set of current argument : {x} + | Hidden footprint of current argument : {x, ps*} + | Capture set of second argument : {y} + | Footprint set of second argument : {y, ps*} + | The two sets overlap at : {ps*} diff --git a/tests/neg-custom-args/captures/reaches2.scala b/tests/neg-custom-args/captures/reaches2.scala index f2447b8c8795..69ee3472cf86 100644 --- a/tests/neg-custom-args/captures/reaches2.scala +++ b/tests/neg-custom-args/captures/reaches2.scala @@ -1,3 +1,5 @@ + + class List[+A]: def map[B](f: A -> B): List[B] = ??? @@ -5,5 +7,5 @@ def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C = z => g(f(z)) def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] = - ps.map((x, y) => compose1(x, y)) // error // error + ps.map((x, y) => compose1(x, y)) // error // error // error diff --git a/tests/neg-custom-args/captures/readOnly.check b/tests/neg-custom-args/captures/readOnly.check new file mode 100644 index 000000000000..46721d3e1627 --- /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 : 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/sep-box.check b/tests/neg-custom-args/captures/sep-box.check new file mode 100644 index 000000000000..2a2608134130 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-box.check @@ -0,0 +1,14 @@ +-- Error: tests/neg-custom-args/captures/sep-box.scala:41:9 ------------------------------------------------------------ +41 | par(h1.value, h2.value) // error + | ^^^^^^^^ + | Separation failure: argument of type Ref^{xs*} + | to method par: (x: Ref^, y: Ref^): Unit + | corresponds to capture-polymorphic formal parameter x of type Ref^ + | and hides capabilities {xs*}. + | Some of these overlap with the captures of the second argument with type Ref^{xs*}. + | + | Hidden set of current argument : {xs*} + | Hidden footprint of current argument : {xs*} + | Capture set of second argument : {xs*} + | Footprint set of second argument : {xs*} + | The two sets overlap at : {xs*} diff --git a/tests/neg-custom-args/captures/sep-box.scala b/tests/neg-custom-args/captures/sep-box.scala new file mode 100644 index 000000000000..fd9348acc893 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-box.scala @@ -0,0 +1,41 @@ +import caps.Mutable +import caps.cap + +abstract class LIST[+T]: + def isEmpty: Boolean + def head: T + def tail: LIST[T] + def map[U](f: T => U): LIST[U] = + if isEmpty then NIL + else CONS(f(head), tail.map(f)) + +class CONS[+T](x: T, xs: LIST[T]) extends LIST[T]: + def isEmpty = false + def head = x + def tail = xs +object NIL extends LIST[Nothing]: + def isEmpty = true + def head = ??? + def tail = ??? + +class Ref extends Mutable: + var x = 0 + def get: Int = x + mut def put(y: Int): Unit = x = y + +class Box[+X](val value: X) + +def listFresh(n: Int): LIST[Box[Ref^]] = + if n == 0 then NIL + else + val hd = Ref() + val tl = listFresh(n - 1) + CONS(Box(hd), tl) + +def par(x: Ref^, y: Ref^): Unit = () + +def test = + val xs = listFresh(10) + val h1 = xs.head + val h2 = xs.head + par(h1.value, h2.value) // error diff --git a/tests/neg-custom-args/captures/sep-compose.check b/tests/neg-custom-args/captures/sep-compose.check new file mode 100644 index 000000000000..459f00789ea8 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-compose.check @@ -0,0 +1,112 @@ +-- Error: tests/neg-custom-args/captures/sep-compose.scala:32:7 -------------------------------------------------------- +32 | seq3(f)(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq3: (x: () => Unit)(y: () ->{a, cap} Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () => Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (f : () ->{a} Unit). + | + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of second argument : {f} + | Footprint set of second argument : {f, a, io} + | The two sets overlap at : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:33:7 -------------------------------------------------------- +33 | seq4(f)(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq4: (x: () ->{a, cap} Unit)(y: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () ->{a, cap} Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (f : () ->{a} Unit). + | + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of second argument : {f} + | Footprint set of second argument : {f, a, io} + | The two sets overlap at : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:34:7 -------------------------------------------------------- +34 | seq5(f)(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq5: (x: () => Unit)(y: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () => Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (f : () ->{a} Unit). + | + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of second argument : {f} + | Footprint set of second argument : {f, a, io} + | The two sets overlap at : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:35:7 -------------------------------------------------------- +35 | seq6(f, f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq6: (x: () => Unit, y: () ->{a, cap} Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () => Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (f : () ->{a} Unit). + | + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of second argument : {f} + | Footprint set of second argument : {f, a, io} + | The two sets overlap at : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:36:7 -------------------------------------------------------- +36 | seq7(f, f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq7: (x: () ->{a, cap} Unit, y: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () ->{a, cap} Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (f : () ->{a} Unit). + | + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of second argument : {f} + | Footprint set of second argument : {f, a, io} + | The two sets overlap at : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:37:7 -------------------------------------------------------- +37 | seq8(f)(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq8: (x: () => Unit)(y: () ->{a} Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () => Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (f : () ->{a} Unit). + | + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of second argument : {f} + | Footprint set of second argument : {f, a, io} + | The two sets overlap at : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:40:5 -------------------------------------------------------- +40 | p1(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to a function of type (() => Unit) ->{f} Unit + | corresponds to capture-polymorphic formal parameter v1 of type () => Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the function prefix. + | + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of function prefix : {p1} + | Footprint set of function prefix : {p1, f, a, io} + | The two sets overlap at : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:41:38 ------------------------------------------------------- +41 | val p8 = (x: () ->{a} Unit) => seq8(f)(x) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq8: (x: () => Unit)(y: () ->{a} Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () => Unit + | and hides capabilities {f}. + | Some of these overlap with the captures of the second argument with type (x : () ->{a} Unit). + | + | Hidden set of current argument : {f} + | Hidden footprint of current argument : {f, a, io} + | Capture set of second argument : {x} + | Footprint set of second argument : {x, a, io} + | The two sets overlap at : {a, io} diff --git a/tests/neg-custom-args/captures/sep-compose.scala b/tests/neg-custom-args/captures/sep-compose.scala new file mode 100644 index 000000000000..cfa3318b315f --- /dev/null +++ b/tests/neg-custom-args/captures/sep-compose.scala @@ -0,0 +1,45 @@ +import caps.cap + +def seq1(x: () => Unit, y: () ->{x, cap} Unit): Unit = + x(); y() + +def seq2(x: () => Unit)(y: () ->{x, cap} Unit): Unit = + x(); y() + +def seq5(x: () ->{cap} Unit)(y: () => Unit): Unit = + x(); y() + +def test(io: Object^, a: Object^{io}): Unit = + + def seq3(x: () => Unit)(y: () ->{a, cap} Unit): Unit = + x(); y() + + def seq4(x: () ->{a, cap} Unit)(y: () => Unit): Unit = + x(); y() + + def seq6(x: () => Unit, y: () ->{a, cap} Unit): Unit = + x(); y() + + def seq7(x: () ->{a, cap} Unit, y: () => Unit): Unit = + x(); y() + + def seq8(x: () => Unit)(y: () ->{a} Unit): Unit = + x(); y() + + val f = () => println(a) + seq1(f, f) // ok + seq2(f)(f) // ok + seq3(f)(f) // error + seq4(f)(f) // error + seq5(f)(f) // error + seq6(f, f) // error + seq7(f, f) // error + seq8(f)(f) // error + + val p1 = (x: () => Unit) => seq1(f, x) + p1(f) // error + val p8 = (x: () ->{a} Unit) => seq8(f)(x) // error + p8(f) + + + diff --git a/tests/neg-custom-args/captures/sep-consume.check b/tests/neg-custom-args/captures/sep-consume.check new file mode 100644 index 000000000000..d8ee2c5ed43e --- /dev/null +++ b/tests/neg-custom-args/captures/sep-consume.check @@ -0,0 +1,28 @@ +-- Error: tests/neg-custom-args/captures/sep-consume.scala:19:2 -------------------------------------------------------- +19 | x.put(42) // error + | ^ + | Separation failure: Illegal access to (x : Ref^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 18 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/sep-consume.scala:21:16 ------------------------------------------------------- +21 | par(rx, () => x.put(42)) // error + | ^ + | Separation failure: Illegal access to (x : Ref^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 18 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/sep-consume.scala:26:16 ------------------------------------------------------- +26 | def foo = bad(f) // error + | ^ + | Separation failure: argument to @consume parameter with type (f : () ->{x.rd} Unit) refers to non-local value f +-- Error: tests/neg-custom-args/captures/sep-consume.scala:34:12 ------------------------------------------------------- +34 | println(p.fst.get) // error + | ^^^^^ + | Separation failure: Illegal access to p.fst*, which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 33 + | and therefore is no longer available. +-- Error: tests/neg-custom-args/captures/sep-consume.scala:40:12 ------------------------------------------------------- +40 | println(p.fst.get) // error + | ^^^^^ + | Separation failure: Illegal access to p.fst*, which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 39 + | and therefore is no longer available. diff --git a/tests/neg-custom-args/captures/sep-consume.scala b/tests/neg-custom-args/captures/sep-consume.scala new file mode 100644 index 000000000000..46c45e8cc6f5 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-consume.scala @@ -0,0 +1,41 @@ +import language.experimental.captureChecking +import caps.* + +class Ref extends Mutable: + private var _data = 0 + def get: Int = _data + mut def put(x: Int): Unit = _data = x + +case class Pair[+A, +B](fst: A, snd: B) + +// require f and g to be non-interfering +def par(f: () => Unit, g: () => Unit): Unit = ??? + +def bad(@consume op: () ->{cap.rd} Unit): () => Unit = op + +def test2(@consume x: Ref^): Unit = + val f: () ->{x.rd} Unit = () => x.get + val rx: () => Unit = bad(f) // hides x.rd in the resulting `cap` + x.put(42) // error + x.get // ok rd/rd + par(rx, () => x.put(42)) // error + par(rx, () => x.get) // ok rd/rd + +def test3(@consume x: Ref^): Unit = + val f: () ->{x.rd} Unit = () => x.get + def foo = bad(f) // error + foo() + foo() + +def test4(@consume @use p: Pair[Ref^, Ref^]): Unit = + val x: Ref^{p.fst*} = p.fst + val y: Ref^{p.snd*} = p.snd + badp(Pair(x, y)) + println(p.fst.get) // error + +def badp(@consume p: Pair[Ref^, Ref^]): Unit = () + +def test5(@consume @use p: Pair[Ref^, Ref^]): Unit = + badp(p) // ok + println(p.fst.get) // error + diff --git a/tests/neg-custom-args/captures/sep-counter.check b/tests/neg-custom-args/captures/sep-counter.check new file mode 100644 index 000000000000..230c1e6f6e53 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-counter.check @@ -0,0 +1,7 @@ +-- Error: tests/neg-custom-args/captures/sep-counter.scala:12:19 ------------------------------------------------------- +12 | def mkCounter(): Pair[Ref^, Ref^] = // error + | ^^^^^^^^^^^^^^^^ + | Separation failure in method mkCounter's result type Pair[box Ref^, box Ref^]. + | One part, box Ref^, hides capabilities {cap}. + | Another part, box Ref^, captures capabilities {cap}. + | The two sets overlap at cap of value c. diff --git a/tests/neg-custom-args/captures/sep-counter.scala b/tests/neg-custom-args/captures/sep-counter.scala new file mode 100644 index 000000000000..42bf5afc172a --- /dev/null +++ b/tests/neg-custom-args/captures/sep-counter.scala @@ -0,0 +1,16 @@ +import caps.Mutable +import caps.cap + +class Ref extends Mutable: + var x = 0 + def get: Int = x + mut def put(y: Int): Unit = x = y + +class Pair[+X, +Y](val fst: X, val snd: Y) + +def test() = + def mkCounter(): Pair[Ref^, Ref^] = // error + val c = Ref() + val p: Pair[Ref^{c}, Ref^{c}] = Pair(c, c) + //val q: Pair[Ref^, Ref^] = p + p diff --git a/tests/neg-custom-args/captures/sep-curried.check b/tests/neg-custom-args/captures/sep-curried.check new file mode 100644 index 000000000000..edf359c4be14 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-curried.check @@ -0,0 +1,77 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/sep-curried.scala:48:43 ---------------------------------- +48 | val f: (y: Ref[Int]^{a}) ->{a} Unit = foo(a) // error + | ^^^^^^ + | Found: (y: Ref[Int]^) ->{a} Unit + | Required: (y: Ref[Int]^{a}) ->{a} Unit + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/sep-curried.scala:16:6 -------------------------------------------------------- +16 | foo(a)(a) // error + | ^ + | Separation failure: argument of type (a : Ref[Int]^) + | to method foo: (x: Ref[Int]^)(y: Ref[Int]^{a}): Unit + | corresponds to capture-polymorphic formal parameter x of type Ref[Int]^ + | and hides capabilities {a}. + | Some of these overlap with the captures of the second argument with type (a : Ref[Int]^). + | + | Hidden set of current argument : {a} + | Hidden footprint of current argument : {a} + | Capture set of second argument : {a} + | Footprint set of second argument : {a} + | The two sets overlap at : {a} +-- Error: tests/neg-custom-args/captures/sep-curried.scala:22:44 ------------------------------------------------------- +22 | val f: (y: Ref[Int]^{a}) ->{a} Unit = foo(a) // error + | ^ + | Separation failure: argument of type (a : Ref[Int]^) + | to a function of type (x: Ref[Int]^) -> (y: Ref[Int]^{a}) ->{x} Unit + | corresponds to capture-polymorphic formal parameter x of type Ref[Int]^ + | and hides capabilities {a}. + | Some of these overlap with the captures of the function result with type (y: Ref[Int]^{a}) ->{a} Unit. + | + | Hidden set of current argument : {a} + | Hidden footprint of current argument : {a} + | Capture set of function result : {a} + | Footprint set of function result : {a} + | The two sets overlap at : {a} +-- Error: tests/neg-custom-args/captures/sep-curried.scala:29:6 -------------------------------------------------------- +29 | foo(a)(a) // error + | ^ + | Separation failure: argument of type (a : Ref[Int]^) + | to a function of type (x: Ref[Int]^) -> (y: Ref[Int]^{a}) ->{x} Unit + | corresponds to capture-polymorphic formal parameter x of type Ref[Int]^ + | and hides capabilities {a}. + | Some of these overlap with the captures of the function result with type (y: Ref[Int]^{a}) ->{a} Unit. + | + | Hidden set of current argument : {a} + | Hidden footprint of current argument : {a} + | Capture set of function result : {a} + | Footprint set of function result : {a} + | The two sets overlap at : {a} +-- Error: tests/neg-custom-args/captures/sep-curried.scala:35:9 -------------------------------------------------------- +35 | foo(a)(a) // error + | ^ + | Separation failure: argument of type (a : Ref[Int]^) + | to a function of type (y: Ref[Int]^) ->{a} Unit + | corresponds to capture-polymorphic formal parameter y of type Ref[Int]^ + | and hides capabilities {a}. + | Some of these overlap with the captures of the function prefix. + | + | Hidden set of current argument : {a} + | Hidden footprint of current argument : {a} + | Capture set of function prefix : {a} + | Footprint set of function prefix : {a} + | The two sets overlap at : {a} +-- Error: tests/neg-custom-args/captures/sep-curried.scala:42:4 -------------------------------------------------------- +42 | f(a) // error + | ^ + | Separation failure: argument of type (a : Ref[Int]^) + | to a function of type (y: Ref[Int]^) ->{a} Unit + | corresponds to capture-polymorphic formal parameter y of type Ref[Int]^ + | and hides capabilities {a}. + | Some of these overlap with the captures of the function prefix. + | + | Hidden set of current argument : {a} + | Hidden footprint of current argument : {a} + | Capture set of function prefix : {f} + | Footprint set of function prefix : {f, a} + | The two sets overlap at : {a} diff --git a/tests/neg-custom-args/captures/sep-curried.scala b/tests/neg-custom-args/captures/sep-curried.scala new file mode 100644 index 000000000000..13a07ad6cca7 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-curried.scala @@ -0,0 +1,49 @@ +import language.experimental.captureChecking +import caps.* + +class Ref[T](init: T) extends Mutable: + private var value: T = init + def get: T = value + mut def set(newValue: T): Unit = value = newValue + +// a library function that assumes that a and b MUST BE separate +def swap[T](a: Ref[Int]^, b: Ref[Int]^): Unit = ??? + +def test0(): Unit = + val a: Ref[Int]^ = Ref(0) + def foo(x: Ref[Int]^)(y: Ref[Int]^{a}): Unit = + swap(x, y) + foo(a)(a) // error + +def test1(): Unit = + val a: Ref[Int]^ = Ref(0) + val foo: (x: Ref[Int]^) -> (y: Ref[Int]^{a}) ->{x} Unit = + x => y => swap(x, y) + val f: (y: Ref[Int]^{a}) ->{a} Unit = foo(a) // error + f(a) + +def test2(): Unit = + val a: Ref[Int]^ = Ref(0) + val foo: (x: Ref[Int]^) -> (y: Ref[Int]^{a}) ->{x} Unit = + x => y => swap(x, y) + foo(a)(a) // error + +def test3(): Unit = + val a: Ref[Int]^ = Ref(0) + val foo: (x: Ref[Int]^) -> (y: Ref[Int]^) ->{x} Unit = + x => y => swap(x, y) + foo(a)(a) // error + +def test4(): Unit = + val a: Ref[Int]^ = Ref(0) + val foo: (x: Ref[Int]^) -> (y: Ref[Int]^) ->{x} Unit = + x => y => swap(x, y) + val f = foo(a) + f(a) // error + +def test5(): Unit = + val a: Ref[Int]^ = Ref(0) + val foo: (x: Ref[Int]^) -> (y: Ref[Int]^) ->{x} Unit = + x => y => swap(x, y) + val f: (y: Ref[Int]^{a}) ->{a} Unit = foo(a) // error + f(a) diff --git a/tests/neg-custom-args/captures/sep-list.check b/tests/neg-custom-args/captures/sep-list.check new file mode 100644 index 000000000000..86d4937677e8 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-list.check @@ -0,0 +1,14 @@ +-- Error: tests/neg-custom-args/captures/sep-list.scala:39:6 ----------------------------------------------------------- +39 | par(h1, h2) // error + | ^^ + | Separation failure: argument of type (h1 : Ref^{xs*}) + | to method par: (x: Ref^, y: Ref^): Unit + | corresponds to capture-polymorphic formal parameter x of type Ref^ + | and hides capabilities {h1}. + | Some of these overlap with the captures of the second argument with type (h2 : Ref^{xs*}). + | + | Hidden set of current argument : {h1} + | Hidden footprint of current argument : {h1, xs*} + | Capture set of second argument : {h2} + | Footprint set of second argument : {h2, xs*} + | The two sets overlap at : {xs*} diff --git a/tests/neg-custom-args/captures/sep-list.scala b/tests/neg-custom-args/captures/sep-list.scala new file mode 100644 index 000000000000..46e9b6988318 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-list.scala @@ -0,0 +1,39 @@ +import caps.Mutable +import caps.cap + +abstract class LIST[+T]: + def isEmpty: Boolean + def head: T + def tail: LIST[T] + def map[U](f: T => U): LIST[U] = + if isEmpty then NIL + else CONS(f(head), tail.map(f)) + +class CONS[+T](x: T, xs: LIST[T]) extends LIST[T]: + def isEmpty = false + def head = x + def tail = xs +object NIL extends LIST[Nothing]: + def isEmpty = true + def head = ??? + def tail = ??? + +class Ref extends Mutable: + var x = 0 + def get: Int = x + mut def put(y: Int): Unit = x = y + +def listFresh(n: Int): LIST[Ref^] = + if n == 0 then NIL + else + val hd = Ref() + val tl = listFresh(n - 1) + CONS(hd, tl) + +def par(x: Ref^, y: Ref^): Unit = () + +def test = + val xs = listFresh(10) + val h1 = xs.head + val h2 = xs.head + par(h1, h2) // error diff --git a/tests/neg-custom-args/captures/sep-pairs.check b/tests/neg-custom-args/captures/sep-pairs.check new file mode 100644 index 000000000000..fd3e00c138bf --- /dev/null +++ b/tests/neg-custom-args/captures/sep-pairs.check @@ -0,0 +1,41 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/sep-pairs.scala:32:10 ------------------------------------ +32 | Pair(Ref(), Ref()) // error // error: universal capability cannot be included in capture set + | ^^^^^ + | Found: box Ref^ + | Required: box Ref^? + | + | Note that the universal capability `cap` + | cannot be included in capture set ? + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/sep-pairs.scala:32:17 ------------------------------------ +32 | Pair(Ref(), Ref()) // error // error: universal capability cannot be included in capture set + | ^^^^^ + | Found: box Ref^ + | Required: box Ref^? + | + | Note that the universal capability `cap` + | cannot be included in capture set ? + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/sep-pairs.scala:15:10 --------------------------------------------------------- +15 | val r1: Pair[Ref^, Ref^] = mkPair(r0) // error: overlap at r0 + | ^^^^^^^^^^^^^^^^ + | Separation failure in value r1's type Pair[box Ref^, box Ref^]. + | One part, box Ref^, hides capabilities {r0}. + | Another part, box Ref^, captures capabilities {r0}. + | The two sets overlap at {r0}. +-- Error: tests/neg-custom-args/captures/sep-pairs.scala:13:9 ---------------------------------------------------------- +13 |def bad: Pair[Ref^, Ref^] = // error: overlap at r1*, r0 + | ^^^^^^^^^^^^^^^^ + | Separation failure in method bad's result type Pair[box Ref^, box Ref^]. + | One part, box Ref^, hides capabilities {cap, cap, r1*, r0}. + | Another part, box Ref^, captures capabilities {cap, cap, r1*, r0}. + | The two sets overlap at {r1*, r0}. +-- Error: tests/neg-custom-args/captures/sep-pairs.scala:44:18 --------------------------------------------------------- +44 | val sameToPair: Pair[Ref^, Ref^] = Pair(fstSame, sndSame) // error + | ^^^^^^^^^^^^^^^^ + | Separation failure in value sameToPair's type Pair[box Ref^, box Ref^]. + | One part, box Ref^, hides capabilities {fstSame}. + | Another part, box Ref^, captures capabilities {sndSame}. + | The two sets overlap at cap of value same. diff --git a/tests/neg-custom-args/captures/sep-pairs.scala b/tests/neg-custom-args/captures/sep-pairs.scala new file mode 100644 index 000000000000..582c635ce27d --- /dev/null +++ b/tests/neg-custom-args/captures/sep-pairs.scala @@ -0,0 +1,47 @@ +import caps.Mutable +import caps.cap + +class Ref extends Mutable: + var x = 0 + def get: Int = x + mut def put(y: Int): Unit = x = y + +class Pair[+X, +Y](val fst: X, val snd: Y) + +def mkPair[X](x: X): Pair[X, X] = Pair(x, x) + +def bad: Pair[Ref^, Ref^] = // error: overlap at r1*, r0 + val r0 = Ref() + val r1: Pair[Ref^, Ref^] = mkPair(r0) // error: overlap at r0 + r1 + +class SamePair[+X](val fst: X, val snd: X) + +def twoRefs(): Pair[Ref^, Ref^] = + val r1 = Ref() + val r2 = Ref() + Pair(r1, r2) + +def twoRefs2(): SamePair[Ref^] = + val r1 = Ref() + val r2 = Ref() + val r3: SamePair[Ref^] = SamePair(r1, r1) // ok + r3 + +def twoRefsBad(): Pair[Ref^, Ref^] = + Pair(Ref(), Ref()) // error // error: universal capability cannot be included in capture set + // but should work since this is morally equivalent to `twoRefs` + +def test(io: Object^): Unit = + val two = twoRefs() + val fst: Ref^{two.fst*} = two.fst + val snd: Ref^{two.snd*} = two.snd + val twoCopy: Pair[Ref^, Ref^] = Pair(fst, snd) // ok + + val same = twoRefs2() + val fstSame = same.fst + val sndSame = same.snd + val sameToPair: Pair[Ref^, Ref^] = Pair(fstSame, sndSame) // error + + + diff --git a/tests/neg-custom-args/captures/sep-use.check b/tests/neg-custom-args/captures/sep-use.check new file mode 100644 index 000000000000..64e2bd7800bc --- /dev/null +++ b/tests/neg-custom-args/captures/sep-use.check @@ -0,0 +1,36 @@ +-- Error: tests/neg-custom-args/captures/sep-use.scala:7:10 ------------------------------------------------------------ +7 | println(io) // error + | ^^ + | Separation failure: Illegal access to {io} which is hidden by the previous definition + | of value x with type () => Unit. + | This type hides capabilities {io} +-- Error: tests/neg-custom-args/captures/sep-use.scala:12:12 ----------------------------------------------------------- +12 | def x: () => Unit = () => println(io) // error + | ^^^^^^^^^^ + | Separation failure: method x's result type () => Unit hides non-local parameter io +-- Error: tests/neg-custom-args/captures/sep-use.scala:13:10 ----------------------------------------------------------- +13 | println(io) // error + | ^^ + | Separation failure: Illegal access to {io} which is hidden by the previous definition + | of method x with result type () => Unit. + | This type hides capabilities {io} +-- Error: tests/neg-custom-args/captures/sep-use.scala:18:10 ----------------------------------------------------------- +18 | def xx: (y: Int) => Unit = _ => println(io) // error + | ^^^^^^^^^^^^^^^^ + | Separation failure: method xx's result type (y: Int) => Unit hides non-local parameter io +-- Error: tests/neg-custom-args/captures/sep-use.scala:19:10 ----------------------------------------------------------- +19 | println(io) // error + | ^^ + | Separation failure: Illegal access to {io} which is hidden by the previous definition + | of method xx with result type (y: Int) => Unit. + | This type hides capabilities {io} +-- Error: tests/neg-custom-args/captures/sep-use.scala:24:19 ----------------------------------------------------------- +24 | def xxx(y: Int): Object^ = io // error + | ^^^^^^^ + | Separation failure: method xxx's result type Object^ hides non-local parameter io +-- Error: tests/neg-custom-args/captures/sep-use.scala:25:10 ----------------------------------------------------------- +25 | println(io) // error + | ^^ + | Separation failure: Illegal access to {io} which is hidden by the previous definition + | of method xxx with result type Object^. + | This type hides capabilities {io} diff --git a/tests/neg-custom-args/captures/sep-use.scala b/tests/neg-custom-args/captures/sep-use.scala new file mode 100644 index 000000000000..e89adb0f060e --- /dev/null +++ b/tests/neg-custom-args/captures/sep-use.scala @@ -0,0 +1,27 @@ +import caps.{cap, consume} + + +def test1(@consume io: Object^): Unit = + + val x: () => Unit = () => println(io) + println(io) // error + println(x) // ok + +def test2(@consume io: Object^): Unit = + + def x: () => Unit = () => println(io) // error + println(io) // error + println(x) // ok + +def test3(@consume io: Object^): Unit = + + def xx: (y: Int) => Unit = _ => println(io) // error + println(io) // error + println(xx(2)) // ok + +def test4(@consume io: Object^): Unit = + + def xxx(y: Int): Object^ = io // error + println(io) // error + println(xxx(2)) // ok + diff --git a/tests/neg-custom-args/captures/sep-use2.check b/tests/neg-custom-args/captures/sep-use2.check new file mode 100644 index 000000000000..a8e3aa884fd5 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-use2.check @@ -0,0 +1,65 @@ +-- Error: tests/neg-custom-args/captures/sep-use2.scala:5:10 ----------------------------------------------------------- +5 | def cc: Object^ = c // error + | ^^^^^^^ + | Separation failure: method cc's result type Object^ hides non-local parameter c +-- Error: tests/neg-custom-args/captures/sep-use2.scala:13:8 ----------------------------------------------------------- +13 | { f(c) } // error + | ^ + | Separation failure: Illegal access to {c} which is hidden by the previous definition + | of method cc with result type Object^. + | This type hides capabilities {c} +-- Error: tests/neg-custom-args/captures/sep-use2.scala:12:10 ---------------------------------------------------------- +12 | val x4: Object^ = // error + | ^^^^^^^ + | Separation failure: value x4's type Object^ hides parameter f. + | The parameter needs to be annotated with @consume to allow this. +-- Error: tests/neg-custom-args/captures/sep-use2.scala:16:10 ---------------------------------------------------------- +16 | def cc: Object^ = c // error + | ^^^^^^^ + | Separation failure: method cc's result type Object^ hides non-local parameter c +-- Error: tests/neg-custom-args/captures/sep-use2.scala:18:6 ----------------------------------------------------------- +18 | { f(cc) } // error // error + | ^ + | Separation failure: Illegal access to {c} which is hidden by the previous definition + | of method cc with result type Object^. + | This type hides capabilities {c} +-- Error: tests/neg-custom-args/captures/sep-use2.scala:18:8 ----------------------------------------------------------- +18 | { f(cc) } // error // error + | ^^ + | Separation failure: argument of type (cc : -> Object^) + | to a function of type (x: Object^) ->{c} Object^ + | corresponds to capture-polymorphic formal parameter x of type Object^ + | and hides capabilities {cap, c}. + | Some of these overlap with the captures of the function prefix. + | + | Hidden set of current argument : {cap, c} + | Hidden footprint of current argument : {c} + | Capture set of function prefix : {f} + | Footprint set of function prefix : {f, c} + | The two sets overlap at : {c} +-- Error: tests/neg-custom-args/captures/sep-use2.scala:20:6 ----------------------------------------------------------- +20 | { f(c) } // error // error + | ^ + | Separation failure: Illegal access to {c} which is hidden by the previous definition + | of method cc with result type Object^. + | This type hides capabilities {c} +-- Error: tests/neg-custom-args/captures/sep-use2.scala:20:8 ----------------------------------------------------------- +20 | { f(c) } // error // error + | ^ + | Separation failure: Illegal access to {c} which is hidden by the previous definition + | of method cc with result type Object^. + | This type hides capabilities {c} +-- Error: tests/neg-custom-args/captures/sep-use2.scala:24:8 ----------------------------------------------------------- +24 | { f(c) } // error + | ^ + | Separation failure: argument of type (c : Object^) + | to a function of type Object^ ->{c} Object^ + | corresponds to capture-polymorphic formal parameter v1 of type Object^ + | and hides capabilities {c}. + | Some of these overlap with the captures of the function prefix. + | + | Hidden set of current argument : {c} + | Hidden footprint of current argument : {c} + | Capture set of function prefix : {f} + | Footprint set of function prefix : {f, c} + | The two sets overlap at : {c} diff --git a/tests/neg-custom-args/captures/sep-use2.scala b/tests/neg-custom-args/captures/sep-use2.scala new file mode 100644 index 000000000000..a31fb098a719 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-use2.scala @@ -0,0 +1,35 @@ + +import caps.consume + +def test1(@consume c: Object^, f: (x: Object^) => Object^) = + def cc: Object^ = c // error + val x1 = + { f(cc) } // ok + val x2 = + f(cc) // ok + val x3: Object^ = + f(cc) // ok + val x4: Object^ = // error + { f(c) } // error + +def test2(@consume c: Object^, f: (x: Object^) ->{c} Object^) = + def cc: Object^ = c // error + val x1 = + { f(cc) } // error // error + val x4: Object^ = // ^ hides just c, since the Object^ in the result of `f` is existential + { f(c) } // error // error + +def test3(@consume c: Object^, f: Object^ ->{c} Object^) = + val x4: Object^ = // ^ hides c and f* + { f(c) } // error + + + + + + + + + + + diff --git a/tests/neg-custom-args/captures/sepchecks.scala b/tests/neg-custom-args/captures/sepchecks.scala new file mode 100644 index 000000000000..4508b6839781 --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks.scala @@ -0,0 +1,62 @@ +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 diff --git a/tests/neg-custom-args/captures/sepchecks2.check b/tests/neg-custom-args/captures/sepchecks2.check new file mode 100644 index 000000000000..47a80306f3b7 --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks2.check @@ -0,0 +1,51 @@ +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:10:10 -------------------------------------------------------- +10 | println(c) // error + | ^ + | Separation failure: Illegal access to {c} which is hidden by the previous definition + | of value xs with type List[box () => Unit]. + | This type hides capabilities {c} +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:13:7 --------------------------------------------------------- +13 | foo((() => println(c)) :: Nil, c) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | Separation failure: argument of type List[box () ->{c} Unit] + | to method foo: (xs: List[box () => Unit], y: Object^): Nothing + | corresponds to capture-polymorphic formal parameter xs of type List[box () => Unit] + | and hides capabilities {c}. + | Some of these overlap with the captures of the second argument with type (c : Object^). + | + | Hidden set of current argument : {c} + | Hidden footprint of current argument : {c} + | Capture set of second argument : {c} + | Footprint set of second argument : {c} + | The two sets overlap at : {c} +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:14:10 -------------------------------------------------------- +14 | val x1: (Object^, Object^) = (c, c) // error + | ^^^^^^^^^^^^^^^^^^ + | Separation failure in value x1's type (box Object^, box Object^). + | One part, box Object^, hides capabilities {c}. + | Another part, box Object^, captures capabilities {c}. + | The two sets overlap at {c}. +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:15:10 -------------------------------------------------------- +15 | val x2: (Object^, Object^{d}) = (d, d) // error + | ^^^^^^^^^^^^^^^^^^^^^ + | Separation failure in value x2's type (box Object^, box Object^{d}). + | One part, box Object^, hides capabilities {d}. + | Another part, box Object^{d}, captures capabilities {d}. + | The two sets overlap at {d}. +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:27:6 --------------------------------------------------------- +27 | bar((c, c)) // error + | ^^^^^^ + | Separation failure in the argument's adapted type (box Object^, box Object^). + | One part, box Object^, hides capabilities {c}. + | Another part, box Object^, captures capabilities {c}. + | The two sets overlap at {c}. +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:30:9 --------------------------------------------------------- +30 | val x: (Object^, Object^{c}) = (d, c) // error + | ^^^^^^^^^^^^^^^^^^^^^ + | Separation failure: value x's type (box Object^, box Object^{c}) hides parameter d. + | The parameter needs to be annotated with @consume to allow this. +-- Error: tests/neg-custom-args/captures/sepchecks2.scala:33:9 --------------------------------------------------------- +33 | val x: (Object^, Object^) = (c, d) // error + | ^^^^^^^^^^^^^^^^^^ + | Separation failure: value x's type (box Object^, box Object^) hides parameters c and d. + | The parameters need to be annotated with @consume to allow this. diff --git a/tests/neg-custom-args/captures/sepchecks2.scala b/tests/neg-custom-args/captures/sepchecks2.scala new file mode 100644 index 000000000000..34ca3a56a243 --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks2.scala @@ -0,0 +1,35 @@ + +import caps.consume + +def foo(xs: List[() => Unit], y: Object^) = ??? + +def bar(x: (Object^, Object^)): Unit = ??? + +def Test(@consume c: Object^) = + val xs: List[() => Unit] = (() => println(c)) :: Nil + println(c) // error + +def Test2(c: Object^, d: Object^): Unit = + foo((() => println(c)) :: Nil, c) // error + val x1: (Object^, Object^) = (c, c) // error + val x2: (Object^, Object^{d}) = (d, d) // error + +def Test3(@consume c: Object^, @consume d: Object^) = + val x: (Object^, Object^) = (c, d) // ok + +def Test4(@consume c: Object^, @consume d: Object^) = + val x: (Object^, Object^{c}) = (d, c) // ok + +def Test5(c: Object^, d: Object^): Unit = + bar((c, d)) // ok + +def Test6(c: Object^, d: Object^): Unit = + bar((c, c)) // error + +def Test7(c: Object^, d: Object^) = + val x: (Object^, Object^{c}) = (d, c) // error + +def Test8(c: Object^, d: Object^) = + val x: (Object^, Object^) = (c, d) // error + + diff --git a/tests/neg-custom-args/captures/sepchecks3.scala b/tests/neg-custom-args/captures/sepchecks3.scala new file mode 100644 index 000000000000..8cc7d705b42b --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks3.scala @@ -0,0 +1,12 @@ + +import caps.consume + +def foo(xs: List[() => Unit], y: Object^) = ??? + +def bar(x: (Object^, Object^)): Unit = ??? + +def Test(c: Object^): Object^ = c // error + +def Test2(@consume c: Object^): Object^ = c // ok + +def Test3(c: Object^): List[Object^] = c :: Nil // error diff --git a/tests/neg-custom-args/captures/sepchecks4.check b/tests/neg-custom-args/captures/sepchecks4.check new file mode 100644 index 000000000000..5934be1ff637 --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks4.check @@ -0,0 +1,24 @@ +-- Error: tests/neg-custom-args/captures/sepchecks4.scala:8:12 --------------------------------------------------------- +8 | val x: () => Unit = () => println(io) // error + | ^^^^^^^^^^ + | Separation failure: value x's type () => Unit hides parameter io. + | The parameter needs to be annotated with @consume to allow this. +-- Error: tests/neg-custom-args/captures/sepchecks4.scala:7:25 --------------------------------------------------------- +7 |def bad(io: Object^): () => Unit = // error + | ^^^^^^^^^^ + | Separation failure: method bad's result type () => Unit hides parameter io. + | The parameter needs to be annotated with @consume to allow this. +-- Error: tests/neg-custom-args/captures/sepchecks4.scala:12:6 --------------------------------------------------------- +12 | par(() => println(io))(() => println(io)) // error // (1) + | ^^^^^^^^^^^^^^^^^ + | Separation failure: argument of type () ->{io} Unit + | to method par: (op1: () => Unit)(op2: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter op1 of type () => Unit + | and hides capabilities {io}. + | Some of these overlap with the captures of the second argument with type () ->{io} Unit. + | + | Hidden set of current argument : {io} + | Hidden footprint of current argument : {io} + | Capture set of second argument : {io} + | Footprint set of second argument : {io} + | The two sets overlap at : {io} diff --git a/tests/neg-custom-args/captures/sepchecks4.scala b/tests/neg-custom-args/captures/sepchecks4.scala new file mode 100644 index 000000000000..d44b31ca02dc --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks4.scala @@ -0,0 +1,16 @@ +import caps.cap +import language.future +import language.experimental.captureChecking + +def par(op1: () => Unit)(op2: () => Unit): Unit = () + +def bad(io: Object^): () => Unit = // error + val x: () => Unit = () => println(io) // error + x + +def test(io: Object^): Unit = + par(() => println(io))(() => println(io)) // error // (1) + + val f = bad(io) + par(f)(() => println(io)) // no error, but it is equivalent to (1) and should failimport caps.consume + diff --git a/tests/neg-custom-args/captures/sepchecks5.check b/tests/neg-custom-args/captures/sepchecks5.check new file mode 100644 index 000000000000..aae5cb89da53 --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks5.check @@ -0,0 +1,16 @@ +-- Error: tests/neg-custom-args/captures/sepchecks5.scala:12:37 -------------------------------------------------------- +12 |def bad(io: Object^): () => Unit = f(io) // error + | ^^ + | Separation failure: argument to @consume parameter with type (io : Object^) refers to parameter io. + | The parameter needs to be annotated with @consume to allow this. +-- Error: tests/neg-custom-args/captures/sepchecks5.scala:19:13 -------------------------------------------------------- +19 | val f2 = g(io) // error + | ^^ + | Separation failure: argument to @consume parameter with type (io : Object^) refers to parameter io. + | The parameter needs to be annotated with @consume to allow this. +-- Error: tests/neg-custom-args/captures/sepchecks5.scala:20:24 -------------------------------------------------------- +20 | par(f2)(() => println(io)) // error + | ^^ + | Separation failure: Illegal access to (io : Object^), which was passed to a + | @consume parameter or was used as a prefix to a @consume method on line 19 + | and therefore is no longer available. diff --git a/tests/neg-custom-args/captures/sepchecks5.scala b/tests/neg-custom-args/captures/sepchecks5.scala new file mode 100644 index 000000000000..9a16051e64bd --- /dev/null +++ b/tests/neg-custom-args/captures/sepchecks5.scala @@ -0,0 +1,21 @@ +import caps.{cap, consume} +import language.future +import language.experimental.captureChecking + +def par(op1: () => Unit)(op2: () => Unit): Unit = () + +def f(@consume io: Object^): () => Unit = + () => println(io) + +def g(@consume io: Object^): () => Unit = f(io) // ok + +def bad(io: Object^): () => Unit = f(io) // error + +def test(io: Object^): Unit = + + val f1 = bad(io) + par(f1)(() => println(io)) // !!! separation failure + + val f2 = g(io) // error + par(f2)(() => println(io)) // error + diff --git a/tests/neg-custom-args/captures/shared-capability.check b/tests/neg-custom-args/captures/shared-capability.check new file mode 100644 index 000000000000..64fb3eb39d44 --- /dev/null +++ b/tests/neg-custom-args/captures/shared-capability.check @@ -0,0 +1,4 @@ +-- Error: tests/neg-custom-args/captures/shared-capability.scala:9:13 -------------------------------------------------- +9 |def test2(a: Async^): Object^ = a // error + | ^^^^^^ + | Async^ extends SharedCapability, so it cannot capture `cap` diff --git a/tests/neg-custom-args/captures/shared-capability.scala b/tests/neg-custom-args/captures/shared-capability.scala new file mode 100644 index 000000000000..262a6db386ba --- /dev/null +++ b/tests/neg-custom-args/captures/shared-capability.scala @@ -0,0 +1,10 @@ + + +import caps.SharedCapability + +class Async extends SharedCapability + +def test1(a: Async): Object^ = a // OK + +def test2(a: Async^): Object^ = a // error + diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 23c1b056c659..b67dc464d929 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -3,11 +3,13 @@ | ^^^^^^^^^^^^^^^^^^^ | Type variable R of method handle cannot be instantiated to box CT[Exception]^ since | that type captures the root capability `cap`. --- Error: tests/neg-custom-args/captures/try.scala:30:65 --------------------------------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:30:32 ------------------------------------------ 30 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error - | ^ - | reference (x : CT[Exception]^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () ->? Nothing + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: () ->{x} Nothing + | Required: () ->? Nothing + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:52:2 ------------------------------------------- 47 |val global: () -> Int = handle { 48 | (x: CanThrow[Exception]) => @@ -16,11 +18,8 @@ 51 | 22 52 |} { // error | ^ - | Found: () ->{x, x²} Int + | Found: () ->{x} Int | Required: () -> Int - | - | where: x is a reference to a value parameter - | x² is a reference to a value parameter 53 | (ex: Exception) => () => 22 54 |} | diff --git a/tests/neg-custom-args/captures/unbox.scala b/tests/neg-custom-args/captures/unbox.scala deleted file mode 100644 index 28feb5f89aff..000000000000 --- a/tests/neg-custom-args/captures/unbox.scala +++ /dev/null @@ -1,6 +0,0 @@ -import language.`3.5` -type Proc = () => Unit - -val xs: List[Proc] = ??? - -val x = xs.head // error diff --git a/tests/neg-custom-args/captures/unsound-reach-2.scala b/tests/neg-custom-args/captures/unsound-reach-2.scala index c7dfa117a2fe..944ef82da5bb 100644 --- a/tests/neg-custom-args/captures/unsound-reach-2.scala +++ b/tests/neg-custom-args/captures/unsound-reach-2.scala @@ -1,4 +1,4 @@ -import language.experimental.captureChecking +import language.experimental.captureChecking; trait Consumer[-T]: def apply(x: T): Unit @@ -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..46ed469a308e 100644 --- a/tests/neg-custom-args/captures/unsound-reach-3.scala +++ b/tests/neg-custom-args/captures/unsound-reach-3.scala @@ -1,23 +1,24 @@ - import language.experimental.captureChecking +import caps.consume + trait File: def close(): Unit def withFile[R](path: String)(op: File^ => R): R = ??? trait Foo[+X]: - def use(x: File^): X + def use(@consume x: File^): X class Bar extends Foo[File^]: // error - def use(x: File^): File^ = x + def use(@consume x: File^): File^ = x // error override 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 withFile("hello.txt"): f => - escaped = boom.use(f) // error - // boom.use: (x: File^) -> File^{backdoor*}, it is a selection so reach capabilities are allowed + escaped = boom.use(f) // error: separation + // was boom.use: (x: File^) -> File^{backdoor*}, it is a selection so reach capabilities are allowed // f: File^, so there is no reach capabilities diff --git a/tests/neg-custom-args/captures/unsound-reach-4.check b/tests/neg-custom-args/captures/unsound-reach-4.check index ca95bf42ba59..f21d1403eb3d 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.check +++ b/tests/neg-custom-args/captures/unsound-reach-4.check @@ -1,9 +1,19 @@ --- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:13:18 --------------------------------------------------- -13 |class Bar extends Foo[File^]: // error +-- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:16:18 --------------------------------------------------- +16 |class Bar extends Foo[File^]: // error | ^^^^^^^^^^ | Type variable X of trait Foo cannot be instantiated to File^ since | that type captures the root capability `cap`. --- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:22:22 --------------------------------------------------- -22 | escaped = boom.use(f) // error - | ^^^^^^^^^^^ - | Local reach capability backdoor* leaks into capture scope of method bad +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach-4.scala:20:29 ------------------------------ +20 | 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` +-- [E164] Declaration Error: tests/neg-custom-args/captures/unsound-reach-4.scala:17:6 --------------------------------- +17 | def use(@consume x: F): File^ = x // error @consume override + | ^ + | error overriding method use in trait Foo of type (x: File^): box File^; + | method use of type (x: File^): File^ has incompatible type + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/unsound-reach-4.scala b/tests/neg-custom-args/captures/unsound-reach-4.scala index 88fbc2f5c1de..715dfcc90ef8 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.scala +++ b/tests/neg-custom-args/captures/unsound-reach-4.scala @@ -1,6 +1,9 @@ -import language.experimental.captureChecking + +import language.experimental.captureChecking; +import caps.consume + trait File: def close(): Unit @@ -11,12 +14,12 @@ type F = File^ trait Foo[+X]: def use(x: F): X class Bar extends Foo[File^]: // error - def use(x: F): File^ = x + def use(@consume x: F): File^ = x // error @consume override 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 withFile("hello.txt"): f => - escaped = boom.use(f) // error + escaped = boom.use(f) // was error diff --git a/tests/neg-custom-args/captures/unsound-reach-6.check b/tests/neg-custom-args/captures/unsound-reach-6.check new file mode 100644 index 000000000000..ed81271efa90 --- /dev/null +++ b/tests/neg-custom-args/captures/unsound-reach-6.check @@ -0,0 +1,28 @@ +-- Error: tests/neg-custom-args/captures/unsound-reach-6.scala:7:13 ---------------------------------------------------- +7 | println(xs.head) // error + | ^^^^^^^ + | Local reach capability xs* leaks into capture scope of method f. + | To allow this, the parameter xs should be declared with a @use annotation +-- Error: tests/neg-custom-args/captures/unsound-reach-6.scala:11:14 --------------------------------------------------- +11 | val z = f(ys) // error @consume failure + | ^^ + | Local reach capability ys* leaks into capture scope of method test. + | To allow this, the parameter ys should be declared with a @use annotation +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach-6.scala:13:22 ------------------------------ +13 | val _: () -> Unit = x // error + | ^ + | Found: (x : () ->{ys*} Unit) + | Required: () -> Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach-6.scala:21:22 ------------------------------ +21 | val _: () -> Unit = x // error + | ^ + | Found: (x : () ->{io} Unit) + | Required: () -> Unit + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/unsound-reach-6.scala:19:14 --------------------------------------------------- +19 | val z = f(ys) // error @consume failure + | ^^ + |Separation failure: argument to @consume parameter with type (ys : -> List[box () ->{io} Unit]) refers to non-local parameter io diff --git a/tests/neg-custom-args/captures/unsound-reach-6.scala b/tests/neg-custom-args/captures/unsound-reach-6.scala index b7306dca4190..4643950d78d0 100644 --- a/tests/neg-custom-args/captures/unsound-reach-6.scala +++ b/tests/neg-custom-args/captures/unsound-reach-6.scala @@ -1,21 +1,24 @@ + +import caps.consume + class IO -def f(xs: List[() => Unit]): () => Unit = () => +def f(@consume xs: List[() => Unit]): () => Unit = () => println(xs.head) // error def test(io: IO^)(ys: List[() ->{io} Unit]) = val x = () => - val z = f(ys) + val z = f(ys) // error @consume failure z() - val _: () -> Unit = x // !!! ys* gets lost + val _: () -> Unit = x // error () def test(io: IO^) = def ys: List[() ->{io} Unit] = ??? val x = () => - val z = f(ys) + val z = f(ys) // error @consume failure z() - val _: () -> Unit = x // !!! io gets lost + val _: () -> Unit = x // error () diff --git a/tests/neg-custom-args/captures/unsound-reach.check b/tests/neg-custom-args/captures/unsound-reach.check index 69794f569edb..a1ebd30e4915 100644 --- a/tests/neg-custom-args/captures/unsound-reach.check +++ b/tests/neg-custom-args/captures/unsound-reach.check @@ -8,8 +8,10 @@ | ^ | Type variable X of constructor Foo2 cannot be instantiated to box File^ since | that type captures the root capability `cap`. --- Error: tests/neg-custom-args/captures/unsound-reach.scala:23:21 ----------------------------------------------------- -23 | boom.use(f): (f1: File^{backdoor*}) => // error - | ^ - | Local reach capability backdoor* leaks into capture scope of method bad -24 | escaped = f1 +-- [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` diff --git a/tests/neg-custom-args/captures/unsound-reach.scala b/tests/neg-custom-args/captures/unsound-reach.scala index 3fb666c7c1fc..a53b091c5918 100644 --- a/tests/neg-custom-args/captures/unsound-reach.scala +++ b/tests/neg-custom-args/captures/unsound-reach.scala @@ -1,4 +1,4 @@ -import language.experimental.captureChecking +import language.experimental.captureChecking; trait File: def close(): Unit @@ -15,11 +15,11 @@ 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 withFile("hello.txt"): f => - boom.use(f): (f1: File^{backdoor*}) => // error + boom.use(f): (f1: File^{backdoor*}) => // was error escaped = f1 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/use-capset.check b/tests/neg-custom-args/captures/use-capset.check index 74afaa05890f..4897698e336d 100644 --- a/tests/neg-custom-args/captures/use-capset.check +++ b/tests/neg-custom-args/captures/use-capset.check @@ -13,7 +13,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/use-capset.scala:13:50 ----------------------------------- 13 | val _: () -> List[Object^{io}] -> Object^{io} = h2 // error, should be ->{io} | ^^ - | Found: (h2 : () ->? (x$0: List[box Object^{io}]^{}) ->{io} Object^{io}) - | Required: () -> List[box Object^{io}] -> Object^{io} + | Found: (h2 : () ->? List[box Object^{io}]^{} ->{io} Object^{io}) + | Required: () -> List[box Object^{io}] -> Object^{io} | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/vars-simple.check b/tests/neg-custom-args/captures/vars-simple.check index 2bc014e9a4e7..71fab0dcf7d2 100644 --- a/tests/neg-custom-args/captures/vars-simple.check +++ b/tests/neg-custom-args/captures/vars-simple.check @@ -15,7 +15,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars-simple.scala:17:12 ---------------------------------- 17 | b = List(g) // error | ^^^^^^^ - | Found: List[box (x$0: String) ->{cap3} String] + | Found: List[box String ->{cap3} String] | Required: List[box String ->{cap1, cap2} String] | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index db5c8083e3b7..bd5e017a2b0c 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -1,15 +1,18 @@ --- Error: tests/neg-custom-args/captures/vars.scala:24:14 -------------------------------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:24:8 ------------------------------------------ 24 | a = x => g(x) // error - | ^^^^ - | reference (cap3 : CC^) is not included in the allowed capture set {cap1} of variable a + | ^^^^^^^^^ + | Found: (x: String) ->{cap3} String + | Required: (x: String) ->{cap1} String | - | Note that reference (cap3 : CC^), 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} of variable a + | + | longer explanation available when compiling with `-explain` -- [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 + | Required: (x: String) ->{cap1} String | | Note that reference (cap3 : CC^), defined in method scope | cannot be included in outer capture set {cap1} of variable a @@ -18,7 +21,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:27:12 ----------------------------------------- 27 | b = List(g) // error | ^^^^^^^ - | Found: List[box (x$0: String) ->{cap3} String] + | Found: List[box String ->{cap3} String] | Required: List[box String ->{cap1, cap2} String] | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/widen-reach.check b/tests/neg-custom-args/captures/widen-reach.check index 9fe1f2bd5de6..4d4e42601cb0 100644 --- a/tests/neg-custom-args/captures/widen-reach.check +++ b/tests/neg-custom-args/captures/widen-reach.check @@ -13,3 +13,10 @@ | ^^^^^^ | Local reach capability x* leaks into capture scope of method test. | To allow this, the parameter x should be declared with a @use annotation +-- [E164] Declaration Error: tests/neg-custom-args/captures/widen-reach.scala:9:6 -------------------------------------- +9 | val foo: IO^ -> IO^ = x => x // error + | ^ + | error overriding value foo in trait Foo of type IO^ -> box IO^; + | value foo of type IO^ -> IO^ has incompatible type + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/widen-reach.scala b/tests/neg-custom-args/captures/widen-reach.scala index 9a9305640473..6d3a57d6a669 100644 --- a/tests/neg-custom-args/captures/widen-reach.scala +++ b/tests/neg-custom-args/captures/widen-reach.scala @@ -6,7 +6,7 @@ trait Foo[+T]: val foo: IO^ -> T trait Bar extends Foo[IO^]: // error - val foo: IO^ -> IO^ = x => x + val foo: IO^ -> IO^ = x => x // error def test(x: Foo[IO^]): Unit = val y1: Foo[IO^{x*}] = x diff --git a/tests/neg/polymorphic-erased-functions-types.check b/tests/neg/polymorphic-erased-functions-types.check index 78a0ab37b1ab..f46940d61fa2 100644 --- a/tests/neg/polymorphic-erased-functions-types.check +++ b/tests/neg/polymorphic-erased-functions-types.check @@ -9,7 +9,10 @@ 4 |def t1b: [T] => (erased t: T) => Unit = [T] => (t: T) => () // error | ^^^^^^^^^^^^^^^^^^^ | Found: [T] => (t: T) => Unit - | Required: [T] => (erased t: T) => Unit + | Required: [T] => (erased t²: T) => Unit + | + | where: t is a reference to a value parameter + | t² is a reference to a value parameter | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/polymorphic-erased-functions-types.scala:6:36 --------------------------------- @@ -23,6 +26,11 @@ 7 |def t2b: [T, U] => (t: T, erased u: U) => Unit = [T, U] => (t: T, u: U) => () // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Found: [T, U] => (t: T, u: U) => Unit - | Required: [T, U] => (t: T, erased u: U) => Unit + | Required: [T, U] => (t²: T, erased u²: U) => Unit + | + | where: t is a reference to a value parameter + | t² is a reference to a value parameter + | u is a reference to a value parameter + | u² is a reference to a value parameter | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/polymorphic-functions1.check b/tests/neg/polymorphic-functions1.check index eef268c298cf..f925fe7b0f52 100644 --- a/tests/neg/polymorphic-functions1.check +++ b/tests/neg/polymorphic-functions1.check @@ -2,6 +2,9 @@ 1 |val f: [T] => (x: T) => x.type = [T] => (x: Int) => x // error | ^^^^^^^^^^^^^^^^^^^^ | Found: [T] => (x: Int) => x.type - | Required: [T] => (x: T) => x.type + | Required: [T] => (x²: T) => x².type + | + | where: x is a reference to a value parameter + | x² is a reference to a value parameter | | 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 20282d5813f9..436132280d40 100644 --- a/tests/pos-custom-args/captures/boxmap-paper.scala +++ b/tests/pos-custom-args/captures/boxmap-paper.scala @@ -1,3 +1,4 @@ +import caps.cap type Cell_orig[+T] = [K] -> (T => K) -> K @@ -18,13 +19,13 @@ 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 } diff --git a/tests/pos-custom-args/captures/capt1.scala b/tests/pos-custom-args/captures/capt1.scala index e3f5c20e724e..f8cf39933bef 100644 --- a/tests/pos-custom-args/captures/capt1.scala +++ b/tests/pos-custom-args/captures/capt1.scala @@ -1,3 +1,6 @@ + +import caps.unsafe.unsafeAssumeSeparate + class C type Cap = C^ def f1(c: Cap): () ->{c} c.type = () => c // ok @@ -22,6 +25,9 @@ def foo(): C^ = val z1: () => Cap = f1(x) def h[X](a: X)(b: X) = a - val z2 = - if x == null then () => x else () => C() - x \ No newline at end of file + val z2 = unsafeAssumeSeparate: + if x == null then + () => x + else + () => C() + unsafeAssumeSeparate(x) \ No newline at end of file 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-custom-args/captures/cc-poly-source-capability.scala b/tests/pos-custom-args/captures/cc-poly-source-capability.scala index 6f6bdd91d20a..6f987658923c 100644 --- a/tests/pos-custom-args/captures/cc-poly-source-capability.scala +++ b/tests/pos-custom-args/captures/cc-poly-source-capability.scala @@ -1,11 +1,11 @@ import language.experimental.captureChecking import annotation.experimental -import caps.{CapSet, Capability} +import caps.{CapSet, SharedCapability} import caps.use @experimental object Test: - class Async extends Capability + class Async extends SharedCapability def listener(async: Async): Listener^{async} = ??? diff --git a/tests/pos-custom-args/captures/cc-this.scala b/tests/pos-custom-args/captures/cc-this.scala index d9705df76c55..638c20d94a91 100644 --- a/tests/pos-custom-args/captures/cc-this.scala +++ b/tests/pos-custom-args/captures/cc-this.scala @@ -1,8 +1,11 @@ +import caps.consume + + class Cap extends caps.Capability def eff(using Cap): Unit = () -def test(using Cap) = +def test(using @consume cc: Cap) = class C(val x: () => Int): val y: C^ = this diff --git a/tests/pos-custom-args/captures/erased-methods.scala b/tests/pos-custom-args/captures/erased-methods.scala index 911c779e08e5..43347c6d9172 100644 --- a/tests/pos-custom-args/captures/erased-methods.scala +++ b/tests/pos-custom-args/captures/erased-methods.scala @@ -7,14 +7,14 @@ class Ex2 extends Exception("Ex2") class Ex3 extends Exception("Ex3") def foo8a(i: Int) = - (erased xx1: CanThrow[Ex2]^) ?=> throw new Ex2 + (erased xx1: CanThrow[Ex2]) ?=> throw new Ex2 def foo9a(i: Int) - : (erased x$0: CanThrow[Ex3]^) - ?=> (erased x$1: CanThrow[Ex2]^) - ?=> (erased x$2: CanThrow[Ex1]^) + : (erased x$0: CanThrow[Ex3]) + ?=> (erased x$1: CanThrow[Ex2]) + ?=> (erased x$2: CanThrow[Ex1]) ?=> Unit - = (erased x$1: CanThrow[Ex3]^) - ?=> (erased x$2: CanThrow[Ex2]^) - ?=> (erased x$3: CanThrow[Ex1]^) + = (erased x$1: CanThrow[Ex3]) + ?=> (erased x$2: CanThrow[Ex2]) + ?=> (erased x$3: CanThrow[Ex1]) ?=> throw new Ex3 diff --git a/tests/pos-custom-args/captures/eta-expansions.scala b/tests/pos-custom-args/captures/eta-expansions.scala index b4e38cdf0856..cbe72137bd65 100644 --- a/tests/pos-custom-args/captures/eta-expansions.scala +++ b/tests/pos-custom-args/captures/eta-expansions.scala @@ -3,7 +3,7 @@ class Cap extends caps.Capability def test(d: Cap) = def map2(xs: List[Int])(f: Int => Int): List[Int] = xs.map(f) val f1 = map2 // capture polymorphic implicit eta expansion - def f2c: List[Int] => (Int => Int) => List[Int] = f1 + val f2c: List[Int] => (Int => Int) => List[Int] = f1 val a0 = identity[Cap ->{d} Unit] // capture monomorphic implicit eta expansion val a0c: (Cap ->{d} Unit) ->{d} Cap ->{d} Unit = a0 val b0 = (x: Cap ->{d} Unit) => identity[Cap ->{d} Unit](x) // not an implicit eta expansion, hence capture polymorphic diff --git a/tests/pos-custom-args/captures/filter-iterable.scala b/tests/pos-custom-args/captures/filter-iterable.scala new file mode 100644 index 000000000000..c8e80af4cd73 --- /dev/null +++ b/tests/pos-custom-args/captures/filter-iterable.scala @@ -0,0 +1,11 @@ +import caps.cap + +class It[A] + +class Filter[A](val underlying: It[A]^, val p: A ->{cap, underlying} Boolean) extends It[A] +object Filter: + def apply[A](underlying: It[A]^, p: A => Boolean): Filter[A]^{cap, p, underlying} = + underlying match + case filter: Filter[A]^ => + val x = new Filter(filter.underlying, a => filter.p(a) && p(a)) + x: Filter[A]^{filter, p} \ No newline at end of file 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/hk-param.scala b/tests/pos-custom-args/captures/hk-param.scala deleted file mode 100644 index 325a2b55a480..000000000000 --- a/tests/pos-custom-args/captures/hk-param.scala +++ /dev/null @@ -1,18 +0,0 @@ -//> using options -source 3.5 -// (to make sure we use the unsealed policy) -/** Concrete collection type: View */ -trait View[+A] extends Itable[A], ILike[A, [X] =>> View[X]^]: - override def fromIterable[B](c: Itable[B]^): View[B]^{c} = ??? - -trait IPolyTransforms[+A, +C[A]] extends Any: - def fromIterable[B](coll: Itable[B]^): C[B] - -trait ILike[+A, +C[X] <: Itable[X]^] extends IPolyTransforms[A, C] - -/** Base trait for generic collections */ -trait Itable[+A] extends ItableOnce[A] with ILike[A, Itable^] - -/** Iterator can be used only once */ -trait ItableOnce[+A] { - def iterator: Iterator[A]^{this} -} diff --git a/tests/pos-custom-args/captures/i15749.scala b/tests/pos-custom-args/captures/i15749.scala index 58274c7cc817..dd92aced77a5 100644 --- a/tests/pos-custom-args/captures/i15749.scala +++ b/tests/pos-custom-args/captures/i15749.scala @@ -1,5 +1,4 @@ -//> using options -source 3.4 -// (to make sure we use the sealed policy) +import caps.use class Unit object unit extends Unit @@ -12,6 +11,6 @@ class Foo[T](val x: T) // Foo[□ Unit => T] type BoxedLazyVal[T] = Foo[LazyVal[T]] -def force[A](v: BoxedLazyVal[A]): A = +def force[A](@use v: BoxedLazyVal[A]): A = // Γ ⊢ v.x : □ {cap} Unit -> A v.x(unit) // should be error: (unbox v.x)(unit), where (unbox v.x) should be untypable, now ok \ No newline at end of file diff --git a/tests/pos-custom-args/captures/i15749a.scala b/tests/pos-custom-args/captures/i15749a.scala index 184f980d6d70..c008d20a155c 100644 --- a/tests/pos-custom-args/captures/i15749a.scala +++ b/tests/pos-custom-args/captures/i15749a.scala @@ -1,6 +1,7 @@ import caps.cap import caps.use + class Unit object u extends Unit @@ -13,7 +14,7 @@ def test = 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] = + def strictMap[A <: Top, B <: Top](mx: Wrapper[A])(f: A ->{cap, mx*} B): Wrapper[B] = mx.value((x: A) => wrapper(f(x))) def force[A](thunk: Unit ->{cap} A): A = thunk(u) diff --git a/tests/pos-custom-args/captures/i22723.scala b/tests/pos-custom-args/captures/i22723.scala new file mode 100644 index 000000000000..7fc20f813f7c --- /dev/null +++ b/tests/pos-custom-args/captures/i22723.scala @@ -0,0 +1,20 @@ + +import language.experimental.captureChecking +import language.experimental.modularity +import caps.* + +case class A() + +trait HasCap: + def mkA: A^{this} + +object HasCap: + def apply[T](body: HasCap^ ?=> T): T = ??? + +class Box(using tracked val h: HasCap^): + var t: A^{h} = h.mkA + +def main() = + HasCap: h1 ?=> + val b = Box(using h1) + b.t = h1.mkA diff --git a/tests/pos-custom-args/captures/lazyref.scala b/tests/pos-custom-args/captures/lazyref.scala index 2e3a0030bcdc..f4c85de9a51a 100644 --- a/tests/pos-custom-args/captures/lazyref.scala +++ b/tests/pos-custom-args/captures/lazyref.scala @@ -1,3 +1,6 @@ + +import caps.consume + class Cap extends caps.Capability class LazyRef[T](val elem: () => T): @@ -11,7 +14,7 @@ def map[A, B](ref: LazyRef[A]^, f: A => B): LazyRef[B]^{f, ref} = def mapc[A, B]: (ref: LazyRef[A]^, f: A => B) => LazyRef[B]^{f, ref} = (ref1, f1) => map[A, B](ref1, f1) -def test(cap1: Cap, cap2: Cap) = +def test(@consume cap1: Cap, @consume cap2: Cap) = def f(x: Int) = if cap1 == cap1 then x else 0 def g(x: Int) = if cap2 == cap2 then x else 0 val ref1 = LazyRef(() => f(0)) diff --git a/tests/pos-custom-args/captures/levels.scala b/tests/pos-custom-args/captures/levels.scala deleted file mode 100644 index 4d9d759e86db..000000000000 --- a/tests/pos-custom-args/captures/levels.scala +++ /dev/null @@ -1,25 +0,0 @@ -//> using options -source 3.5 -// (to make sure we use the unsealed policy) -class CC - -def test1(cap1: CC^) = - - class Ref[T](init: T): - private var v: T = init - def setV(x: T): Unit = v = x - def getV: T = v - -def test2(cap1: CC^) = - - class Ref[T](init: T): - private var v: T = init - def setV(x: T): Unit = v = x - def getV: T = v - - val _ = Ref[String => String]((x: String) => x) - val r = Ref((x: String) => x) - - def scope(cap3: CC^) = - def g(x: String): String = if cap3 == cap3 then "" else "a" - r.setV(g) - () diff --git a/tests/pos-custom-args/captures/list-encoding.scala b/tests/pos-custom-args/captures/list-encoding.scala index d959b523404b..7c593bb524e3 100644 --- a/tests/pos-custom-args/captures/list-encoding.scala +++ b/tests/pos-custom-args/captures/list-encoding.scala @@ -4,7 +4,7 @@ import annotation.retains class Cap type Op[T, C] = - (v: T) => (s: C) => C + T => C => C type List[T] = [C] -> (op: Op[T, C]) -> (s: C) ->{op} C 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/path-use.scala b/tests/pos-custom-args/captures/path-use.scala index 629fa04315a7..49268a449567 100644 --- a/tests/pos-custom-args/captures/path-use.scala +++ b/tests/pos-custom-args/captures/path-use.scala @@ -1,22 +1,25 @@ import language.experimental.namedTuples -import caps.use class IO -class C(val f: IO^): +class C(val ff: IO^): val procs: List[Proc] = ??? type Proc = () => Unit def test(io: IO^) = - def test1(@use c: C { val f: IO^{io}}^{io}) = - val f = () => println(c.f) - val _: () ->{c.f} Unit = f + val c = C(io) + val f = () => println(c.ff) + val _: () ->{c.ff} Unit = f - val x = c.procs - val _: List[() ->{c.procs*} Unit] = x + val x = c.procs + val _: List[() ->{c.procs*} Unit] = x - val g = () => println(c.procs.head) - val _: () ->{c.procs*} Unit = g - test1(C(io)) + val g = () => println(c.procs.head) // was error, local reach capability c.procs* leaks + val _: () ->{c.procs*} Unit = g + val cc: C { val f: IO^{io}; val procs: List[() ->{io} Unit] }^{io} = + ??? + + val gg = () => println(cc.procs.head) // OK, since cc.procs* has {io} as underlying capture set + val _: () ->{io} Unit = gg diff --git a/tests/pos-custom-args/captures/reach-capability.scala b/tests/pos-custom-args/captures/reach-capability.scala index 08e82a1dabe9..7160b280ce4f 100644 --- a/tests/pos-custom-args/captures/reach-capability.scala +++ b/tests/pos-custom-args/captures/reach-capability.scala @@ -1,6 +1,6 @@ import language.experimental.captureChecking import annotation.experimental -import caps.Capability +import caps.SharedCapability import caps.use @experimental object Test2: @@ -8,7 +8,7 @@ import caps.use class List[+A]: def map[B](f: A => B): List[B] = ??? - class Label extends Capability + class Label extends SharedCapability class Listener diff --git a/tests/pos-custom-args/captures/reaches.scala b/tests/pos-custom-args/captures/reaches.scala index cbe88e60020b..131dce862b02 100644 --- a/tests/pos-custom-args/captures/reaches.scala +++ b/tests/pos-custom-args/captures/reaches.scala @@ -1,4 +1,5 @@ -import caps.use + +import caps.{use, consume} class C def f(xs: List[C^]) = @@ -36,7 +37,7 @@ def cons(x: Proc, xs: List[Proc]): List[() ->{x, xs*} Unit] = val y = x :: xs y -def addOneProc(xs: List[Proc]): List[Proc] = +def addOneProc(@consume xs: List[Proc]): List[Proc] = val x: Proc = () => println("hello") val result: List[() ->{x, xs*} Unit] = x :: xs result // OK, we can widen () ->{x, xs*} Unit to cap here. @@ -44,7 +45,7 @@ def addOneProc(xs: List[Proc]): List[Proc] = def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C = z => g(f(z)) -def compose2[A, B, C](f: A => B, g: B => C): A => C = +def compose2[A, B, C](@consume f: A => B, @consume g: B => C): A => C = z => g(f(z)) //def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] = diff --git a/tests/pos-custom-args/captures/sep-compose.scala b/tests/pos-custom-args/captures/sep-compose.scala new file mode 100644 index 000000000000..3a56bd789282 --- /dev/null +++ b/tests/pos-custom-args/captures/sep-compose.scala @@ -0,0 +1,21 @@ +import caps.cap + + +def seq1(x: () => Unit, y: () ->{x, cap} Unit): Unit = + x(); y() + +def seq2(x: () => Unit)(y: () ->{x, cap} Unit): Unit = + x(); y() + +def test(io: Object^, a: Object^{io}): Unit = + val f = () => println(a) + val g = () => println(a) + seq1(f, f) + seq2(f)(f) + seq1(g, g) + seq2(g)(g) + + seq1(f, g) + seq2(f)(g) + seq1(g, f) + seq2(g)(f) \ No newline at end of file diff --git a/tests/pos-custom-args/captures/sep-eq.scala b/tests/pos-custom-args/captures/sep-eq.scala new file mode 100644 index 000000000000..94a64cc0c8f4 --- /dev/null +++ b/tests/pos-custom-args/captures/sep-eq.scala @@ -0,0 +1,20 @@ +import caps.Mutable +import caps.cap + + +extension (x: Object^) + infix def eql (y: Object^{x, cap}): Boolean = x eq y + +def eql1(x: Object^, y: Object^{x, cap}): Boolean = x eql y +def eql2(x: Object^)(y: Object^{x, cap}): Boolean = x eql y + +class LLI extends Object: + this: LLI^ => + + val f: Object^ = ??? + + def foo = + def these = f + val eq0 = these eql these + val eq1 = eql2(f)(f) + val eq2 = eql2(these)(these) diff --git a/tests/pos-custom-args/captures/sep-pairs.scala b/tests/pos-custom-args/captures/sep-pairs.scala new file mode 100644 index 000000000000..df1a3098d22d --- /dev/null +++ b/tests/pos-custom-args/captures/sep-pairs.scala @@ -0,0 +1,21 @@ +import caps.Mutable +import caps.{cap, consume, use} + +class Ref extends Mutable: + var x = 0 + def get: Int = x + mut def put(y: Int): Unit = x = y + +case class Pair[+A, +B](fst: A, snd: B) + +def mkPair: Pair[Ref^, Ref^] = + val r1 = Ref() + val r2 = Ref() + val p_exact: Pair[Ref^{r1}, Ref^{r2}] = Pair(r1, r2) + p_exact + +def copyPair(@consume @use p: Pair[Ref^, Ref^]): Pair[Ref^, Ref^] = + val x: Ref^{p.fst*} = p.fst + val y: Ref^{p.snd*} = p.snd + Pair(x, y) + 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..a9ff6e258317 --- /dev/null +++ b/tests/pos-custom-args/captures/skolems2.scala @@ -0,0 +1,19 @@ + +import caps.consume +import caps.unsafe.unsafeAssumeSeparate + +def Test(@consume c: Object^, @consume f: Object^ => Object^) = + def cc: Object^ = unsafeAssumeSeparate(c) + val x1 = + { f(cc) } + val x2 = + f(cc) + val x3: Object^ = + f(cc) + val x4: Object^ = + { unsafeAssumeSeparate(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/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala index 5443758afa72..0d09e5c8cb40 100644 --- a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -5,6 +5,8 @@ import Predef.{augmentString as _, wrapString as _, *} import scala.reflect.ClassTag import annotation.unchecked.{uncheckedVariance, uncheckedCaptures} import annotation.tailrec +import caps.cap +import caps.unsafe.unsafeAssumeSeparate /** A strawman architecture for new collections. It contains some * example collection classes and methods with the intent to expose @@ -29,7 +31,7 @@ object CollectionStrawMan5 { /** Base trait for instances that can construct a collection from an iterable */ trait FromIterable { type C[X] <: Iterable[X]^ - def fromIterable[B](it: Iterable[B]^): C[B]^{it} + def fromIterable[B](it: Iterable[B]^{this, cap}): C[B]^{it} } type FromIterableOf[+CC[X] <: Iterable[X]^] = FromIterable { @@ -60,17 +62,16 @@ object CollectionStrawMan5 { trait SeqFactory extends IterableFactory { type C[X] <: Seq[X] - def fromIterable[B](it: Iterable[B]^): C[B] + def fromIterable[B](it: Iterable[B]^{this, cap}): C[B] } /** Base trait for strict collections */ trait Buildable[+A] extends Iterable[A] { - protected[this] def newBuilder: Builder[A, Repr] @uncheckedVariance - override def partition(p: A => Boolean): (Repr, Repr) = { + protected def newBuilder: Builder[A, Repr] @uncheckedVariance + override def partition(p: A => Boolean): (Repr, Repr) = val l, r = newBuilder iterator.foreach(x => (if (p(x)) l else r) += x) (l.result, r.result) - } // one might also override other transforms here to avoid generating // iterators if it helps efficiency. } @@ -105,7 +106,7 @@ object CollectionStrawMan5 { with IterablePolyTransforms[A] with IterableMonoTransforms[A] { // sound bcs of VarianceNote type Repr = C[A] @uncheckedVariance - protected[this] def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr @uncheckedVariance ^{coll} = + protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^ {this, cap}): Repr @uncheckedVariance ^{coll} = fromIterable(coll) } @@ -115,7 +116,7 @@ object CollectionStrawMan5 { this: SeqLike[A] => type C[X] <: Seq[X] def fromIterable[B](coll: Iterable[B]^): C[B] - override protected[this] def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr = + override protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr = fromIterable(coll) trait IterableOps[+A] extends Any { @@ -134,7 +135,7 @@ object CollectionStrawMan5 { this: IterableMonoTransforms[A]^ => type Repr protected def coll: Iterable[A]^{this} - protected[this] def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr^{coll} + protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^ {this, cap}): Repr^{coll} def filter(p: A => Boolean): Repr^{this, p} = fromLikeIterable(View.Filter(coll, p)) def partition(p: A => Boolean): (Repr^{this, p}, Repr^{this, p}) = { @@ -153,7 +154,7 @@ object CollectionStrawMan5 { this: IterablePolyTransforms[A]^ => type C[A] protected def coll: Iterable[A]^{this} - def fromIterable[B](coll: Iterable[B]^): C[B]^{coll} + def fromIterable[B](coll: Iterable[B]^{this, cap}): C[B]^{coll} def map[B](f: A => B): C[B]^{this, f} = fromIterable(View.Map(coll, f)) def flatMap[B](f: A => IterableOnce[B]^): C[B]^{this, f} = fromIterable(View.FlatMap(coll, f)) def ++[B >: A](xs: IterableOnce[B]^): C[B]^{this, xs} = fromIterable(View.Concat(coll, xs)) @@ -169,7 +170,7 @@ object CollectionStrawMan5 { while (it.hasNext) xs = new Cons(it.next(), xs) fromLikeIterable(xs) - override protected[this] def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr + override protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr override def filter(p: A => Boolean): Repr = fromLikeIterable(View.Filter(coll, p)) @@ -204,7 +205,7 @@ object CollectionStrawMan5 { def head: A def tail: List[A] def iterator = new Iterator[A] { - private[this] var current = self + private var current = self def hasNext = !current.isEmpty def next() = { val r = current.head; current = current.tail; r } } @@ -215,7 +216,7 @@ object CollectionStrawMan5 { } def length: Int = if (isEmpty) 0 else 1 + tail.length - protected[this] def newBuilder = new ListBuffer[A @uncheckedVariance @uncheckedCaptures] + protected def newBuilder = new ListBuffer[A @uncheckedVariance @uncheckedCaptures] def ++:[B >: A](prefix: List[B]): List[B] = if (prefix.isEmpty) this else Cons(prefix.head, prefix.tail ++: this) @@ -407,7 +408,7 @@ object CollectionStrawMan5 { this: View[A]^ => type C[X] = View[X]^{this} override def view: this.type = this - override def fromIterable[B](c: Iterable[B]^): View[B]^{this, c} = { + override def fromIterable[B](c: Iterable[B]^{this, cap}): View[B]^{this, c} = { c match { case c: View[B] => c case _ => View.fromIterator(c.iterator) @@ -450,6 +451,20 @@ object CollectionStrawMan5 { this: Filter[A]^{underlying, p} => def iterator: Iterator[A]^{this} = underlying.iterator.filter(p) } + + object Filter: + def apply[A](underlying: Iterable[A]^, pp: A => Boolean, isFlipped: Boolean): Filter[A]^{underlying, pp} = + underlying match + case filter: Filter[A] => + new Filter(filter.underlying, a => filter.p(a) && pp(a)) + .asInstanceOf[Filter[A]^{underlying, pp}] + //unsafeAssumeSeparate: + // See filter-iterable.scala for a test where a variant of Filter + // works without the unsafeAssumeSeparate. But it requires significant + // changes compared to the version here. + //new Filter(filter.underlying, a => filter.p(a) && pp(a)) + case _ => new Filter(underlying, pp) + case class Partition[A](val underlying: Iterable[A]^, p: A => Boolean) { self: Partition[A]^{underlying, p} => 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)