From 026818592dcd86382559677901568fb43173f459 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 11 May 2024 13:08:22 +0200 Subject: [PATCH 01/10] Distinguish maximal from root capabilities A maximal capability is one that derives from `caps.Cap`. Also: drop the given caps.Cap. It's not clear why there needs to be a given for it. --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 10 +++++----- compiler/src/dotty/tools/dotc/core/Types.scala | 9 ++++++++- library/src/scala/caps.scala | 2 -- tests/neg-custom-args/captures/filevar.scala | 2 +- tests/neg-custom-args/captures/i15923.scala | 2 +- tests/neg-custom-args/captures/stack-alloc.scala | 2 +- tests/neg-custom-args/captures/usingLogFile.scala | 2 +- 7 files changed, 17 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index d1a5a07f6a0f..5a60506cc49a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -115,7 +115,7 @@ sealed abstract class CaptureSet extends Showable: * capture set. */ protected final def addNewElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if elem.isRootCapability || summon[VarState] == FrozenState then + if elem.isMaxCapability || summon[VarState] == FrozenState then addThisElem(elem) else addThisElem(elem).orElse: @@ -167,11 +167,11 @@ sealed abstract class CaptureSet extends Showable: if comparer.isInstanceOf[ExplainingTypeComparer] then // !!! DEBUG reporting.trace.force(i"$this accountsFor $x, ${x.captureSetOfInfo}?", show = true): elems.exists(_.subsumes(x)) - || !x.isRootCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK + || !x.isMaxCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK else reporting.trace(i"$this accountsFor $x, ${x.captureSetOfInfo}?", show = true): elems.exists(_.subsumes(x)) - || !x.isRootCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK + || !x.isMaxCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK /** 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 @@ -183,7 +183,7 @@ sealed abstract class CaptureSet extends Showable: def mightAccountFor(x: CaptureRef)(using Context): Boolean = reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true) { elems.exists(_.subsumes(x)) - || !x.isRootCapability + || !x.isMaxCapability && { val elems = x.captureSetOfInfo.elems !elems.isEmpty && elems.forall(mightAccountFor) @@ -1032,7 +1032,7 @@ object CaptureSet: /** The capture set of the type underlying CaptureRef */ def ofInfo(ref: CaptureRef)(using Context): CaptureSet = ref match - case ref: TermRef if ref.isRootCapability => ref.singletonCaptureSet + case ref: (TermRef | TermParamRef) if ref.isMaxCapability => ref.singletonCaptureSet case ReachCapability(ref1) => deepCaptureSet(ref1.widen) .showing(i"Deep capture set of $ref: ${ref1.widen} = $result", capt) case _ => ofType(ref.underlying, followResult = true) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index eeffc41d4159..63f49b6ee8ef 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -2259,7 +2259,7 @@ object Types extends TypeUtils { * set of the underlying type is not always empty. */ final def isTracked(using Context): Boolean = - isTrackableRef && (isRootCapability || !captureSetOfInfo.isAlwaysEmpty) + isTrackableRef && (isMaxCapability || !captureSetOfInfo.isAlwaysEmpty) /** Is this a reach reference of the form `x*`? */ def isReach(using Context): Boolean = false // overridden in AnnotatedType @@ -2273,6 +2273,9 @@ object Types extends TypeUtils { /** Is this reference the generic root capability `cap` ? */ def isRootCapability(using Context): Boolean = false + /** Is this reference capability that does not derive from another capability ? */ + def isMaxCapability(using Context): Boolean = false + /** Normalize reference so that it can be compared with `eq` for equality */ def normalizedRef(using Context): CaptureRef = this @@ -3010,6 +3013,9 @@ object Types extends TypeUtils { override def isRootCapability(using Context): Boolean = name == nme.CAPTURE_ROOT && symbol == defn.captureRoot + override def isMaxCapability(using Context): Boolean = + widen.derivesFrom(defn.Caps_Cap) && symbol.isStableMember + override def normalizedRef(using Context): CaptureRef = if isTrackableRef then symbol.termRef else this } @@ -4809,6 +4815,7 @@ object Types extends TypeUtils { def kindString: String = "Term" def copyBoundType(bt: BT): Type = bt.paramRefs(paramNum) override def isTrackableRef(using Context) = true + override def isMaxCapability(using Context) = widen.derivesFrom(defn.Caps_Cap) } private final class TermParamRefImpl(binder: TermLambda, paramNum: Int) extends TermParamRef(binder, paramNum) diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index c7fc8e7ba584..9e21b4af1f1e 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -13,8 +13,6 @@ import annotation.experimental /** The universal capture reference */ val cap: Cap = Cap() - given Cap = cap - /** Reach capabilities x* which appear as terms in @retains annotations are encoded * as `caps.reachCapability(x)`. When converted to CaptureRef types in capture sets * they are represented as `x.type @annotation.internal.reachCapability`. diff --git a/tests/neg-custom-args/captures/filevar.scala b/tests/neg-custom-args/captures/filevar.scala index 59b8415d6e0f..bcf2d7beccbf 100644 --- a/tests/neg-custom-args/captures/filevar.scala +++ b/tests/neg-custom-args/captures/filevar.scala @@ -9,7 +9,7 @@ class Service: def log = file.write("log") def withFile[T](op: (l: caps.Cap) ?-> (f: File^{l}) => T): T = - op(new File) + op(using caps.cap)(new File) def test = withFile: f => diff --git a/tests/neg-custom-args/captures/i15923.scala b/tests/neg-custom-args/captures/i15923.scala index 754fd0687037..89688449bdac 100644 --- a/tests/neg-custom-args/captures/i15923.scala +++ b/tests/neg-custom-args/captures/i15923.scala @@ -5,7 +5,7 @@ def mkId[X](x: X): Id[X] = [T] => (op: X => T) => op(x) def bar() = { def withCap[X](op: (lcap: caps.Cap) ?-> Cap^{lcap} => X): X = { val cap: Cap = new Cap { def use() = { println("cap is used"); 0 } } - val result = op(cap) + val result = op(using caps.cap)(cap) result } diff --git a/tests/neg-custom-args/captures/stack-alloc.scala b/tests/neg-custom-args/captures/stack-alloc.scala index befafbf13003..1b93d2e5129d 100644 --- a/tests/neg-custom-args/captures/stack-alloc.scala +++ b/tests/neg-custom-args/captures/stack-alloc.scala @@ -9,7 +9,7 @@ def withFreshPooled[T](op: (lcap: caps.Cap) ?-> Pooled^{lcap} => T): T = if nextFree >= stack.size then stack.append(new Pooled) val pooled = stack(nextFree) nextFree = nextFree + 1 - val ret = op(pooled) + val ret = op(using caps.cap)(pooled) nextFree = nextFree - 1 ret diff --git a/tests/neg-custom-args/captures/usingLogFile.scala b/tests/neg-custom-args/captures/usingLogFile.scala index 67e6f841e7ce..25b853913af9 100644 --- a/tests/neg-custom-args/captures/usingLogFile.scala +++ b/tests/neg-custom-args/captures/usingLogFile.scala @@ -5,7 +5,7 @@ object Test1: def usingLogFile[T](op: (local: caps.Cap) ?-> FileOutputStream => T): T = val logFile = FileOutputStream("log") - val result = op(logFile) + val result = op(using caps.cap)(logFile) logFile.close() result From 0c1394ffef07d6ae61506137b54ebba4298b3262 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 11 May 2024 23:08:05 +0200 Subject: [PATCH 02/10] Handle tracked class parameters The current handling of class type refinements is unsound. We cannot simply use a variable for the capture set of a class argument. What we need to do instead is treat class arguments as tracked. In this commit we at least allow explicitly declared tracked arguments. This needed two modifications: - Don't additionally add a capture set for tracked arguments - Handle the case where a capture reference is of a singleton type which is another capture reference. As a next step we should treat all class arguments as implicitly tracked. --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 5 +++++ compiler/src/dotty/tools/dotc/cc/Setup.scala | 4 +++- compiler/src/dotty/tools/dotc/core/Types.scala | 11 +++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 5a60506cc49a..3c2dab7c46e0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -147,6 +147,7 @@ sealed abstract class CaptureSet extends Showable: * this subsumes this.f * x subsumes y ==> x* subsumes y, x subsumes y? * x subsumes y ==> x* subsumes y*, x? subsumes y? + * x: x1.type /\ x1 subsumes y ==> x subsumes y */ extension (x: CaptureRef) private def subsumes(y: CaptureRef)(using Context): Boolean = @@ -158,6 +159,10 @@ sealed abstract class CaptureSet extends Showable: case _ => false || x.match case ReachCapability(x1) => x1.subsumes(y.stripReach) + case x: TermRef => + x.info match + case x1: CaptureRef => x1.subsumes(y) + case _ => false case _ => false /** {x} <:< this where <:< is subcapturing, but treating all variables diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index e6953dbf67b7..d909bccc8070 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -196,7 +196,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case cls: ClassSymbol if !defn.isFunctionClass(cls) && cls.is(CaptureChecked) => cls.paramGetters.foldLeft(tp) { (core, getter) => - if atPhase(thisPhase.next)(getter.termRef.isTracked) then + if atPhase(thisPhase.next)(getter.termRef.isTracked) + && !getter.is(Tracked) + then val getterType = mapInferred(refine = false)(tp.memberInfo(getter)).strippedDealias RefinedType(core, getter.name, diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 63f49b6ee8ef..a007842c937c 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -6162,8 +6162,15 @@ object Types extends TypeUtils { def inverse: BiTypeMap /** A restriction of this map to a function on tracked CaptureRefs */ - def forward(ref: CaptureRef): CaptureRef = this(ref) match - case result: CaptureRef if result.isTrackableRef => result + def forward(ref: CaptureRef): CaptureRef = + val result = this(ref) + def ensureTrackable(tp: Type): CaptureRef = tp match + case tp: CaptureRef => + if tp.isTrackableRef then tp + else ensureTrackable(tp.underlying) + case _ => + assert(false, i"not a trackable captureRef ref: $result, ${result.underlyingIterator.toList}") + ensureTrackable(result) /** A restriction of the inverse to a function on tracked CaptureRefs */ def backward(ref: CaptureRef): CaptureRef = inverse(ref) match From b76bc3ef2aa70a5f927cb3d6958cca9080dc6ff5 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 11 May 2024 23:08:50 +0200 Subject: [PATCH 03/10] Drop @capability annotations Replace with references that inherit trait `Capability`. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 19 ++++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 3 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 10 ++- compiler/src/dotty/tools/dotc/cc/Setup.scala | 6 +- .../dotty/tools/dotc/core/Definitions.scala | 5 +- .../src/dotty/tools/dotc/core/Types.scala | 7 +- library/src/scala/CanThrow.scala | 4 +- library/src/scala/annotation/capability.scala | 4 +- library/src/scala/caps.scala | 11 ++- tests/disabled/pos/lazylist.scala | 2 +- .../captures/box-unsoundness.scala | 1 - tests/neg-custom-args/captures/byname.check | 2 +- tests/neg-custom-args/captures/byname.scala | 2 +- .../captures/capt-box-env.scala | 2 +- tests/neg-custom-args/captures/capt-box.scala | 2 +- tests/neg-custom-args/captures/capt-wf2.scala | 2 +- .../captures/caseclass/Test_2.scala | 2 +- tests/neg-custom-args/captures/cc-this.scala | 2 +- tests/neg-custom-args/captures/cc-this2.check | 2 +- tests/neg-custom-args/captures/cc-this3.scala | 2 +- tests/neg-custom-args/captures/cc-this5.check | 2 +- tests/neg-custom-args/captures/cc-this5.scala | 4 +- .../captures/class-constr.scala | 2 +- .../captures/effect-swaps-explicit.check | 29 +++++++ .../captures/effect-swaps-explicit.scala | 76 +++++++++++++++++++ .../captures/effect-swaps.check | 15 ++-- .../captures/effect-swaps.scala | 14 +++- .../captures/exception-definitions.check | 2 +- .../captures/extending-cap-classes.scala | 15 ---- tests/neg-custom-args/captures/filevar.scala | 2 +- tests/neg-custom-args/captures/i15923.scala | 2 +- tests/neg-custom-args/captures/i16725.scala | 7 +- .../captures/inner-classes.scala | 2 +- .../captures/stack-alloc.scala | 2 +- tests/neg-custom-args/captures/try3.scala | 2 +- .../captures/usingLogFile.scala | 2 +- tests/neg/unsound-reach.check | 10 +-- tests/neg/unsound-reach.scala | 16 ++-- tests/pos-custom-args/captures/boxed1.scala | 2 +- .../captures/capt-capability.scala | 16 ++-- .../pos-custom-args/captures/caseclass.scala | 2 +- tests/pos-custom-args/captures/cc-this.scala | 2 +- .../captures/eta-expansions.scala | 2 +- .../captures/filevar-tracked.scala | 38 ++++++++++ tests/pos-custom-args/captures/filevar.scala | 13 ++-- tests/pos-custom-args/captures/i16116.scala | 3 +- tests/pos-custom-args/captures/i16226.scala | 2 +- tests/pos-custom-args/captures/i19751.scala | 2 +- tests/pos-custom-args/captures/lazyref.scala | 2 +- tests/pos-custom-args/captures/lists.scala | 4 +- .../captures/logger-tracked.scala | 68 +++++++++++++++++ tests/pos-custom-args/captures/logger.scala | 9 ++- .../captures/nested-classes-tracked.scala | 22 ++++++ .../captures/nested-classes.scala | 9 ++- .../captures/null-logger.scala | 2 +- tests/pos-custom-args/captures/pairs.scala | 6 +- tests/pos-custom-args/captures/reaches.scala | 2 +- tests/pos-custom-args/captures/try3.scala | 2 +- tests/pos-custom-args/captures/vars.scala | 2 +- tests/pos/dotty-experimental.scala | 2 +- tests/pos/i20237.scala | 2 +- tests/pos/into-bigint.scala | 4 +- 62 files changed, 382 insertions(+), 130 deletions(-) create mode 100644 tests/neg-custom-args/captures/effect-swaps-explicit.check create mode 100644 tests/neg-custom-args/captures/effect-swaps-explicit.scala delete mode 100644 tests/neg-custom-args/captures/extending-cap-classes.scala create mode 100644 tests/pos-custom-args/captures/filevar-tracked.scala create mode 100644 tests/pos-custom-args/captures/logger-tracked.scala create mode 100644 tests/pos-custom-args/captures/nested-classes-tracked.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 5c9946f6134a..9b899030bc02 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -203,6 +203,23 @@ extension (tp: Type) case _ => false + /** Does type derive from caps.Capability?, which means it references of this + * type are maximal capabilities? + */ + def derivesFromCapability(using Context): Boolean = tp.dealias match + case tp: (TypeRef | AppliedType) => + val sym = tp.typeSymbol + if sym.isClass then sym.derivesFrom(defn.Caps_Capability) + else tp.superType.derivesFromCapability + case tp: TypeProxy => + tp.superType.derivesFromCapability + case tp: AndType => + tp.tp1.derivesFromCapability || tp.tp2.derivesFromCapability + case tp: OrType => + tp.tp1.derivesFromCapability && tp.tp2.derivesFromCapability + case _ => + false + /** Drop @retains annotations everywhere */ def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling val tm = new TypeMap: @@ -408,7 +425,7 @@ extension (sym: Symbol) /** The owner of the current level. Qualifying owners are * - methods other than constructors and anonymous functions * - anonymous functions, provided they either define a local - * root of type caps.Cap, or they are the rhs of a val definition. + * root of type caps.Capability, or they are the rhs of a val definition. * - classes, if they are not staticOwners * - _root_ */ diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 3c2dab7c46e0..06ba36bd5e24 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1051,7 +1051,8 @@ object CaptureSet: case tp: TermParamRef => tp.captureSet case tp: TypeRef => - if tp.typeSymbol == defn.Caps_Cap then universal else empty + if tp.derivesFromCapability then universal // TODO: maybe return another value that indicates that the underltinf ref is maximal? + else empty case _: TypeParamRef => empty case CapturingType(parent, refs) => diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index a5bb8792af2c..cd0f21129ac9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -537,8 +537,8 @@ class CheckCaptures extends Recheck, SymTransformer: */ def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = var refined: Type = core - var allCaptures: CaptureSet = if setup.isCapabilityClassRef(core) - then CaptureSet.universal else initCs + var allCaptures: CaptureSet = + if core.derivesFromCapability then CaptureSet.universal else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do val getter = cls.info.member(getterName).suchThat(_.is(ParamAccessor)).symbol if getter.termRef.isTracked && !getter.is(Private) then @@ -572,8 +572,10 @@ class CheckCaptures extends Recheck, SymTransformer: val TypeApply(fn, args) = tree val polyType = atPhase(thisPhase.prev): fn.tpe.widen.asInstanceOf[TypeLambda] + def isExempt(sym: Symbol) = + sym.isTypeTestOrCast || sym == defn.Compiletime_erasedValue for case (arg: TypeTree, formal, pname) <- args.lazyZip(polyType.paramRefs).lazyZip((polyType.paramNames)) do - if !tree.symbol.isTypeTestOrCast then + if !isExempt(tree.symbol) then def where = if fn.symbol.exists then i" in an argument of ${fn.symbol}" else "" disallowRootCapabilitiesIn(arg.knownType, NoSymbol, i"Sealed type variable $pname", "be instantiated to", @@ -1305,7 +1307,7 @@ class CheckCaptures extends Recheck, SymTransformer: case ref: TermParamRef if !allowed.contains(ref) && !seen.contains(ref) => seen += ref - if ref.underlying.isRef(defn.Caps_Cap) then + if ref.underlying.isRef(defn.Caps_Capability) then report.error(i"escaping local reference $ref", tree.srcPos) else val widened = ref.captureSetOfInfo diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index d909bccc8070..1a8c65c89a43 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -77,9 +77,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def isCapabilityClassRef(tp: Type)(using Context): Boolean = tp.dealiasKeepAnnots match case _: TypeRef | _: AppliedType => val sym = tp.classSymbol - def checkSym: Boolean = - sym.hasAnnotation(defn.CapabilityAnnot) - || sym.info.parents.exists(hasUniversalCapability) + def checkSym: Boolean = sym.info.parents.exists(hasUniversalCapability) sym.isClass && capabilityClassMap.getOrElseUpdate(sym, checkSym) case _ => false @@ -594,7 +592,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if sym.isClass then !sym.isPureClass else - sym != defn.Caps_Cap && instanceCanBeImpure(tp.superType) + sym != defn.Caps_Capability && instanceCanBeImpure(tp.superType) case tp: (RefinedOrRecType | MatchType) => instanceCanBeImpure(tp.underlying) case tp: AndType => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 11a4a8473e79..52535f26c692 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -991,7 +991,7 @@ class Definitions { @tu lazy val CapsModule: Symbol = requiredModule("scala.caps") @tu lazy val captureRoot: TermSymbol = CapsModule.requiredValue("cap") - @tu lazy val Caps_Cap: TypeSymbol = CapsModule.requiredType("Cap") + @tu lazy val Caps_Capability: TypeSymbol = CapsModule.requiredType("Capability") @tu lazy val Caps_reachCapability: TermSymbol = CapsModule.requiredMethod("reachCapability") @tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe") @tu lazy val Caps_unsafeAssumePure: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumePure") @@ -1014,7 +1014,6 @@ class Definitions { @tu lazy val BeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BeanProperty") @tu lazy val BooleanBeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BooleanBeanProperty") @tu lazy val BodyAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Body") - @tu lazy val CapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.capability") @tu lazy val ChildAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Child") @tu lazy val ContextResultCountAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ContextResultCount") @tu lazy val ProvisionalSuperClassAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ProvisionalSuperClass") @@ -2033,7 +2032,7 @@ class Definitions { */ @tu lazy val ccExperimental: Set[Symbol] = Set( CapsModule, CapsModule.moduleClass, PureClass, - CapabilityAnnot, RequiresCapabilityAnnot, + RequiresCapabilityAnnot, RetainsAnnot, RetainsCapAnnot, RetainsByNameAnnot) /** Experimental language features defined in `scala.runtime.stdLibPatches.language.experimental`. diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index a007842c937c..5be6774d0ff0 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3014,7 +3014,8 @@ object Types extends TypeUtils { name == nme.CAPTURE_ROOT && symbol == defn.captureRoot override def isMaxCapability(using Context): Boolean = - widen.derivesFrom(defn.Caps_Cap) && symbol.isStableMember + import cc.* + this.derivesFromCapability && symbol.isStableMember override def normalizedRef(using Context): CaptureRef = if isTrackableRef then symbol.termRef else this @@ -4815,7 +4816,9 @@ object Types extends TypeUtils { def kindString: String = "Term" def copyBoundType(bt: BT): Type = bt.paramRefs(paramNum) override def isTrackableRef(using Context) = true - override def isMaxCapability(using Context) = widen.derivesFrom(defn.Caps_Cap) + override def isMaxCapability(using Context) = + import cc.* + this.derivesFromCapability } private final class TermParamRefImpl(binder: TermLambda, paramNum: Int) extends TermParamRef(binder, paramNum) diff --git a/library/src/scala/CanThrow.scala b/library/src/scala/CanThrow.scala index c7f23a393715..91c94229c43c 100644 --- a/library/src/scala/CanThrow.scala +++ b/library/src/scala/CanThrow.scala @@ -6,9 +6,9 @@ import annotation.{implicitNotFound, experimental, capability} * experimental.saferExceptions feature, a `throw Ex()` expression will require * a given of class `CanThrow[Ex]` to be available. */ -@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] +erased class CanThrow[-E <: Exception] extends caps.Capability @experimental object unsafeExceptions: diff --git a/library/src/scala/annotation/capability.scala b/library/src/scala/annotation/capability.scala index 4696ed6a015e..d3453e3c8168 100644 --- a/library/src/scala/annotation/capability.scala +++ b/library/src/scala/annotation/capability.scala @@ -11,4 +11,6 @@ import annotation.experimental * THere, the capture set of any instance of `CanThrow` is assumed to be * `{*}`. */ -@experimental final class capability extends StaticAnnotation +@experimental +@deprecated("To make a class a capability, let it derive from the `Capability` trait instead") +final class capability extends StaticAnnotation diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 9e21b4af1f1e..215ad2cb5697 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -4,14 +4,17 @@ import annotation.experimental @experimental object caps: - class Cap // should be @erased + trait Capability // should be @erased + + /** The universal capture reference */ + val cap: Capability = new Capability() {} /** The universal capture reference (deprecated) */ @deprecated("Use `cap` instead") - val `*`: Cap = cap + val `*`: Capability = cap - /** The universal capture reference */ - val cap: Cap = Cap() + @deprecated("Use `Capability` instead") + type Cap = Capability /** Reach capabilities x* which appear as terms in @retains annotations are encoded * as `caps.reachCapability(x)`. When converted to CaptureRef types in capture sets diff --git a/tests/disabled/pos/lazylist.scala b/tests/disabled/pos/lazylist.scala index c24f8677b91f..e56eb484894c 100644 --- a/tests/disabled/pos/lazylist.scala +++ b/tests/disabled/pos/lazylist.scala @@ -34,7 +34,7 @@ object LazyNil extends LazyList[Nothing]: def map[A, B](xs: {*} LazyList[A], f: {*} A => B): {f, xs} LazyList[B] = xs.map(f) -@annotation.capability class Cap +class Cap extends caps.Capability def test(cap1: Cap, cap2: Cap, cap3: Cap) = def f[T](x: LazyList[T]): LazyList[T] = if cap1 == cap1 then x else LazyNil diff --git a/tests/neg-custom-args/captures/box-unsoundness.scala b/tests/neg-custom-args/captures/box-unsoundness.scala index d1331f16df1f..8c1c22bc7fa6 100644 --- a/tests/neg-custom-args/captures/box-unsoundness.scala +++ b/tests/neg-custom-args/captures/box-unsoundness.scala @@ -1,4 +1,3 @@ -//@annotation.capability class CanIO { def use(): Unit = () } def use[X](x: X): (op: X -> Unit) -> Unit = op => op(x) def test(io: CanIO^): Unit = diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index e06a3a1f8268..2b48428e97bc 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -1,7 +1,7 @@ -- 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 -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:4:2 ----------------------------------------- 4 | def f() = if cap1 == cap1 then g else g // error diff --git a/tests/neg-custom-args/captures/byname.scala b/tests/neg-custom-args/captures/byname.scala index 279122f54735..0ed3a09cb414 100644 --- a/tests/neg-custom-args/captures/byname.scala +++ b/tests/neg-custom-args/captures/byname.scala @@ -1,4 +1,4 @@ -@annotation.capability class Cap +class Cap extends caps.Capability def test(cap1: Cap, cap2: Cap) = def f() = if cap1 == cap1 then g else g // error diff --git a/tests/neg-custom-args/captures/capt-box-env.scala b/tests/neg-custom-args/captures/capt-box-env.scala index 605b446d5262..bfe1874d073b 100644 --- a/tests/neg-custom-args/captures/capt-box-env.scala +++ b/tests/neg-custom-args/captures/capt-box-env.scala @@ -1,4 +1,4 @@ -@annotation.capability class Cap +class Cap extends caps.Capability class Pair[+A, +B](x: A, y: B): def fst: A = x diff --git a/tests/neg-custom-args/captures/capt-box.scala b/tests/neg-custom-args/captures/capt-box.scala index 634470704fc5..291882bed36d 100644 --- a/tests/neg-custom-args/captures/capt-box.scala +++ b/tests/neg-custom-args/captures/capt-box.scala @@ -1,4 +1,4 @@ -@annotation.capability class Cap +class Cap extends caps.Capability def test(x: Cap) = diff --git a/tests/neg-custom-args/captures/capt-wf2.scala b/tests/neg-custom-args/captures/capt-wf2.scala index 6c65e0dc77f7..8bb04a230fdd 100644 --- a/tests/neg-custom-args/captures/capt-wf2.scala +++ b/tests/neg-custom-args/captures/capt-wf2.scala @@ -1,4 +1,4 @@ -@annotation.capability class C +class C extends caps.Capability def test(c: C) = var x: Any^{c} = ??? diff --git a/tests/neg-custom-args/captures/caseclass/Test_2.scala b/tests/neg-custom-args/captures/caseclass/Test_2.scala index bffc0a295bdc..9d97d5537c72 100644 --- a/tests/neg-custom-args/captures/caseclass/Test_2.scala +++ b/tests/neg-custom-args/captures/caseclass/Test_2.scala @@ -1,4 +1,4 @@ -@annotation.capability class C +class C extends caps.Capability def test(c: C) = val pure: () -> Unit = () => () val impure: () => Unit = pure diff --git a/tests/neg-custom-args/captures/cc-this.scala b/tests/neg-custom-args/captures/cc-this.scala index 4c05be702c51..e4336ed457af 100644 --- a/tests/neg-custom-args/captures/cc-this.scala +++ b/tests/neg-custom-args/captures/cc-this.scala @@ -1,4 +1,4 @@ -@annotation.capability class Cap +class Cap extends caps.Capability def eff(using Cap): Unit = () diff --git a/tests/neg-custom-args/captures/cc-this2.check b/tests/neg-custom-args/captures/cc-this2.check index bd9a1085d262..6cb3010d6174 100644 --- a/tests/neg-custom-args/captures/cc-this2.check +++ b/tests/neg-custom-args/captures/cc-this2.check @@ -2,7 +2,7 @@ -- Error: tests/neg-custom-args/captures/cc-this2/D_2.scala:3:8 -------------------------------------------------------- 3 | this: D^ => // error | ^^ - |reference (caps.cap : caps.Cap) captured by this self type is not included in the allowed capture set {} of pure base class class C + |reference (caps.cap : caps.Capability) captured by this self type is not included in the allowed capture set {} of pure base class class C -- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this2/D_2.scala:2:6 ----------------------------------- 2 |class D extends C: // error | ^ diff --git a/tests/neg-custom-args/captures/cc-this3.scala b/tests/neg-custom-args/captures/cc-this3.scala index 25af19dd6c4a..0a36cde8173b 100644 --- a/tests/neg-custom-args/captures/cc-this3.scala +++ b/tests/neg-custom-args/captures/cc-this3.scala @@ -1,4 +1,4 @@ -@annotation.capability class Cap +class Cap extends caps.Capability def eff(using Cap): Unit = () diff --git a/tests/neg-custom-args/captures/cc-this5.check b/tests/neg-custom-args/captures/cc-this5.check index 8affe7005e2e..1329734ce37d 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 | ^ - | (c : Cap^) cannot be referenced here; it is not included in the allowed capture set {} + | (c : Cap) cannot be referenced here; it 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/cc-this5.scala b/tests/neg-custom-args/captures/cc-this5.scala index e84c2a41f55c..4c9a8a706670 100644 --- a/tests/neg-custom-args/captures/cc-this5.scala +++ b/tests/neg-custom-args/captures/cc-this5.scala @@ -1,7 +1,7 @@ class C: val x: C = this -@annotation.capability class Cap +class Cap extends caps.Capability def foo(c: Cap) = object D extends C: // error @@ -17,5 +17,5 @@ def test(c: Cap) = def test2(c: Cap) = class A: - def f = println(c) + def f = println(c) val x: A = this // error diff --git a/tests/neg-custom-args/captures/class-constr.scala b/tests/neg-custom-args/captures/class-constr.scala index 9afb6972ccfa..619fa9fa0341 100644 --- a/tests/neg-custom-args/captures/class-constr.scala +++ b/tests/neg-custom-args/captures/class-constr.scala @@ -1,6 +1,6 @@ import annotation.{capability, constructorOnly} -@capability class Cap +class Cap extends caps.Capability class C(x: Cap, @constructorOnly y: Cap) diff --git a/tests/neg-custom-args/captures/effect-swaps-explicit.check b/tests/neg-custom-args/captures/effect-swaps-explicit.check new file mode 100644 index 000000000000..47559ab97568 --- /dev/null +++ b/tests/neg-custom-args/captures/effect-swaps-explicit.check @@ -0,0 +1,29 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps-explicit.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] +65 | fr.await.ok + |-------------------------------------------------------------------------------------------------------------------- + |Inline stack trace + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + |This location contains code that was inlined from effect-swaps-explicit.scala:41 +41 | boundary(Ok(body)) + | ^^^^^^^^ + -------------------------------------------------------------------------------------------------------------------- + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:74:10 ------------------------ +74 | Future: fut ?=> // error: type mismatch + | ^ + | Found: Future[box T^?]^{fr, lbl} + | Required: Future[box T^?]^? +75 | fr.await.ok + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:68:15 --------------------------------------------- +68 | Result.make: //lbl ?=> // error, escaping label from Result + | ^^^^^^^^^^^ + |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-explicit.scala b/tests/neg-custom-args/captures/effect-swaps-explicit.scala new file mode 100644 index 000000000000..814199706721 --- /dev/null +++ b/tests/neg-custom-args/captures/effect-swaps-explicit.scala @@ -0,0 +1,76 @@ +import annotation.capability + +object boundary: + + final class Label[-T] // extends caps.Capability + + /** Abort current computation and instead return `value` as the value of + * the enclosing `boundary` call that created `label`. + */ + def break[T](value: T)(using label: Label[T]^): Nothing = ??? + + def apply[T](body: Label[T]^ ?=> T): T = ??? +end boundary + +import boundary.{Label, break} + +trait Async extends caps.Capability +object Async: + def blocking[T](body: Async ?=> T): T = ??? + +class Future[+T]: + this: Future[T]^ => + def await(using Async): T = ??? +object Future: + def apply[T](op: Async ?=> T)(using Async): Future[T]^{op} = ??? + +enum Result[+T, +E]: + case Ok[+T](value: T) extends Result[T, Nothing] + case Err[+E](error: E) extends Result[Nothing, E] + + +object Result: + extension [T, E](r: Result[T, E]^)(using Label[Err[E]]^) + + /** `_.ok` propagates Err to current Label */ + def ok: T = r match + case Ok(value) => value + case Err(value) => break[Err[E]](Err(value)) + + transparent inline def apply[T, E](inline body: Label[Result[T, E]]^ ?=> T): Result[T, E] = + boundary(Ok(body)) + + // same as apply, but not an inline method + def make[T, E](body: Label[Result[T, E]]^ ?=> T): Result[T, E] = + boundary(Ok(body)) + +end Result + +def test[T, E](using Async) = + import Result.* + Async.blocking: async ?=> + val good1: List[Future[Result[T, E]]] => Future[Result[List[T], E]] = frs => + Future: + Result: + frs.map(_.await.ok) // OK + + val good2: Result[Future[T], E] => Future[Result[T, E]] = rf => + Future: + Result: + rf.ok.await // OK, Future argument has type Result[T] + + def fail3(fr: Future[Result[T, E]]^) = + Result: + Future: // error, type mismatch + fr.await.ok + + def fail4[T, E](fr: Future[Result[T, E]]^) = + Result.make: //lbl ?=> // error, escaping label from Result + Future: fut ?=> + fr.await.ok + + def fail5[T, E](fr: Future[Result[T, E]]^) = + Result.make[Future[T], E]: lbl ?=> + Future: fut ?=> // error: type mismatch + fr.await.ok + diff --git a/tests/neg-custom-args/captures/effect-swaps.check b/tests/neg-custom-args/captures/effect-swaps.check index bda3509645d1..f16019c15513 100644 --- a/tests/neg-custom-args/captures/effect-swaps.check +++ b/tests/neg-custom-args/captures/effect-swaps.check @@ -1,6 +1,6 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:64:8 ---------------------------------- 63 | Result: -64 | Future: // error, escaping label from Result +64 | Future: // error, type mismatch | ^ | Found: Result.Ok[box Future[box T^?]^{fr, contextual$1}] | Required: Result[Future[T], Nothing] @@ -14,8 +14,11 @@ -------------------------------------------------------------------------------------------------------------------- | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/effect-swaps.scala:68:15 ------------------------------------------------------ -68 | Result.make: //lbl ?=> // error, escaping label from Result - | ^^^^^^^^^^^ - |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 +-- [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^?]^? +75 | fr.await.ok + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/effect-swaps.scala b/tests/neg-custom-args/captures/effect-swaps.scala index 1d72077bb8da..af44501371a9 100644 --- a/tests/neg-custom-args/captures/effect-swaps.scala +++ b/tests/neg-custom-args/captures/effect-swaps.scala @@ -2,7 +2,7 @@ import annotation.capability object boundary: - @capability final class Label[-T] + final class Label[-T] extends caps.Capability /** Abort current computation and instead return `value` as the value of * the enclosing `boundary` call that created `label`. @@ -14,7 +14,7 @@ end boundary import boundary.{Label, break} -@capability trait Async +trait Async extends caps.Capability object Async: def blocking[T](body: Async ?=> T): T = ??? @@ -61,10 +61,16 @@ def test[T, E](using Async) = def fail3(fr: Future[Result[T, E]]^) = Result: - Future: // error, escaping label from Result + Future: // error, type mismatch fr.await.ok def fail4[T, E](fr: Future[Result[T, E]]^) = - Result.make: //lbl ?=> // error, escaping label from Result + Result.make: //lbl ?=> // should be error, escaping label from Result but infers Result[Any, Any] Future: fut ?=> fr.await.ok + + def fail5[T, E](fr: Future[Result[T, E]]^) = + Result.make[Future[T], E]: lbl ?=> + Future: fut ?=> // error: type mismatch + fr.await.ok + diff --git a/tests/neg-custom-args/captures/exception-definitions.check b/tests/neg-custom-args/captures/exception-definitions.check index 72b88f252e59..7f915ebd9833 100644 --- a/tests/neg-custom-args/captures/exception-definitions.check +++ b/tests/neg-custom-args/captures/exception-definitions.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/exception-definitions.scala:3:8 ----------------------------------------------- 3 | self: Err^ => // error | ^^^^ - |reference (caps.cap : caps.Cap) captured by this self type is not included in the allowed capture set {} of pure base class class Throwable + |reference (caps.cap : caps.Capability) captured by this self type is not included in the allowed capture set {} of pure base class class Throwable -- Error: tests/neg-custom-args/captures/exception-definitions.scala:7:12 ---------------------------------------------- 7 | val x = c // error | ^ diff --git a/tests/neg-custom-args/captures/extending-cap-classes.scala b/tests/neg-custom-args/captures/extending-cap-classes.scala deleted file mode 100644 index 17497e415a1e..000000000000 --- a/tests/neg-custom-args/captures/extending-cap-classes.scala +++ /dev/null @@ -1,15 +0,0 @@ -import annotation.capability - -class C1 -@capability class C2 extends C1 -class C3 extends C2 - -def test = - val x1: C1 = new C1 - val x2: C1 = new C2 // error - val x3: C1 = new C3 // error - - val y1: C2 = new C2 - val y2: C2 = new C3 - - val z1: C3 = new C3 \ No newline at end of file diff --git a/tests/neg-custom-args/captures/filevar.scala b/tests/neg-custom-args/captures/filevar.scala index bcf2d7beccbf..0d9cbed164e3 100644 --- a/tests/neg-custom-args/captures/filevar.scala +++ b/tests/neg-custom-args/captures/filevar.scala @@ -8,7 +8,7 @@ class Service: var file: File^ = uninitialized // error def log = file.write("log") -def withFile[T](op: (l: caps.Cap) ?-> (f: File^{l}) => T): T = +def withFile[T](op: (l: caps.Capability) ?-> (f: File^{l}) => T): T = op(using caps.cap)(new File) def test = diff --git a/tests/neg-custom-args/captures/i15923.scala b/tests/neg-custom-args/captures/i15923.scala index 89688449bdac..e71f01996938 100644 --- a/tests/neg-custom-args/captures/i15923.scala +++ b/tests/neg-custom-args/captures/i15923.scala @@ -3,7 +3,7 @@ type Id[X] = [T] -> (op: X => T) -> T def mkId[X](x: X): Id[X] = [T] => (op: X => T) => op(x) def bar() = { - def withCap[X](op: (lcap: caps.Cap) ?-> Cap^{lcap} => X): X = { + def withCap[X](op: (lcap: caps.Capability) ?-> Cap^{lcap} => X): X = { val cap: Cap = new Cap { def use() = { println("cap is used"); 0 } } val result = op(using caps.cap)(cap) result diff --git a/tests/neg-custom-args/captures/i16725.scala b/tests/neg-custom-args/captures/i16725.scala index ff06b3be78a7..853b3e96ca5c 100644 --- a/tests/neg-custom-args/captures/i16725.scala +++ b/tests/neg-custom-args/captures/i16725.scala @@ -1,6 +1,5 @@ import language.experimental.captureChecking -@annotation.capability -class IO: +class IO extends caps.Capability: def brewCoffee(): Unit = ??? def usingIO[T](op: IO => T): T = ??? @@ -8,8 +7,8 @@ type Wrapper[T] = [R] -> (f: T => R) -> R def mk[T](x: T): Wrapper[T] = [R] => f => f(x) def useWrappedIO(wrapper: Wrapper[IO]): () -> Unit = () => - wrapper: io => // error + wrapper: io => io.brewCoffee() def main(): Unit = - val escaped = usingIO(io => useWrappedIO(mk(io))) + val escaped = usingIO(io => useWrappedIO(mk(io))) // error // error escaped() // boom diff --git a/tests/neg-custom-args/captures/inner-classes.scala b/tests/neg-custom-args/captures/inner-classes.scala index 181b830e4996..fd500e607970 100644 --- a/tests/neg-custom-args/captures/inner-classes.scala +++ b/tests/neg-custom-args/captures/inner-classes.scala @@ -1,6 +1,6 @@ object test: - @annotation.capability class FileSystem + class FileSystem extends caps.Capability def foo(fs: FileSystem) = diff --git a/tests/neg-custom-args/captures/stack-alloc.scala b/tests/neg-custom-args/captures/stack-alloc.scala index 1b93d2e5129d..80e7e4169720 100644 --- a/tests/neg-custom-args/captures/stack-alloc.scala +++ b/tests/neg-custom-args/captures/stack-alloc.scala @@ -5,7 +5,7 @@ class Pooled val stack = mutable.ArrayBuffer[Pooled]() var nextFree = 0 -def withFreshPooled[T](op: (lcap: caps.Cap) ?-> Pooled^{lcap} => T): T = +def withFreshPooled[T](op: (lcap: caps.Capability) ?-> Pooled^{lcap} => T): T = if nextFree >= stack.size then stack.append(new Pooled) val pooled = stack(nextFree) nextFree = nextFree + 1 diff --git a/tests/neg-custom-args/captures/try3.scala b/tests/neg-custom-args/captures/try3.scala index 004cda6a399c..880d20ef16a0 100644 --- a/tests/neg-custom-args/captures/try3.scala +++ b/tests/neg-custom-args/captures/try3.scala @@ -4,7 +4,7 @@ class CT[E] type CanThrow[E] = CT[E]^ type Top = Any^ -def handle[E <: Exception, T <: Top](op: (lcap: caps.Cap) ?-> CT[E]^{lcap} ?=> T)(handler: E => T): T = +def handle[E <: Exception, T <: Top](op: (lcap: caps.Capability) ?-> CT[E]^{lcap} ?=> T)(handler: E => T): T = val x: CT[E] = ??? try op(using caps.cap)(using x) catch case ex: E => handler(ex) diff --git a/tests/neg-custom-args/captures/usingLogFile.scala b/tests/neg-custom-args/captures/usingLogFile.scala index 25b853913af9..b25e4e75a784 100644 --- a/tests/neg-custom-args/captures/usingLogFile.scala +++ b/tests/neg-custom-args/captures/usingLogFile.scala @@ -3,7 +3,7 @@ import annotation.capability object Test1: - def usingLogFile[T](op: (local: caps.Cap) ?-> FileOutputStream => T): T = + def usingLogFile[T](op: (local: caps.Capability) ?-> FileOutputStream => T): T = val logFile = FileOutputStream("log") val result = op(using caps.cap)(logFile) logFile.close() diff --git a/tests/neg/unsound-reach.check b/tests/neg/unsound-reach.check index fd5c401416d1..8cabbe1571a0 100644 --- a/tests/neg/unsound-reach.check +++ b/tests/neg/unsound-reach.check @@ -1,5 +1,5 @@ --- Error: tests/neg/unsound-reach.scala:18:9 --------------------------------------------------------------------------- -18 | boom.use(f): (f1: File^{backdoor*}) => // error - | ^^^^^^^^ - | Reach capability backdoor* and universal capability cap cannot both - | appear in the type (x: File^)(op: box File^{backdoor*} => Unit): Unit of this expression +-- Error: tests/neg/unsound-reach.scala:18:13 -------------------------------------------------------------------------- +18 | boom.use(f): (f1: File^{backdoor*}) => // error + | ^^^^^^^^ + | Reach capability backdoor* and universal capability cap cannot both + | appear in the type (x: File^)(op: box File^{backdoor*} => Unit): Unit of this expression diff --git a/tests/neg/unsound-reach.scala b/tests/neg/unsound-reach.scala index 468730168019..48a74f86d311 100644 --- a/tests/neg/unsound-reach.scala +++ b/tests/neg/unsound-reach.scala @@ -5,16 +5,16 @@ trait File: def withFile[R](path: String)(op: File^ => R): R = ??? trait Foo[+X]: - def use(x: File^)(op: X => Unit): Unit + def use(x: File^)(op: X => Unit): Unit class Bar extends Foo[File^]: - def use(x: File^)(op: File^ => Unit): Unit = op(x) + def use(x: File^)(op: File^ => Unit): Unit = op(x) def bad(): Unit = - val backdoor: Foo[File^] = new Bar - val boom: Foo[File^{backdoor*}] = backdoor + val backdoor: Foo[File^] = new Bar + val boom: Foo[File^{backdoor*}] = backdoor - var escaped: File^{backdoor*} = null - withFile("hello.txt"): f => - boom.use(f): (f1: File^{backdoor*}) => // error - escaped = f1 + var escaped: File^{backdoor*} = null + withFile("hello.txt"): f => + boom.use(f): (f1: File^{backdoor*}) => // error + escaped = f1 diff --git a/tests/pos-custom-args/captures/boxed1.scala b/tests/pos-custom-args/captures/boxed1.scala index 8c6b63ef0134..e2ff69c305d2 100644 --- a/tests/pos-custom-args/captures/boxed1.scala +++ b/tests/pos-custom-args/captures/boxed1.scala @@ -1,6 +1,6 @@ class Box[T](val x: T) -@annotation.capability class Cap +class Cap extends caps.Capability def foo(x: => Int): Unit = () diff --git a/tests/pos-custom-args/captures/capt-capability.scala b/tests/pos-custom-args/captures/capt-capability.scala index 830d341c7bca..64892218ee41 100644 --- a/tests/pos-custom-args/captures/capt-capability.scala +++ b/tests/pos-custom-args/captures/capt-capability.scala @@ -1,7 +1,7 @@ import annotation.capability +import caps.Capability -@capability class Cap -def f1(c: Cap): () ->{c} c.type = () => c // ok +def f1(c: Capability): () ->{c} c.type = () => c // ok def f2: Int = val g: Boolean => Int = ??? @@ -15,15 +15,15 @@ def f3: Int = x def foo() = - val x: Cap = ??? - val y: Cap = x - val x2: () ->{x} Cap = ??? - val y2: () ->{x} Cap = x2 + val x: Capability = ??? + val y: Capability = x + val x2: () ->{x} Capability = ??? + val y2: () ->{x} Capability = x2 - val z1: () => Cap = f1(x) + val z1: () => Capability = f1(x) def h[X](a: X)(b: X) = a val z2 = - if x == null then () => x else () => Cap() + if x == null then () => x else () => new Capability() {} val _ = x diff --git a/tests/pos-custom-args/captures/caseclass.scala b/tests/pos-custom-args/captures/caseclass.scala index ffbf878dca49..0aa656eaf9cb 100644 --- a/tests/pos-custom-args/captures/caseclass.scala +++ b/tests/pos-custom-args/captures/caseclass.scala @@ -1,4 +1,4 @@ -@annotation.capability class C +class C extends caps.Capability object test1: case class Ref(x: String^) diff --git a/tests/pos-custom-args/captures/cc-this.scala b/tests/pos-custom-args/captures/cc-this.scala index 12c62e99d186..d9705df76c55 100644 --- a/tests/pos-custom-args/captures/cc-this.scala +++ b/tests/pos-custom-args/captures/cc-this.scala @@ -1,4 +1,4 @@ -@annotation.capability class Cap +class Cap extends caps.Capability def eff(using Cap): Unit = () diff --git a/tests/pos-custom-args/captures/eta-expansions.scala b/tests/pos-custom-args/captures/eta-expansions.scala index 1aac7ded1b50..b4e38cdf0856 100644 --- a/tests/pos-custom-args/captures/eta-expansions.scala +++ b/tests/pos-custom-args/captures/eta-expansions.scala @@ -1,4 +1,4 @@ -@annotation.capability class Cap +class Cap extends caps.Capability def test(d: Cap) = def map2(xs: List[Int])(f: Int => Int): List[Int] = xs.map(f) diff --git a/tests/pos-custom-args/captures/filevar-tracked.scala b/tests/pos-custom-args/captures/filevar-tracked.scala new file mode 100644 index 000000000000..6fb7000ad4c2 --- /dev/null +++ b/tests/pos-custom-args/captures/filevar-tracked.scala @@ -0,0 +1,38 @@ +import language.experimental.captureChecking +import language.experimental.modularity +import annotation.capability +import compiletime.uninitialized + +object test1: + class File: + def write(x: String): Unit = ??? + + class Service(f: File^): + def log = f.write("log") + + def withFile[T](op: (f: File^) => T): T = + op(new File) + + def test = + withFile: f => + val o = Service(f) + o.log + +object test2: + class IO extends caps.Capability + + class File: + def write(x: String): Unit = ??? + + class Service(tracked val io: IO): + var file: File^{io} = uninitialized + def log = file.write("log") + + def withFile[T](io2: IO)(op: (f: File^{io2}) => T): T = + op(new File) + + def test(io3: IO) = + withFile(io3): f => + val o = Service(io3) + o.file = f + o.log diff --git a/tests/pos-custom-args/captures/filevar.scala b/tests/pos-custom-args/captures/filevar.scala index a6cc7ca9ff47..c5571ca88849 100644 --- a/tests/pos-custom-args/captures/filevar.scala +++ b/tests/pos-custom-args/captures/filevar.scala @@ -1,4 +1,5 @@ import language.experimental.captureChecking +import language.experimental.modularity import annotation.capability import compiletime.uninitialized @@ -18,20 +19,20 @@ object test1: o.log object test2: - @capability class IO + class IO class File: def write(x: String): Unit = ??? - class Service(io: IO): + class Service(io: IO^): var file: File^{io} = uninitialized def log = file.write("log") - def withFile[T](io: IO)(op: (f: File^{io}) => T): T = + def withFile[T](io2: IO^)(op: (f: File^{io2}) => T): T = op(new File) - def test(io: IO) = - withFile(io): f => - val o = Service(io) + def test(io3: IO^) = + withFile(io3): f => + val o = Service(io3) o.file = f o.log diff --git a/tests/pos-custom-args/captures/i16116.scala b/tests/pos-custom-args/captures/i16116.scala index 0311e744f146..979bfdbe4328 100644 --- a/tests/pos-custom-args/captures/i16116.scala +++ b/tests/pos-custom-args/captures/i16116.scala @@ -15,8 +15,7 @@ object CpsMonad { @experimental object Test { - @capability - class CpsTransform[F[_]] { + class CpsTransform[F[_]] extends caps.Capability { def await[T](ft: F[T]): T^{ this } = ??? } diff --git a/tests/pos-custom-args/captures/i16226.scala b/tests/pos-custom-args/captures/i16226.scala index 4cd7f0ceea81..071eefbd3420 100644 --- a/tests/pos-custom-args/captures/i16226.scala +++ b/tests/pos-custom-args/captures/i16226.scala @@ -1,4 +1,4 @@ -@annotation.capability class Cap +class Cap extends caps.Capability class LazyRef[T](val elem: () => T): val get: () ->{elem} T = elem diff --git a/tests/pos-custom-args/captures/i19751.scala b/tests/pos-custom-args/captures/i19751.scala index b6023cc0ff87..30bd8677f024 100644 --- a/tests/pos-custom-args/captures/i19751.scala +++ b/tests/pos-custom-args/captures/i19751.scala @@ -3,7 +3,7 @@ import annotation.capability import caps.cap trait Ptr[A] -@capability trait Scope: +trait Scope extends caps.Capability: def allocate(size: Int): Ptr[Unit]^{this} diff --git a/tests/pos-custom-args/captures/lazyref.scala b/tests/pos-custom-args/captures/lazyref.scala index 3dae51b491b4..2e3a0030bcdc 100644 --- a/tests/pos-custom-args/captures/lazyref.scala +++ b/tests/pos-custom-args/captures/lazyref.scala @@ -1,4 +1,4 @@ -@annotation.capability class Cap +class Cap extends caps.Capability class LazyRef[T](val elem: () => T): val get: () ->{elem} T = elem diff --git a/tests/pos-custom-args/captures/lists.scala b/tests/pos-custom-args/captures/lists.scala index 99505f0bb7a2..5f4991c6be54 100644 --- a/tests/pos-custom-args/captures/lists.scala +++ b/tests/pos-custom-args/captures/lists.scala @@ -18,7 +18,7 @@ object NIL extends LIST[Nothing]: def map[A, B](f: A => B)(xs: LIST[A]): LIST[B] = xs.map(f) -@annotation.capability class Cap +class Cap extends caps.Capability def test(c: Cap, d: Cap, e: Cap) = def f(x: Cap): Unit = if c == x then () @@ -30,7 +30,7 @@ def test(c: Cap, d: Cap, e: Cap) = CONS(z, ys) val zsc: LIST[Cap ->{d, y} Unit] = zs val z1 = zs.head - val z1c: Cap^ ->{y, d} Unit = z1 + val z1c: Cap ->{y, d} Unit = z1 val ys1 = zs.tail val y1 = ys1.head diff --git a/tests/pos-custom-args/captures/logger-tracked.scala b/tests/pos-custom-args/captures/logger-tracked.scala new file mode 100644 index 000000000000..1949e25b00d9 --- /dev/null +++ b/tests/pos-custom-args/captures/logger-tracked.scala @@ -0,0 +1,68 @@ +import annotation.capability +import language.experimental.saferExceptions +import language.experimental.modularity + +class FileSystem extends caps.Capability + +class Logger(using tracked val fs: FileSystem): + def log(s: String): Unit = ??? + +def test(using fs: FileSystem) = + val l: Logger^{fs} = Logger(using fs) + l.log("hello world!") + val xs: LazyList[Int]^{l} = + LazyList.from(1) + .map { i => + l.log(s"computing elem # $i") + i * i + } + +trait LazyList[+A]: + def isEmpty: Boolean + def head: A + def tail: LazyList[A]^{this} + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + +final class LazyCons[+T](val x: T, val xs: () => LazyList[T]^) extends LazyList[T]: + def isEmpty = false + def head = x + def tail: LazyList[T]^{this} = xs() +end LazyCons + +extension [A](x: A) + def #::(xs1: => LazyList[A]^): LazyList[A]^{xs1} = + LazyCons(x, () => xs1) + +extension [A](xs: LazyList[A]^) + def map[B](f: A => B): LazyList[B]^{xs, f} = + if xs.isEmpty then LazyNil + else f(xs.head) #:: xs.tail.map(f) + +object LazyList: + def from(start: Int): LazyList[Int] = + start #:: from(start + 1) + +class Pair[+A, +B](x: A, y: B): + def fst: A = x + def snd: B = y + +def test2(ct: CanThrow[Exception], fs: FileSystem) = + def x: Int ->{ct} String = ??? + def y: Logger^{fs} = ??? + def p = Pair[Int ->{ct} String, Logger^{fs}](x, y) + def p3 = Pair(x, y) + def f = () => p.fst + + +/* + val l1: Int => String = ??? + val l2: Object^{c} = ??? + val pd = () => Pair(l1, l2) + val p2: Pair[Int => String, Object]^{c} = pd() + val hd = () => p2.fst + +*/ \ No newline at end of file diff --git a/tests/pos-custom-args/captures/logger.scala b/tests/pos-custom-args/captures/logger.scala index d95eeaae74cf..75ea3ac3fbc0 100644 --- a/tests/pos-custom-args/captures/logger.scala +++ b/tests/pos-custom-args/captures/logger.scala @@ -1,12 +1,13 @@ import annotation.capability import language.experimental.saferExceptions +import language.experimental.modularity -@capability class FileSystem +class FileSystem // does not work with extends caps.Capability -class Logger(using fs: FileSystem): +class Logger(using fs: FileSystem^): def log(s: String): Unit = ??? -def test(using fs: FileSystem) = +def test(using fs: FileSystem^) = val l: Logger^{fs} = Logger(using fs) l.log("hello world!") val xs: LazyList[Int]^{l} = @@ -49,7 +50,7 @@ class Pair[+A, +B](x: A, y: B): def fst: A = x def snd: B = y -def test2(ct: CanThrow[Exception], fs: FileSystem) = +def test2(ct: CanThrow[Exception], fs: FileSystem^) = def x: Int ->{ct} String = ??? def y: Logger^{fs} = ??? def p = Pair[Int ->{ct} String, Logger^{fs}](x, y) diff --git a/tests/pos-custom-args/captures/nested-classes-tracked.scala b/tests/pos-custom-args/captures/nested-classes-tracked.scala new file mode 100644 index 000000000000..1c81441f321b --- /dev/null +++ b/tests/pos-custom-args/captures/nested-classes-tracked.scala @@ -0,0 +1,22 @@ +import language.experimental.captureChecking +import language.experimental.modularity +import annotation.{capability, constructorOnly} + +class IO extends caps.Capability +class Blah +class Pkg(using tracked val io: IO): + class Foo: + def m(foo: Blah^{io}) = ??? +class Pkg2(using tracked val io: IO): + class Foo: + def m(foo: Blah^{io}): Any = io; ??? + +def main(using io: IO) = + val pkg = Pkg() + val f = pkg.Foo() + f.m(???) + val pkg2 = Pkg2() + val f2 = pkg2.Foo() + f2.m(???) + + diff --git a/tests/pos-custom-args/captures/nested-classes.scala b/tests/pos-custom-args/captures/nested-classes.scala index b16fc4365183..4a0da34faf5c 100644 --- a/tests/pos-custom-args/captures/nested-classes.scala +++ b/tests/pos-custom-args/captures/nested-classes.scala @@ -1,16 +1,17 @@ import language.experimental.captureChecking +import language.experimental.modularity import annotation.{capability, constructorOnly} -@capability class IO +class IO // does not work with extends caps.Capability class Blah -class Pkg(using @constructorOnly io: IO): +class Pkg(using io: IO^): class Foo: def m(foo: Blah^{io}) = ??? -class Pkg2(using io: IO): +class Pkg2(using io: IO^): class Foo: def m(foo: Blah^{io}): Any = io; ??? -def main(using io: IO) = +def main(using io: IO^) = val pkg = Pkg() val f = pkg.Foo() f.m(???) diff --git a/tests/pos-custom-args/captures/null-logger.scala b/tests/pos-custom-args/captures/null-logger.scala index 0b32d045778c..958002ad0358 100644 --- a/tests/pos-custom-args/captures/null-logger.scala +++ b/tests/pos-custom-args/captures/null-logger.scala @@ -1,7 +1,7 @@ import annotation.capability import annotation.constructorOnly -@capability class FileSystem +class FileSystem extends caps.Capability class NullLogger(using @constructorOnly fs: FileSystem) diff --git a/tests/pos-custom-args/captures/pairs.scala b/tests/pos-custom-args/captures/pairs.scala index e15a76970c29..da7f30185ad3 100644 --- a/tests/pos-custom-args/captures/pairs.scala +++ b/tests/pos-custom-args/captures/pairs.scala @@ -1,6 +1,6 @@ //class CC //type Cap = CC^ -@annotation.capability class Cap +class Cap extends caps.Capability object Generic: @@ -13,6 +13,6 @@ object Generic: def g(x: Cap): Unit = if d == x then () val p = Pair(f, g) val x1 = p.fst - val x1c: Cap^ ->{c} Unit = x1 + val x1c: Cap ->{c} Unit = x1 val y1 = p.snd - val y1c: Cap^ ->{d} Unit = y1 + val y1c: Cap ->{d} Unit = y1 diff --git a/tests/pos-custom-args/captures/reaches.scala b/tests/pos-custom-args/captures/reaches.scala index f17c25712c39..f82c792c8445 100644 --- a/tests/pos-custom-args/captures/reaches.scala +++ b/tests/pos-custom-args/captures/reaches.scala @@ -48,7 +48,7 @@ def compose2[A, B, C](f: A => B, g: B => C): A => C = def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] = ps.map((x, y) => compose1(x, y)) // Does not work if map takes an impure function, see reaches in neg -@annotation.capability class IO +class IO extends caps.Capability def test(io: IO) = val a: () ->{io} Unit = () => () diff --git a/tests/pos-custom-args/captures/try3.scala b/tests/pos-custom-args/captures/try3.scala index b44ea57ccae4..305069d3ae9f 100644 --- a/tests/pos-custom-args/captures/try3.scala +++ b/tests/pos-custom-args/captures/try3.scala @@ -2,7 +2,7 @@ import language.experimental.erasedDefinitions import annotation.capability import java.io.IOException -@annotation.capability class CanThrow[-E] +class CanThrow[-E] extends caps.Capability def handle[E <: Exception, T](op: CanThrow[E] ?=> T)(handler: E => T): T = val x: CanThrow[E] = ??? diff --git a/tests/pos-custom-args/captures/vars.scala b/tests/pos-custom-args/captures/vars.scala index a335be96fed1..5c9598fab508 100644 --- a/tests/pos-custom-args/captures/vars.scala +++ b/tests/pos-custom-args/captures/vars.scala @@ -1,4 +1,4 @@ -@annotation.capability class Cap +class Cap extends caps.Capability def test(cap1: Cap, cap2: Cap) = def f(x: String): String = if cap1 == cap1 then "" else "a" diff --git a/tests/pos/dotty-experimental.scala b/tests/pos/dotty-experimental.scala index ee9a84a1b497..813c9b5920c1 100644 --- a/tests/pos/dotty-experimental.scala +++ b/tests/pos/dotty-experimental.scala @@ -3,6 +3,6 @@ import language.experimental.captureChecking object test { - val x: caps.Cap = caps.cap + val x: caps.Capability = caps.cap } diff --git a/tests/pos/i20237.scala b/tests/pos/i20237.scala index da3e902b78b4..0a5eb6d9a332 100644 --- a/tests/pos/i20237.scala +++ b/tests/pos/i20237.scala @@ -1,7 +1,7 @@ import language.experimental.captureChecking import scala.annotation.capability -@capability class Cap: +class Cap extends caps.Capability: def use[T](body: Cap ?=> T) = body(using this) class Box[T](body: Cap ?=> T): diff --git a/tests/pos/into-bigint.scala b/tests/pos/into-bigint.scala index d7ecee40b3ba..409b5e79da2c 100644 --- a/tests/pos/into-bigint.scala +++ b/tests/pos/into-bigint.scala @@ -14,8 +14,8 @@ object BigInt: @main def Test = val x = BigInt(2) val y = 3 - val a1 = x + y - val a2 = y * x + val a1 = x + y // uses conversion on `y` + val a2 = y * x // uses conversion on `y` val a3 = x * x val a4 = y + y From 6baff2dfac206fac0f7c92c56adb6c12fd41ca08 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 14 May 2024 18:29:48 +0200 Subject: [PATCH 04/10] Drop convention on classes inheriting from universal capturing types Drop convention that classes inheriting from universal capturing types are capability classes. Capture sets of parents are instead ignored. The convention led to algebraic anomalies. For instance if class C extends A => B, Serializable then C <: (A => B) & Serializable, which has an empty capture set. Yet we treat every occurrence of C as implicitly carrying `cap`. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 29 +------------------ .../dotty/tools/dotc/transform/Recheck.scala | 1 - .../captures/extending-impure-function.scala | 0 3 files changed, 1 insertion(+), 29 deletions(-) rename tests/{neg-custom-args => pos-custom-args}/captures/extending-impure-function.scala (100%) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 1a8c65c89a43..bdbc00674563 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -23,7 +23,6 @@ trait SetupAPI: def setupUnit(tree: Tree, recheckDef: DefRecheck)(using Context): Unit def isPreCC(sym: Symbol)(using Context): Boolean def postCheck()(using Context): Unit - def isCapabilityClassRef(tp: Type)(using Context): Boolean object Setup: @@ -68,29 +67,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: && !sym.owner.is(CaptureChecked) && !defn.isFunctionSymbol(sym.owner) - private val capabilityClassMap = new util.HashMap[Symbol, Boolean] - - /** Check if the class is capability, which means: - * 1. the class has a capability annotation, - * 2. or at least one of its parent type has universal capability. - */ - def isCapabilityClassRef(tp: Type)(using Context): Boolean = tp.dealiasKeepAnnots match - case _: TypeRef | _: AppliedType => - val sym = tp.classSymbol - def checkSym: Boolean = sym.info.parents.exists(hasUniversalCapability) - sym.isClass && capabilityClassMap.getOrElseUpdate(sym, checkSym) - case _ => false - - private def hasUniversalCapability(tp: Type)(using Context): Boolean = tp.dealiasKeepAnnots match - case CapturingType(parent, refs) => - refs.isUniversal || hasUniversalCapability(parent) - case AnnotatedType(parent, ann) => - if ann.symbol.isRetains then - try ann.tree.toCaptureSet.isUniversal || hasUniversalCapability(parent) - catch case ex: IllegalCaptureRef => false - else hasUniversalCapability(parent) - case tp => isCapabilityClassRef(tp) - private def fluidify(using Context) = new TypeMap with IdempotentCaptRefMap: def apply(t: Type): Type = t match case t: MethodType => @@ -317,10 +293,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case t: TypeVar => this(t.underlying) case t => - // Map references to capability classes C to C^ - if isCapabilityClassRef(t) - then CapturingType(t, defn.expandedUniversalSet, boxed = false) - else recur(t) + recur(t) end expandAliases val tp1 = expandAliases(tp) // TODO: Do we still need to follow aliases? diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index f809fbd176ce..e40c8346ed82 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -33,7 +33,6 @@ object Recheck: * Scala2ModuleVar cannot be also ParamAccessors. */ val ResetPrivate = Scala2ModuleVar - val ResetPrivateParamAccessor = ResetPrivate | ParamAccessor /** Attachment key for rechecked types of TypeTrees */ val RecheckedType = Property.Key[Type] diff --git a/tests/neg-custom-args/captures/extending-impure-function.scala b/tests/pos-custom-args/captures/extending-impure-function.scala similarity index 100% rename from tests/neg-custom-args/captures/extending-impure-function.scala rename to tests/pos-custom-args/captures/extending-impure-function.scala From 08557a16fecf5cc41686c99d6fcd745147073055 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 14 May 2024 20:56:42 +0200 Subject: [PATCH 05/10] Add fewer parameter refinements. Only enrich classes with capture refinements for a parameter if the deep capture set of the parameter's type is nonempty. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index bdbc00674563..67977b027137 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -57,7 +57,20 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: private val toBeUpdated = new mutable.HashSet[Symbol] private def newFlagsFor(symd: SymDenotation)(using Context): FlagSet = - if symd.isAllOf(PrivateParamAccessor) && symd.owner.is(CaptureChecked) && !symd.hasAnnotation(defn.ConstructorOnlyAnnot) + + object containsCovarRetains extends TypeAccumulator[Boolean]: + def apply(x: Boolean, tp: Type): Boolean = + if x then true + else if tp.derivesFromCapability && variance >= 0 then true + else tp match + case AnnotatedType(_, ann) if ann.symbol.isRetains && variance >= 0 => true + case _ => foldOver(x, tp) + def apply(tp: Type): Boolean = apply(false, tp) + + if symd.isAllOf(PrivateParamAccessor) + && symd.owner.is(CaptureChecked) + && !symd.hasAnnotation(defn.ConstructorOnlyAnnot) + //&& containsCovarRetains(symd.symbol.originDenotation.info) then symd.flags &~ Private | Recheck.ResetPrivate else symd.flags From f02b5fbca3e1f830a51f3e02a3db10a6ac7c40ee Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 15 May 2024 15:42:19 +0200 Subject: [PATCH 06/10] Drop ResetPrivate flag --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 4 ++-- .../src/dotty/tools/dotc/transform/OverridingPairs.scala | 4 +++- compiler/src/dotty/tools/dotc/transform/Recheck.scala | 7 ------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 67977b027137..0175d40c186c 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -70,8 +70,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if symd.isAllOf(PrivateParamAccessor) && symd.owner.is(CaptureChecked) && !symd.hasAnnotation(defn.ConstructorOnlyAnnot) - //&& containsCovarRetains(symd.symbol.originDenotation.info) - then symd.flags &~ Private | Recheck.ResetPrivate + && containsCovarRetains(symd.symbol.originDenotation.info) + then symd.flags &~ Private else symd.flags def isPreCC(sym: Symbol)(using Context): Boolean = diff --git a/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala b/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala index 4020291dded0..6529eed77fa0 100644 --- a/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala +++ b/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala @@ -34,7 +34,9 @@ object OverridingPairs: */ protected def exclude(sym: Symbol): Boolean = !sym.memberCanMatchInheritedSymbols - || isCaptureChecking && sym.is(Recheck.ResetPrivate) + || isCaptureChecking && atPhase(ctx.phase.prev)(sym.is(Private)) + // for capture checking we drop the private flag of certain parameter accessors + // but these still need no overriding checks /** The parents of base that are checked when deciding whether an overriding * pair has already been treated in a parent class. diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index e40c8346ed82..79dfe3393578 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -27,13 +27,6 @@ import annotation.tailrec object Recheck: import tpd.* - /** A flag used to indicate that a ParamAccessor has been temporarily made not-private - * Only used at the start of the Recheck phase, reset at its end. - * The flag repurposes the Scala2ModuleVar flag. No confusion is possible since - * Scala2ModuleVar cannot be also ParamAccessors. - */ - val ResetPrivate = Scala2ModuleVar - /** Attachment key for rechecked types of TypeTrees */ val RecheckedType = Property.Key[Type] From 01775760b007fad97eff82905018ecf3a769ef60 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 17 May 2024 13:31:21 +0200 Subject: [PATCH 07/10] Turn nested environment capture sets into constants at the end of box adaptation This change lets more ref trees with underlying function types keep their singleton types. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 2 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 4 +-- .../dotty/tools/dotc/core/TypeComparer.scala | 28 ++++++++++++++----- tests/neg-custom-args/captures/byname.check | 6 ++-- tests/neg-custom-args/captures/eta.check | 2 +- tests/neg-custom-args/captures/i15772.check | 2 +- .../neg-custom-args/captures/outer-var.check | 4 +-- 7 files changed, 31 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 06ba36bd5e24..1a81e2bc1014 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1080,7 +1080,7 @@ object CaptureSet: case _ => empty recur(tp) - .showing(i"capture set of $tp = $result", captDebug) + //.showing(i"capture set of $tp = $result", captDebug) private def deepCaptureSet(tp: Type)(using Context): CaptureSet = val collect = new TypeAccumulator[CaptureSet]: diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index cd0f21129ac9..30d05a744e31 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1006,7 +1006,7 @@ class CheckCaptures extends Recheck, SymTransformer: if (ares1 eq ares) && (aargs1 eq aargs) then actual else reconstruct(aargs1, ares1) - (resTp, curEnv.captured) + (resTp, CaptureSet(curEnv.captured.elems)) end adaptFun /** Adapt type function type `actual` to the expected type. @@ -1028,7 +1028,7 @@ class CheckCaptures extends Recheck, SymTransformer: if ares1 eq ares then actual else reconstruct(ares1) - (resTp, curEnv.captured) + (resTp, CaptureSet(curEnv.captured.elems)) end adaptTypeFun def adaptInfo(actual: Type, expected: Type, covariant: Boolean): String = diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index c2c502a984c4..a9bb0406a14d 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -842,13 +842,27 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling val refs1 = tp1.captureSet try if refs1.isAlwaysEmpty then recur(tp1, parent2) - else subCaptures(refs1, refs2, frozenConstraint).isOK - && sameBoxed(tp1, tp2, refs1) - && (recur(tp1.widen.stripCapturing, parent2) - || tp1.isInstanceOf[SingletonType] && recur(tp1, parent2) - // this alternative is needed in case the right hand side is a - // capturing type that contains the lhs as an alternative of a union type. - ) + else + // The singletonOK branch is because we sometimes have a larger capture set in a singleton + // than in its underlying type. An example is `f: () -> () ->{x} T`, which might be + // the type of a closure. In that case the capture set of `f.type` is `{x}` but the + // capture set of the underlying type is `{}`. So without the `singletonOK` test, a singleton + // might not be a subtype of its underlying type. Examples where this arises is + // capt-capibility.scala and function-combinators.scala + val singletonOK = tp1 match + case tp1: SingletonType + if subCaptures(tp1.underlying.captureSet, refs2, frozen = true).isOK => + recur(tp1.widen, tp2) + case _ => + false + singletonOK + || subCaptures(refs1, refs2, frozenConstraint).isOK + && sameBoxed(tp1, tp2, refs1) + && (recur(tp1.widen.stripCapturing, parent2) + || tp1.isInstanceOf[SingletonType] && recur(tp1, parent2) + // this alternative is needed in case the right hand side is a + // capturing type that contains the lhs as an alternative of a union type. + ) catch case ex: AssertionError => println(i"assertion failed while compare captured $tp1 <:< $tp2") throw ex diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index 2b48428e97bc..b9e5c81b721d 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -6,13 +6,13 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:4:2 ----------------------------------------- 4 | def f() = if cap1 == cap1 then g else g // error | ^ - | Found: (x$0: Int) ->{cap2} Int - | Required: (x$0: Int) -> Int + | Found: ((x$0: Int) ->{cap2} Int)^{} + | Required: Int -> Int | | Note that the expected type Int ->{} Int | is the previously inferred result type of method test | which is also the type seen in separately compiled sources. - | The new inferred type (x$0: Int) ->{cap2} Int + | The new inferred type ((x$0: Int) ->{cap2} Int)^{} | must conform to this type. 5 | def g(x: Int) = if cap2 == cap2 then 1 else x 6 | def g2(x: Int) = if cap1 == cap1 then 1 else x diff --git a/tests/neg-custom-args/captures/eta.check b/tests/neg-custom-args/captures/eta.check index 91dfdf06d3cd..9850e54a7fdf 100644 --- a/tests/neg-custom-args/captures/eta.check +++ b/tests/neg-custom-args/captures/eta.check @@ -1,7 +1,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/eta.scala:4:9 -------------------------------------------- 4 | g // error | ^ - | Found: () ->? A + | Found: (g : () -> A) | Required: () -> Proc^{f} | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index cce58da1b93b..0f8f0bf6eac5 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -35,7 +35,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:44:2 ---------------------------------------- 44 | x: (() -> Unit) // error | ^ - | Found: () ->{x} Unit + | Found: (x : () ->{filesList, sayHello} Unit) | Required: () -> Unit | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/outer-var.check b/tests/neg-custom-args/captures/outer-var.check index c250280961d9..b9f1f57be769 100644 --- a/tests/neg-custom-args/captures/outer-var.check +++ b/tests/neg-custom-args/captures/outer-var.check @@ -1,7 +1,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:11:8 ------------------------------------- 11 | x = q // error | ^ - | Found: () ->{q} Unit + | Found: (q : Proc) | Required: () ->{p, q²} Unit | | where: q is a parameter in method inner @@ -28,7 +28,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:14:8 ------------------------------------- 14 | y = q // error | ^ - | Found: () ->{q} Unit + | Found: (q : Proc) | Required: () ->{p} Unit | | Note that reference (q : Proc), defined in method inner From 4487eebad7be7ffcbfafcfa2a4b1d6851a6c0071 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 19 May 2024 10:36:57 +0200 Subject: [PATCH 08/10] Two fixes to make tests pass as before - Avoid creating capture sets of untrackable references - Refine disallowRootCapability to consider only explicit captures --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 4 ++- .../dotty/tools/dotc/cc/CheckCaptures.scala | 6 ++-- tests/neg-custom-args/captures/capt1.check | 2 +- tests/neg-custom-args/captures/i16725.scala | 2 +- .../captures/filevar-expanded.scala | 36 +++++++++++++++++++ tests/pos-custom-args/captures/filevar.scala | 10 +++--- tests/pos-custom-args/captures/logger.scala | 8 ++--- .../captures/nested-classes.scala | 12 +++---- 8 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 tests/pos-custom-args/captures/filevar-expanded.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 1a81e2bc1014..5422706f7c40 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1037,7 +1037,9 @@ object CaptureSet: /** The capture set of the type underlying CaptureRef */ def ofInfo(ref: CaptureRef)(using Context): CaptureSet = ref match - case ref: (TermRef | TermParamRef) if ref.isMaxCapability => ref.singletonCaptureSet + case ref: (TermRef | TermParamRef) if ref.isMaxCapability => + if ref.isTrackableRef then ref.singletonCaptureSet + else CaptureSet.universal case ReachCapability(ref1) => deepCaptureSet(ref1.widen) .showing(i"Deep capture set of $ref: ${ref1.widen} = $result", capt) case _ => ofType(ref.underlying, followResult = true) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 30d05a744e31..bde97a6d0387 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -157,15 +157,17 @@ object CheckCaptures: case _ => case AnnotatedType(_, ann) if ann.symbol == defn.UncheckedCapturesAnnot => () - case t => + case CapturingType(parent, refs) => if variance >= 0 then - t.captureSet.disallowRootCapability: () => + refs.disallowRootCapability: () => def part = if t eq tp then "" else i"the part $t of " report.error( em"""$what cannot $have $tp since |${part}that type captures the root capability `cap`. |$addendum""", pos) + traverse(parent) + case t => traverseChildren(t) check.traverse(tp) end disallowRootCapabilitiesIn diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index 74b9db728983..0e99d1876d3c 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -49,6 +49,6 @@ 34 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | Sealed type variable X cannot be instantiated to box () ->{x} Cap since - | the part C^ of that type captures the root capability `cap`. + | the part Cap of that type captures the root capability `cap`. | This is often caused by a local capability in an argument of method h | leaking as part of its result. diff --git a/tests/neg-custom-args/captures/i16725.scala b/tests/neg-custom-args/captures/i16725.scala index 853b3e96ca5c..733c2c562bbc 100644 --- a/tests/neg-custom-args/captures/i16725.scala +++ b/tests/neg-custom-args/captures/i16725.scala @@ -10,5 +10,5 @@ def useWrappedIO(wrapper: Wrapper[IO]): () -> Unit = wrapper: io => io.brewCoffee() def main(): Unit = - val escaped = usingIO(io => useWrappedIO(mk(io))) // error // error + val escaped = usingIO(io => useWrappedIO(mk(io))) // error escaped() // boom diff --git a/tests/pos-custom-args/captures/filevar-expanded.scala b/tests/pos-custom-args/captures/filevar-expanded.scala new file mode 100644 index 000000000000..13051994f346 --- /dev/null +++ b/tests/pos-custom-args/captures/filevar-expanded.scala @@ -0,0 +1,36 @@ +import language.experimental.captureChecking +import compiletime.uninitialized + +object test1: + class File: + def write(x: String): Unit = ??? + + class Service(f: File^): + def log = f.write("log") + + def withFile[T](op: (f: File^) => T): T = + op(new File) + + def test = + withFile: f => + val o = Service(f) + o.log + +object test2: + class IO + + class File: + def write(x: String): Unit = ??? + + class Service(io: IO^): + var file: File^{io} = uninitialized + def log = file.write("log") + + def withFile[T](io2: IO^)(op: (f: File^{io2}) => T): T = + op(new File) + + def test(io3: IO^) = + withFile(io3): f => + val o = Service(io3) + o.file = f + o.log diff --git a/tests/pos-custom-args/captures/filevar.scala b/tests/pos-custom-args/captures/filevar.scala index c5571ca88849..9ab34fe617b5 100644 --- a/tests/pos-custom-args/captures/filevar.scala +++ b/tests/pos-custom-args/captures/filevar.scala @@ -1,6 +1,4 @@ import language.experimental.captureChecking -import language.experimental.modularity -import annotation.capability import compiletime.uninitialized object test1: @@ -19,19 +17,19 @@ object test1: o.log object test2: - class IO + class IO extends caps.Capability class File: def write(x: String): Unit = ??? - class Service(io: IO^): + class Service(io: IO): var file: File^{io} = uninitialized def log = file.write("log") - def withFile[T](io2: IO^)(op: (f: File^{io2}) => T): T = + def withFile[T](io2: IO)(op: (f: File^{io2}) => T): T = op(new File) - def test(io3: IO^) = + def test(io3: IO) = withFile(io3): f => val o = Service(io3) o.file = f diff --git a/tests/pos-custom-args/captures/logger.scala b/tests/pos-custom-args/captures/logger.scala index 75ea3ac3fbc0..04aee89a227e 100644 --- a/tests/pos-custom-args/captures/logger.scala +++ b/tests/pos-custom-args/captures/logger.scala @@ -2,12 +2,12 @@ import annotation.capability import language.experimental.saferExceptions import language.experimental.modularity -class FileSystem // does not work with extends caps.Capability +class FileSystem extends caps.Capability -class Logger(using fs: FileSystem^): +class Logger(using fs: FileSystem): def log(s: String): Unit = ??? -def test(using fs: FileSystem^) = +def test(using fs: FileSystem) = val l: Logger^{fs} = Logger(using fs) l.log("hello world!") val xs: LazyList[Int]^{l} = @@ -50,7 +50,7 @@ class Pair[+A, +B](x: A, y: B): def fst: A = x def snd: B = y -def test2(ct: CanThrow[Exception], fs: FileSystem^) = +def test2(ct: CanThrow[Exception], fs: FileSystem) = def x: Int ->{ct} String = ??? def y: Logger^{fs} = ??? def p = Pair[Int ->{ct} String, Logger^{fs}](x, y) diff --git a/tests/pos-custom-args/captures/nested-classes.scala b/tests/pos-custom-args/captures/nested-classes.scala index 4a0da34faf5c..4a76a88c03ff 100644 --- a/tests/pos-custom-args/captures/nested-classes.scala +++ b/tests/pos-custom-args/captures/nested-classes.scala @@ -2,21 +2,21 @@ import language.experimental.captureChecking import language.experimental.modularity import annotation.{capability, constructorOnly} -class IO // does not work with extends caps.Capability +class IO extends caps.Capability class Blah -class Pkg(using io: IO^): +class Pkg(using io: IO): class Foo: def m(foo: Blah^{io}) = ??? -class Pkg2(using io: IO^): +class Pkg2(using io: IO): class Foo: def m(foo: Blah^{io}): Any = io; ??? -def main(using io: IO^) = +def main(using io: IO) = val pkg = Pkg() val f = pkg.Foo() - f.m(???) + val x1 = f.m(???) val pkg2 = Pkg2() val f2 = pkg2.Foo() - f2.m(???) + val x2 = f2.m(???) From 999be5ea470dda53f57ade1e19303b26ba6f4671 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 19 May 2024 10:37:22 +0200 Subject: [PATCH 09/10] Add assertions that all references in capture sets are trackable --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 5422706f7c40..697b6f707309 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -388,7 +388,7 @@ object CaptureSet: def apply(elems: CaptureRef*)(using Context): CaptureSet.Const = if elems.isEmpty then empty - else Const(SimpleIdentitySet(elems.map(_.normalizedRef)*)) + else Const(SimpleIdentitySet(elems.map(_.normalizedRef.ensuring(_.isTrackableRef))*)) def apply(elems: Refs)(using Context): CaptureSet.Const = if elems.isEmpty then empty else Const(elems) @@ -496,6 +496,7 @@ object CaptureSet: CompareResult.LevelError(this, elem) else //if id == 34 then assert(!elem.isUniversalRootCapability) + assert(elem.isTrackableRef, elem) elems += elem if elem.isRootCapability then rootAddedHandler() From 81cf8d8b5090da0609f1b4bf3cd44db881b1f926 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 26 May 2024 16:21:48 +0200 Subject: [PATCH 10/10] Make capture sets of expressions deriving Capability explicit When an expression has a type that derives from caps.Capability, add an explicit capture set. Also: Address other review comments --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 4 +-- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 6 +++- .../dotty/tools/dotc/cc/CheckCaptures.scala | 29 +++++++++++++++---- library/src/scala/caps.scala | 2 +- .../captures/effect-swaps-explicit.check | 22 +++++++------- .../captures/effect-swaps-explicit.scala | 2 -- .../captures/effect-swaps.check | 18 ++++++------ .../captures/effect-swaps.scala | 2 -- .../captures/extending-cap-classes.check | 21 ++++++++++++++ .../captures/extending-cap-classes.scala | 14 +++++++++ .../captures/usingLogFile.check | 12 ++++---- .../captures/usingLogFile.scala | 1 - .../captures/capt-capability.scala | 1 - .../captures/filevar-tracked.scala | 1 - tests/pos-custom-args/captures/i19751.scala | 1 - .../captures/logger-tracked.scala | 1 - tests/pos-custom-args/captures/logger.scala | 1 - .../captures/null-logger.scala | 1 - tests/pos-custom-args/captures/try3.scala | 1 - .../dotc/core/Definitions.scala | 1 - tests/pos/i20237.scala | 1 - 21 files changed, 92 insertions(+), 50 deletions(-) create mode 100644 tests/neg-custom-args/captures/extending-cap-classes.check create mode 100644 tests/neg-custom-args/captures/extending-cap-classes.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 9b899030bc02..8276a0987003 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -203,8 +203,8 @@ extension (tp: Type) case _ => false - /** Does type derive from caps.Capability?, which means it references of this - * type are maximal capabilities? + /** Tests whether the type derives from `caps.Capability`, which means + * references of this type are maximal capabilities. */ def derivesFromCapability(using Context): Boolean = tp.dealias match case tp: (TypeRef | AppliedType) => diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 697b6f707309..f78ed1a91bd6 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -154,7 +154,11 @@ sealed abstract class CaptureSet extends Showable: (x eq y) || x.isRootCapability || y.match - case y: TermRef => y.prefix eq x + case y: TermRef => + (y.prefix eq x) + || y.info.match + case y1: CaptureRef => x.subsumes(y1) + case _ => false case MaybeCapability(y1) => x.stripMaybe.subsumes(y1) case _ => false || x.match diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index bde97a6d0387..5daa56b9cc07 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1037,7 +1037,7 @@ class CheckCaptures extends Recheck, SymTransformer: val arrow = if covariant then "~~>" else "<~~" i"adapting $actual $arrow $expected" - def adapt(actual: Type, expected: Type, covariant: Boolean): Type = trace(adaptInfo(actual, expected, covariant), recheckr, show = true) { + def adapt(actual: Type, expected: Type, covariant: Boolean): Type = trace(adaptInfo(actual, expected, covariant), recheckr, show = true): if expected.isInstanceOf[WildcardType] then actual else // Decompose the actual type into the inner shape type, the capture set and the box status @@ -1117,7 +1117,22 @@ class CheckCaptures extends Recheck, SymTransformer: adaptedType(!boxed) else adaptedType(boxed) - } + end adapt + + /** If result derives from caps.Capability, yet is not a capturing type itself, + * make its capture set explicit. + */ + def makeCaptureSetExplicit(result: Type) = result match + case CapturingType(_, _) => result + case _ => + if result.derivesFromCapability then + val cap: CaptureRef = actual match + case ref: CaptureRef if ref.isTracked => + ref + case _ => + defn.captureRoot.termRef // TODO: skolemize? + CapturingType(result, cap.singletonCaptureSet) + else result if expected == LhsProto || expected.isSingleton && actual.isSingleton then actual @@ -1133,10 +1148,12 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => case _ => val adapted = adapt(actualw.withReachCaptures(actual), expected, covariant = true) - if adapted ne actualw then - capt.println(i"adapt boxed $actual vs $expected ===> $adapted") - adapted - else actual + makeCaptureSetExplicit: + if adapted ne actualw then + capt.println(i"adapt boxed $actual vs $expected ===> $adapted") + adapted + else + actual end adaptBoxed /** Check overrides again, taking capture sets into account. diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 215ad2cb5697..808bdba34e3f 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -4,7 +4,7 @@ import annotation.experimental @experimental object caps: - trait Capability // should be @erased + trait Capability extends Any /** The universal capture reference */ val cap: Capability = new Capability() {} diff --git a/tests/neg-custom-args/captures/effect-swaps-explicit.check b/tests/neg-custom-args/captures/effect-swaps-explicit.check index 47559ab97568..8c4d1f315fd8 100644 --- a/tests/neg-custom-args/captures/effect-swaps-explicit.check +++ b/tests/neg-custom-args/captures/effect-swaps-explicit.check @@ -1,29 +1,29 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:64:8 ------------------------- -63 | Result: -64 | Future: // error, type mismatch +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:62:8 ------------------------- +61 | Result: +62 | Future: // error, type mismatch | ^ | Found: Result.Ok[box Future[box T^?]^{fr, contextual$1}] | Required: Result[Future[T], Nothing] -65 | fr.await.ok +63 | fr.await.ok |-------------------------------------------------------------------------------------------------------------------- |Inline stack trace |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |This location contains code that was inlined from effect-swaps-explicit.scala:41 -41 | boundary(Ok(body)) + |This location contains code that was inlined from effect-swaps-explicit.scala:39 +39 | boundary(Ok(body)) | ^^^^^^^^ -------------------------------------------------------------------------------------------------------------------- | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:74:10 ------------------------ -74 | Future: fut ?=> // error: type mismatch +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:72:10 ------------------------ +72 | Future: fut ?=> // error: type mismatch | ^ | Found: Future[box T^?]^{fr, lbl} | Required: Future[box T^?]^? -75 | fr.await.ok +73 | fr.await.ok | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:68:15 --------------------------------------------- -68 | Result.make: //lbl ?=> // error, escaping label from Result +-- Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:66:15 --------------------------------------------- +66 | Result.make: //lbl ?=> // error, escaping label from Result | ^^^^^^^^^^^ |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-explicit.scala b/tests/neg-custom-args/captures/effect-swaps-explicit.scala index 814199706721..052beaab01b2 100644 --- a/tests/neg-custom-args/captures/effect-swaps-explicit.scala +++ b/tests/neg-custom-args/captures/effect-swaps-explicit.scala @@ -1,5 +1,3 @@ -import annotation.capability - object boundary: final class Label[-T] // extends caps.Capability diff --git a/tests/neg-custom-args/captures/effect-swaps.check b/tests/neg-custom-args/captures/effect-swaps.check index f16019c15513..ef5a95d333bf 100644 --- a/tests/neg-custom-args/captures/effect-swaps.check +++ b/tests/neg-custom-args/captures/effect-swaps.check @@ -1,24 +1,24 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:64:8 ---------------------------------- -63 | Result: -64 | Future: // error, type mismatch +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:62:8 ---------------------------------- +61 | Result: +62 | Future: // error, type mismatch | ^ | Found: Result.Ok[box Future[box T^?]^{fr, contextual$1}] | Required: Result[Future[T], Nothing] -65 | fr.await.ok +63 | fr.await.ok |-------------------------------------------------------------------------------------------------------------------- |Inline stack trace |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |This location contains code that was inlined from effect-swaps.scala:41 -41 | boundary(Ok(body)) + |This location contains code that was inlined from effect-swaps.scala:39 +39 | boundary(Ok(body)) | ^^^^^^^^ -------------------------------------------------------------------------------------------------------------------- | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:74:10 --------------------------------- -74 | Future: fut ?=> // error: type mismatch +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps.scala:72:10 --------------------------------- +72 | Future: fut ?=> // error: type mismatch | ^ | Found: Future[box T^?]^{fr, lbl} | Required: Future[box T^?]^? -75 | fr.await.ok +73 | fr.await.ok | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/effect-swaps.scala b/tests/neg-custom-args/captures/effect-swaps.scala index af44501371a9..d4eed2bae2f2 100644 --- a/tests/neg-custom-args/captures/effect-swaps.scala +++ b/tests/neg-custom-args/captures/effect-swaps.scala @@ -1,5 +1,3 @@ -import annotation.capability - object boundary: final class Label[-T] extends caps.Capability diff --git a/tests/neg-custom-args/captures/extending-cap-classes.check b/tests/neg-custom-args/captures/extending-cap-classes.check new file mode 100644 index 000000000000..3bdddfd9dd3c --- /dev/null +++ b/tests/neg-custom-args/captures/extending-cap-classes.check @@ -0,0 +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^ + | 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^ + | 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)^{y2} + | Required: C1 + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/extending-cap-classes.scala b/tests/neg-custom-args/captures/extending-cap-classes.scala new file mode 100644 index 000000000000..6f5a8f48c30a --- /dev/null +++ b/tests/neg-custom-args/captures/extending-cap-classes.scala @@ -0,0 +1,14 @@ +class C1 +class C2 extends C1, caps.Capability +class C3 extends C2 + +def test = + val x1: C1 = new C1 + val x2: C1 = new C2 // error + val x3: C1 = new C3 // error + + val y2: C2 = new C2 + val y3: C3 = new C3 + + val z2: C1 = y2 // error + diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index bf5c1dc4f83a..068d8be78c70 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -1,12 +1,12 @@ --- Error: tests/neg-custom-args/captures/usingLogFile.scala:23:14 ------------------------------------------------------ -23 | val later = usingLogFile { f => () => f.write(0) } // error +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:22:14 ------------------------------------------------------ +22 | val later = usingLogFile { f => () => f.write(0) } // error | ^^^^^^^^^^^^ | local reference f leaks into outer capture set of type parameter T of method usingLogFile in object Test2 --- Error: tests/neg-custom-args/captures/usingLogFile.scala:28:23 ------------------------------------------------------ -28 | private val later2 = usingLogFile { f => Cell(() => f.write(0)) } // error +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:27:23 ------------------------------------------------------ +27 | private val later2 = usingLogFile { f => Cell(() => f.write(0)) } // error | ^^^^^^^^^^^^ | local reference f leaks into outer capture set of type parameter T of method usingLogFile in object Test2 --- Error: tests/neg-custom-args/captures/usingLogFile.scala:44:16 ------------------------------------------------------ -44 | val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:43:16 ------------------------------------------------------ +43 | val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error | ^^^^^^^^^ | local reference f leaks into outer capture set of type parameter T of method usingFile in object Test3 diff --git a/tests/neg-custom-args/captures/usingLogFile.scala b/tests/neg-custom-args/captures/usingLogFile.scala index b25e4e75a784..2b46a5401f46 100644 --- a/tests/neg-custom-args/captures/usingLogFile.scala +++ b/tests/neg-custom-args/captures/usingLogFile.scala @@ -1,5 +1,4 @@ import java.io.* -import annotation.capability object Test1: diff --git a/tests/pos-custom-args/captures/capt-capability.scala b/tests/pos-custom-args/captures/capt-capability.scala index 64892218ee41..03b5cb1bbabf 100644 --- a/tests/pos-custom-args/captures/capt-capability.scala +++ b/tests/pos-custom-args/captures/capt-capability.scala @@ -1,4 +1,3 @@ -import annotation.capability import caps.Capability def f1(c: Capability): () ->{c} c.type = () => c // ok diff --git a/tests/pos-custom-args/captures/filevar-tracked.scala b/tests/pos-custom-args/captures/filevar-tracked.scala index 6fb7000ad4c2..dc8d0b18908b 100644 --- a/tests/pos-custom-args/captures/filevar-tracked.scala +++ b/tests/pos-custom-args/captures/filevar-tracked.scala @@ -1,6 +1,5 @@ import language.experimental.captureChecking import language.experimental.modularity -import annotation.capability import compiletime.uninitialized object test1: diff --git a/tests/pos-custom-args/captures/i19751.scala b/tests/pos-custom-args/captures/i19751.scala index 30bd8677f024..b41017f4f3e7 100644 --- a/tests/pos-custom-args/captures/i19751.scala +++ b/tests/pos-custom-args/captures/i19751.scala @@ -1,5 +1,4 @@ import language.experimental.captureChecking -import annotation.capability import caps.cap trait Ptr[A] diff --git a/tests/pos-custom-args/captures/logger-tracked.scala b/tests/pos-custom-args/captures/logger-tracked.scala index 1949e25b00d9..053731de444d 100644 --- a/tests/pos-custom-args/captures/logger-tracked.scala +++ b/tests/pos-custom-args/captures/logger-tracked.scala @@ -1,4 +1,3 @@ -import annotation.capability import language.experimental.saferExceptions import language.experimental.modularity diff --git a/tests/pos-custom-args/captures/logger.scala b/tests/pos-custom-args/captures/logger.scala index 04aee89a227e..81eeb521fee5 100644 --- a/tests/pos-custom-args/captures/logger.scala +++ b/tests/pos-custom-args/captures/logger.scala @@ -1,4 +1,3 @@ -import annotation.capability import language.experimental.saferExceptions import language.experimental.modularity diff --git a/tests/pos-custom-args/captures/null-logger.scala b/tests/pos-custom-args/captures/null-logger.scala index 958002ad0358..d532b5f74b38 100644 --- a/tests/pos-custom-args/captures/null-logger.scala +++ b/tests/pos-custom-args/captures/null-logger.scala @@ -1,4 +1,3 @@ -import annotation.capability import annotation.constructorOnly class FileSystem extends caps.Capability diff --git a/tests/pos-custom-args/captures/try3.scala b/tests/pos-custom-args/captures/try3.scala index 305069d3ae9f..a1a1bab8724a 100644 --- a/tests/pos-custom-args/captures/try3.scala +++ b/tests/pos-custom-args/captures/try3.scala @@ -1,5 +1,4 @@ import language.experimental.erasedDefinitions -import annotation.capability import java.io.IOException class CanThrow[-E] extends caps.Capability diff --git a/tests/pos-with-compiler-cc/dotc/core/Definitions.scala b/tests/pos-with-compiler-cc/dotc/core/Definitions.scala index 603088dd8f26..8faf208e36d0 100644 --- a/tests/pos-with-compiler-cc/dotc/core/Definitions.scala +++ b/tests/pos-with-compiler-cc/dotc/core/Definitions.scala @@ -985,7 +985,6 @@ class Definitions { @tu lazy val BeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BeanProperty") @tu lazy val BooleanBeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BooleanBeanProperty") @tu lazy val BodyAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Body") - @tu lazy val CapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.capability") @tu lazy val ChildAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Child") @tu lazy val ContextResultCountAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ContextResultCount") @tu lazy val ProvisionalSuperClassAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ProvisionalSuperClass") diff --git a/tests/pos/i20237.scala b/tests/pos/i20237.scala index 0a5eb6d9a332..973f5d2025e3 100644 --- a/tests/pos/i20237.scala +++ b/tests/pos/i20237.scala @@ -1,5 +1,4 @@ import language.experimental.captureChecking -import scala.annotation.capability class Cap extends caps.Capability: def use[T](body: Cap ?=> T) = body(using this)