From ecf3428926d039b314dea548d68e91a7a74eb26a Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 10 Jan 2025 17:25:42 +0100 Subject: [PATCH 1/8] Fix various issues with maximal capabilities The subsumes check mistakenly allowed any capability to subsume `cap`, since `cap` is expanded as `caps.cap`, and by the path subcapturing rule `caps.cap <: caps`, where the capture set of `caps` is empty. This allowed quite a few hidden errors to go through. This commit fixes the subcapturing issue and all downstream issues caused by that fix. In particular: - Don't use path comparison for `x subsumes caps.cap` - Don't allow an opened existential on the left of a comparison to leak into a capture set on the right. This would give a "leak" error later in healCaptures. - Print pre-cc annotated capturing types with @retains annotations with `^`. The annotation is already rendered as a set in this case, but the `^` was missing. - Don't recheck `_` right hand sides of uninitialized variables. These were handled in ways that broke freshness checking. The new `uninitialied` scheme does not have this problem. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 14 +++++++ .../src/dotty/tools/dotc/cc/CaptureRef.scala | 4 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 14 ++++--- compiler/src/dotty/tools/dotc/cc/Setup.scala | 14 ++++--- .../dotty/tools/dotc/core/TypeComparer.scala | 6 +++ .../tools/dotc/printing/PlainPrinter.scala | 7 +++- .../dotty/tools/dotc/transform/Recheck.scala | 5 ++- .../captures/box-adapt-cases.check | 14 +++++++ .../captures/boxmap-paper.scala | 14 ++++++- tests/pos-custom-args/captures/cc-cast.scala | 12 ++++++ .../captures/ex-fun-aliases.scala.disabled} | 4 ++ .../captures/i20237-explicit.scala | 15 ++++++++ .../captures}/i20237.scala | 0 .../captures/open-existential.scala | 15 ++++++++ tests/pos/boxmap-paper.scala | 38 ------------------- 15 files changed, 120 insertions(+), 56 deletions(-) create mode 100644 tests/neg-custom-args/captures/box-adapt-cases.check create mode 100644 tests/pos-custom-args/captures/cc-cast.scala rename tests/{pos/cc-ex-unpack.scala => pos-custom-args/captures/ex-fun-aliases.scala.disabled} (79%) create mode 100644 tests/pos-custom-args/captures/i20237-explicit.scala rename tests/{pos => pos-custom-args/captures}/i20237.scala (100%) create mode 100644 tests/pos-custom-args/captures/open-existential.scala delete mode 100644 tests/pos/boxmap-paper.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index bc4eb92234eb..62a9fe33fc5c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -46,6 +46,7 @@ object ccConfig: */ def useSealed(using Context) = Feature.sourceVersion.stable != SourceVersion.`3.5` + end ccConfig @@ -629,6 +630,19 @@ class CleanupRetains(using Context) extends TypeMap: RetainingType(tp, Nil, byName = annot.symbol == defn.RetainsByNameAnnot) case _ => mapOver(tp) +/** A typemap that follows aliases and keeps their transformed results if + * there is a change. + */ +trait FollowAliasesMap(using Context) extends TypeMap: + var follow = true // Used for debugging so that we can compare results with and w/o following. + def mapFollowingAliases(t: Type): Type = + val t1 = t.dealiasKeepAnnots + if follow && (t1 ne t) then + val t2 = apply(t1) + if t2 ne t1 then t2 + else t + else mapOver(t) + /** An extractor for `caps.reachCapability(ref)`, which is used to express a reach * capability as a tree in a @retains annotation. */ diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 9bda9102cbb8..e5beb56c6c56 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -83,7 +83,7 @@ trait CaptureRef extends TypeProxy, ValueType: else myCaptureSet = CaptureSet.Pending val computed = CaptureSet.ofInfo(this) - if !isCaptureChecking || underlying.isProvisional then + if !isCaptureChecking || ctx.mode.is(Mode.IgnoreCaptures) || underlying.isProvisional then myCaptureSet = null else myCaptureSet = computed @@ -124,7 +124,7 @@ trait CaptureRef extends TypeProxy, ValueType: (this eq y) || this.isRootCapability || y.match - case y: TermRef => + case y: TermRef if !y.isRootCapability => y.prefix.match case ypre: CaptureRef => this.subsumes(ypre) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 1750e98f708a..7f4a34bab1f9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -508,8 +508,13 @@ object CaptureSet: res.addToTrace(this) private def levelOK(elem: CaptureRef)(using Context): Boolean = - if elem.isRootCapability || Existential.isExistentialVar(elem) then + if elem.isRootCapability then !noUniversal + else if Existential.isExistentialVar(elem) then + !noUniversal + && !TypeComparer.isOpenedExistential(elem) + // Opened existentials on the left cannot be added to nested capture sets on the right + // of a comparison. Test case is open-existential.scala. else elem match case elem: TermRef if level.isDefined => elem.prefix match @@ -1065,13 +1070,12 @@ object CaptureSet: /** The capture set of the type underlying CaptureRef */ def ofInfo(ref: CaptureRef)(using Context): CaptureSet = ref match - case ref: (TermRef | TermParamRef) if ref.isMaxCapability => - if ref.isTrackableRef then ref.singletonCaptureSet - else CaptureSet.universal case ReachCapability(ref1) => ref1.widen.deepCaptureSet(includeTypevars = true) .showing(i"Deep capture set of $ref: ${ref1.widen} = ${result}", capt) - case _ => ofType(ref.underlying, followResult = true) + case _ => + if ref.isMaxCapability then ref.singletonCaptureSet + else ofType(ref.underlying, followResult = true) /** Capture set of a type */ def ofType(tp: Type, followResult: Boolean)(using Context): CaptureSet = diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index ebe128d7776c..7cc7e0514599 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -13,7 +13,7 @@ import ast.tpd, tpd.* import transform.{PreRecheck, Recheck}, Recheck.* import CaptureSet.{IdentityCaptRefMap, IdempotentCaptRefMap} import Synthetics.isExcluded -import util.{Property, SimpleIdentitySet} +import util.SimpleIdentitySet import reporting.Message import printing.{Printer, Texts}, Texts.{Text, Str} import collection.mutable @@ -40,7 +40,7 @@ trait SetupAPI: object Setup: - val name: String = "ccSetup" + val name: String = "setupCC" val description: String = "prepare compilation unit for capture checking" /** Recognizer for `res $throws exc`, returning `(res, exc)` in case of success */ @@ -192,11 +192,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * 3. Refine other class types C by adding capture set variables to their parameter getters * (see addCaptureRefinements), provided `refine` is true. * 4. Add capture set variables to all types that can be tracked + * 5. Perform normalizeCaptures * * Polytype bounds are only cleaned using step 1, but not otherwise transformed. */ private def transformInferredType(tp: Type)(using Context): Type = - def mapInferred(refine: Boolean): TypeMap = new TypeMap: + def mapInferred(refine: Boolean): TypeMap = new TypeMap with FollowAliasesMap: override def toString = "map inferred" /** Refine a possibly applied class type C where the class has tracked parameters @@ -299,9 +300,10 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * 3. Add universal capture sets to types deriving from Capability * 4. Map `cap` in function result types to existentially bound variables. * 5. Schedule deferred well-formed tests for types with retains annotations. + * 6. Perform normalizeCaptures */ private def transformExplicitType(tp: Type, tptToCheck: Tree = EmptyTree)(using Context): Type = - val toCapturing = new DeepTypeMap: + val toCapturing = new DeepTypeMap with FollowAliasesMap: override def toString = "expand aliases" /** Expand $throws aliases. This is hard-coded here since $throws aliases in stdlib @@ -337,7 +339,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tp @ CapturingType(parent, refs) if (refs eq defn.universalCSImpliedByCapability) && !tp.isBoxedCapturing => parent - case tp @ CapturingType(parent, refs) => tp case _ => tp def apply(t: Type) = @@ -819,7 +820,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed) case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) - case tp @ AppliedType(tycon, args) if !defn.isFunctionClass(tp.dealias.typeSymbol) => + case tp @ AppliedType(tycon, args) + if !defn.isFunctionClass(tp.dealias.typeSymbol) => tp.derivedAppliedType(tycon, args.mapConserve(box)) case tp: RealTypeBounds => tp.derivedTypeBounds(tp.lo, box(tp.hi)) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 8414c3795f49..e9143ae88741 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2845,6 +2845,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false Existential.isExistentialVar(tp1) && canInstantiateWith(assocExistentials) + def isOpenedExistential(ref: CaptureRef)(using Context): Boolean = + openedExistentials.contains(ref) + /** bi-map taking existentials to the left of a comparison to matching * existentials on the right. This is not a bijection. However * we have `forwards(backwards(bv)) == bv` for an existentially bound `bv`. @@ -3476,6 +3479,9 @@ object TypeComparer { def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context) = comparing(_.subsumesExistentially(tp1, tp2)) + + def isOpenedExistential(ref: CaptureRef)(using Context) = + comparing(_.isOpenedExistential(ref)) } object MatchReducer: diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index cac82eb0c4bd..e90aeb217362 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -15,7 +15,7 @@ import util.SourcePosition import scala.util.control.NonFatal import scala.annotation.switch import config.{Config, Feature} -import cc.{CapturingType, RetainingType, CaptureSet, ReachCapability, MaybeCapability, isBoxed, retainedElems, isRetainsLike} +import cc.* class PlainPrinter(_ctx: Context) extends Printer { @@ -297,7 +297,10 @@ class PlainPrinter(_ctx: Context) extends Printer { else if (annot.symbol == defn.IntoAnnot || annot.symbol == defn.IntoParamAnnot) && !printDebug then atPrec(GlobalPrec)( Str("into ") ~ toText(tpe) ) - else toTextLocal(tpe) ~ " " ~ toText(annot) + else if annot.isInstanceOf[CaptureAnnotation] then + toTextLocal(tpe) ~ "^" ~ toText(annot) + else + toTextLocal(tpe) ~ " " ~ toText(annot) case FlexibleType(_, tpe) => "(" ~ toText(tpe) ~ ")?" case tp: TypeVar => diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 172ae337d6e6..8936c460de81 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -255,7 +255,10 @@ abstract class Recheck extends Phase, SymTransformer: def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type = val resType = recheck(tree.tpt) - if tree.rhs.isEmpty then resType + def isUninitWildcard = tree.rhs match + case Ident(nme.WILDCARD) => tree.symbol.is(Mutable) + case _ => false + if tree.rhs.isEmpty || isUninitWildcard then resType else recheck(tree.rhs, resType) def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Type = diff --git a/tests/neg-custom-args/captures/box-adapt-cases.check b/tests/neg-custom-args/captures/box-adapt-cases.check new file mode 100644 index 000000000000..8dc088c6f713 --- /dev/null +++ b/tests/neg-custom-args/captures/box-adapt-cases.check @@ -0,0 +1,14 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:14:4 ------------------------------- +14 | x(cap => cap.use()) // error + | ^^^^^^^^^^^^^^^^ + | Found: (cap: box Cap^?) ->{io} Int + | Required: (cap: box Cap^{io}) -> Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:28:4 ------------------------------- +28 | x(cap => cap.use()) // error + | ^^^^^^^^^^^^^^^^ + | Found: (cap: box Cap^?) ->{io, fs} Int + | Required: (cap: box Cap^{io, fs}) ->{io} Int + | + | longer explanation available when compiling with `-explain` diff --git a/tests/pos-custom-args/captures/boxmap-paper.scala b/tests/pos-custom-args/captures/boxmap-paper.scala index 9d5bb49af25d..20282d5813f9 100644 --- a/tests/pos-custom-args/captures/boxmap-paper.scala +++ b/tests/pos-custom-args/captures/boxmap-paper.scala @@ -1,7 +1,13 @@ -type Cell[+T] = [K] -> (T => K) -> K +type Cell_orig[+T] = [K] -> (T => K) -> K -def cell[T](x: T): Cell[T] = +def cell_orig[T](x: T): Cell_orig[T] = + [K] => (k: T => K) => k(x) + +class Cell[+T](val value: [K] -> (T => K) -> K): + def apply[K]: (T => K) -> K = value[K] + +def cell[T](x: T): Cell[T] = Cell: [K] => (k: T => K) => k(x) def get[T](c: Cell[T]): T = c[T](identity) @@ -22,6 +28,10 @@ def test(io: IO^) = val loggedOne: () ->{io} Int = () => { io.print("1"); 1 } + // We have a leakage of io because type arguments to alias type `Cell` are not boxed. + val c_orig: Cell[() ->{io} Int]^{io} + = cell[() ->{io} Int](loggedOne) + val c: Cell[() ->{io} Int] = cell[() ->{io} Int](loggedOne) diff --git a/tests/pos-custom-args/captures/cc-cast.scala b/tests/pos-custom-args/captures/cc-cast.scala new file mode 100644 index 000000000000..cfd96d63bee7 --- /dev/null +++ b/tests/pos-custom-args/captures/cc-cast.scala @@ -0,0 +1,12 @@ +import annotation.unchecked.uncheckedCaptures +import compiletime.uninitialized + +def foo(x: Int => Int) = () + + +object Test: + def test(x: Object) = + foo(x.asInstanceOf[Int => Int]) + + @uncheckedCaptures var x1: Object^ = uninitialized + @uncheckedCaptures var x2: Object^ = _ diff --git a/tests/pos/cc-ex-unpack.scala b/tests/pos-custom-args/captures/ex-fun-aliases.scala.disabled similarity index 79% rename from tests/pos/cc-ex-unpack.scala rename to tests/pos-custom-args/captures/ex-fun-aliases.scala.disabled index ae9b4ea5d805..ff86927b874c 100644 --- a/tests/pos/cc-ex-unpack.scala +++ b/tests/pos-custom-args/captures/ex-fun-aliases.scala.disabled @@ -11,8 +11,12 @@ type EX3 = () -> (c: Exists) -> () -> C^{c} type EX4 = () -> () -> (c: Exists) -> C^{c} +type FUN1 = (c: C^) -> (C^{c}, C^{c}) + def Test = def f = val ex1: EX1 = ??? val c1 = ex1 + val fun1: FUN1 = c => (c, c) + val fun2 = fun1 c1 diff --git a/tests/pos-custom-args/captures/i20237-explicit.scala b/tests/pos-custom-args/captures/i20237-explicit.scala new file mode 100644 index 000000000000..0999d4acd50e --- /dev/null +++ b/tests/pos-custom-args/captures/i20237-explicit.scala @@ -0,0 +1,15 @@ +import language.experimental.captureChecking + +class Cap extends caps.Capability: + def use[T](body: Cap => T) = body(this) + +class Box[T](body: Cap => T): + def open(cap: Cap) = cap.use(body) + +object Box: + def make[T](body: Cap => T)(cap: Cap): Box[T]^{body} = Box(x => body(x)) + +def main = + val givenCap: Cap = new Cap + val xx: Cap => Int = y => 1 + val box = Box.make[Int](xx)(givenCap).open \ No newline at end of file diff --git a/tests/pos/i20237.scala b/tests/pos-custom-args/captures/i20237.scala similarity index 100% rename from tests/pos/i20237.scala rename to tests/pos-custom-args/captures/i20237.scala diff --git a/tests/pos-custom-args/captures/open-existential.scala b/tests/pos-custom-args/captures/open-existential.scala new file mode 100644 index 000000000000..8b43f27a051c --- /dev/null +++ b/tests/pos-custom-args/captures/open-existential.scala @@ -0,0 +1,15 @@ +trait Async extends caps.Capability + +class Future[+T](x: () => T)(using val a: Async) + +class Collector[T](val futs: Seq[Future[T]^]): + def add(fut: Future[T]^{futs*}) = ??? + +def main() = + given async: Async = ??? + val futs = (1 to 20).map(x => Future(() => x)) + val col = Collector(futs) + val col1: Collector[Int] { val futs: Seq[Future[Int]^{async}] } + = Collector(futs) + + diff --git a/tests/pos/boxmap-paper.scala b/tests/pos/boxmap-paper.scala deleted file mode 100644 index aa983114ed8a..000000000000 --- a/tests/pos/boxmap-paper.scala +++ /dev/null @@ -1,38 +0,0 @@ -import language.experimental.captureChecking - -type Cell[+T] = [K] -> (T => K) -> K - -def cell[T](x: T): Cell[T] = - [K] => (k: T => K) => k(x) - -def get[T](c: Cell[T]): T = c[T](identity) - -def map[A, B](c: Cell[A])(f: A => B): Cell[B] - = c[Cell[B]]((x: A) => cell(f(x))) - -def pureMap[A, B](c: Cell[A])(f: A -> B): Cell[B] - = c[Cell[B]]((x: A) => cell(f(x))) - -def lazyMap[A, B](c: Cell[A])(f: A => B): () ->{f} Cell[B] - = () => c[Cell[B]]((x: A) => cell(f(x))) - -trait IO: - def print(s: String): Unit - -def test(io: IO^) = - - val loggedOne: () ->{io} Int = () => { io.print("1"); 1 } - - val c: Cell[() ->{io} Int] - = cell[() ->{io} Int](loggedOne) - - val g = (f: () ->{io} Int) => - val x = f(); io.print(" + ") - val y = f(); io.print(s" = ${x + y}") - - val r = lazyMap[() ->{io} Int, Unit](c)(f => g(f)) - val r2 = lazyMap[() ->{io} Int, Unit](c)(g) - val r3 = lazyMap(c)(g) - val _ = r() - val _ = r2() - val _ = r3() From ec86d5e74dead0891c2ce92897a5b31b4e65749b Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 10 Jan 2025 18:09:17 +0100 Subject: [PATCH 2/8] Expand aliases when mapping explicit types in Setup This is necessary because the compiler is free in previous phases to dealias or not. Therefore, capture checking should not depend on aliasing. The main difference is that now arguments to type aliases are not necessarily boxed. They are boxed only if they need boxing in the dealiased type. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 1 - compiler/src/dotty/tools/dotc/cc/Setup.scala | 6 ++--- .../captures/boundschecks2.scala | 2 +- .../captures/boundschecks3.check | 4 ++++ .../captures/boundschecks3.scala | 13 ++++++++++ .../captures/box-adapt-cases.check | 20 ++++++++-------- .../captures/box-adapt-cases.scala | 16 ++++++------- .../captures/box-adapt-cov.scala | 10 ++++---- .../captures/box-adapt-depfun.scala | 12 +++++----- .../captures/box-adapt-typefun.scala | 8 +++---- tests/neg-custom-args/captures/capt1.check | 12 +++++----- .../captures/cc-ex-conformance.scala | 2 +- .../captures/existential-mapping.check | 24 +++++++++---------- tests/neg-custom-args/captures/i15922.scala | 4 ++-- tests/neg-custom-args/captures/i16725.scala | 8 +++---- tests/neg-custom-args/captures/i19330.check | 5 ++++ tests/neg-custom-args/captures/i21401.check | 8 +++---- .../neg-custom-args/captures/outer-var.check | 10 ++++---- tests/neg-custom-args/captures/try.check | 4 ++-- tests/neg-custom-args/captures/vars.check | 6 ++--- .../captures/i15749a.scala | 10 ++++---- 21 files changed, 102 insertions(+), 83 deletions(-) create mode 100644 tests/neg-custom-args/captures/boundschecks3.check create mode 100644 tests/neg-custom-args/captures/boundschecks3.scala create mode 100644 tests/neg-custom-args/captures/i19330.check rename tests/{neg-custom-args => pos-custom-args}/captures/i15749a.scala (51%) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 62a9fe33fc5c..92cd40a65d5a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -49,7 +49,6 @@ object ccConfig: end ccConfig - /** Are we at checkCaptures phase? */ def isCaptureChecking(using Context): Boolean = ctx.phaseId == Phases.checkCapturesPhase.id diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 7cc7e0514599..e28aeb8e0313 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -278,7 +278,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: paramInfos = tp.paramInfos.mapConserve(_.dropAllRetains.bounds), resType = this(tp.resType)) case _ => - mapOver(tp) + mapFollowingAliases(tp) addVar(addCaptureRefinements(normalizeCaptures(tp1)), ctx.owner) end apply end mapInferred @@ -364,7 +364,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // Map references to capability classes C to C^ if t.derivesFromCapability && !t.isSingleton && t.typeSymbol != defn.Caps_Exists then CapturingType(t, defn.universalCSImpliedByCapability, boxed = false) - else normalizeCaptures(mapOver(t)) + else normalizeCaptures(mapFollowingAliases(t)) end toCapturing def fail(msg: Message) = @@ -821,7 +821,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) case tp @ AppliedType(tycon, args) - if !defn.isFunctionClass(tp.dealias.typeSymbol) => + if !defn.isFunctionClass(tp.dealias.typeSymbol) && (tp.dealias eq tp) => tp.derivedAppliedType(tycon, args.mapConserve(box)) case tp: RealTypeBounds => tp.derivedTypeBounds(tp.lo, box(tp.hi)) diff --git a/tests/neg-custom-args/captures/boundschecks2.scala b/tests/neg-custom-args/captures/boundschecks2.scala index 923758d722f9..99366a8e7aff 100644 --- a/tests/neg-custom-args/captures/boundschecks2.scala +++ b/tests/neg-custom-args/captures/boundschecks2.scala @@ -8,6 +8,6 @@ object test { val foo: C[Tree^] = ??? // error type T = C[Tree^] // error - val bar: T -> T = ??? + //val bar: T -> T = ??? // --> boundschecks3.scala for what happens if we uncomment val baz: C[Tree^] -> Unit = ??? // error } diff --git a/tests/neg-custom-args/captures/boundschecks3.check b/tests/neg-custom-args/captures/boundschecks3.check new file mode 100644 index 000000000000..36e1336e8f05 --- /dev/null +++ b/tests/neg-custom-args/captures/boundschecks3.check @@ -0,0 +1,4 @@ +-- Error: tests/neg-custom-args/captures/boundschecks3.scala:11:13 ----------------------------------------------------- +11 | val bar: T -> T = ??? // error, since `T` is expanded here. But the msg is not very good. + | ^^^^^^ + | test.C[box test.Tree^] captures the root capability `cap` in invariant position diff --git a/tests/neg-custom-args/captures/boundschecks3.scala b/tests/neg-custom-args/captures/boundschecks3.scala new file mode 100644 index 000000000000..f5e9652c0913 --- /dev/null +++ b/tests/neg-custom-args/captures/boundschecks3.scala @@ -0,0 +1,13 @@ +object test { + + class Tree + + def f[X <: Tree](x: X): Unit = () + + class C[X <: Tree](x: X) + + val foo: C[Tree^] = ??? // hidden error + type T = C[Tree^] // hidden error + val bar: T -> T = ??? // error, since `T` is expanded here. But the msg is not very good. + val baz: C[Tree^] -> Unit = ??? // hidden error +} diff --git a/tests/neg-custom-args/captures/box-adapt-cases.check b/tests/neg-custom-args/captures/box-adapt-cases.check index 8dc088c6f713..7ff185c499a5 100644 --- a/tests/neg-custom-args/captures/box-adapt-cases.check +++ b/tests/neg-custom-args/captures/box-adapt-cases.check @@ -1,14 +1,14 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:14:4 ------------------------------- -14 | x(cap => cap.use()) // error - | ^^^^^^^^^^^^^^^^ - | Found: (cap: box Cap^?) ->{io} Int - | Required: (cap: box Cap^{io}) -> Int +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:14:10 ------------------------------ +14 | x.value(cap => cap.use()) // error + | ^^^^^^^^^^^^^^^^ + | Found: (cap: box Cap^?) ->{io} Int + | Required: (cap: box Cap^{io}) -> Int | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:28:4 ------------------------------- -28 | x(cap => cap.use()) // error - | ^^^^^^^^^^^^^^^^ - | Found: (cap: box Cap^?) ->{io, fs} Int - | Required: (cap: box Cap^{io, fs}) ->{io} Int +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:28:10 ------------------------------ +28 | x.value(cap => cap.use()) // error + | ^^^^^^^^^^^^^^^^ + | Found: (cap: box Cap^?) ->{io, fs} Int + | Required: (cap: box Cap^{io, fs}) ->{io} Int | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/box-adapt-cases.scala b/tests/neg-custom-args/captures/box-adapt-cases.scala index d9ec0f80a548..8f7d7a0a6667 100644 --- a/tests/neg-custom-args/captures/box-adapt-cases.scala +++ b/tests/neg-custom-args/captures/box-adapt-cases.scala @@ -1,29 +1,29 @@ trait Cap { def use(): Int } def test1(): Unit = { - type Id[X] = [T] -> (op: X => T) -> T + class Id[X](val value: [T] -> (op: X => T) -> T) val x: Id[Cap^] = ??? - x(cap => cap.use()) + x.value(cap => cap.use()) } def test2(io: Cap^): Unit = { - type Id[X] = [T] -> (op: X -> T) -> T + class Id[X](val value: [T] -> (op: X -> T) -> T) val x: Id[Cap^{io}] = ??? - x(cap => cap.use()) // error + x.value(cap => cap.use()) // error } def test3(io: Cap^): Unit = { - type Id[X] = [T] -> (op: X ->{io} T) -> T + class Id[X](val value: [T] -> (op: X ->{io} T) -> T) val x: Id[Cap^{io}] = ??? - x(cap => cap.use()) // ok + x.value(cap => cap.use()) // ok } def test4(io: Cap^, fs: Cap^): Unit = { - type Id[X] = [T] -> (op: X ->{io} T) -> T + class Id[X](val value: [T] -> (op: X ->{io} T) -> T) val x: Id[Cap^{io, fs}] = ??? - x(cap => cap.use()) // error + x.value(cap => cap.use()) // error } diff --git a/tests/neg-custom-args/captures/box-adapt-cov.scala b/tests/neg-custom-args/captures/box-adapt-cov.scala index 2c1f15a5c77f..e1e6dd4cec1a 100644 --- a/tests/neg-custom-args/captures/box-adapt-cov.scala +++ b/tests/neg-custom-args/captures/box-adapt-cov.scala @@ -1,14 +1,14 @@ trait Cap def test1(io: Cap^) = { - type Op[X] = [T] -> Unit -> X + class Op[+X](val value: [T] -> Unit -> X) val f: Op[Cap^{io}] = ??? - val x: [T] -> Unit -> Cap^{io} = f // error + val x: [T] -> Unit -> Cap^{io} = f.value // error } def test2(io: Cap^) = { - type Op[X] = [T] -> Unit -> X^{io} + class Op[+X](val value: [T] -> Unit -> X^{io}) val f: Op[Cap^{io}] = ??? - val x: Unit -> Cap^{io} = f[Unit] // error - val x1: Unit ->{io} Cap^{io} = f[Unit] // ok + val x: Unit -> Cap^{io} = f.value[Unit] // error + val x1: Unit ->{io} Cap^{io} = f.value[Unit] // ok } diff --git a/tests/neg-custom-args/captures/box-adapt-depfun.scala b/tests/neg-custom-args/captures/box-adapt-depfun.scala index d1c1c73f8207..f673c657f753 100644 --- a/tests/neg-custom-args/captures/box-adapt-depfun.scala +++ b/tests/neg-custom-args/captures/box-adapt-depfun.scala @@ -1,23 +1,23 @@ trait Cap { def use(): Int } def test1(io: Cap^): Unit = { - type Id[X] = [T] -> (op: X ->{io} T) -> T + class Id[X](val value: [T] -> (op: X ->{io} T) -> T) val x: Id[Cap]^{io} = ??? - x(cap => cap.use()) // ok + x.value(cap => cap.use()) // ok } def test2(io: Cap^): Unit = { - type Id[X] = [T] -> (op: (x: X) ->{io} T) -> T + class Id[X](val value: [T] -> (op: (x: X) ->{io} T) -> T) val x: Id[Cap^{io}] = ??? - x(cap => cap.use()) + x.value(cap => cap.use()) // should work when the expected type is a dependent function } def test3(io: Cap^): Unit = { - type Id[X] = [T] -> (op: (x: X) ->{} T) -> T + class Id[X](val value: [T] -> (op: (x: X) ->{} T) -> T) val x: Id[Cap^{io}] = ??? - x(cap => cap.use()) // error + x.value(cap => cap.use()) // error } diff --git a/tests/neg-custom-args/captures/box-adapt-typefun.scala b/tests/neg-custom-args/captures/box-adapt-typefun.scala index 175acdda1c8f..76da047f42a9 100644 --- a/tests/neg-custom-args/captures/box-adapt-typefun.scala +++ b/tests/neg-custom-args/captures/box-adapt-typefun.scala @@ -1,13 +1,13 @@ trait Cap { def use(): Int } def test1(io: Cap^): Unit = { - type Op[X] = [T] -> X -> Unit + class Op[X](val value: [T] -> X -> Unit) val f: [T] -> (Cap^{io}) -> Unit = ??? - val op: Op[Cap^{io}] = f // error + val op: Op[Cap^{io}] = Op(f) // was error, now ok } def test2(io: Cap^): Unit = { - type Lazy[X] = [T] -> Unit -> X + class Lazy[X](val value: [T] -> Unit -> X) val f: Lazy[Cap^{io}] = ??? - val test: [T] -> Unit -> (Cap^{io}) = f // error + val test: [T] -> Unit -> (Cap^{io}) = f.value // error } diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index f63c55ca48c4..acf8faa7a969 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -36,15 +36,15 @@ -- Error: tests/neg-custom-args/captures/capt1.scala:34:16 ------------------------------------------------------------- 34 | val z2 = h[() -> Cap](() => x) // error // error | ^^^^^^^^^ - | Type variable X of method h cannot be instantiated to () -> box C^ since - | the part box C^ of that type captures the root capability `cap`. + | Type variable X of method h cannot be instantiated to () -> (ex$15: caps.Exists) -> C^{ex$15} since + | the part C^{ex$15} of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/capt1.scala:34:30 ------------------------------------------------------------- 34 | val z2 = h[() -> Cap](() => x) // error // error | ^ - | reference (x : C^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> box C^ + | reference (x : C^) is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> (ex$15: caps.Exists) -> C^{ex$15} -- Error: tests/neg-custom-args/captures/capt1.scala:36:13 ------------------------------------------------------------- 36 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error | ^^^^^^^^^^^^^^^^^^^^^^^ - | Type variable X of method h cannot be instantiated to box () ->{x} Cap since - | the part Cap of that type captures the root capability `cap`. + | Type variable X of method h cannot be instantiated to box () ->{x} (ex$20: caps.Exists) -> C^{ex$20} since + | the part C^{ex$20} of that type captures the root capability `cap`. diff --git a/tests/neg-custom-args/captures/cc-ex-conformance.scala b/tests/neg-custom-args/captures/cc-ex-conformance.scala index a953466daa9a..16e13376c5b3 100644 --- a/tests/neg-custom-args/captures/cc-ex-conformance.scala +++ b/tests/neg-custom-args/captures/cc-ex-conformance.scala @@ -21,5 +21,5 @@ def Test = val ex3: EX3 = ??? val ex4: EX4 = ??? val _: EX4 = ex3 // ok - val _: EX4 = ex4 + val _: EX4 = ex4 // error (???) Probably since we also introduce existentials on expansion val _: EX3 = ex4 // error diff --git a/tests/neg-custom-args/captures/existential-mapping.check b/tests/neg-custom-args/captures/existential-mapping.check index cd71337868e1..30836bc427cf 100644 --- a/tests/neg-custom-args/captures/existential-mapping.check +++ b/tests/neg-custom-args/captures/existential-mapping.check @@ -33,56 +33,56 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:21:30 -------------------------- 21 | val _: A^ -> (x: C^) -> C = x5 // error | ^^ - | Found: (x5 : A^ -> (ex$27: caps.Exists) -> Fun[C^{ex$27}]) + | Found: (x5 : A^ -> (x: C^) -> (ex$27: caps.Exists) -> C^{ex$27}) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:24:30 -------------------------- 24 | val _: A^ -> (x: C^) => C = x6 // error | ^^ - | Found: (x6 : A^ -> (ex$33: caps.Exists) -> IFun[C^{ex$33}]) - | Required: A^ -> (ex$36: caps.Exists) -> (x: C^) ->{ex$36} C + | Found: (x6 : A^ -> (ex$36: caps.Exists) -> (x: C^) ->{ex$36} (ex$35: caps.Exists) -> C^{ex$35}) + | Required: A^ -> (ex$39: caps.Exists) -> (x: C^) ->{ex$39} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:27:25 -------------------------- 27 | val _: (x: C^) => C = y1 // error | ^^ - | Found: (y1 : (x: C^) => (ex$38: caps.Exists) -> C^{ex$38}) + | Found: (y1 : (x: C^) => (ex$41: caps.Exists) -> C^{ex$41}) | Required: (x: C^) => C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:30:20 -------------------------- 30 | val _: C^ => C = y2 // error | ^^ - | Found: (y2 : C^ => (ex$42: caps.Exists) -> C^{ex$42}) + | Found: (y2 : C^ => (ex$45: caps.Exists) -> C^{ex$45}) | Required: C^ => C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:33:30 -------------------------- 33 | val _: A^ => (x: C^) => C = y3 // error | ^^ - | Found: (y3 : A^ => (ex$47: caps.Exists) -> (x: C^) ->{ex$47} (ex$46: caps.Exists) -> C^{ex$46}) - | Required: A^ => (ex$50: caps.Exists) -> (x: C^) ->{ex$50} C + | Found: (y3 : A^ => (ex$50: caps.Exists) -> (x: C^) ->{ex$50} (ex$49: caps.Exists) -> C^{ex$49}) + | Required: A^ => (ex$53: caps.Exists) -> (x: C^) ->{ex$53} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:36:25 -------------------------- 36 | val _: A^ => C^ => C = y4 // error | ^^ - | Found: (y4 : A^ => (ex$53: caps.Exists) -> C^ ->{ex$53} (ex$52: caps.Exists) -> C^{ex$52}) - | Required: A^ => (ex$56: caps.Exists) -> C^ ->{ex$56} C + | Found: (y4 : A^ => (ex$56: caps.Exists) -> C^ ->{ex$56} (ex$55: caps.Exists) -> C^{ex$55}) + | Required: A^ => (ex$59: caps.Exists) -> C^ ->{ex$59} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:39:30 -------------------------- 39 | val _: A^ => (x: C^) -> C = y5 // error | ^^ - | Found: (y5 : A^ => (ex$58: caps.Exists) -> Fun[C^{ex$58}]) + | Found: (y5 : A^ => (x: C^) -> (ex$61: caps.Exists) -> C^{ex$61}) | Required: A^ => (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:42:30 -------------------------- 42 | val _: A^ => (x: C^) => C = y6 // error | ^^ - | Found: (y6 : A^ => (ex$64: caps.Exists) -> IFun[C^{ex$64}]) - | Required: A^ => (ex$67: caps.Exists) -> (x: C^) ->{ex$67} C + | Found: (y6 : A^ => (ex$70: caps.Exists) -> (x: C^) ->{ex$70} (ex$69: caps.Exists) -> C^{ex$69}) + | Required: A^ => (ex$73: caps.Exists) -> (x: C^) ->{ex$73} C | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i15922.scala b/tests/neg-custom-args/captures/i15922.scala index 848a22fe5341..b8bcc346ef81 100644 --- a/tests/neg-custom-args/captures/i15922.scala +++ b/tests/neg-custom-args/captures/i15922.scala @@ -1,8 +1,8 @@ trait Cap { def use(): Int } -type Id[X] = [T] -> (op: X => T) -> T -def mkId[X](x: X): Id[X] = [T] => (op: X => T) => op(x) +class Id[+X](val value: [T] -> (op: X => T) -> T) +def mkId[X](x: X): Id[X] = Id([T] => (op: X => T) => op(x)) def withCap[X](op: (Cap^) => X): X = { val cap: Cap^ = new Cap { def use() = { println("cap is used"); 0 } } diff --git a/tests/neg-custom-args/captures/i16725.scala b/tests/neg-custom-args/captures/i16725.scala index 1accf197c626..96cf44e72f3c 100644 --- a/tests/neg-custom-args/captures/i16725.scala +++ b/tests/neg-custom-args/captures/i16725.scala @@ -3,12 +3,12 @@ class IO extends caps.Capability: def brewCoffee(): Unit = ??? def usingIO[T](op: IO => T): T = ??? -type Wrapper[T] = [R] -> (f: T => R) -> R -def mk[T](x: T): Wrapper[T] = [R] => f => f(x) +class Wrapper[T](val value: [R] -> (f: T => R) -> R) +def mk[T](x: T): Wrapper[T] = Wrapper([R] => f => f(x)) def useWrappedIO(wrapper: Wrapper[IO]): () -> Unit = () => - wrapper: io => // error + wrapper.value: io => // error io.brewCoffee() def main(): Unit = - val escaped = usingIO(io => useWrappedIO(mk(io))) + val escaped = usingIO(io => useWrappedIO(mk(io))) // error escaped() // boom diff --git a/tests/neg-custom-args/captures/i19330.check b/tests/neg-custom-args/captures/i19330.check new file mode 100644 index 000000000000..a8925b117611 --- /dev/null +++ b/tests/neg-custom-args/captures/i19330.check @@ -0,0 +1,5 @@ +-- Error: tests/neg-custom-args/captures/i19330.scala:15:29 ------------------------------------------------------------ +15 | val leaked = usingLogger[x.T]: l => // error + | ^^^ + | Type variable T of method usingLogger cannot be instantiated to x.T since + | the part () => Logger^ of that type captures the root capability `cap`. diff --git a/tests/neg-custom-args/captures/i21401.check b/tests/neg-custom-args/captures/i21401.check index 679c451949bd..e7483e10bfa6 100644 --- a/tests/neg-custom-args/captures/i21401.check +++ b/tests/neg-custom-args/captures/i21401.check @@ -11,8 +11,8 @@ -- Error: tests/neg-custom-args/captures/i21401.scala:16:66 ------------------------------------------------------------ 16 | val leaked: [R, X <: Boxed[IO^] -> R] -> (op: X) -> R = usingIO[Res](mkRes) // error | ^^^ - | Type variable R of method usingIO cannot be instantiated to Res since - | the part box IO^ of that type captures the root capability `cap`. + | Type variable R of method usingIO cannot be instantiated to [R, X <: Boxed[box IO^] -> R] => (op: X) -> R since + | the part box IO^ of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/i21401.scala:17:29 ------------------------------------------------------------ 17 | val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) // error // error | ^^^^^^^^^^ @@ -21,8 +21,8 @@ -- Error: tests/neg-custom-args/captures/i21401.scala:17:52 ------------------------------------------------------------ 17 | val x: Boxed[IO^] = leaked[Boxed[IO^], Boxed[IO^] -> Boxed[IO^]](x => x) // error // error | ^^^^^^^^^^^^^^^^^^^^^^^^ - |Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> (ex$18: caps.Exists) -> Boxed[box IO^{ex$18}] since - |the part box IO^{ex$18} of that type captures the root capability `cap`. + |Type variable X of value leaked cannot be instantiated to Boxed[box IO^] -> (ex$20: caps.Exists) -> Boxed[box IO^{ex$20}] since + |the part box IO^{ex$20} of that type captures the root capability `cap`. -- Error: tests/neg-custom-args/captures/i21401.scala:18:21 ------------------------------------------------------------ 18 | val y: IO^{x*} = x.unbox // error | ^^^^^^^ diff --git a/tests/neg-custom-args/captures/outer-var.check b/tests/neg-custom-args/captures/outer-var.check index 72af842728a1..b24579b7a69f 100644 --- a/tests/neg-custom-args/captures/outer-var.check +++ b/tests/neg-custom-args/captures/outer-var.check @@ -1,7 +1,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:11:8 ------------------------------------- 11 | x = q // error | ^ - | Found: (q : Proc) + | Found: (q : () => Unit) | Required: () ->{p, q²} Unit | | where: q is a parameter in method inner @@ -11,14 +11,14 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:12:9 ------------------------------------- 12 | x = (q: Proc) // error | ^^^^^^^ - | Found: Proc + | Found: () => Unit | Required: () ->{p, q} Unit | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:13:9 ------------------------------------- 13 | y = (q: Proc) // error | ^^^^^^^ - | Found: Proc + | Found: () => Unit | Required: () ->{p} Unit | | Note that the universal capability `cap` @@ -28,10 +28,10 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:14:8 ------------------------------------- 14 | y = q // error, was OK under unsealed | ^ - | Found: (q : Proc) + | Found: (q : () => Unit) | Required: () ->{p} Unit | - | Note that reference (q : Proc), defined in method inner + | Note that reference (q : () => Unit), defined in method inner | cannot be included in outer capture set {p} of variable y | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 72604451472c..23c1b056c659 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -6,8 +6,8 @@ -- Error: tests/neg-custom-args/captures/try.scala:30:65 --------------------------------------------------------------- 30 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error | ^ - | reference (x : CanThrow[Exception]) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () ->? Nothing + | reference (x : CT[Exception]^) is not included in the allowed capture set {} + | of an enclosing function literal with expected type () ->? Nothing -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:52:2 ------------------------------------------- 47 |val global: () -> Int = handle { 48 | (x: CanThrow[Exception]) => diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index e4b1e71a2000..db5c8083e3b7 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -1,9 +1,9 @@ -- Error: tests/neg-custom-args/captures/vars.scala:24:14 -------------------------------------------------------------- 24 | a = x => g(x) // error | ^^^^ - | reference (cap3 : Cap) is not included in the allowed capture set {cap1} of variable a + | reference (cap3 : CC^) is not included in the allowed capture set {cap1} of variable a | - | Note that reference (cap3 : Cap), defined in method scope + | Note that reference (cap3 : CC^), defined in method scope | cannot be included in outer capture set {cap1} of variable a -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:25:8 ------------------------------------------ 25 | a = g // error @@ -11,7 +11,7 @@ | Found: (x: String) ->{cap3} String | Required: (x$0: String) ->{cap1} String | - | Note that reference (cap3 : Cap), defined in method scope + | Note that reference (cap3 : CC^), defined in method scope | cannot be included in outer capture set {cap1} of variable a | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i15749a.scala b/tests/pos-custom-args/captures/i15749a.scala similarity index 51% rename from tests/neg-custom-args/captures/i15749a.scala rename to tests/pos-custom-args/captures/i15749a.scala index d3c1fce13322..184f980d6d70 100644 --- a/tests/neg-custom-args/captures/i15749a.scala +++ b/tests/pos-custom-args/captures/i15749a.scala @@ -6,19 +6,17 @@ object u extends Unit type Top = Any^ -type Wrapper[+T] = [X] -> (op: T ->{cap} X) -> X +class Wrapper[+T](val value: [X] -> (op: T ->{cap} X) -> X) def test = - def wrapper[T](x: T): Wrapper[T] = + def wrapper[T](x: T): Wrapper[T] = Wrapper: [X] => (op: T ->{cap} X) => op(x) def strictMap[A <: Top, B <: Top](mx: Wrapper[A])(f: A ->{cap} B): Wrapper[B] = - mx((x: A) => wrapper(f(x))) + mx.value((x: A) => wrapper(f(x))) def force[A](thunk: Unit ->{cap} A): A = thunk(u) def forceWrapper[A](@use mx: Wrapper[Unit ->{cap} A]): Wrapper[A] = - // Γ ⊢ mx: Wrapper[□ {cap} Unit => A] - // `force` should be typed as ∀(□ {cap} Unit -> A) A, but it can not - strictMap[Unit ->{mx*} A, A](mx)(t => force[A](t)) // error // should work + strictMap[Unit ->{mx*} A, A](mx)(t => force[A](t)) // was error when Wrapper was an alias type From 4ee31543ddd329ba3d2bc7a6e5e47f479b295e4d Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 6 Dec 2024 16:34:35 +0100 Subject: [PATCH 3/8] Elide capabilities implied by Capability subtypes when printing When printing a type `C^` where `C` extends `Capability`, don't show the `^`. This is overridden under -Yprint-debug. --- .../src/dotty/tools/dotc/printing/PlainPrinter.scala | 11 ++++++++--- tests/neg-custom-args/captures/byname.check | 4 ++-- tests/neg-custom-args/captures/cc-this5.check | 2 +- tests/neg-custom-args/captures/effect-swaps.check | 2 +- .../captures/explain-under-approx.check | 4 ++-- .../captures/extending-cap-classes.check | 6 +++--- tests/neg-custom-args/captures/i21614.check | 4 ++-- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index e90aeb217362..bace43b767bd 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -177,11 +177,16 @@ class PlainPrinter(_ctx: Context) extends Printer { * capturing function types. */ protected def toTextCapturing(parent: Type, refsText: Text, boxText: Text): Text = - changePrec(InfixPrec): - boxText ~ toTextLocal(parent) ~ "^" - ~ (refsText provided refsText != rootSetText) + def coreText = boxText ~ toTextLocal(parent) + if parent.derivesFrom(defn.Caps_Capability) + && refsText == impliedByCapabilitySetText + && !printDebug + then coreText + else changePrec(InfixPrec): + coreText~ "^" ~ (refsText provided refsText != rootSetText) final protected def rootSetText = Str("{cap}") // TODO Use disambiguation + final protected def impliedByCapabilitySetText = Str("{cap}") def toText(tp: Type): Text = controlled { homogenize(tp) match { diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index 1c113591922d..de2078ddf30a 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -8,10 +8,10 @@ -- Error: tests/neg-custom-args/captures/byname.scala:19:5 ------------------------------------------------------------- 19 | h(g()) // error | ^^^ - | reference (cap2 : Cap^) is not included in the allowed capture set {cap1} + | reference (cap2 : Cap) is not included in the allowed capture set {cap1} | of an enclosing function literal with expected type () ?->{cap1} I -- Error: tests/neg-custom-args/captures/byname.scala:22:12 ------------------------------------------------------------ 22 | h2(() => g())() // error | ^^^ - | reference (cap2 : Cap^) is not included in the allowed capture set {cap1} + | reference (cap2 : Cap) is not included in the allowed capture set {cap1} | of an enclosing function literal with expected type () ->{cap1} I diff --git a/tests/neg-custom-args/captures/cc-this5.check b/tests/neg-custom-args/captures/cc-this5.check index 21b5b36e0574..a69c482300f8 100644 --- a/tests/neg-custom-args/captures/cc-this5.check +++ b/tests/neg-custom-args/captures/cc-this5.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/cc-this5.scala:16:20 ---------------------------------------------------------- 16 | def f = println(c) // error | ^ - | reference (c : Cap^) is not included in the allowed capture set {} + | reference (c : Cap) is not included in the allowed capture set {} | of the enclosing class A -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-this5.scala:21:15 ------------------------------------- 21 | val x: A = this // error diff --git a/tests/neg-custom-args/captures/effect-swaps.check b/tests/neg-custom-args/captures/effect-swaps.check index b74c165fd6b6..48dc46c09821 100644 --- a/tests/neg-custom-args/captures/effect-swaps.check +++ b/tests/neg-custom-args/captures/effect-swaps.check @@ -25,5 +25,5 @@ -- Error: tests/neg-custom-args/captures/effect-swaps.scala:66:15 ------------------------------------------------------ 66 | Result.make: // error: local reference leaks | ^^^^^^^^^^^ - |local reference contextual$9 from (using contextual$9: boundary.Label[Result[box Future[box T^?]^{fr, contextual$9}, box E^?]]^): + |local reference contextual$9 from (using contextual$9: boundary.Label[Result[box Future[box T^?]^{fr, contextual$9}, box E^?]]): | box Future[box T^?]^{fr, contextual$9} leaks into outer capture set of type parameter T of method make in object Result diff --git a/tests/neg-custom-args/captures/explain-under-approx.check b/tests/neg-custom-args/captures/explain-under-approx.check index c186fc6adb11..f84ac5eb2b53 100644 --- a/tests/neg-custom-args/captures/explain-under-approx.check +++ b/tests/neg-custom-args/captures/explain-under-approx.check @@ -1,14 +1,14 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:12:10 ------------------------- 12 | col.add(Future(() => 25)) // error | ^^^^^^^^^^^^^^^^ - | Found: Future[Int]{val a: (async : Async^)}^{async} + | Found: Future[Int]{val a: (async : Async)}^{async} | Required: Future[Int]^{col.futs*} | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:15:11 ------------------------- 15 | col1.add(Future(() => 25)) // error | ^^^^^^^^^^^^^^^^ - | Found: Future[Int]{val a: (async : Async^)}^{async} + | Found: Future[Int]{val a: (async : Async)}^{async} | Required: Future[Int]^{col1.futs*} | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/extending-cap-classes.check b/tests/neg-custom-args/captures/extending-cap-classes.check index 0936f48576e5..4a77a638a4d8 100644 --- a/tests/neg-custom-args/captures/extending-cap-classes.check +++ b/tests/neg-custom-args/captures/extending-cap-classes.check @@ -1,21 +1,21 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:7:15 ------------------------- 7 | val x2: C1 = new C2 // error | ^^^^^^ - | Found: C2^ + | Found: C2 | Required: C1 | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:8:15 ------------------------- 8 | val x3: C1 = new C3 // error | ^^^^^^ - | Found: C3^ + | Found: C3 | Required: C1 | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:13:15 ------------------------ 13 | val z2: C1 = y2 // error | ^^ - | Found: (y2 : C2^) + | Found: (y2 : C2) | Required: C1 | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index f4967253455f..d4d64424e297 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -1,8 +1,8 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:12:33 --------------------------------------- 12 | files.map((f: F) => new Logger(f)) // error, Q: can we make this pass (see #19076)? | ^ - | Found: (f : F^) - | Required: File^ + | Found: (f : F) + | Required: File | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:15:12 --------------------------------------- From 96e2218d197b9d90eb2417bef7984860b63d6f0d Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 10 Jan 2025 19:05:44 +0100 Subject: [PATCH 4/8] Add Mutable classes and ReadOnly capabilities - Add Mutable trait and mut modifier. - Add dedicated tests `isMutableVar` and `isMutableVarOrAccessor` so that update methods can share the same flag `Mutable` with mutable vars. - Disallow update methods overriding normal methods - Disallow update methods which are not members of classes extending Mutable - Add design document from papers repo to docs/internals - Add readOnly capabilities - Implement raeadOnly access - Check that update methods are only called on references with exclusive capture sets. - Use cap.rd as default capture set of Capability subtypes - Make Mutable a Capability, this means Mutable class references get {cap.rd} as default capture set. - Use {cap} as captu --- .../tools/backend/jvm/BTypesFromSymbols.scala | 2 +- .../src/dotty/tools/dotc/ast/Desugar.scala | 2 + .../src/dotty/tools/dotc/ast/TreeInfo.scala | 2 +- compiler/src/dotty/tools/dotc/ast/untpd.scala | 3 + .../src/dotty/tools/dotc/cc/CaptureOps.scala | 165 ++++-- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 77 ++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 55 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 81 ++- .../src/dotty/tools/dotc/cc/Existential.scala | 3 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 4 +- .../dotty/tools/dotc/core/Definitions.scala | 9 +- .../src/dotty/tools/dotc/core/Flags.scala | 1 - .../src/dotty/tools/dotc/core/StdNames.scala | 3 + .../tools/dotc/core/SymDenotations.scala | 7 + .../src/dotty/tools/dotc/core/SymUtils.scala | 2 +- .../dotty/tools/dotc/core/TypeComparer.scala | 2 +- .../dotty/tools/dotc/parsing/Parsers.scala | 44 +- .../dotty/tools/dotc/parsing/Scanners.scala | 5 +- .../tools/dotc/printing/PlainPrinter.scala | 35 +- .../tools/dotc/printing/RefinedPrinter.scala | 4 +- .../dotty/tools/dotc/reporting/messages.scala | 2 +- .../src/dotty/tools/dotc/sbt/ExtractAPI.scala | 2 +- .../tools/dotc/transform/CapturedVars.scala | 2 +- .../tools/dotc/transform/CheckReentrant.scala | 2 +- .../tools/dotc/transform/CheckStatic.scala | 2 +- .../tools/dotc/transform/CheckUnused.scala | 2 +- .../tools/dotc/transform/Constructors.scala | 2 +- .../dotty/tools/dotc/transform/LazyVals.scala | 4 +- .../tools/dotc/transform/MoveStatics.scala | 2 +- .../dotc/transform/UninitializedDefs.scala | 2 +- .../tools/dotc/transform/init/Objects.scala | 2 +- .../tools/dotc/transform/init/Util.scala | 2 +- .../src/dotty/tools/dotc/typer/Checking.scala | 10 +- .../tools/dotc/typer/ErrorReporting.scala | 2 +- .../dotty/tools/dotc/typer/Nullables.scala | 6 +- .../tools/dotc/typer/QuotesAndSplices.scala | 2 +- .../dotty/tools/dotc/typer/RefChecks.scala | 9 +- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- .../tools/dotc/typer/VarianceChecker.scala | 2 +- .../_docs/internals/exclusive-capabilities.md | 551 ++++++++++++++++++ .../internal/readOnlyCapability.scala | 7 + library/src/scala/caps.scala | 15 +- project/MiMaFilters.scala | 1 + tests/neg-custom-args/captures/i21614.check | 19 +- .../captures/lazylists-exceptions.check | 2 +- .../captures/mut-outside-mutable.check | 8 + .../captures/mut-outside-mutable.scala | 10 + .../captures/mut-override.scala | 19 + tests/neg-custom-args/captures/readOnly.check | 19 + tests/neg-custom-args/captures/readOnly.scala | 22 + tests/neg-custom-args/captures/real-try.check | 10 +- tests/pos-custom-args/captures/mutRef.scala | 5 + tests/pos-custom-args/captures/readOnly.scala | 46 ++ 53 files changed, 1091 insertions(+), 208 deletions(-) create mode 100644 docs/_docs/internals/exclusive-capabilities.md create mode 100644 library/src/scala/annotation/internal/readOnlyCapability.scala create mode 100644 tests/neg-custom-args/captures/mut-outside-mutable.check create mode 100644 tests/neg-custom-args/captures/mut-outside-mutable.scala create mode 100644 tests/neg-custom-args/captures/mut-override.scala create mode 100644 tests/neg-custom-args/captures/readOnly.check create mode 100644 tests/neg-custom-args/captures/readOnly.scala create mode 100644 tests/pos-custom-args/captures/mutRef.scala create mode 100644 tests/pos-custom-args/captures/readOnly.scala diff --git a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala index 97934935f352..817d0be54d26 100644 --- a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala +++ b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala @@ -285,7 +285,7 @@ class BTypesFromSymbols[I <: DottyBackendInterface](val int: I, val frontendAcce // tests/run/serialize.scala and https://github.com/typelevel/cats-effect/pull/2360). val privateFlag = !sym.isClass && (sym.is(Private) || (sym.isPrimaryConstructor && sym.owner.isTopLevelModuleClass)) - val finalFlag = sym.is(Final) && !toDenot(sym).isClassConstructor && !sym.is(Mutable, butNot = Accessor) && !sym.enclosingClass.is(Trait) + val finalFlag = sym.is(Final) && !toDenot(sym).isClassConstructor && !sym.isMutableVar && !sym.enclosingClass.is(Trait) import asm.Opcodes.* import GenBCodeOps.addFlagIf diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 67e1885b511f..3eb186786be5 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -2231,6 +2231,8 @@ object desugar { New(ref(defn.RepeatedAnnot.typeRef), Nil :: Nil)) else if op.name == nme.CC_REACH then Apply(ref(defn.Caps_reachCapability), t :: Nil) + else if op.name == nme.CC_READONLY then + Apply(ref(defn.Caps_readOnlyCapability), t :: Nil) else assert(ctx.mode.isExpr || ctx.reporter.errorsReported || ctx.mode.is(Mode.Interactive), ctx.mode) Select(t, op.name) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index e0fe17755257..6ea6c27331dd 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -755,7 +755,7 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => */ def isVariableOrGetter(tree: Tree)(using Context): Boolean = { def sym = tree.symbol - def isVar = sym.is(Mutable) + def isVar = sym.isMutableVarOrAccessor def isGetter = mayBeVarGetter(sym) && sym.owner.info.member(sym.name.asTermName.setterName).exists diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 2acfc4cf86e3..e89dc2c1cdb5 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -206,6 +206,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class Var()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Mutable) + case class Mut()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Mutable) + case class Implicit()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Implicit) case class Given()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Given) @@ -332,6 +334,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def isEnumCase: Boolean = isEnum && is(Case) def isEnumClass: Boolean = isEnum && !is(Case) + def isMutableVar: Boolean = is(Mutable) && mods.exists(_.isInstanceOf[Mod.Var]) } @sharable val EmptyModifiers: Modifiers = Modifiers() diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 92cd40a65d5a..1a9421aea142 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -136,6 +136,8 @@ extension (tree: Tree) def toCaptureRefs(using Context): List[CaptureRef] = tree match case ReachCapabilityApply(arg) => arg.toCaptureRefs.map(_.reach) + case ReadOnlyCapabilityApply(arg) => + arg.toCaptureRefs.map(_.readOnly) case CapsOfApply(arg) => arg.toCaptureRefs case _ => tree.tpe.dealiasKeepAnnots match @@ -184,7 +186,7 @@ extension (tp: Type) case tp: TermRef => ((tp.prefix eq NoPrefix) || tp.symbol.isField && !tp.symbol.isStatic && tp.prefix.isTrackableRef - || tp.isRootCapability + || tp.isCap ) && !tp.symbol.isOneOf(UnstableValueFlags) case tp: TypeRef => tp.symbol.isType && tp.derivesFrom(defn.Caps_CapSet) @@ -193,6 +195,7 @@ extension (tp: Type) case AnnotatedType(parent, annot) => (annot.symbol == defn.ReachCapabilityAnnot || annot.symbol == defn.MaybeCapabilityAnnot + || annot.symbol == defn.ReadOnlyCapabilityAnnot ) && parent.isTrackableRef case _ => false @@ -222,6 +225,8 @@ extension (tp: Type) else tp match case tp @ ReachCapability(_) => tp.singletonCaptureSet + case ReadOnlyCapability(ref) => + ref.deepCaptureSet(includeTypevars) case tp: SingletonCaptureRef if tp.isTrackableRef => tp.reach.singletonCaptureSet case _ => @@ -345,7 +350,8 @@ extension (tp: Type) def forceBoxStatus(boxed: Boolean)(using Context): Type = tp.widenDealias match case tp @ CapturingType(parent, refs) if tp.isBoxed != boxed => val refs1 = tp match - case ref: CaptureRef if ref.isTracked || ref.isReach => ref.singletonCaptureSet + case ref: CaptureRef if ref.isTracked || ref.isReach || ref.isReadOnly => + ref.singletonCaptureSet case _ => refs CapturingType(parent, refs1, boxed) case _ => @@ -379,23 +385,32 @@ extension (tp: Type) case _ => false + /** Is this a type extending `Mutable` that has update methods? */ + def isMutableType(using Context): Boolean = + tp.derivesFrom(defn.Caps_Mutable) + && tp.membersBasedOnFlags(Mutable | Method, EmptyFlags) + .exists(_.hasAltWith(_.symbol.isUpdateMethod)) + /** Tests whether the type derives from `caps.Capability`, which means * references of this type are maximal capabilities. */ - def derivesFromCapability(using Context): Boolean = tp.dealias match + def derivesFromCapTrait(cls: ClassSymbol)(using Context): Boolean = tp.dealias match case tp: (TypeRef | AppliedType) => val sym = tp.typeSymbol - if sym.isClass then sym.derivesFrom(defn.Caps_Capability) - else tp.superType.derivesFromCapability + if sym.isClass then sym.derivesFrom(cls) + else tp.superType.derivesFromCapTrait(cls) case tp: (TypeProxy & ValueType) => - tp.superType.derivesFromCapability + tp.superType.derivesFromCapTrait(cls) case tp: AndType => - tp.tp1.derivesFromCapability || tp.tp2.derivesFromCapability + tp.tp1.derivesFromCapTrait(cls) || tp.tp2.derivesFromCapTrait(cls) case tp: OrType => - tp.tp1.derivesFromCapability && tp.tp2.derivesFromCapability + tp.tp1.derivesFromCapTrait(cls) && tp.tp2.derivesFromCapTrait(cls) case _ => false + def derivesFromCapability(using Context): Boolean = derivesFromCapTrait(defn.Caps_Capability) + def derivesFromMutable(using Context): Boolean = derivesFromCapTrait(defn.Caps_Mutable) + /** Drop @retains annotations everywhere */ def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling val tm = new TypeMap: @@ -406,17 +421,6 @@ extension (tp: Type) mapOver(t) tm(tp) - /** If `x` is a capture ref, its reach capability `x*`, represented internally - * as `x @reachCapability`. `x*` stands for all capabilities reachable through `x`". - * We have `{x} <: {x*} <: dcs(x)}` where the deep capture set `dcs(x)` of `x` - * is the union of all capture sets that appear in covariant position in the - * type of `x`. If `x` and `y` are different variables then `{x*}` and `{y*}` - * are unrelated. - */ - def reach(using Context): CaptureRef = tp match - case tp: CaptureRef if tp.isTrackableRef => - if tp.isReach then tp else ReachCapability(tp) - /** If `x` is a capture ref, its maybe capability `x?`, represented internally * as `x @maybeCapability`. `x?` stands for a capability `x` that might or might * not be part of a capture set. We have `{} <: {x?} <: {x}`. Maybe capabilities @@ -436,42 +440,43 @@ extension (tp: Type) * but it has fewer issues with type inference. */ def maybe(using Context): CaptureRef = tp match - case tp: CaptureRef if tp.isTrackableRef => - if tp.isMaybe then tp else MaybeCapability(tp) + case tp @ AnnotatedType(_, annot) if annot.symbol == defn.MaybeCapabilityAnnot => tp + case _ => MaybeCapability(tp) - /** If `ref` is a trackable capture ref, and `tp` has only covariant occurrences of a - * universal capture set, replace all these occurrences by `{ref*}`. This implements - * the new aspect of the (Var) rule, which can now be stated as follows: - * - * x: T in E - * ----------- - * E |- x: T' - * - * where T' is T with (1) the toplevel capture set replaced by `{x}` and - * (2) all covariant occurrences of cap replaced by `x*`, provided there - * are no occurrences in `T` at other variances. (1) is standard, whereas - * (2) is new. - * - * For (2), multiple-flipped covariant occurrences of cap won't be replaced. - * In other words, - * - * - For xs: List[File^] ==> List[File^{xs*}], the cap is replaced; - * - while f: [R] -> (op: File^ => R) -> R remains unchanged. - * - * Without this restriction, the signature of functions like withFile: - * - * (path: String) -> [R] -> (op: File^ => R) -> R - * - * could be refined to - * - * (path: String) -> [R] -> (op: File^{withFile*} => R) -> R - * - * which is clearly unsound. - * - * Why is this sound? Covariant occurrences of cap must represent capabilities - * that are reachable from `x`, so they are included in the meaning of `{x*}`. - * At the same time, encapsulation is still maintained since no covariant - * occurrences of cap are allowed in instance types of type variables. + /** If `x` is a capture ref, its reach capability `x*`, represented internally + * as `x @reachCapability`. `x*` stands for all capabilities reachable through `x`". + * We have `{x} <: {x*} <: dcs(x)}` where the deep capture set `dcs(x)` of `x` + * is the union of all capture sets that appear in covariant position in the + * type of `x`. If `x` and `y` are different variables then `{x*}` and `{y*}` + * are unrelated. + */ + def reach(using Context): CaptureRef = tp match + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.MaybeCapabilityAnnot => + tp.derivedAnnotatedType(tp1.reach, annot) + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.ReachCapabilityAnnot => + tp + case _ => + ReachCapability(tp) + + /** If `x` is a capture ref, its read-only capability `x.rd`, represented internally + * as `x @readOnlyCapability`. We have {x.rd} <: {x}. If `x` is a reach capability `y*`, + * then its read-only version is `x.rd*`. + */ + def readOnly(using Context): CaptureRef = tp match + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.MaybeCapabilityAnnot + || annot.symbol == defn.ReachCapabilityAnnot => + tp.derivedAnnotatedType(tp1.readOnly, annot) + case tp @ AnnotatedType(tp1: CaptureRef, annot) + if annot.symbol == defn.ReadOnlyCapabilityAnnot => + tp + case _ => + ReadOnlyCapability(tp) + + /** If `x` is a capture ref, replacxe all no-flip covariant occurrences of `cap` + * in type `tp` with `x*`. */ def withReachCaptures(ref: Type)(using Context): Type = object narrowCaps extends TypeMap: @@ -479,9 +484,10 @@ extension (tp: Type) def apply(t: Type) = if variance <= 0 then t else t.dealiasKeepAnnots match - case t @ CapturingType(p, cs) if cs.isUniversal => + case t @ CapturingType(p, cs) if cs.containsRootCapability => change = true - t.derivedCapturingType(apply(p), ref.reach.singletonCaptureSet) + val reachRef = if cs.isReadOnly then ref.reach.readOnly else ref.reach + t.derivedCapturingType(apply(p), reachRef.singletonCaptureSet) case t @ AnnotatedType(parent, ann) => // Don't map annotations, which includes capture sets t.derivedAnnotatedType(this(parent), ann) @@ -615,6 +621,16 @@ extension (sym: Symbol) case c: TypeRef => c.symbol == sym case _ => false + def isUpdateMethod(using Context): Boolean = + sym.isAllOf(Mutable | Method, butNot = Accessor) + + def isReadOnlyMethod(using Context): Boolean = + sym.is(Method, butNot = Mutable | Accessor) && sym.owner.derivesFrom(defn.Caps_Mutable) + + def isInReadOnlyMethod(using Context): Boolean = + if sym.is(Method) && sym.owner.isClass then isReadOnlyMethod + else sym.owner.isInReadOnlyMethod + extension (tp: AnnotatedType) /** Is this a boxed capturing type? */ def isBoxed(using Context): Boolean = tp.annot match @@ -650,6 +666,14 @@ object ReachCapabilityApply: case Apply(reach, arg :: Nil) if reach.symbol == defn.Caps_reachCapability => Some(arg) case _ => None +/** An extractor for `caps.readOnlyCapability(ref)`, which is used to express a read-only + * capability as a tree in a @retains annotation. + */ +object ReadOnlyCapabilityApply: + def unapply(tree: Apply)(using Context): Option[Tree] = tree match + case Apply(ro, arg :: Nil) if ro.symbol == defn.Caps_readOnlyCapability => Some(arg) + case _ => None + /** An extractor for `caps.capsOf[X]`, which is used to express a generic capture set * as a tree in a @retains annotation. */ @@ -658,22 +682,35 @@ object CapsOfApply: case TypeApply(capsOf, arg :: Nil) if capsOf.symbol == defn.Caps_capsOf => Some(arg) case _ => None -class AnnotatedCapability(annot: Context ?=> ClassSymbol): - def apply(tp: Type)(using Context) = +abstract class AnnotatedCapability(annot: Context ?=> ClassSymbol): + def apply(tp: Type)(using Context): AnnotatedType = + assert(tp.isTrackableRef) + tp match + case AnnotatedType(_, annot) => assert(!unwrappable.contains(annot.symbol)) + case _ => AnnotatedType(tp, Annotation(annot, util.Spans.NoSpan)) def unapply(tree: AnnotatedType)(using Context): Option[CaptureRef] = tree match case AnnotatedType(parent: CaptureRef, ann) if ann.symbol == annot => Some(parent) case _ => None - -/** An extractor for `ref @annotation.internal.reachCapability`, which is used to express - * the reach capability `ref*` as a type. - */ -object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot) + protected def unwrappable(using Context): Set[Symbol] /** An extractor for `ref @maybeCapability`, which is used to express * the maybe capability `ref?` as a type. */ -object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot) +object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot): + protected def unwrappable(using Context) = Set() + +/** An extractor for `ref @readOnlyCapability`, which is used to express + * the rad-only capability `ref.rd` as a type. + */ +object ReadOnlyCapability extends AnnotatedCapability(defn.ReadOnlyCapabilityAnnot): + protected def unwrappable(using Context) = Set(defn.MaybeCapabilityAnnot) + +/** An extractor for `ref @annotation.internal.reachCapability`, which is used to express + * the reach capability `ref*` as a type. + */ +object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot): + protected def unwrappable(using Context) = Set(defn.MaybeCapabilityAnnot, defn.ReadOnlyCapabilityAnnot) /** Offers utility method to be used for type maps that follow aliases */ trait ConservativeFollowAliasMap(using Context) extends TypeMap: diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index e5beb56c6c56..00e872cb2d4c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -15,7 +15,9 @@ import compiletime.uninitialized import StdNames.nme /** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs, - * as well as two kinds of AnnotatedTypes representing reach and maybe capabilities. + * as well as three kinds of AnnotatedTypes representing readOnly, reach, and maybe capabilities. + * If there are several annotations they come with an orderL + * `*` first, `.rd` next, `?` last. */ trait CaptureRef extends TypeProxy, ValueType: private var myCaptureSet: CaptureSet | Null = uninitialized @@ -28,39 +30,69 @@ trait CaptureRef extends TypeProxy, ValueType: final def isTracked(using Context): Boolean = this.isTrackableRef && (isMaxCapability || !captureSetOfInfo.isAlwaysEmpty) - /** Is this a reach reference of the form `x*`? */ - final def isReach(using Context): Boolean = this match - case AnnotatedType(_, annot) => annot.symbol == defn.ReachCapabilityAnnot - case _ => false - /** Is this a maybe reference of the form `x?`? */ - final def isMaybe(using Context): Boolean = this match - case AnnotatedType(_, annot) => annot.symbol == defn.MaybeCapabilityAnnot - case _ => false + final def isMaybe(using Context): Boolean = this ne stripMaybe - final def stripReach(using Context): CaptureRef = - if isReach then - val AnnotatedType(parent: CaptureRef, _) = this: @unchecked - parent - else this + /** Is this a read-only reference of the form `x.rd` or a capture set variable + * with only read-ony references in its upper bound? + */ + final def isReadOnly(using Context): Boolean = this match + case tp: TypeRef => tp.captureSetOfInfo.isReadOnly + case _ => this ne stripReadOnly - final def stripMaybe(using Context): CaptureRef = - if isMaybe then - val AnnotatedType(parent: CaptureRef, _) = this: @unchecked - parent - else this + /** Is this a reach reference of the form `x*`? */ + final def isReach(using Context): Boolean = this ne stripReach + + final def stripMaybe(using Context): CaptureRef = this match + case AnnotatedType(tp1: CaptureRef, annot) if annot.symbol == defn.MaybeCapabilityAnnot => + tp1 + case _ => + this + + final def stripReadOnly(using Context): CaptureRef = this match + case tp @ AnnotatedType(tp1: CaptureRef, annot) => + val sym = annot.symbol + if sym == defn.ReadOnlyCapabilityAnnot then + tp1 + else if sym == defn.MaybeCapabilityAnnot then + tp.derivedAnnotatedType(tp1.stripReadOnly, annot) + else + this + case _ => + this + + final def stripReach(using Context): CaptureRef = this match + case tp @ AnnotatedType(tp1: CaptureRef, annot) => + val sym = annot.symbol + if sym == defn.ReachCapabilityAnnot then + tp1 + else if sym == defn.ReadOnlyCapabilityAnnot || sym == defn.MaybeCapabilityAnnot then + tp.derivedAnnotatedType(tp1.stripReach, annot) + else + this + case _ => + this /** Is this reference the generic root capability `cap` ? */ - final def isRootCapability(using Context): Boolean = this match + final def isCap(using Context): Boolean = this match case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot case _ => false + /** Is this reference one the generic root capabilities `cap` or `cap.rd` ? */ + final def isRootCapability(using Context): Boolean = this match + case ReadOnlyCapability(tp1) => tp1.isCap + case _ => isCap + /** Is this reference capability that does not derive from another capability ? */ final def isMaxCapability(using Context): Boolean = this match - case tp: TermRef => tp.isRootCapability || tp.info.derivesFrom(defn.Caps_Exists) + case tp: TermRef => tp.isCap || tp.info.derivesFrom(defn.Caps_Exists) case tp: TermParamRef => tp.underlying.derivesFrom(defn.Caps_Exists) + case ReadOnlyCapability(tp1) => tp1.isMaxCapability case _ => false + final def isExclusive(using Context): Boolean = + !isReadOnly && (isMaxCapability || captureSetOfInfo.isExclusive) + // With the support of pathes, we don't need to normalize the `TermRef`s anymore. // /** Normalize reference so that it can be compared with `eq` for equality */ // final def normalizedRef(using Context): CaptureRef = this match @@ -122,7 +154,7 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false (this eq y) - || this.isRootCapability + || this.isCap || y.match case y: TermRef if !y.isRootCapability => y.prefix.match @@ -142,6 +174,7 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false || viaInfo(y.info)(subsumingRefs(this, _)) case MaybeCapability(y1) => this.stripMaybe.subsumes(y1) + case ReadOnlyCapability(y1) => this.stripReadOnly.subsumes(y1) case y: TypeRef if y.derivesFrom(defn.Caps_CapSet) => // The upper and lower bounds don't have to be in the form of `CapSet^{...}`. // They can be other capture set variables, which are bounded by `CapSet`, diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 7f4a34bab1f9..e1f5a557bc0d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -83,11 +83,21 @@ sealed abstract class CaptureSet extends Showable: /** Does this capture set contain the root reference `cap` as element? */ final def isUniversal(using Context) = + elems.exists(_.isCap) + + /** Does this capture set contain a root reference `cap` or `cap.rd` as element? */ + final def containsRootCapability(using Context) = elems.exists(_.isRootCapability) final def isUnboxable(using Context) = elems.exists(elem => elem.isRootCapability || Existential.isExistentialVar(elem)) + final def isReadOnly(using Context): Boolean = + elems.forall(_.isReadOnly) + + final def isExclusive(using Context): Boolean = + elems.exists(_.isExclusive) + final def keepAlways: Boolean = this.isInstanceOf[EmptyWithProvenance] /** Try to include an element in this capture set. @@ -310,6 +320,8 @@ sealed abstract class CaptureSet extends Showable: def maybe(using Context): CaptureSet = map(MaybeMap()) + def readOnly(using Context): CaptureSet = map(ReadOnlyMap()) + /** Invoke handler if this set has (or later aquires) the root capability `cap` */ def disallowRootCapability(handler: () => Context ?=> Unit)(using Context): this.type = if isUnboxable then handler() @@ -373,6 +385,10 @@ object CaptureSet: def universal(using Context): CaptureSet = defn.captureRoot.termRef.singletonCaptureSet + /** The shared capture set `{cap.rd}` */ + def shared(using Context): CaptureSet = + defn.captureRoot.termRef.readOnly.singletonCaptureSet + /** Used as a recursion brake */ @sharable private[dotc] val Pending = Const(SimpleIdentitySet.empty) @@ -526,6 +542,8 @@ object CaptureSet: elem.cls.ccLevel.nextInner <= level case ReachCapability(elem1) => levelOK(elem1) + case ReadOnlyCapability(elem1) => + levelOK(elem1) case MaybeCapability(elem1) => levelOK(elem1) case _ => @@ -558,8 +576,10 @@ object CaptureSet: final def upperApprox(origin: CaptureSet)(using Context): CaptureSet = if isConst then this - else if elems.exists(_.isRootCapability) || computingApprox then + else if isUniversal || computingApprox then universal + else if containsRootCapability && isReadOnly then + shared else computingApprox = true try @@ -1026,25 +1046,29 @@ object CaptureSet: /** The current VarState, as passed by the implicit context */ def varState(using state: VarState): VarState = state - /** Maps `x` to `x?` */ - private class MaybeMap(using Context) extends BiTypeMap: + /** A template for maps on capabilities where f(c) <: c and f(f(c)) = c */ + private abstract class NarrowingCapabilityMap(using Context) extends BiTypeMap: + def mapRef(ref: CaptureRef): CaptureRef def apply(t: Type) = t match - case t: CaptureRef if t.isTrackableRef => t.maybe + case t: CaptureRef if t.isTrackableRef => mapRef(t) case _ => mapOver(t) - override def toString = "Maybe" - lazy val inverse = new BiTypeMap: + def apply(t: Type) = t // since f(c) <: c, this is the best inverse + def inverse = NarrowingCapabilityMap.this + override def toString = NarrowingCapabilityMap.this.toString ++ ".inverse" + end NarrowingCapabilityMap - def apply(t: Type) = t match - case t: CaptureRef if t.isMaybe => t.stripMaybe - case t => mapOver(t) - - def inverse = MaybeMap.this + /** Maps `x` to `x?` */ + private class MaybeMap(using Context) extends NarrowingCapabilityMap: + def mapRef(ref: CaptureRef): CaptureRef = ref.maybe + override def toString = "Maybe" - override def toString = "Maybe.inverse" - end MaybeMap + /** Maps `x` to `x.rd` */ + private class ReadOnlyMap(using Context) extends NarrowingCapabilityMap: + def mapRef(ref: CaptureRef): CaptureRef = ref.readOnly + override def toString = "ReadOnly" /* Not needed: def ofClass(cinfo: ClassInfo, argTypes: List[Type])(using Context): CaptureSet = @@ -1073,6 +1097,8 @@ object CaptureSet: case ReachCapability(ref1) => ref1.widen.deepCaptureSet(includeTypevars = true) .showing(i"Deep capture set of $ref: ${ref1.widen} = ${result}", capt) + case ReadOnlyCapability(ref1) => + ref1.captureSetOfInfo.map(ReadOnlyMap()) case _ => if ref.isMaxCapability then ref.singletonCaptureSet else ofType(ref.underlying, followResult = true) @@ -1197,9 +1223,10 @@ object CaptureSet: for CompareResult.LevelError(cs, ref) <- ccState.levelError.toList yield ccState.levelError = None if ref.isRootCapability then + def capStr = if ref.isReadOnly then "cap.rd" else "cap" i""" | - |Note that the universal capability `cap` + |Note that the universal capability `$capStr` |cannot be included in capture set $cs""" else val levelStr = ref match diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 830d9ad0a4d4..eab11d03144d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -150,6 +150,7 @@ object CheckCaptures: |is must be a type parameter or abstract type with a caps.CapSet upper bound.""", elem.srcPos) case ReachCapabilityApply(arg) => check(arg, elem.srcPos) + case ReadOnlyCapabilityApply(arg) => check(arg, elem.srcPos) case _ => check(elem, elem.srcPos) /** Under the sealed policy, report an error if some part of `tp` contains the @@ -381,7 +382,7 @@ class CheckCaptures extends Recheck, SymTransformer: def markFree(sym: Symbol, pos: SrcPos)(using Context): Unit = markFree(sym, sym.termRef, pos) - def markFree(sym: Symbol, ref: TermRef, pos: SrcPos)(using Context): Unit = + def markFree(sym: Symbol, ref: CaptureRef, pos: SrcPos)(using Context): Unit = if sym.exists && ref.isTracked then markFree(ref.captureSet, pos) /** Make sure the (projected) `cs` is a subset of the capture sets of all enclosing @@ -484,7 +485,8 @@ class CheckCaptures extends Recheck, SymTransformer: def includeCallCaptures(sym: Symbol, resType: Type, pos: SrcPos)(using Context): Unit = resType match case _: MethodOrPoly => // wait until method is fully applied case _ => - if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos) + if sym.exists then + if curEnv.isOpen then markFree(capturedVars(sym), pos) /** Under the sealed policy, disallow the root capability in type arguments. * Type arguments come either from a TypeApply node or from an AppliedType @@ -530,13 +532,18 @@ class CheckCaptures extends Recheck, SymTransformer: // expected type `pt`. // Example: If we have `x` and the expected type says we select that with `.a.b`, // we charge `x.a.b` instead of `x`. - def addSelects(ref: TermRef, pt: Type): TermRef = pt match + def addSelects(ref: TermRef, pt: Type): CaptureRef = pt match case pt: PathSelectionProto if ref.isTracked => - // if `ref` is not tracked then the selection could not give anything new - // class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters. - addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt) + if pt.sym.isReadOnlyMethod then + ref.readOnly + else + // if `ref` is not tracked then the selection could not give anything new + // class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters. + addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt) case _ => ref - val pathRef = addSelects(sym.termRef, pt) + var pathRef: CaptureRef = addSelects(sym.termRef, pt) + if pathRef.derivesFrom(defn.Caps_Mutable) && pt.isValueType && !pt.isMutableType then + pathRef = pathRef.readOnly markFree(sym, pathRef, tree.srcPos) super.recheckIdent(tree, pt) @@ -545,7 +552,9 @@ class CheckCaptures extends Recheck, SymTransformer: */ override def selectionProto(tree: Select, pt: Type)(using Context): Type = val sym = tree.symbol - if !sym.isOneOf(UnstableValueFlags) && !sym.isStatic then PathSelectionProto(sym, pt) + if !sym.isOneOf(UnstableValueFlags) && !sym.isStatic + || sym.isReadOnlyMethod + then PathSelectionProto(sym, pt) else super.selectionProto(tree, pt) /** A specialized implementation of the selection rule. @@ -573,6 +582,12 @@ class CheckCaptures extends Recheck, SymTransformer: } case _ => denot + if tree.symbol.isUpdateMethod && !qualType.captureSet.isExclusive then + report.error( + em"""cannot call update ${tree.symbol} from $qualType, + |since its capture set ${qualType.captureSet} is read-only""", + tree.srcPos) + val selType = recheckSelection(tree, qualType, name, disambiguate) val selWiden = selType.widen @@ -731,7 +746,9 @@ class CheckCaptures extends Recheck, SymTransformer: def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = var refined: Type = core var allCaptures: CaptureSet = - if core.derivesFromCapability then defn.universalCSImpliedByCapability else initCs + if core.derivesFromMutable then CaptureSet.universal + else if core.derivesFromCapability then initCs ++ defn.universalCSImpliedByCapability + else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol if !getter.is(Private) && getter.hasTrackedParts then @@ -1105,6 +1122,7 @@ class CheckCaptures extends Recheck, SymTransformer: if tree.isTerm && !pt.isBoxedCapturing && pt != LhsProto then markFree(res.boxedCaptureSet, tree.srcPos) res + end recheck /** Under the old unsealed policy: check that cap is ot unboxed */ override def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = @@ -1427,6 +1445,25 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => widened case _ => widened + /** If actual is a capturing type T^C extending Mutable, and expected is an + * unboxed non-singleton value type not extending mutable, narrow the capture + * set `C` to `ro(C)`. + * The unboxed condition ensures that the expected is not a type variable + * that's upper bounded by a read-only type. In this case it would not be sound + * to narrow to the read-only set, since that set can be propagated + * by the type variable instantiatiin. + */ + private def improveReadOnly(actual: Type, expected: Type)(using Context): Type = actual match + case actual @ CapturingType(parent, refs) + if parent.derivesFrom(defn.Caps_Mutable) + && expected.isValueType + && !expected.isMutableType + && !expected.isSingleton + && !expected.isBoxedCapturing => + actual.derivedCapturingType(parent, refs.readOnly) + case _ => + actual + /** Adapt `actual` type to `expected` type. This involves: * - narrow toplevel captures of `x`'s underlying type to `{x}` according to CC's VAR rule * - narrow nested captures of `x`'s underlying type to `{x*}` @@ -1436,12 +1473,14 @@ class CheckCaptures extends Recheck, SymTransformer: if expected == LhsProto || expected.isSingleton && actual.isSingleton then actual else - val widened = improveCaptures(actual.widen.dealiasKeepAnnots, actual) + val improvedVAR = improveCaptures(actual.widen.dealiasKeepAnnots, actual) + val improvedRO = improveReadOnly(improvedVAR, expected) val adapted = adaptBoxed( - widened.withReachCaptures(actual), expected, pos, + improvedRO.withReachCaptures(actual), expected, pos, covariant = true, alwaysConst = false, boxErrors) - if adapted eq widened then actual - else adapted.showing(i"adapt boxed $actual vs $expected = $adapted", capt) + if adapted eq improvedVAR // no .rd improvement, no box-adaptation + then actual // might as well use actual instead of improved widened + else adapted.showing(i"adapt $actual vs $expected = $adapted", capt) end adapt // ---- Unit-level rechecking ------------------------------------------- @@ -1484,18 +1523,16 @@ class CheckCaptures extends Recheck, SymTransformer: /** Check that overrides don't change the @use status of their parameters */ override def additionalChecks(member: Symbol, other: Symbol)(using Context): Unit = + def fail(msg: String) = + report.error( + OverrideError(msg, self, member, other, self.memberInfo(member), self.memberInfo(other)), + if member.owner == clazz then member.srcPos else clazz.srcPos) for (params1, params2) <- member.rawParamss.lazyZip(other.rawParamss) (param1, param2) <- params1.lazyZip(params2) do if param1.hasAnnotation(defn.UseAnnot) != param2.hasAnnotation(defn.UseAnnot) then - report.error( - OverrideError( - i"has a parameter ${param1.name} with different @use status than the corresponding parameter in the overridden definition", - self, member, other, self.memberInfo(member), self.memberInfo(other) - ), - if member.owner == clazz then member.srcPos else clazz.srcPos - ) + fail(i"has a parameter ${param1.name} with different @use status than the corresponding parameter in the overridden definition") end OverridingPairsCheckerCC def traverse(t: Tree)(using Context) = @@ -1526,7 +1563,7 @@ class CheckCaptures extends Recheck, SymTransformer: def traverse(tree: Tree)(using Context) = tree match case id: Ident => val sym = id.symbol - if sym.is(Mutable, butNot = Method) && sym.owner.isTerm then + if sym.isMutableVar && sym.owner.isTerm then val enclMeth = ctx.owner.enclosingMethod if sym.enclosingMethod != enclMeth then capturedBy(sym) = enclMeth @@ -1601,7 +1638,7 @@ class CheckCaptures extends Recheck, SymTransformer: selfType match case CapturingType(_, refs: CaptureSet.Var) if !root.isEffectivelySealed - && !refs.elems.exists(_.isRootCapability) + && !refs.isUniversal && !root.matchesExplicitRefsInBaseClass(refs) => // Forbid inferred self types unless they are already implied by an explicit diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index ea979e0b9f7f..943254a7ba4e 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -4,7 +4,6 @@ package cc import core.* import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* -import CaptureSet.IdempotentCaptRefMap import StdNames.nme import ast.tpd.* import Decorators.* @@ -303,7 +302,7 @@ object Existential: class Wrap(boundVar: TermParamRef) extends CapMap: def apply(t: Type) = t match - case t: TermRef if t.isRootCapability => + case t: TermRef if t.isCap => if variance > 0 then needsWrap = true boundVar diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index e28aeb8e0313..19522ddf603c 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -443,7 +443,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: try transformTT(tpt, boxed = - sym.is(Mutable, butNot = Method) + sym.isMutableVar && !ccConfig.useSealed && !sym.hasAnnotation(defn.UncheckedCapturesAnnot), // Under the sealed policy, we disallow root capabilities in the type of mutable @@ -735,7 +735,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case RetainingType(parent, refs) => needsVariable(parent) && !refs.tpes.exists: - case ref: TermRef => ref.isRootCapability + case ref: TermRef => ref.isCap case _ => false case AnnotatedType(parent, _) => needsVariable(parent) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 2890bdf306be..f108034d9070 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -15,7 +15,7 @@ import Comments.{Comment, docCtx} import util.Spans.NoSpan import config.Feature import Symbols.requiredModuleRef -import cc.{CaptureSet, RetainingType, Existential} +import cc.{CaptureSet, RetainingType, Existential, readOnly} import ast.tpd.ref import scala.annotation.tailrec @@ -992,18 +992,20 @@ class Definitions { @tu lazy val CapsModule: Symbol = requiredModule("scala.caps") @tu lazy val captureRoot: TermSymbol = CapsModule.requiredValue("cap") - @tu lazy val Caps_Capability: TypeSymbol = CapsModule.requiredType("Capability") + @tu lazy val Caps_Capability: ClassSymbol = requiredClass("scala.caps.Capability") @tu lazy val Caps_CapSet: ClassSymbol = requiredClass("scala.caps.CapSet") @tu lazy val Caps_reachCapability: TermSymbol = CapsModule.requiredMethod("reachCapability") + @tu lazy val Caps_readOnlyCapability: TermSymbol = CapsModule.requiredMethod("readOnlyCapability") @tu lazy val Caps_capsOf: TermSymbol = CapsModule.requiredMethod("capsOf") @tu lazy val Caps_Exists: ClassSymbol = requiredClass("scala.caps.Exists") @tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe") @tu lazy val Caps_unsafeAssumePure: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumePure") @tu lazy val Caps_ContainsTrait: TypeSymbol = CapsModule.requiredType("Contains") @tu lazy val Caps_containsImpl: TermSymbol = CapsModule.requiredMethod("containsImpl") + @tu lazy val Caps_Mutable: ClassSymbol = requiredClass("scala.caps.Mutable") /** The same as CaptureSet.universal but generated implicitly for references of Capability subtypes */ - @tu lazy val universalCSImpliedByCapability = CaptureSet(captureRoot.termRef) + @tu lazy val universalCSImpliedByCapability = CaptureSet(captureRoot.termRef.readOnly) @tu lazy val PureClass: Symbol = requiredClass("scala.Pure") @@ -1074,6 +1076,7 @@ class Definitions { @tu lazy val TargetNameAnnot: ClassSymbol = requiredClass("scala.annotation.targetName") @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") @tu lazy val ReachCapabilityAnnot = requiredClass("scala.annotation.internal.reachCapability") + @tu lazy val ReadOnlyCapabilityAnnot = requiredClass("scala.annotation.internal.readOnlyCapability") @tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability") @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains") @tu lazy val RetainsCapAnnot: ClassSymbol = requiredClass("scala.annotation.retainsCap") diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 0775b3caaf0c..57bf870c6b64 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -597,7 +597,6 @@ object Flags { val JavaInterface: FlagSet = JavaDefined | NoInits | Trait val JavaProtected: FlagSet = JavaDefined | Protected val MethodOrLazy: FlagSet = Lazy | Method - val MutableOrLazy: FlagSet = Lazy | Mutable val MethodOrLazyOrMutable: FlagSet = Lazy | Method | Mutable val LiftedMethod: FlagSet = Lifted | Method val LocalParam: FlagSet = Local | Param diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 56d71c7fb57e..dc30ae2be7fb 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -121,6 +121,7 @@ object StdNames { val BITMAP_CHECKINIT: N = s"${BITMAP_PREFIX}init$$" // initialization bitmap for checkinit values val BITMAP_CHECKINIT_TRANSIENT: N = s"${BITMAP_PREFIX}inittrans$$" // initialization bitmap for transient checkinit values val CC_REACH: N = "$reach" + val CC_READONLY: N = "$readOnly" val DEFAULT_GETTER: N = str.DEFAULT_GETTER val DEFAULT_GETTER_INIT: N = "$lessinit$greater" val DO_WHILE_PREFIX: N = "doWhile$" @@ -553,6 +554,7 @@ object StdNames { val materializeTypeTag: N = "materializeTypeTag" val mirror : N = "mirror" val moduleClass : N = "moduleClass" + val mut: N = "mut" val name: N = "name" val nameDollar: N = "$name" val ne: N = "ne" @@ -587,6 +589,7 @@ object StdNames { val productPrefix: N = "productPrefix" val quotes : N = "quotes" val raw_ : N = "raw" + val rd: N = "rd" val refl: N = "refl" val reflect: N = "reflect" val reflectiveSelectable: N = "reflectiveSelectable" diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index be651842d9b0..07506749ff07 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -806,6 +806,13 @@ object SymDenotations { final def isRealMethod(using Context): Boolean = this.is(Method, butNot = Accessor) && !isAnonymousFunction + /** A mutable variable (not a getter or setter for it) */ + final def isMutableVar(using Context): Boolean = is(Mutable, butNot = Method) + + /** A mutable variable or its getter or setter */ + final def isMutableVarOrAccessor(using Context): Boolean = + is(Mutable) && (!is(Method) || is(Accessor)) + /** Is this a getter? */ final def isGetter(using Context): Boolean = this.is(Accessor) && !originalName.isSetterName && !(originalName.isScala2LocalSuffix && symbol.owner.is(Scala2x)) diff --git a/compiler/src/dotty/tools/dotc/core/SymUtils.scala b/compiler/src/dotty/tools/dotc/core/SymUtils.scala index 1a762737d52f..baaeb025c6d5 100644 --- a/compiler/src/dotty/tools/dotc/core/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/SymUtils.scala @@ -287,7 +287,7 @@ class SymUtils: */ def isConstExprFinalVal(using Context): Boolean = atPhaseNoLater(erasurePhase) { - self.is(Final, butNot = Mutable) && self.info.resultType.isInstanceOf[ConstantType] + self.is(Final) && !self.isMutableVarOrAccessor && self.info.resultType.isInstanceOf[ConstantType] } && !self.sjsNeedsField /** The `ConstantType` of a val known to be `isConstrExprFinalVal`. diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index e9143ae88741..8e6bb78bd0e6 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2170,7 +2170,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling val info2 = tp2.refinedInfo val isExpr2 = info2.isInstanceOf[ExprType] var info1 = m.info match - case info1: ValueType if isExpr2 || m.symbol.is(Mutable) => + case info1: ValueType if isExpr2 || m.symbol.isMutableVarOrAccessor => // OK: { val x: T } <: { def x: T } // OK: { var x: T } <: { def x: T } // NO: { var x: T } <: { val x: T } diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 7933cbbea12f..fca810cf0efe 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1588,22 +1588,36 @@ object Parsers { case _ => None } - /** CaptureRef ::= { SimpleRef `.` } SimpleRef [`*`] + /** CaptureRef ::= { SimpleRef `.` } SimpleRef [`*`] [`.` rd] * | [ { SimpleRef `.` } SimpleRef `.` ] id `^` */ def captureRef(): Tree = - val ref = dotSelectors(simpleRef()) - if isIdent(nme.raw.STAR) then - in.nextToken() - atSpan(startOffset(ref)): - PostfixOp(ref, Ident(nme.CC_REACH)) - else if isIdent(nme.UPARROW) then + + def derived(ref: Tree, name: TermName) = in.nextToken() - atSpan(startOffset(ref)): - convertToTypeId(ref) match - case ref: RefTree => makeCapsOf(ref) - case ref => ref - else ref + atSpan(startOffset(ref)) { PostfixOp(ref, Ident(name)) } + + def recur(ref: Tree): Tree = + if in.token == DOT then + in.nextToken() + if in.isIdent(nme.rd) then derived(ref, nme.CC_READONLY) + else recur(selector(ref)) + else if in.isIdent(nme.raw.STAR) then + val reachRef = derived(ref, nme.CC_REACH) + if in.token == DOT && in.lookahead.isIdent(nme.rd) then + in.nextToken() + derived(reachRef, nme.CC_READONLY) + else reachRef + else if isIdent(nme.UPARROW) then + in.nextToken() + atSpan(startOffset(ref)): + convertToTypeId(ref) match + case ref: RefTree => makeCapsOf(ref) + case ref => ref + else ref + + recur(simpleRef()) + end captureRef /** CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` -- under captureChecking */ @@ -3285,13 +3299,14 @@ object Parsers { case SEALED => Mod.Sealed() case IDENTIFIER => name match { - case nme.erased if in.erasedEnabled => Mod.Erased() case nme.inline => Mod.Inline() case nme.opaque => Mod.Opaque() case nme.open => Mod.Open() case nme.transparent => Mod.Transparent() case nme.infix => Mod.Infix() case nme.tracked => Mod.Tracked() + case nme.erased if in.erasedEnabled => Mod.Erased() + case nme.mut if Feature.ccEnabled => Mod.Mut() } } @@ -4672,7 +4687,8 @@ object Parsers { syntaxError(msg, tree.span) Nil tree match - case tree: MemberDef if !(tree.mods.flags & (ModifierFlags &~ Mutable)).isEmpty => + case tree: MemberDef + if !(tree.mods.flags & ModifierFlags).isEmpty && !tree.mods.isMutableVar => // vars are OK, mut defs are not fail(em"refinement cannot be ${(tree.mods.flags & ModifierFlags).flagStrings().mkString("`", "`, `", "`")}") case tree: DefDef if tree.termParamss.nestedExists(!_.rhs.isEmpty) => fail(em"refinement cannot have default arguments") diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 2007b633a7c5..e007e3e689b3 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -1196,7 +1196,10 @@ object Scanners { def isSoftModifier: Boolean = token == IDENTIFIER - && (softModifierNames.contains(name) || name == nme.erased && erasedEnabled || name == nme.tracked && trackedEnabled) + && (softModifierNames.contains(name) + || name == nme.erased && erasedEnabled + || name == nme.tracked && trackedEnabled + || name == nme.mut && Feature.ccEnabled) def isSoftModifierInModifierPosition: Boolean = isSoftModifier && inModifierPosition() diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index bace43b767bd..0f8e81154058 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -167,8 +167,9 @@ class PlainPrinter(_ctx: Context) extends Printer { toTextCaptureRef(ref.typeOpt) case TypeApply(fn, arg :: Nil) if fn.symbol == defn.Caps_capsOf => toTextRetainedElem(arg) - case _ => - toText(ref) + case ReachCapabilityApply(ref1) => toTextRetainedElem(ref1) ~ "*" + case ReadOnlyCapabilityApply(ref1) => toTextRetainedElem(ref1) ~ ".rd" + case _ => toText(ref) private def toTextRetainedElems[T <: Untyped](refs: List[Tree[T]]): Text = "{" ~ Text(refs.map(ref => toTextRetainedElem(ref)), ", ") ~ "}" @@ -177,16 +178,10 @@ class PlainPrinter(_ctx: Context) extends Printer { * capturing function types. */ protected def toTextCapturing(parent: Type, refsText: Text, boxText: Text): Text = - def coreText = boxText ~ toTextLocal(parent) - if parent.derivesFrom(defn.Caps_Capability) - && refsText == impliedByCapabilitySetText - && !printDebug - then coreText - else changePrec(InfixPrec): - coreText~ "^" ~ (refsText provided refsText != rootSetText) + changePrec(InfixPrec): + boxText ~ toTextLocal(parent) ~ "^" ~ (refsText provided refsText != rootSetText) final protected def rootSetText = Str("{cap}") // TODO Use disambiguation - final protected def impliedByCapabilitySetText = Str("{cap}") def toText(tp: Type): Text = controlled { homogenize(tp) match { @@ -195,7 +190,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp: TermRef if !tp.denotationIsCurrent && !homogenizedView // always print underlying when testing picklers - && !tp.isRootCapability + && !tp.isCap || tp.symbol.is(Module) || tp.symbol.name == nme.IMPORT => toTextRef(tp) ~ ".type" @@ -247,9 +242,16 @@ class PlainPrinter(_ctx: Context) extends Printer { }.close case tp @ CapturingType(parent, refs) => val boxText: Text = Str("box ") provided tp.isBoxed //&& ctx.settings.YccDebug.value - val showAsCap = refs.isUniversal && (refs.elems.size == 1 || !printDebug) - val refsText = if showAsCap then rootSetText else toTextCaptureSet(refs) - toTextCapturing(parent, refsText, boxText) + if parent.derivesFrom(defn.Caps_Capability) + && refs.containsRootCapability && refs.isReadOnly && !printDebug + then + toText(parent) + else + val refsText = + if refs.isUniversal && (refs.elems.size == 1 || !printDebug) + then rootSetText + else toTextCaptureSet(refs) + toTextCapturing(parent, refsText, boxText) case tp @ RetainingType(parent, refs) => if Feature.ccEnabledSomewhere then val refsText = refs match @@ -425,6 +427,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp: TermRef if tp.symbol == defn.captureRoot => Str("cap") case tp: SingletonType => toTextRef(tp) case tp: (TypeRef | TypeParamRef) => toText(tp) ~ "^" + case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd" case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" case tp => toText(tp) @@ -541,7 +544,7 @@ class PlainPrinter(_ctx: Context) extends Printer { else if sym.is(Param) then "parameter" else if sym.is(Given) then "given instance" else if (flags.is(Lazy)) "lazy value" - else if (flags.is(Mutable)) "variable" + else if (sym.isMutableVar) "variable" else if (sym.isClassConstructor && sym.isPrimaryConstructor) "primary constructor" else if (sym.isClassConstructor) "constructor" else if (sym.is(Method)) "method" @@ -557,7 +560,7 @@ class PlainPrinter(_ctx: Context) extends Printer { else if (flags.is(Module)) "object" else if (sym.isClass) "class" else if (sym.isType) "type" - else if (flags.is(Mutable)) "var" + else if (sym.isMutableVarOrAccessor) "var" else if (flags.is(Package)) "package" else if (sym.is(Method)) "def" else if (sym.isTerm && !flags.is(Param)) "val" diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index b7f2eef8c8f9..071d8fc94cd6 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -336,7 +336,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { "?" ~ (("(ignored: " ~ toText(ignored) ~ ")") provided printDebug) case tp @ PolyProto(targs, resType) => "[applied to [" ~ toTextGlobal(targs, ", ") ~ "] returning " ~ toText(resType) - case ReachCapability(_) | MaybeCapability(_) => + case ReachCapability(_) | MaybeCapability(_) | ReadOnlyCapability(_) => toTextCaptureRef(tp) case _ => super.toText(tp) @@ -742,6 +742,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case PostfixOp(l, op) => if op.name == nme.CC_REACH then changePrec(DotPrec) { toText(l) ~ "*" } + else if op.name == nme.CC_READONLY then + changePrec(DotPrec) { toText(l) ~ ".rd" } else changePrec(InfixPrec) { toText(l) ~ " " ~ toText(op) } case PrefixOp(op, r) => diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 692b87f68821..8d688891fa47 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -1694,7 +1694,7 @@ class OnlyClassesCanHaveDeclaredButUndefinedMembers(sym: Symbol)( def msg(using Context) = i"""Declaration of $sym not allowed here: only classes can have declared but undefined members""" def explain(using Context) = - if sym.is(Mutable) then "Note that variables need to be initialized to be defined." + if sym.isMutableVarOrAccessor then "Note that variables need to be initialized to be defined." else "" } diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index 75e859111932..5dd69ebc3386 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -418,7 +418,7 @@ private class ExtractAPICollector(nonLocalClassSymbols: mutable.HashSet[Symbol]) apiClass(sym.asClass) } else if (sym.isType) { apiTypeMember(sym.asType) - } else if (sym.is(Mutable, butNot = Accessor)) { + } else if (sym.isMutableVar) { api.Var.of(sym.name.toString, apiAccess(sym), apiModifiers(sym), apiAnnotations(sym, inlineOrigin).toArray, apiType(sym.info)) } else if (sym.isStableMember && !sym.isRealMethod) { diff --git a/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala b/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala index c1725cbd0255..7263bce0478c 100644 --- a/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala +++ b/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala @@ -120,7 +120,7 @@ object CapturedVars: def traverse(tree: Tree)(using Context) = tree match case id: Ident => val sym = id.symbol - if sym.is(Mutable, butNot = Method) && sym.owner.isTerm then + if sym.isMutableVar && sym.owner.isTerm then val enclMeth = ctx.owner.enclosingMethod if sym.enclosingMethod != enclMeth then report.log(i"capturing $sym in ${sym.enclosingMethod}, referenced from $enclMeth") diff --git a/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala b/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala index e8a402068bfc..5f52ac82879a 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala @@ -65,7 +65,7 @@ class CheckReentrant extends MiniPhase { scanning(cls) { for (sym <- cls.classInfo.decls) if (sym.isTerm && !sym.isSetter && !isIgnored(sym)) - if (sym.is(Mutable)) { + if (sym.isMutableVarOrAccessor) { report.error( em"""possible data race involving globally reachable ${sym.showLocated}: ${sym.info} | use -Ylog:checkReentrant+ to find out more about why the variable is reachable.""") diff --git a/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala b/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala index 6c74f302b65d..957fd78e9c2c 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala @@ -52,7 +52,7 @@ class CheckStatic extends MiniPhase { report.error(MissingCompanionForStatic(defn.symbol), defn.srcPos) else if (clashes.exists) report.error(MemberWithSameNameAsStatic(), defn.srcPos) - else if (defn.symbol.is(Flags.Mutable) && companion.is(Flags.Trait)) + else if (defn.symbol.isMutableVarOrAccessor && companion.is(Flags.Trait)) report.error(TraitCompanionWithMutableStatic(), defn.srcPos) else if (defn.symbol.is(Flags.Lazy)) report.error(LazyStaticField(), defn.srcPos) diff --git a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala index d647d50560d3..818fb3fc029d 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala @@ -803,7 +803,7 @@ object CheckUnused: private def isUnsetVarDef(using Context): Boolean = val sym = memDef.symbol - sym.is(Mutable) && !setVars(sym) + sym.isMutableVarOrAccessor && !setVars(sym) extension (imp: tpd.Import) /** Enum generate an import for its cases (but outside them), which should be ignored */ diff --git a/compiler/src/dotty/tools/dotc/transform/Constructors.scala b/compiler/src/dotty/tools/dotc/transform/Constructors.scala index 9a0df830c6d7..b373565489f0 100644 --- a/compiler/src/dotty/tools/dotc/transform/Constructors.scala +++ b/compiler/src/dotty/tools/dotc/transform/Constructors.scala @@ -155,7 +155,7 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase = case Ident(_) | Select(This(_), _) => var sym = tree.symbol def isOverridableSelect = tree.isInstanceOf[Select] && !sym.isEffectivelyFinal - def switchOutsideSupercall = !sym.is(Mutable) && !isOverridableSelect + def switchOutsideSupercall = !sym.isMutableVarOrAccessor && !isOverridableSelect // If true, switch to constructor parameters also in the constructor body // that follows the super call. // Variables need to go through the getter since they might have been updated. diff --git a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala index e2712a7d6302..2fd777f715d9 100644 --- a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala +++ b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala @@ -255,7 +255,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { def transformMemberDefThreadUnsafe(x: ValOrDefDef)(using Context): Thicket = { val claz = x.symbol.owner.asClass val tpe = x.tpe.widen.resultType.widen - assert(!(x.symbol is Mutable)) + assert(!x.symbol.isMutableVarOrAccessor) val containerName = LazyLocalName.fresh(x.name.asTermName) val containerSymbol = newSymbol(claz, containerName, x.symbol.flags &~ containerFlagsMask | containerFlags | Private, @@ -447,7 +447,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { } def transformMemberDefThreadSafe(x: ValOrDefDef)(using Context): Thicket = { - assert(!(x.symbol is Mutable)) + assert(!x.symbol.isMutableVarOrAccessor) if ctx.settings.YlegacyLazyVals.value then transformMemberDefThreadSafeLegacy(x) else diff --git a/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala b/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala index 95975ad9e6b8..b3ec05501b5b 100644 --- a/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala +++ b/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala @@ -28,7 +28,7 @@ class MoveStatics extends MiniPhase with SymTransformer { def transformSym(sym: SymDenotation)(using Context): SymDenotation = if (sym.hasAnnotation(defn.ScalaStaticAnnot) && sym.owner.is(Flags.Module) && sym.owner.companionClass.exists && - (sym.is(Flags.Method) || !(sym.is(Flags.Mutable) && sym.owner.companionClass.is(Flags.Trait)))) { + (sym.is(Flags.Method) || !(sym.isMutableVarOrAccessor && sym.owner.companionClass.is(Flags.Trait)))) { sym.owner.asClass.delete(sym.symbol) sym.owner.companionClass.asClass.enter(sym.symbol) sym.copySymDenotation(owner = sym.owner.companionClass) diff --git a/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala b/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala index f22fc53e9b6e..7531b6e41c19 100644 --- a/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala +++ b/compiler/src/dotty/tools/dotc/transform/UninitializedDefs.scala @@ -33,7 +33,7 @@ class UninitializedDefs extends MiniPhase: def recur(rhs: Tree): Boolean = rhs match case rhs: RefTree => rhs.symbol == defn.Compiletime_uninitialized - && tree.symbol.is(Mutable) && tree.symbol.owner.isClass + && tree.symbol.isMutableVarOrAccessor && tree.symbol.owner.isClass case closureDef(ddef) if defn.isContextFunctionType(tree.tpt.tpe.dealias) => recur(ddef.rhs) case _ => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 52760cf8b6c7..115037d1930a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -829,7 +829,7 @@ class Objects(using Context @constructorOnly): Bottom else if target.exists then def isNextFieldOfColonColon: Boolean = ref.klass == defn.ConsClass && target.name.toString == "next" - if target.isOneOf(Flags.Mutable) && !isNextFieldOfColonColon then + if target.isMutableVarOrAccessor && !isNextFieldOfColonColon then if ref.hasVar(target) then val addr = ref.varAddr(target) if addr.owner == State.currentObject then diff --git a/compiler/src/dotty/tools/dotc/transform/init/Util.scala b/compiler/src/dotty/tools/dotc/transform/init/Util.scala index e11d0e1e21a5..ca30e2d32a4d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Util.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Util.scala @@ -112,5 +112,5 @@ object Util: /** Whether the class or its super class/trait contains any mutable fields? */ def isMutable(cls: ClassSymbol)(using Context): Boolean = - cls.classInfo.decls.exists(_.is(Flags.Mutable)) || + cls.classInfo.decls.exists(_.isMutableVarOrAccessor) || cls.parentSyms.exists(parentCls => isMutable(parentCls.asClass)) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index e870ffd0fc90..ca12b4f64ff7 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -37,7 +37,7 @@ import config.Feature, Feature.{sourceVersion, modularity} import config.SourceVersion.* import config.MigrationVersion import printing.Formatting.hlAsKeyword -import cc.{isCaptureChecking, isRetainsLike} +import cc.{isCaptureChecking, isRetainsLike, isUpdateMethod} import collection.mutable import reporting.* @@ -578,7 +578,7 @@ object Checking { if (sym.isConstructor && !sym.isPrimaryConstructor && sym.owner.is(Trait, butNot = JavaDefined)) val addendum = if ctx.settings.Ydebug.value then s" ${sym.owner.flagsString}" else "" fail(em"Traits cannot have secondary constructors$addendum") - checkApplicable(Inline, sym.isTerm && !sym.isOneOf(Mutable | Module)) + checkApplicable(Inline, sym.isTerm && !sym.is(Module) && !sym.isMutableVarOrAccessor) checkApplicable(Lazy, !sym.isOneOf(Method | Mutable)) if (sym.isType && !sym.isOneOf(Deferred | JavaDefined)) for (cls <- sym.allOverriddenSymbols.filter(_.isClass)) { @@ -587,8 +587,12 @@ object Checking { } if sym.isWrappedToplevelDef && !sym.isType && sym.flags.is(Infix, butNot = Extension) then fail(ModifierNotAllowedForDefinition(Flags.Infix, s"A top-level ${sym.showKind} cannot be infix.")) + if sym.isUpdateMethod && !sym.owner.derivesFrom(defn.Caps_Mutable) then + fail(em"Update methods can only be used as members of classes deriving from the `Mutable` trait") checkApplicable(Erased, - !sym.isOneOf(MutableOrLazy, butNot = Given) && !sym.isType || sym.isClass) + !sym.is(Lazy, butNot = Given) + && !sym.isMutableVarOrAccessor + && (!sym.isType || sym.isClass)) checkCombination(Final, Open) checkCombination(Sealed, Open) checkCombination(Final, Sealed) diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index 13e75be75838..58119981dfc4 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -85,7 +85,7 @@ object ErrorReporting { /** An explanatory note to be added to error messages * when there's a problem with abstract var defs */ def abstractVarMessage(sym: Symbol): String = - if (sym.underlyingSymbol.is(Mutable)) + if sym.underlyingSymbol.isMutableVarOrAccessor then "\n(Note that variables need to be initialized to be defined)" else "" diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 310ca999f4c5..86b9a337e69a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -253,7 +253,7 @@ object Nullables: val mutables = infos.foldLeft(Set[TermRef]()): (ms, info) => ms.union( if info.asserted == null then Set.empty - else info.asserted.filter(_.symbol.is(Mutable))) + else info.asserted.filter(_.symbol.isMutableVarOrAccessor)) infos.extendWith(NotNullInfo(Set(), mutables)) end extension @@ -307,7 +307,7 @@ object Nullables: || s.isClass // not in a class || recur(s.owner)) - refSym.is(Mutable) // if it is immutable, we don't need to check the rest conditions + refSym.isMutableVarOrAccessor // if it is immutable, we don't need to check the rest conditions && refOwner.isTerm && recur(ctx.owner) end extension @@ -574,7 +574,7 @@ object Nullables: object dropNotNull extends TreeMap: var dropped: Boolean = false override def transform(t: Tree)(using Context) = t match - case AssertNotNull(t0) if t0.symbol.is(Mutable) => + case AssertNotNull(t0) if t0.symbol.isMutableVarOrAccessor => nullables.println(i"dropping $t") dropped = true transform(t0) diff --git a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala index 59993a69797d..4e7c4336b852 100644 --- a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala +++ b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala @@ -130,7 +130,7 @@ trait QuotesAndSplices { report.error("Open pattern expected an identifier", arg.srcPos) EmptyTree } - for arg <- typedArgs if arg.symbol.is(Mutable) do // TODO support these patterns. Possibly using scala.quoted.util.Var + for arg <- typedArgs if arg.symbol.isMutableVarOrAccessor do // TODO support these patterns. Possibly using scala.quoted.util.Var report.error("References to `var`s cannot be used in higher-order pattern", arg.srcPos) val argTypes = typedArgs.map(_.tpe.widenTermRefExpr) val patType = (tree.typeargs.isEmpty, tree.args.isEmpty) match diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index dcdabaf3a72d..46806cc85a1c 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -21,7 +21,7 @@ import config.MigrationVersion import config.Printers.refcheck import reporting.* import Constants.Constant -import cc.stripCapturing +import cc.{stripCapturing, isUpdateMethod} object RefChecks { import tpd.* @@ -594,7 +594,7 @@ object RefChecks { overrideError("needs `override` modifier") else if (other.is(AbsOverride) && other.isIncompleteIn(clazz) && !member.is(AbsOverride)) overrideError("needs `abstract override` modifiers") - else if member.is(Override) && other.is(Mutable) then + else if member.is(Override) && other.isMutableVarOrAccessor then overrideError("cannot override a mutable variable") else if (member.isAnyOverride && !(member.owner.thisType.baseClasses exists (_ isSubClass other.owner)) && @@ -615,6 +615,8 @@ object RefChecks { overrideError("is erased, cannot override non-erased member") else if (other.is(Erased) && !member.isOneOf(Erased | Inline)) // (1.9) overrideError("is not erased, cannot override erased member") + else if member.isUpdateMethod && !other.is(Mutable) then + overrideError(i"is an update method, cannot override a read-only method") else if other.is(Inline) && !member.is(Inline) then // (1.10) overrideError("is not inline, cannot implement an inline method") else if (other.isScala2Macro && !member.isScala2Macro) // (1.11) @@ -772,7 +774,7 @@ object RefChecks { // Give a specific error message for abstract vars based on why it fails: // It could be unimplemented, have only one accessor, or be uninitialized. - if (underlying.is(Mutable)) { + if underlying.isMutableVarOrAccessor then val isMultiple = grouped.getOrElse(underlying.name, Nil).size > 1 // If both getter and setter are missing, squelch the setter error. @@ -781,7 +783,6 @@ object RefChecks { if (member.isSetter) "\n(Note that an abstract var requires a setter in addition to the getter)" else if (member.isGetter && !isMultiple) "\n(Note that an abstract var requires a getter in addition to the setter)" else err.abstractVarMessage(member)) - } else if (underlying.is(Method)) { // If there is a concrete method whose name matches the unimplemented // abstract method, and a cursory examination of the difference reveals diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 76b853c4aabd..2f98ac5b2d93 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1348,7 +1348,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer cpy.Assign(tree)(lhsCore, typed(tree.rhs, lhs1.tpe.widen)).withType(defn.UnitType) def canAssign(sym: Symbol) = - sym.is(Mutable, butNot = Accessor) || + sym.isMutableVar || ctx.owner.isPrimaryConstructor && !sym.is(Method) && sym.maybeOwner == ctx.owner.owner || // allow assignments from the primary constructor to class fields ctx.owner.name.is(TraitSetterName) || ctx.owner.isStaticConstructor diff --git a/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala b/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala index 3699ca80d011..0c2929283ee3 100644 --- a/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala +++ b/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala @@ -157,7 +157,7 @@ class VarianceChecker(using Context) { def isLocal = base.isAllOf(PrivateLocal) || base.is(Private) && !base.hasAnnotation(defn.AssignedNonLocallyAnnot) - if base.is(Mutable, butNot = Method) && !isLocal then + if base.isMutableVar && !isLocal then base.removeAnnotation(defn.AssignedNonLocallyAnnot) variance = 0 try checkInfo(base.info) diff --git a/docs/_docs/internals/exclusive-capabilities.md b/docs/_docs/internals/exclusive-capabilities.md new file mode 100644 index 000000000000..97c6592ac693 --- /dev/null +++ b/docs/_docs/internals/exclusive-capabilities.md @@ -0,0 +1,551 @@ +# Exclusive Capabilities + +Language design draft + + +## Capability Kinds + +A capability is called + - _exclusive_ if it is `cap` or it has an exclusive capability in its capture set. + - _shared_ otherwise. + +There is a new top capability `shared` which can be used as a capability for deriving shared capture sets. Other shared capabilities are created as read-only versions of exclusive capabilities. + +## Update Methods + +We introduce a new trait +```scala +trait Mutable +``` +It is used as a base trait for types that define _update methods_ using +a new modifier `mut`. + +`mut` can only be used in classes or objects extending `Mutable`. An update method is allowed to access exclusive capabilities in the method's environment. By contrast, a normal method in a type extending `Mutable` may access exclusive capabilities only if they are defined locally or passed to it in parameters. + +**Example:** +```scala +class Ref(init: Int) extends Mutable: + private var current = init + def get: Int = current + mut def put(x: Int): Unit = current = x +``` +Here, `put` needs to be declared as an update method since it accesses the exclusive write capability of the variable `current` in its environment. +`mut` can also be used on an inner class of a class or object extending `Mutable`. It gives all code in the class the right +to access exclusive capabilities in the class environment. Normal classes +can only access exclusive capabilities defined in the class or passed to it in parameters. + +```scala +object Registry extends Mutable: + var count = 0 + mut class Counter: + mut def next: Int = + count += 1 + count +``` +Normal method members of `Mutable` classes cannot call update methods. This is indicated since accesses in the callee are recorded in the caller. So if the callee captures exclusive capabilities so does the caller. + +An update method cannot implement or override a normal method, whereas normal methods may implement or override update methods. Since methods such as `toString` or `==` inherited from Object are normal methods, it follows that none of these methods may be implemented as an update method. + +The `apply` method of a function type is also a normal method, hence `Mutable` classes may not implement a function type with an update method as the `apply` method. + +## Mutable Types + +A type is called a _mutable_ if it extends `Mutable` and it has an update method or an update class as non-private member or constructor. + +When we create an instance of a mutable type we always add `cap` to its capture set. For instance, if class `Ref` is declared as shown previously then `new Ref(1)` has type `Ref[Int]^{cap}`. + +**Restriction:** A non-mutable type cannot be downcast by a pattern match to a mutable type. + +**Definition:** A class is _read_only_ if the following conditions are met: + + 1. It does not extend any exclusive capabilities from its environment. + 2. It does not take parameters with exclusive capabilities. + 3. It does not contain mutable fields, or fields that take exclusive capabilities. + +**Restriction:** If a class or trait extends `Mutable` all its parent classes or traits must either extend `Mutable` or be read-only. + +The idea is that when we upcast a reference to a type extending `Mutable` to a type that does not extend `Mutable`, we cannot possibly call a method on this reference that uses an exclusive capability. Indeed, by the previous restriction this class must be a read-only class, which means that none of the code implemented +in the class can access exclusive capabilities on its own. And we +also cannot override any of the methods of this class with a method +accessing exclusive capabilities, since such a method would have +to be an update method and update methods are not allowed to override regular methods. + + + +**Example:** + +Consider trait `IterableOnce` from the standard library. + +```scala +trait IterableOnce[+T] extends Mutable: + def iterator: Iterator[T]^{this} + mut def foreach(op: T => Unit): Unit + mut def exists(op: T => Boolean): Boolean + ... +``` +The trait is a mutable type with many update methods, among them `foreach` and `exists`. These need to be classified as `mut` because their implementation in the subtrait `Iterator` uses the update method `next`. +```scala +trait Iterator[T] extends IterableOnce[T]: + def iterator = this + def hasNext: Boolean + mut def next(): T + mut def foreach(op: T => Unit): Unit = ... + mut def exists(op; T => Boolean): Boolean = ... + ... +``` +But there are other implementations of `IterableOnce` that are not mutable types (even though they do indirectly extend the `Mutable` trait). Notably, collection classes implement `IterableOnce` by creating a fresh +`iterator` each time one is required. The mutation via `next()` is then restricted to the state of that iterator, whereas the underlying collection is unaffected. These implementations would implement each `mut` method in `IterableOnce` by a normal method without the `mut` modifier. + +```scala +trait Iterable[T] extends IterableOnce[T]: + def iterator = new Iterator[T] { ... } + def foreach(op: T => Unit) = iterator.foreach(op) + def exists(op: T => Boolean) = iterator.exists(op) +``` +Here, `Iterable` is not a mutable type since it has no update method as member. +All inherited update methods are (re-)implemented by normal methods. + +**Note:** One might think that we don't need a base trait `Mutable` since in any case +a mutable type is defined by the presence of update methods, not by what it extends. In fact the importance of `Mutable` is that it defines _the other methods_ as read-only methods that _cannot_ access exclusive capabilities. For types not extending `Mutable`, this is not the case. For instance, the `apply` method of a function type is not an update method and the type itself does not extend `Mutable`. But `apply` may well be implemented by +a method that accesses exclusive capabilities. + + + +## Read-only Capabilities + +If `x` is an exclusive capability of a type extending `Mutable`, `x.rd` is its associated, shared _read-only_ capability. + +`shared` can be understood as the read-only capability corresponding to `cap`. +```scala + shared = cap.rd +``` + +A _top capability_ is either `cap` or `shared`. + + +## Shorthands + +**Meaning of `^`:** + +The meaning of `^` and `=>` is the same as before: + + - `C^` means `C^{cap}`. + - `A => B` means `(A -> B)^{cap}`. + +**Implicitly added capture sets** + +A reference to a type extending any of the traits `Capability` or `Mutable` gets an implicit capture set `{shared}` in case no explicit capture set is given. + +For instance, a matrix multiplication method can be expressed as follows: + +```scala +class Matrix(nrows: Int, ncols: Int) extends Mutable: + mut def update(i: Int, j: Int, x: Double): Unit = ... + def apply(i: Int, j: Int): Double = ... + +def mul(a: Matrix, b: Matrix, c: Matrix^): Unit = + // multiply a and b, storing the result in c +``` +Here, `a` and `b` are implicitly read-only, and `c`'s type has capture set `cap`. I.e. with explicit capture sets this would read: +```scala +def mul(a: Matrix^{shared}, b: Matrix^{shared}, c: Matrix^{cap}): Unit +``` +Separation checking will then make sure that `a` and `b` must be different from `c`. + + +## Capture Sets + +As the previous example showed, we would like to use a `Mutable` type such as `Array` or `Matrix` in two permission levels: read-only and unrestricted. A standard technique is to invent a type qualifier such as "read-only" or "mutable" to indicate access permissions. What we would like to do instead is to combine the qualifier with the capture set of a type. So we +distinguish two kinds of capture sets: regular and read-only. Read-only sets can contain only shared capabilities. + +Internally, in the discussion that follows we use a label after the set to indicate its mode. `{...}_` is regular and `{...}rd` is read-only. We could envisage source language to specify read-only sets, e.g. something like + +```scala +{io, async}.rd +``` + +But in almost all cases we don't need an explicit mode in source code to indicate the kind of capture set, since the contents of the set itself tell us what kind it is. A capture set is assumed to be read-only if it is on a +type extending `Mutable` and it contains only shared capabilities, otherwise it is assumed to be regular. + +The read-only function `ro` maps capture sets to read-only capture sets. It is defined pointwise on capabilities as follows: + + - `ro ({ x1, ..., xn } _) = { ro(x1), ..., ro(xn) }` + - `ro(x) = x` if `x` is shared + - `ro(x) = x.rd` if `x` is exclusive + + + +## Subcapturing + +Subcapturing has to take the mode of capture sets into account. We let `m` stand for arbitrary modes. + +1. Rule (sc-var) comes in two variants. If `x` is defined as `S^C` then + + - `{x, xs} m <: (C u {xs}) m` + - `{x.rd, xs} m <: (ro(C) u {xs}) m` + +3. The subset rule works only between sets of the same kind: + + - `C _ <: C _ u {x}` + - `C rd <: C rd u {x}` if `x` is a shared capability. + +4. We can map regular capture sets to read-only sets: + + - `C _ <: ro(C) rd` + +5. Read-only capabilities in regular capture sets can be widened to exclusive capabilities: + + - `{x.rd, xs} _ <: {x, xs} _` + +One case where an explicit capture set mode would be useful concerns +refinements of type variable bounds, as in the following example. +```scala +class A: + type T <: Object^{x.rd, y} +class B extends A: + type T <: Object^{x.rd} +class C extends B: + type T = Matrix^{x.rd} +``` +We assume that `x` and `y` are exclusive capabilities. +The capture set of type `T` in class `C` is a read-only set since `Matrix` extends `Mutable`. But the capture sets of the occurrences of +`T` in `A` and `B` are regular. This leads to an error in bounds checking +the definition of `T` in `C` against the one in `B` +since read-only sets do not subcapture regular sets. We can fix the +problem by declaring the capture set in class `B` as read-only: +```scala +class B extends A: + type T <: Object^{x.rd}.rd +``` +But now a different problem arises since the capture set of `T` in `B` is +read-only but the capture set of `T` and `A` is regular. The capture set of +`T` in `A` cannot be made read-only since it contains an exclusive capability `y`. So we'd have to drop `y` and declare class `A` like this: +```scala +class A: + type T <: Object^{x.rd}.rd +``` + + + +## Accesses to Mutable Types + +A _read-only access_ is a reference `x` to a type extending `Mutable` with a regular capture set if the expected type is one of the following: + + - a value type that is not a mutable type, or + - a select prototype with a member that is a normal method or class (not an update method or class). + +A read-only access contributes the read-only capability `x.rd` to its environment (as formalized by _cv_). Other accesses contribute the full capability `x`. + +A reference `p.m` to an update method or class `m` of a mutable type is allowed only if `p`'s capture set is regular. + +If `e` is an expression of a type `T^cs` extending `Mutable` and the expected type is a value type that is not a mutable type, then the type of `e` is mapped to `T^ro(cs)`. + + +## Expression Typing + +An expression's type should never contain a top capability in its deep capture set. This is achieved by the following rules: + + - On var access `x`: + + - replace all direct capture sets with `x` + - replace all boxed caps with `x*` + + _Variant_: If the type of the typevar corresponding to a boxed cap can be uniquely reached by a path `this.p`, replace the `cap` with `x.p*`. + + - On select `t.foo` where `C` is the capture set of `t`: apply the SELECT rule, which amounts to: + + - replace all direct caps with `C` + - replace all boxed caps with `C*` + + - On applications: `t(args)`, `new C(args)` if the result type `T` contains `cap` (deeply): + + - create a fresh skolem `val sk: T` + - set result type to `sk.type` + + Skolem symbols are eliminated before they reach the type of the enclosing val or def. + + - When avoiding a variable in a local block, as in: + ```scala + { val x: T^ = ...; ... r: List[T^{x}] } + ``` + where the capture set of `x` contains a top capability, + replace `x` by a fresh skolem `val sk: T`. Alternatively: keep it as is, but don't widen it. + + +## Post Processing Right Hand Sides + +The type of the right hand sides of `val`s or `def`s is post-processed before it becomes the inferred type or is compared with the declared type. Post processing +means that all local skolems in the type are avoided, which might mean `cap` can now occur in the the type. + +However, if a local skolem `sk` has `cap` as underlying type, but is only used +in its read-only form `sk.rd` in the result type, we can drop the skolem instead of widening to `shared`. + +**Example:** + +```scala + def f(x: Int): Double = ... + + def precomputed(n: Int)(f: Int -> Double): Int -> Double = + val a: Array[Double]^ = Array.tabulate(n)(f) + a(_) +``` +Here, `Array.tabulate(n)(f)` returns a value of type `Array[Double]^{cap}`. +The last expression `a(_)` expands to the closure `idx => a(idx)`, which +has type `Int ->{a.rd} Double`, since `a` appears only in the context of a +selection with the `apply` method of `Array`, which is not an update method. The type of the enclosing block then has type `Int ->{sk.rd} Double` for a fresh skolem `sk`, +since `a` is no longer visible. After post processing, this type becomes +`Int -> Double`. + +This pattern allows to use mutation in the construction of a local data structure, returning a pure result when the construction is done. Such +data structures are said to have _transient mutability_. + +## Separation checking + +Separation checking checks that we don't have hidden aliases. A hidden alias arises when we have two definitions `x` and `y` with overlapping transitive capture sets that are not manifest in the types of `x` and `y` because one of these types has widened the alias to a top capability. + +Since expression types can't mention cap, widening happens only + - when passing an argument to a parameter + - when widening to a declared (result) type of a val or def + +**Definitions:** + + - The _transitive capture set_ `tcs(c)` of a capability `c` with underlying capture set `C` is `c` itself, plus the transitive capture set of `C`, but excluding `cap` or `shared`. + + - The _transitive capture set_ `tcs(C)` of a capture set C is the union + of `tcs(c)` for all elements `c` of `C`. + + - Two capture sets _interfere_ if one contains an exclusive capability `x` and the other + also contains `x` or contains the read-only capability `x.rd`. + + - If `C1 <: C2` and `C2` contains a top capability, then let `C2a` be `C2` without top capabilities. The hidden set `hidden(C1, C2)` of `C1` relative to `C2` is the smallest subset `C1h` of `C1` such that `C1 \ C1h <: C2a`. + + - If `T1 <: T2` then let the hidden set `hidden(T1, T2)` of `T1` relative to `T2` be the + union of all hidden sets of corresponding capture sets in `T1` and `T2`. + + +**Algorithm outline:** + + - Associate _shadowed sets_ with blocks, template statement sequences, applications, and val symbols. The idea is that a shadowed set gets populated when a capture reference is widened to cap. In that case the original references that were widened get added to the set. + + - After processing a `val x: T2 = t` with `t: T1` after post-processing: + + - If `T2` is declared, add `tcs(hidden(T1, T2))` to the shadowed set + of the enclosing statement sequence and remember it as `shadowed(x)`. + - If`T2` is inferred, add `tcs(T1)` to the shadowed set + of the enclosing statement sequence and remember it as `shadowed(x)`. + + - When processing the right hand side of a `def f(params): T2 = t` with `t: T1` after post-processing + + - If `T2` is declared, check that `shadowed*(hidden(T1, T2))` contains only local values (including skolems). + - If `T2` is inferred, check that `shadowed*(tcs(T1))` contains only local values (including skolems). + + Here, `shadowed*` is the transitive closure of `shadowed`. + + - When processing an application `p.f(arg1, ..., arg_n)`, after processing `p`, add its transitive capture set to the shadowed set of the call. Then, in sequence, process each argument by adding `tcs(hidden(T1, T2))` to the shadowed set of the call, where `T1` is the argument type and `T2` is the type of the formal parameter. + + - When adding a reference `r` or capture set `C` in `markFree` to enclosing environments, check that `tcs(r)` (respectively, `tcs(C)`) does not interfere with an enclosing shadowed set. + + +This requires, first, a linear processing of the program in evaluation order, and, second, that all capture sets are known. Normal rechecking violates both of these requirements. First, definitions +without declared result types are lazily rechecked using completers. Second, capture sets are constructed +incrementally. So we probably need a second scan after rechecking proper. In order not to duplicate work, we need to record during rechecking all additions to environments via `markFree`. + +**Notes:** + + - Mutable variables are not allowed to have top capabilities in their deep capture sets, so separation checking is not needed for checking var definitions or assignments. + + - A lazy val can be thought of conceptually as a value with possibly a capturing type and as a method computing that value. A reference to a lazy val is interpreted as a call to that method. It's use set is the reference to the lazy val itself as well as the use set of the called method. + + - + +## Escape Checking + +The rules for separation checking also check that capabilities do not escape. Separate +rules for explicitly preventing cap to be boxed or unboxed are not needed anymore. Consider the canonical `withFile` example: +```scala +def withFile[T](body: File^ => T): T = + ... + +withFile: f => + () => f.write("too late") +``` +Here, the argument to `withFile` has the dependent function type +```scala +(f: File^) -> () ->{f} Unit +``` +A non-dependent type is required so the expected result type of the closure is +``` +() ->{cap} Unit +``` +When typing a closure, we type an anonymous function. The result type of that function is determined by type inference. That means the generated closure looks like this +```scala +{ def $anon(f: File^): () ->{cap} Unit = + () => f.write("too late") + $anon +} +``` +By the rules of separation checking the hidden set of the body of $anon is `f`, which refers +to a value outside the rhs of `$anon`. This is illegal according to separation checking. + +In the last example, `f: File^` was an exclusive capability. But it could equally have been a shared capability, i.e. `withFile` could be formulated as follows: +```scala +def withFile[T](body: File^{shared} => T): T = +``` +The same reasoning as before would enforce that there are no leaks. + + +## Mutable Variables + +Local mutable variables are tracked by default. It is essentially as if a mutable variable `x` was decomposed into a new private field of class `Ref` together with a getter and setter. I.e. instead of +```scala +var x: T = init +``` +we'd deal with +```scala +val x$ = Ref[T](init) +def x = x$.get +mut def x_=(y: T) = x$.put(y) +``` + +There should be a way to exclude a mutable variable or field from tracking. Maybe an annotation or modifier such as `transparent` or `untracked`? + +The expansion outlined above justifies the following rules for handling mutable variables directly: + + - A type with non-private tracked mutable fields is classified as mutable. + It has to extend the `Mutable` class. + - A read access to a local mutable variable `x` charges the capability `x.rd` to the environment. + - An assignment to a local mutable variable `x` charges the capability `x` to the environment. + - A read access to a mutable field `this.x` charges the capability `this.rd` to the environment. + - A write access to a mutable field `this.x` charges the capability `this` to the environment. + +Mutable Scopes +============== + +We sometimes want to make separation checking coarser. For instance when constructing a doubly linked list we want to create `Mutable` objects and +store them in mutable variables. Since a variable's type cannot contain `cap`, +we must know beforehand what mutable objects it can be refer to. This is impossible if the other objects are created later. + +Mutable scopes provide a solution to this they permit to derive a set of variables from a common exclusive reference. We define a new class +```scala +class MutableScope extends Mutable +``` +To make mutable scopes useful, we need a small tweak +of the rule governing `new` in the _Mutable Types_ section. The previous rule was: + +> When we create an instance of a mutable type we always add `cap` to its capture set. + +The new rule is: + +> When we create an instance of a mutable type we search for a given value of type `MutableScope`. If such a value is found (say it is `ms`) then we use +`ms` as the capture set of the created instance. Otherwise we use `cap`. + +We could envisage using mutable scopes like this: +``` +object enclave: + private given ms: MutableScope() + + ... +``` +Within `enclave` all mutable objects have `ms` as their capture set. So they can contain variables that also have `ms` as their capture set of their values. + +Mutable scopes should count as mutable types (this can be done either by decree or by adding an update method to `MutableScope`). Hence, mutable scopes can themselves be nested inside other mutable scopes. + +## Consumed Capabilities + +We allow `consume` as a modifier on parameters and methods. Example: + +```scala +class C extends Capability + +class Channel[T]: + def send(consume x: T) + + + +class Buffer[+T] extends Mutable: + consume def append(x: T): Buffer[T]^ + +b.append(x) +b1.append(y) + +def concat[T](consume buf1: Buffer[T]^, buf2: Buffer[T]): Buffer[T]^ + +A ->{x.consume} B + + +A + + C , Gamma, x: S |- t; T + --------------------------- + , Gamma |- (x -> t): S ->C T + + + C, Gamma |- let x = s in t: T + + +class Iterator[T]: + consume def filter(p: T => Boolean): Iterator[T]^ + consume def exists(p: T => Boolean): Boolean +``` + +As a parameter, `consume` implies `^` as capture set of the parameter type. The `^` can be given, but is redundant. + +When a method with a `consume` parameter of type `T2^` is called with an argument of type `T1`, we add the elements of `tcs(hidden(T1, T2^))` not just to the enclosing shadowed set but to all enclosing shadowed sets where elements are visible. This makes these elements permanently inaccessible. + + + +val f = Future { ... } +val g = Future { ... } + + +A parameter is implicitly @unbox if it contains a boxed cap. Example: + +def apply[T](f: Box[T => T], y: T): T = + xs.head(y) + +def compose[T](fs: @unbox List[T => T]) = + xs.foldRight(identity)((f: T => T, g: T => T) => x => g(f(x))) + + + +compose(List(f, g)) + +f :: g :: Nil + +def compose[T](fs: List[Unbox[T => T]], x: T) = + val combined = (xs.foldRight(identity)((f: T => T, g: T => T) => x => g(f(x)))): T->{fs*} T + combined(x) + + +With explicit diff --git a/library/src/scala/annotation/internal/readOnlyCapability.scala b/library/src/scala/annotation/internal/readOnlyCapability.scala new file mode 100644 index 000000000000..8e939aea6bb9 --- /dev/null +++ b/library/src/scala/annotation/internal/readOnlyCapability.scala @@ -0,0 +1,7 @@ +package scala.annotation +package internal + +/** An annotation that marks a capture ref as a read-only capability. + * `x.rd` is encoded as `x.type @readOnlyCapability` + */ +class readOnlyCapability extends StaticAnnotation diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index c35b3b55e813..fb4bacd1a948 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -16,6 +16,8 @@ import annotation.{experimental, compileTimeOnly, retainsCap} @deprecated("Use `Capability` instead") type Cap = Capability + trait Mutable extends Capability + /** Carrier trait for capture set type parameters */ trait CapSet extends Any @@ -41,6 +43,12 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ extension (x: Any) def reachCapability: Any = x + /** Unique capabilities x! which appear as terms in @retains annotations are encoded + * as `caps.uniqueCapability(x)`. When converted to CaptureRef types in capture sets + * they are represented as `x.type @annotation.internal.uniqueCapability`. + */ + extension (x: Any) def readOnlyCapability: Any = x + /** A trait to allow expressing existential types such as * * (x: Exists) => A ->{x} B @@ -52,7 +60,12 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ final class untrackedCaptures extends annotation.StaticAnnotation - /** This should go into annotations. For now it is here, so that we + /** An annotation on parameters `x` stating that the method's body makes + * use of the reach capability `x*`. Consequently, when calling the method + * we need to charge the deep capture set of the actual argiment to the + * environment. + * + * Note: This should go into annotations. For now it is here, so that we * can experiment with it quickly between minor releases */ final class use extends annotation.StaticAnnotation diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 00e7153bcb83..414ff7d92653 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -13,6 +13,7 @@ object MiMaFilters { ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.quotedPatternsWithPolymorphicFunctions"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$quotedPatternsWithPolymorphicFunctions$"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.runtime.Patterns.higherOrderHoleWithTypes"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.readOnlyCapability"), ), // Additions since last LTS diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index d4d64424e297..f7b45ddf0eaa 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -1,17 +1,20 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:12:33 --------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:12:12 --------------------------------------- 12 | files.map((f: F) => new Logger(f)) // error, Q: can we make this pass (see #19076)? - | ^ - | Found: (f : F) - | Required: File + | ^^^^^^^^^^^^^^^^^^^^^^^ + | Found: (f: F) ->{files.rd*} box Logger{val f²: File^?}^? + | Required: (f: box F^{files.rd*}) => box Logger{val f²: File^?}^? + | + | where: f is a reference to a value parameter + | f² is a value in class Logger | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:15:12 --------------------------------------- 15 | files.map(new Logger(_)) // error, Q: can we improve the error message? | ^^^^^^^^^^^^^ - | Found: (_$1: box File^{files*}) ->{files*} (ex$16: caps.Exists) -> box Logger{val f: File^{_$1}}^{ex$16} - | Required: (_$1: box File^{files*}) => box Logger{val f: File^?}^? + |Found: (_$1: box File^{files*}) ->{files*} (ex$16: caps.Exists) -> box Logger{val f: File^{_$1}}^{ex$16.rd, _$1} + |Required: (_$1: box File^{files*}) => box Logger{val f: File^?}^? | - | Note that the universal capability `cap` - | cannot be included in capture set ? + |Note that reference ex$16.rd + |cannot be included in outer capture set ? | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check index 111719a81f07..bdd053910ac8 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.check +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/lazylists-exceptions.scala:36:2 ----------------------------------------------- 36 | try // error | ^ - | The result of `try` cannot have type LazyList[Int]^ since + | The result of `try` cannot have type LazyList[Int]^{cap.rd} since | that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 37 | tabulate(10) { i => diff --git a/tests/neg-custom-args/captures/mut-outside-mutable.check b/tests/neg-custom-args/captures/mut-outside-mutable.check new file mode 100644 index 000000000000..0407f35745b9 --- /dev/null +++ b/tests/neg-custom-args/captures/mut-outside-mutable.check @@ -0,0 +1,8 @@ +-- Error: tests/neg-custom-args/captures/mut-outside-mutable.scala:5:10 ------------------------------------------------ +5 | mut def foreach(op: T => Unit): Unit // error + | ^ + | Update methods can only be used as members of classes deriving from the `Mutable` trait +-- Error: tests/neg-custom-args/captures/mut-outside-mutable.scala:9:12 ------------------------------------------------ +9 | mut def baz() = 1 // error + | ^ + | Update methods can only be used as members of classes deriving from the `Mutable` trait diff --git a/tests/neg-custom-args/captures/mut-outside-mutable.scala b/tests/neg-custom-args/captures/mut-outside-mutable.scala new file mode 100644 index 000000000000..18c0e59c5bd8 --- /dev/null +++ b/tests/neg-custom-args/captures/mut-outside-mutable.scala @@ -0,0 +1,10 @@ +import caps.Mutable + +trait IterableOnce[T]: + def iterator: Iterator[T]^{this} + mut def foreach(op: T => Unit): Unit // error + +trait Foo extends Mutable: + def bar = + mut def baz() = 1 // error + baz() diff --git a/tests/neg-custom-args/captures/mut-override.scala b/tests/neg-custom-args/captures/mut-override.scala new file mode 100644 index 000000000000..848e4d880223 --- /dev/null +++ b/tests/neg-custom-args/captures/mut-override.scala @@ -0,0 +1,19 @@ +import caps.Mutable + +trait IterableOnce[T] extends Mutable: + def iterator: Iterator[T]^{this} + mut def foreach(op: T => Unit): Unit + +trait Iterator[T] extends IterableOnce[T]: + def iterator = this + def hasNext: Boolean + mut def next(): T + mut def foreach(op: T => Unit): Unit = ??? + override mut def toString = ??? // error + +trait Iterable[T] extends IterableOnce[T]: + def iterator: Iterator[T] = ??? + def foreach(op: T => Unit) = iterator.foreach(op) + +trait BadIterator[T] extends Iterator[T]: + override mut def hasNext: Boolean // error diff --git a/tests/neg-custom-args/captures/readOnly.check b/tests/neg-custom-args/captures/readOnly.check new file mode 100644 index 000000000000..e1aed07657e5 --- /dev/null +++ b/tests/neg-custom-args/captures/readOnly.check @@ -0,0 +1,19 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/readOnly.scala:14:21 ------------------------------------- +14 | val _: () -> Int = getA // error + | ^^^^ + | Found: (getA : () ->{a.rd} Int) + | Required: () -> Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/readOnly.scala:17:23 ------------------------------------- +17 | val _: Int -> Unit = putA // error + | ^^^^ + | Found: (putA : (x$0: Int) ->{a} Unit) + | Required: Int -> Unit + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/readOnly.scala:20:23 ---------------------------------------------------------- +20 | val doit = () => z.put(x.get max y.get) // error + | ^^^^^ + | cannot call update method put from (z : Ref), + | since its capture set {z} is read-only diff --git a/tests/neg-custom-args/captures/readOnly.scala b/tests/neg-custom-args/captures/readOnly.scala new file mode 100644 index 000000000000..4edea6638980 --- /dev/null +++ b/tests/neg-custom-args/captures/readOnly.scala @@ -0,0 +1,22 @@ +import caps.Mutable +import caps.cap + +class Ref(init: Int) extends Mutable: + private var current = init + def get: Int = current + mut def put(x: Int): Unit = current = x + +def Test(c: Object^) = + val a: Ref^ = Ref(1) + val b: Ref^ = Ref(2) + + val getA = () => a.get + val _: () -> Int = getA // error + + val putA = (x: Int) => a.put(x) + val _: Int -> Unit = putA // error + + def setMax(x: Ref^{cap.rd}, y: Ref^{cap.rd}, z: Ref^{cap.rd}) = + val doit = () => z.put(x.get max y.get) // error + val _: () ->{x.rd, y.rd, z} Unit = doit + doit() diff --git a/tests/neg-custom-args/captures/real-try.check b/tests/neg-custom-args/captures/real-try.check index 7a4b12ac08f6..6b478b48515a 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -7,7 +7,7 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:14:2 ----------------------------------------------------------- 14 | try // error | ^ - | The result of `try` cannot have type () => Unit since + | The result of `try` cannot have type () ->{cap.rd} Unit since | that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 15 | () => foo(1) @@ -17,7 +17,7 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:20:10 ---------------------------------------------------------- 20 | val x = try // error | ^ - | The result of `try` cannot have type () => Unit since + | The result of `try` cannot have type () ->{cap.rd} Unit since | that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 21 | () => foo(1) @@ -27,7 +27,7 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:26:10 ---------------------------------------------------------- 26 | val y = try // error | ^ - | The result of `try` cannot have type () => Cell[Unit]^? since + | The result of `try` cannot have type () ->{cap.rd} Cell[Unit]^? since | that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 27 | () => Cell(foo(1)) @@ -37,8 +37,8 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:32:10 ---------------------------------------------------------- 32 | val b = try // error | ^ - | The result of `try` cannot have type Cell[box () => Unit]^? since - | the part box () => Unit of that type captures the root capability `cap`. + | The result of `try` cannot have type Cell[box () ->{cap.rd} Unit]^? since + | the part box () ->{cap.rd} Unit of that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. 33 | Cell(() => foo(1)) 34 | catch diff --git a/tests/pos-custom-args/captures/mutRef.scala b/tests/pos-custom-args/captures/mutRef.scala new file mode 100644 index 000000000000..5fe82c9b987a --- /dev/null +++ b/tests/pos-custom-args/captures/mutRef.scala @@ -0,0 +1,5 @@ +import caps.Mutable +class Ref(init: Int) extends Mutable: + private var current = init + def get: Int = current + mut def put(x: Int): Unit = current = x diff --git a/tests/pos-custom-args/captures/readOnly.scala b/tests/pos-custom-args/captures/readOnly.scala new file mode 100644 index 000000000000..a550010360a3 --- /dev/null +++ b/tests/pos-custom-args/captures/readOnly.scala @@ -0,0 +1,46 @@ +import caps.Mutable +import caps.cap + +trait Rdr[T]: + def get: T + +class Ref[T](init: T) extends Rdr[T], Mutable: + private var current = init + def get: T = current + mut def put(x: T): Unit = current = x + +def Test(c: Object^) = + val a: Ref[Int]^ = Ref(1) + val b: Ref[Int]^ = Ref(2) + def aa = a + + val getA = () => a.get + val _: () ->{a.rd} Int = getA + + val putA = (x: Int) => a.put(x) + val _: Int ->{a} Unit = putA + + def setMax(x: Ref[Int]^{cap.rd}, y: Ref[Int]^{cap.rd}, z: Ref[Int]^{cap}) = + val doit = () => z.put(x.get max y.get) + val _: () ->{x.rd, y.rd, z} Unit = doit + doit() + + def setMax2(x: Rdr[Int]^{cap.rd}, y: Rdr[Int]^{cap.rd}, z: Ref[Int]^{cap}) = ??? + + setMax2(aa, aa, b) + setMax2(a, aa, b) + + abstract class IMatrix: + def apply(i: Int, j: Int): Double + + class Matrix(nrows: Int, ncols: Int) extends IMatrix, Mutable: + val arr = Array.fill(nrows, ncols)(0.0) + def apply(i: Int, j: Int): Double = arr(i)(j) + mut def update(i: Int, j: Int, x: Double): Unit = arr(i)(j) = x + + def mul(x: IMatrix^{cap.rd}, y: IMatrix^{cap.rd}, z: Matrix^) = ??? + + val m1 = Matrix(10, 10) + val m2 = Matrix(10, 10) + mul(m1, m2, m2) // will fail separation checking + mul(m1, m1, m2) // ok From ab6b979080ebca9cfb51cfcddd71c52188bd251a Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 15 Dec 2024 11:25:15 +0100 Subject: [PATCH 5/8] Drop special handling of functions with pure arguments in Existential.toCap If existentials are mapped to fresh, it matters where they are opened. Pure or not arguments don't have anything to do with that. --- compiler/src/dotty/tools/dotc/cc/Existential.scala | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 943254a7ba4e..19800a12a05c 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -242,18 +242,10 @@ object Existential: case _ => core - /** Map top-level existentials to `cap`. Do the same for existentials - * in function results if all preceding arguments are known to be always pure. - */ + /** Map top-level existentials to `cap`. */ def toCap(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => - val transformed = unpacked.substParam(boundVar, defn.captureRoot.termRef) - transformed match - case FunctionOrMethod(args, res @ Existential(_, _)) - if args.forall(_.isAlwaysPure) => - transformed.derivedFunctionOrMethod(args, toCap(res)) - case _ => - transformed + unpacked.substParam(boundVar, defn.captureRoot.termRef) case tp1 @ CapturingType(parent, refs) => tp1.derivedCapturingType(toCap(parent), refs) case tp1 @ AnnotatedType(parent, ann) => From d8f7782a43a4bcfbc6ca224ae08576e47b067f75 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 11 Jan 2025 11:15:30 +0100 Subject: [PATCH 6/8] Implement fresh capabilities These are represented as Fresh.Cap(hidden) where hidden is the set of capabilities subsumed by a fresh. The underlying representation is as an annotated type `T @annotation.internal.freshCapability`. Require -source `3.7` for caps to be converted to Fresh.Cap Also: - Refacture and document CaputureSet - Make SimpleIdentitySets showable - Refactor VarState - Drop Frozen enum - Make VarState subclasses inner classes of companion object - Rename them - Give implicit parameter VarState of subCapture method a default value - Fix printing of capturesets containing cap and some other capability - Revise handing of @uncheckedAnnotation --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 40 +++- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 46 +++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 209 +++++++++++++----- .../dotty/tools/dotc/cc/CheckCaptures.scala | 70 +++--- .../src/dotty/tools/dotc/cc/Existential.scala | 13 +- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 139 ++++++++++++ compiler/src/dotty/tools/dotc/cc/Setup.scala | 54 +++-- .../src/dotty/tools/dotc/cc/Synthetics.scala | 2 +- .../dotty/tools/dotc/core/Definitions.scala | 4 + .../dotty/tools/dotc/core/TypeComparer.scala | 40 ++-- .../src/dotty/tools/dotc/core/TypeOps.scala | 4 +- .../src/dotty/tools/dotc/core/Types.scala | 2 +- .../tools/dotc/printing/Formatting.scala | 5 +- .../tools/dotc/printing/PlainPrinter.scala | 55 +++-- .../tools/dotc/printing/RefinedPrinter.scala | 2 +- .../dotty/tools/dotc/transform/Recheck.scala | 6 +- project/MiMaFilters.scala | 1 + .../captures/explain-under-approx.check | 14 -- 18 files changed, 521 insertions(+), 185 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/Fresh.scala delete mode 100644 tests/neg-custom-args/captures/explain-under-approx.check diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 1a9421aea142..55f8118e9b11 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -16,9 +16,14 @@ import config.Feature import collection.mutable import CCState.* import reporting.Message +import CaptureSet.VarState +/** Attachment key for capturing type trees */ private val Captures: Key[CaptureSet] = Key() +/** Context property to print Fresh.Cap as "fresh" instead of "cap" */ +val PrintFresh: Key[Unit] = Key() + object ccConfig: /** If true, allow mapping capture set variables under captureChecking with maps that are neither @@ -47,6 +52,10 @@ object ccConfig: def useSealed(using Context) = Feature.sourceVersion.stable != SourceVersion.`3.5` + /** If true, turn on separation checking */ + def useFresh(using Context): Boolean = + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`future`) + end ccConfig /** Are we at checkCaptures phase? */ @@ -193,10 +202,7 @@ extension (tp: Type) case tp: TypeParamRef => tp.derivesFrom(defn.Caps_CapSet) case AnnotatedType(parent, annot) => - (annot.symbol == defn.ReachCapabilityAnnot - || annot.symbol == defn.MaybeCapabilityAnnot - || annot.symbol == defn.ReadOnlyCapabilityAnnot - ) && parent.isTrackableRef + defn.capabilityWrapperAnnots.contains(annot.symbol) && parent.isTrackableRef case _ => false @@ -244,7 +250,7 @@ extension (tp: Type) * the two capture sets are combined. */ def capturing(cs: CaptureSet)(using Context): Type = - if (cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, frozen = true).isOK) + if (cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, VarState.Separate).isOK) && !cs.keepAlways then tp else tp match @@ -421,6 +427,10 @@ extension (tp: Type) mapOver(t) tm(tp) + def hasUseAnnot(using Context): Boolean = tp match + case AnnotatedType(_, ann) => ann.symbol == defn.UseAnnot + case _ => false + /** If `x` is a capture ref, its maybe capability `x?`, represented internally * as `x @maybeCapability`. `x?` stands for a capability `x` that might or might * not be part of a capture set. We have `{} <: {x?} <: {x}`. Maybe capabilities @@ -512,6 +522,24 @@ extension (tp: Type) tp case _ => tp + end withReachCaptures + + /** Does this type contain no-flip covariant occurrences of `cap`? */ + def containsCap(using Context): Boolean = + val acc = new TypeAccumulator[Boolean]: + def apply(x: Boolean, t: Type) = + x + || variance > 0 && t.dealiasKeepAnnots.match + case t @ CapturingType(p, cs) if cs.containsCap => + true + case t @ AnnotatedType(parent, ann) => + // Don't traverse annotations, which includes capture sets + this(x, parent) + case Existential(_, _) => + false + case _ => + foldOver(x, t) + acc(false, tp) def level(using Context): Level = tp match @@ -690,7 +718,7 @@ abstract class AnnotatedCapability(annot: Context ?=> ClassSymbol): case _ => AnnotatedType(tp, Annotation(annot, util.Spans.NoSpan)) def unapply(tree: AnnotatedType)(using Context): Option[CaptureRef] = tree match - case AnnotatedType(parent: CaptureRef, ann) if ann.symbol == annot => Some(parent) + case AnnotatedType(parent: CaptureRef, ann) if ann.hasSymbol(annot) => Some(parent) case _ => None protected def unwrappable(using Context): Set[Symbol] diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 00e872cb2d4c..927a02989ff9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -13,6 +13,7 @@ import CCState.* import Periods.NoRunId import compiletime.uninitialized import StdNames.nme +import CaptureSet.VarState /** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs, * as well as three kinds of AnnotatedTypes representing readOnly, reach, and maybe capabilities. @@ -78,15 +79,24 @@ trait CaptureRef extends TypeProxy, ValueType: case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot case _ => false + /** Is this reference a Fresh.Cap instance? */ + final def isFresh(using Context): Boolean = this match + case Fresh.Cap(_) => true + case _ => false + + /** Is this reference the generic root capability `cap` or a Fresh.Cap instance? */ + final def isCapOrFresh(using Context): Boolean = isCap || isFresh + /** Is this reference one the generic root capabilities `cap` or `cap.rd` ? */ final def isRootCapability(using Context): Boolean = this match - case ReadOnlyCapability(tp1) => tp1.isCap - case _ => isCap + case ReadOnlyCapability(tp1) => tp1.isCapOrFresh + case _ => isCapOrFresh /** Is this reference capability that does not derive from another capability ? */ final def isMaxCapability(using Context): Boolean = this match case tp: TermRef => tp.isCap || tp.info.derivesFrom(defn.Caps_Exists) case tp: TermParamRef => tp.underlying.derivesFrom(defn.Caps_Exists) + case Fresh.Cap(_) => true case ReadOnlyCapability(tp1) => tp1.isMaxCapability case _ => false @@ -137,9 +147,9 @@ trait CaptureRef extends TypeProxy, ValueType: * Y: CapSet^c1...CapSet^c2, x subsumes (CapSet^c2) ==> x subsumes Y * Contains[X, y] ==> X subsumes y * - * TODO: Document cases with more comments. + * TODO: Move to CaptureSet */ - final def subsumes(y: CaptureRef)(using Context): Boolean = + final def subsumes(y: CaptureRef)(using ctx: Context, vs: VarState = VarState.Separate): Boolean = def subsumingRefs(x: Type, y: Type): Boolean = x match case x: CaptureRef => y match @@ -147,16 +157,17 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false case _ => false - def viaInfo(info: Type)(test: Type => Boolean): Boolean = info.match + def viaInfo(info: Type)(test: Type => Boolean): Boolean = info.dealias match case info: SingletonCaptureRef => test(info) + case CapturingType(parent, _) => viaInfo(parent)(test) case info: AndType => viaInfo(info.tp1)(test) || viaInfo(info.tp2)(test) case info: OrType => viaInfo(info.tp1)(test) && viaInfo(info.tp2)(test) case _ => false (this eq y) - || this.isCap + || maxSubsumes(y, canAddHidden = !vs.isOpen) || y.match - case y: TermRef if !y.isRootCapability => + case y: TermRef if !y.isCap => y.prefix.match case ypre: CaptureRef => this.subsumes(ypre) @@ -201,6 +212,27 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false end subsumes + /** This is a maximal capabaility that subsumes `y` in given context and VarState. + * @param canAddHidden If true we allow maximal capabilties to subsume all other capabilities. + * We add those capabilities to the hidden set if this is Fresh.Cap + * If false we only accept `y` elements that are already in the + * hidden set of this Fresh.Cap. The idea is that in a VarState that + * accepts additions we first run `maxSubsumes` with `canAddHidden = false` + * so that new variables get added to the sets. If that fails, we run + * the test again with canAddHidden = true as a last effort before we + * fail a comparison. + */ + def maxSubsumes(y: CaptureRef, canAddHidden: Boolean)(using ctx: Context, vs: VarState = VarState.Separate): Boolean = + this.match + case Fresh.Cap(hidden) => + vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) + || !y.stripReadOnly.isCap && canAddHidden && vs.addHidden(hidden, y) + case _ => + this.isCap && canAddHidden + || y.match + case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden) + case _ => false + def assumedContainsOf(x: TypeRef)(using Context): SimpleIdentitySet[CaptureRef] = CaptureSet.assumedContains.getOrElse(x, SimpleIdentitySet.empty) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index e1f5a557bc0d..863afaa0aaf9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -14,7 +14,6 @@ import printing.{Showable, Printer} import printing.Texts.* import util.{SimpleIdentitySet, Property} import typer.ErrorReporting.Addenda -import TypeComparer.subsumesExistentially import util.common.alwaysTrue import scala.collection.{mutable, immutable} import CCState.* @@ -81,14 +80,26 @@ sealed abstract class CaptureSet extends Showable: assert(!isConst) asInstanceOf[Var] + /** Convert to Const with current elements unconditionally */ + def toConst: Const = this match + case c: Const => c + case v: Var => Const(v.elems) + /** Does this capture set contain the root reference `cap` as element? */ final def isUniversal(using Context) = elems.exists(_.isCap) + /** Does this capture set contain the root reference `cap` as element? */ + final def isUniversalOrFresh(using Context) = + elems.exists(_.isCapOrFresh) + /** Does this capture set contain a root reference `cap` or `cap.rd` as element? */ final def containsRootCapability(using Context) = elems.exists(_.isRootCapability) + final def containsCap(using Context) = + elems.exists(_.stripReadOnly.isCap) + final def isUnboxable(using Context) = elems.exists(elem => elem.isRootCapability || Existential.isExistentialVar(elem)) @@ -135,8 +146,8 @@ sealed abstract class CaptureSet extends Showable: * element is not the root capability, try instead to include its underlying * capture set. */ - protected final def addNewElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if elem.isMaxCapability || summon[VarState] == FrozenState then + protected final def addNewElem(elem: CaptureRef)(using ctx: Context, vs: VarState): CompareResult = + if elem.isMaxCapability || !vs.isOpen then addThisElem(elem) else addThisElem(elem).orElse: @@ -156,27 +167,40 @@ sealed abstract class CaptureSet extends Showable: */ protected def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult + protected def addHiddenElem(elem: CaptureRef)(using ctx: Context, vs: VarState): CompareResult = + if elems.exists(_.maxSubsumes(elem, canAddHidden = true)) + then CompareResult.OK + else CompareResult.Fail(this :: Nil) + /** If this is a variable, add `cs` as a dependent set */ protected def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult /** If `cs` is a variable, add this capture set as one of its dependent sets */ protected def addAsDependentTo(cs: CaptureSet)(using Context): this.type = - cs.addDependent(this)(using ctx, UnrecordedState) + cs.addDependent(this)(using ctx, VarState.Unrecorded) this /** {x} <:< this where <:< is subcapturing, but treating all variables * as frozen. */ - def accountsFor(x: CaptureRef)(using Context): Boolean = + def accountsFor(x: CaptureRef)(using ctx: Context, vs: VarState = VarState.Separate): Boolean = + def debugInfo(using Context) = i"$this accountsFor $x, which has capture set ${x.captureSetOfInfo}" + def test(using Context) = reporting.trace(debugInfo): elems.exists(_.subsumes(x)) - || !x.isMaxCapability + || // Even though subsumes already follows captureSetOfInfo, this is not enough. + // For instance x: C^{y, z}. Then neither y nor z subsumes x but {y, z} accounts for x. + !x.isMaxCapability && !x.derivesFrom(defn.Caps_CapSet) - && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK + && !(vs == VarState.Separate && x.captureSetOfInfo.containsRootCapability) + // in VarState.Separate, don't try to widen to cap since that might succeed with {cap} <: {cap} + && x.captureSetOfInfo.subCaptures(this, VarState.Separate).isOK + comparer match case comparer: ExplainingTypeComparer => comparer.traceIndented(debugInfo)(test) case _ => test + end accountsFor /** A more optimistic version of accountsFor, which does not take variable supersets * of the `x` reference into account. A set might account for `x` if it accounts @@ -186,14 +210,13 @@ sealed abstract class CaptureSet extends Showable: * root capability `cap`. */ def mightAccountFor(x: CaptureRef)(using Context): Boolean = - reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true) { - elems.exists(_.subsumes(x)) + reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true): + elems.exists(_.subsumes(x)(using ctx, VarState.ClosedUnrecorded)) || !x.isMaxCapability && { val elems = x.captureSetOfInfo.elems !elems.isEmpty && elems.forall(mightAccountFor) } - } /** A more optimistic version of subCaptures used to choose one of two typing rules * for selections and applications. `cs1 mightSubcapture cs2` if `cs2` might account for @@ -209,11 +232,11 @@ sealed abstract class CaptureSet extends Showable: * be added when making this test. An attempt to add either * will result in failure. */ - final def subCaptures(that: CaptureSet, frozen: Boolean)(using Context): CompareResult = - subCaptures(that)(using ctx, if frozen then FrozenState else VarState()) + final def subCaptures(that: CaptureSet, vs: VarState)(using Context): CompareResult = + subCaptures(that)(using ctx, vs) /** The subcapturing test, using a given VarState */ - private def subCaptures(that: CaptureSet)(using Context, VarState): CompareResult = + final def subCaptures(that: CaptureSet)(using ctx: Context, vs: VarState = VarState()): CompareResult = val result = that.tryInclude(elems, this) if result.isOK then addDependent(that) @@ -227,19 +250,22 @@ sealed abstract class CaptureSet extends Showable: * in a frozen state. */ def =:= (that: CaptureSet)(using Context): Boolean = - this.subCaptures(that, frozen = true).isOK - && that.subCaptures(this, frozen = true).isOK + this.subCaptures(that, VarState.Separate).isOK + && that.subCaptures(this, VarState.Separate).isOK /** The smallest capture set (via <:<) that is a superset of both * `this` and `that` */ def ++ (that: CaptureSet)(using Context): CaptureSet = - if this.subCaptures(that, frozen = true).isOK then + if this.subCaptures(that, VarState.Separate).isOK then if that.isAlwaysEmpty && this.keepAlways then this else that - else if that.subCaptures(this, frozen = true).isOK then this + else if that.subCaptures(this, VarState.Separate).isOK then this else if this.isConst && that.isConst then Const(this.elems ++ that.elems) else Union(this, that) + def ++ (that: CaptureSet.Const)(using Context): CaptureSet.Const = + Const(this.elems ++ that.elems) + /** The smallest superset (via <:<) of this capture set that also contains `ref`. */ def + (ref: CaptureRef)(using Context): CaptureSet = @@ -248,8 +274,8 @@ sealed abstract class CaptureSet extends Showable: /** The largest capture set (via <:<) that is a subset of both `this` and `that` */ def **(that: CaptureSet)(using Context): CaptureSet = - if this.subCaptures(that, frozen = true).isOK then this - else if that.subCaptures(this, frozen = true).isOK then that + if this.subCaptures(that, VarState.Closed()).isOK then this + else if that.subCaptures(this, VarState.Closed()).isOK then that else if this.isConst && that.isConst then Const(elemIntersection(this, that)) else Intersection(this, that) @@ -366,6 +392,11 @@ sealed abstract class CaptureSet extends Showable: override def toText(printer: Printer): Text = printer.toTextCaptureSet(this) ~~ description + /** Apply function `f` to the elements. Typcially used for printing. + * Overridden in HiddenSet so that we don't run into infinite recursions + */ + def processElems[T](f: Refs => T): T = f(elems) + object CaptureSet: type Refs = SimpleIdentitySet[CaptureRef] type Vars = SimpleIdentitySet[Var] @@ -376,7 +407,7 @@ object CaptureSet: /** If set to `true`, capture stack traces that tell us where sets are created */ private final val debugSets = false - private val emptySet = SimpleIdentitySet.empty + val emptySet = SimpleIdentitySet.empty /** The empty capture set `{}` */ val empty: CaptureSet.Const = Const(emptySet) @@ -385,6 +416,9 @@ object CaptureSet: def universal(using Context): CaptureSet = defn.captureRoot.termRef.singletonCaptureSet + def fresh(owner: Symbol = NoSymbol)(using Context): CaptureSet = + Fresh.Cap(owner).singletonCaptureSet + /** The shared capture set `{cap.rd}` */ def shared(using Context): CaptureSet = defn.captureRoot.termRef.readOnly.singletonCaptureSet @@ -405,7 +439,7 @@ object CaptureSet: def isAlwaysEmpty = elems.isEmpty def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = - CompareResult.Fail(this :: Nil) + addHiddenElem(elem) def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK @@ -435,7 +469,7 @@ object CaptureSet: object Fluid extends Const(emptySet): override def isAlwaysEmpty = false override def addThisElem(elem: CaptureRef)(using Context, VarState) = CompareResult.OK - override def accountsFor(x: CaptureRef)(using Context): Boolean = true + override def accountsFor(x: CaptureRef)(using Context, VarState): Boolean = true override def mightAccountFor(x: CaptureRef)(using Context): Boolean = true override def toString = "" end Fluid @@ -501,16 +535,16 @@ object CaptureSet: deps = state.deps(this) final def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if isConst // Fail if variable is solved, - || !recordElemsState() // or given VarState is frozen, - || Existential.isBadExistential(elem) // or `elem` is an out-of-scope existential, - then + if isConst || !recordElemsState() then // Fail if variable is solved or given VarState is frozen + addHiddenElem(elem) + else if Existential.isBadExistential(elem) then // Fail if `elem` is an out-of-scope existential CompareResult.Fail(this :: Nil) else if !levelOK(elem) then CompareResult.LevelError(this, elem) // or `elem` is not visible at the level of the set. else - //if id == 34 then assert(!elem.isUniversalRootCapability) + // id == 108 then assert(false, i"trying to add $elem to $this") assert(elem.isTrackableRef, elem) + assert(!this.isInstanceOf[HiddenSet] || summon[VarState] == VarState.Separate, summon[VarState]) elems += elem if elem.isRootCapability then rootAddedHandler() @@ -578,7 +612,7 @@ object CaptureSet: this else if isUniversal || computingApprox then universal - else if containsRootCapability && isReadOnly then + else if containsCap && isReadOnly then shared else computingApprox = true @@ -602,11 +636,12 @@ object CaptureSet: */ def solve()(using Context): Unit = if !isConst then - val approx = upperApprox(empty) + val approx = upperApprox(empty).map(Fresh.FromCap(NoSymbol).inverse) .showing(i"solve $this = $result", capt) //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") val newElems = approx.elems -- elems - if tryInclude(newElems, empty)(using ctx, VarState()).isOK then + given VarState() + if tryInclude(newElems, empty).isOK then markSolved() /** Mark set as solved and propagate this info to all dependent sets */ @@ -890,6 +925,21 @@ object CaptureSet: def elemIntersection(cs1: CaptureSet, cs2: CaptureSet)(using Context): Refs = cs1.elems.filter(cs2.mightAccountFor) ++ cs2.elems.filter(cs1.mightAccountFor) + /** A capture set variable used to record the references hidden by a Fresh.Cap instance */ + class HiddenSet(initialHidden: Refs = emptySet)(using @constructorOnly ictx: Context) + extends Var(initialElems = initialHidden): + + /** Apply function `f` to `elems` while setting `elems` to empty for the + * duration. This is used to escape infinite recursions if two Frash.Caps + * refer to each other in their hidden sets. + */ + override def processElems[T](f: Refs => T): T = + val savedElems = elems + elems = emptySet + try f(savedElems) + finally elems = savedElems + end HiddenSet + /** Extrapolate tm(r) according to `variance`. Let r1 be the result of tm(r). * - If r1 is a tracked CaptureRef, return {r1} * - If r1 has an empty capture set, return {} @@ -925,7 +975,7 @@ object CaptureSet: */ def subCapturesRange(arg1: TypeBounds, arg2: Type)(using Context): Boolean = arg1 match case TypeBounds(CapturingType(lo, loRefs), CapturingType(hi, hiRefs)) if lo =:= hi => - given VarState = VarState() + given VarState() val cs2 = arg2.captureSet hiRefs.subCaptures(cs2).isOK && cs2.subCaptures(loRefs).isOK case _ => @@ -1001,8 +1051,7 @@ object CaptureSet: def getElems(v: Var): Option[Refs] = elemsMap.get(v) /** Record elements, return whether this was allowed. - * By default, recording is allowed but the special state FrozenState - * overrides this. + * By default, recording is allowed in regular both not in frozen states. */ def putElems(v: Var, elems: Refs): Boolean = { elemsMap(v) = elems; true } @@ -1013,36 +1062,78 @@ object CaptureSet: def getDeps(v: Var): Option[Deps] = depsMap.get(v) /** Record dependent sets, return whether this was allowed. - * By default, recording is allowed but the special state FrozenState - * overrides this. + * By default, recording is allowed in regular both not in frozen states. */ def putDeps(v: Var, deps: Deps): Boolean = { depsMap(v) = deps; true } + /** Does this state allow additions of elements to capture set variables? */ + def isOpen = true + + /** Add element to hidden set, recording it in elemsMap, + * return whether this was allowed. By default, recording is allowed + * but the special state VarState.Separate overrides this. + */ + def addHidden(hidden: HiddenSet, elem: CaptureRef): Boolean = + elemsMap.get(hidden) match + case None => elemsMap(hidden) = hidden.elems + case _ => + hidden.elems += elem + true + /** Roll back global state to what was recorded in this VarState */ def rollBack(): Unit = elemsMap.keysIterator.foreach(_.resetElems()(using this)) depsMap.keysIterator.foreach(_.resetDeps()(using this)) - end VarState - /** A special state that does not allow to record elements or dependent sets. - * In effect this means that no new elements or dependent sets can be added - * in this state (since the previous state cannot be recorded in a snapshot) - */ - @sharable - object FrozenState extends VarState: - override def putElems(v: Var, refs: Refs) = false - override def putDeps(v: Var, deps: Deps) = false - override def rollBack(): Unit = () + private var seen: util.EqHashSet[CaptureRef] = new util.EqHashSet - @sharable - /** A special state that turns off recording of elements. Used only - * in `addSub` to prevent cycles in recordings. - */ - private object UnrecordedState extends VarState: - override def putElems(v: Var, refs: Refs) = true - override def putDeps(v: Var, deps: Deps) = true - override def rollBack(): Unit = () + /** Run test `pred` unless `ref` was seen in an enclosing `ifNotSeen` operation */ + def ifNotSeen(ref: CaptureRef)(pred: => Boolean): Boolean = + if seen.add(ref) then + try pred finally seen -= ref + else false + + object VarState: + /** A class for states that do not allow to record elements or dependent sets. + * In effect this means that no new elements or dependent sets can be added + * in these states (since the previous state cannot be recorded in a snapshot) + * On the other hand, these states do allow by default Fresh.Cap instances to + * subsume arbitary types, which are then recorded in their hidden sets. + */ + class Closed extends VarState: + override def putElems(v: Var, refs: Refs) = false + override def putDeps(v: Var, deps: Deps) = false + override def isOpen = false + + /** A closed state that allows a Fresh.Cap instance to subsume a + * reference `r` only if `r` is already present in the hidden set of the instance. + * No new references can be added. + */ + @sharable + object Separate extends Closed: + override def addHidden(hidden: HiddenSet, elem: CaptureRef): Boolean = false + + /** A special state that turns off recording of elements. Used only + * in `addSub` to prevent cycles in recordings. + */ + @sharable + private[CaptureSet] object Unrecorded extends VarState: + override def putElems(v: Var, refs: Refs) = true + override def putDeps(v: Var, deps: Deps) = true + override def rollBack(): Unit = () + override def addHidden(hidden: HiddenSet, elem: CaptureRef): Boolean = true + + /** A closed state that turns off recording of hidden elements (but allows + * adding them). Used in `mightAccountFor`. + */ + @sharable + private[CaptureSet] object ClosedUnrecorded extends Closed: + override def addHidden(hidden: HiddenSet, elem: CaptureRef): Boolean = true + + end VarState + + @sharable /** The current VarState, as passed by the implicit context */ def varState(using state: VarState): VarState = state @@ -1111,13 +1202,15 @@ object CaptureSet: tp.captureSet case tp: TermParamRef => tp.captureSet - case _: TypeRef => - empty - case _: TypeParamRef => - empty + case tp: (TypeRef | TypeParamRef) => + if tp.derivesFrom(defn.Caps_CapSet) then tp.captureSet + else empty case CapturingType(parent, refs) => recur(parent) ++ refs case tp @ AnnotatedType(parent, ann) if ann.hasSymbol(defn.ReachCapabilityAnnot) => + // Note: we don't use the `ReachCapability(parent)` extractor here since that + // only works if `parent` is a CaptureRef, but in illegal programs it might not be. + // And then we do not want to fall back to empty. parent match case parent: SingletonCaptureRef if parent.isTrackableRef => tp.singletonCaptureSet @@ -1168,7 +1261,7 @@ object CaptureSet: case t: TypeRef if t.symbol.isAbstractOrParamType && !seen.contains(t.symbol) => seen += t.symbol val upper = t.info.bounds.hi - if includeTypevars && upper.isExactlyAny then CaptureSet.universal + if includeTypevars && upper.isExactlyAny then CaptureSet.fresh(t.symbol) else this(cs, upper) case t @ FunctionOrMethod(args, res @ Existential(_, _)) if args.forall(_.isAlwaysPure) => diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index eab11d03144d..0b1d2397629b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -18,11 +18,12 @@ import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property} import transform.{Recheck, PreRecheck, CapturedVars} import Recheck.* import scala.collection.mutable -import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult} +import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult, VarState} import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} import reporting.{trace, Message, OverrideError} +import Existential.derivedExistentialType /** The capture checker */ object CheckCaptures: @@ -88,6 +89,7 @@ object CheckCaptures: tp case _ => mapOver(tp) + override def toString = "SubstParamsMap" end SubstParamsMap /** Used for substituting parameters in a special case: when all actual arguments @@ -107,6 +109,7 @@ object CheckCaptures: tp case _ => mapOver(tp) + override def toString = "SubstParamsBiMap" lazy val inverse = new BiTypeMap: def apply(tp: Type): Type = tp match @@ -123,6 +126,7 @@ object CheckCaptures: tp case _ => mapOver(tp) + override def toString = "SubstParamsBiMap.inverse" def inverse = thisMap end SubstParamsBiMap @@ -307,32 +311,33 @@ class CheckCaptures extends Recheck, SymTransformer: /** Assert subcapturing `cs1 <: cs2` (available for debugging, otherwise unused) */ def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = - assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2") + assert(cs1.subCaptures(cs2).isOK, i"$cs1 is not a subset of $cs2") /** If `res` is not CompareResult.OK, report an error */ - def checkOK(res: CompareResult, prefix: => String, pos: SrcPos, provenance: => String = "")(using Context): Unit = + def checkOK(res: CompareResult, prefix: => String, added: CaptureRef | CaptureSet, pos: SrcPos, provenance: => String = "")(using Context): Unit = if !res.isOK then - def toAdd: String = CaptureSet.levelErrors.toAdd.mkString - def descr: String = - val d = res.blocking.description - if d.isEmpty then provenance else "" - report.error(em"$prefix included in the allowed capture set ${res.blocking}$descr$toAdd", pos) + inContext(Fresh.printContext(added, res.blocking)): + def toAdd: String = CaptureSet.levelErrors.toAdd.mkString + def descr: String = + val d = res.blocking.description + if d.isEmpty then provenance else "" + report.error(em"$prefix included in the allowed capture set ${res.blocking}$descr$toAdd", pos) /** Check subcapturing `{elem} <: cs`, report error on failure */ def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos, provenance: => String = "")(using Context) = checkOK( - elem.singletonCaptureSet.subCaptures(cs, frozen = false), + elem.singletonCaptureSet.subCaptures(cs), i"$elem cannot be referenced here; it is not", - pos, provenance) + elem, pos, provenance) /** Check subcapturing `cs1 <: cs2`, report error on failure */ def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos, provenance: => String = "", cs1description: String = "")(using Context) = checkOK( - cs1.subCaptures(cs2, frozen = false), + cs1.subCaptures(cs2), if cs1.elems.size == 1 then i"reference ${cs1.elems.toList.head}$cs1description is not" else i"references $cs1$cs1description are not all", - pos, provenance) + cs1, pos, provenance) /** If `sym` is a class or method nested inside a term, a capture set variable representing * the captured variables of the environment associated with `sym`. @@ -635,11 +640,11 @@ class CheckCaptures extends Recheck, SymTransformer: val meth = tree.fun.symbol if meth == defn.Caps_unsafeAssumePure then val arg :: Nil = tree.args: @unchecked - val argType0 = recheck(arg, pt.capturing(CaptureSet.universal)) + val argType0 = recheck(arg, pt.stripCapturing.capturing(CaptureSet.universal)) val argType = if argType0.captureSet.isAlwaysEmpty then argType0 else argType0.widen.stripCapturing - capt.println(i"rechecking $arg with $pt: $argType") + capt.println(i"rechecking unsafeAssumePure of $arg with $pt: $argType") super.recheckFinish(argType, tree, pt) else val res = super.recheckApply(tree, pt) @@ -650,13 +655,13 @@ class CheckCaptures extends Recheck, SymTransformer: * charge the deep capture set of the actual argument to the environment. */ protected override def recheckArg(arg: Tree, formal: Type)(using Context): Type = - val argType = recheck(arg, formal) - formal match - case AnnotatedType(formal1, ann) if ann.symbol == defn.UseAnnot => - // The UseAnnot is added to `formal` by `prepareFunction` - capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") - markFree(argType.deepCaptureSet, arg.srcPos) - case _ => + val freshenedFormal = Fresh.fromCap(formal) + val argType = recheck(arg, freshenedFormal) + .showing(i"recheck arg $arg vs $freshenedFormal", capt) + if formal.hasUseAnnot then + // The @use annotation is added to `formal` by `prepareFunction` + capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") + markFree(argType.deepCaptureSet, arg.srcPos) argType /** Map existential captures in result to `cap` and implement the following @@ -686,9 +691,7 @@ class CheckCaptures extends Recheck, SymTransformer: val qualCaptures = qualType.captureSet val argCaptures = for (argType, formal) <- argTypes.lazyZip(funType.paramInfos) yield - formal match - case AnnotatedType(_, ann) if ann.symbol == defn.UseAnnot => argType.deepCaptureSet - case _ => argType.captureSet + if formal.hasUseAnnot then argType.deepCaptureSet else argType.captureSet appType match case appType @ CapturingType(appType1, refs) if qualType.exists @@ -746,8 +749,8 @@ class CheckCaptures extends Recheck, SymTransformer: def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = var refined: Type = core var allCaptures: CaptureSet = - if core.derivesFromMutable then CaptureSet.universal - else if core.derivesFromCapability then initCs ++ defn.universalCSImpliedByCapability + if core.derivesFromMutable then CaptureSet.fresh() + else if core.derivesFromCapability then initCs ++ Fresh.Cap().readOnly.singletonCaptureSet else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol @@ -768,6 +771,8 @@ class CheckCaptures extends Recheck, SymTransformer: // can happen for curried constructors if instantiate of a previous step // added capture set to result. augmentConstructorType(parent, initCs ++ refs) + case core @ Existential(boundVar, core1) => + core.derivedExistentialType(augmentConstructorType(core1, initCs)) case _ => val (refined, cs) = addParamArgRefinements(core, initCs) refined.capturing(cs) @@ -1201,10 +1206,11 @@ class CheckCaptures extends Recheck, SymTransformer: actualBoxed else capt.println(i"conforms failed for ${tree}: $actual vs $expected") - err.typeMismatch(tree.withType(actualBoxed), expected1, - addApproxAddenda( - addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors), - expected1)) + inContext(Fresh.printContext(actualBoxed, expected1)): + err.typeMismatch(tree.withType(actualBoxed), expected1, + addApproxAddenda( + addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors), + expected1)) actual end checkConformsExpr @@ -1370,7 +1376,7 @@ class CheckCaptures extends Recheck, SymTransformer: val cs = actual.captureSet if covariant then cs ++ leaked else - if !leaked.subCaptures(cs, frozen = false).isOK then + if !leaked.subCaptures(cs).isOK then report.error( em"""$expected cannot be box-converted to ${actual.capturing(leaked)} |since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", pos) @@ -1693,7 +1699,7 @@ class CheckCaptures extends Recheck, SymTransformer: val widened = ref.captureSetOfInfo val added = widened.filter(isAllowed(_)) capt.println(i"heal $ref in $cs by widening to $added") - if !added.subCaptures(cs, frozen = false).isOK then + if !added.subCaptures(cs).isOK then val location = if meth.exists then i" of ${meth.showLocated}" else "" val paramInfo = if ref.paramName.info.kind.isInstanceOf[UniqueNameKind] diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 19800a12a05c..39f6fcf14fd9 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -242,10 +242,10 @@ object Existential: case _ => core - /** Map top-level existentials to `cap`. */ + /** Map top-level existentials to `Fresh.Cap`. */ def toCap(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => - unpacked.substParam(boundVar, defn.captureRoot.termRef) + unpacked.substParam(boundVar, Fresh.Cap()) case tp1 @ CapturingType(parent, refs) => tp1.derivedCapturingType(toCap(parent), refs) case tp1 @ AnnotatedType(parent, ann) => @@ -256,7 +256,7 @@ object Existential: */ def toCapDeeply(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => - toCapDeeply(unpacked.substParam(boundVar, defn.captureRoot.termRef)) + toCapDeeply(unpacked.substParam(boundVar, Fresh.Cap())) case tp1 @ FunctionOrMethod(args, res) => val tp2 = tp1.derivedFunctionOrMethod(args, toCapDeeply(res)) if tp2 ne tp1 then tp2 else tp @@ -273,7 +273,7 @@ object Existential: case AppliedType(tycon, _) => !defn.isFunctionSymbol(tycon.typeSymbol) case _ => false - /** Replace all occurrences of `cap` in parts of this type by an existentially bound + /** Replace all occurrences of `cap` (or fresh) in parts of this type by an existentially bound * variable. If there are such occurrences, or there might be in the future due to embedded * capture set variables, create an existential with the variable wrapping the type. * Stop at function or method types since these have been mapped before. @@ -294,7 +294,7 @@ object Existential: class Wrap(boundVar: TermParamRef) extends CapMap: def apply(t: Type) = t match - case t: TermRef if t.isCap => + case t: CaptureRef if t.isCapOrFresh => // !!! we should map different fresh refs to different existentials if variance > 0 then needsWrap = true boundVar @@ -317,8 +317,9 @@ object Existential: //.showing(i"mapcap $t = $result") lazy val inverse = new BiTypeMap: + lazy val freshCap = Fresh.Cap() def apply(t: Type) = t match - case t: TermParamRef if t eq boundVar => defn.captureRoot.termRef + case t: TermParamRef if t eq boundVar => freshCap case _ => mapOver(t) def inverse = Wrap.this override def toString = "Wrap.inverse" diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala new file mode 100644 index 000000000000..14c4c03e4115 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -0,0 +1,139 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* +import StdNames.nme +import ast.tpd.* +import Decorators.* +import typer.ErrorReporting.errorType +import Names.TermName +import NameKinds.ExistentialBinderName +import NameOps.isImpureFunction +import reporting.Message +import util.SimpleIdentitySet.empty +import CaptureSet.{Refs, emptySet, NarrowingCapabilityMap} +import dotty.tools.dotc.util.SimpleIdentitySet + +/** Handling fresh in CC: + +*/ +object Fresh: + + case class Annot(hidden: CaptureSet.HiddenSet) extends Annotation: + override def symbol(using Context) = defn.FreshCapabilityAnnot + override def tree(using Context) = New(symbol.typeRef, Nil) + override def derivedAnnotation(tree: Tree)(using Context): Annotation = this + + override def hash: Int = hidden.hashCode + override def eql(that: Annotation) = that match + case Annot(hidden) => this.hidden eq hidden + case _ => false + end Annot + + private def ownerToHidden(owner: Symbol, reach: Boolean)(using Context): Refs = + val ref = owner.termRef + if reach then + if ref.isTrackableRef then SimpleIdentitySet(ref.reach) else emptySet + else + if ref.isTracked then SimpleIdentitySet(ref) else emptySet + + object Cap: + + def apply(initialHidden: Refs = emptySet)(using Context): CaptureRef = + if ccConfig.useFresh then + AnnotatedType(defn.captureRoot.termRef, Annot(CaptureSet.HiddenSet(initialHidden))) + else + defn.captureRoot.termRef + + def apply(owner: Symbol, reach: Boolean)(using Context): CaptureRef = + apply(ownerToHidden(owner, reach)) + + def apply(owner: Symbol)(using Context): CaptureRef = + apply(ownerToHidden(owner, reach = false)) + + def unapply(tp: AnnotatedType)(using Context): Option[CaptureSet.HiddenSet] = tp.annot match + case Annot(hidden) => Some(hidden) + case _ => None + end Cap + + class FromCap(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap: + thisMap => + + var reach = false + + private def initHidden = + val ref = owner.termRef + if reach then + if ref.isTrackableRef then SimpleIdentitySet(ref.reach) else emptySet + else + if ref.isTracked then SimpleIdentitySet(ref) else emptySet + + override def apply(t: Type) = + if variance <= 0 then t + else t match + case t: CaptureRef if t.isCap => + Cap(initHidden) + case t @ CapturingType(_, refs) => + val savedReach = reach + if t.isBoxed then reach = true + try mapOver(t) finally reach = savedReach + case t @ AnnotatedType(parent, ann) => + val parent1 = this(parent) + if ann.symbol.isRetains && ann.tree.toCaptureSet.containsCap then + this(CapturingType(parent1, ann.tree.toCaptureSet)) + else + t.derivedAnnotatedType(parent1, ann) + case _ => + mapFollowingAliases(t) + + override def toString = "CapToFresh" + + lazy val inverse: BiTypeMap & FollowAliasesMap = new BiTypeMap with FollowAliasesMap: + def apply(t: Type): Type = t match + case t @ Cap(_) => defn.captureRoot.termRef + case t @ CapturingType(_, refs) => mapOver(t) + case _ => mapFollowingAliases(t) + + def inverse = thisMap + override def toString = thisMap.toString + ".inverse" + + end FromCap + + /** Maps cap to fresh */ + def fromCap(tp: Type, owner: Symbol = NoSymbol)(using Context): Type = + if ccConfig.useFresh then FromCap(owner)(tp) else tp + + /** Maps fresh to cap */ + def toCap(tp: Type)(using Context): Type = + if ccConfig.useFresh then FromCap(NoSymbol).inverse(tp) else tp + + /** If `refs` contains an occurrence of `cap` or `cap.rd`, the current context + * with an added property PrintFresh. This addition causes all occurrences of + * `Fresh.Cap` to be printed as `fresh` instead of `cap`, so that one avoids + * confusion in error messages. + */ + def printContext(refs: (Type | CaptureSet)*)(using Context): Context = + def hasCap = new TypeAccumulator[Boolean]: + def apply(x: Boolean, t: Type) = + x || t.dealiasKeepAnnots.match + case Fresh.Cap(_) => false + case t: TermRef => t.isCap || this(x, t.widen) + case x: ThisType => false + case _ => foldOver(x, t) + def containsFresh(x: Type | CaptureSet): Boolean = x match + case tp: Type => + hasCap(false, tp) + case refs: CaptureSet => + refs.elems.exists(_.stripReadOnly.isCap) + + if refs.exists(containsFresh) then ctx.withProperty(PrintFresh, Some(())) + else ctx + end printContext +end Fresh + + + + + diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 19522ddf603c..2b64a9bf4b66 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -132,7 +132,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def mappedInfo = if toBeUpdated.contains(sym) then symd.info // don't transform symbols that will anyway be updated - else transformExplicitType(symd.info) + else Fresh.fromCap(transformExplicitType(symd.info), sym) if Synthetics.needsTransform(symd) then Synthetics.transform(symd, mappedInfo) else if isPreCC(sym) then @@ -356,6 +356,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: catch case ex: IllegalCaptureRef => report.error(em"Illegal capture reference: ${ex.getMessage.nn}", tptToCheck.srcPos) parent2 + else if ann.symbol == defn.UncheckedCapturesAnnot then + makeUnchecked(apply(parent)) else t.derivedAnnotatedType(parent1, ann) case throwsAlias(res, exc) => @@ -428,20 +430,30 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def setupTraverser(checker: CheckerAPI) = new TreeTraverserWithPreciseImportContexts: import checker.* - /** Transform type of tree, and remember the transformed type as the type the tree */ - private def transformTT(tree: TypeTree, boxed: Boolean)(using Context): Unit = + private val paramSigChange = util.EqHashSet[Tree]() + + /** Transform type of tree, and remember the transformed type as the type the tree + * @pre !(boxed && sym.exists) + */ + private def transformTT(tree: TypeTree, sym: Symbol, boxed: Boolean)(using Context): Unit = if !tree.hasNuType then - val transformed = + var transformed = if tree.isInferred then transformInferredType(tree.tpe) else transformExplicitType(tree.tpe, tptToCheck = tree) - tree.setNuType(if boxed then box(transformed) else transformed) + if boxed then transformed = box(transformed) + if sym.is(Param) && (transformed ne tree.tpe) then + paramSigChange += tree + tree.setNuType( + if boxed then transformed + else if sym.hasAnnotation(defn.UncheckedCapturesAnnot) then makeUnchecked(transformed) + else Fresh.fromCap(transformed, sym)) /** Transform the type of a val or var or the result type of a def */ def transformResultType(tpt: TypeTree, sym: Symbol)(using Context): Unit = // First step: Transform the type and record it as knownType of tpt. try - transformTT(tpt, + transformTT(tpt, sym, boxed = sym.isMutableVar && !ccConfig.useSealed @@ -490,9 +502,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tree @ TypeApply(fn, args) => traverse(fn) - if !defn.isTypeTestOrCast(fn.symbol) then - for case arg: TypeTree <- args do - transformTT(arg, boxed = true) // type arguments in type applications are boxed + for case arg: TypeTree <- args do + if defn.isTypeTestOrCast(fn.symbol) then + arg.setNuType(Fresh.fromCap(arg.tpe)) + else + transformTT(arg, NoSymbol, boxed = true) // type arguments in type applications are boxed case tree: TypeDef if tree.symbol.isClass => val sym = tree.symbol @@ -501,6 +515,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: inContext(ctx.withOwner(sym)) traverseChildren(tree) + case tree @ TypeDef(_, rhs: TypeTree) => + transformTT(rhs, tree.symbol, boxed = false) + case tree @ SeqLiteral(elems, tpt: TypeTree) => traverse(elems) tpt.setNuType(box(transformInferredType(tpt.tpe))) @@ -517,7 +534,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** Processing done on node `tree` after its children are traversed */ def postProcess(tree: Tree)(using Context): Unit = tree match case tree: TypeTree => - transformTT(tree, boxed = false) + transformTT(tree, NoSymbol, boxed = false) case tree: ValOrDefDef => // Make sure denotation of tree's symbol is correct val sym = tree.symbol @@ -544,8 +561,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def paramSignatureChanges = tree.match case tree: DefDef => tree.paramss.nestedExists: - case param: ValDef => param.tpt.hasNuType - case param: TypeDef => param.rhs.hasNuType + case param: ValDef => paramSigChange.contains(param.tpt) + case param: TypeDef => paramSigChange.contains(param.rhs) case _ => false // A symbol's signature changes if some of its parameter types or its result type @@ -580,7 +597,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: mt.paramInfos else val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) - psyms.map(psym => adaptedInfo(psym, subst(psym.nextInfo).asInstanceOf[mt.PInfo])), + psyms.map(psym => adaptedInfo(psym, subst(Fresh.toCap(psym.nextInfo)).asInstanceOf[mt.PInfo])), mt1 => integrateRT(mt.resType, psymss.tail, resType, psyms :: prevPsymss, mt1 :: prevLambdas) ) @@ -798,6 +815,16 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if variance > 0 then t1 else decorate(t1, Function.const(CaptureSet.Fluid)) + /** Replace all universal capture sets in this type by */ + private def makeUnchecked(using Context): TypeMap = new TypeMap with FollowAliasesMap: + def apply(t: Type) = t match + case t @ CapturingType(parent, refs) => + val parent1 = this(parent) + if refs.isUniversal then t.derivedCapturingType(parent1, CaptureSet.Fluid) + else t + case Existential(_) => t + case _ => mapFollowingAliases(t) + /** Pull out an embedded capture set from a part of `tp` */ def normalizeCaptures(tp: Type)(using Context): Type = tp match case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) => @@ -877,6 +904,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: for j <- 0 until retained.length if j != i r <- retained(j).toCaptureRefs + if !r.isMaxCapability yield r val remaining = CaptureSet(others*) check(remaining, remaining) diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 1372ebafe82f..9e2729eb7f31 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -116,7 +116,7 @@ object Synthetics: def transformUnapplyCaptures(info: Type)(using Context): Type = info match case info: MethodType => val paramInfo :: Nil = info.paramInfos: @unchecked - val newParamInfo = CapturingType(paramInfo, CaptureSet.universal) + val newParamInfo = CapturingType(paramInfo, CaptureSet.fresh()) val trackedParam = info.paramRefs.head def newResult(tp: Type): Type = tp match case tp: MethodOrPoly => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index f108034d9070..f45abe9c61f2 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1076,6 +1076,7 @@ class Definitions { @tu lazy val TargetNameAnnot: ClassSymbol = requiredClass("scala.annotation.targetName") @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") @tu lazy val ReachCapabilityAnnot = requiredClass("scala.annotation.internal.reachCapability") + @tu lazy val FreshCapabilityAnnot = requiredClass("scala.annotation.internal.freshCapability") @tu lazy val ReadOnlyCapabilityAnnot = requiredClass("scala.annotation.internal.readOnlyCapability") @tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability") @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains") @@ -1525,6 +1526,9 @@ class Definitions { @tu lazy val pureSimpleClasses = Set(StringClass, NothingClass, NullClass) ++ ScalaValueClasses() + @tu lazy val capabilityWrapperAnnots: Set[Symbol] = + Set(ReachCapabilityAnnot, ReadOnlyCapabilityAnnot, MaybeCapabilityAnnot, FreshCapabilityAnnot) + @tu lazy val AbstractFunctionType: Array[TypeRef] = mkArityArray("scala.runtime.AbstractFunction", MaxImplementedFunctionArity, 0).asInstanceOf[Array[TypeRef]] val AbstractFunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun(AbstractFunctionType.map(_.symbol.asClass)) def AbstractFunctionClass(n: Int)(using Context): Symbol = AbstractFunctionClassPerRun()(using ctx)(n) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 8e6bb78bd0e6..79b7213565a5 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -545,7 +545,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1 @ CapturingType(parent1, refs1) => def compareCapturing = if tp2.isAny then true - else if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK && sameBoxed(tp1, tp2, refs1) + else if subCaptures(refs1, tp2.captureSet).isOK && sameBoxed(tp1, tp2, refs1) || !ctx.mode.is(Mode.CheckBoundsOrSelfType) && tp1.isAlwaysPure then val tp2a = @@ -672,12 +672,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) case (info1 @ CapturingType(parent1, refs1), info2: Type) if info2.stripCapturing.isInstanceOf[MethodOrPoly] => - subCaptures(refs1, info2.captureSet, frozenConstraint).isOK && sameBoxed(info1, info2, refs1) + subCaptures(refs1, info2.captureSet).isOK && sameBoxed(info1, info2, refs1) && isSubInfo(parent1, info2) case (info1: Type, CapturingType(parent2, refs2)) if info1.stripCapturing.isInstanceOf[MethodOrPoly] => val refs1 = info1.captureSet - (refs1.isAlwaysEmpty || subCaptures(refs1, refs2, frozenConstraint).isOK) && sameBoxed(info1, info2, refs1) + (refs1.isAlwaysEmpty || subCaptures(refs1, refs2).isOK) && sameBoxed(info1, info2, refs1) && isSubInfo(info1, parent2) case _ => isSubType(info1, info2) @@ -871,12 +871,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // capt-capibility.scala and function-combinators.scala val singletonOK = tp1 match case tp1: SingletonType - if subCaptures(tp1.underlying.captureSet, refs2, frozen = true).isOK => + if subCaptures(tp1.underlying.captureSet, refs2, CaptureSet.VarState.Separate).isOK => recur(tp1.widen, tp2) case _ => false singletonOK - || subCaptures(refs1, refs2, frozenConstraint).isOK + || subCaptures(refs1, refs2).isOK && sameBoxed(tp1, tp2, refs1) && (recur(tp1.widen.stripCapturing, parent2) || tp1.isInstanceOf[SingletonType] && recur(tp1, parent2) @@ -2892,29 +2892,30 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling end inverse end MapExistentials - protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = + protected def makeVarState() = + if frozenConstraint then CaptureSet.VarState.Closed() else CaptureSet.VarState() + + protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, + vs: CaptureSet.VarState = makeVarState())(using Context): CaptureSet.CompareResult = try if assocExistentials.isEmpty then - refs1.subCaptures(refs2, frozen) + refs1.subCaptures(refs2, vs) else val mapped = refs1.map(MapExistentials(assocExistentials)) if mapped.elems.exists(Existential.isBadExistential) then CaptureSet.CompareResult.Fail(refs2 :: Nil) - else subCapturesMapped(mapped, refs2, frozen) + else mapped.subCaptures(refs2, vs) catch case ex: AssertionError => println(i"fail while subCaptures $refs1 <:< $refs2") throw ex - protected def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - refs1.subCaptures(refs2, frozen) - /** Is the boxing status of tp1 and tp2 the same, or alternatively, is * the capture sets `refs1` of `tp1` a subcapture of the empty set? * In the latter case, boxing status does not matter. */ protected def sameBoxed(tp1: Type, tp2: Type, refs1: CaptureSet)(using Context): Boolean = (tp1.isBoxedCapturing == tp2.isBoxedCapturing) - || refs1.subCaptures(CaptureSet.empty, frozenConstraint).isOK + || refs1.subCaptures(CaptureSet.empty, makeVarState()).isOK // ----------- Diagnostics -------------------------------------------------- @@ -3474,8 +3475,8 @@ object TypeComparer { def reduceMatchWith[T](op: MatchReducer => T)(using Context): T = comparing(_.reduceMatchWith(op)) - def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - comparing(_.subCaptures(refs1, refs2, frozen)) + def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState)(using Context): CaptureSet.CompareResult = + comparing(_.subCaptures(refs1, refs2, vs)) def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context) = comparing(_.subsumesExistentially(tp1, tp2)) @@ -3956,14 +3957,9 @@ class ExplainingTypeComparer(initctx: Context, short: Boolean) extends TypeCompa super.gadtAddBound(sym, b, isUpper) } - override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - traceIndented(i"subcaptures $refs1 <:< $refs2 ${if frozen then "frozen" else ""}") { - super.subCaptures(refs1, refs2, frozen) - } - - override def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - traceIndented(i"subcaptures mapped $refs1 <:< $refs2 ${if frozen then "frozen" else ""}") { - super.subCapturesMapped(refs1, refs2, frozen) + override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, vs: CaptureSet.VarState)(using Context): CaptureSet.CompareResult = + traceIndented(i"subcaptures $refs1 <:< $refs2, varState = ${vs.toString}") { + super.subCaptures(refs1, refs2, vs) } def lastTrace(header: String): String = header + { try b.toString finally b.clear() } diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 7ae790c62a2c..32dfff940ad7 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -19,7 +19,7 @@ import typer.Inferencing.* import typer.IfBottom import reporting.TestingReporter import cc.{CapturingType, derivedCapturingType, CaptureSet, captureSet, isBoxed, isBoxedCapturing} -import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} +import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap, VarState} import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -161,7 +161,7 @@ object TypeOps: TypeComparer.lub(simplify(l, theMap), simplify(r, theMap), isSoft = tp.isSoft) case tp @ CapturingType(parent, refs) => if !ctx.mode.is(Mode.Type) - && refs.subCaptures(parent.captureSet, frozen = true).isOK + && refs.subCaptures(parent.captureSet, VarState.Separate).isOK && (tp.isBoxed || !parent.isBoxedCapturing) // fuse types with same boxed status and outer boxed with any type then diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index c5937074f4bc..857714ffa940 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4178,7 +4178,7 @@ object Types extends TypeUtils { tl => params.map(p => tl.integrate(params, adaptParamInfo(p))), tl => tl.integrate(params, resultType)) - /** Adapt info of parameter symbol to be integhrated into corresponding MethodType + /** Adapt info of parameter symbol to be integrated into corresponding MethodType * using the scheme described in `fromSymbols`. */ def adaptParamInfo(param: Symbol, pinfo: Type)(using Context): Type = diff --git a/compiler/src/dotty/tools/dotc/printing/Formatting.scala b/compiler/src/dotty/tools/dotc/printing/Formatting.scala index ccd7b4e4e282..741b997d9926 100644 --- a/compiler/src/dotty/tools/dotc/printing/Formatting.scala +++ b/compiler/src/dotty/tools/dotc/printing/Formatting.scala @@ -8,7 +8,7 @@ import core.* import Texts.*, Types.*, Flags.*, Symbols.*, Contexts.* import Decorators.* import reporting.Message -import util.DiffUtil +import util.{DiffUtil, SimpleIdentitySet} import Highlighting.* object Formatting { @@ -87,6 +87,9 @@ object Formatting { def show(x: H *: T) = CtxShow(toStr(x.head) *: toShown(x.tail).asInstanceOf[Tuple]) + given [X <: AnyRef: Show]: Show[SimpleIdentitySet[X]] with + def show(x: SimpleIdentitySet[X]) = summon[Show[List[X]]].show(x.toList) + given Show[FlagSet] with def show(x: FlagSet) = x.flagsString diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 0f8e81154058..94656cc33bb2 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -27,6 +27,12 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def printDebug = ctx.settings.YprintDebug.value + /** Print Fresh.Cap instances as */ + protected def printFreshDetailed = printDebug + + /** Print Fresh.Cap instances as "fresh" */ + protected def printFresh = printFreshDetailed || ctx.property(PrintFresh).isDefined + private var openRecs: List[RecType] = Nil protected def maxToTextRecursions: Int = 100 @@ -153,12 +159,14 @@ class PlainPrinter(_ctx: Context) extends Printer { + defn.FromJavaObjectSymbol def toTextCaptureSet(cs: CaptureSet): Text = - if printDebug && ctx.settings.YccDebug.value && !cs.isConst then cs.toString + if printDebug && ctx.settings.YccDebug.value + && !cs.isConst && !cs.isInstanceOf[CaptureSet.HiddenSet] //HiddenSets can be cyclic + then cs.toString else if cs == CaptureSet.Fluid then "" else val core: Text = if !cs.isConst && cs.elems.isEmpty then "?" - else "{" ~ Text(cs.elems.toList.map(toTextCaptureRef), ", ") ~ "}" + else "{" ~ Text(cs.processElems(_.toList.map(toTextCaptureRef)), ", ") ~ "}" // ~ Str("?").provided(!cs.isConst) core ~ cs.optionalInfo @@ -202,14 +210,14 @@ class PlainPrinter(_ctx: Context) extends Printer { else toTextPrefixOf(tp) ~ selectionString(tp) case tp: TermParamRef => - ParamRefNameString(tp) ~ lambdaHash(tp.binder) ~ ".type" + ParamRefNameString(tp) ~ hashStr(tp.binder) ~ ".type" case tp: TypeParamRef => val suffix = if showNestingLevel then val tvar = ctx.typerState.constraint.typeVarOfParam(tp) if tvar.exists then s"#${tvar.asInstanceOf[TypeVar].nestingLevel.toString}" else "" else "" - ParamRefNameString(tp) ~ lambdaHash(tp.binder) ~ suffix + ParamRefNameString(tp) ~ hashStr(tp.binder) ~ suffix case tp: SingletonType => toTextSingleton(tp) case AppliedType(tycon, args) => @@ -248,9 +256,12 @@ class PlainPrinter(_ctx: Context) extends Printer { toText(parent) else val refsText = - if refs.isUniversal && (refs.elems.size == 1 || !printDebug) - then rootSetText - else toTextCaptureSet(refs) + if refs.isUniversal then + if refs.elems.size == 1 then rootSetText else toTextCaptureSet(refs) + else if !refs.elems.isEmpty && refs.elems.forall(_.isCapOrFresh) && !printFresh then + rootSetText + else + toTextCaptureSet(refs) toTextCapturing(parent, refsText, boxText) case tp @ RetainingType(parent, refs) => if Feature.ccEnabledSomewhere then @@ -282,19 +293,19 @@ class PlainPrinter(_ctx: Context) extends Printer { case ExprType(restp) => def arrowText: Text = restp match case AnnotatedType(parent, ann) if ann.symbol == defn.RetainsByNameAnnot => - val refs = ann.tree.retainedElems - if refs.exists(_.symbol == defn.captureRoot) then Str("=>") - else Str("->") ~ toTextRetainedElems(refs) + ann.tree.retainedElems match + case ref :: Nil if ref.symbol == defn.captureRoot => Str("=>") + case refs => Str("->") ~ toTextRetainedElems(refs) case _ => if Feature.pureFunsEnabled then "->" else "=>" changePrec(GlobalPrec)(arrowText ~ " " ~ toText(restp)) case tp: HKTypeLambda => changePrec(GlobalPrec) { - "[" ~ paramsText(tp) ~ "]" ~ lambdaHash(tp) ~ Str(" =>> ") ~ toTextGlobal(tp.resultType) + "[" ~ paramsText(tp) ~ "]" ~ hashStr(tp) ~ Str(" =>> ") ~ toTextGlobal(tp.resultType) } case tp: PolyType => changePrec(GlobalPrec) { - "[" ~ paramsText(tp) ~ "]" ~ lambdaHash(tp) ~ + "[" ~ paramsText(tp) ~ "]" ~ hashStr(tp) ~ (Str(": ") provided !tp.resultType.isInstanceOf[MethodOrPoly]) ~ toTextGlobal(tp.resultType) } @@ -345,7 +356,7 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def paramsText(lam: LambdaType): Text = { def paramText(ref: ParamRef) = val erased = ref.underlying.hasAnnotation(defn.ErasedParamAnnot) - keywordText("erased ").provided(erased) ~ ParamRefNameString(ref) ~ lambdaHash(lam) ~ toTextRHS(ref.underlying, isParameter = true) + keywordText("erased ").provided(erased) ~ ParamRefNameString(ref) ~ hashStr(lam) ~ toTextRHS(ref.underlying, isParameter = true) Text(lam.paramRefs.map(paramText), ", ") } @@ -357,11 +368,11 @@ class PlainPrinter(_ctx: Context) extends Printer { /** The name of the symbol without a unique id. */ protected def simpleNameString(sym: Symbol): String = nameString(sym.name) - /** If -uniqid is set, the hashcode of the lambda type, after a # */ - protected def lambdaHash(pt: LambdaType): Text = - if (showUniqueIds) - try "#" + pt.hashCode - catch { case ex: NullPointerException => "" } + /** If -uniqid is set, the hashcode of the type, after a # */ + protected def hashStr(tp: Type): String = + if showUniqueIds then + try "#" + tp.hashCode + catch case ex: NullPointerException => "" else "" /** A string to append to a symbol composed of: @@ -410,7 +421,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp @ ConstantType(value) => toText(value) case pref: TermParamRef => - ParamRefNameString(pref) ~ lambdaHash(pref.binder) + ParamRefNameString(pref) ~ hashStr(pref.binder) case tp: RecThis => val idx = openRecs.reverse.indexOf(tp.binder) if (idx >= 0) selfRecName(idx + 1) @@ -424,12 +435,16 @@ class PlainPrinter(_ctx: Context) extends Printer { def toTextCaptureRef(tp: Type): Text = homogenize(tp) match - case tp: TermRef if tp.symbol == defn.captureRoot => Str("cap") + case tp: TermRef if tp.symbol == defn.captureRoot => "cap" case tp: SingletonType => toTextRef(tp) case tp: (TypeRef | TypeParamRef) => toText(tp) ~ "^" case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd" case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" + case Fresh.Cap(hidden) => + if printFreshDetailed then s"" + else if printFresh then "fresh" + else "cap" case tp => toText(tp) protected def isOmittablePrefix(sym: Symbol): Boolean = diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 071d8fc94cd6..2c7f970908ba 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -336,7 +336,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { "?" ~ (("(ignored: " ~ toText(ignored) ~ ")") provided printDebug) case tp @ PolyProto(targs, resType) => "[applied to [" ~ toTextGlobal(targs, ", ") ~ "] returning " ~ toText(resType) - case ReachCapability(_) | MaybeCapability(_) | ReadOnlyCapability(_) => + case tp: AnnotatedType if tp.isTrackableRef => toTextCaptureRef(tp) case _ => super.toText(tp) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 8936c460de81..e8227f759ad4 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -167,7 +167,11 @@ abstract class Recheck extends Phase, SymTransformer: * from the current type. */ def setNuType(tpe: Type): Unit = - if nuTypes.lookup(tree) == null && (tpe ne tree.tpe) then nuTypes(tree) = tpe + if nuTypes.lookup(tree) == null then updNuType(tpe) + + /** Set new type of the tree unconditionally. */ + def updNuType(tpe: Type): Unit = + if tpe ne tree.tpe then nuTypes(tree) = tpe /** The new type of the tree, or if none was installed, the original type */ def nuType(using Context): Type = diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 414ff7d92653..4723fd745d6a 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -13,6 +13,7 @@ object MiMaFilters { ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.quotedPatternsWithPolymorphicFunctions"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$quotedPatternsWithPolymorphicFunctions$"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.runtime.Patterns.higherOrderHoleWithTypes"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.freshCapability"), ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.readOnlyCapability"), ), diff --git a/tests/neg-custom-args/captures/explain-under-approx.check b/tests/neg-custom-args/captures/explain-under-approx.check deleted file mode 100644 index f84ac5eb2b53..000000000000 --- a/tests/neg-custom-args/captures/explain-under-approx.check +++ /dev/null @@ -1,14 +0,0 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:12:10 ------------------------- -12 | col.add(Future(() => 25)) // error - | ^^^^^^^^^^^^^^^^ - | Found: Future[Int]{val a: (async : Async)}^{async} - | Required: Future[Int]^{col.futs*} - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:15:11 ------------------------- -15 | col1.add(Future(() => 25)) // error - | ^^^^^^^^^^^^^^^^ - | Found: Future[Int]{val a: (async : Async)}^{async} - | Required: Future[Int]^{col1.futs*} - | - | longer explanation available when compiling with `-explain` From bc2b33f66677e052a244d2d8dc4c1730202164cb Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 11 Jan 2025 19:56:29 +0100 Subject: [PATCH 7/8] Separation checking for applications Check separation from source 3.7 on. We currently only check applications, other areas of separation checking are still to be implemented. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 23 ++ .../src/dotty/tools/dotc/cc/SepCheck.scala | 202 ++++++++++++++++++ .../src/dotty/tools/dotc/cc/Synthetics.scala | 8 +- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../annotation/internal/freshCapability.scala | 7 + library/src/scala/caps.scala | 5 + .../src/scala/collection/IterableOnce.scala | 2 +- .../immutable/LazyListIterable.scala | 14 +- .../captures/box-adapt-cases.check | 15 +- .../captures/box-adapt-cases.scala | 3 +- .../captures/caseclass/Test_2.scala | 2 +- .../captures/cc-subst-param-exact.scala | 6 +- .../captures/depfun-reach.check | 4 +- .../captures/depfun-reach.scala | 2 +- .../captures/existential-mapping.check | 24 +-- .../captures/existential-mapping.scala | 2 +- .../captures/filevar-expanded.check | 19 ++ .../captures/filevar-expanded.scala | 4 +- tests/neg-custom-args/captures/filevar.check | 9 + tests/neg-custom-args/captures/i19330.check | 7 + tests/neg-custom-args/captures/i19330.scala | 6 +- tests/neg-custom-args/captures/i21614.check | 2 +- tests/neg-custom-args/captures/i21614.scala | 2 +- tests/neg-custom-args/captures/lazyref.check | 31 ++- tests/neg-custom-args/captures/lazyref.scala | 3 +- .../neg-custom-args/captures/outer-var.check | 22 +- .../neg-custom-args/captures/outer-var.scala | 1 + tests/neg-custom-args/captures/reaches.check | 14 ++ tests/neg-custom-args/captures/reaches.scala | 6 +- .../captures/sep-compose.check | 120 +++++++++++ .../captures/sep-compose.scala | 45 ++++ .../captures/sepchecks.scala} | 22 +- .../captures/unsound-reach-2.scala | 4 +- .../captures/unsound-reach-3.scala | 4 +- .../captures/unsound-reach-4.check | 7 + .../captures/unsound-reach-4.scala | 4 +- .../captures/unsound-reach.check | 7 + .../captures/unsound-reach.scala | 4 +- .../captures/update-call.scala | 19 ++ tests/neg-custom-args/captures/vars.check | 7 +- tests/neg-custom-args/captures/vars.scala | 2 +- .../captures/boxmap-paper.scala | 5 +- .../captures/cc-dep-param.scala | 3 +- tests/pos-custom-args/captures/foreach2.scala | 7 + .../captures/nested-classes-2.scala | 18 +- .../captures/sep-compose.scala | 21 ++ tests/pos-custom-args/captures/sep-eq.scala | 20 ++ .../captures/simple-apply.scala | 6 + tests/pos-custom-args/captures/skolems2.scala | 15 ++ tests/pos-special/stdlib/Test2.scala | 9 +- .../colltest5/CollectionStrawManCC5_1.scala | 24 ++- .../captures/colltest5/Test_2.scala | 6 +- 53 files changed, 712 insertions(+), 115 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/SepCheck.scala create mode 100644 library/src/scala/annotation/internal/freshCapability.scala create mode 100644 tests/neg-custom-args/captures/filevar-expanded.check rename tests/{pos-custom-args => neg-custom-args}/captures/filevar-expanded.scala (90%) create mode 100644 tests/neg-custom-args/captures/filevar.check create mode 100644 tests/neg-custom-args/captures/sep-compose.check create mode 100644 tests/neg-custom-args/captures/sep-compose.scala rename tests/{pos-custom-args/captures/readOnly.scala => neg-custom-args/captures/sepchecks.scala} (66%) create mode 100644 tests/neg-custom-args/captures/update-call.scala create mode 100644 tests/pos-custom-args/captures/foreach2.scala create mode 100644 tests/pos-custom-args/captures/sep-compose.scala create mode 100644 tests/pos-custom-args/captures/sep-eq.scala create mode 100644 tests/pos-custom-args/captures/simple-apply.scala create mode 100644 tests/pos-custom-args/captures/skolems2.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 55f8118e9b11..49eb73dd762e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -54,7 +54,7 @@ object ccConfig: /** If true, turn on separation checking */ def useFresh(using Context): Boolean = - Feature.sourceVersion.stable.isAtLeast(SourceVersion.`future`) + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`) end ccConfig diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 0b1d2397629b..d494bc8d9e22 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -242,6 +242,17 @@ object CheckCaptures: /** Was a new type installed for this tree? */ def hasNuType: Boolean + + /** Is this tree passed to a parameter or assigned to a value with a type + * that contains cap in no-flip covariant position, which will necessite + * a separation check? + */ + def needsSepCheck: Boolean + + /** If a tree is an argument for which needsSepCheck is true, + * the type of the formal paremeter corresponding to the argument. + */ + def formalType: Type end CheckerAPI class CheckCaptures extends Recheck, SymTransformer: @@ -282,6 +293,15 @@ class CheckCaptures extends Recheck, SymTransformer: */ private val todoAtPostCheck = new mutable.ListBuffer[() => Unit] + /** Maps trees that need a separation check because they are arguments to + * polymorphic parameters. The trees are mapped to the formal parameter type. + */ + private val sepCheckFormals = util.EqHashMap[Tree, Type]() + + extension [T <: Tree](tree: T) + def needsSepCheck: Boolean = sepCheckFormals.contains(tree) + def formalType: Type = sepCheckFormals.getOrElse(tree, NoType) + /** Instantiate capture set variables appearing contra-variantly to their * upper approximation. */ @@ -662,6 +682,8 @@ class CheckCaptures extends Recheck, SymTransformer: // The @use annotation is added to `formal` by `prepareFunction` capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") markFree(argType.deepCaptureSet, arg.srcPos) + if formal.containsCap then + sepCheckFormals(arg) = freshenedFormal argType /** Map existential captures in result to `cap` and implement the following @@ -1786,6 +1808,7 @@ class CheckCaptures extends Recheck, SymTransformer: end checker checker.traverse(unit)(using ctx.withOwner(defn.RootClass)) + if ccConfig.useFresh then SepChecker(this).traverse(unit) if !ctx.reporter.errorsReported then // We dont report errors here if previous errors were reported, because other // errors often result in bad applied types, but flagging these bad types gives diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala new file mode 100644 index 000000000000..9f5e8187d1d0 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -0,0 +1,202 @@ +package dotty.tools +package dotc +package cc +import ast.tpd +import collection.mutable + +import core.* +import Symbols.*, Types.* +import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* +import CaptureSet.{Refs, emptySet} +import config.Printers.capt +import StdNames.nme + +class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: + import tpd.* + import checker.* + + extension (refs: Refs) + private def footprint(using Context): Refs = + def recur(elems: Refs, newElems: List[CaptureRef]): Refs = newElems match + case newElem :: newElems1 => + val superElems = newElem.captureSetOfInfo.elems.filter: superElem => + !superElem.isMaxCapability && !elems.contains(superElem) + recur(elems ++ superElems, newElems1 ++ superElems.toList) + case Nil => elems + val elems: Refs = refs.filter(!_.isMaxCapability) + recur(elems, elems.toList) + + private def overlapWith(other: Refs)(using Context): Refs = + val refs1 = refs + val refs2 = other + def common(refs1: Refs, refs2: Refs) = + refs1.filter: ref => + ref.isExclusive && refs2.exists(_.stripReadOnly eq ref) + common(refs, other) ++ common(other, refs) + + private def hidden(refs: Refs)(using Context): Refs = + val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet + + def hiddenByElem(elem: CaptureRef): Refs = + if seen.add(elem) then elem match + case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs.elems) + case ReadOnlyCapability(ref) => hiddenByElem(ref).map(_.readOnly) + case _ => emptySet + else emptySet + + def recur(cs: Refs): Refs = + (emptySet /: cs): (elems, elem) => + elems ++ hiddenByElem(elem) + + recur(refs) + end hidden + + /** The captures of an argument or prefix widened to the formal parameter, if + * the latter contains a cap. + */ + private def formalCaptures(arg: Tree)(using Context): Refs = + val argType = arg.formalType.orElse(arg.nuType) + (if arg.nuType.hasUseAnnot then argType.deepCaptureSet else argType.captureSet) + .elems + + /** The captures of an argument of prefix. No widening takes place */ + private def actualCaptures(arg: Tree)(using Context): Refs = + val argType = arg.nuType + (if argType.hasUseAnnot then argType.deepCaptureSet else argType.captureSet) + .elems + + private def sepError(fn: Tree, args: List[Tree], argIdx: Int, + overlap: Refs, hiddenInArg: Refs, footprints: List[(Refs, Int)], + deps: collection.Map[Tree, List[Tree]])(using Context): Unit = + val arg = args(argIdx) + def paramName(mt: Type, idx: Int): Option[Name] = mt match + case mt @ MethodType(pnames) => + if idx < pnames.length then Some(pnames(idx)) else paramName(mt.resType, idx - pnames.length) + case mt: PolyType => paramName(mt.resType, idx) + case _ => None + def formalName = paramName(fn.nuType.widen, argIdx) match + case Some(pname) => i"$pname " + case _ => "" + def whatStr = if overlap.size == 1 then "this capability is" else "these capabilities are" + def funStr = + if fn.symbol.exists then i"${fn.symbol}: ${fn.symbol.info}" + else i"a function of type ${fn.nuType.widen}" + val clashIdx = footprints + .collect: + case (fp, idx) if !hiddenInArg.overlapWith(fp).isEmpty => idx + .head + def whereStr = clashIdx match + case 0 => "function prefix" + case 1 => "first argument " + case 2 => "second argument" + case 3 => "third argument " + case n => s"${n}th argument " + def clashTree = + if clashIdx == 0 then methPart(fn).asInstanceOf[Select].qualifier + else args(clashIdx - 1) + def clashType = clashTree.nuType + def clashCaptures = actualCaptures(clashTree) + def hiddenCaptures = hidden(formalCaptures(arg)) + def clashFootprint = clashCaptures.footprint + def hiddenFootprint = hiddenCaptures.footprint + def declaredFootprint = deps(arg).map(actualCaptures(_)).foldLeft(emptySet)(_ ++ _).footprint + def footprintOverlap = hiddenFootprint.overlapWith(clashFootprint) -- declaredFootprint + report.error( + em"""Separation failure: argument of type ${arg.nuType} + |to $funStr + |corresponds to capture-polymorphic formal parameter ${formalName}of type ${arg.formalType} + |and captures ${CaptureSet(overlap)}, but $whatStr also passed separately + |in the ${whereStr.trim} with type $clashType. + | + | Capture set of $whereStr : ${CaptureSet(clashCaptures)} + | Hidden set of current argument : ${CaptureSet(hiddenCaptures)} + | Footprint of $whereStr : ${CaptureSet(clashFootprint)} + | Hidden footprint of current argument : ${CaptureSet(hiddenFootprint)} + | Declared footprint of current argument: ${CaptureSet(declaredFootprint)} + | Undeclared overlap of footprints : ${CaptureSet(footprintOverlap)}""", + arg.srcPos) + end sepError + + private def checkApply(fn: Tree, args: List[Tree], deps: collection.Map[Tree, List[Tree]])(using Context): Unit = + val fnCaptures = methPart(fn) match + case Select(qual, _) => qual.nuType.captureSet + case _ => CaptureSet.empty + capt.println(i"check separate $fn($args), fnCaptures = $fnCaptures, argCaptures = ${args.map(arg => CaptureSet(formalCaptures(arg)))}, deps = ${deps.toList}") + var footprint = fnCaptures.elems.footprint + val footprints = mutable.ListBuffer[(Refs, Int)]((footprint, 0)) + val indexedArgs = args.zipWithIndex + + def subtractDeps(elems: Refs, arg: Tree): Refs = + deps(arg).foldLeft(elems): (elems, dep) => + elems -- actualCaptures(dep).footprint + + for (arg, idx) <- indexedArgs do + if !arg.needsSepCheck then + footprint = footprint ++ subtractDeps(actualCaptures(arg).footprint, arg) + footprints += ((footprint, idx + 1)) + for (arg, idx) <- indexedArgs do + if arg.needsSepCheck then + val ac = formalCaptures(arg) + val hiddenInArg = hidden(ac).footprint + //println(i"check sep $arg: $ac, footprint so far = $footprint, hidden = $hiddenInArg") + val overlap = subtractDeps(hiddenInArg.overlapWith(footprint), arg) + if !overlap.isEmpty then + sepError(fn, args, idx, overlap, hiddenInArg, footprints.toList, deps) + footprint ++= actualCaptures(arg).footprint + footprints += ((footprint, idx + 1)) + end checkApply + + private def collectMethodTypes(tp: Type): List[TermLambda] = tp match + case tp: MethodType => tp :: collectMethodTypes(tp.resType) + case tp: PolyType => collectMethodTypes(tp.resType) + case _ => Nil + + private def dependencies(fn: Tree, argss: List[List[Tree]])(using Context): collection.Map[Tree, List[Tree]] = + val mtpe = + if fn.symbol.exists then fn.symbol.info + else fn.tpe.widen // happens for PolyFunction applies + val mtps = collectMethodTypes(mtpe) + assert(mtps.hasSameLengthAs(argss), i"diff for $fn: ${fn.symbol} /// $mtps /// $argss") + val mtpsWithArgs = mtps.zip(argss) + val argMap = mtpsWithArgs.toMap + val deps = mutable.HashMap[Tree, List[Tree]]().withDefaultValue(Nil) + for + (mt, args) <- mtpsWithArgs + (formal, arg) <- mt.paramInfos.zip(args) + dep <- formal.captureSet.elems.toList + do + val referred = dep match + case dep: TermParamRef => + argMap(dep.binder)(dep.paramNum) :: Nil + case dep: ThisType if dep.cls == fn.symbol.owner => + val Select(qual, _) = fn: @unchecked + qual :: Nil + case _ => + Nil + deps(arg) ++= referred + deps + + private def traverseApply(tree: Tree, argss: List[List[Tree]])(using Context): Unit = tree match + case Apply(fn, args) => traverseApply(fn, args :: argss) + case TypeApply(fn, args) => traverseApply(fn, argss) // skip type arguments + case _ => + if argss.nestedExists(_.needsSepCheck) then + checkApply(tree, argss.flatten, dependencies(tree, argss)) + + def traverse(tree: Tree)(using Context): Unit = + tree match + case tree: GenericApply => + if tree.symbol != defn.Caps_unsafeAssumeSeparate then + tree.tpe match + case _: MethodOrPoly => + case _ => traverseApply(tree, Nil) + traverseChildren(tree) + case _ => + traverseChildren(tree) +end SepChecker + + + + + + diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 9e2729eb7f31..cfdcbbc401bf 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -132,8 +132,9 @@ object Synthetics: val (pt: PolyType) = info: @unchecked val (mt: MethodType) = pt.resType: @unchecked val (enclThis: ThisType) = owner.thisType: @unchecked + val paramCaptures = CaptureSet(enclThis, defn.captureRoot.termRef) pt.derivedLambdaType(resType = MethodType(mt.paramNames)( - mt1 => mt.paramInfos.map(_.capturing(CaptureSet.universal)), + mt1 => mt.paramInfos.map(_.capturing(paramCaptures)), mt1 => CapturingType(mt.resType, CaptureSet(enclThis, mt1.paramRefs.head)))) def transformCurriedTupledCaptures(info: Type, owner: Symbol) = @@ -148,7 +149,10 @@ object Synthetics: ExprType(mapFinalResult(et.resType, CapturingType(_, CaptureSet(enclThis)))) def transformCompareCaptures = - MethodType(defn.ObjectType.capturing(CaptureSet.universal) :: Nil, defn.BooleanType) + val (enclThis: ThisType) = symd.owner.thisType: @unchecked + MethodType( + defn.ObjectType.capturing(CaptureSet(defn.captureRoot.termRef, enclThis)) :: Nil, + defn.BooleanType) symd.copySymDenotation(info = symd.name match case DefaultGetterName(nme.copy, n) => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index f45abe9c61f2..7381eda18dcf 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1000,6 +1000,7 @@ class Definitions { @tu lazy val Caps_Exists: ClassSymbol = requiredClass("scala.caps.Exists") @tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe") @tu lazy val Caps_unsafeAssumePure: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumePure") + @tu lazy val Caps_unsafeAssumeSeparate: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumeSeparate") @tu lazy val Caps_ContainsTrait: TypeSymbol = CapsModule.requiredType("Contains") @tu lazy val Caps_containsImpl: TermSymbol = CapsModule.requiredMethod("containsImpl") @tu lazy val Caps_Mutable: ClassSymbol = requiredClass("scala.caps.Mutable") diff --git a/library/src/scala/annotation/internal/freshCapability.scala b/library/src/scala/annotation/internal/freshCapability.scala new file mode 100644 index 000000000000..a25eee4f4c6d --- /dev/null +++ b/library/src/scala/annotation/internal/freshCapability.scala @@ -0,0 +1,7 @@ +package scala.annotation +package internal + +/** An annotation used internally for fresh capability wrappers of `cap` + */ +class freshCapability extends StaticAnnotation + diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index fb4bacd1a948..9d0a8883cde9 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -79,4 +79,9 @@ import annotation.{experimental, compileTimeOnly, retainsCap} */ def unsafeAssumePure: T = x + /** A wrapper around code for which separation checks are suppressed. + */ + def unsafeAssumeSeparate[T](op: T): T = op + end unsafe +end caps \ No newline at end of file diff --git a/scala2-library-cc/src/scala/collection/IterableOnce.scala b/scala2-library-cc/src/scala/collection/IterableOnce.scala index 7e8555421c53..7ea62a9e1a65 100644 --- a/scala2-library-cc/src/scala/collection/IterableOnce.scala +++ b/scala2-library-cc/src/scala/collection/IterableOnce.scala @@ -805,7 +805,7 @@ trait IterableOnceOps[+A, +CC[_], +C] extends Any { this: IterableOnce[A]^ => case _ => Some(reduceLeft(op)) } private final def reduceLeftOptionIterator[B >: A](op: (B, A) => B): Option[B] = reduceOptionIterator[A, B](iterator)(op) - private final def reduceOptionIterator[X >: A, B >: X](it: Iterator[X]^)(op: (B, X) => B): Option[B] = { + private final def reduceOptionIterator[X >: A, B >: X](it: Iterator[X]^{this, caps.cap})(op: (B, X) => B): Option[B] = { if (it.hasNext) { var acc: B = it.next() while (it.hasNext) diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index 28ce8da104aa..cae2f4299e87 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -25,6 +25,7 @@ import scala.runtime.Statics import language.experimental.captureChecking import annotation.unchecked.uncheckedCaptures import caps.untrackedCaptures +import caps.unsafe.unsafeAssumeSeparate /** This class implements an immutable linked list. We call it "lazy" * because it computes its elements only when they are needed. @@ -879,6 +880,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz if (!cursor.stateDefined) b.append(sep).append("") } else { @inline def same(a: LazyListIterable[A]^, b: LazyListIterable[A]^): Boolean = (a eq b) || (a.state eq b.state) + // !!!CC with qualifiers, same should have cap.rd parameters // Cycle. // If we have a prefix of length P followed by a cycle of length C, // the scout will be at position (P%C) in the cycle when the cursor @@ -890,7 +892,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz // the start of the loop. var runner = this var k = 0 - while (!same(runner, scout)) { + while (!unsafeAssumeSeparate(same(runner, scout))) { runner = runner.tail scout = scout.tail k += 1 @@ -900,11 +902,11 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz // everything once. If cursor is already at beginning, we'd better // advance one first unless runner didn't go anywhere (in which case // we've already looped once). - if (same(cursor, scout) && (k > 0)) { + if (unsafeAssumeSeparate(same(cursor, scout)) && (k > 0)) { appendCursorElement() cursor = cursor.tail } - while (!same(cursor, scout)) { + while (!unsafeAssumeSeparate(same(cursor, scout))) { appendCursorElement() cursor = cursor.tail } @@ -1052,7 +1054,9 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { val head = it.next() rest = rest.tail restRef = rest // restRef.elem = rest - sCons(head, newLL(stateFromIteratorConcatSuffix(it)(flatMapImpl(rest, f).state))) + sCons(head, newLL( + unsafeAssumeSeparate( + stateFromIteratorConcatSuffix(it)(flatMapImpl(rest, f).state)))) } else State.Empty } } @@ -1181,7 +1185,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { def iterate[A](start: => A)(f: A => A): LazyListIterable[A]^{start, f} = newLL { val head = start - sCons(head, iterate(f(head))(f)) + sCons(head, unsafeAssumeSeparate(iterate(f(head))(f))) } /** diff --git a/tests/neg-custom-args/captures/box-adapt-cases.check b/tests/neg-custom-args/captures/box-adapt-cases.check index 7ff185c499a5..e5cadb051ac1 100644 --- a/tests/neg-custom-args/captures/box-adapt-cases.check +++ b/tests/neg-custom-args/captures/box-adapt-cases.check @@ -1,12 +1,19 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:14:10 ------------------------------ -14 | x.value(cap => cap.use()) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:8:10 ------------------------------- +8 | x.value(cap => cap.use()) // error, was OK + | ^^^^^^^^^^^^^^^^ + | Found: (cap: box Cap^?) => Int + | Required: (cap: box Cap^) ->{fresh} Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:15:10 ------------------------------ +15 | x.value(cap => cap.use()) // error | ^^^^^^^^^^^^^^^^ | Found: (cap: box Cap^?) ->{io} Int | Required: (cap: box Cap^{io}) -> Int | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:28:10 ------------------------------ -28 | x.value(cap => cap.use()) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:29:10 ------------------------------ +29 | x.value(cap => cap.use()) // error | ^^^^^^^^^^^^^^^^ | Found: (cap: box Cap^?) ->{io, fs} Int | Required: (cap: box Cap^{io, fs}) ->{io} Int diff --git a/tests/neg-custom-args/captures/box-adapt-cases.scala b/tests/neg-custom-args/captures/box-adapt-cases.scala index 8f7d7a0a6667..55371c4e50b7 100644 --- a/tests/neg-custom-args/captures/box-adapt-cases.scala +++ b/tests/neg-custom-args/captures/box-adapt-cases.scala @@ -1,10 +1,11 @@ +import language.`3.7` // sepchecks on trait Cap { def use(): Int } def test1(): Unit = { class Id[X](val value: [T] -> (op: X => T) -> T) val x: Id[Cap^] = ??? - x.value(cap => cap.use()) + x.value(cap => cap.use()) // error, was OK } def test2(io: Cap^): Unit = { diff --git a/tests/neg-custom-args/captures/caseclass/Test_2.scala b/tests/neg-custom-args/captures/caseclass/Test_2.scala index e54ab1774202..8c13a0d831ef 100644 --- a/tests/neg-custom-args/captures/caseclass/Test_2.scala +++ b/tests/neg-custom-args/captures/caseclass/Test_2.scala @@ -5,7 +5,7 @@ def test(c: C) = val mixed: () ->{c} Unit = pure val x = Ref(impure) val _: Ref = x // error - val y = x.copy() + val y = caps.unsafe.unsafeAssumeSeparate(x.copy()) // TODO remove val yc: Ref = y // error val y0 = x.copy(pure) val yc0: Ref = y0 diff --git a/tests/neg-custom-args/captures/cc-subst-param-exact.scala b/tests/neg-custom-args/captures/cc-subst-param-exact.scala index 35e4acb95fdc..08a3efaaffdf 100644 --- a/tests/neg-custom-args/captures/cc-subst-param-exact.scala +++ b/tests/neg-custom-args/captures/cc-subst-param-exact.scala @@ -5,13 +5,13 @@ trait Ref[T] { def set(x: T): T } def test() = { def swap[T](x: Ref[T]^)(y: Ref[T]^{x}): Unit = ??? - def foo[T](x: Ref[T]^): Unit = + def foo[T](x: Ref[T]^{cap.rd}): Unit = swap(x)(x) - def bar[T](x: () => Ref[T]^)(y: Ref[T]^{x}): Unit = + def bar[T](x: () => Ref[T]^{cap.rd})(y: Ref[T]^{x}): Unit = swap(x())(y) // error - def baz[T](x: Ref[T]^)(y: Ref[T]^{x}): Unit = + def baz[T](x: Ref[T]^{cap.rd})(y: Ref[T]^{x}): Unit = swap(x)(y) } diff --git a/tests/neg-custom-args/captures/depfun-reach.check b/tests/neg-custom-args/captures/depfun-reach.check index c1d7d05dc8d6..676ca7c5104f 100644 --- a/tests/neg-custom-args/captures/depfun-reach.check +++ b/tests/neg-custom-args/captures/depfun-reach.check @@ -2,13 +2,13 @@ 13 | op // error | ^^ | Found: (xs: List[(X, box () ->{io} Unit)]) ->{op} List[box () ->{xs*} Unit] - | Required: (xs: List[(X, box () ->{io} Unit)]) => List[() -> Unit] + | Required: (xs: List[(X, box () ->{io} Unit)]) ->{fresh} List[() -> Unit] | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/depfun-reach.scala:20:60 --------------------------------- 20 | val b: (xs: List[() ->{io} Unit]) => List[() ->{} Unit] = a // error | ^ | Found: (xs: List[box () ->{io} Unit]) ->{a} List[box () ->{xs*} Unit] - | Required: (xs: List[box () ->{io} Unit]) => List[() -> Unit] + | Required: (xs: List[box () ->{io} Unit]) ->{fresh} List[() -> Unit] | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/depfun-reach.scala b/tests/neg-custom-args/captures/depfun-reach.scala index 94b10f7dbcdb..6c198ff8fd9f 100644 --- a/tests/neg-custom-args/captures/depfun-reach.scala +++ b/tests/neg-custom-args/captures/depfun-reach.scala @@ -1,6 +1,6 @@ import language.experimental.captureChecking import caps.cap - +import language.`3.7` // sepchecks on def test(io: Object^, async: Object^) = def compose(op: List[(() ->{cap} Unit, () ->{cap} Unit)]): List[() ->{op*} Unit] = List(() => op.foreach((f,g) => { f(); g() })) diff --git a/tests/neg-custom-args/captures/existential-mapping.check b/tests/neg-custom-args/captures/existential-mapping.check index 30836bc427cf..b52fdb5750ed 100644 --- a/tests/neg-custom-args/captures/existential-mapping.check +++ b/tests/neg-custom-args/captures/existential-mapping.check @@ -47,42 +47,42 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:27:25 -------------------------- 27 | val _: (x: C^) => C = y1 // error | ^^ - | Found: (y1 : (x: C^) => (ex$41: caps.Exists) -> C^{ex$41}) - | Required: (x: C^) => C + | Found: (y1 : (x: C^) ->{fresh} (ex$41: caps.Exists) -> C^{ex$41}) + | Required: (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:30:20 -------------------------- 30 | val _: C^ => C = y2 // error | ^^ - | Found: (y2 : C^ => (ex$45: caps.Exists) -> C^{ex$45}) - | Required: C^ => C + | Found: (y2 : C^ ->{fresh} (ex$45: caps.Exists) -> C^{ex$45}) + | Required: C^ ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:33:30 -------------------------- 33 | val _: A^ => (x: C^) => C = y3 // error | ^^ - | Found: (y3 : A^ => (ex$50: caps.Exists) -> (x: C^) ->{ex$50} (ex$49: caps.Exists) -> C^{ex$49}) - | Required: A^ => (ex$53: caps.Exists) -> (x: C^) ->{ex$53} C + | Found: (y3 : A^ ->{fresh} (ex$50: caps.Exists) -> (x: C^) ->{ex$50} (ex$49: caps.Exists) -> C^{ex$49}) + | Required: A^ ->{fresh} (ex$53: caps.Exists) -> (x: C^) ->{ex$53} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:36:25 -------------------------- 36 | val _: A^ => C^ => C = y4 // error | ^^ - | Found: (y4 : A^ => (ex$56: caps.Exists) -> C^ ->{ex$56} (ex$55: caps.Exists) -> C^{ex$55}) - | Required: A^ => (ex$59: caps.Exists) -> C^ ->{ex$59} C + | Found: (y4 : A^ ->{fresh} (ex$56: caps.Exists) -> C^ ->{ex$56} (ex$55: caps.Exists) -> C^{ex$55}) + | Required: A^ ->{fresh} (ex$59: caps.Exists) -> C^ ->{ex$59} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:39:30 -------------------------- 39 | val _: A^ => (x: C^) -> C = y5 // error | ^^ - | Found: (y5 : A^ => (x: C^) -> (ex$61: caps.Exists) -> C^{ex$61}) - | Required: A^ => (x: C^) -> C + | Found: (y5 : A^ ->{fresh} (x: C^) -> (ex$61: caps.Exists) -> C^{ex$61}) + | Required: A^ ->{fresh} (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/existential-mapping.scala:42:30 -------------------------- 42 | val _: A^ => (x: C^) => C = y6 // error | ^^ - | Found: (y6 : A^ => (ex$70: caps.Exists) -> (x: C^) ->{ex$70} (ex$69: caps.Exists) -> C^{ex$69}) - | Required: A^ => (ex$73: caps.Exists) -> (x: C^) ->{ex$73} C + | Found: (y6 : A^ ->{fresh} (ex$70: caps.Exists) -> (x: C^) ->{ex$70} (ex$69: caps.Exists) -> C^{ex$69}) + | Required: A^ ->{fresh} (ex$73: caps.Exists) -> (x: C^) ->{ex$73} C | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/existential-mapping.scala b/tests/neg-custom-args/captures/existential-mapping.scala index 290f7dc767a6..aa45e60cdabc 100644 --- a/tests/neg-custom-args/captures/existential-mapping.scala +++ b/tests/neg-custom-args/captures/existential-mapping.scala @@ -1,5 +1,5 @@ import language.experimental.captureChecking - +import language.`3.7` // sepchecks on class A class C type Fun[X] = (x: C^) -> X diff --git a/tests/neg-custom-args/captures/filevar-expanded.check b/tests/neg-custom-args/captures/filevar-expanded.check new file mode 100644 index 000000000000..e1991890f6fa --- /dev/null +++ b/tests/neg-custom-args/captures/filevar-expanded.check @@ -0,0 +1,19 @@ +-- Error: tests/neg-custom-args/captures/filevar-expanded.scala:34:19 -------------------------------------------------- +34 | withFile(io3): f => // error: separation failure + | ^ + | Separation failure: argument of type (f: test2.File^{io3}) ->{io3} Unit + | to method withFile: [T](io2: test2.IO^)(op: (f: test2.File^{io2}) => T): T + | corresponds to capture-polymorphic formal parameter op of type (f: test2.File^{io3}) => Unit + | and captures {io3}, but this capability is also passed separately + | in the first argument with type (io3 : test2.IO^). + | + | Capture set of first argument : {io3} + | Hidden set of current argument : {io3} + | Footprint of first argument : {io3} + | Hidden footprint of current argument : {io3} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {io3} +35 | val o = Service(io3) +36 | o.file = f // this is a bit dubious. It's legal since we treat class refinements +37 | // as capture set variables that can be made to include refs coming from outside. +38 | o.log diff --git a/tests/pos-custom-args/captures/filevar-expanded.scala b/tests/neg-custom-args/captures/filevar-expanded.scala similarity index 90% rename from tests/pos-custom-args/captures/filevar-expanded.scala rename to tests/neg-custom-args/captures/filevar-expanded.scala index 58e7a0e67e0a..c42f9478256f 100644 --- a/tests/pos-custom-args/captures/filevar-expanded.scala +++ b/tests/neg-custom-args/captures/filevar-expanded.scala @@ -1,7 +1,7 @@ import language.experimental.captureChecking import language.experimental.modularity import compiletime.uninitialized - +import language.future // sepchecks on object test1: class File: def write(x: String): Unit = ??? @@ -31,7 +31,7 @@ object test2: op(new File) def test(io3: IO^) = - withFile(io3): f => + withFile(io3): f => // error: separation failure val o = Service(io3) o.file = f // this is a bit dubious. It's legal since we treat class refinements // as capture set variables that can be made to include refs coming from outside. diff --git a/tests/neg-custom-args/captures/filevar.check b/tests/neg-custom-args/captures/filevar.check new file mode 100644 index 000000000000..22efd36053b4 --- /dev/null +++ b/tests/neg-custom-args/captures/filevar.check @@ -0,0 +1,9 @@ +-- Error: tests/neg-custom-args/captures/filevar.scala:8:6 ------------------------------------------------------------- +8 | var file: File^ = uninitialized // error, was OK under unsealed + | ^ + | Mutable variable file cannot have type File^ since + | that type captures the root capability `cap`. +-- Warning: tests/neg-custom-args/captures/filevar.scala:11:55 --------------------------------------------------------- +11 |def withFile[T](op: (l: caps.Capability) ?-> (f: File^{l}) => T): T = + | ^ + | redundant capture: File already accounts for l.type diff --git a/tests/neg-custom-args/captures/i19330.check b/tests/neg-custom-args/captures/i19330.check index a8925b117611..78219e0316ee 100644 --- a/tests/neg-custom-args/captures/i19330.check +++ b/tests/neg-custom-args/captures/i19330.check @@ -3,3 +3,10 @@ | ^^^ | Type variable T of method usingLogger cannot be instantiated to x.T since | the part () => Logger^ of that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i19330.scala:22:22 --------------------------------------- +22 | val bad: bar.T = foo(bar) // error + | ^^^^^^^^ + | Found: () => Logger^ + | Required: () ->{fresh} (ex$9: caps.Exists) -> Logger^{ex$9} + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i19330.scala b/tests/neg-custom-args/captures/i19330.scala index 715b670860cd..23fcfa0ffc4f 100644 --- a/tests/neg-custom-args/captures/i19330.scala +++ b/tests/neg-custom-args/captures/i19330.scala @@ -1,7 +1,7 @@ - - +import language.`3.7` // sepchecks on import language.experimental.captureChecking + trait Logger def usingLogger[T](op: Logger^ => T): T = ??? @@ -19,5 +19,5 @@ def foo(x: Foo): x.T = def test(): Unit = val bar = new Bar - val bad: bar.T = foo(bar) + val bad: bar.T = foo(bar) // error val leaked: Logger^ = bad() // leaked scoped capability! diff --git a/tests/neg-custom-args/captures/i21614.check b/tests/neg-custom-args/captures/i21614.check index f7b45ddf0eaa..109283eae01f 100644 --- a/tests/neg-custom-args/captures/i21614.check +++ b/tests/neg-custom-args/captures/i21614.check @@ -2,7 +2,7 @@ 12 | files.map((f: F) => new Logger(f)) // error, Q: can we make this pass (see #19076)? | ^^^^^^^^^^^^^^^^^^^^^^^ | Found: (f: F) ->{files.rd*} box Logger{val f²: File^?}^? - | Required: (f: box F^{files.rd*}) => box Logger{val f²: File^?}^? + | Required: (f: box F^{files.rd*}) ->{fresh} box Logger{val f²: File^?}^? | | where: f is a reference to a value parameter | f² is a value in class Logger diff --git a/tests/neg-custom-args/captures/i21614.scala b/tests/neg-custom-args/captures/i21614.scala index f5bab90f543b..d21fb2f5d3a0 100644 --- a/tests/neg-custom-args/captures/i21614.scala +++ b/tests/neg-custom-args/captures/i21614.scala @@ -1,7 +1,7 @@ import language.experimental.captureChecking import caps.Capability import caps.use - +import language.`3.7` // sepchecks on trait List[+T]: def map[U](f: T => U): List[U] diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index 8683615c07d8..85a76bf5a87c 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -1,28 +1,43 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:19:28 -------------------------------------- -19 | val ref1c: LazyRef[Int] = ref1 // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:20:28 -------------------------------------- +20 | val ref1c: LazyRef[Int] = ref1 // error | ^^^^ | Found: (ref1 : LazyRef[Int]{val elem: () ->{cap1} Int}^{cap1}) | Required: LazyRef[Int] | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:21:35 -------------------------------------- -21 | val ref2c: LazyRef[Int]^{cap2} = ref2 // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:22:35 -------------------------------------- +22 | val ref2c: LazyRef[Int]^{cap2} = ref2 // error | ^^^^ | Found: LazyRef[Int]{val elem: () ->{ref2*} Int}^{ref2} | Required: LazyRef[Int]^{cap2} | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:23:35 -------------------------------------- -23 | val ref3c: LazyRef[Int]^{ref1} = ref3 // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:24:35 -------------------------------------- +24 | val ref3c: LazyRef[Int]^{ref1} = ref3 // error | ^^^^ | Found: LazyRef[Int]{val elem: () ->{ref3*} Int}^{ref3} | Required: LazyRef[Int]^{ref1} | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:25:35 -------------------------------------- -25 | val ref4c: LazyRef[Int]^{cap1} = ref4 // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:26:35 -------------------------------------- +26 | val ref4c: LazyRef[Int]^{cap1} = ref4 // error | ^^^^ | Found: LazyRef[Int]{val elem: () ->{ref4*} Int}^{ref4} | Required: LazyRef[Int]^{cap1} | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/lazyref.scala:25:55 ----------------------------------------------------------- +25 | val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) // error: separation failure + | ^ + |Separation failure: argument of type (x: Int) ->{cap2} Int + |to method map: [U](f: T => U): LazyRef[U]^{f, LazyRef.this} + |corresponds to capture-polymorphic formal parameter f of type Int => Int + |and captures {cap2}, but this capability is also passed separately + |in the function prefix with type (LazyRef[Int]{val elem: () ->{ref2*} Int} | (ref1 : LazyRef[Int]{val elem: () ->{cap1} Int}^{cap1}))^{ref2}. + | + | Capture set of function prefix : {ref1, ref2} + | Hidden set of current argument : {cap2} + | Footprint of function prefix : {ref1, ref2, cap1, cap2} + | Hidden footprint of current argument : {cap2} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {cap2} diff --git a/tests/neg-custom-args/captures/lazyref.scala b/tests/neg-custom-args/captures/lazyref.scala index 99aa10d5d2b2..52e274b65175 100644 --- a/tests/neg-custom-args/captures/lazyref.scala +++ b/tests/neg-custom-args/captures/lazyref.scala @@ -1,3 +1,4 @@ +import language.`3.7` // sepchecks on class CC type Cap = CC^ @@ -21,5 +22,5 @@ def test(cap1: Cap, cap2: Cap) = val ref2c: LazyRef[Int]^{cap2} = ref2 // error val ref3 = ref1.map(g) val ref3c: LazyRef[Int]^{ref1} = ref3 // error - val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) + val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) // error: separation failure val ref4c: LazyRef[Int]^{cap1} = ref4 // error diff --git a/tests/neg-custom-args/captures/outer-var.check b/tests/neg-custom-args/captures/outer-var.check index b24579b7a69f..0c86213ff118 100644 --- a/tests/neg-custom-args/captures/outer-var.check +++ b/tests/neg-custom-args/captures/outer-var.check @@ -1,5 +1,5 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:11:8 ------------------------------------- -11 | x = q // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:12:8 ------------------------------------- +12 | x = q // error | ^ | Found: (q : () => Unit) | Required: () ->{p, q²} Unit @@ -8,15 +8,15 @@ | q² is a parameter in method test | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:12:9 ------------------------------------- -12 | x = (q: Proc) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:13:9 ------------------------------------- +13 | x = (q: Proc) // error | ^^^^^^^ | Found: () => Unit | Required: () ->{p, q} Unit | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:13:9 ------------------------------------- -13 | y = (q: Proc) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:14:9 ------------------------------------- +14 | y = (q: Proc) // error | ^^^^^^^ | Found: () => Unit | Required: () ->{p} Unit @@ -25,18 +25,18 @@ | cannot be included in capture set {p} of variable y | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:14:8 ------------------------------------- -14 | y = q // error, was OK under unsealed +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:15:8 ------------------------------------- +15 | y = q // error, was OK under unsealed | ^ | Found: (q : () => Unit) | Required: () ->{p} Unit | | Note that reference (q : () => Unit), defined in method inner - | cannot be included in outer capture set {p} of variable y + | cannot be included in outer capture set {p} | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/outer-var.scala:16:57 --------------------------------------------------------- -16 | var finalizeActions = collection.mutable.ListBuffer[() => Unit]() // error, was OK under unsealed +-- Error: tests/neg-custom-args/captures/outer-var.scala:17:57 --------------------------------------------------------- +17 | var finalizeActions = collection.mutable.ListBuffer[() => Unit]() // error, was OK under unsealed | ^^^^^^^^^^ | Type variable A of object ListBuffer cannot be instantiated to box () => Unit since | that type captures the root capability `cap`. diff --git a/tests/neg-custom-args/captures/outer-var.scala b/tests/neg-custom-args/captures/outer-var.scala index f869bfbfc387..4ec19d8f8971 100644 --- a/tests/neg-custom-args/captures/outer-var.scala +++ b/tests/neg-custom-args/captures/outer-var.scala @@ -1,3 +1,4 @@ +import language.`3.7` // sepchecks on class CC type Cap = CC^ diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 7c00fa7299fe..ef755ebfcbd2 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -25,6 +25,20 @@ | ^^^^^^^^^^ | Type variable T of constructor Ref cannot be instantiated to List[box () => Unit] since | the part box () => Unit of that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:45:35 -------------------------------------- +45 | val next: () => Unit = cur.get.head // error + | ^^^^^^^^^^^^ + | Found: () => Unit + | Required: () ->{fresh} Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:47:20 -------------------------------------- +47 | cur.set(cur.get.tail: List[Proc]) // error + | ^^^^^^^^^^^^ + | Found: List[box () => Unit] + | Required: List[box () ->{fresh} Unit] + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/reaches.scala:53:51 ----------------------------------------------------------- 53 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error | ^ diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index a9773b76f445..34f05340a1e7 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -1,4 +1,4 @@ -import caps.use +import caps.use; import language.`3.7` // sepchecks on class File: def write(): Unit = ??? @@ -42,9 +42,9 @@ def runAll2(xs: List[Proc]): Unit = def runAll3(xs: List[Proc]): Unit = val cur = Ref[List[Proc]](xs) // error while cur.get.nonEmpty do - val next: () => Unit = cur.get.head + val next: () => Unit = cur.get.head // error next() - cur.set(cur.get.tail: List[Proc]) + cur.set(cur.get.tail: List[Proc]) // error class Id[-A, +B >: A](): def apply(a: A): B = a diff --git a/tests/neg-custom-args/captures/sep-compose.check b/tests/neg-custom-args/captures/sep-compose.check new file mode 100644 index 000000000000..7ecab087904e --- /dev/null +++ b/tests/neg-custom-args/captures/sep-compose.check @@ -0,0 +1,120 @@ +-- Error: tests/neg-custom-args/captures/sep-compose.scala:32:10 ------------------------------------------------------- +32 | seq3(f)(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq3: (x: () => Unit)(y: () ->{a, cap} Unit): Unit + | corresponds to capture-polymorphic formal parameter y of type () ->{a, cap} Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the first argument with type (f : () ->{a} Unit). + | + | Capture set of first argument : {f} + | Hidden set of current argument : {f} + | Footprint of first argument : {f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:33:10 ------------------------------------------------------- +33 | seq4(f)(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq4: (x: () ->{a, cap} Unit)(y: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter y of type () => Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the first argument with type (f : () ->{a} Unit). + | + | Capture set of first argument : {f} + | Hidden set of current argument : {f} + | Footprint of first argument : {f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:34:10 ------------------------------------------------------- +34 | seq5(f)(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq5: (x: () => Unit)(y: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter y of type () => Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the first argument with type (f : () ->{a} Unit). + | + | Capture set of first argument : {f} + | Hidden set of current argument : {f} + | Footprint of first argument : {f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:35:10 ------------------------------------------------------- +35 | seq6(f, f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq6: (x: () => Unit, y: () ->{a, cap} Unit): Unit + | corresponds to capture-polymorphic formal parameter y of type () ->{a, cap} Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the first argument with type (f : () ->{a} Unit). + | + | Capture set of first argument : {f} + | Hidden set of current argument : {f} + | Footprint of first argument : {f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:36:10 ------------------------------------------------------- +36 | seq7(f, f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq7: (x: () ->{a, cap} Unit, y: () => Unit): Unit + | corresponds to capture-polymorphic formal parameter y of type () => Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the first argument with type (f : () ->{a} Unit). + | + | Capture set of first argument : {f} + | Hidden set of current argument : {f} + | Footprint of first argument : {f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:37:7 -------------------------------------------------------- +37 | seq8(f)(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq8: (x: () => Unit)(y: () ->{a} Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () => Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the second argument with type (f : () ->{a} Unit). + | + | Capture set of second argument : {f} + | Hidden set of current argument : {f} + | Footprint of second argument : {f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:40:5 -------------------------------------------------------- +40 | p1(f) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method apply: (v1: T1): R + | corresponds to capture-polymorphic formal parameter x$0 of type () => Unit + | and captures {f, a, io}, but these capabilities are also passed separately + | in the function prefix with type (p1 : (x$0: () => Unit) ->{f} Unit). + | + | Capture set of function prefix : {p1} + | Hidden set of current argument : {f} + | Footprint of function prefix : {p1, f, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {f, a, io} +-- Error: tests/neg-custom-args/captures/sep-compose.scala:41:38 ------------------------------------------------------- +41 | val p8 = (x: () ->{a} Unit) => seq8(f)(x) // error + | ^ + | Separation failure: argument of type (f : () ->{a} Unit) + | to method seq8: (x: () => Unit)(y: () ->{a} Unit): Unit + | corresponds to capture-polymorphic formal parameter x of type () => Unit + | and captures {a, io}, but these capabilities are also passed separately + | in the second argument with type (x : () ->{a} Unit). + | + | Capture set of second argument : {x} + | Hidden set of current argument : {f} + | Footprint of second argument : {x, a, io} + | Hidden footprint of current argument : {f, a, io} + | Declared footprint of current argument: {} + | Undeclared overlap of footprints : {a, io} diff --git a/tests/neg-custom-args/captures/sep-compose.scala b/tests/neg-custom-args/captures/sep-compose.scala new file mode 100644 index 000000000000..268076cd40aa --- /dev/null +++ b/tests/neg-custom-args/captures/sep-compose.scala @@ -0,0 +1,45 @@ +import caps.cap +import language.future // sepchecks on +def seq1(x: () => Unit, y: () ->{x, cap} Unit): Unit = + x(); y() + +def seq2(x: () => Unit)(y: () ->{x, cap} Unit): Unit = + x(); y() + +def seq5(x: () ->{cap} Unit)(y: () => Unit): Unit = + x(); y() + +def test(io: Object^, a: Object^{io}): Unit = + + def seq3(x: () => Unit)(y: () ->{a, cap} Unit): Unit = + x(); y() + + def seq4(x: () ->{a, cap} Unit)(y: () => Unit): Unit = + x(); y() + + def seq6(x: () => Unit, y: () ->{a, cap} Unit): Unit = + x(); y() + + def seq7(x: () ->{a, cap} Unit, y: () => Unit): Unit = + x(); y() + + def seq8(x: () => Unit)(y: () ->{a} Unit): Unit = + x(); y() + + val f = () => println(a) + seq1(f, f) // ok + seq2(f)(f) // ok + seq3(f)(f) // error + seq4(f)(f) // error + seq5(f)(f) // error + seq6(f, f) // error + seq7(f, f) // error + seq8(f)(f) // error + + val p1 = (x: () => Unit) => seq1(f, x) + p1(f) // error + val p8 = (x: () ->{a} Unit) => seq8(f)(x) // error + p8(f) + + + diff --git a/tests/pos-custom-args/captures/readOnly.scala b/tests/neg-custom-args/captures/sepchecks.scala similarity index 66% rename from tests/pos-custom-args/captures/readOnly.scala rename to tests/neg-custom-args/captures/sepchecks.scala index a550010360a3..ceb6ce7b30bb 100644 --- a/tests/pos-custom-args/captures/readOnly.scala +++ b/tests/neg-custom-args/captures/sepchecks.scala @@ -1,5 +1,6 @@ import caps.Mutable import caps.cap +import language.future // sepchecks on trait Rdr[T]: def get: T @@ -9,7 +10,7 @@ class Ref[T](init: T) extends Rdr[T], Mutable: def get: T = current mut def put(x: T): Unit = current = x -def Test(c: Object^) = +def Test(c: Object^): Unit = val a: Ref[Int]^ = Ref(1) val b: Ref[Int]^ = Ref(2) def aa = a @@ -29,6 +30,8 @@ def Test(c: Object^) = setMax2(aa, aa, b) setMax2(a, aa, b) + setMax2(a, b, b) // error + setMax2(b, b, b) // error abstract class IMatrix: def apply(i: Int, j: Int): Double @@ -38,9 +41,22 @@ def Test(c: Object^) = def apply(i: Int, j: Int): Double = arr(i)(j) mut def update(i: Int, j: Int, x: Double): Unit = arr(i)(j) = x - def mul(x: IMatrix^{cap.rd}, y: IMatrix^{cap.rd}, z: Matrix^) = ??? + def mul(x: IMatrix^{cap.rd}, y: IMatrix^{cap.rd}, z: Matrix^): Matrix^ = ??? val m1 = Matrix(10, 10) val m2 = Matrix(10, 10) - mul(m1, m2, m2) // will fail separation checking + mul(m1, m2, m2) // error: will fail separation checking mul(m1, m1, m2) // ok + + def move(get: () => Int, set: Int => Unit) = + set(get()) + + val geta = () => a.get + + def get2(x: () => Int, y: () => Int): (Int, Int) = + (x(), y()) + + move(geta, b.put(_)) // ok + move(geta, a.put(_)) // error + get2(geta, geta) // ok + get2(geta, () => a.get) // ok diff --git a/tests/neg-custom-args/captures/unsound-reach-2.scala b/tests/neg-custom-args/captures/unsound-reach-2.scala index c7dfa117a2fe..90dd3824099f 100644 --- a/tests/neg-custom-args/captures/unsound-reach-2.scala +++ b/tests/neg-custom-args/captures/unsound-reach-2.scala @@ -1,4 +1,4 @@ -import language.experimental.captureChecking +import language.experimental.captureChecking; import language.`3.7` // sepchecks on trait Consumer[-T]: def apply(x: T): Unit @@ -13,7 +13,7 @@ class Bar extends Foo[File^]: // error def use(x: File^)(op: Consumer[File^]): Unit = op.apply(x) def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/unsound-reach-3.scala b/tests/neg-custom-args/captures/unsound-reach-3.scala index c5cdfca9d87a..0992dffb63ff 100644 --- a/tests/neg-custom-args/captures/unsound-reach-3.scala +++ b/tests/neg-custom-args/captures/unsound-reach-3.scala @@ -1,6 +1,6 @@ -import language.experimental.captureChecking +import language.experimental.captureChecking; import language.`3.7` // sepchecks on trait File: def close(): Unit @@ -12,7 +12,7 @@ class Bar extends Foo[File^]: // error def use(x: File^): File^ = x def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/unsound-reach-4.check b/tests/neg-custom-args/captures/unsound-reach-4.check index ca95bf42ba59..2d00eb0364e0 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.check +++ b/tests/neg-custom-args/captures/unsound-reach-4.check @@ -3,6 +3,13 @@ | ^^^^^^^^^^ | Type variable X of trait Foo cannot be instantiated to File^ since | that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach-4.scala:17:29 ------------------------------ +17 | val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). + | ^^^^^^^ + | Found: Bar^? + | Required: Foo[box File^] + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:22:22 --------------------------------------------------- 22 | escaped = boom.use(f) // error | ^^^^^^^^^^^ diff --git a/tests/neg-custom-args/captures/unsound-reach-4.scala b/tests/neg-custom-args/captures/unsound-reach-4.scala index 88fbc2f5c1de..bba09c0286e3 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.scala +++ b/tests/neg-custom-args/captures/unsound-reach-4.scala @@ -1,6 +1,6 @@ -import language.experimental.captureChecking +import language.experimental.captureChecking; import language.`3.7` // sepchecks on trait File: def close(): Unit @@ -14,7 +14,7 @@ class Bar extends Foo[File^]: // error def use(x: F): File^ = x def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/unsound-reach.check b/tests/neg-custom-args/captures/unsound-reach.check index 69794f569edb..17d4a4420833 100644 --- a/tests/neg-custom-args/captures/unsound-reach.check +++ b/tests/neg-custom-args/captures/unsound-reach.check @@ -8,6 +8,13 @@ | ^ | Type variable X of constructor Foo2 cannot be instantiated to box File^ since | that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach.scala:18:31 -------------------------------- +18 | val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). + | ^^^^^^^ + | Found: Bar^? + | Required: Foo[box File^] + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/unsound-reach.scala:23:21 ----------------------------------------------------- 23 | boom.use(f): (f1: File^{backdoor*}) => // error | ^ diff --git a/tests/neg-custom-args/captures/unsound-reach.scala b/tests/neg-custom-args/captures/unsound-reach.scala index 3fb666c7c1fc..fc8e2328ceb8 100644 --- a/tests/neg-custom-args/captures/unsound-reach.scala +++ b/tests/neg-custom-args/captures/unsound-reach.scala @@ -1,4 +1,4 @@ -import language.experimental.captureChecking +import language.experimental.captureChecking; import language.`3.7` // sepchecks on trait File: def close(): Unit @@ -15,7 +15,7 @@ class Bar2 extends Foo2[File^]: // error def use(x: File^)(op: File^ => Unit): Unit = op(x) // OK using sealed checking def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/update-call.scala b/tests/neg-custom-args/captures/update-call.scala new file mode 100644 index 000000000000..848e4d880223 --- /dev/null +++ b/tests/neg-custom-args/captures/update-call.scala @@ -0,0 +1,19 @@ +import caps.Mutable + +trait IterableOnce[T] extends Mutable: + def iterator: Iterator[T]^{this} + mut def foreach(op: T => Unit): Unit + +trait Iterator[T] extends IterableOnce[T]: + def iterator = this + def hasNext: Boolean + mut def next(): T + mut def foreach(op: T => Unit): Unit = ??? + override mut def toString = ??? // error + +trait Iterable[T] extends IterableOnce[T]: + def iterator: Iterator[T] = ??? + def foreach(op: T => Unit) = iterator.foreach(op) + +trait BadIterator[T] extends Iterator[T]: + override mut def hasNext: Boolean // error diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index db5c8083e3b7..4fe4163aa433 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -1,10 +1,11 @@ -- Error: tests/neg-custom-args/captures/vars.scala:24:14 -------------------------------------------------------------- 24 | a = x => g(x) // error | ^^^^ - | reference (cap3 : CC^) is not included in the allowed capture set {cap1} of variable a + | reference (cap3 : CC^) is not included in the allowed capture set {cap1} + | of an enclosing function literal with expected type (x$0: String) ->{cap1} String | | Note that reference (cap3 : CC^), defined in method scope - | cannot be included in outer capture set {cap1} of variable a + | cannot be included in outer capture set {cap1} -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:25:8 ------------------------------------------ 25 | a = g // error | ^ @@ -12,7 +13,7 @@ | Required: (x$0: String) ->{cap1} String | | Note that reference (cap3 : CC^), defined in method scope - | cannot be included in outer capture set {cap1} of variable a + | cannot be included in outer capture set {cap1} | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:27:12 ----------------------------------------- diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala index eb9719cd2adf..fc0de7354dd3 100644 --- a/tests/neg-custom-args/captures/vars.scala +++ b/tests/neg-custom-args/captures/vars.scala @@ -1,4 +1,4 @@ - +import language.`3.7` // sepchecks on class CC type Cap = CC^ diff --git a/tests/pos-custom-args/captures/boxmap-paper.scala b/tests/pos-custom-args/captures/boxmap-paper.scala index 20282d5813f9..436132280d40 100644 --- a/tests/pos-custom-args/captures/boxmap-paper.scala +++ b/tests/pos-custom-args/captures/boxmap-paper.scala @@ -1,3 +1,4 @@ +import caps.cap type Cell_orig[+T] = [K] -> (T => K) -> K @@ -18,13 +19,13 @@ def map[A, B](c: Cell[A])(f: A => B): Cell[B] def pureMap[A, B](c: Cell[A])(f: A -> B): Cell[B] = c[Cell[B]]((x: A) => cell(f(x))) -def lazyMap[A, B](c: Cell[A])(f: A => B): () ->{f} Cell[B] +def lazyMap[A, B](c: Cell[A])(f: A ->{cap.rd} B): () ->{f} Cell[B] = () => c[Cell[B]]((x: A) => cell(f(x))) trait IO: def print(s: String): Unit -def test(io: IO^) = +def test(io: IO^{cap.rd}) = val loggedOne: () ->{io} Int = () => { io.print("1"); 1 } diff --git a/tests/pos-custom-args/captures/cc-dep-param.scala b/tests/pos-custom-args/captures/cc-dep-param.scala index 1440cd4d7d40..5fd634de9040 100644 --- a/tests/pos-custom-args/captures/cc-dep-param.scala +++ b/tests/pos-custom-args/captures/cc-dep-param.scala @@ -1,8 +1,9 @@ import language.experimental.captureChecking +import caps.cap trait Foo[T] def test(): Unit = - val a: Foo[Int]^ = ??? + val a: Foo[Int]^{cap.rd} = ??? val useA: () ->{a} Unit = ??? def foo[X](x: Foo[X]^, op: () ->{x} Unit): Unit = ??? foo(a, useA) diff --git a/tests/pos-custom-args/captures/foreach2.scala b/tests/pos-custom-args/captures/foreach2.scala new file mode 100644 index 000000000000..318bcb9cddfc --- /dev/null +++ b/tests/pos-custom-args/captures/foreach2.scala @@ -0,0 +1,7 @@ +import annotation.unchecked.uncheckedCaptures + +class ArrayBuffer[T]: + def foreach(op: T => Unit): Unit = ??? +def test = + val tasks = new ArrayBuffer[(() => Unit) @uncheckedCaptures] + val _: Unit = tasks.foreach(((task: () => Unit) => task())) diff --git a/tests/pos-custom-args/captures/nested-classes-2.scala b/tests/pos-custom-args/captures/nested-classes-2.scala index 744635ee949b..7290ed4a12ea 100644 --- a/tests/pos-custom-args/captures/nested-classes-2.scala +++ b/tests/pos-custom-args/captures/nested-classes-2.scala @@ -1,21 +1,7 @@ - -def f(x: (() => Unit)): (() => Unit) => (() => Unit) = - def g(y: (() => Unit)): (() => Unit) = x - g - -def test1(x: (() => Unit)): Unit = - def test2(y: (() => Unit)) = - val a: (() => Unit) => (() => Unit) = f(y) - a(x) // OK, but should be error - test2(() => ()) - def test2(x1: (() => Unit), x2: (() => Unit) => Unit) = class C1(x1: (() => Unit), xx2: (() => Unit) => Unit): - def c2(y1: (() => Unit), y2: (() => Unit) => Unit): C2^ = C2(y1, y2) - class C2(y1: (() => Unit), y2: (() => Unit) => Unit): - val a: (() => Unit) => (() => Unit) = f(y1) - a(x1) //OK, but should be error - C2(() => (), x => ()) + def c2(y1: (() => Unit), y2: (() => Unit) => Unit): C2^ = ??? + class C2(y1: (() => Unit), y2: (() => Unit) => Unit) def test3(y1: (() => Unit), y2: (() => Unit) => Unit) = val cc1: C1^{y1, y2} = C1(y1, y2) diff --git a/tests/pos-custom-args/captures/sep-compose.scala b/tests/pos-custom-args/captures/sep-compose.scala new file mode 100644 index 000000000000..3f6ef2968a6e --- /dev/null +++ b/tests/pos-custom-args/captures/sep-compose.scala @@ -0,0 +1,21 @@ +import caps.cap +import language.future // sepchecks on + +def seq1(x: () => Unit, y: () ->{x, cap} Unit): Unit = + x(); y() + +def seq2(x: () => Unit)(y: () ->{x, cap} Unit): Unit = + x(); y() + +def test(io: Object^, a: Object^{io}): Unit = + val f = () => println(a) + val g = () => println(a) + seq1(f, f) + seq2(f)(f) + seq1(g, g) + seq2(g)(g) + + seq1(f, g) + seq2(f)(g) + seq1(g, f) + seq2(g)(f) \ No newline at end of file diff --git a/tests/pos-custom-args/captures/sep-eq.scala b/tests/pos-custom-args/captures/sep-eq.scala new file mode 100644 index 000000000000..836633feee9e --- /dev/null +++ b/tests/pos-custom-args/captures/sep-eq.scala @@ -0,0 +1,20 @@ +import caps.Mutable +import caps.cap +import language.future // sepchecks on + +extension (x: Object^) + infix def eql (y: Object^{x, cap}): Boolean = x eq y + +def eql1(x: Object^, y: Object^{x, cap}): Boolean = x eql y +def eql2(x: Object^)(y: Object^{x, cap}): Boolean = x eql y + +class LLI extends Object: + this: LLI^ => + + val f: Object^ = ??? + + def foo = + def these = f + val eq0 = these eql these + val eq1 = eql2(f)(f) + val eq2 = eql2(these)(these) diff --git a/tests/pos-custom-args/captures/simple-apply.scala b/tests/pos-custom-args/captures/simple-apply.scala new file mode 100644 index 000000000000..1e2a6715dd79 --- /dev/null +++ b/tests/pos-custom-args/captures/simple-apply.scala @@ -0,0 +1,6 @@ +object Test: + + def foo(x: Object^, ys: List[Object^]) = ??? + def test(io: Object^, async: Object^): Unit = + val v: Object^{io} = ??? + foo(v, List(async)) diff --git a/tests/pos-custom-args/captures/skolems2.scala b/tests/pos-custom-args/captures/skolems2.scala new file mode 100644 index 000000000000..dd6417042339 --- /dev/null +++ b/tests/pos-custom-args/captures/skolems2.scala @@ -0,0 +1,15 @@ +def Test(c: Object^, f: Object^ => Object^) = + def cc: Object^ = c + val x1 = + { f(cc) } + val x2 = + f(cc) + val x3: Object^ = + f(cc) + val x4: Object^ = + { f(cc) } + + + + + diff --git a/tests/pos-special/stdlib/Test2.scala b/tests/pos-special/stdlib/Test2.scala index cab9440c17db..e0d9a1491516 100644 --- a/tests/pos-special/stdlib/Test2.scala +++ b/tests/pos-special/stdlib/Test2.scala @@ -2,6 +2,7 @@ import scala.reflect.ClassTag import language.experimental.captureChecking import collection.{View, Seq} import collection.mutable.{ArrayBuffer, ListBuffer} +import caps.unsafe.unsafeAssumeSeparate object Test { @@ -87,7 +88,7 @@ object Test { val ys9: Iterator[Boolean]^{xs9} = xs9 val xs10 = xs.flatMap(flips) val ys10: Iterator[Int]^{xs10} = xs10 - val xs11 = xs ++ xs + val xs11 = unsafeAssumeSeparate(xs ++ xs) val ys11: Iterator[Int]^{xs11} = xs11 val xs12 = xs ++ Nil val ys12: Iterator[Int]^{xs12} = xs12 @@ -95,7 +96,7 @@ object Test { val ys13: List[Int] = xs13 val xs14 = xs ++ ("a" :: Nil) val ys14: Iterator[Any]^{xs14} = xs14 - val xs15 = xs.zip(xs9) + val xs15 = unsafeAssumeSeparate(xs.zip(xs9)) val ys15: Iterator[(Int, Boolean)]^{xs15} = xs15 println("-------") println(x1) @@ -141,7 +142,7 @@ object Test { val ys9: View[Boolean]^{xs9} = xs9 val xs10 = xs.flatMap(flips) val ys10: View[Int]^{xs10} = xs10 - val xs11 = xs ++ xs + val xs11 = unsafeAssumeSeparate(xs ++ xs) val ys11: View[Int]^{xs11} = xs11 val xs12 = xs ++ Nil val ys12: View[Int]^{xs12} = xs12 @@ -149,7 +150,7 @@ object Test { val ys13: List[Int] = xs13 val xs14 = xs ++ ("a" :: Nil) val ys14: View[Any]^{xs14} = xs14 - val xs15 = xs.zip(xs9) + val xs15 = unsafeAssumeSeparate(xs.zip(xs9)) val ys15: View[(Int, Boolean)]^{xs15} = xs15 println("-------") println(x1) diff --git a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala index 5443758afa72..c22e1308db6d 100644 --- a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -5,6 +5,8 @@ import Predef.{augmentString as _, wrapString as _, *} import scala.reflect.ClassTag import annotation.unchecked.{uncheckedVariance, uncheckedCaptures} import annotation.tailrec +import caps.cap +import language.`3.7` // sepchecks on /** A strawman architecture for new collections. It contains some * example collection classes and methods with the intent to expose @@ -29,7 +31,7 @@ object CollectionStrawMan5 { /** Base trait for instances that can construct a collection from an iterable */ trait FromIterable { type C[X] <: Iterable[X]^ - def fromIterable[B](it: Iterable[B]^): C[B]^{it} + def fromIterable[B](it: Iterable[B]^{this, cap}): C[B]^{it} } type FromIterableOf[+CC[X] <: Iterable[X]^] = FromIterable { @@ -60,12 +62,12 @@ object CollectionStrawMan5 { trait SeqFactory extends IterableFactory { type C[X] <: Seq[X] - def fromIterable[B](it: Iterable[B]^): C[B] + def fromIterable[B](it: Iterable[B]^{this, cap}): C[B] } /** Base trait for strict collections */ trait Buildable[+A] extends Iterable[A] { - protected[this] def newBuilder: Builder[A, Repr] @uncheckedVariance + protected def newBuilder: Builder[A, Repr] @uncheckedVariance override def partition(p: A => Boolean): (Repr, Repr) = { val l, r = newBuilder iterator.foreach(x => (if (p(x)) l else r) += x) @@ -105,7 +107,7 @@ object CollectionStrawMan5 { with IterablePolyTransforms[A] with IterableMonoTransforms[A] { // sound bcs of VarianceNote type Repr = C[A] @uncheckedVariance - protected[this] def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr @uncheckedVariance ^{coll} = + protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^ {this, cap}): Repr @uncheckedVariance ^{coll} = fromIterable(coll) } @@ -115,7 +117,7 @@ object CollectionStrawMan5 { this: SeqLike[A] => type C[X] <: Seq[X] def fromIterable[B](coll: Iterable[B]^): C[B] - override protected[this] def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr = + override protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^ ): Repr = fromIterable(coll) trait IterableOps[+A] extends Any { @@ -134,7 +136,7 @@ object CollectionStrawMan5 { this: IterableMonoTransforms[A]^ => type Repr protected def coll: Iterable[A]^{this} - protected[this] def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr^{coll} + protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^ {this, cap}): Repr^{coll} def filter(p: A => Boolean): Repr^{this, p} = fromLikeIterable(View.Filter(coll, p)) def partition(p: A => Boolean): (Repr^{this, p}, Repr^{this, p}) = { @@ -153,7 +155,7 @@ object CollectionStrawMan5 { this: IterablePolyTransforms[A]^ => type C[A] protected def coll: Iterable[A]^{this} - def fromIterable[B](coll: Iterable[B]^): C[B]^{coll} + def fromIterable[B](coll: Iterable[B]^{this, cap}): C[B]^{coll} def map[B](f: A => B): C[B]^{this, f} = fromIterable(View.Map(coll, f)) def flatMap[B](f: A => IterableOnce[B]^): C[B]^{this, f} = fromIterable(View.FlatMap(coll, f)) def ++[B >: A](xs: IterableOnce[B]^): C[B]^{this, xs} = fromIterable(View.Concat(coll, xs)) @@ -169,7 +171,7 @@ object CollectionStrawMan5 { while (it.hasNext) xs = new Cons(it.next(), xs) fromLikeIterable(xs) - override protected[this] def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr + override protected def fromLikeIterable(coll: Iterable[A] @uncheckedVariance ^): Repr override def filter(p: A => Boolean): Repr = fromLikeIterable(View.Filter(coll, p)) @@ -204,7 +206,7 @@ object CollectionStrawMan5 { def head: A def tail: List[A] def iterator = new Iterator[A] { - private[this] var current = self + private var current = self def hasNext = !current.isEmpty def next() = { val r = current.head; current = current.tail; r } } @@ -215,7 +217,7 @@ object CollectionStrawMan5 { } def length: Int = if (isEmpty) 0 else 1 + tail.length - protected[this] def newBuilder = new ListBuffer[A @uncheckedVariance @uncheckedCaptures] + protected def newBuilder = new ListBuffer[A @uncheckedVariance @uncheckedCaptures] def ++:[B >: A](prefix: List[B]): List[B] = if (prefix.isEmpty) this else Cons(prefix.head, prefix.tail ++: this) @@ -407,7 +409,7 @@ object CollectionStrawMan5 { this: View[A]^ => type C[X] = View[X]^{this} override def view: this.type = this - override def fromIterable[B](c: Iterable[B]^): View[B]^{this, c} = { + override def fromIterable[B](c: Iterable[B]^{this, cap}): View[B]^{this, c} = { c match { case c: View[B] => c case _ => View.fromIterator(c.iterator) diff --git a/tests/run-custom-args/captures/colltest5/Test_2.scala b/tests/run-custom-args/captures/colltest5/Test_2.scala index f6f47b536541..2b3b27c94243 100644 --- a/tests/run-custom-args/captures/colltest5/Test_2.scala +++ b/tests/run-custom-args/captures/colltest5/Test_2.scala @@ -1,5 +1,7 @@ import Predef.{augmentString as _, wrapString as _, *} import scala.reflect.ClassTag +import caps.unsafe.unsafeAssumeSeparate +import language.`3.7` // sepchecks on object Test { import colltest5.strawman.collections.* @@ -89,7 +91,7 @@ object Test { val ys9: View[Boolean]^{xs9} = xs9 val xs10 = xs.flatMap(flips) val ys10: View[Int]^{xs10} = xs10 - val xs11 = xs ++ xs + val xs11 = unsafeAssumeSeparate(xs ++ xs) val ys11: View[Int]^{xs11} = xs11 val xs12 = xs ++ Nil val ys12: View[Int]^{xs12} = xs12 @@ -97,7 +99,7 @@ object Test { val ys13: List[Int] = xs13 val xs14 = xs ++ Cons("a", Nil) val ys14: View[Any]^{xs14} = xs14 - val xs15 = xs.zip(xs9) + val xs15 = unsafeAssumeSeparate(xs.zip(xs9)) val ys15: View[(Int, Boolean)]^{xs15} = xs15 println("-------") println(x1) From af23b3f2262c095547da6902618f736f32f1f1ba Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 12 Jan 2025 19:21:15 +0100 Subject: [PATCH 8/8] Separation checking for blocks Check that a capability that gets hidden in the (result-)type of some definition is not used afterwards in the same or a nested scope. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 78 +++++---- .../src/dotty/tools/dotc/cc/SepCheck.scala | 159 +++++++++++++----- project/Build.scala | 2 +- .../immutable/LazyListIterable.scala | 3 +- tests/neg-custom-args/captures/capt1.check | 68 +++++--- tests/neg-custom-args/captures/capt1.scala | 14 +- .../captures/cc-ex-conformance.scala | 3 +- tests/neg-custom-args/captures/i15772.check | 24 +-- tests/neg-custom-args/captures/i15772.scala | 2 + .../captures/sep-compose.check | 4 +- tests/neg-custom-args/captures/sep-use.check | 24 +++ tests/neg-custom-args/captures/sep-use.scala | 27 +++ tests/neg-custom-args/captures/sep-use2.scala | 28 +++ tests/pos-custom-args/captures/capt1.scala | 12 +- tests/pos-custom-args/captures/skolems2.scala | 2 + 15 files changed, 328 insertions(+), 122 deletions(-) create mode 100644 tests/neg-custom-args/captures/sep-use.check create mode 100644 tests/neg-custom-args/captures/sep-use.scala create mode 100644 tests/neg-custom-args/captures/sep-use2.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index d494bc8d9e22..c5de4e97807e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -18,7 +18,7 @@ import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property} import transform.{Recheck, PreRecheck, CapturedVars} import Recheck.* import scala.collection.mutable -import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult, VarState} +import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult} import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} @@ -253,6 +253,10 @@ object CheckCaptures: * the type of the formal paremeter corresponding to the argument. */ def formalType: Type + + /** The "use set", i.e. the capture set marked as free at this node. */ + def markedFree: CaptureSet + end CheckerAPI class CheckCaptures extends Recheck, SymTransformer: @@ -298,9 +302,12 @@ class CheckCaptures extends Recheck, SymTransformer: */ private val sepCheckFormals = util.EqHashMap[Tree, Type]() + private val usedSet = util.EqHashMap[Tree, CaptureSet]() + extension [T <: Tree](tree: T) def needsSepCheck: Boolean = sepCheckFormals.contains(tree) def formalType: Type = sepCheckFormals.getOrElse(tree, NoType) + def markedFree = usedSet.getOrElse(tree, CaptureSet.empty) /** Instantiate capture set variables appearing contra-variantly to their * upper approximation. @@ -404,17 +411,17 @@ class CheckCaptures extends Recheck, SymTransformer: /** Include `sym` in the capture sets of all enclosing environments nested in the * the environment in which `sym` is defined. */ - def markFree(sym: Symbol, pos: SrcPos)(using Context): Unit = - markFree(sym, sym.termRef, pos) + def markFree(sym: Symbol, tree: Tree)(using Context): Unit = + markFree(sym, sym.termRef, tree) - def markFree(sym: Symbol, ref: CaptureRef, pos: SrcPos)(using Context): Unit = - if sym.exists && ref.isTracked then markFree(ref.captureSet, pos) + def markFree(sym: Symbol, ref: CaptureRef, tree: Tree)(using Context): Unit = + if sym.exists && ref.isTracked then markFree(ref.captureSet, tree) /** Make sure the (projected) `cs` is a subset of the capture sets of all enclosing * environments. At each stage, only include references from `cs` that are outside * the environment's owner */ - def markFree(cs: CaptureSet, pos: SrcPos)(using Context): Unit = + def markFree(cs: CaptureSet, tree: Tree)(using Context): Unit = // A captured reference with the symbol `sym` is visible from the environment // if `sym` is not defined inside the owner of the environment. inline def isVisibleFromEnv(sym: Symbol, env: Env) = @@ -436,7 +443,7 @@ class CheckCaptures extends Recheck, SymTransformer: val what = if ref.isType then "Capture set parameter" else "Local reach capability" report.error( em"""$what $c leaks into capture scope of ${env.ownerString}. - |To allow this, the ${ref.symbol} should be declared with a @use annotation""", pos) + |To allow this, the ${ref.symbol} should be declared with a @use annotation""", tree.srcPos) case _ => /** Avoid locally defined capability by charging the underlying type @@ -456,7 +463,7 @@ class CheckCaptures extends Recheck, SymTransformer: CaptureSet.ofType(c.widen, followResult = false) capt.println(i"Widen reach $c to $underlying in ${env.owner}") underlying.disallowRootCapability: () => - report.error(em"Local capability $c in ${env.ownerString} cannot have `cap` as underlying capture set", pos) + report.error(em"Local capability $c in ${env.ownerString} cannot have `cap` as underlying capture set", tree.srcPos) recur(underlying, env, lastEnv) /** Avoid locally defined capability if it is a reach capability or capture set @@ -479,7 +486,7 @@ class CheckCaptures extends Recheck, SymTransformer: val underlying = CaptureSet.ofTypeDeeply(c1.widen) capt.println(i"Widen reach $c to $underlying in ${env.owner}") underlying.disallowRootCapability: () => - report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", pos) + report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", tree.srcPos) recur(underlying, env, null) case c: TypeRef if c.isParamPath => checkUseDeclared(c, env, null) @@ -496,7 +503,7 @@ class CheckCaptures extends Recheck, SymTransformer: then avoidLocalCapability(c, env, lastEnv) else avoidLocalReachCapability(c, env) isVisible - checkSubset(included, env.captured, pos, provenance(env)) + checkSubset(included, env.captured, tree.srcPos, provenance(env)) capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}") if !isOfNestedMethod(env) then recur(included, nextEnvToCharge(env, !_.owner.isStaticOwner), env) @@ -504,14 +511,15 @@ class CheckCaptures extends Recheck, SymTransformer: // will be charged when that method is called. recur(cs, curEnv, null) + usedSet(tree) = tree.markedFree ++ cs end markFree /** Include references captured by the called method in the current environment stack */ - def includeCallCaptures(sym: Symbol, resType: Type, pos: SrcPos)(using Context): Unit = resType match + def includeCallCaptures(sym: Symbol, resType: Type, tree: Tree)(using Context): Unit = resType match case _: MethodOrPoly => // wait until method is fully applied case _ => if sym.exists then - if curEnv.isOpen then markFree(capturedVars(sym), pos) + if curEnv.isOpen then markFree(capturedVars(sym), tree) /** Under the sealed policy, disallow the root capability in type arguments. * Type arguments come either from a TypeApply node or from an AppliedType @@ -535,23 +543,23 @@ class CheckCaptures extends Recheck, SymTransformer: for case (arg: TypeTree, pname) <- args.lazyZip(paramNames) do def where = if sym.exists then i" in an argument of $sym" else "" - val (addendum, pos) = + val (addendum, errTree) = if arg.isInferred - then ("\nThis is often caused by a local capability$where\nleaking as part of its result.", fn.srcPos) - else if arg.span.exists then ("", arg.srcPos) - else ("", fn.srcPos) + then ("\nThis is often caused by a local capability$where\nleaking as part of its result.", fn) + else if arg.span.exists then ("", arg) + else ("", fn) disallowRootCapabilitiesIn(arg.nuType, NoSymbol, - i"Type variable $pname of $sym", "be instantiated to", addendum, pos) + i"Type variable $pname of $sym", "be instantiated to", addendum, errTree.srcPos) val param = fn.symbol.paramNamed(pname) - if param.isUseParam then markFree(arg.nuType.deepCaptureSet, pos) + if param.isUseParam then markFree(arg.nuType.deepCaptureSet, errTree) end disallowCapInTypeArgs override def recheckIdent(tree: Ident, pt: Type)(using Context): Type = val sym = tree.symbol if sym.is(Method) then // If ident refers to a parameterless method, charge its cv to the environment - includeCallCaptures(sym, sym.info, tree.srcPos) + includeCallCaptures(sym, sym.info, tree) else if !sym.isStatic then // Otherwise charge its symbol, but add all selections implied by the e // expected type `pt`. @@ -569,7 +577,7 @@ class CheckCaptures extends Recheck, SymTransformer: var pathRef: CaptureRef = addSelects(sym.termRef, pt) if pathRef.derivesFrom(defn.Caps_Mutable) && pt.isValueType && !pt.isMutableType then pathRef = pathRef.readOnly - markFree(sym, pathRef, tree.srcPos) + markFree(sym, pathRef, tree) super.recheckIdent(tree, pt) /** The expected type for the qualifier of a selection. If the selection @@ -668,7 +676,7 @@ class CheckCaptures extends Recheck, SymTransformer: super.recheckFinish(argType, tree, pt) else val res = super.recheckApply(tree, pt) - includeCallCaptures(meth, res, tree.srcPos) + includeCallCaptures(meth, res, tree) res /** Recheck argument, and, if formal parameter carries a `@use`, @@ -681,7 +689,7 @@ class CheckCaptures extends Recheck, SymTransformer: if formal.hasUseAnnot then // The @use annotation is added to `formal` by `prepareFunction` capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") - markFree(argType.deepCaptureSet, arg.srcPos) + markFree(argType.deepCaptureSet, arg) if formal.containsCap then sepCheckFormals(arg) = freshenedFormal argType @@ -815,7 +823,7 @@ class CheckCaptures extends Recheck, SymTransformer: case fun => fun.symbol disallowCapInTypeArgs(tree.fun, meth, tree.args) val res = Existential.toCap(super.recheckTypeApply(tree, pt)) - includeCallCaptures(tree.symbol, res, tree.srcPos) + includeCallCaptures(tree.symbol, res, tree) checkContains(tree) res end recheckTypeApply @@ -1092,7 +1100,7 @@ class CheckCaptures extends Recheck, SymTransformer: case AnnotatedType(_, annot) if annot.symbol == defn.RequiresCapabilityAnnot => annot.tree match case Apply(_, cap :: Nil) => - markFree(cap.symbol, tree.srcPos) + markFree(cap.symbol, tree) case _ => case _ => super.recheckTyped(tree) @@ -1147,7 +1155,7 @@ class CheckCaptures extends Recheck, SymTransformer: super.recheck(tree, pt) finally curEnv = saved if tree.isTerm && !pt.isBoxedCapturing && pt != LhsProto then - markFree(res.boxedCaptureSet, tree.srcPos) + markFree(res.boxedCaptureSet, tree) res end recheck @@ -1214,7 +1222,7 @@ class CheckCaptures extends Recheck, SymTransformer: override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type = var expected1 = alignDependentFunction(expected, actual.stripCapturing) val boxErrors = new mutable.ListBuffer[Message] - val actualBoxed = adapt(actual, expected1, tree.srcPos, boxErrors) + val actualBoxed = adapt(actual, expected1, tree, boxErrors) //println(i"check conforms $actualBoxed <<< $expected1") if actualBoxed eq actual then @@ -1334,7 +1342,7 @@ class CheckCaptures extends Recheck, SymTransformer: * * @param alwaysConst always make capture set variables constant after adaptation */ - def adaptBoxed(actual: Type, expected: Type, pos: SrcPos, covariant: Boolean, alwaysConst: Boolean, boxErrors: BoxErrors)(using Context): Type = + def adaptBoxed(actual: Type, expected: Type, tree: Tree, covariant: Boolean, alwaysConst: Boolean, boxErrors: BoxErrors)(using Context): Type = def recur(actual: Type, expected: Type, covariant: Boolean): Type = @@ -1401,7 +1409,7 @@ class CheckCaptures extends Recheck, SymTransformer: if !leaked.subCaptures(cs).isOK then report.error( em"""$expected cannot be box-converted to ${actual.capturing(leaked)} - |since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", pos) + |since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", tree.srcPos) cs def adaptedType(resultBoxed: Boolean) = @@ -1433,11 +1441,11 @@ class CheckCaptures extends Recheck, SymTransformer: return actual // Disallow future addition of `cap` to `criticalSet`. criticalSet.disallowRootCapability: () => - report.error(msg, pos) + report.error(msg, tree.srcPos) if !insertBox then // we are unboxing //debugShowEnvs() - markFree(criticalSet, pos) + markFree(criticalSet, tree) end if // Compute the adapted type. @@ -1497,14 +1505,14 @@ class CheckCaptures extends Recheck, SymTransformer: * - narrow nested captures of `x`'s underlying type to `{x*}` * - do box adaptation */ - def adapt(actual: Type, expected: Type, pos: SrcPos, boxErrors: BoxErrors)(using Context): Type = + def adapt(actual: Type, expected: Type, tree: Tree, boxErrors: BoxErrors)(using Context): Type = if expected == LhsProto || expected.isSingleton && actual.isSingleton then actual else val improvedVAR = improveCaptures(actual.widen.dealiasKeepAnnots, actual) val improvedRO = improveReadOnly(improvedVAR, expected) val adapted = adaptBoxed( - improvedRO.withReachCaptures(actual), expected, pos, + improvedRO.withReachCaptures(actual), expected, tree, covariant = true, alwaysConst = false, boxErrors) if adapted eq improvedVAR // no .rd improvement, no box-adaptation then actual // might as well use actual instead of improved widened @@ -1519,19 +1527,19 @@ class CheckCaptures extends Recheck, SymTransformer: * But maybe we can then elide the check during the RefChecks phase under captureChecking? */ def checkOverrides = new TreeTraverser: - class OverridingPairsCheckerCC(clazz: ClassSymbol, self: Type, srcPos: SrcPos)(using Context) extends OverridingPairsChecker(clazz, self): + class OverridingPairsCheckerCC(clazz: ClassSymbol, self: Type, tree: Tree)(using Context) extends OverridingPairsChecker(clazz, self): /** Check subtype with box adaptation. * This function is passed to RefChecks to check the compatibility of overriding pairs. * @param sym symbol of the field definition that is being checked */ override def checkSubType(actual: Type, expected: Type)(using Context): Boolean = - val expected1 = alignDependentFunction(addOuterRefs(expected, actual, srcPos), actual.stripCapturing) + val expected1 = alignDependentFunction(addOuterRefs(expected, actual, tree.srcPos), actual.stripCapturing) val actual1 = val saved = curEnv try curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv) val adapted = - adaptBoxed(actual, expected1, srcPos, covariant = true, alwaysConst = true, null) + adaptBoxed(actual, expected1, tree, covariant = true, alwaysConst = true, null) actual match case _: MethodType => // We remove the capture set resulted from box adaptation for method types, diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 9f5e8187d1d0..ecdb2cc93a82 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -5,16 +5,32 @@ import ast.tpd import collection.mutable import core.* -import Symbols.*, Types.* +import Symbols.*, Types.*, Flags.* import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* -import CaptureSet.{Refs, emptySet} +import CaptureSet.{Refs, emptySet, HiddenSet} import config.Printers.capt import StdNames.nme +import util.{SimpleIdentitySet, EqHashMap} class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: import tpd.* import checker.* + /** The set of capabilities that are hidden by a polymorphic result type + * of some previous definition. + */ + private var defsShadow: Refs = SimpleIdentitySet.empty + + /** A map from definitions to their internal result types. + * Populated during separation checking traversal. + */ + private val resultType = EqHashMap[Symbol, Type]() + + /** The previous val or def definitions encountered during separation checking. + * These all enclose and precede the current traversal node. + */ + private var previousDefs: List[mutable.ListBuffer[ValOrDefDef]] = Nil + extension (refs: Refs) private def footprint(using Context): Refs = def recur(elems: Refs, newElems: List[CaptureRef]): Refs = newElems match @@ -34,38 +50,39 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: ref.isExclusive && refs2.exists(_.stripReadOnly eq ref) common(refs, other) ++ common(other, refs) - private def hidden(refs: Refs)(using Context): Refs = - val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet + private def hidden(using Context): Refs = + val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet - def hiddenByElem(elem: CaptureRef): Refs = - if seen.add(elem) then elem match - case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs.elems) - case ReadOnlyCapability(ref) => hiddenByElem(ref).map(_.readOnly) - case _ => emptySet - else emptySet + def hiddenByElem(elem: CaptureRef): Refs = + if seen.add(elem) then elem match + case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs.elems) + case ReadOnlyCapability(ref) => hiddenByElem(ref).map(_.readOnly) + case _ => emptySet + else emptySet - def recur(cs: Refs): Refs = - (emptySet /: cs): (elems, elem) => - elems ++ hiddenByElem(elem) + def recur(cs: Refs): Refs = + (emptySet /: cs): (elems, elem) => + elems ++ hiddenByElem(elem) - recur(refs) - end hidden + recur(refs) + end hidden + end extension /** The captures of an argument or prefix widened to the formal parameter, if * the latter contains a cap. */ private def formalCaptures(arg: Tree)(using Context): Refs = val argType = arg.formalType.orElse(arg.nuType) - (if arg.nuType.hasUseAnnot then argType.deepCaptureSet else argType.captureSet) + (if argType.hasUseAnnot then argType.deepCaptureSet else argType.captureSet) .elems - /** The captures of an argument of prefix. No widening takes place */ - private def actualCaptures(arg: Tree)(using Context): Refs = - val argType = arg.nuType - (if argType.hasUseAnnot then argType.deepCaptureSet else argType.captureSet) + /** The captures of a node */ + private def captures(tree: Tree)(using Context): Refs = + val tpe = tree.nuType + (if tree.formalType.hasUseAnnot then tpe.deepCaptureSet else tpe.captureSet) .elems - private def sepError(fn: Tree, args: List[Tree], argIdx: Int, + private def sepApplyError(fn: Tree, args: List[Tree], argIdx: Int, overlap: Refs, hiddenInArg: Refs, footprints: List[(Refs, Int)], deps: collection.Map[Tree, List[Tree]])(using Context): Unit = val arg = args(argIdx) @@ -78,9 +95,15 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case Some(pname) => i"$pname " case _ => "" def whatStr = if overlap.size == 1 then "this capability is" else "these capabilities are" + def qualifier = methPart(fn) match + case Select(qual, _) => qual + case _ => EmptyTree + def isShowableMethod = fn.symbol.exists && !defn.isFunctionSymbol(fn.symbol.maybeOwner) + def funType = + if fn.symbol.exists && !qualifier.isEmpty then qualifier.nuType else fn.nuType def funStr = - if fn.symbol.exists then i"${fn.symbol}: ${fn.symbol.info}" - else i"a function of type ${fn.nuType.widen}" + if isShowableMethod then i"${fn.symbol}: ${fn.symbol.info}" + else i"a function of type ${funType.widen}" val clashIdx = footprints .collect: case (fp, idx) if !hiddenInArg.overlapWith(fp).isEmpty => idx @@ -92,21 +115,23 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: case 3 => "third argument " case n => s"${n}th argument " def clashTree = - if clashIdx == 0 then methPart(fn).asInstanceOf[Select].qualifier + if clashIdx == 0 then qualifier else args(clashIdx - 1) - def clashType = clashTree.nuType - def clashCaptures = actualCaptures(clashTree) - def hiddenCaptures = hidden(formalCaptures(arg)) + def clashTypeStr = + if clashIdx == 0 && !isShowableMethod then "" // we already mentioned the type in `funStr` + else i" with type ${clashTree.nuType}" + def clashCaptures = captures(clashTree) + def hiddenCaptures = formalCaptures(arg).hidden def clashFootprint = clashCaptures.footprint def hiddenFootprint = hiddenCaptures.footprint - def declaredFootprint = deps(arg).map(actualCaptures(_)).foldLeft(emptySet)(_ ++ _).footprint + def declaredFootprint = deps(arg).map(captures(_)).foldLeft(emptySet)(_ ++ _).footprint def footprintOverlap = hiddenFootprint.overlapWith(clashFootprint) -- declaredFootprint report.error( em"""Separation failure: argument of type ${arg.nuType} |to $funStr |corresponds to capture-polymorphic formal parameter ${formalName}of type ${arg.formalType} |and captures ${CaptureSet(overlap)}, but $whatStr also passed separately - |in the ${whereStr.trim} with type $clashType. + |in the ${whereStr.trim}$clashTypeStr. | | Capture set of $whereStr : ${CaptureSet(clashCaptures)} | Hidden set of current argument : ${CaptureSet(hiddenCaptures)} @@ -115,7 +140,28 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: | Declared footprint of current argument: ${CaptureSet(declaredFootprint)} | Undeclared overlap of footprints : ${CaptureSet(footprintOverlap)}""", arg.srcPos) - end sepError + end sepApplyError + + def sepUseError(tree: Tree, used: Refs, globalOverlap: Refs)(using Context): Unit = + val individualChecks = for mdefs <- previousDefs.iterator; mdef <- mdefs.iterator yield + val hiddenByDef = captures(mdef.tpt).hidden + val overlap = defUseOverlap(hiddenByDef, used, tree.symbol) + if !overlap.isEmpty then + def resultStr = if mdef.isInstanceOf[DefDef] then " result" else "" + report.error( + em"""Separation failure: Illegal access to ${CaptureSet(overlap)} which is hidden by the previous definition + |of ${mdef.symbol} with$resultStr type ${mdef.tpt.nuType}. + |This type hides capabilities ${CaptureSet(hiddenByDef)}""", + tree.srcPos) + true + else false + val clashes = individualChecks.filter(identity) + if clashes.hasNext then clashes.next // issues error as a side effect + else report.error( + em"""Separation failure: Illegal access to ${CaptureSet(globalOverlap)} which is hidden by some previous definitions + |No clashing definitions were found. This might point to an internal error.""", + tree.srcPos) + end sepUseError private def checkApply(fn: Tree, args: List[Tree], deps: collection.Map[Tree, List[Tree]])(using Context): Unit = val fnCaptures = methPart(fn) match @@ -128,24 +174,41 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def subtractDeps(elems: Refs, arg: Tree): Refs = deps(arg).foldLeft(elems): (elems, dep) => - elems -- actualCaptures(dep).footprint + elems -- captures(dep).footprint for (arg, idx) <- indexedArgs do if !arg.needsSepCheck then - footprint = footprint ++ subtractDeps(actualCaptures(arg).footprint, arg) + footprint = footprint ++ subtractDeps(captures(arg).footprint, arg) footprints += ((footprint, idx + 1)) for (arg, idx) <- indexedArgs do if arg.needsSepCheck then val ac = formalCaptures(arg) - val hiddenInArg = hidden(ac).footprint + val hiddenInArg = ac.hidden.footprint //println(i"check sep $arg: $ac, footprint so far = $footprint, hidden = $hiddenInArg") val overlap = subtractDeps(hiddenInArg.overlapWith(footprint), arg) if !overlap.isEmpty then - sepError(fn, args, idx, overlap, hiddenInArg, footprints.toList, deps) - footprint ++= actualCaptures(arg).footprint + sepApplyError(fn, args, idx, overlap, hiddenInArg, footprints.toList, deps) + footprint ++= captures(arg).footprint footprints += ((footprint, idx + 1)) end checkApply + def defUseOverlap(hiddenByDef: Refs, used: Refs, sym: Symbol)(using Context): Refs = + val overlap = hiddenByDef.overlapWith(used) + resultType.get(sym) match + case Some(tp) if !overlap.isEmpty => + val declared = tp.captureSet.elems + overlap -- declared.footprint -- declared.hidden.footprint + case _ => + overlap + + def checkUse(tree: Tree)(using Context) = + val used = tree.markedFree + if !used.elems.isEmpty then + val usedFootprint = used.elems.footprint + val overlap = defUseOverlap(defsShadow, usedFootprint, tree.symbol) + if !overlap.isEmpty then + sepUseError(tree, usedFootprint, overlap) + private def collectMethodTypes(tp: Type): List[TermLambda] = tp match case tp: MethodType => tp :: collectMethodTypes(tp.resType) case tp: PolyType => collectMethodTypes(tp.resType) @@ -184,13 +247,29 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: checkApply(tree, argss.flatten, dependencies(tree, argss)) def traverse(tree: Tree)(using Context): Unit = + tree match + case tree: Apply if tree.symbol == defn.Caps_unsafeAssumeSeparate => return + case _ => + checkUse(tree) tree match case tree: GenericApply => - if tree.symbol != defn.Caps_unsafeAssumeSeparate then - tree.tpe match - case _: MethodOrPoly => - case _ => traverseApply(tree, Nil) - traverseChildren(tree) + tree.tpe match + case _: MethodOrPoly => + case _ => traverseApply(tree, Nil) + traverseChildren(tree) + case tree: Block => + val saved = defsShadow + previousDefs = mutable.ListBuffer() :: previousDefs + try traverseChildren(tree) + finally + previousDefs = previousDefs.tail + defsShadow = saved + case tree: ValOrDefDef => + traverseChildren(tree) + if previousDefs.nonEmpty && !tree.symbol.isOneOf(TermParamOrAccessor) then + defsShadow ++= captures(tree.tpt).hidden.footprint + resultType(tree.symbol) = tree.tpt.nuType + previousDefs.head += tree case _ => traverseChildren(tree) end SepChecker diff --git a/project/Build.scala b/project/Build.scala index a7be508ce11a..bcdbd3ae8a8d 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1214,7 +1214,7 @@ object Build { settings(scala2LibraryBootstrappedSettings). settings( moduleName := "scala2-library-cc", - scalacOptions += "-Ycheck:all", + scalacOptions ++= Seq("-Ycheck:all", "-source", "3.7") ) lazy val scala2LibraryBootstrappedSettings = Seq( diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index cae2f4299e87..3cb57784ad95 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -683,7 +683,8 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz remaining -= 1 scout = scout.tail } - dropRightState(scout) + unsafeAssumeSeparate: + dropRightState(scout) } } diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index acf8faa7a969..d9b10129e3f9 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -1,50 +1,68 @@ --- Error: tests/neg-custom-args/captures/capt1.scala:6:11 -------------------------------------------------------------- -6 | () => if x == null then y else y // error +-- Error: tests/neg-custom-args/captures/capt1.scala:5:11 -------------------------------------------------------------- +5 | () => if x == null then y else y // error | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> C --- Error: tests/neg-custom-args/captures/capt1.scala:9:11 -------------------------------------------------------------- -9 | () => if x == null then y else y // error +-- Error: tests/neg-custom-args/captures/capt1.scala:8:11 -------------------------------------------------------------- +8 | () => if x == null then y else y // error | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type Matchable --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:16:2 ----------------------------------------- -16 | def f(y: Int) = if x == null then y else y // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:15:2 ----------------------------------------- +15 | def f(y: Int) = if x == null then y else y // error | ^ | Found: (y: Int) ->{x} Int | Required: Matchable -17 | f +16 | f | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:23:2 ----------------------------------------- -23 | class F(y: Int) extends A: // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:22:2 ----------------------------------------- +22 | class F(y: Int) extends A: // error | ^ | Found: A^{x} | Required: A -24 | def m() = if x == null then y else y -25 | F(22) +23 | def m() = if x == null then y else y +24 | F(22) | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:28:2 ----------------------------------------- -28 | new A: // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:27:2 ----------------------------------------- +27 | new A: // error | ^ | Found: A^{x} | Required: A -29 | def m() = if x == null then y else y +28 | def m() = if x == null then y else y | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/capt1.scala:34:16 ------------------------------------------------------------- -34 | val z2 = h[() -> Cap](() => x) // error // error +-- Error: tests/neg-custom-args/captures/capt1.scala:36:16 ------------------------------------------------------------- +36 | val z2 = h[() -> Cap](() => x) // error // error | ^^^^^^^^^ - | Type variable X of method h cannot be instantiated to () -> (ex$15: caps.Exists) -> C^{ex$15} since - | the part C^{ex$15} of that type captures the root capability `cap`. --- Error: tests/neg-custom-args/captures/capt1.scala:34:30 ------------------------------------------------------------- -34 | val z2 = h[() -> Cap](() => x) // error // error + | Type variable X of method h cannot be instantiated to () -> (ex$18: caps.Exists) -> C^{ex$18} since + | the part C^{ex$18} of that type captures the root capability `cap`. +-- Error: tests/neg-custom-args/captures/capt1.scala:36:30 ------------------------------------------------------------- +36 | val z2 = h[() -> Cap](() => x) // error // error | ^ | reference (x : C^) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> (ex$15: caps.Exists) -> C^{ex$15} --- Error: tests/neg-custom-args/captures/capt1.scala:36:13 ------------------------------------------------------------- -36 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error + | of an enclosing function literal with expected type () -> (ex$18: caps.Exists) -> C^{ex$18} +-- Error: tests/neg-custom-args/captures/capt1.scala:38:13 ------------------------------------------------------------- +38 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error | ^^^^^^^^^^^^^^^^^^^^^^^ - | Type variable X of method h cannot be instantiated to box () ->{x} (ex$20: caps.Exists) -> C^{ex$20} since - | the part C^{ex$20} of that type captures the root capability `cap`. + | Type variable X of method h cannot be instantiated to box () ->{x} (ex$23: caps.Exists) -> C^{ex$23} since + | the part C^{ex$23} of that type captures the root capability `cap`. +-- Error: tests/neg-custom-args/captures/capt1.scala:43:7 -------------------------------------------------------------- +43 | if x == null then // error: separation + | ^ + | Separation failure: Illegal access to {x} which is hidden by the previous definition + | of value z1 with type () => (ex$27: caps.Exists) -> C^{ex$27}. + | This type hides capabilities {x} +-- Error: tests/neg-custom-args/captures/capt1.scala:44:12 ------------------------------------------------------------- +44 | () => x // error: separation + | ^ + | Separation failure: Illegal access to {x} which is hidden by the previous definition + | of value z1 with type () => (ex$27: caps.Exists) -> C^{ex$27}. + | This type hides capabilities {x} +-- Error: tests/neg-custom-args/captures/capt1.scala:47:2 -------------------------------------------------------------- +47 | x // error: separation + | ^ + | Separation failure: Illegal access to {x} which is hidden by the previous definition + | of value z1 with type () => (ex$27: caps.Exists) -> C^{ex$27}. + | This type hides capabilities {x} diff --git a/tests/neg-custom-args/captures/capt1.scala b/tests/neg-custom-args/captures/capt1.scala index 8da7e633ca51..687073c3cdae 100644 --- a/tests/neg-custom-args/captures/capt1.scala +++ b/tests/neg-custom-args/captures/capt1.scala @@ -1,5 +1,4 @@ - - +import language.future // sepchecks on import annotation.retains class C def f(x: C @retains(caps.cap), y: C): () -> C = @@ -28,10 +27,21 @@ def h4(x: Cap, y: Int): A = new A: // error def m() = if x == null then y else y +def f1(c: Cap): () ->{c} c.type = () => c // ok + def foo() = val x: C @retains(caps.cap) = ??? def h[X](a: X)(b: X) = a + val z2 = h[() -> Cap](() => x) // error // error (() => C()) val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error + val z1: () => Cap = f1(x) + + val z4 = + if x == null then // error: separation + () => x // error: separation + else + () => C() + x // error: separation diff --git a/tests/neg-custom-args/captures/cc-ex-conformance.scala b/tests/neg-custom-args/captures/cc-ex-conformance.scala index 16e13376c5b3..4920f26ac380 100644 --- a/tests/neg-custom-args/captures/cc-ex-conformance.scala +++ b/tests/neg-custom-args/captures/cc-ex-conformance.scala @@ -1,5 +1,6 @@ import language.experimental.captureChecking import caps.{Exists, Capability} +import language.future // sepchecks on class C @@ -15,7 +16,7 @@ def Test = val ex1: EX1 = ??? val ex2: EX2 = ??? val _: EX1 = ex1 - val _: EX2 = ex1 // ok + val _: EX2 = ex1 // error separation val _: EX1 = ex2 // ok val ex3: EX3 = ??? diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index 67685d5663b8..e45a8dad6092 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -1,29 +1,29 @@ --- Error: tests/neg-custom-args/captures/i15772.scala:19:26 ------------------------------------------------------------ -19 | val c : C^{x} = new C(x) // error +-- Error: tests/neg-custom-args/captures/i15772.scala:21:26 ------------------------------------------------------------ +21 | val c : C^{x} = new C(x) // error | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Int --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:20:46 --------------------------------------- -20 | val boxed1 : ((C^) => Unit) -> Unit = box1(c) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:22:46 --------------------------------------- +22 | val boxed1 : ((C^) => Unit) -> Unit = box1(c) // error | ^^^^^^^ | Found: (C{val arg: C^}^{c} => Unit) ->{c} Unit | Required: (C^ => Unit) -> Unit | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/i15772.scala:26:26 ------------------------------------------------------------ -26 | val c : C^{x} = new C(x) // error +-- Error: tests/neg-custom-args/captures/i15772.scala:28:26 ------------------------------------------------------------ +28 | val c : C^{x} = new C(x) // error | ^ | reference (x : C^) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> Int --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:27:35 --------------------------------------- -27 | val boxed2 : Observe[C^] = box2(c) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:29:35 --------------------------------------- +29 | val boxed2 : Observe[C^] = box2(c) // error | ^^^^^^^ | Found: (C{val arg: C^}^{c} => Unit) ->{c} Unit | Required: (C^ => Unit) -> Unit | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:33:34 --------------------------------------- -33 | val boxed2 : Observe[C]^ = box2(c) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:35:34 --------------------------------------- +35 | val boxed2 : Observe[C]^ = box2(c) // error | ^ | Found: box C^ | Required: box C{val arg: C^?}^? @@ -32,8 +32,8 @@ | cannot be included in capture set ? | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:44:2 ---------------------------------------- -44 | x: (() -> Unit) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:46:2 ---------------------------------------- +46 | x: (() -> Unit) // error | ^ | Found: (x : () ->{filesList, sayHello} Unit) | Required: () -> Unit diff --git a/tests/neg-custom-args/captures/i15772.scala b/tests/neg-custom-args/captures/i15772.scala index a054eac835c1..c6e1d8693815 100644 --- a/tests/neg-custom-args/captures/i15772.scala +++ b/tests/neg-custom-args/captures/i15772.scala @@ -1,3 +1,5 @@ +import language.future // sepchecks on + type Observe[T] = (T => Unit) -> Unit def unsafe(cap: C^) = cap.bad() diff --git a/tests/neg-custom-args/captures/sep-compose.check b/tests/neg-custom-args/captures/sep-compose.check index 7ecab087904e..d763a180b9ed 100644 --- a/tests/neg-custom-args/captures/sep-compose.check +++ b/tests/neg-custom-args/captures/sep-compose.check @@ -92,10 +92,10 @@ 40 | p1(f) // error | ^ | Separation failure: argument of type (f : () ->{a} Unit) - | to method apply: (v1: T1): R + | to a function of type (x$0: () => Unit) ->{f} Unit | corresponds to capture-polymorphic formal parameter x$0 of type () => Unit | and captures {f, a, io}, but these capabilities are also passed separately - | in the function prefix with type (p1 : (x$0: () => Unit) ->{f} Unit). + | in the function prefix. | | Capture set of function prefix : {p1} | Hidden set of current argument : {f} diff --git a/tests/neg-custom-args/captures/sep-use.check b/tests/neg-custom-args/captures/sep-use.check new file mode 100644 index 000000000000..9379c29fc950 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-use.check @@ -0,0 +1,24 @@ +-- Error: tests/neg-custom-args/captures/sep-use.scala:7:10 ------------------------------------------------------------ +7 | println(io) // error + | ^^ + | Separation failure: Illegal access to {io} which is hidden by the previous definition + | of value x with type () => Unit. + | This type hides capabilities {io} +-- Error: tests/neg-custom-args/captures/sep-use.scala:13:10 ----------------------------------------------------------- +13 | println(io) // error + | ^^ + | Separation failure: Illegal access to {io} which is hidden by the previous definition + | of method x with result type () => Unit. + | This type hides capabilities {io} +-- Error: tests/neg-custom-args/captures/sep-use.scala:19:10 ----------------------------------------------------------- +19 | println(io) // error + | ^^ + | Separation failure: Illegal access to {io} which is hidden by the previous definition + | of method xx with result type (y: Int) => Unit. + | This type hides capabilities {io} +-- Error: tests/neg-custom-args/captures/sep-use.scala:25:10 ----------------------------------------------------------- +25 | println(io) // error + | ^^ + | Separation failure: Illegal access to {io} which is hidden by the previous definition + | of method xxx with result type Object^. + | This type hides capabilities {io} diff --git a/tests/neg-custom-args/captures/sep-use.scala b/tests/neg-custom-args/captures/sep-use.scala new file mode 100644 index 000000000000..80be5073d06e --- /dev/null +++ b/tests/neg-custom-args/captures/sep-use.scala @@ -0,0 +1,27 @@ +import caps.cap +import language.future // sepchecks on + +def test1(io: Object^): Unit = + + val x: () => Unit = () => println(io) + println(io) // error + println(x) // ok + +def test2(io: Object^): Unit = + + def x: () => Unit = () => println(io) + println(io) // error + println(x) // ok + +def test3(io: Object^): Unit = + + def xx: (y: Int) => Unit = _ => println(io) + println(io) // error + println(xx(2)) // ok + +def test4(io: Object^): Unit = + + def xxx(y: Int): Object^ = io + println(io) // error + println(xxx(2)) // ok + diff --git a/tests/neg-custom-args/captures/sep-use2.scala b/tests/neg-custom-args/captures/sep-use2.scala new file mode 100644 index 000000000000..dc485196ac79 --- /dev/null +++ b/tests/neg-custom-args/captures/sep-use2.scala @@ -0,0 +1,28 @@ +import language.future // sepchecks on + +def test1(c: Object^, f: Object^ => Object^) = + def cc: Object^ = c + val x1 = + { f(cc) } // ok + val x2 = + f(cc) // ok + val x3: Object^ = + f(cc) // ok + val x4: Object^ = + { f(c) } // error + +def test2(c: Object^, f: Object^ ->{c} Object^) = + def cc: Object^ = c + val x1 = + { f(cc) } // error // error + val x4: Object^ = + { f(c) } // error // error + + + + + + + + + diff --git a/tests/pos-custom-args/captures/capt1.scala b/tests/pos-custom-args/captures/capt1.scala index e3f5c20e724e..34e9e40e7fdb 100644 --- a/tests/pos-custom-args/captures/capt1.scala +++ b/tests/pos-custom-args/captures/capt1.scala @@ -1,3 +1,6 @@ +import language.future // sepchecks on +import caps.unsafe.unsafeAssumeSeparate + class C type Cap = C^ def f1(c: Cap): () ->{c} c.type = () => c // ok @@ -22,6 +25,9 @@ def foo(): C^ = val z1: () => Cap = f1(x) def h[X](a: X)(b: X) = a - val z2 = - if x == null then () => x else () => C() - x \ No newline at end of file + val z2 = unsafeAssumeSeparate: + if x == null then + () => x + else + () => C() + unsafeAssumeSeparate(x) \ No newline at end of file diff --git a/tests/pos-custom-args/captures/skolems2.scala b/tests/pos-custom-args/captures/skolems2.scala index dd6417042339..387616e023ec 100644 --- a/tests/pos-custom-args/captures/skolems2.scala +++ b/tests/pos-custom-args/captures/skolems2.scala @@ -1,3 +1,5 @@ +import language.future // sepchecks on + def Test(c: Object^, f: Object^ => Object^) = def cc: Object^ = c val x1 =