From c04390eb507380679d5dc75a57d3af743dab091f Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 6 Dec 2024 16:34:35 +0100 Subject: [PATCH 01/32] 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 cac82eb0c4bd..501e839aae8c 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 5d1fc24b39d730bf5812e90e3d62274ce930c8e7 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 27 Nov 2024 11:36:47 +0100 Subject: [PATCH 02/32] Add Mutable trait and mut modifier --- compiler/src/dotty/tools/dotc/ast/untpd.scala | 2 ++ compiler/src/dotty/tools/dotc/core/Flags.scala | 7 +++++-- compiler/src/dotty/tools/dotc/core/StdNames.scala | 1 + compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala | 1 + .../src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala | 1 + compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 3 ++- compiler/src/dotty/tools/dotc/parsing/Scanners.scala | 5 ++++- library/src/scala/caps.scala | 2 ++ tasty/src/dotty/tools/tasty/TastyFormat.scala | 5 ++++- tests/pos-custom-args/captures/mutRef.scala | 5 +++++ 10 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 tests/pos-custom-args/captures/mutRef.scala diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 2acfc4cf86e3..c6d75c821c20 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -234,6 +234,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class Tracked()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Tracked) + case class Mut()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Mut) + /** Used under pureFunctions to mark impure function types `A => B` in `FunctionWithMods` */ case class Impure()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Impure) } diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 0775b3caaf0c..66a0a40f686e 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -380,6 +380,9 @@ object Flags { /** Tracked modifier for class parameter / a class with some tracked parameters */ val (Tracked @ _, _, Dependent @ _) = newFlags(46, "tracked") + /** `mut` modifier for update methods (cc only) */ + val (Mut @_, MutVal @ _, _) = newFlags(47, "mut") + // ------------ Flags following this one are not pickled ---------------------------------- /** Symbol is not a member of its owner */ @@ -455,7 +458,7 @@ object Flags { CommonSourceModifierFlags.toTypeFlags | Abstract | Sealed | Opaque | Open val TermSourceModifierFlags: FlagSet = - CommonSourceModifierFlags.toTermFlags | Inline | AbsOverride | Lazy | Tracked + CommonSourceModifierFlags.toTermFlags | Inline | AbsOverride | Lazy | Tracked | Mut /** Flags representing modifiers that can appear in trees */ val ModifierFlags: FlagSet = @@ -469,7 +472,7 @@ object Flags { val FromStartFlags: FlagSet = commonFlags( Module, Package, Deferred, Method, Case, Enum, Param, ParamAccessor, Scala2SpecialFlags, MutableOrOpen, Opaque, Touched, JavaStatic, - OuterOrCovariant, LabelOrContravariant, CaseAccessor, Tracked, + OuterOrCovariant, LabelOrContravariant, CaseAccessor, Tracked, Mut, Extension, NonMember, Implicit, Given, Permanent, Synthetic, Exported, SuperParamAliasOrScala2x, Inline, Macro, ConstructorProxy, Invisible) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 56d71c7fb57e..8fea8c0f02de 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -553,6 +553,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" diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 7b80c7c80a21..8a601c96e378 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -895,6 +895,7 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { if (flags.is(Extension)) writeModTag(EXTENSION) if (flags.is(ParamAccessor)) writeModTag(PARAMsetter) if (flags.is(SuperParamAlias)) writeModTag(PARAMalias) + if (flags.is(Mut)) writeModTag(MUT) assert(!(flags.is(Label))) } else { diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index d9ae4ddb6006..456de4ddee0a 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -757,6 +757,7 @@ class TreeUnpickler(reader: TastyReader, case TRANSPARENT => addFlag(Transparent) case INFIX => addFlag(Infix) case TRACKED => addFlag(Tracked) + case MUT => addFlag(Mut) case PRIVATEqualified => readByte() privateWithin = readWithin diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 7933cbbea12f..64f88c6a30f9 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3285,13 +3285,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() } } 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/library/src/scala/caps.scala b/library/src/scala/caps.scala index c35b3b55e813..9e1a546cb2e4 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -19,6 +19,8 @@ import annotation.{experimental, compileTimeOnly, retainsCap} /** Carrier trait for capture set type parameters */ trait CapSet extends Any + trait Mutable + /** A type constraint expressing that the capture set `C` needs to contain * the capability `R` */ diff --git a/tasty/src/dotty/tools/tasty/TastyFormat.scala b/tasty/src/dotty/tools/tasty/TastyFormat.scala index 8ff590fefec5..7165f342f52f 100644 --- a/tasty/src/dotty/tools/tasty/TastyFormat.scala +++ b/tasty/src/dotty/tools/tasty/TastyFormat.scala @@ -229,6 +229,7 @@ Standard-Section: "ASTs" TopLevelStat* OPEN -- an open class INVISIBLE -- invisible during typechecking TRACKED -- a tracked class parameter / a dependent class + MUT -- an update method (cc only) Annotation Variance = STABLE -- invariant @@ -511,6 +512,7 @@ object TastyFormat { final val EMPTYCLAUSE = 45 final val SPLITCLAUSE = 46 final val TRACKED = 47 + final val MUT = 48 // Tree Cat. 2: tag Nat final val firstNatTreeTag = SHAREDterm @@ -703,7 +705,8 @@ object TastyFormat { | ANNOTATION | PRIVATEqualified | PROTECTEDqualified - | TRACKED => true + | TRACKED + | MUT => true case _ => false } 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 From 4520f9080996fcb8d3140941c08e3792797fb207 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 27 Nov 2024 18:45:37 +0100 Subject: [PATCH 03/32] Allow other uses of Mutable flag outside vars Add dedicated tests `isMutableVar` and `isMutableVarOrAccessor` where we now check for the `Mutable` flag. This will allow to use `Mutable` also for update methods, so that we don't have to burn another flag bit for them. --- .../src/dotty/tools/backend/jvm/BTypesFromSymbols.scala | 2 +- compiler/src/dotty/tools/dotc/ast/TreeInfo.scala | 2 +- compiler/src/dotty/tools/dotc/ast/untpd.scala | 1 + compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 2 +- compiler/src/dotty/tools/dotc/core/Flags.scala | 1 - compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 7 +++++++ compiler/src/dotty/tools/dotc/core/SymUtils.scala | 2 +- compiler/src/dotty/tools/dotc/core/TypeComparer.scala | 2 +- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 3 ++- compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala | 4 ++-- compiler/src/dotty/tools/dotc/reporting/messages.scala | 2 +- compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala | 2 +- compiler/src/dotty/tools/dotc/transform/CapturedVars.scala | 2 +- .../src/dotty/tools/dotc/transform/CheckReentrant.scala | 2 +- compiler/src/dotty/tools/dotc/transform/CheckStatic.scala | 2 +- compiler/src/dotty/tools/dotc/transform/CheckUnused.scala | 2 +- compiler/src/dotty/tools/dotc/transform/Constructors.scala | 2 +- compiler/src/dotty/tools/dotc/transform/LazyVals.scala | 4 ++-- compiler/src/dotty/tools/dotc/transform/MoveStatics.scala | 2 +- .../src/dotty/tools/dotc/transform/UninitializedDefs.scala | 2 +- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 2 +- compiler/src/dotty/tools/dotc/transform/init/Util.scala | 2 +- compiler/src/dotty/tools/dotc/typer/Checking.scala | 6 ++++-- compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala | 2 +- compiler/src/dotty/tools/dotc/typer/Nullables.scala | 6 +++--- compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala | 2 +- compiler/src/dotty/tools/dotc/typer/RefChecks.scala | 5 ++--- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala | 2 +- 30 files changed, 44 insertions(+), 35 deletions(-) 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/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 c6d75c821c20..516dd06292da 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -334,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/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 830d9ad0a4d4..c56d83240f55 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1526,7 +1526,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 diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index ebe128d7776c..c1c33cf2615a 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -442,7 +442,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 diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 66a0a40f686e..7371afe00cf5 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -600,7 +600,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/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index be651842d9b0..07506749ff07 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -806,6 +806,13 @@ object SymDenotations { final def isRealMethod(using Context): Boolean = this.is(Method, butNot = Accessor) && !isAnonymousFunction + /** A mutable variable (not a getter or setter for it) */ + final def isMutableVar(using Context): Boolean = is(Mutable, butNot = Method) + + /** A mutable variable or its getter or setter */ + final def isMutableVarOrAccessor(using Context): Boolean = + is(Mutable) && (!is(Method) || is(Accessor)) + /** Is this a getter? */ final def isGetter(using Context): Boolean = this.is(Accessor) && !originalName.isSetterName && !(originalName.isScala2LocalSuffix && symbol.owner.is(Scala2x)) diff --git a/compiler/src/dotty/tools/dotc/core/SymUtils.scala b/compiler/src/dotty/tools/dotc/core/SymUtils.scala index 1a762737d52f..baaeb025c6d5 100644 --- a/compiler/src/dotty/tools/dotc/core/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/SymUtils.scala @@ -287,7 +287,7 @@ class SymUtils: */ def isConstExprFinalVal(using Context): Boolean = atPhaseNoLater(erasurePhase) { - self.is(Final, butNot = Mutable) && self.info.resultType.isInstanceOf[ConstantType] + self.is(Final) && !self.isMutableVarOrAccessor && self.info.resultType.isInstanceOf[ConstantType] } && !self.sjsNeedsField /** The `ConstantType` of a val known to be `isConstrExprFinalVal`. diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 8414c3795f49..0781f40ab880 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 64f88c6a30f9..2f59cea63b08 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -4673,7 +4673,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/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 501e839aae8c..7215467eff29 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -538,7 +538,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" @@ -554,7 +554,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/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 28a2b5757a93..0c2e524a4733 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -1694,7 +1694,7 @@ class OnlyClassesCanHaveDeclaredButUndefinedMembers(sym: Symbol)( def msg(using Context) = i"""Declaration of $sym not allowed here: only classes can have declared but undefined members""" def explain(using Context) = - if sym.is(Mutable) then "Note that variables need to be initialized to be defined." + if sym.isMutableVarOrAccessor then "Note that variables need to be initialized to be defined." else "" } diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index 75e859111932..5dd69ebc3386 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -418,7 +418,7 @@ private class ExtractAPICollector(nonLocalClassSymbols: mutable.HashSet[Symbol]) apiClass(sym.asClass) } else if (sym.isType) { apiTypeMember(sym.asType) - } else if (sym.is(Mutable, butNot = Accessor)) { + } else if (sym.isMutableVar) { api.Var.of(sym.name.toString, apiAccess(sym), apiModifiers(sym), apiAnnotations(sym, inlineOrigin).toArray, apiType(sym.info)) } else if (sym.isStableMember && !sym.isRealMethod) { diff --git a/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala b/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala index c1725cbd0255..7263bce0478c 100644 --- a/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala +++ b/compiler/src/dotty/tools/dotc/transform/CapturedVars.scala @@ -120,7 +120,7 @@ object CapturedVars: def traverse(tree: Tree)(using Context) = tree match case id: Ident => val sym = id.symbol - if sym.is(Mutable, butNot = Method) && sym.owner.isTerm then + if sym.isMutableVar && sym.owner.isTerm then val enclMeth = ctx.owner.enclosingMethod if sym.enclosingMethod != enclMeth then report.log(i"capturing $sym in ${sym.enclosingMethod}, referenced from $enclMeth") diff --git a/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala b/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala index e8a402068bfc..5f52ac82879a 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckReentrant.scala @@ -65,7 +65,7 @@ class CheckReentrant extends MiniPhase { scanning(cls) { for (sym <- cls.classInfo.decls) if (sym.isTerm && !sym.isSetter && !isIgnored(sym)) - if (sym.is(Mutable)) { + if (sym.isMutableVarOrAccessor) { report.error( em"""possible data race involving globally reachable ${sym.showLocated}: ${sym.info} | use -Ylog:checkReentrant+ to find out more about why the variable is reachable.""") diff --git a/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala b/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala index 6c74f302b65d..957fd78e9c2c 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckStatic.scala @@ -52,7 +52,7 @@ class CheckStatic extends MiniPhase { report.error(MissingCompanionForStatic(defn.symbol), defn.srcPos) else if (clashes.exists) report.error(MemberWithSameNameAsStatic(), defn.srcPos) - else if (defn.symbol.is(Flags.Mutable) && companion.is(Flags.Trait)) + else if (defn.symbol.isMutableVarOrAccessor && companion.is(Flags.Trait)) report.error(TraitCompanionWithMutableStatic(), defn.srcPos) else if (defn.symbol.is(Flags.Lazy)) report.error(LazyStaticField(), defn.srcPos) diff --git a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala index d647d50560d3..818fb3fc029d 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala @@ -803,7 +803,7 @@ object CheckUnused: private def isUnsetVarDef(using Context): Boolean = val sym = memDef.symbol - sym.is(Mutable) && !setVars(sym) + sym.isMutableVarOrAccessor && !setVars(sym) extension (imp: tpd.Import) /** Enum generate an import for its cases (but outside them), which should be ignored */ diff --git a/compiler/src/dotty/tools/dotc/transform/Constructors.scala b/compiler/src/dotty/tools/dotc/transform/Constructors.scala index 9a0df830c6d7..b373565489f0 100644 --- a/compiler/src/dotty/tools/dotc/transform/Constructors.scala +++ b/compiler/src/dotty/tools/dotc/transform/Constructors.scala @@ -155,7 +155,7 @@ class Constructors extends MiniPhase with IdentityDenotTransformer { thisPhase = case Ident(_) | Select(This(_), _) => var sym = tree.symbol def isOverridableSelect = tree.isInstanceOf[Select] && !sym.isEffectivelyFinal - def switchOutsideSupercall = !sym.is(Mutable) && !isOverridableSelect + def switchOutsideSupercall = !sym.isMutableVarOrAccessor && !isOverridableSelect // If true, switch to constructor parameters also in the constructor body // that follows the super call. // Variables need to go through the getter since they might have been updated. diff --git a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala index e2712a7d6302..2fd777f715d9 100644 --- a/compiler/src/dotty/tools/dotc/transform/LazyVals.scala +++ b/compiler/src/dotty/tools/dotc/transform/LazyVals.scala @@ -255,7 +255,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { def transformMemberDefThreadUnsafe(x: ValOrDefDef)(using Context): Thicket = { val claz = x.symbol.owner.asClass val tpe = x.tpe.widen.resultType.widen - assert(!(x.symbol is Mutable)) + assert(!x.symbol.isMutableVarOrAccessor) val containerName = LazyLocalName.fresh(x.name.asTermName) val containerSymbol = newSymbol(claz, containerName, x.symbol.flags &~ containerFlagsMask | containerFlags | Private, @@ -447,7 +447,7 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer { } def transformMemberDefThreadSafe(x: ValOrDefDef)(using Context): Thicket = { - assert(!(x.symbol is Mutable)) + assert(!x.symbol.isMutableVarOrAccessor) if ctx.settings.YlegacyLazyVals.value then transformMemberDefThreadSafeLegacy(x) else diff --git a/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala b/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala index 95975ad9e6b8..b3ec05501b5b 100644 --- a/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala +++ b/compiler/src/dotty/tools/dotc/transform/MoveStatics.scala @@ -28,7 +28,7 @@ class MoveStatics extends MiniPhase with SymTransformer { def transformSym(sym: SymDenotation)(using Context): SymDenotation = if (sym.hasAnnotation(defn.ScalaStaticAnnot) && sym.owner.is(Flags.Module) && sym.owner.companionClass.exists && - (sym.is(Flags.Method) || !(sym.is(Flags.Mutable) && sym.owner.companionClass.is(Flags.Trait)))) { + (sym.is(Flags.Method) || !(sym.isMutableVarOrAccessor && sym.owner.companionClass.is(Flags.Trait)))) { sym.owner.asClass.delete(sym.symbol) sym.owner.companionClass.asClass.enter(sym.symbol) sym.copySymDenotation(owner = sym.owner.companionClass) diff --git a/compiler/src/dotty/tools/dotc/transform/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..9b1afc405704 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -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)) { @@ -588,7 +588,9 @@ 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.")) checkApplicable(Erased, - !sym.isOneOf(MutableOrLazy, butNot = Given) && !sym.isType || sym.isClass) + !sym.is(Lazy, butNot = Given) + && !sym.isMutableVarOrAccessor + && (!sym.isType || sym.isClass)) checkCombination(Final, Open) checkCombination(Sealed, Open) checkCombination(Final, Sealed) diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index 13e75be75838..58119981dfc4 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -85,7 +85,7 @@ object ErrorReporting { /** An explanatory note to be added to error messages * when there's a problem with abstract var defs */ def abstractVarMessage(sym: Symbol): String = - if (sym.underlyingSymbol.is(Mutable)) + if sym.underlyingSymbol.isMutableVarOrAccessor then "\n(Note that variables need to be initialized to be defined)" else "" diff --git a/compiler/src/dotty/tools/dotc/typer/Nullables.scala b/compiler/src/dotty/tools/dotc/typer/Nullables.scala index 310ca999f4c5..86b9a337e69a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Nullables.scala +++ b/compiler/src/dotty/tools/dotc/typer/Nullables.scala @@ -253,7 +253,7 @@ object Nullables: val mutables = infos.foldLeft(Set[TermRef]()): (ms, info) => ms.union( if info.asserted == null then Set.empty - else info.asserted.filter(_.symbol.is(Mutable))) + else info.asserted.filter(_.symbol.isMutableVarOrAccessor)) infos.extendWith(NotNullInfo(Set(), mutables)) end extension @@ -307,7 +307,7 @@ object Nullables: || s.isClass // not in a class || recur(s.owner)) - refSym.is(Mutable) // if it is immutable, we don't need to check the rest conditions + refSym.isMutableVarOrAccessor // if it is immutable, we don't need to check the rest conditions && refOwner.isTerm && recur(ctx.owner) end extension @@ -574,7 +574,7 @@ object Nullables: object dropNotNull extends TreeMap: var dropped: Boolean = false override def transform(t: Tree)(using Context) = t match - case AssertNotNull(t0) if t0.symbol.is(Mutable) => + case AssertNotNull(t0) if t0.symbol.isMutableVarOrAccessor => nullables.println(i"dropping $t") dropped = true transform(t0) diff --git a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala index 59993a69797d..4e7c4336b852 100644 --- a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala +++ b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala @@ -130,7 +130,7 @@ trait QuotesAndSplices { report.error("Open pattern expected an identifier", arg.srcPos) EmptyTree } - for arg <- typedArgs if arg.symbol.is(Mutable) do // TODO support these patterns. Possibly using scala.quoted.util.Var + for arg <- typedArgs if arg.symbol.isMutableVarOrAccessor do // TODO support these patterns. Possibly using scala.quoted.util.Var report.error("References to `var`s cannot be used in higher-order pattern", arg.srcPos) val argTypes = typedArgs.map(_.tpe.widenTermRefExpr) val patType = (tree.typeargs.isEmpty, tree.args.isEmpty) match diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 7e53b38b5f98..f92ce6f48b71 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -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)) && @@ -772,7 +772,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 +781,6 @@ object RefChecks { if (member.isSetter) "\n(Note that an abstract var requires a setter in addition to the getter)" else if (member.isGetter && !isMultiple) "\n(Note that an abstract var requires a getter in addition to the setter)" else err.abstractVarMessage(member)) - } else if (underlying.is(Method)) { // If there is a concrete method whose name matches the unimplemented // abstract method, and a cursory examination of the difference reveals diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index c941ffe74e18..ddcd0f55e3ae 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1348,7 +1348,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer cpy.Assign(tree)(lhsCore, typed(tree.rhs, lhs1.tpe.widen)).withType(defn.UnitType) def canAssign(sym: Symbol) = - sym.is(Mutable, butNot = Accessor) || + sym.isMutableVar || ctx.owner.isPrimaryConstructor && !sym.is(Method) && sym.maybeOwner == ctx.owner.owner || // allow assignments from the primary constructor to class fields ctx.owner.name.is(TraitSetterName) || ctx.owner.isStaticConstructor diff --git a/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala b/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala index 3699ca80d011..0c2929283ee3 100644 --- a/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala +++ b/compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala @@ -157,7 +157,7 @@ class VarianceChecker(using Context) { def isLocal = base.isAllOf(PrivateLocal) || base.is(Private) && !base.hasAnnotation(defn.AssignedNonLocallyAnnot) - if base.is(Mutable, butNot = Method) && !isLocal then + if base.isMutableVar && !isLocal then base.removeAnnotation(defn.AssignedNonLocallyAnnot) variance = 0 try checkInfo(base.info) From 95f64b4668698a24b24414898cd2fa178c6cd39d Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 27 Nov 2024 19:37:02 +0100 Subject: [PATCH 04/32] Merge Mut and Mutable flags --- compiler/src/dotty/tools/dotc/ast/untpd.scala | 4 ++-- compiler/src/dotty/tools/dotc/core/Flags.scala | 7 ++----- compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala | 1 - .../src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala | 1 - 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 516dd06292da..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) @@ -234,8 +236,6 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class Tracked()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Tracked) - case class Mut()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Mut) - /** Used under pureFunctions to mark impure function types `A => B` in `FunctionWithMods` */ case class Impure()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Impure) } diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 7371afe00cf5..57bf870c6b64 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -380,9 +380,6 @@ object Flags { /** Tracked modifier for class parameter / a class with some tracked parameters */ val (Tracked @ _, _, Dependent @ _) = newFlags(46, "tracked") - /** `mut` modifier for update methods (cc only) */ - val (Mut @_, MutVal @ _, _) = newFlags(47, "mut") - // ------------ Flags following this one are not pickled ---------------------------------- /** Symbol is not a member of its owner */ @@ -458,7 +455,7 @@ object Flags { CommonSourceModifierFlags.toTypeFlags | Abstract | Sealed | Opaque | Open val TermSourceModifierFlags: FlagSet = - CommonSourceModifierFlags.toTermFlags | Inline | AbsOverride | Lazy | Tracked | Mut + CommonSourceModifierFlags.toTermFlags | Inline | AbsOverride | Lazy | Tracked /** Flags representing modifiers that can appear in trees */ val ModifierFlags: FlagSet = @@ -472,7 +469,7 @@ object Flags { val FromStartFlags: FlagSet = commonFlags( Module, Package, Deferred, Method, Case, Enum, Param, ParamAccessor, Scala2SpecialFlags, MutableOrOpen, Opaque, Touched, JavaStatic, - OuterOrCovariant, LabelOrContravariant, CaseAccessor, Tracked, Mut, + OuterOrCovariant, LabelOrContravariant, CaseAccessor, Tracked, Extension, NonMember, Implicit, Given, Permanent, Synthetic, Exported, SuperParamAliasOrScala2x, Inline, Macro, ConstructorProxy, Invisible) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 8a601c96e378..7b80c7c80a21 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -895,7 +895,6 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) { if (flags.is(Extension)) writeModTag(EXTENSION) if (flags.is(ParamAccessor)) writeModTag(PARAMsetter) if (flags.is(SuperParamAlias)) writeModTag(PARAMalias) - if (flags.is(Mut)) writeModTag(MUT) assert(!(flags.is(Label))) } else { diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 456de4ddee0a..d9ae4ddb6006 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -757,7 +757,6 @@ class TreeUnpickler(reader: TastyReader, case TRANSPARENT => addFlag(Transparent) case INFIX => addFlag(Infix) case TRACKED => addFlag(Tracked) - case MUT => addFlag(Mut) case PRIVATEqualified => readByte() privateWithin = readWithin From 9bd69d2b2e533f2b5ec4bebe32cc6877a5bdd861 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 27 Nov 2024 20:25:48 +0100 Subject: [PATCH 05/32] Disallow mut methods overriding normal methods --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 10 ++++++++++ .../dotty/tools/dotc/cc/CheckCaptures.scala | 15 +++++++-------- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../dotty/tools/dotc/typer/RefChecks.scala | 4 +++- .../captures/mut-override.scala | 19 +++++++++++++++++++ 5 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 tests/neg-custom-args/captures/mut-override.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index bc4eb92234eb..a5096b6e50a8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -615,6 +615,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 diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index c56d83240f55..d745a6e1339e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -484,7 +484,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 @@ -1484,18 +1485,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) = diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 2890bdf306be..0ec7e5fe690e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1001,6 +1001,7 @@ class Definitions { @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) diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index f92ce6f48b71..a10815815a7b 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -21,7 +21,7 @@ import config.MigrationVersion import config.Printers.refcheck import reporting.* import Constants.Constant -import cc.stripCapturing +import cc.{stripCapturing, isUpdateMethod} object RefChecks { import tpd.* @@ -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) 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 From b285be9950ce05285596fea44c82b654e25183c5 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 29 Nov 2024 17:46:32 +0100 Subject: [PATCH 06/32] Add readOnly capabilities Also, add design document from papers to docs/internals --- .../src/dotty/tools/dotc/ast/Desugar.scala | 2 + .../src/dotty/tools/dotc/cc/CaptureOps.scala | 101 +++- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 60 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 34 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 + .../dotty/tools/dotc/core/Definitions.scala | 4 +- .../src/dotty/tools/dotc/core/StdNames.scala | 2 + .../dotty/tools/dotc/parsing/Parsers.scala | 36 +- .../tools/dotc/printing/PlainPrinter.scala | 3 +- .../tools/dotc/printing/RefinedPrinter.scala | 2 + .../_docs/internals/exclusive-capabilities.md | 531 ++++++++++++++++++ .../internal/readOnlyCapability.scala | 7 + library/src/scala/caps.scala | 13 +- 13 files changed, 724 insertions(+), 73 deletions(-) create mode 100644 docs/_docs/internals/exclusive-capabilities.md create mode 100644 library/src/scala/annotation/internal/readOnlyCapability.scala 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/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index a5096b6e50a8..3ce31b644887 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 @@ -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 @@ -345,7 +348,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 _ => @@ -382,20 +386,23 @@ extension (tp: Type) /** 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 +413,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,8 +432,40 @@ 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 `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 `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 @@ -647,6 +675,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. */ @@ -655,22 +691,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 + 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): + protected def unwrappable(using Context) = Set() /** 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) +object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot): + protected def unwrappable(using Context) = Set(defn.MaybeCapabilityAnnot) -/** An extractor for `ref @maybeCapability`, which is used to express - * the maybe capability `ref?` as a type. +/** An extractor for `ref @readOnlyCapability`, which is used to express + * the rad-only capability `ref.rd` as a type. */ -object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot) +object ReadOnlyCapability extends AnnotatedCapability(defn.ReadOnlyCapabilityAnnot): + protected def unwrappable(using Context) = Set(defn.ReachCapabilityAnnot, defn.MaybeCapabilityAnnot) /** Offers utility method to be used for type maps that follow aliases */ trait ConservativeFollowAliasMap(using Context) extends TypeMap: diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 9bda9102cbb8..a5d4c0c20e22 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 + * `.rd` first, `*` next, `?` last. */ trait CaptureRef extends TypeProxy, ValueType: private var myCaptureSet: CaptureSet | Null = uninitialized @@ -28,27 +30,44 @@ 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 - - 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 + + /** Is this a unique reference of the form `x!`? */ + final def isReadOnly(using Context): Boolean = this ne stripReadOnly + + final def stripMaybe(using Context): CaptureRef = this match + case AnnotatedType(tp1: CaptureRef, annot) if annot.symbol == defn.MaybeCapabilityAnnot => + tp1 + case _ => + this + + final def stripReach(using Context): CaptureRef = this match + case tp @ AnnotatedType(tp1: CaptureRef, annot) => + val sym = annot.symbol + if sym == defn.ReachCapabilityAnnot then + tp1 + else if sym == defn.MaybeCapabilityAnnot then + tp.derivedAnnotatedType(tp1.stripReach, annot) + else + this + 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.ReachCapabilityAnnot || sym == defn.MaybeCapabilityAnnot then + tp.derivedAnnotatedType(tp1.stripReadOnly, annot) + else + this + case _ => + this /** Is this reference the generic root capability `cap` ? */ final def isRootCapability(using Context): Boolean = this match @@ -142,6 +161,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 1750e98f708a..bf48dc012309 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -373,6 +373,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) @@ -521,6 +525,8 @@ object CaptureSet: elem.cls.ccLevel.nextInner <= level case ReachCapability(elem1) => levelOK(elem1) + case ReadOnlyCapability(elem1) => + levelOK(elem1) case MaybeCapability(elem1) => levelOK(elem1) case _ => @@ -1021,25 +1027,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 = diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index d745a6e1339e..ab1d0d8971e5 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 @@ -1106,6 +1107,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 = diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 0ec7e5fe690e..eb7a3a391a2d 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -992,9 +992,10 @@ 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") @@ -1075,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/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 8fea8c0f02de..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$" @@ -588,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/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 2f59cea63b08..c0fc9904c048 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1588,22 +1588,34 @@ 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 optStar(ref: Tree): Tree = + if isIdent(nme.raw.STAR) then derived(ref, nme.CC_REACH) + else ref + + def recur(ref: Tree): Tree = + if in.token == DOT then + in.nextToken() + if in.isIdent(nme.rd) then optStar(derived(ref, nme.CC_READONLY)) + else recur(selector(ref)) + else if isIdent(nme.UPARROW) then + in.nextToken() + atSpan(startOffset(ref)): + convertToTypeId(ref) match + case ref: RefTree => makeCapsOf(ref) + case ref => ref + else optStar(ref) + + recur(simpleRef()) + end captureRef /** CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` -- under captureChecking */ diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 7215467eff29..1d491d30479b 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 { @@ -422,6 +422,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) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index b7f2eef8c8f9..9d31042cb6bf 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -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/docs/_docs/internals/exclusive-capabilities.md b/docs/_docs/internals/exclusive-capabilities.md new file mode 100644 index 000000000000..629d33d51729 --- /dev/null +++ b/docs/_docs/internals/exclusive-capabilities.md @@ -0,0 +1,531 @@ +# 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. + +**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) } rd` + - `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 `Mutable` type with a regular capture set if the expected type is one of the following: + + - a value type that has a capture set containing only shared capabilities, 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. + +## 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 9e1a546cb2e4..a2f8c55f3214 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -43,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 @@ -54,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 From b5109643f4038a6dba3c4883c28a7c3f33e64317 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 3 Dec 2024 17:51:34 +0100 Subject: [PATCH 07/32] Fixes to capability ordering and handling root capabilities --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 25 +++++++++------ .../src/dotty/tools/dotc/cc/CaptureRef.scala | 31 +++++++++++-------- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 13 ++++++-- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- .../src/dotty/tools/dotc/cc/Existential.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 2 +- .../dotty/tools/dotc/parsing/Parsers.scala | 16 +++++----- .../tools/dotc/printing/PlainPrinter.scala | 2 +- 8 files changed, 57 insertions(+), 36 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 3ce31b644887..128c4591119d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -186,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) @@ -225,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 _ => @@ -507,9 +509,12 @@ 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.elems.forall(_.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) @@ -709,17 +714,17 @@ abstract class AnnotatedCapability(annot: Context ?=> ClassSymbol): object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot): protected def unwrappable(using Context) = Set() -/** 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) - /** 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.ReachCapabilityAnnot, defn.MaybeCapabilityAnnot) + 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 a5d4c0c20e22..3d6addfd9a34 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -17,7 +17,7 @@ import StdNames.nme /** 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. * If there are several annotations they come with an orderL - * `.rd` first, `*` next, `?` last. + * `*` first, `.rd` next, `?` last. */ trait CaptureRef extends TypeProxy, ValueType: private var myCaptureSet: CaptureSet | Null = uninitialized @@ -33,47 +33,52 @@ trait CaptureRef extends TypeProxy, ValueType: /** Is this a maybe reference of the form `x?`? */ final def isMaybe(using Context): Boolean = this ne stripMaybe - /** Is this a reach reference of the form `x*`? */ - final def isReach(using Context): Boolean = this ne stripReach - /** Is this a unique reference of the form `x!`? */ final def isReadOnly(using Context): Boolean = this ne stripReadOnly + /** 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 stripReach(using Context): CaptureRef = this match + final def stripReadOnly(using Context): CaptureRef = this match case tp @ AnnotatedType(tp1: CaptureRef, annot) => val sym = annot.symbol - if sym == defn.ReachCapabilityAnnot then + if sym == defn.ReadOnlyCapabilityAnnot then tp1 else if sym == defn.MaybeCapabilityAnnot then - tp.derivedAnnotatedType(tp1.stripReach, annot) + tp.derivedAnnotatedType(tp1.stripReadOnly, annot) else this case _ => this - final def stripReadOnly(using Context): CaptureRef = this match + final def stripReach(using Context): CaptureRef = this match case tp @ AnnotatedType(tp1: CaptureRef, annot) => val sym = annot.symbol - if sym == defn.ReadOnlyCapabilityAnnot then + if sym == defn.ReachCapabilityAnnot then tp1 - else if sym == defn.ReachCapabilityAnnot || sym == defn.MaybeCapabilityAnnot then - tp.derivedAnnotatedType(tp1.stripReadOnly, annot) + 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) @@ -141,7 +146,7 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false (this eq y) - || this.isRootCapability + || this.isCap || y.match case y: TermRef => y.prefix.match diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index bf48dc012309..0c2628d3262f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -83,6 +83,10 @@ 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) = @@ -559,8 +563,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 && elems.forall(_.isReadOnly) then + shared else computingApprox = true try @@ -1081,6 +1087,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 _ => ofType(ref.underlying, followResult = true) /** Capture set of a type */ @@ -1203,9 +1211,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 ab1d0d8971e5..d78320148062 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1602,7 +1602,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..dfff2b823f3b 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -303,7 +303,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 c1c33cf2615a..0c6b12874288 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -734,7 +734,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/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index c0fc9904c048..fca810cf0efe 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1588,7 +1588,7 @@ object Parsers { case _ => None } - /** CaptureRef ::= { SimpleRef `.` } SimpleRef [`.` rd] [`*`] + /** CaptureRef ::= { SimpleRef `.` } SimpleRef [`*`] [`.` rd] * | [ { SimpleRef `.` } SimpleRef `.` ] id `^` */ def captureRef(): Tree = @@ -1597,22 +1597,24 @@ object Parsers { in.nextToken() atSpan(startOffset(ref)) { PostfixOp(ref, Ident(name)) } - def optStar(ref: Tree): Tree = - if isIdent(nme.raw.STAR) then derived(ref, nme.CC_REACH) - else ref - def recur(ref: Tree): Tree = if in.token == DOT then in.nextToken() - if in.isIdent(nme.rd) then optStar(derived(ref, nme.CC_READONLY)) + 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 optStar(ref) + else ref recur(simpleRef()) end captureRef diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 1d491d30479b..16c0e8ed1aac 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -195,7 +195,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" From 487625052361e511049ea28ce93b4bd41790bfe2 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 3 Dec 2024 18:12:20 +0100 Subject: [PATCH 08/32] Disallow update methods which are not members of classes extending Mutable --- compiler/src/dotty/tools/dotc/typer/Checking.scala | 4 +++- .../neg-custom-args/captures/mut-outside-mutable.check | 8 ++++++++ .../neg-custom-args/captures/mut-outside-mutable.scala | 10 ++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 tests/neg-custom-args/captures/mut-outside-mutable.check create mode 100644 tests/neg-custom-args/captures/mut-outside-mutable.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 9b1afc405704..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.* @@ -587,6 +587,8 @@ 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.is(Lazy, butNot = Given) && !sym.isMutableVarOrAccessor 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() From 8aabf14a55991765fad5ec45bcebe2ec424cfbad Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 3 Dec 2024 22:13:40 +0100 Subject: [PATCH 09/32] Implement readOnly access --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 10 ++++++--- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 14 +++++++++--- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 8 ++++++- .../dotty/tools/dotc/cc/CheckCaptures.scala | 21 ++++++++++++------ tests/pos-custom-args/captures/readOnly.scala | 22 +++++++++++++++++++ 5 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 tests/pos-custom-args/captures/readOnly.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 128c4591119d..051838f7fa6f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -385,6 +385,12 @@ 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. */ @@ -511,9 +517,7 @@ extension (tp: Type) else t.dealiasKeepAnnots match case t @ CapturingType(p, cs) if cs.containsRootCapability => change = true - val reachRef = - if cs.elems.forall(_.isReadOnly) then ref.reach.readOnly - else ref.reach + 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 diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 3d6addfd9a34..00ae32232d6d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -33,8 +33,12 @@ trait CaptureRef extends TypeProxy, ValueType: /** Is this a maybe reference of the form `x?`? */ final def isMaybe(using Context): Boolean = this ne stripMaybe - /** Is this a unique reference of the form `x!`? */ - final def isReadOnly(using Context): Boolean = this ne stripReadOnly + /** Is this a read-only reference of the form `x.rd` or a capture set variable + * with only read-ony references in its upper bound? + */ + final def isReadOnly(using Context): Boolean = this match + case tp: TypeRef => tp.captureSetOfInfo.isReadOnly + case _ => this ne stripReadOnly /** Is this a reach reference of the form `x*`? */ final def isReach(using Context): Boolean = this ne stripReach @@ -81,10 +85,14 @@ trait CaptureRef extends TypeProxy, ValueType: /** 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 diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 0c2628d3262f..7335ad3c16c3 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -92,6 +92,12 @@ sealed abstract class CaptureSet extends Showable: 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. @@ -565,7 +571,7 @@ object CaptureSet: this else if isUniversal || computingApprox then universal - else if containsRootCapability && elems.forall(_.isReadOnly) then + else if containsRootCapability && isReadOnly then shared else computingApprox = true diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index d78320148062..f8331ddaf96b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -382,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 @@ -532,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) @@ -547,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. diff --git a/tests/pos-custom-args/captures/readOnly.scala b/tests/pos-custom-args/captures/readOnly.scala new file mode 100644 index 000000000000..95e5a903aa22 --- /dev/null +++ b/tests/pos-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 _: () ->{a.rd} Int = getA + + val putA = (x: Int) => a.put(x) + val _: Int ->{a} Unit = putA + + def setMax(x: Ref^{cap.rd}, y: Ref^{cap.rd}, z: Ref^{cap}) = + val doit = () => z.put(x.get max y.get) + val _: () ->{x.rd, y.rd, z} Unit = doit + doit() From 94849cf3391ca374fdb34671e9268349efe478ab Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 5 Dec 2024 16:36:17 +0100 Subject: [PATCH 10/32] Check that update methods are only called on references with exclusive capture sets. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 6 +++++ tests/neg-custom-args/captures/readOnly.check | 19 ++++++++++++++++ tests/neg-custom-args/captures/readOnly.scala | 22 +++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 tests/neg-custom-args/captures/readOnly.check create mode 100644 tests/neg-custom-args/captures/readOnly.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index f8331ddaf96b..0338342f66dd 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -582,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 diff --git a/tests/neg-custom-args/captures/readOnly.check b/tests/neg-custom-args/captures/readOnly.check new file mode 100644 index 000000000000..b558a39723b8 --- /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^{cap.rd}), + | 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() From ab855e199d746bef53a53375d864fb6f45d76652 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 6 Dec 2024 19:06:50 +0100 Subject: [PATCH 11/32] Use cap.rd as default capture set of Capability subtypes --- .../dotty/tools/dotc/core/Definitions.scala | 4 ++-- .../tools/dotc/printing/PlainPrinter.scala | 23 ++++++++++--------- .../tools/dotc/printing/RefinedPrinter.scala | 2 +- .../_docs/internals/exclusive-capabilities.md | 9 +++++--- tests/neg-custom-args/captures/i21614.check | 19 ++++++++------- .../captures/lazylists-exceptions.check | 2 +- tests/neg-custom-args/captures/real-try.check | 10 ++++---- 7 files changed, 38 insertions(+), 31 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index eb7a3a391a2d..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 @@ -1005,7 +1005,7 @@ class Definitions { @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") diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 16c0e8ed1aac..49b98376ecaa 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -177,16 +177,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 { @@ -247,9 +241,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 diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 9d31042cb6bf..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) diff --git a/docs/_docs/internals/exclusive-capabilities.md b/docs/_docs/internals/exclusive-capabilities.md index 629d33d51729..0adeafdbb566 100644 --- a/docs/_docs/internals/exclusive-capabilities.md +++ b/docs/_docs/internals/exclusive-capabilities.md @@ -152,7 +152,7 @@ type extending `Mutable` and it contains only shared capabilities, otherwise it 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) } rd` + - `ro ({ x1, ..., xn } _) = { ro(x1), ..., ro(xn) }` - `ro(x) = x` if `x` is shared - `ro(x) = x.rd` if `x` is exclusive @@ -243,15 +243,18 @@ Then ## Accesses to Mutable Types -A _read-only access_ is a reference `x` to a `Mutable` type with a regular capture set if the expected type is one of the following: +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 has a capture set containing only shared capabilities, or + - 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: 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/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 From 7fa1e8d2818df9ee586e909a28fb92ab98c3fad2 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 7 Dec 2024 16:59:46 +0100 Subject: [PATCH 12/32] Narrow retained capture set if expected type is read-only. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 2 ++ .../dotty/tools/dotc/cc/CheckCaptures.scala | 29 ++++++++++++--- .../tools/dotc/printing/PlainPrinter.scala | 5 +-- .../_docs/internals/exclusive-capabilities.md | 17 +++++++++ tests/pos-custom-args/captures/readOnly.scala | 36 +++++++++++++++---- 5 files changed, 77 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 7335ad3c16c3..d626e9edf61f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -320,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() diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 0338342f66dd..45810a08fb28 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1443,6 +1443,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*}` @@ -1452,12 +1471,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 ------------------------------------------- diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 49b98376ecaa..bd26f8c75523 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)), ", ") ~ "}" diff --git a/docs/_docs/internals/exclusive-capabilities.md b/docs/_docs/internals/exclusive-capabilities.md index 0adeafdbb566..97c6592ac693 100644 --- a/docs/_docs/internals/exclusive-capabilities.md +++ b/docs/_docs/internals/exclusive-capabilities.md @@ -56,6 +56,22 @@ When we create an instance of a mutable type we always add `cap` to its capture **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. @@ -94,6 +110,7 @@ a mutable type is defined by the presence of update methods, not by what it exte 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. diff --git a/tests/pos-custom-args/captures/readOnly.scala b/tests/pos-custom-args/captures/readOnly.scala index 95e5a903aa22..a550010360a3 100644 --- a/tests/pos-custom-args/captures/readOnly.scala +++ b/tests/pos-custom-args/captures/readOnly.scala @@ -1,14 +1,18 @@ import caps.Mutable import caps.cap -class Ref(init: Int) extends Mutable: +trait Rdr[T]: + def get: T + +class Ref[T](init: T) extends Rdr[T], Mutable: private var current = init - def get: Int = current - mut def put(x: Int): Unit = current = x + def get: T = current + mut def put(x: T): Unit = current = x def Test(c: Object^) = - val a: Ref^ = Ref(1) - val b: Ref^ = Ref(2) + val a: Ref[Int]^ = Ref(1) + val b: Ref[Int]^ = Ref(2) + def aa = a val getA = () => a.get val _: () ->{a.rd} Int = getA @@ -16,7 +20,27 @@ def Test(c: Object^) = val putA = (x: Int) => a.put(x) val _: Int ->{a} Unit = putA - def setMax(x: Ref^{cap.rd}, y: Ref^{cap.rd}, z: Ref^{cap}) = + 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 19ce87b100b9783f9cf7bfea4698b995978e920b Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 8 Dec 2024 12:45:27 +0100 Subject: [PATCH 13/32] Make Mutable a Capability This means Mutable classes get {cap.rd} as default capture set. --- library/src/scala/caps.scala | 4 ++-- tests/neg-custom-args/captures/readOnly.check | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index a2f8c55f3214..fb4bacd1a948 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -16,11 +16,11 @@ 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 - trait Mutable - /** A type constraint expressing that the capture set `C` needs to contain * the capability `R` */ diff --git a/tests/neg-custom-args/captures/readOnly.check b/tests/neg-custom-args/captures/readOnly.check index b558a39723b8..e1aed07657e5 100644 --- a/tests/neg-custom-args/captures/readOnly.check +++ b/tests/neg-custom-args/captures/readOnly.check @@ -15,5 +15,5 @@ -- 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^{cap.rd}), + | cannot call update method put from (z : Ref), | since its capture set {z} is read-only From 219e3c0e69ce129ce0894d26b896b8edeaca0eff Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 8 Dec 2024 12:46:03 +0100 Subject: [PATCH 14/32] Use {cap} as capture set for creation of types extending Mutable --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 35 ++----------------- .../dotty/tools/dotc/cc/CheckCaptures.scala | 4 ++- .../src/dotty/tools/dotc/cc/Existential.scala | 1 - 3 files changed, 5 insertions(+), 35 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 051838f7fa6f..109b402bf89a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -475,39 +475,8 @@ extension (tp: Type) case _ => ReadOnlyCapability(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, replacxe all no-flip covariant occurrences of `cap` + * in type `tp` with `x*`. */ def withReachCaptures(ref: Type)(using Context): Type = object narrowCaps extends TypeMap: diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 45810a08fb28..eab11d03144d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -746,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 diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index dfff2b823f3b..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.* From 812ed8dc4ceb87e975784198bf7c14fdd7d88017 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 14 Dec 2024 17:28:50 +0100 Subject: [PATCH 15/32] Infrastructure for fresh capture refs --- .../tools/dotc/cc/CaptureAnnotation.scala | 5 - .../src/dotty/tools/dotc/cc/CaptureOps.scala | 25 ++++- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 1 + .../src/dotty/tools/dotc/cc/CaptureSet.scala | 83 ++++++++++++++--- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 91 +++++++++++++++++++ .../dotty/tools/dotc/core/Definitions.scala | 4 + .../tools/dotc/printing/PlainPrinter.scala | 25 ++--- .../tools/dotc/printing/RefinedPrinter.scala | 2 +- .../annotation/internal/freshCapability.scala | 7 ++ .../captures/update-call.scala | 19 ++++ .../captures/simple-apply.scala | 6 ++ tests/pos-custom-args/captures/skolems2.scala | 15 +++ 12 files changed, 250 insertions(+), 33 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/Fresh.scala create mode 100644 library/src/scala/annotation/internal/freshCapability.scala create mode 100644 tests/neg-custom-args/captures/update-call.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/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala index f0018cc93d7e..8977d146a639 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -55,11 +55,6 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte if (this.refs eq refs) && (this.boxed == boxed) then this else CaptureAnnotation(refs, boxed)(cls) - override def sameAnnotation(that: Annotation)(using Context): Boolean = that match - case CaptureAnnotation(refs, boxed) => - this.refs == refs && this.boxed == boxed && this.symbol == that.symbol - case _ => false - override def mapWith(tm: TypeMap)(using Context) = val elems = refs.elems.toList val elems1 = elems.mapConserve(tm) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 109b402bf89a..17fcfd120fed 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -46,6 +46,8 @@ object ccConfig: */ def useSealed(using Context) = Feature.sourceVersion.stable != SourceVersion.`3.5` + + val useFresh: Boolean = false end ccConfig @@ -193,10 +195,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 @@ -512,6 +511,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.containsRootCapability => + 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 diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 00ae32232d6d..f932636fa5f2 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -87,6 +87,7 @@ trait CaptureRef extends TypeProxy, ValueType: 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 diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index d626e9edf61f..abb46c4b87c2 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.* @@ -136,7 +135,7 @@ sealed abstract class CaptureSet extends Showable: * capture set. */ protected final def addNewElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if elem.isMaxCapability || summon[VarState] == FrozenState then + if elem.isMaxCapability || summon[VarState].isInstanceOf[FrozenState] then addThisElem(elem) else addThisElem(elem).orElse: @@ -167,16 +166,39 @@ sealed abstract class CaptureSet extends Showable: /** {x} <:< this where <:< is subcapturing, but treating all variables * as frozen. */ - def accountsFor(x: CaptureRef)(using Context): Boolean = + def accountsFor(x: CaptureRef)(using ctx: Context, vs: VarState = FrozenSepState): Boolean = + + /** Like `refs.exists(p)`, but testing fresh cap instances in refs last */ + def existsElem(refs: SimpleIdentitySet[CaptureRef], p: CaptureRef => Boolean): Boolean = + refs.exists: + case Fresh.Cap(_) => false + case elem => p(elem) + || + refs.exists: + case elem @ Fresh.Cap(_) => p(elem) + case elem => false + + def subsumesX(elem: CaptureRef) = elem match + case Fresh.Cap(hidden) => + if (elem eq x) || hidden.elems.exists(_.subsumes(x)) then true + else if !hidden.recordElemsState() || x.stripReadOnly.isCap then false + else + hidden.elems += x + true + case _ => elem.subsumes(x) + def debugInfo(using Context) = i"$this accountsFor $x, which has capture set ${x.captureSetOfInfo}" + def test(using Context) = reporting.trace(debugInfo): - elems.exists(_.subsumes(x)) + existsElem(elems, subsumesX(_)) || !x.isMaxCapability && !x.derivesFrom(defn.Caps_CapSet) && x.captureSetOfInfo.subCaptures(this, frozen = true).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 @@ -210,7 +232,7 @@ sealed abstract class CaptureSet extends Showable: * will result in failure. */ final def subCaptures(that: CaptureSet, frozen: Boolean)(using Context): CompareResult = - subCaptures(that)(using ctx, if frozen then FrozenState else VarState()) + subCaptures(that)(using ctx, if frozen then FrozenState() else VarState()) /** The subcapturing test, using a given VarState */ private def subCaptures(that: CaptureSet)(using Context, VarState): CompareResult = @@ -366,6 +388,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 +403,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) @@ -435,7 +462,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 @@ -479,7 +506,7 @@ object CaptureSet: /** Record current elements in given VarState provided it does not yet * contain an entry for this variable. */ - private def recordElemsState()(using VarState): Boolean = + protected[CaptureSet] def recordElemsState()(using VarState): Boolean = varState.getElems(this) match case None => varState.putElems(this, elems) case _ => true @@ -511,6 +538,7 @@ object CaptureSet: else //if id == 34 then assert(!elem.isUniversalRootCapability) assert(elem.isTrackableRef, elem) + assert(!this.isInstanceOf[HiddenSet] || summon[VarState] == FrozenSepState, summon[VarState]) elems += elem if elem.isRootCapability then rootAddedHandler() @@ -885,6 +913,25 @@ object CaptureSet: def elemIntersection(cs1: CaptureSet, cs2: CaptureSet)(using Context): Refs = cs1.elems.filter(cs2.mightAccountFor) ++ cs2.elems.filter(cs1.mightAccountFor) + /** A capture set variable used to record the references hidden by a Fresh.Cap instance */ + class HiddenSet(initialHidden: Refs = emptySet)(using @constructorOnly ictx: Context) + extends Var(initialElems = initialHidden): + override def recordElemsState()(using VarState): Boolean = + varState.getElems(this) match + case None => varState.putHidden(this, elems) + case _ => true + + /** Apply function `f` to `elems` while setting `elems` to empty for the + * duration. This is used to escape infinite recursions if two Frash.Caps + * refer to each other in their hidden sets. + */ + override def processElems[T](f: Refs => T): T = + val savedElems = elems + elems = emptySet + try f(savedElems) + finally elems = savedElems + end HiddenSet + /** Extrapolate tm(r) according to `variance`. Let r1 be the result of tm(r). * - If r1 is a tracked CaptureRef, return {r1} * - If r1 has an empty capture set, return {} @@ -1013,6 +1060,12 @@ object CaptureSet: */ def putDeps(v: Var, deps: Deps): Boolean = { depsMap(v) = deps; true } + /** Record hidden elements in elemsMap of hidden set `v`, + * return whether this was allowed. By default, recording is allowed + * but the special state FrozenSepState overrides this. + */ + def putHidden(v: HiddenSet, elems: Refs): Boolean = { elemsMap(v) = elems; true } + /** Roll back global state to what was recorded in this VarState */ def rollBack(): Unit = elemsMap.keysIterator.foreach(_.resetElems()(using this)) @@ -1022,12 +1075,19 @@ object CaptureSet: /** 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) + * On the other hand, this state does allow by default Fresh.Cap to subsume arbitary + * types, which are then recorded in its hidden set. */ - @sharable - object FrozenState extends VarState: + class FrozenState extends VarState: override def putElems(v: Var, refs: Refs) = false override def putDeps(v: Var, deps: Deps) = false - override def rollBack(): Unit = () + + @sharable + /** A frozen state that allows a Fresh.Cap instncce to subsume a + * reference `r` only if `r` is present in the hidden set of the instance. + */ + object FrozenSepState extends FrozenState: + override def putHidden(v: HiddenSet, elems: Refs): Boolean = false @sharable /** A special state that turns off recording of elements. Used only @@ -1092,6 +1152,7 @@ object CaptureSet: case ref: (TermRef | TermParamRef) if ref.isMaxCapability => if ref.isTrackableRef then ref.singletonCaptureSet else CaptureSet.universal + case Fresh.Cap(_) => ref.singletonCaptureSet case ReachCapability(ref1) => ref1.widen.deepCaptureSet(includeTypevars = true) .showing(i"Deep capture set of $ref: ${ref1.widen} = ${result}", capt) 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..efa95a136370 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -0,0 +1,91 @@ +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 + + object Cap: + def apply(initialHidden: Refs = emptySet)(using Context): AnnotatedType = + AnnotatedType(defn.captureRoot.termRef, Annot(CaptureSet.HiddenSet(initialHidden))) + + def unapply(tp: AnnotatedType)(using Context): Option[CaptureSet.HiddenSet] = tp.annot match + case Annot(hidden) => Some(hidden) + case _ => None + end Cap + + class FromCap(initialHidden: Refs = emptySet)(using Context) extends BiTypeMap: + thisMap => + + var change = false + + override def apply(t: Type) = + if variance <= 0 then t + else t.dealiasKeepAnnots match + case t: CaptureRef if t.isCap => + change = true + Cap(initialHidden) + case CapturingType(_, v: CaptureSet.Var) => + change = true + mapOver(t) + case _ => + mapOver(t) + + override def toString = "CapToFresh" + + lazy val inverse = new BiTypeMap: + def apply(t: Type): Type = t match + case t @ Cap(_) => defn.captureRoot.termRef + case _ => mapOver(t) + def inverse = thisMap + override def toString = thisMap.toString + ".inverse" + + end FromCap + + /** Maps cap to fresh */ + def fromCap(tp: Type, initialHidden: Refs = emptySet)(using Context): Type = + val mapper = FromCap(initialHidden) + val mapped = mapper(tp) + if mapper.change then mapped else tp + + def fromCap(tp: Type, initialHidden: CaptureRef)(using Context): Type = + fromCap(tp, SimpleIdentitySet(initialHidden)) + + def fromCap(info: Type, sym: Symbol)(using Context): Type = + val initHidden = + if sym.exists && sym.termRef.isTracked then SimpleIdentitySet(sym.termRef) + else emptySet + fromCap(info, initHidden) +end Fresh + + + + + 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/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index bd26f8c75523..a7f6ed157a46 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -158,7 +158,7 @@ class PlainPrinter(_ctx: Context) extends Printer { else val core: Text = if !cs.isConst && cs.elems.isEmpty then "?" - else "{" ~ Text(cs.elems.toList.map(toTextCaptureRef), ", ") ~ "}" + else "{" ~ Text(cs.processElems(_.toList.map(toTextCaptureRef)), ", ") ~ "}" // ~ Str("?").provided(!cs.isConst) core ~ cs.optionalInfo @@ -202,14 +202,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) => @@ -290,11 +290,11 @@ class PlainPrinter(_ctx: Context) extends Printer { 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) } @@ -342,7 +342,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), ", ") } @@ -354,11 +354,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: @@ -407,7 +407,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) @@ -427,6 +427,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case ReadOnlyCapability(tp1) => toTextCaptureRef(tp1) ~ ".rd" case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" + case Fresh.Cap(hidden) => s"" 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/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/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/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) } + + + + + From a897283d21d84af493438b0c140ed8e78fd48e6e Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 15 Dec 2024 11:25:15 +0100 Subject: [PATCH 16/32] 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 3969eba06827c8df1c7bb6fa1a01f0e6e86c2aee Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 15 Dec 2024 13:23:41 +0100 Subject: [PATCH 17/32] Simplify CaptureSet.ofInfo --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index abb46c4b87c2..f5406b6beb91 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1149,16 +1149,14 @@ object CaptureSet: /** The capture set of the type underlying CaptureRef */ def ofInfo(ref: CaptureRef)(using Context): CaptureSet = ref match - case ref: (TermRef | TermParamRef) if ref.isMaxCapability => - if ref.isTrackableRef then ref.singletonCaptureSet - else CaptureSet.universal - case Fresh.Cap(_) => ref.singletonCaptureSet 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 _ => 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 = From cc84115c8cbb9c129b19dab5fecd91a7f3c97fe2 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 15 Dec 2024 20:14:02 +0100 Subject: [PATCH 18/32] WIP: Move from cap to Fresh.Cap --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 17 ++++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 14 +++- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- .../src/dotty/tools/dotc/cc/Existential.scala | 11 ++-- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 66 ++++++++++++++----- compiler/src/dotty/tools/dotc/cc/Setup.scala | 10 +-- .../src/dotty/tools/dotc/cc/Synthetics.scala | 2 +- .../tools/dotc/printing/PlainPrinter.scala | 8 +-- 9 files changed, 96 insertions(+), 36 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 17fcfd120fed..4343a71303e8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -519,7 +519,7 @@ extension (tp: Type) def apply(x: Boolean, t: Type) = x || variance > 0 && t.dealiasKeepAnnots.match - case t @ CapturingType(p, cs) if cs.containsRootCapability => + case t @ CapturingType(p, cs) if cs.containsCap => true case t @ AnnotatedType(parent, ann) => // Don't traverse annotations, which includes capture sets diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index f932636fa5f2..a9b9b08fcb65 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -78,10 +78,16 @@ trait CaptureRef extends TypeProxy, ValueType: case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot case _ => false + /** Is this reference the generic root capability `cap` ? */ + final def isCapOrFresh(using Context): Boolean = this match + case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot + case Fresh.Cap(_) => true + 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 + 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 @@ -156,6 +162,13 @@ trait CaptureRef extends TypeProxy, ValueType: (this eq y) || this.isCap + /* need to do black hole detection here + || this.match + case Fresh.Cap(hidden) => + hidden.elems.exists(_.subsumes(y)) + case _ => + false + */ || y.match case y: TermRef => y.prefix.match diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index f5406b6beb91..ceaf212c9398 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -84,10 +84,17 @@ sealed abstract class CaptureSet extends Showable: 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)) @@ -412,6 +419,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 @@ -601,7 +611,7 @@ object CaptureSet: this else if isUniversal || computingApprox then universal - else if containsRootCapability && isReadOnly then + else if containsCap && isReadOnly then shared else computingApprox = true @@ -1223,7 +1233,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..572390ba6b8c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -746,7 +746,7 @@ 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 + if core.derivesFromMutable then CaptureSet.fresh() else if core.derivesFromCapability then initCs ++ defn.universalCSImpliedByCapability else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 19800a12a05c..b98c138d49b4 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 @@ -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: TermRef 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 index efa95a136370..8e91d5d3a081 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -32,29 +32,56 @@ object Fresh: 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): AnnotatedType = - AnnotatedType(defn.captureRoot.termRef, Annot(CaptureSet.HiddenSet(initialHidden))) + + 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(initialHidden: Refs = emptySet)(using Context) extends BiTypeMap: + class FromCap(owner: Symbol)(using Context) extends BiTypeMap: thisMap => var change = false + 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.dealiasKeepAnnots match case t: CaptureRef if t.isCap => change = true - Cap(initialHidden) - case CapturingType(_, v: CaptureSet.Var) => - change = true - mapOver(t) + Cap(initHidden) + case t @ CapturingType(_, refs) => + change = refs.isInstanceOf[CaptureSet.Var] + val savedReach = reach + if t.isBoxed then reach = true + try mapOver(t) finally reach = savedReach case _ => mapOver(t) @@ -70,19 +97,28 @@ object Fresh: end FromCap /** Maps cap to fresh */ - def fromCap(tp: Type, initialHidden: Refs = emptySet)(using Context): Type = - val mapper = FromCap(initialHidden) - val mapped = mapper(tp) - if mapper.change then mapped else tp + def fromCap(tp: Type, owner: Symbol)(using Context): Type = + if ccConfig.useFresh then + val mapper = FromCap(owner) + val mapped = mapper(tp) + if mapper.change then mapped else tp + else + tp +/* + def fromCap(tp: CaptureRef, initialHidden: Refs)(using Context): CaptureRef = + fromCap(tp: Type, initialHidden).asInstanceOf[CaptureRef] def fromCap(tp: Type, initialHidden: CaptureRef)(using Context): Type = fromCap(tp, SimpleIdentitySet(initialHidden)) - def fromCap(info: Type, sym: Symbol)(using Context): Type = + def fromCap(info: Type, owner: Symbol)(using Context): CaptureRef = + val ref = sym.termRef val initHidden = - if sym.exists && sym.termRef.isTracked then SimpleIdentitySet(sym.termRef) - else emptySet - fromCap(info, initHidden) + if reach then + if ref.isTrackableRef then SimpleIdentitySet(ref.reach) else emptySet + else + if ref.isTracked then SimpleIdentitySet(ref) else emptySet + fromCap(info, initHidden)*/ end Fresh diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 0c6b12874288..ee5074ff4945 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 @@ -440,13 +440,13 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 && !sym.hasAnnotation(defn.UncheckedCapturesAnnot), // Under the sealed policy, we disallow root capabilities in the type of mutable - // variables, no need to box them here. + // variables, no need to box them here ) catch case ex: IllegalCaptureRef => capt.println(i"fail while transforming result type $tpt of $sym") @@ -491,7 +491,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 + transformTT(arg, NoSymbol, boxed = true) // type arguments in type applications are boxed case tree: TypeDef if tree.symbol.isClass => val sym = tree.symbol @@ -516,7 +516,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 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/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index a7f6ed157a46..9b1c422335ad 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -248,7 +248,7 @@ class PlainPrinter(_ctx: Context) extends Printer { toText(parent) else val refsText = - if refs.isUniversal && (refs.elems.size == 1 || !printDebug) + if refs.isUniversal && (refs.elems.size == 1 || !printDebug) //??? then rootSetText else toTextCaptureSet(refs) toTextCapturing(parent, refsText, boxText) @@ -282,9 +282,9 @@ 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)) From e0ce6943d736f3797bb378f1e69e40d90d45086f Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 16 Dec 2024 11:44:13 +0100 Subject: [PATCH 19/32] Print "" only under -Yprint-debug --- .../dotty/tools/dotc/printing/PlainPrinter.scala | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 9b1c422335ad..7e0d77b1c8e8 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -248,9 +248,13 @@ 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 || !printDebug then rootSetText + else toTextCaptureSet(refs) + else if !refs.elems.isEmpty && refs.elems.forall(_.isCapOrFresh) && !printDebug then + rootSetText + else + toTextCaptureSet(refs) toTextCapturing(parent, refsText, boxText) case tp @ RetainingType(parent, refs) => if Feature.ccEnabledSomewhere then @@ -421,13 +425,15 @@ 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) => s"" + case Fresh.Cap(hidden) => + if printDebug then s"" + else "cap" case tp => toText(tp) protected def isOmittablePrefix(sym: Symbol): Boolean = From 66ee7089659a270c5417442437054df1afb6b545 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 16 Dec 2024 11:53:49 +0100 Subject: [PATCH 20/32] Fix fresh handling and turn it on by default --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 3 ++- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 23 ++++++++++++------- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 16 +++---------- .../dotty/tools/dotc/cc/CheckCaptures.scala | 4 +++- .../src/dotty/tools/dotc/cc/Existential.scala | 4 ++-- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 17 +------------- tests/neg-custom-args/captures/eta.check | 2 +- .../neg-custom-args/captures/outer-var.check | 2 +- tests/neg-custom-args/captures/vars.check | 7 +++--- .../captures/nested-classes-2.scala | 18 ++------------- 10 files changed, 34 insertions(+), 62 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 4343a71303e8..e769d248093f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -47,7 +47,8 @@ object ccConfig: def useSealed(using Context) = Feature.sourceVersion.stable != SourceVersion.`3.5` - val useFresh: Boolean = false + def useFresh(using Context): Boolean = + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.6`) end ccConfig diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index a9b9b08fcb65..878642cc8608 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -78,12 +78,14 @@ trait CaptureRef extends TypeProxy, ValueType: case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot case _ => false - /** Is this reference the generic root capability `cap` ? */ - final def isCapOrFresh(using Context): Boolean = this match - case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot + /** Is this reference a Fresh.Cap instance? */ + final def isFresh(using Context): Boolean = this match case Fresh.Cap(_) => true case _ => false + /** Is this reference the generic root capability `cap` or a Fresh.Cap instance? */ + final def isCapOrFresh(using Context): Boolean = isCap || isFresh + /** Is this reference one the generic root capabilities `cap` or `cap.rd` ? */ final def isRootCapability(using Context): Boolean = this match case ReadOnlyCapability(tp1) => tp1.isCapOrFresh @@ -132,6 +134,8 @@ trait CaptureRef extends TypeProxy, ValueType: final def invalidateCaches() = myCaptureSetRunId = NoRunId + import CaptureSet.{VarState, FrozenSepState} + /** x subsumes x * x =:= y ==> x subsumes y * x subsumes y ==> x subsumes y.f @@ -144,9 +148,9 @@ trait CaptureRef extends TypeProxy, ValueType: * Y: CapSet^c1...CapSet^c2, x subsumes (CapSet^c2) ==> x subsumes Y * Contains[X, y] ==> X subsumes y * - * TODO: Document cases with more comments. + * TODO: Move to CaptureSet */ - final def subsumes(y: CaptureRef)(using Context): Boolean = + final def subsumes(y: CaptureRef)(using ctx: Context, vs: VarState = FrozenSepState): Boolean = def subsumingRefs(x: Type, y: Type): Boolean = x match case x: CaptureRef => y match @@ -162,13 +166,16 @@ trait CaptureRef extends TypeProxy, ValueType: (this eq y) || this.isCap - /* need to do black hole detection here || this.match case Fresh.Cap(hidden) => - hidden.elems.exists(_.subsumes(y)) + if hidden.elems.contains(y) then true + //if hidden.elems.exists(_.subsumes(y)) then true + else if !hidden.recordElemsState() || y.stripReadOnly.isCap then false + else + hidden.elems += y + true case _ => false - */ || y.match case y: TermRef => y.prefix.match diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index ceaf212c9398..310cb135e320 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -185,19 +185,10 @@ sealed abstract class CaptureSet extends Showable: case elem @ Fresh.Cap(_) => p(elem) case elem => false - def subsumesX(elem: CaptureRef) = elem match - case Fresh.Cap(hidden) => - if (elem eq x) || hidden.elems.exists(_.subsumes(x)) then true - else if !hidden.recordElemsState() || x.stripReadOnly.isCap then false - else - hidden.elems += x - true - case _ => elem.subsumes(x) - def debugInfo(using Context) = i"$this accountsFor $x, which has capture set ${x.captureSetOfInfo}" def test(using Context) = reporting.trace(debugInfo): - existsElem(elems, subsumesX(_)) + existsElem(elems, _.subsumes(x)) || !x.isMaxCapability && !x.derivesFrom(defn.Caps_CapSet) && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK @@ -215,14 +206,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, FrozenState())) || !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 diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 572390ba6b8c..e104a52fd774 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -650,7 +650,9 @@ 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) + val freshenedFormal = Fresh.fromCap(formal) + val argType = recheck(arg, freshenedFormal) + .showing(i"recheck arg $arg vs $freshenedFormal", capt) formal match case AnnotatedType(formal1, ann) if ann.symbol == defn.UseAnnot => // The UseAnnot is added to `formal` by `prepareFunction` diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index b98c138d49b4..39f6fcf14fd9 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -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.isCapOrFresh => // !!! we should map different fresh refs to different existentials + case t: CaptureRef if t.isCapOrFresh => // !!! we should map different fresh refs to different existentials if variance > 0 then needsWrap = true boundVar diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index 8e91d5d3a081..796d596ef9f2 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -97,28 +97,13 @@ object Fresh: end FromCap /** Maps cap to fresh */ - def fromCap(tp: Type, owner: Symbol)(using Context): Type = + def fromCap(tp: Type, owner: Symbol = NoSymbol)(using Context): Type = if ccConfig.useFresh then val mapper = FromCap(owner) val mapped = mapper(tp) if mapper.change then mapped else tp else tp -/* - def fromCap(tp: CaptureRef, initialHidden: Refs)(using Context): CaptureRef = - fromCap(tp: Type, initialHidden).asInstanceOf[CaptureRef] - - def fromCap(tp: Type, initialHidden: CaptureRef)(using Context): Type = - fromCap(tp, SimpleIdentitySet(initialHidden)) - - def fromCap(info: Type, owner: Symbol)(using Context): CaptureRef = - val ref = sym.termRef - val initHidden = - if reach then - if ref.isTrackableRef then SimpleIdentitySet(ref.reach) else emptySet - else - if ref.isTracked then SimpleIdentitySet(ref) else emptySet - fromCap(info, initHidden)*/ end Fresh diff --git a/tests/neg-custom-args/captures/eta.check b/tests/neg-custom-args/captures/eta.check index b7669e9b68ea..b96788403e7a 100644 --- a/tests/neg-custom-args/captures/eta.check +++ b/tests/neg-custom-args/captures/eta.check @@ -8,5 +8,5 @@ -- Error: tests/neg-custom-args/captures/eta.scala:6:20 ---------------------------------------------------------------- 6 | bar( () => f ) // error | ^ - | reference (f : Proc^) is not included in the allowed capture set {} + | reference (f : () => Unit) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> box () ->? Unit diff --git a/tests/neg-custom-args/captures/outer-var.check b/tests/neg-custom-args/captures/outer-var.check index 72af842728a1..2511a78e70c1 100644 --- a/tests/neg-custom-args/captures/outer-var.check +++ b/tests/neg-custom-args/captures/outer-var.check @@ -32,7 +32,7 @@ | Required: () ->{p} Unit | | Note that reference (q : Proc), 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 --------------------------------------------------------- diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index e4b1e71a2000..d692dc4dbf9a 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 : Cap) is not included in the allowed capture set {cap1} of variable a + | reference (cap3 : Cap) is not included in the allowed capture set {cap1} + | of an enclosing function literal with expected type (x$0: String) ->{cap1} String | | Note that reference (cap3 : Cap), defined in method scope - | cannot be included in outer capture set {cap1} of variable a + | 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 : Cap), 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/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) From b242c96dfea9ce449d3d9d4e386868f6474b7192 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 16 Dec 2024 13:28:23 +0100 Subject: [PATCH 21/32] Improve subsumes test Don't add elements to hidden sets if they were subsumed previously. --- compiler/src/dotty/tools/dotc/cc/CaptureRef.scala | 3 +-- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 8 ++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 878642cc8608..00bb4f442401 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -168,8 +168,7 @@ trait CaptureRef extends TypeProxy, ValueType: || this.isCap || this.match case Fresh.Cap(hidden) => - if hidden.elems.contains(y) then true - //if hidden.elems.exists(_.subsumes(y)) then true + if vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) then true else if !hidden.recordElemsState() || y.stripReadOnly.isCap then false else hidden.elems += y diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 310cb135e320..2522eef476c9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1070,6 +1070,14 @@ object CaptureSet: def rollBack(): Unit = elemsMap.keysIterator.foreach(_.resetElems()(using this)) depsMap.keysIterator.foreach(_.resetDeps()(using this)) + + private var seen: util.EqHashSet[CaptureRef] = new util.EqHashSet + + /** Run test `pred` unless `ref` was seen in an enclosing `ifNotSeen` operation */ + def ifNotSeen(ref: CaptureRef)(pred: => Boolean): Boolean = + if seen.add(ref) then + try pred finally seen -= ref + else false end VarState /** A special state that does not allow to record elements or dependent sets. From ec26caa82072f346c35419fcb400529b09becee0 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 16 Dec 2024 18:26:16 +0100 Subject: [PATCH 22/32] Revert TastyFormat changes and fix Mima filters --- project/MiMaFilters.scala | 2 ++ tasty/src/dotty/tools/tasty/TastyFormat.scala | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 00e7153bcb83..4723fd745d6a 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -13,6 +13,8 @@ object MiMaFilters { ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.quotedPatternsWithPolymorphicFunctions"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$quotedPatternsWithPolymorphicFunctions$"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.runtime.Patterns.higherOrderHoleWithTypes"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.freshCapability"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.readOnlyCapability"), ), // Additions since last LTS diff --git a/tasty/src/dotty/tools/tasty/TastyFormat.scala b/tasty/src/dotty/tools/tasty/TastyFormat.scala index 7165f342f52f..8ff590fefec5 100644 --- a/tasty/src/dotty/tools/tasty/TastyFormat.scala +++ b/tasty/src/dotty/tools/tasty/TastyFormat.scala @@ -229,7 +229,6 @@ Standard-Section: "ASTs" TopLevelStat* OPEN -- an open class INVISIBLE -- invisible during typechecking TRACKED -- a tracked class parameter / a dependent class - MUT -- an update method (cc only) Annotation Variance = STABLE -- invariant @@ -512,7 +511,6 @@ object TastyFormat { final val EMPTYCLAUSE = 45 final val SPLITCLAUSE = 46 final val TRACKED = 47 - final val MUT = 48 // Tree Cat. 2: tag Nat final val firstNatTreeTag = SHAREDterm @@ -705,8 +703,7 @@ object TastyFormat { | ANNOTATION | PRIVATEqualified | PROTECTEDqualified - | TRACKED - | MUT => true + | TRACKED => true case _ => false } From 27f2a1a224fa5ae0d75deef31394d05aa571b5a7 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 18 Dec 2024 19:03:14 +0100 Subject: [PATCH 23/32] Refactor handling of rechecked types - Always store new types on rechecking - Store them in a hashmap which is associated with the rechecker of the current compilation unit - After rechecking is done, the map is forgotten, unless keepTypes is true. Under keepTypes, then map is kept in an attachment of the unit's root tree. Change in nomenclature: knownType --> nuType rememberType --> setNuType hasRememberedType --> hasNuType --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index ee5074ff4945..384222010b0f 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -428,13 +428,14 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 def transformTT(tree: TypeTree, sym: Symbol, boxed: Boolean)(using Context): Unit = if !tree.hasNuType then val transformed = if tree.isInferred then transformInferredType(tree.tpe) else transformExplicitType(tree.tpe, tptToCheck = tree) - tree.setNuType(if boxed then box(transformed) else transformed) + tree.setNuType( + if boxed then box(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 = From e6ceff979c8dedeb3c8bac17bf99e834dc0a5823 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 20 Dec 2024 00:05:00 +0100 Subject: [PATCH 24/32] Fix: Use cap instead of Fresh.Cap for parameter types of methods --- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 14 +++++++++++++- compiler/src/dotty/tools/dotc/cc/Setup.scala | 18 +++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index 796d596ef9f2..04269fa3ab60 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -89,7 +89,9 @@ object Fresh: lazy val inverse = new BiTypeMap: def apply(t: Type): Type = t match - case t @ Cap(_) => defn.captureRoot.termRef + case t @ Cap(_) => + change = true + defn.captureRoot.termRef case _ => mapOver(t) def inverse = thisMap override def toString = thisMap.toString + ".inverse" @@ -104,6 +106,16 @@ object Fresh: if mapper.change then mapped else tp else tp + + /** Maps fresh to cap */ + def toCap(tp: Type)(using Context): Type = + if ccConfig.useFresh then + val fromCap = FromCap(NoSymbol) + val mapper = fromCap.inverse + val mapped = mapper(tp) + if fromCap.change then mapped else tp + else + tp end Fresh diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 384222010b0f..445d327d82c1 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -427,15 +427,20 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def setupTraverser(checker: CheckerAPI) = new TreeTraverserWithPreciseImportContexts: import checker.* + private val paramSigChange = util.EqHashSet[Tree]() + /** Transform type of tree, and remember the transformed type as the type the tree */ 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) + if boxed then transformed = box(transformed) + if sym.is(Param) && (transformed ne tree.tpe) then + paramSigChange += tree tree.setNuType( - if boxed then box(transformed) else Fresh.fromCap(transformed, sym)) + if boxed then 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 = @@ -501,6 +506,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))) @@ -544,8 +552,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 +588,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) ) From 41f27608948ea17f3fe5d69968a92a1d469ae2d4 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 20 Dec 2024 00:07:04 +0100 Subject: [PATCH 25/32] Fix: Skip existentials in when accessing result type in augmentConstructorType --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index e104a52fd774..e52b5b6a9329 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -23,6 +23,7 @@ import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} import reporting.{trace, Message, OverrideError} +import Existential.derivedExistentialType /** The capture checker */ object CheckCaptures: @@ -770,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) From 0e6da1a7ee625ac807c44ca1ff3d952f7e6b7b21 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 21 Dec 2024 11:57:09 +0100 Subject: [PATCH 26/32] Turn on separation checking for applications - Use unsafeAssumeSeparate(...) as an escape hatch --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 20 ++- .../src/dotty/tools/dotc/cc/SepCheck.scala | 116 ++++++++++++++++++ .../dotty/tools/dotc/core/Definitions.scala | 1 + .../src/dotty/tools/dotc/core/Types.scala | 2 +- .../dotty/tools/dotc/transform/Recheck.scala | 6 +- library/src/scala/caps.scala | 5 + .../src/scala/collection/IndexedSeqView.scala | 2 +- .../immutable/LazyListIterable.scala | 13 +- .../mutable/CheckedIndexedSeqView.scala | 2 +- .../captures/cc-dep-param.check | 5 + .../captures/cc-dep-param.scala | 8 ++ .../captures/cc-subst-param-exact.scala | 12 +- .../captures/filevar-expanded.check | 9 ++ .../captures/filevar-expanded.scala | 2 +- .../captures/function-combinators.check | 5 + .../captures/function-combinators.scala | 30 +++++ tests/neg-custom-args/captures/lazyref.check | 5 + tests/neg-custom-args/captures/lazyref.scala | 2 +- .../captures/sepchecks.scala} | 21 +++- .../captures/boxmap-paper.scala | 5 +- .../captures/cc-dep-param.scala | 3 +- .../captures/function-combinators.scala | 3 +- tests/pos-special/stdlib/Test2.scala | 9 +- tests/pos/boxmap-paper.scala | 38 ------ .../captures/colltest5/Test_2.scala | 5 +- 25 files changed, 259 insertions(+), 70 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/SepCheck.scala create mode 100644 tests/neg-custom-args/captures/cc-dep-param.check create mode 100644 tests/neg-custom-args/captures/cc-dep-param.scala create mode 100644 tests/neg-custom-args/captures/filevar-expanded.check rename tests/{pos-custom-args => neg-custom-args}/captures/filevar-expanded.scala (94%) create mode 100644 tests/neg-custom-args/captures/function-combinators.check create mode 100644 tests/neg-custom-args/captures/function-combinators.scala rename tests/{pos-custom-args/captures/readOnly.scala => neg-custom-args/captures/sepchecks.scala} (68%) delete mode 100644 tests/pos/boxmap-paper.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index e52b5b6a9329..b677b8d1b504 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -239,6 +239,12 @@ 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 end CheckerAPI class CheckCaptures extends Recheck, SymTransformer: @@ -279,6 +285,12 @@ class CheckCaptures extends Recheck, SymTransformer: */ private val todoAtPostCheck = new mutable.ListBuffer[() => Unit] + /** Trees that will need a separation check because they contain cap */ + private val sepCheckable = util.EqHashSet[Tree]() + + extension [T <: Tree](tree: T) + def needsSepCheck: Boolean = sepCheckable.contains(tree) + /** Instantiate capture set variables appearing contra-variantly to their * upper approximation. */ @@ -636,11 +648,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) @@ -660,6 +672,9 @@ class CheckCaptures extends Recheck, SymTransformer: capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") markFree(argType.deepCaptureSet, arg.srcPos) case _ => + if formal.containsCap then + arg.updNuType(freshenedFormal) + sepCheckable += arg argType /** Map existential captures in result to `cap` and implement the following @@ -1785,6 +1800,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..7f69e11d5d4e --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -0,0 +1,116 @@ +package dotty.tools +package dotc +package cc +import ast.tpd +import collection.mutable + +import core.* +import Symbols.*, Types.* +import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* +import CaptureSet.{Refs, emptySet} +import config.Printers.capt +import StdNames.nme + +class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: + import tpd.* + import checker.* + + extension (cs: CaptureSet) + def footprint(using Context): CaptureSet = + def recur(elems: CaptureSet.Refs, newElems: List[CaptureRef]): CaptureSet.Refs = newElems match + case newElem :: newElems1 => + val superElems = newElem.captureSetOfInfo.elems.filter: superElem => + !superElem.isMaxCapability && !elems.contains(superElem) + recur(superElems ++ elems, superElems.toList ++ newElems1) + case Nil => elems + val elems: CaptureSet.Refs = cs.elems.filter(!_.isMaxCapability) + CaptureSet(recur(elems, elems.toList)) + + def overlapWith(other: CaptureSet)(using Context): CaptureSet.Refs = + val refs1 = cs.elems + val refs2 = other.elems + def common(refs1: CaptureSet.Refs, refs2: CaptureSet.Refs) = + refs1.filter: ref => + ref.isExclusive && refs2.exists(_.stripReadOnly eq ref) + common(refs1, refs2) ++ common(refs2, refs1) + + private def hidden(elem: CaptureRef)(using Context): CaptureSet.Refs = elem match + case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ hidden(hcs) + case ReadOnlyCapability(ref) => hidden(ref).map(_.readOnly) + case _ => emptySet + + private def hidden(cs: CaptureSet)(using Context): CaptureSet.Refs = + val seen: util.EqHashSet[CaptureRef] = new util.EqHashSet + + def hiddenByElem(elem: CaptureRef): CaptureSet.Refs = + if seen.add(elem) then elem match + case Fresh.Cap(hcs) => hcs.elems.filter(!_.isRootCapability) ++ recur(hcs) + case ReadOnlyCapability(ref) => hiddenByElem(ref).map(_.readOnly) + case _ => emptySet + else emptySet + + def recur(cs: CaptureSet): CaptureSet.Refs = + (emptySet /: cs.elems): (elems, elem) => + elems ++ hiddenByElem(elem) + + recur(cs) + end hidden + + private def checkApply(fn: Tree, args: List[Tree])(using Context): Unit = + val fnCaptures = fn.nuType.deepCaptureSet + + def captures(arg: Tree) = + val argType = arg.nuType + argType match + case AnnotatedType(formal1, ann) if ann.symbol == defn.UseAnnot => + argType.deepCaptureSet + case _ => + argType.captureSet + + val argCaptures = args.map(captures) + capt.println(i"check separate $fn($args), fnCaptures = $fnCaptures, argCaptures = $argCaptures") + var footprint = argCaptures.foldLeft(fnCaptures.footprint): (fp, ac) => + fp ++ ac.footprint + val paramNames = fn.nuType.widen match + case MethodType(pnames) => pnames + case _ => args.indices.map(nme.syntheticParamName(_)) + for (arg, ac, pname) <- args.lazyZip(argCaptures).lazyZip(paramNames) do + if arg.needsSepCheck then + val hiddenInArg = CaptureSet(hidden(ac)) + //println(i"check sep $arg / $footprint / $hiddenInArg") + val overlap = hiddenInArg.footprint.overlapWith(footprint) + if !overlap.isEmpty then + def whatStr = if overlap.size == 1 then "this capability" else "these capabilities" + def funStr = + if fn.symbol.exists then i"${fn.symbol}" + else "the function" + report.error( + em"""Separation failure: argument to capture-polymorphic parameter $pname: ${arg.nuType} + |captures ${CaptureSet(overlap)} and also passes $whatStr separately to $funStr""", + arg.srcPos) + footprint ++= hiddenInArg + + private def traverseApply(tree: Tree, argss: List[List[Tree]])(using Context): Unit = tree match + case Apply(fn, args) => traverseApply(fn, args :: argss) + case TypeApply(fn, args) => traverseApply(fn, argss) // skip type arguments + case _ => + if argss.nestedExists(_.needsSepCheck) then + checkApply(tree, argss.flatten) + + def traverse(tree: Tree)(using Context): Unit = + tree match + case tree: GenericApply => + if tree.symbol != defn.Caps_unsafeAssumeSeparate then + tree.tpe match + case _: MethodOrPoly => + case _ => traverseApply(tree, Nil) + traverseChildren(tree) + case _ => + traverseChildren(tree) +end SepChecker + + + + + + diff --git a/compiler/src/dotty/tools/dotc/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/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/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 172ae337d6e6..22f83aa785de 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/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/IndexedSeqView.scala b/scala2-library-cc/src/scala/collection/IndexedSeqView.scala index 78f8abb8e327..cf23b0440869 100644 --- a/scala2-library-cc/src/scala/collection/IndexedSeqView.scala +++ b/scala2-library-cc/src/scala/collection/IndexedSeqView.scala @@ -136,7 +136,7 @@ object IndexedSeqView { @SerialVersionUID(3L) class Concat[A](prefix: SomeIndexedSeqOps[A]^, suffix: SomeIndexedSeqOps[A]^) - extends SeqView.Concat[A](prefix, suffix) with IndexedSeqView[A] + extends SeqView.Concat[A](prefix, caps.unsafe.unsafeAssumeSeparate(suffix)) with IndexedSeqView[A] @SerialVersionUID(3L) class Take[A](underlying: SomeIndexedSeqOps[A]^, n: Int) diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index 28ce8da104aa..2d04855cc227 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -682,7 +682,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz remaining -= 1 scout = scout.tail } - dropRightState(scout) + caps.unsafe.unsafeAssumeSeparate(dropRightState(scout)) } } @@ -879,6 +879,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 +891,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz // the start of the loop. var runner = this var k = 0 - while (!same(runner, scout)) { + while (!caps.unsafe.unsafeAssumeSeparate(same(runner, scout))) { runner = runner.tail scout = scout.tail k += 1 @@ -900,11 +901,11 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz // everything once. If cursor is already at beginning, we'd better // advance one first unless runner didn't go anywhere (in which case // we've already looped once). - if (same(cursor, scout) && (k > 0)) { + if (caps.unsafe.unsafeAssumeSeparate(same(cursor, scout)) && (k > 0)) { appendCursorElement() cursor = cursor.tail } - while (!same(cursor, scout)) { + while (!caps.unsafe.unsafeAssumeSeparate(same(cursor, scout))) { appendCursorElement() cursor = cursor.tail } @@ -1052,7 +1053,9 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { val head = it.next() rest = rest.tail restRef = rest // restRef.elem = rest - sCons(head, newLL(stateFromIteratorConcatSuffix(it)(flatMapImpl(rest, f).state))) + sCons(head, newLL( + caps.unsafe.unsafeAssumeSeparate( + stateFromIteratorConcatSuffix(it)(flatMapImpl(rest, f).state)))) } else State.Empty } } diff --git a/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala b/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala index 9ce0399e0662..fb120ac2a930 100644 --- a/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala +++ b/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala @@ -75,7 +75,7 @@ private[mutable] object CheckedIndexedSeqView { @SerialVersionUID(3L) class Concat[A](prefix: SomeIndexedSeqOps[A]^, suffix: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int) - extends IndexedSeqView.Concat[A](prefix, suffix) with CheckedIndexedSeqView[A] + extends IndexedSeqView.Concat[A](prefix, caps.unsafe.unsafeAssumeSeparate(suffix)) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) class Take[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int) diff --git a/tests/neg-custom-args/captures/cc-dep-param.check b/tests/neg-custom-args/captures/cc-dep-param.check new file mode 100644 index 000000000000..bca48d3758f4 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-dep-param.check @@ -0,0 +1,5 @@ +-- Error: tests/neg-custom-args/captures/cc-dep-param.scala:8:6 -------------------------------------------------------- +8 | foo(a, useA) // error: separation failure + | ^ + | Separation failure: argument to capture-polymorphic parameter x$0: Foo[Int]^ + | captures {a} and also passes this capability separately to method foo diff --git a/tests/neg-custom-args/captures/cc-dep-param.scala b/tests/neg-custom-args/captures/cc-dep-param.scala new file mode 100644 index 000000000000..5fcb722a1a6c --- /dev/null +++ b/tests/neg-custom-args/captures/cc-dep-param.scala @@ -0,0 +1,8 @@ +import language.experimental.captureChecking + +trait Foo[T] +def test(): Unit = + val a: Foo[Int]^ = ??? + val useA: () ->{a} Unit = ??? + def foo[X](x: Foo[X]^, op: () ->{x} Unit): Unit = ??? + foo(a, useA) // error: separation failure diff --git a/tests/neg-custom-args/captures/cc-subst-param-exact.scala b/tests/neg-custom-args/captures/cc-subst-param-exact.scala index 35e4acb95fdc..2482bb18c727 100644 --- a/tests/neg-custom-args/captures/cc-subst-param-exact.scala +++ b/tests/neg-custom-args/captures/cc-subst-param-exact.scala @@ -5,13 +5,13 @@ trait Ref[T] { def set(x: T): T } def test() = { def swap[T](x: Ref[T]^)(y: Ref[T]^{x}): Unit = ??? - def foo[T](x: Ref[T]^): Unit = + def foo[T](x: Ref[T]^{cap.rd}): Unit = swap(x)(x) - def bar[T](x: () => Ref[T]^)(y: Ref[T]^{x}): Unit = + def bar[T](x: () => Ref[T]^{cap.rd})(y: Ref[T]^{x}): Unit = swap(x())(y) // error - def baz[T](x: Ref[T]^)(y: Ref[T]^{x}): Unit = + def baz[T](x: Ref[T]^{cap.rd})(y: Ref[T]^{x}): Unit = swap(x)(y) } @@ -19,15 +19,15 @@ trait IO type Op = () -> Unit def test2(c: IO^, f: Op^{c}) = { def run(io: IO^)(op: Op^{io}): Unit = op() - run(c)(f) + run(c)(f) // error: separation failure def bad(getIO: () => IO^, g: Op^{getIO}): Unit = - run(getIO())(g) // error + run(getIO())(g) // error // error: separation failure } def test3() = { def run(io: IO^)(op: Op^{io}): Unit = ??? val myIO: IO^ = ??? val myOp: Op^{myIO} = ??? - run(myIO)(myOp) + run(myIO)(myOp) // error: separation failure } diff --git a/tests/neg-custom-args/captures/filevar-expanded.check b/tests/neg-custom-args/captures/filevar-expanded.check new file mode 100644 index 000000000000..cdac5d6b26ac --- /dev/null +++ b/tests/neg-custom-args/captures/filevar-expanded.check @@ -0,0 +1,9 @@ +-- Error: tests/neg-custom-args/captures/filevar-expanded.scala:34:19 -------------------------------------------------- +34 | withFile(io3): f => // error: separation failure + | ^ + | Separation failure: argument to capture-polymorphic parameter x$1: (f: test2.File^{io3}) => Unit + | captures {io3} and also passes this capability separately to method withFile +35 | val o = Service(io3) +36 | o.file = f // this is a bit dubious. It's legal since we treat class refinements +37 | // as capture set variables that can be made to include refs coming from outside. +38 | o.log diff --git a/tests/pos-custom-args/captures/filevar-expanded.scala b/tests/neg-custom-args/captures/filevar-expanded.scala similarity index 94% rename from tests/pos-custom-args/captures/filevar-expanded.scala rename to tests/neg-custom-args/captures/filevar-expanded.scala index 58e7a0e67e0a..461a617bde0d 100644 --- a/tests/pos-custom-args/captures/filevar-expanded.scala +++ b/tests/neg-custom-args/captures/filevar-expanded.scala @@ -31,7 +31,7 @@ object test2: op(new File) def test(io3: IO^) = - withFile(io3): f => + withFile(io3): f => // error: separation failure val o = Service(io3) o.file = f // this is a bit dubious. It's legal since we treat class refinements // as capture set variables that can be made to include refs coming from outside. diff --git a/tests/neg-custom-args/captures/function-combinators.check b/tests/neg-custom-args/captures/function-combinators.check new file mode 100644 index 000000000000..05d0ee6854a4 --- /dev/null +++ b/tests/neg-custom-args/captures/function-combinators.check @@ -0,0 +1,5 @@ +-- Error: tests/neg-custom-args/captures/function-combinators.scala:15:22 ---------------------------------------------- +15 | val b2 = g1.andThen(g1); // error: separation failure + | ^^ + | Separation failure: argument to capture-polymorphic parameter x$0: Int => Int + | captures {ctx1} and also passes this capability separately to method andThen diff --git a/tests/neg-custom-args/captures/function-combinators.scala b/tests/neg-custom-args/captures/function-combinators.scala new file mode 100644 index 000000000000..bda77013a7a5 --- /dev/null +++ b/tests/neg-custom-args/captures/function-combinators.scala @@ -0,0 +1,30 @@ +class ContextClass +type Context = ContextClass^ +import caps.unsafe.unsafeAssumePure + +def Test(using ctx1: Context, ctx2: Context) = + val f: Int => Int = identity + val g1: Int ->{ctx1} Int = identity + val g2: Int ->{ctx2} Int = identity + val h: Int -> Int = identity + val a1 = f.andThen(f); val _: Int ->{f} Int = a1 + val a2 = f.andThen(g1); val _: Int ->{f, g1} Int = a2 + val a3 = f.andThen(g2); val _: Int ->{f, g2} Int = a3 + val a4 = f.andThen(h); val _: Int ->{f} Int = a4 + val b1 = g1.andThen(f); val _: Int ->{f, g1} Int = b1 + val b2 = g1.andThen(g1); // error: separation failure + val _: Int ->{g1} Int = b2 + val b3 = g1.andThen(g2); val _: Int ->{g1, g2} Int = b3 + val b4 = g1.andThen(h); val _: Int ->{g1} Int = b4 + val c1 = h.andThen(f); val _: Int ->{f} Int = c1 + val c2 = h.andThen(g1); val _: Int ->{g1} Int = c2 + val c3 = h.andThen(g2); val _: Int ->{g2} Int = c3 + val c4 = h.andThen(h); val _: Int -> Int = c4 + + val f2: (Int, Int) => Int = _ + _ + val f2c = f2.curried; val _: Int -> Int ->{f2} Int = f2c + val f2t = f2.tupled; val _: ((Int, Int)) ->{f2} Int = f2t + + val f3: (Int, Int, Int) => Int = ??? + val f3c = f3.curried; val _: Int -> Int -> Int ->{f3} Int = f3c + val f3t = f3.tupled; val _: ((Int, Int, Int)) ->{f3} Int = f3t diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index 8683615c07d8..0e2eea582b50 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -26,3 +26,8 @@ | Required: LazyRef[Int]^{cap1} | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/lazyref.scala:24:55 ----------------------------------------------------------- +24 | val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) // error: separation failure + | ^ + | Separation failure: argument to capture-polymorphic parameter x$0: Int => Int + | captures {cap2} and also passes this capability separately to method map diff --git a/tests/neg-custom-args/captures/lazyref.scala b/tests/neg-custom-args/captures/lazyref.scala index 99aa10d5d2b2..9772b10abf50 100644 --- a/tests/neg-custom-args/captures/lazyref.scala +++ b/tests/neg-custom-args/captures/lazyref.scala @@ -21,5 +21,5 @@ def test(cap1: Cap, cap2: Cap) = val ref2c: LazyRef[Int]^{cap2} = ref2 // error val ref3 = ref1.map(g) val ref3c: LazyRef[Int]^{ref1} = ref3 // error - val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) + val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) // error: separation failure val ref4c: LazyRef[Int]^{cap1} = ref4 // error diff --git a/tests/pos-custom-args/captures/readOnly.scala b/tests/neg-custom-args/captures/sepchecks.scala similarity index 68% rename from tests/pos-custom-args/captures/readOnly.scala rename to tests/neg-custom-args/captures/sepchecks.scala index a550010360a3..67e4cadaf992 100644 --- a/tests/pos-custom-args/captures/readOnly.scala +++ b/tests/neg-custom-args/captures/sepchecks.scala @@ -9,7 +9,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 +29,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 +40,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 \ No newline at end of file diff --git a/tests/pos-custom-args/captures/boxmap-paper.scala b/tests/pos-custom-args/captures/boxmap-paper.scala index 9d5bb49af25d..aace9e538642 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[+T] = [K] -> (T => K) -> K @@ -12,13 +13,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/function-combinators.scala b/tests/pos-custom-args/captures/function-combinators.scala index 4354af4c7636..e7250388f738 100644 --- a/tests/pos-custom-args/captures/function-combinators.scala +++ b/tests/pos-custom-args/captures/function-combinators.scala @@ -1,5 +1,6 @@ class ContextClass type Context = ContextClass^ +import caps.unsafe.unsafeAssumeSeparate def Test(using ctx1: Context, ctx2: Context) = val f: Int => Int = identity @@ -11,7 +12,7 @@ def Test(using ctx1: Context, ctx2: Context) = val a3 = f.andThen(g2); val _: Int ->{f, g2} Int = a3 val a4 = f.andThen(h); val _: Int ->{f} Int = a4 val b1 = g1.andThen(f); val _: Int ->{f, g1} Int = b1 - val b2 = g1.andThen(g1); val _: Int ->{g1} Int = b2 + val b2 = unsafeAssumeSeparate(g1.andThen(g1)); val _: Int ->{g1} Int = b2 val b3 = g1.andThen(g2); val _: Int ->{g1, g2} Int = b3 val b4 = g1.andThen(h); val _: Int ->{g1} Int = b4 val c1 = h.andThen(f); val _: Int ->{f} Int = c1 diff --git a/tests/pos-special/stdlib/Test2.scala b/tests/pos-special/stdlib/Test2.scala index cab9440c17db..e0d9a1491516 100644 --- a/tests/pos-special/stdlib/Test2.scala +++ b/tests/pos-special/stdlib/Test2.scala @@ -2,6 +2,7 @@ import scala.reflect.ClassTag import language.experimental.captureChecking import collection.{View, Seq} import collection.mutable.{ArrayBuffer, ListBuffer} +import caps.unsafe.unsafeAssumeSeparate object Test { @@ -87,7 +88,7 @@ object Test { val ys9: Iterator[Boolean]^{xs9} = xs9 val xs10 = xs.flatMap(flips) val ys10: Iterator[Int]^{xs10} = xs10 - val xs11 = xs ++ xs + val xs11 = unsafeAssumeSeparate(xs ++ xs) val ys11: Iterator[Int]^{xs11} = xs11 val xs12 = xs ++ Nil val ys12: Iterator[Int]^{xs12} = xs12 @@ -95,7 +96,7 @@ object Test { val ys13: List[Int] = xs13 val xs14 = xs ++ ("a" :: Nil) val ys14: Iterator[Any]^{xs14} = xs14 - val xs15 = xs.zip(xs9) + val xs15 = unsafeAssumeSeparate(xs.zip(xs9)) val ys15: Iterator[(Int, Boolean)]^{xs15} = xs15 println("-------") println(x1) @@ -141,7 +142,7 @@ object Test { val ys9: View[Boolean]^{xs9} = xs9 val xs10 = xs.flatMap(flips) val ys10: View[Int]^{xs10} = xs10 - val xs11 = xs ++ xs + val xs11 = unsafeAssumeSeparate(xs ++ xs) val ys11: View[Int]^{xs11} = xs11 val xs12 = xs ++ Nil val ys12: View[Int]^{xs12} = xs12 @@ -149,7 +150,7 @@ object Test { val ys13: List[Int] = xs13 val xs14 = xs ++ ("a" :: Nil) val ys14: View[Any]^{xs14} = xs14 - val xs15 = xs.zip(xs9) + val xs15 = unsafeAssumeSeparate(xs.zip(xs9)) val ys15: View[(Int, Boolean)]^{xs15} = xs15 println("-------") println(x1) diff --git a/tests/pos/boxmap-paper.scala b/tests/pos/boxmap-paper.scala deleted file mode 100644 index aa983114ed8a..000000000000 --- a/tests/pos/boxmap-paper.scala +++ /dev/null @@ -1,38 +0,0 @@ -import language.experimental.captureChecking - -type Cell[+T] = [K] -> (T => K) -> K - -def cell[T](x: T): Cell[T] = - [K] => (k: T => K) => k(x) - -def get[T](c: Cell[T]): T = c[T](identity) - -def map[A, B](c: Cell[A])(f: A => B): Cell[B] - = c[Cell[B]]((x: A) => cell(f(x))) - -def pureMap[A, B](c: Cell[A])(f: A -> B): Cell[B] - = c[Cell[B]]((x: A) => cell(f(x))) - -def lazyMap[A, B](c: Cell[A])(f: A => B): () ->{f} Cell[B] - = () => c[Cell[B]]((x: A) => cell(f(x))) - -trait IO: - def print(s: String): Unit - -def test(io: IO^) = - - val loggedOne: () ->{io} Int = () => { io.print("1"); 1 } - - val c: Cell[() ->{io} Int] - = cell[() ->{io} Int](loggedOne) - - val g = (f: () ->{io} Int) => - val x = f(); io.print(" + ") - val y = f(); io.print(s" = ${x + y}") - - val r = lazyMap[() ->{io} Int, Unit](c)(f => g(f)) - val r2 = lazyMap[() ->{io} Int, Unit](c)(g) - val r3 = lazyMap(c)(g) - val _ = r() - val _ = r2() - val _ = r3() diff --git a/tests/run-custom-args/captures/colltest5/Test_2.scala b/tests/run-custom-args/captures/colltest5/Test_2.scala index f6f47b536541..2bde8cb5a885 100644 --- a/tests/run-custom-args/captures/colltest5/Test_2.scala +++ b/tests/run-custom-args/captures/colltest5/Test_2.scala @@ -1,5 +1,6 @@ import Predef.{augmentString as _, wrapString as _, *} import scala.reflect.ClassTag +import caps.unsafe.unsafeAssumeSeparate object Test { import colltest5.strawman.collections.* @@ -89,7 +90,7 @@ object Test { val ys9: View[Boolean]^{xs9} = xs9 val xs10 = xs.flatMap(flips) val ys10: View[Int]^{xs10} = xs10 - val xs11 = xs ++ xs + val xs11 = unsafeAssumeSeparate(xs ++ xs) val ys11: View[Int]^{xs11} = xs11 val xs12 = xs ++ Nil val ys12: View[Int]^{xs12} = xs12 @@ -97,7 +98,7 @@ object Test { val ys13: List[Int] = xs13 val xs14 = xs ++ Cons("a", Nil) val ys14: View[Any]^{xs14} = xs14 - val xs15 = xs.zip(xs9) + val xs15 = unsafeAssumeSeparate(xs.zip(xs9)) val ys15: View[(Int, Boolean)]^{xs15} = xs15 println("-------") println(x1) From 2ad10de31f28da871ceb46eb27c63ceaed100c7f Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 21 Dec 2024 16:05:38 +0100 Subject: [PATCH 27/32] Refine FrozenState categorizations - In ++, use a FrozenSepState in order not to pollute hidden sets - This avoids two spurious separation failures in stdlib --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 3 +- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 4 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 62 ++++++++++++------- .../dotty/tools/dotc/cc/CheckCaptures.scala | 12 ++-- .../dotty/tools/dotc/core/TypeComparer.scala | 9 ++- .../src/dotty/tools/dotc/core/TypeOps.scala | 4 +- .../src/scala/collection/IndexedSeqView.scala | 2 +- .../mutable/CheckedIndexedSeqView.scala | 2 +- 8 files changed, 58 insertions(+), 40 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index e769d248093f..eb383cb8839d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -16,6 +16,7 @@ import config.Feature import collection.mutable import CCState.* import reporting.Message +import CaptureSet.Frozen private val Captures: Key[CaptureSet] = Key() @@ -244,7 +245,7 @@ extension (tp: Type) * the two capture sets are combined. */ def capturing(cs: CaptureSet)(using Context): Type = - if (cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, frozen = true).isOK) + if (cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, Frozen.All).isOK) && !cs.keepAlways then tp else tp match diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 00bb4f442401..025f02505f5b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -134,7 +134,7 @@ trait CaptureRef extends TypeProxy, ValueType: final def invalidateCaches() = myCaptureSetRunId = NoRunId - import CaptureSet.{VarState, FrozenSepState} + import CaptureSet.{VarState, FrozenAllState} /** x subsumes x * x =:= y ==> x subsumes y @@ -150,7 +150,7 @@ trait CaptureRef extends TypeProxy, ValueType: * * TODO: Move to CaptureSet */ - final def subsumes(y: CaptureRef)(using ctx: Context, vs: VarState = FrozenSepState): Boolean = + final def subsumes(y: CaptureRef)(using ctx: Context, vs: VarState = FrozenAllState): Boolean = def subsumingRefs(x: Type, y: Type): Boolean = x match case x: CaptureRef => y match diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 2522eef476c9..a6baca97c093 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -142,7 +142,7 @@ sealed abstract class CaptureSet extends Showable: * capture set. */ protected final def addNewElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if elem.isMaxCapability || summon[VarState].isInstanceOf[FrozenState] then + if elem.isMaxCapability || summon[VarState].isInstanceOf[FrozenVarState] then addThisElem(elem) else addThisElem(elem).orElse: @@ -173,7 +173,7 @@ sealed abstract class CaptureSet extends Showable: /** {x} <:< this where <:< is subcapturing, but treating all variables * as frozen. */ - def accountsFor(x: CaptureRef)(using ctx: Context, vs: VarState = FrozenSepState): Boolean = + def accountsFor(x: CaptureRef)(using ctx: Context, vs: VarState = FrozenAllState): Boolean = /** Like `refs.exists(p)`, but testing fresh cap instances in refs last */ def existsElem(refs: SimpleIdentitySet[CaptureRef], p: CaptureRef => Boolean): Boolean = @@ -191,7 +191,10 @@ sealed abstract class CaptureSet extends Showable: existsElem(elems, _.subsumes(x)) || !x.isMaxCapability && !x.derivesFrom(defn.Caps_CapSet) - && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK + && x.captureSetOfInfo.subCaptures(this)(using ctx, + vs match + case vs: FrozenVarState => vs + case _ => FrozenVarState()).isOK comparer match case comparer: ExplainingTypeComparer => comparer.traceIndented(debugInfo)(test) @@ -207,7 +210,7 @@ sealed abstract class CaptureSet extends Showable: */ def mightAccountFor(x: CaptureRef)(using Context): Boolean = reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true): - elems.exists(_.subsumes(x)(using ctx, FrozenState())) + elems.exists(_.subsumes(x)(using ctx, FrozenVarState())) || !x.isMaxCapability && { val elems = x.captureSetOfInfo.elems @@ -228,8 +231,12 @@ sealed abstract class CaptureSet extends Showable: * be added when making this test. An attempt to add either * will result in failure. */ - final def subCaptures(that: CaptureSet, frozen: Boolean)(using Context): CompareResult = - subCaptures(that)(using ctx, if frozen then FrozenState() else VarState()) + final def subCaptures(that: CaptureSet, frozen: Frozen)(using Context): CompareResult = + val state = frozen match + case Frozen.None => VarState() + case Frozen.Vars => FrozenVarState() + case Frozen.All => FrozenAllState + subCaptures(that)(using ctx, state) /** The subcapturing test, using a given VarState */ private def subCaptures(that: CaptureSet)(using Context, VarState): CompareResult = @@ -246,16 +253,16 @@ sealed abstract class CaptureSet extends Showable: * in a frozen state. */ def =:= (that: CaptureSet)(using Context): Boolean = - this.subCaptures(that, frozen = true).isOK - && that.subCaptures(this, frozen = true).isOK + this.subCaptures(that, Frozen.All).isOK + && that.subCaptures(this, Frozen.All).isOK /** The smallest capture set (via <:<) that is a superset of both * `this` and `that` */ def ++ (that: CaptureSet)(using Context): CaptureSet = - if this.subCaptures(that, frozen = true).isOK then + if this.subCaptures(that, Frozen.All).isOK then if that.isAlwaysEmpty && this.keepAlways then this else that - else if that.subCaptures(this, frozen = true).isOK then this + else if that.subCaptures(this, Frozen.All).isOK then this else if this.isConst && that.isConst then Const(this.elems ++ that.elems) else Union(this, that) @@ -267,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, Frozen.Vars).isOK then this + else if that.subCaptures(this, Frozen.Vars).isOK then that else if this.isConst && that.isConst then Const(elemIntersection(this, that)) else Intersection(this, that) @@ -395,6 +402,12 @@ object CaptureSet: type Vars = SimpleIdentitySet[Var] type Deps = SimpleIdentitySet[CaptureSet] + /** An enum indicating a Frozen degree for subCapturing tests */ + enum Frozen: + case None // operations are performed in a regular VarState + case Vars // operations are performed in a FrozenVarState + case All // operations are performed in FrozenAllState + @sharable private var varId = 0 /** If set to `true`, capture stack traces that tell us where sets are created */ @@ -538,7 +551,7 @@ object CaptureSet: else //if id == 34 then assert(!elem.isUniversalRootCapability) assert(elem.isTrackableRef, elem) - assert(!this.isInstanceOf[HiddenSet] || summon[VarState] == FrozenSepState, summon[VarState]) + assert(!this.isInstanceOf[HiddenSet] || summon[VarState] == FrozenAllState, summon[VarState]) elems += elem if elem.isRootCapability then rootAddedHandler() @@ -1043,7 +1056,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 + * By default, recording is allowed in regular both not in frozen states. * overrides this. */ def putElems(v: Var, elems: Refs): Boolean = { elemsMap(v) = elems; true } @@ -1055,14 +1068,14 @@ 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 + * By default, recording is allowed in regular both not in frozen states. * overrides this. */ def putDeps(v: Var, deps: Deps): Boolean = { depsMap(v) = deps; true } /** Record hidden elements in elemsMap of hidden set `v`, * return whether this was allowed. By default, recording is allowed - * but the special state FrozenSepState overrides this. + * but the special state FrozenAllState overrides this. */ def putHidden(v: HiddenSet, elems: Refs): Boolean = { elemsMap(v) = elems; true } @@ -1080,21 +1093,22 @@ object CaptureSet: else false end VarState - /** A special state that does not allow to record elements or dependent sets. + /** A class for states that do not allow to record elements or dependent sets. * In effect this means that no new elements or dependent sets can be added - * in this state (since the previous state cannot be recorded in a snapshot) - * On the other hand, this state does allow by default Fresh.Cap to subsume arbitary - * types, which are then recorded in its hidden set. + * 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 FrozenState extends VarState: + class FrozenVarState extends VarState: override def putElems(v: Var, refs: Refs) = false override def putDeps(v: Var, deps: Deps) = false @sharable - /** A frozen state that allows a Fresh.Cap instncce to subsume a - * reference `r` only if `r` is present in the hidden set of the instance. + /** A frozen state that allows a Fresh.Cap instancce to subsume a + * reference `r` only if `r` is already present in the hidden set of the instance. + * No new references can be added. */ - object FrozenSepState extends FrozenState: + object FrozenAllState extends FrozenVarState: override def putHidden(v: HiddenSet, elems: Refs): Boolean = false @sharable diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index b677b8d1b504..b981c14a0e9b 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} +import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult, Frozen} import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} @@ -320,7 +320,7 @@ class CheckCaptures extends Recheck, SymTransformer: /** Assert subcapturing `cs1 <: cs2` (available for debugging, otherwise unused) */ def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = - assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2") + assert(cs1.subCaptures(cs2, Frozen.None).isOK, i"$cs1 is not a subset of $cs2") /** If `res` is not CompareResult.OK, report an error */ def checkOK(res: CompareResult, prefix: => String, pos: SrcPos, provenance: => String = "")(using Context): Unit = @@ -334,7 +334,7 @@ class CheckCaptures extends Recheck, SymTransformer: /** Check subcapturing `{elem} <: cs`, report error on failure */ def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos, provenance: => String = "")(using Context) = checkOK( - elem.singletonCaptureSet.subCaptures(cs, frozen = false), + elem.singletonCaptureSet.subCaptures(cs, Frozen.None), i"$elem cannot be referenced here; it is not", pos, provenance) @@ -342,7 +342,7 @@ class CheckCaptures extends Recheck, SymTransformer: def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos, provenance: => String = "", cs1description: String = "")(using Context) = checkOK( - cs1.subCaptures(cs2, frozen = false), + cs1.subCaptures(cs2, Frozen.None), if cs1.elems.size == 1 then i"reference ${cs1.elems.toList.head}$cs1description is not" else i"references $cs1$cs1description are not all", pos, provenance) @@ -1390,7 +1390,7 @@ class CheckCaptures extends Recheck, SymTransformer: val cs = actual.captureSet if covariant then cs ++ leaked else - if !leaked.subCaptures(cs, frozen = false).isOK then + if !leaked.subCaptures(cs, Frozen.None).isOK then report.error( em"""$expected cannot be box-converted to ${actual.capturing(leaked)} |since the additional capture set $leaked resulted from box conversion is not allowed in $actual""", pos) @@ -1713,7 +1713,7 @@ class CheckCaptures extends Recheck, SymTransformer: val widened = ref.captureSetOfInfo val added = widened.filter(isAllowed(_)) capt.println(i"heal $ref in $cs by widening to $added") - if !added.subCaptures(cs, frozen = false).isOK then + if !added.subCaptures(cs, Frozen.None).isOK then val location = if meth.exists then i" of ${meth.showLocated}" else "" val paramInfo = if ref.paramName.info.kind.isInstanceOf[UniqueNameKind] diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 0781f40ab880..c495a160f054 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2889,10 +2889,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling end inverse end MapExistentials + protected def frozenDegree(frozen: Boolean) = + if frozen then CaptureSet.Frozen.Vars else CaptureSet.Frozen.None + protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = try if assocExistentials.isEmpty then - refs1.subCaptures(refs2, frozen) + refs1.subCaptures(refs2, frozenDegree(frozen)) else val mapped = refs1.map(MapExistentials(assocExistentials)) if mapped.elems.exists(Existential.isBadExistential) @@ -2903,7 +2906,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling throw ex protected def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - refs1.subCaptures(refs2, frozen) + refs1.subCaptures(refs2, frozenDegree(frozen)) /** Is the boxing status of tp1 and tp2 the same, or alternatively, is * the capture sets `refs1` of `tp1` a subcapture of the empty set? @@ -2911,7 +2914,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling */ protected def sameBoxed(tp1: Type, tp2: Type, refs1: CaptureSet)(using Context): Boolean = (tp1.isBoxedCapturing == tp2.isBoxedCapturing) - || refs1.subCaptures(CaptureSet.empty, frozenConstraint).isOK + || refs1.subCaptures(CaptureSet.empty, frozenDegree(frozenConstraint)).isOK // ----------- Diagnostics -------------------------------------------------- diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 7ae790c62a2c..6b7d804f28c4 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -19,7 +19,7 @@ import typer.Inferencing.* import typer.IfBottom import reporting.TestingReporter import cc.{CapturingType, derivedCapturingType, CaptureSet, captureSet, isBoxed, isBoxedCapturing} -import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} +import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap, Frozen} import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -161,7 +161,7 @@ object TypeOps: TypeComparer.lub(simplify(l, theMap), simplify(r, theMap), isSoft = tp.isSoft) case tp @ CapturingType(parent, refs) => if !ctx.mode.is(Mode.Type) - && refs.subCaptures(parent.captureSet, frozen = true).isOK + && refs.subCaptures(parent.captureSet, Frozen.All).isOK && (tp.isBoxed || !parent.isBoxedCapturing) // fuse types with same boxed status and outer boxed with any type then diff --git a/scala2-library-cc/src/scala/collection/IndexedSeqView.scala b/scala2-library-cc/src/scala/collection/IndexedSeqView.scala index cf23b0440869..78f8abb8e327 100644 --- a/scala2-library-cc/src/scala/collection/IndexedSeqView.scala +++ b/scala2-library-cc/src/scala/collection/IndexedSeqView.scala @@ -136,7 +136,7 @@ object IndexedSeqView { @SerialVersionUID(3L) class Concat[A](prefix: SomeIndexedSeqOps[A]^, suffix: SomeIndexedSeqOps[A]^) - extends SeqView.Concat[A](prefix, caps.unsafe.unsafeAssumeSeparate(suffix)) with IndexedSeqView[A] + extends SeqView.Concat[A](prefix, suffix) with IndexedSeqView[A] @SerialVersionUID(3L) class Take[A](underlying: SomeIndexedSeqOps[A]^, n: Int) diff --git a/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala b/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala index fb120ac2a930..9ce0399e0662 100644 --- a/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala +++ b/scala2-library-cc/src/scala/collection/mutable/CheckedIndexedSeqView.scala @@ -75,7 +75,7 @@ private[mutable] object CheckedIndexedSeqView { @SerialVersionUID(3L) class Concat[A](prefix: SomeIndexedSeqOps[A]^, suffix: SomeIndexedSeqOps[A]^)(protected val mutationCount: () => Int) - extends IndexedSeqView.Concat[A](prefix, caps.unsafe.unsafeAssumeSeparate(suffix)) with CheckedIndexedSeqView[A] + extends IndexedSeqView.Concat[A](prefix, suffix) with CheckedIndexedSeqView[A] @SerialVersionUID(3L) class Take[A](underlying: SomeIndexedSeqOps[A]^, n: Int)(protected val mutationCount: () => Int) From 8abb7b180021963a0462598e707ac168d5835ad8 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 22 Dec 2024 17:15:52 +0100 Subject: [PATCH 28/32] Improve error message --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 4 +++ .../dotty/tools/dotc/cc/CheckCaptures.scala | 30 +++++++++++-------- .../src/dotty/tools/dotc/cc/SepCheck.scala | 11 +++---- .../captures/cc-dep-param.check | 4 +-- .../captures/filevar-expanded.check | 4 +-- .../captures/function-combinators.check | 4 +-- tests/neg-custom-args/captures/lazyref.check | 4 +-- 7 files changed, 34 insertions(+), 27 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index eb383cb8839d..6e2705e2e0d8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -422,6 +422,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 diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index b981c14a0e9b..34cb99659f69 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -245,6 +245,13 @@ object CheckCaptures: * a separation check? */ def needsSepCheck: Boolean + + /** If a tree is an argument for which needsSepCheck is true, + * the actual type of the argument before it was widened to formal. + * The nuType of this argument is the formal parameter type in this case, + * but for error diagnosis it's important to know what the actual type was. + */ + def actualType: Type end CheckerAPI class CheckCaptures extends Recheck, SymTransformer: @@ -285,11 +292,14 @@ class CheckCaptures extends Recheck, SymTransformer: */ private val todoAtPostCheck = new mutable.ListBuffer[() => Unit] - /** Trees that will need a separation check because they contain cap */ - private val sepCheckable = util.EqHashSet[Tree]() + /** Maps trees that will need a separation check because they contain cap + * to the actual, non-widened type. + */ + private val sepCheckable = util.EqHashMap[Tree, Type]() extension [T <: Tree](tree: T) def needsSepCheck: Boolean = sepCheckable.contains(tree) + def actualType: Type = sepCheckable.getOrElse(tree, NoType) /** Instantiate capture set variables appearing contra-variantly to their * upper approximation. @@ -666,15 +676,13 @@ class CheckCaptures extends Recheck, SymTransformer: val freshenedFormal = Fresh.fromCap(formal) val argType = recheck(arg, freshenedFormal) .showing(i"recheck arg $arg vs $freshenedFormal", capt) - 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 _ => + if formal.hasUseAnnot then + // The @use annotation is added to `formal` by `prepareFunction` + capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") + markFree(argType.deepCaptureSet, arg.srcPos) if formal.containsCap then arg.updNuType(freshenedFormal) - sepCheckable += arg + sepCheckable(arg) = argType argType /** Map existential captures in result to `cap` and implement the following @@ -704,9 +712,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 diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index 7f69e11d5d4e..e0a53e3390ed 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -61,11 +61,7 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: def captures(arg: Tree) = val argType = arg.nuType - argType match - case AnnotatedType(formal1, ann) if ann.symbol == defn.UseAnnot => - argType.deepCaptureSet - case _ => - argType.captureSet + if argType.hasUseAnnot then argType.deepCaptureSet else argType.captureSet val argCaptures = args.map(captures) capt.println(i"check separate $fn($args), fnCaptures = $fnCaptures, argCaptures = $argCaptures") @@ -80,13 +76,14 @@ class SepChecker(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: //println(i"check sep $arg / $footprint / $hiddenInArg") val overlap = hiddenInArg.footprint.overlapWith(footprint) if !overlap.isEmpty then + def formalName = if pname.toString.contains('$') then "" else i"$pname " def whatStr = if overlap.size == 1 then "this capability" else "these capabilities" def funStr = if fn.symbol.exists then i"${fn.symbol}" else "the function" report.error( - em"""Separation failure: argument to capture-polymorphic parameter $pname: ${arg.nuType} - |captures ${CaptureSet(overlap)} and also passes $whatStr separately to $funStr""", + em"""Separation failure: argument of type ${arg.actualType} to capture-polymorphic parameter + |${formalName}of type ${arg.nuType} captures ${CaptureSet(overlap)}, and $whatStr is also passed separately to $funStr.""", arg.srcPos) footprint ++= hiddenInArg diff --git a/tests/neg-custom-args/captures/cc-dep-param.check b/tests/neg-custom-args/captures/cc-dep-param.check index bca48d3758f4..69b1e9f33412 100644 --- a/tests/neg-custom-args/captures/cc-dep-param.check +++ b/tests/neg-custom-args/captures/cc-dep-param.check @@ -1,5 +1,5 @@ -- Error: tests/neg-custom-args/captures/cc-dep-param.scala:8:6 -------------------------------------------------------- 8 | foo(a, useA) // error: separation failure | ^ - | Separation failure: argument to capture-polymorphic parameter x$0: Foo[Int]^ - | captures {a} and also passes this capability separately to method foo + | Separation failure: argument of type (a : Foo[Int]^) to capture-polymorphic parameter + | of type Foo[Int]^ captures {a}, and this capability is also passed separately to method foo. diff --git a/tests/neg-custom-args/captures/filevar-expanded.check b/tests/neg-custom-args/captures/filevar-expanded.check index cdac5d6b26ac..13df5fc0c9c4 100644 --- a/tests/neg-custom-args/captures/filevar-expanded.check +++ b/tests/neg-custom-args/captures/filevar-expanded.check @@ -1,8 +1,8 @@ -- Error: tests/neg-custom-args/captures/filevar-expanded.scala:34:19 -------------------------------------------------- 34 | withFile(io3): f => // error: separation failure | ^ - | Separation failure: argument to capture-polymorphic parameter x$1: (f: test2.File^{io3}) => Unit - | captures {io3} and also passes this capability separately to method withFile + |Separation failure: argument of type (f: test2.File^{io3}) ->{io3} Unit to capture-polymorphic parameter + |of type (f: test2.File^{io3}) => Unit captures {io3}, and this capability is also passed separately to method withFile. 35 | val o = Service(io3) 36 | o.file = f // this is a bit dubious. It's legal since we treat class refinements 37 | // as capture set variables that can be made to include refs coming from outside. diff --git a/tests/neg-custom-args/captures/function-combinators.check b/tests/neg-custom-args/captures/function-combinators.check index 05d0ee6854a4..b60b3f0a3e2b 100644 --- a/tests/neg-custom-args/captures/function-combinators.check +++ b/tests/neg-custom-args/captures/function-combinators.check @@ -1,5 +1,5 @@ -- Error: tests/neg-custom-args/captures/function-combinators.scala:15:22 ---------------------------------------------- 15 | val b2 = g1.andThen(g1); // error: separation failure | ^^ - | Separation failure: argument to capture-polymorphic parameter x$0: Int => Int - | captures {ctx1} and also passes this capability separately to method andThen + | Separation failure: argument of type (g1 : Int ->{ctx1} Int) to capture-polymorphic parameter + | of type Int => Int captures {ctx1}, and this capability is also passed separately to method andThen. diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index 0e2eea582b50..634b77d47c91 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -29,5 +29,5 @@ -- Error: tests/neg-custom-args/captures/lazyref.scala:24:55 ----------------------------------------------------------- 24 | val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) // error: separation failure | ^ - | Separation failure: argument to capture-polymorphic parameter x$0: Int => Int - | captures {cap2} and also passes this capability separately to method map + | Separation failure: argument of type (x: Int) ->{cap2} Int to capture-polymorphic parameter + | of type Int => Int captures {cap2}, and this capability is also passed separately to method map. From 4ae86f766b70fa2875f3b2db70bfff6d0562f044 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 23 Dec 2024 09:31:26 +0100 Subject: [PATCH 29/32] Simplify accountsFor --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index a6baca97c093..1f091d3f0a86 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -191,10 +191,7 @@ sealed abstract class CaptureSet extends Showable: existsElem(elems, _.subsumes(x)) || !x.isMaxCapability && !x.derivesFrom(defn.Caps_CapSet) - && x.captureSetOfInfo.subCaptures(this)(using ctx, - vs match - case vs: FrozenVarState => vs - case _ => FrozenVarState()).isOK + && x.captureSetOfInfo.subCaptures(this, Frozen.All).isOK comparer match case comparer: ExplainingTypeComparer => comparer.traceIndented(debugInfo)(test) @@ -1044,7 +1041,7 @@ object CaptureSet: class VarState: /** A map from captureset variables to their elements at the time of the snapshot. */ - private val elemsMap: util.EqHashMap[Var, Refs] = new util.EqHashMap + protected val elemsMap: util.EqHashMap[Var, Refs] = new util.EqHashMap /** A map from captureset variables to their dependent sets at the time of the snapshot. */ private val depsMap: util.EqHashMap[Var, Deps] = new util.EqHashMap @@ -1102,6 +1099,7 @@ object CaptureSet: class FrozenVarState extends VarState: override def putElems(v: Var, refs: Refs) = false override def putDeps(v: Var, deps: Deps) = false + override def putHidden(v: HiddenSet, elems: Refs): Boolean = { elemsMap(v) = elems; true } @sharable /** A frozen state that allows a Fresh.Cap instancce to subsume a From 8049a9d10f8b1ff864a8ebb028e27e63aa2d7c2b Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 29 Dec 2024 12:15:37 +0100 Subject: [PATCH 30/32] 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 `Fresh.Cap` as `fresh` in error messages where both `cap` and `Fresh.Cap` are printed. This avoid confustion with `cap`. Similarly, print `A => B` as `A ->{fresh B` in that case. - 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. - Convert cap to fresh in type arguments of asInstanceOf - Have Fresh.FromCap also work for pre-cc @retains annotated types - Don't cache captureSetOfInfos under mode IgnoreCaptures - Let cap and Fresh.Cap subsume other refs only if these others refs cannot be added separately to a capture set. - When creating a instance of a capability class, assume fresh.cap, not cap as capability. Three tests had to be disabled, they were renamed from test.scala to test.scala.disabled. Two of these will work again when we stop boxing alias types (next commit). The third, ex-fun-aliases needs more investigation. Also, the neg test box-adapt-cases needs to be re-evaluated. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 21 ++++++- .../src/dotty/tools/dotc/cc/CaptureRef.scala | 42 +++++++++---- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 43 ++++++++----- .../dotty/tools/dotc/cc/CheckCaptures.scala | 28 +++++---- compiler/src/dotty/tools/dotc/cc/Fresh.scala | 63 ++++++++++++------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 34 +++++++--- .../dotty/tools/dotc/core/TypeComparer.scala | 6 ++ .../tools/dotc/printing/PlainPrinter.scala | 16 ++++- .../dotty/tools/dotc/transform/Recheck.scala | 5 +- .../immutable/LazyListIterable.scala | 3 +- .../captures/box-adapt-cases.check | 21 +++++++ .../captures/box-adapt-cases.scala | 16 ++--- .../captures/depfun-reach.check | 4 +- tests/neg-custom-args/captures/eta.check | 2 +- .../captures/explain-under-approx.check | 3 + .../captures/function-combinators.check | 9 ++- .../captures/function-combinators.scala | 3 +- tests/neg-custom-args/captures/i16725.scala | 2 +- tests/neg-custom-args/captures/i19330.check | 12 ++++ tests/neg-custom-args/captures/i19330.scala | 2 +- tests/neg-custom-args/captures/i21614.check | 2 +- .../neg-custom-args/captures/outer-var.check | 10 +-- tests/neg-custom-args/captures/reaches.check | 14 +++++ tests/neg-custom-args/captures/reaches.scala | 4 +- tests/neg-custom-args/captures/try.check | 4 +- .../captures/unsound-reach-2.scala | 2 +- .../captures/unsound-reach-3.scala | 2 +- .../captures/unsound-reach-4.check | 7 +++ .../captures/unsound-reach-4.scala | 2 +- .../captures/unsound-reach.check | 7 +++ .../captures/unsound-reach.scala | 2 +- tests/neg-custom-args/captures/vars.check | 6 +- tests/neg/existential-mapping.check | 26 ++++---- .../captures/boxmap-paper.scala | 14 ++++- tests/pos-custom-args/captures/cc-cast.scala | 12 ++++ .../captures/ex-fun-aliases.scala.disabled} | 4 ++ .../captures/function-combinators.scala | 2 +- ...ases.scala => i15923-cases.scala.disabled} | 0 .../{i15925.scala => i15925.scala.disabled} | 0 .../captures/i20237-explicit.scala | 15 +++++ .../captures}/i20237.scala | 0 .../captures/open-existential.scala | 15 +++++ 42 files changed, 355 insertions(+), 130 deletions(-) create mode 100644 tests/neg-custom-args/captures/box-adapt-cases.check create mode 100644 tests/neg-custom-args/captures/i19330.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%) rename tests/pos-custom-args/captures/{i15923-cases.scala => i15923-cases.scala.disabled} (100%) rename tests/pos-custom-args/captures/{i15925.scala => i15925.scala.disabled} (100%) 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 diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 6e2705e2e0d8..3aae27246b17 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -18,8 +18,12 @@ import CCState.* import reporting.Message import CaptureSet.Frozen +/** Attachment key for capturing type trees */ private val Captures: Key[CaptureSet] = Key() +/** Context property to print Fresh.Cap as "fresh" instead of "cap" */ +val PrintFresh: Key[Unit] = Key() + object ccConfig: /** If true, allow mapping capture set variables under captureChecking with maps that are neither @@ -50,8 +54,10 @@ object ccConfig: def useFresh(using Context): Boolean = Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.6`) -end ccConfig + def followAliases(using Context): Boolean = + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`) +end ccConfig /** Are we at checkCaptures phase? */ def isCaptureChecking(using Context): Boolean = @@ -668,6 +674,19 @@ class CleanupRetains(using Context) extends TypeMap: RetainingType(tp, Nil, byName = annot.symbol == defn.RetainsByNameAnnot) case _ => mapOver(tp) +/** A typemap that follows aliases and keeps their transformed results if + * there is a change. + */ +trait FollowAliasesMap(using Context) extends TypeMap: + var follow = true // Used for debugging so that we can compare results with and w/o following. + def mapFollowingAliases(t: Type): Type = + val t1 = t.dealiasKeepAnnots + if follow && (t1 ne t) then + val t2 = apply(t1) + if t2 ne t1 then t2 + else t + else mapOver(t) + /** An extractor for `caps.reachCapability(ref)`, which is used to express a reach * capability as a tree in a @retains annotation. */ diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala index 025f02505f5b..e4fbe5770888 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -124,7 +124,7 @@ trait CaptureRef extends TypeProxy, ValueType: else myCaptureSet = CaptureSet.Pending val computed = CaptureSet.ofInfo(this) - if !isCaptureChecking || underlying.isProvisional then + if !isCaptureChecking || ctx.mode.is(Mode.IgnoreCaptures) || underlying.isProvisional then myCaptureSet = null else myCaptureSet = computed @@ -165,18 +165,9 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false (this eq y) - || this.isCap - || this.match - case Fresh.Cap(hidden) => - if vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) then true - else if !hidden.recordElemsState() || y.stripReadOnly.isCap then false - else - hidden.elems += y - true - case _ => - false + || maxSubsumes(y, canAddHidden = vs.frozen != CaptureSet.Frozen.None) || y.match - case y: TermRef => + case y: TermRef if !y.isCap => y.prefix.match case ypre: CaptureRef => this.subsumes(ypre) @@ -221,6 +212,33 @@ trait CaptureRef extends TypeProxy, ValueType: case _ => false end subsumes + /** This is a maximal capabaility that subsumes `y` in given context and VarState. + * @param canAddHidden If true we allow maximal capabilties to subsume all other capabilities. + * We add those capabilities to the hidden set if this is Fresh.Cap + * If false we only accept `y` elements that are already in the + * hidden set of this Fresh.Cap. The idea is that in a VarState that + * accepts additions we first run `maxSubsumes` with `canAddHidden = false` + * so that new variables get added to the sets. If that fails, we run + * the test again with canAddHidden = true as a last effort before we + * fail a comparison. + */ + def maxSubsumes(y: CaptureRef, canAddHidden: Boolean)(using ctx: Context, vs: VarState = FrozenAllState): Boolean = + this.match + case Fresh.Cap(hidden) => + if vs.ifNotSeen(this)(hidden.elems.exists(_.subsumes(y))) then true + else if !y.stripReadOnly.isCap && hidden.recordElemsState() then + if canAddHidden then + hidden.elems += y + true + else + false + else false + case _ => + this.isCap && canAddHidden + || y.match + case ReadOnlyCapability(y1) => this.stripReadOnly.maxSubsumes(y1, canAddHidden) + case _ => false + def assumedContainsOf(x: TypeRef)(using Context): SimpleIdentitySet[CaptureRef] = CaptureSet.assumedContains.getOrElse(x, SimpleIdentitySet.empty) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 1f091d3f0a86..ba4a209354f0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -162,6 +162,11 @@ sealed abstract class CaptureSet extends Showable: */ protected def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult + protected def addHiddenElem(elem: CaptureRef)(using ctx: Context, vs: VarState): CompareResult = + if elems.exists(_.maxSubsumes(elem, canAddHidden = true)) + then CompareResult.OK + else CompareResult.Fail(this :: Nil) + /** If this is a variable, add `cs` as a dependent set */ protected def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult @@ -399,12 +404,6 @@ object CaptureSet: type Vars = SimpleIdentitySet[Var] type Deps = SimpleIdentitySet[CaptureSet] - /** An enum indicating a Frozen degree for subCapturing tests */ - enum Frozen: - case None // operations are performed in a regular VarState - case Vars // operations are performed in a FrozenVarState - case All // operations are performed in FrozenAllState - @sharable private var varId = 0 /** If set to `true`, capture stack traces that tell us where sets are created */ @@ -442,7 +441,7 @@ object CaptureSet: def isAlwaysEmpty = elems.isEmpty def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = - CompareResult.Fail(this :: Nil) + addHiddenElem(elem) def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK @@ -538,15 +537,14 @@ object CaptureSet: deps = state.deps(this) final def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if isConst // Fail if variable is solved, - || !recordElemsState() // or given VarState is frozen, - || Existential.isBadExistential(elem) // or `elem` is an out-of-scope existential, - then + if isConst || !recordElemsState() then // Fail if variable is solved or given VarState is frozen + addHiddenElem(elem) + else if Existential.isBadExistential(elem) then // Fail if `elem` is an out-of-scope existential CompareResult.Fail(this :: Nil) else if !levelOK(elem) then CompareResult.LevelError(this, elem) // or `elem` is not visible at the level of the set. else - //if id == 34 then assert(!elem.isUniversalRootCapability) + // id == 108 then assert(false, i"trying to add $elem to $this") assert(elem.isTrackableRef, elem) assert(!this.isInstanceOf[HiddenSet] || summon[VarState] == FrozenAllState, summon[VarState]) elems += elem @@ -562,8 +560,13 @@ object CaptureSet: res.addToTrace(this) private def levelOK(elem: CaptureRef)(using Context): Boolean = - if elem.isRootCapability || Existential.isExistentialVar(elem) then + if elem.isRootCapability then + !noUniversal + else if Existential.isExistentialVar(elem) then !noUniversal + && !TypeComparer.isOpenedExistential(elem) + // Opened existentials on the left cannot be added to nested capture sets on the right + // of a comparison. Test case is open-existential.scala. else elem match case elem: TermRef if level.isDefined => elem.prefix match @@ -635,7 +638,7 @@ object CaptureSet: */ def solve()(using Context): Unit = if !isConst then - val approx = upperApprox(empty) + val approx = upperApprox(empty).map(Fresh.FromCap(NoSymbol).inverse) .showing(i"solve $this = $result", capt) //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") val newElems = approx.elems -- elems @@ -1035,11 +1038,19 @@ object CaptureSet: case _ => this end CompareResult + /** An enum indicating a Frozen degree for subCapturing tests */ + enum Frozen: + case None // operations are performed in a regular VarState + case Vars // operations are performed in a FrozenVarState + case All // operations are performed in FrozenAllState + /** A VarState serves as a snapshot mechanism that can undo * additions of elements or super sets if an operation fails */ class VarState: + def frozen: Frozen = Frozen.None + /** A map from captureset variables to their elements at the time of the snapshot. */ protected val elemsMap: util.EqHashMap[Var, Refs] = new util.EqHashMap @@ -1054,7 +1065,6 @@ object CaptureSet: /** Record elements, return whether this was allowed. * By default, recording is allowed in regular both not in frozen states. - * overrides this. */ def putElems(v: Var, elems: Refs): Boolean = { elemsMap(v) = elems; true } @@ -1066,7 +1076,6 @@ object CaptureSet: /** Record dependent sets, return whether this was allowed. * By default, recording is allowed in regular both not in frozen states. - * overrides this. */ def putDeps(v: Var, deps: Deps): Boolean = { depsMap(v) = deps; true } @@ -1097,6 +1106,7 @@ object CaptureSet: * subsume arbitary types, which are then recorded in their hidden sets. */ class FrozenVarState extends VarState: + override def frozen = Frozen.Vars override def putElems(v: Var, refs: Refs) = false override def putDeps(v: Var, deps: Deps) = false override def putHidden(v: HiddenSet, elems: Refs): Boolean = { elemsMap(v) = elems; true } @@ -1107,6 +1117,7 @@ object CaptureSet: * No new references can be added. */ object FrozenAllState extends FrozenVarState: + override def frozen = Frozen.All override def putHidden(v: HiddenSet, elems: Refs): Boolean = false @sharable diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 34cb99659f69..50b2fd94d1d7 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -333,20 +333,21 @@ class CheckCaptures extends Recheck, SymTransformer: assert(cs1.subCaptures(cs2, Frozen.None).isOK, i"$cs1 is not a subset of $cs2") /** If `res` is not CompareResult.OK, report an error */ - def checkOK(res: CompareResult, prefix: => String, pos: SrcPos, provenance: => String = "")(using Context): Unit = + def checkOK(res: CompareResult, prefix: => String, added: CaptureRef | CaptureSet, pos: SrcPos, provenance: => String = "")(using Context): Unit = if !res.isOK then - def toAdd: String = CaptureSet.levelErrors.toAdd.mkString - def descr: String = - val d = res.blocking.description - if d.isEmpty then provenance else "" - report.error(em"$prefix included in the allowed capture set ${res.blocking}$descr$toAdd", pos) + inContext(Fresh.printContext(added, res.blocking)): + def toAdd: String = CaptureSet.levelErrors.toAdd.mkString + def descr: String = + val d = res.blocking.description + if d.isEmpty then provenance else "" + report.error(em"$prefix included in the allowed capture set ${res.blocking}$descr$toAdd", pos) /** Check subcapturing `{elem} <: cs`, report error on failure */ def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos, provenance: => String = "")(using Context) = checkOK( elem.singletonCaptureSet.subCaptures(cs, Frozen.None), i"$elem cannot be referenced here; it is not", - pos, provenance) + elem, pos, provenance) /** Check subcapturing `cs1 <: cs2`, report error on failure */ def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos, @@ -355,7 +356,7 @@ class CheckCaptures extends Recheck, SymTransformer: cs1.subCaptures(cs2, Frozen.None), if cs1.elems.size == 1 then i"reference ${cs1.elems.toList.head}$cs1description is not" else i"references $cs1$cs1description are not all", - pos, provenance) + cs1, pos, provenance) /** If `sym` is a class or method nested inside a term, a capture set variable representing * the captured variables of the environment associated with `sym`. @@ -771,7 +772,7 @@ class CheckCaptures extends Recheck, SymTransformer: var refined: Type = core var allCaptures: CaptureSet = if core.derivesFromMutable then CaptureSet.fresh() - else if core.derivesFromCapability then initCs ++ defn.universalCSImpliedByCapability + 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 @@ -1227,10 +1228,11 @@ class CheckCaptures extends Recheck, SymTransformer: actualBoxed else capt.println(i"conforms failed for ${tree}: $actual vs $expected") - err.typeMismatch(tree.withType(actualBoxed), expected1, - addApproxAddenda( - addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors), - expected1)) + inContext(Fresh.printContext(actualBoxed, expected1)): + err.typeMismatch(tree.withType(actualBoxed), expected1, + addApproxAddenda( + addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors), + expected1)) actual end checkConformsExpr diff --git a/compiler/src/dotty/tools/dotc/cc/Fresh.scala b/compiler/src/dotty/tools/dotc/cc/Fresh.scala index 04269fa3ab60..14c4c03e4115 100644 --- a/compiler/src/dotty/tools/dotc/cc/Fresh.scala +++ b/compiler/src/dotty/tools/dotc/cc/Fresh.scala @@ -58,10 +58,9 @@ object Fresh: case _ => None end Cap - class FromCap(owner: Symbol)(using Context) extends BiTypeMap: + class FromCap(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap: thisMap => - var change = false var reach = false private def initHidden = @@ -73,26 +72,30 @@ object Fresh: override def apply(t: Type) = if variance <= 0 then t - else t.dealiasKeepAnnots match + else t match case t: CaptureRef if t.isCap => - change = true Cap(initHidden) case t @ CapturingType(_, refs) => - change = refs.isInstanceOf[CaptureSet.Var] 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 _ => - mapOver(t) + mapFollowingAliases(t) override def toString = "CapToFresh" - lazy val inverse = new BiTypeMap: + lazy val inverse: BiTypeMap & FollowAliasesMap = new BiTypeMap with FollowAliasesMap: def apply(t: Type): Type = t match - case t @ Cap(_) => - change = true - defn.captureRoot.termRef - case _ => mapOver(t) + case t @ Cap(_) => defn.captureRoot.termRef + case t @ CapturingType(_, refs) => mapOver(t) + case _ => mapFollowingAliases(t) + def inverse = thisMap override def toString = thisMap.toString + ".inverse" @@ -100,22 +103,34 @@ object Fresh: /** Maps cap to fresh */ def fromCap(tp: Type, owner: Symbol = NoSymbol)(using Context): Type = - if ccConfig.useFresh then - val mapper = FromCap(owner) - val mapped = mapper(tp) - if mapper.change then mapped else tp - else - tp + if ccConfig.useFresh then FromCap(owner)(tp) else tp /** Maps fresh to cap */ def toCap(tp: Type)(using Context): Type = - if ccConfig.useFresh then - val fromCap = FromCap(NoSymbol) - val mapper = fromCap.inverse - val mapped = mapper(tp) - if fromCap.change then mapped else tp - else - tp + 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 445d327d82c1..ac720b7863dc 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 @@ -277,7 +278,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: paramInfos = tp.paramInfos.mapConserve(_.dropAllRetains.bounds), resType = this(tp.resType)) case _ => - mapOver(tp) + if ccConfig.followAliases then mapFollowingAliases(tp) else mapOver(tp) addVar(addCaptureRefinements(normalizeCaptures(tp1)), ctx.owner) end apply end mapInferred @@ -299,9 +300,10 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * 3. Add universal capture sets to types deriving from Capability * 4. Map `cap` in function result types to existentially bound variables. * 5. Schedule deferred well-formed tests for types with retains annotations. + * 6. Perform normalizeCaptures */ private def transformExplicitType(tp: Type, tptToCheck: Tree = EmptyTree)(using Context): Type = - val toCapturing = new DeepTypeMap: + val toCapturing = new DeepTypeMap with FollowAliasesMap: override def toString = "expand aliases" /** Expand $throws aliases. This is hard-coded here since $throws aliases in stdlib @@ -337,7 +339,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tp @ CapturingType(parent, refs) if (refs eq defn.universalCSImpliedByCapability) && !tp.isBoxedCapturing => parent - case tp @ CapturingType(parent, refs) => tp case _ => tp def apply(t: Type) = @@ -363,13 +364,22 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // Map references to capability classes C to C^ if t.derivesFromCapability && !t.isSingleton && t.typeSymbol != defn.Caps_Exists then CapturingType(t, defn.universalCSImpliedByCapability, boxed = false) - else normalizeCaptures(mapOver(t)) + else normalizeCaptures( + if ccConfig.followAliases then mapFollowingAliases(t) else mapOver(t)) end toCapturing def fail(msg: Message) = if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos) val tp1 = toCapturing(tp) + if false then + val tp1alt = + toCapturing.follow = false + toCapturing(tp) + if tp1 ne tp1alt then + println(i"""DIFF mapping $tp + |WAS $tp1alt + |NOW $tp1""") val tp2 = Existential.mapCapInResults(fail)(tp1) if tp2 ne tp then capt.println(i"expanded explicit in ${ctx.owner}: $tp --> $tp1 --> $tp2") tp2 @@ -495,8 +505,10 @@ 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 + 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 => @@ -828,7 +840,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed) case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) - case tp @ AppliedType(tycon, args) if !defn.isFunctionClass(tp.dealias.typeSymbol) => + case tp @ AppliedType(tycon, args) + if !defn.isFunctionClass(tp.dealias.typeSymbol) + && (!ccConfig.followAliases || (tp.dealias eq tp)) => tp.derivedAppliedType(tycon, args.mapConserve(box)) case tp: RealTypeBounds => tp.derivedTypeBounds(tp.lo, box(tp.hi)) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index c495a160f054..22cf1036786d 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`. @@ -3479,6 +3482,9 @@ object TypeComparer { def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context) = comparing(_.subsumesExistentially(tp1, tp2)) + + def isOpenedExistential(ref: CaptureRef)(using Context) = + comparing(_.isOpenedExistential(ref)) } object MatchReducer: diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 7e0d77b1c8e8..eb6b5bcf569a 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 @@ -251,7 +257,7 @@ class PlainPrinter(_ctx: Context) extends Printer { if refs.isUniversal then if refs.elems.size == 1 || !printDebug then rootSetText else toTextCaptureSet(refs) - else if !refs.elems.isEmpty && refs.elems.forall(_.isCapOrFresh) && !printDebug then + else if !refs.elems.isEmpty && refs.elems.forall(_.isCapOrFresh) && !printFresh then rootSetText else toTextCaptureSet(refs) @@ -308,7 +314,10 @@ class PlainPrinter(_ctx: Context) extends Printer { else if (annot.symbol == defn.IntoAnnot || annot.symbol == defn.IntoParamAnnot) && !printDebug then atPrec(GlobalPrec)( Str("into ") ~ toText(tpe) ) - else toTextLocal(tpe) ~ " " ~ toText(annot) + else if annot.isInstanceOf[CaptureAnnotation] then + toTextLocal(tpe) ~ "^" ~ toText(annot) + else + toTextLocal(tpe) ~ " " ~ toText(annot) case FlexibleType(_, tpe) => "(" ~ toText(tpe) ~ ")?" case tp: TypeVar => @@ -432,7 +441,8 @@ class PlainPrinter(_ctx: Context) extends Printer { case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" case Fresh.Cap(hidden) => - if printDebug then s"" + if printFreshDetailed then s"" ~ "//" + else if printFresh then "fresh" else "cap" case tp => toText(tp) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 22f83aa785de..e8227f759ad4 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -259,7 +259,10 @@ abstract class Recheck extends Phase, SymTransformer: def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type = val resType = recheck(tree.tpt) - if tree.rhs.isEmpty then resType + def isUninitWildcard = tree.rhs match + case Ident(nme.WILDCARD) => tree.symbol.is(Mutable) + case _ => false + if tree.rhs.isEmpty || isUninitWildcard then resType else recheck(tree.rhs, resType) def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Type = diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index 2d04855cc227..853f3f9328a1 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -25,6 +25,7 @@ import scala.runtime.Statics import language.experimental.captureChecking import annotation.unchecked.uncheckedCaptures import caps.untrackedCaptures +import caps.unsafe.unsafeAssumeSeparate /** This class implements an immutable linked list. We call it "lazy" * because it computes its elements only when they are needed. @@ -1184,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 new file mode 100644 index 000000000000..dba556dd98a9 --- /dev/null +++ b/tests/neg-custom-args/captures/box-adapt-cases.check @@ -0,0 +1,21 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:7:10 ------------------------------- +7 | x.value(cap => cap.use()) // error, was OK + | ^^^^^^^^^^^^^^^^ + | Found: (cap: box Cap^?) => Int + | Required: (cap: box Cap^) ->{fresh} Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:14:10 ------------------------------ +14 | x.value(cap => cap.use()) // error + | ^^^^^^^^^^^^^^^^ + | Found: (cap: box Cap^?) ->{io} Int + | Required: (cap: box Cap^{io}) -> Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:28:10 ------------------------------ +28 | x.value(cap => cap.use()) // error + | ^^^^^^^^^^^^^^^^ + | Found: (cap: box Cap^?) ->{io, fs} Int + | Required: (cap: box Cap^{io, fs}) ->{io} Int + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/box-adapt-cases.scala b/tests/neg-custom-args/captures/box-adapt-cases.scala index d9ec0f80a548..150b3cc2c3e7 100644 --- a/tests/neg-custom-args/captures/box-adapt-cases.scala +++ b/tests/neg-custom-args/captures/box-adapt-cases.scala @@ -1,29 +1,29 @@ trait Cap { def use(): Int } def test1(): Unit = { - type Id[X] = [T] -> (op: X => T) -> T + class Id[X](val value: [T] -> (op: X => T) -> T) val x: Id[Cap^] = ??? - x(cap => cap.use()) + x.value(cap => cap.use()) // error, was OK } def test2(io: Cap^): Unit = { - type Id[X] = [T] -> (op: X -> T) -> T + class Id[X](val value: [T] -> (op: X -> T) -> T) val x: Id[Cap^{io}] = ??? - x(cap => cap.use()) // error + x.value(cap => cap.use()) // error } def test3(io: Cap^): Unit = { - type Id[X] = [T] -> (op: X ->{io} T) -> T + class Id[X](val value: [T] -> (op: X ->{io} T) -> T) val x: Id[Cap^{io}] = ??? - x(cap => cap.use()) // ok + x.value(cap => cap.use()) // ok } def test4(io: Cap^, fs: Cap^): Unit = { - type Id[X] = [T] -> (op: X ->{io} T) -> T + class Id[X](val value: [T] -> (op: X ->{io} T) -> T) val x: Id[Cap^{io, fs}] = ??? - x(cap => cap.use()) // error + x.value(cap => cap.use()) // error } diff --git a/tests/neg-custom-args/captures/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/eta.check b/tests/neg-custom-args/captures/eta.check index b96788403e7a..b7669e9b68ea 100644 --- a/tests/neg-custom-args/captures/eta.check +++ b/tests/neg-custom-args/captures/eta.check @@ -8,5 +8,5 @@ -- Error: tests/neg-custom-args/captures/eta.scala:6:20 ---------------------------------------------------------------- 6 | bar( () => f ) // error | ^ - | reference (f : () => Unit) is not included in the allowed capture set {} + | reference (f : Proc^) is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> box () ->? Unit diff --git a/tests/neg-custom-args/captures/explain-under-approx.check b/tests/neg-custom-args/captures/explain-under-approx.check index f84ac5eb2b53..bc59612ebef6 100644 --- a/tests/neg-custom-args/captures/explain-under-approx.check +++ b/tests/neg-custom-args/captures/explain-under-approx.check @@ -4,6 +4,9 @@ | Found: Future[Int]{val a: (async : Async)}^{async} | Required: Future[Int]^{col.futs*} | + | Note that reference ex$25.type + | cannot be included in outer capture set {cap} + | | 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 diff --git a/tests/neg-custom-args/captures/function-combinators.check b/tests/neg-custom-args/captures/function-combinators.check index b60b3f0a3e2b..e8dfcf79112c 100644 --- a/tests/neg-custom-args/captures/function-combinators.check +++ b/tests/neg-custom-args/captures/function-combinators.check @@ -1,5 +1,10 @@ --- Error: tests/neg-custom-args/captures/function-combinators.scala:15:22 ---------------------------------------------- -15 | val b2 = g1.andThen(g1); // error: separation failure +-- Error: tests/neg-custom-args/captures/function-combinators.scala:10:21 ---------------------------------------------- +10 | val a1 = f.andThen(f); // error: separation failure + | ^ + | Separation failure: argument of type (f : Int => Int) to capture-polymorphic parameter + | of type Int => Int captures {f}, and this capability is also passed separately to method andThen. +-- Error: tests/neg-custom-args/captures/function-combinators.scala:16:22 ---------------------------------------------- +16 | val b2 = g1.andThen(g1); // error: separation failure | ^^ | Separation failure: argument of type (g1 : Int ->{ctx1} Int) to capture-polymorphic parameter | of type Int => Int captures {ctx1}, and this capability is also passed separately to method andThen. diff --git a/tests/neg-custom-args/captures/function-combinators.scala b/tests/neg-custom-args/captures/function-combinators.scala index bda77013a7a5..a96ab54416ae 100644 --- a/tests/neg-custom-args/captures/function-combinators.scala +++ b/tests/neg-custom-args/captures/function-combinators.scala @@ -7,7 +7,8 @@ def Test(using ctx1: Context, ctx2: Context) = val g1: Int ->{ctx1} Int = identity val g2: Int ->{ctx2} Int = identity val h: Int -> Int = identity - val a1 = f.andThen(f); val _: Int ->{f} Int = a1 + val a1 = f.andThen(f); // error: separation failure + val _: Int ->{f} Int = a1 val a2 = f.andThen(g1); val _: Int ->{f, g1} Int = a2 val a3 = f.andThen(g2); val _: Int ->{f, g2} Int = a3 val a4 = f.andThen(h); val _: Int ->{f} Int = a4 diff --git a/tests/neg-custom-args/captures/i16725.scala b/tests/neg-custom-args/captures/i16725.scala index 1accf197c626..14ac04712f02 100644 --- a/tests/neg-custom-args/captures/i16725.scala +++ b/tests/neg-custom-args/captures/i16725.scala @@ -10,5 +10,5 @@ def useWrappedIO(wrapper: Wrapper[IO]): () -> Unit = wrapper: io => // 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..a3422c8ba26a --- /dev/null +++ b/tests/neg-custom-args/captures/i19330.check @@ -0,0 +1,12 @@ +-- Error: tests/neg-custom-args/captures/i19330.scala:15:29 ------------------------------------------------------------ +15 | val leaked = usingLogger[x.T]: l => // error + | ^^^ + | Type variable T of method usingLogger cannot be instantiated to x.T since + | the part () => Logger^ of that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i19330.scala:22:22 --------------------------------------- +22 | val bad: bar.T = foo(bar) // error + | ^^^^^^^^ + | Found: () => Logger^ + | Required: () ->{fresh} Logger^{fresh} + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i19330.scala b/tests/neg-custom-args/captures/i19330.scala index 715b670860cd..3e6131fc51b3 100644 --- a/tests/neg-custom-args/captures/i19330.scala +++ b/tests/neg-custom-args/captures/i19330.scala @@ -19,5 +19,5 @@ def foo(x: Foo): x.T = def test(): Unit = val bar = new Bar - val bad: bar.T = foo(bar) + val bad: bar.T = foo(bar) // error val leaked: Logger^ = bad() // leaked scoped capability! diff --git a/tests/neg-custom-args/captures/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/outer-var.check b/tests/neg-custom-args/captures/outer-var.check index 2511a78e70c1..a5b077cd8d32 100644 --- a/tests/neg-custom-args/captures/outer-var.check +++ b/tests/neg-custom-args/captures/outer-var.check @@ -1,7 +1,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:11:8 ------------------------------------- 11 | x = q // error | ^ - | Found: (q : Proc) + | Found: (q : () => Unit) | Required: () ->{p, q²} Unit | | where: q is a parameter in method inner @@ -11,14 +11,14 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:12:9 ------------------------------------- 12 | x = (q: Proc) // error | ^^^^^^^ - | Found: Proc + | Found: () => Unit | Required: () ->{p, q} Unit | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:13:9 ------------------------------------- 13 | y = (q: Proc) // error | ^^^^^^^ - | Found: Proc + | Found: () => Unit | Required: () ->{p} Unit | | Note that the universal capability `cap` @@ -28,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} | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 7c00fa7299fe..ef755ebfcbd2 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -25,6 +25,20 @@ | ^^^^^^^^^^ | Type variable T of constructor Ref cannot be instantiated to List[box () => Unit] since | the part box () => Unit of that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:45:35 -------------------------------------- +45 | val next: () => Unit = cur.get.head // error + | ^^^^^^^^^^^^ + | Found: () => Unit + | Required: () ->{fresh} Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:47:20 -------------------------------------- +47 | cur.set(cur.get.tail: List[Proc]) // error + | ^^^^^^^^^^^^ + | Found: List[box () => Unit] + | Required: List[box () ->{fresh} Unit] + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/reaches.scala:53:51 ----------------------------------------------------------- 53 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error | ^ diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index a9773b76f445..fcb64355b6cd 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -42,9 +42,9 @@ def runAll2(xs: List[Proc]): Unit = def runAll3(xs: List[Proc]): Unit = val cur = Ref[List[Proc]](xs) // error while cur.get.nonEmpty do - val next: () => Unit = cur.get.head + val next: () => Unit = cur.get.head // error next() - cur.set(cur.get.tail: List[Proc]) + cur.set(cur.get.tail: List[Proc]) // error class Id[-A, +B >: A](): def apply(a: A): B = a diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 72604451472c..23c1b056c659 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -6,8 +6,8 @@ -- Error: tests/neg-custom-args/captures/try.scala:30:65 --------------------------------------------------------------- 30 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error | ^ - | reference (x : CanThrow[Exception]) is not included in the allowed capture set {} - | of an enclosing function literal with expected type () ->? Nothing + | reference (x : CT[Exception]^) is not included in the allowed capture set {} + | of an enclosing function literal with expected type () ->? Nothing -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:52:2 ------------------------------------------- 47 |val global: () -> Int = handle { 48 | (x: CanThrow[Exception]) => diff --git a/tests/neg-custom-args/captures/unsound-reach-2.scala b/tests/neg-custom-args/captures/unsound-reach-2.scala index c7dfa117a2fe..f0b163a687e6 100644 --- a/tests/neg-custom-args/captures/unsound-reach-2.scala +++ b/tests/neg-custom-args/captures/unsound-reach-2.scala @@ -13,7 +13,7 @@ class Bar extends Foo[File^]: // error def use(x: File^)(op: Consumer[File^]): Unit = op.apply(x) def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/unsound-reach-3.scala b/tests/neg-custom-args/captures/unsound-reach-3.scala index c5cdfca9d87a..32feb5f73e76 100644 --- a/tests/neg-custom-args/captures/unsound-reach-3.scala +++ b/tests/neg-custom-args/captures/unsound-reach-3.scala @@ -12,7 +12,7 @@ class Bar extends Foo[File^]: // error def use(x: File^): File^ = x def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/unsound-reach-4.check b/tests/neg-custom-args/captures/unsound-reach-4.check index ca95bf42ba59..2d00eb0364e0 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.check +++ b/tests/neg-custom-args/captures/unsound-reach-4.check @@ -3,6 +3,13 @@ | ^^^^^^^^^^ | Type variable X of trait Foo cannot be instantiated to File^ since | that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach-4.scala:17:29 ------------------------------ +17 | val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). + | ^^^^^^^ + | Found: Bar^? + | Required: Foo[box File^] + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:22:22 --------------------------------------------------- 22 | escaped = boom.use(f) // error | ^^^^^^^^^^^ diff --git a/tests/neg-custom-args/captures/unsound-reach-4.scala b/tests/neg-custom-args/captures/unsound-reach-4.scala index 88fbc2f5c1de..efda110d1989 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.scala +++ b/tests/neg-custom-args/captures/unsound-reach-4.scala @@ -14,7 +14,7 @@ class Bar extends Foo[File^]: // error def use(x: F): File^ = x def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/unsound-reach.check b/tests/neg-custom-args/captures/unsound-reach.check index 69794f569edb..17d4a4420833 100644 --- a/tests/neg-custom-args/captures/unsound-reach.check +++ b/tests/neg-custom-args/captures/unsound-reach.check @@ -8,6 +8,13 @@ | ^ | Type variable X of constructor Foo2 cannot be instantiated to box File^ since | that type captures the root capability `cap`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach.scala:18:31 -------------------------------- +18 | val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). + | ^^^^^^^ + | Found: Bar^? + | Required: Foo[box File^] + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/unsound-reach.scala:23:21 ----------------------------------------------------- 23 | boom.use(f): (f1: File^{backdoor*}) => // error | ^ diff --git a/tests/neg-custom-args/captures/unsound-reach.scala b/tests/neg-custom-args/captures/unsound-reach.scala index 3fb666c7c1fc..b8bf1d5f694e 100644 --- a/tests/neg-custom-args/captures/unsound-reach.scala +++ b/tests/neg-custom-args/captures/unsound-reach.scala @@ -15,7 +15,7 @@ class Bar2 extends Foo2[File^]: // error def use(x: File^)(op: File^ => Unit): Unit = op(x) // OK using sealed checking def bad(): Unit = - val backdoor: Foo[File^] = new Bar + val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index d692dc4dbf9a..4fe4163aa433 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -1,10 +1,10 @@ -- 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} + | reference (cap3 : CC^) is not included in the allowed capture set {cap1} | of an enclosing function literal with expected type (x$0: String) ->{cap1} String | - | Note that reference (cap3 : Cap), defined in method scope + | Note that reference (cap3 : CC^), defined in method scope | cannot be included in outer capture set {cap1} -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:25:8 ------------------------------------------ 25 | a = g // error @@ -12,7 +12,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} | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/existential-mapping.check b/tests/neg/existential-mapping.check index edfce67f6eef..be7720433692 100644 --- a/tests/neg/existential-mapping.check +++ b/tests/neg/existential-mapping.check @@ -40,49 +40,49 @@ -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:24:30 ----------------------------------------------- 24 | val _: A^ -> (x: C^) => C = x6 // error | ^^ - | Found: (x6 : A^ -> (ex$33: caps.Exists) -> IFun[C^{ex$33}]) + | Found: (x6 : A^ -> (ex$33: caps.Exists) -> (x: C^) ->{fresh} C^{ex$33}) | Required: A^ -> (ex$36: caps.Exists) -> (x: C^) ->{ex$36} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:27:25 ----------------------------------------------- 27 | val _: (x: C^) => C = y1 // error | ^^ - | Found: (y1 : (x: C^) => (ex$38: caps.Exists) -> C^{ex$38}) - | Required: (x: C^) => C + | Found: (y1 : (x: C^) ->{fresh} (ex$38: caps.Exists) -> C^{ex$38}) + | Required: (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:30:20 ----------------------------------------------- 30 | val _: C^ => C = y2 // error | ^^ - | Found: (y2 : C^ => (ex$42: caps.Exists) -> C^{ex$42}) - | Required: C^ => C + | Found: (y2 : C^ ->{fresh} (ex$42: caps.Exists) -> C^{ex$42}) + | Required: C^ ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:33:30 ----------------------------------------------- 33 | val _: A^ => (x: C^) => C = y3 // error | ^^ - | Found: (y3 : A^ => (ex$47: caps.Exists) -> (x: C^) ->{ex$47} (ex$46: caps.Exists) -> C^{ex$46}) - | Required: A^ => (ex$50: caps.Exists) -> (x: C^) ->{ex$50} C + | Found: (y3 : A^ ->{fresh} (ex$47: caps.Exists) -> (x: C^) ->{ex$47} (ex$46: caps.Exists) -> C^{ex$46}) + | Required: A^ ->{fresh} (ex$50: caps.Exists) -> (x: C^) ->{ex$50} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:36:25 ----------------------------------------------- 36 | val _: A^ => C^ => C = y4 // error | ^^ - | Found: (y4 : A^ => (ex$53: caps.Exists) -> C^ ->{ex$53} (ex$52: caps.Exists) -> C^{ex$52}) - | Required: A^ => (ex$56: caps.Exists) -> C^ ->{ex$56} C + | Found: (y4 : A^ ->{fresh} (ex$53: caps.Exists) -> C^ ->{ex$53} (ex$52: caps.Exists) -> C^{ex$52}) + | Required: A^ ->{fresh} (ex$56: caps.Exists) -> C^ ->{ex$56} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:39:30 ----------------------------------------------- 39 | val _: A^ => (x: C^) -> C = y5 // error | ^^ - | Found: (y5 : A^ => (ex$58: caps.Exists) -> Fun[C^{ex$58}]) - | Required: A^ => (x: C^) -> C + | Found: (y5 : A^ ->{fresh} (ex$58: caps.Exists) -> Fun[C^{ex$58}]) + | Required: A^ ->{fresh} (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:42:30 ----------------------------------------------- 42 | val _: A^ => (x: C^) => C = y6 // error | ^^ - | Found: (y6 : A^ => (ex$64: caps.Exists) -> IFun[C^{ex$64}]) - | Required: A^ => (ex$67: caps.Exists) -> (x: C^) ->{ex$67} C + | Found: (y6 : A^ ->{fresh} (ex$64: caps.Exists) -> (x: C^) ->{fresh} C^{ex$64}) + | Required: A^ ->{fresh} (ex$67: caps.Exists) -> (x: C^) ->{ex$67} C | | longer explanation available when compiling with `-explain` diff --git a/tests/pos-custom-args/captures/boxmap-paper.scala b/tests/pos-custom-args/captures/boxmap-paper.scala index aace9e538642..436132280d40 100644 --- a/tests/pos-custom-args/captures/boxmap-paper.scala +++ b/tests/pos-custom-args/captures/boxmap-paper.scala @@ -1,8 +1,14 @@ import caps.cap -type Cell[+T] = [K] -> (T => K) -> K +type Cell_orig[+T] = [K] -> (T => K) -> K -def cell[T](x: T): Cell[T] = +def cell_orig[T](x: T): Cell_orig[T] = + [K] => (k: T => K) => k(x) + +class Cell[+T](val value: [K] -> (T => K) -> K): + def apply[K]: (T => K) -> K = value[K] + +def cell[T](x: T): Cell[T] = Cell: [K] => (k: T => K) => k(x) def get[T](c: Cell[T]): T = c[T](identity) @@ -23,6 +29,10 @@ def test(io: IO^{cap.rd}) = val loggedOne: () ->{io} Int = () => { io.print("1"); 1 } + // We have a leakage of io because type arguments to alias type `Cell` are not boxed. + val c_orig: Cell[() ->{io} Int]^{io} + = cell[() ->{io} Int](loggedOne) + val c: Cell[() ->{io} Int] = cell[() ->{io} Int](loggedOne) diff --git a/tests/pos-custom-args/captures/cc-cast.scala b/tests/pos-custom-args/captures/cc-cast.scala new file mode 100644 index 000000000000..cfd96d63bee7 --- /dev/null +++ b/tests/pos-custom-args/captures/cc-cast.scala @@ -0,0 +1,12 @@ +import annotation.unchecked.uncheckedCaptures +import compiletime.uninitialized + +def foo(x: Int => Int) = () + + +object Test: + def test(x: Object) = + foo(x.asInstanceOf[Int => Int]) + + @uncheckedCaptures var x1: Object^ = uninitialized + @uncheckedCaptures var x2: Object^ = _ diff --git a/tests/pos/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/function-combinators.scala b/tests/pos-custom-args/captures/function-combinators.scala index e7250388f738..07233d296b68 100644 --- a/tests/pos-custom-args/captures/function-combinators.scala +++ b/tests/pos-custom-args/captures/function-combinators.scala @@ -7,7 +7,7 @@ def Test(using ctx1: Context, ctx2: Context) = val g1: Int ->{ctx1} Int = identity val g2: Int ->{ctx2} Int = identity val h: Int -> Int = identity - val a1 = f.andThen(f); val _: Int ->{f} Int = a1 + val a1 = unsafeAssumeSeparate(f.andThen(f)); val _: Int ->{f} Int = a1 val a2 = f.andThen(g1); val _: Int ->{f, g1} Int = a2 val a3 = f.andThen(g2); val _: Int ->{f, g2} Int = a3 val a4 = f.andThen(h); val _: Int ->{f} Int = a4 diff --git a/tests/pos-custom-args/captures/i15923-cases.scala b/tests/pos-custom-args/captures/i15923-cases.scala.disabled similarity index 100% rename from tests/pos-custom-args/captures/i15923-cases.scala rename to tests/pos-custom-args/captures/i15923-cases.scala.disabled diff --git a/tests/pos-custom-args/captures/i15925.scala b/tests/pos-custom-args/captures/i15925.scala.disabled similarity index 100% rename from tests/pos-custom-args/captures/i15925.scala rename to tests/pos-custom-args/captures/i15925.scala.disabled diff --git a/tests/pos-custom-args/captures/i20237-explicit.scala b/tests/pos-custom-args/captures/i20237-explicit.scala new file mode 100644 index 000000000000..0999d4acd50e --- /dev/null +++ b/tests/pos-custom-args/captures/i20237-explicit.scala @@ -0,0 +1,15 @@ +import language.experimental.captureChecking + +class Cap extends caps.Capability: + def use[T](body: Cap => T) = body(this) + +class Box[T](body: Cap => T): + def open(cap: Cap) = cap.use(body) + +object Box: + def make[T](body: Cap => T)(cap: Cap): Box[T]^{body} = Box(x => body(x)) + +def main = + val givenCap: Cap = new Cap + val xx: Cap => Int = y => 1 + val box = Box.make[Int](xx)(givenCap).open \ No newline at end of file diff --git a/tests/pos/i20237.scala b/tests/pos-custom-args/captures/i20237.scala similarity index 100% rename from tests/pos/i20237.scala rename to tests/pos-custom-args/captures/i20237.scala diff --git a/tests/pos-custom-args/captures/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) + + From eae9c0709f6192aec7e6eabc16994452fb965f7f Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 31 Dec 2024 18:21:57 +0100 Subject: [PATCH 31/32] 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 | 2 +- .../captures/boundschecks2.scala | 2 +- .../captures/boundschecks3.check | 4 ++++ .../captures/boundschecks3.scala | 13 ++++++++++ .../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/explain-under-approx.check | 2 +- tests/neg-custom-args/captures/i15922.scala | 4 ++-- tests/neg-custom-args/captures/i16725.scala | 6 ++--- tests/neg-custom-args/captures/i19330.check | 2 +- tests/neg-custom-args/captures/i21401.check | 8 +++---- tests/neg/cc-ex-conformance.scala | 2 +- tests/neg/existential-mapping.check | 24 +++++++++---------- .../captures/i15749a.scala | 10 ++++---- 16 files changed, 68 insertions(+), 53 deletions(-) create mode 100644 tests/neg-custom-args/captures/boundschecks3.check create mode 100644 tests/neg-custom-args/captures/boundschecks3.scala 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 3aae27246b17..7a9e7d558580 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -56,7 +56,7 @@ object ccConfig: Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.6`) def followAliases(using Context): Boolean = - Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.7`) + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.6`) end ccConfig /** Are we at checkCaptures phase? */ 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-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/explain-under-approx.check b/tests/neg-custom-args/captures/explain-under-approx.check index bc59612ebef6..d4cafc7840b7 100644 --- a/tests/neg-custom-args/captures/explain-under-approx.check +++ b/tests/neg-custom-args/captures/explain-under-approx.check @@ -4,7 +4,7 @@ | Found: Future[Int]{val a: (async : Async)}^{async} | Required: Future[Int]^{col.futs*} | - | Note that reference ex$25.type + | Note that reference ex$22.type | cannot be included in outer capture set {cap} | | 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 14ac04712f02..96cf44e72f3c 100644 --- a/tests/neg-custom-args/captures/i16725.scala +++ b/tests/neg-custom-args/captures/i16725.scala @@ -3,11 +3,11 @@ 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))) // error diff --git a/tests/neg-custom-args/captures/i19330.check b/tests/neg-custom-args/captures/i19330.check index a3422c8ba26a..78219e0316ee 100644 --- a/tests/neg-custom-args/captures/i19330.check +++ b/tests/neg-custom-args/captures/i19330.check @@ -7,6 +7,6 @@ 22 | val bad: bar.T = foo(bar) // error | ^^^^^^^^ | Found: () => Logger^ - | Required: () ->{fresh} Logger^{fresh} + | Required: () ->{fresh} (ex$9: caps.Exists) -> Logger^{ex$9} | | longer explanation available when compiling with `-explain` 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/cc-ex-conformance.scala b/tests/neg/cc-ex-conformance.scala index a953466daa9a..16e13376c5b3 100644 --- a/tests/neg/cc-ex-conformance.scala +++ b/tests/neg/cc-ex-conformance.scala @@ -21,5 +21,5 @@ def Test = val ex3: EX3 = ??? val ex4: EX4 = ??? val _: EX4 = ex3 // ok - val _: EX4 = ex4 + val _: EX4 = ex4 // error (???) Probably since we also introduce existentials on expansion val _: EX3 = ex4 // error diff --git a/tests/neg/existential-mapping.check b/tests/neg/existential-mapping.check index be7720433692..e739d6db993c 100644 --- a/tests/neg/existential-mapping.check +++ b/tests/neg/existential-mapping.check @@ -33,56 +33,56 @@ -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:21:30 ----------------------------------------------- 21 | val _: A^ -> (x: C^) -> C = x5 // error | ^^ - | Found: (x5 : A^ -> (ex$27: caps.Exists) -> Fun[C^{ex$27}]) + | Found: (x5 : A^ -> (x: C^) -> (ex$27: caps.Exists) -> C^{ex$27}) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:24:30 ----------------------------------------------- 24 | val _: A^ -> (x: C^) => C = x6 // error | ^^ - | Found: (x6 : A^ -> (ex$33: caps.Exists) -> (x: C^) ->{fresh} C^{ex$33}) - | Required: A^ -> (ex$36: caps.Exists) -> (x: C^) ->{ex$36} C + | Found: (x6 : A^ -> (ex$36: caps.Exists) -> (x: C^) ->{ex$36} (ex$35: caps.Exists) -> C^{ex$35}) + | Required: A^ -> (ex$39: caps.Exists) -> (x: C^) ->{ex$39} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:27:25 ----------------------------------------------- 27 | val _: (x: C^) => C = y1 // error | ^^ - | Found: (y1 : (x: C^) ->{fresh} (ex$38: caps.Exists) -> C^{ex$38}) + | Found: (y1 : (x: C^) ->{fresh} (ex$41: caps.Exists) -> C^{ex$41}) | Required: (x: C^) ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:30:20 ----------------------------------------------- 30 | val _: C^ => C = y2 // error | ^^ - | Found: (y2 : C^ ->{fresh} (ex$42: caps.Exists) -> C^{ex$42}) + | Found: (y2 : C^ ->{fresh} (ex$45: caps.Exists) -> C^{ex$45}) | Required: C^ ->{fresh} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:33:30 ----------------------------------------------- 33 | val _: A^ => (x: C^) => C = y3 // error | ^^ - | Found: (y3 : A^ ->{fresh} (ex$47: caps.Exists) -> (x: C^) ->{ex$47} (ex$46: caps.Exists) -> C^{ex$46}) - | Required: A^ ->{fresh} (ex$50: caps.Exists) -> (x: C^) ->{ex$50} C + | Found: (y3 : A^ ->{fresh} (ex$50: caps.Exists) -> (x: C^) ->{ex$50} (ex$49: caps.Exists) -> C^{ex$49}) + | Required: A^ ->{fresh} (ex$53: caps.Exists) -> (x: C^) ->{ex$53} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:36:25 ----------------------------------------------- 36 | val _: A^ => C^ => C = y4 // error | ^^ - | Found: (y4 : A^ ->{fresh} (ex$53: caps.Exists) -> C^ ->{ex$53} (ex$52: caps.Exists) -> C^{ex$52}) - | Required: A^ ->{fresh} (ex$56: caps.Exists) -> C^ ->{ex$56} C + | Found: (y4 : A^ ->{fresh} (ex$56: caps.Exists) -> C^ ->{ex$56} (ex$55: caps.Exists) -> C^{ex$55}) + | Required: A^ ->{fresh} (ex$59: caps.Exists) -> C^ ->{ex$59} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:39:30 ----------------------------------------------- 39 | val _: A^ => (x: C^) -> C = y5 // error | ^^ - | Found: (y5 : A^ ->{fresh} (ex$58: caps.Exists) -> Fun[C^{ex$58}]) + | Found: (y5 : A^ ->{fresh} (x: C^) -> (ex$61: caps.Exists) -> C^{ex$61}) | Required: A^ ->{fresh} (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:42:30 ----------------------------------------------- 42 | val _: A^ => (x: C^) => C = y6 // error | ^^ - | Found: (y6 : A^ ->{fresh} (ex$64: caps.Exists) -> (x: C^) ->{fresh} C^{ex$64}) - | Required: A^ ->{fresh} (ex$67: caps.Exists) -> (x: C^) ->{ex$67} C + | Found: (y6 : A^ ->{fresh} (ex$70: caps.Exists) -> (x: C^) ->{ex$70} (ex$69: caps.Exists) -> C^{ex$69}) + | Required: A^ ->{fresh} (ex$73: caps.Exists) -> (x: C^) ->{ex$73} C | | longer explanation available when compiling with `-explain` diff --git a/tests/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 b33b097d52e6b7da74bcd5bb81f05aed5a1d6ff6 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 1 Jan 2025 11:27:34 +0100 Subject: [PATCH 32/32] Revise handing of @uncheckedAnnotation The previous meaning did not do enough in the presence of fresh. The new meaning is that all universal capture sets under a @uncheckedCaptures become capture sets. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 20 +++++++++++++++++-- .../captures/explain-under-approx.check | 17 ---------------- tests/pos-custom-args/captures/foreach2.scala | 7 +++++++ 3 files changed, 25 insertions(+), 19 deletions(-) delete mode 100644 tests/neg-custom-args/captures/explain-under-approx.check create mode 100644 tests/pos-custom-args/captures/foreach2.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index ac720b7863dc..ede036e6a875 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -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) => @@ -439,7 +441,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: private val paramSigChange = util.EqHashSet[Tree]() - /** Transform type of tree, and remember the transformed type as the type the 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 var transformed = @@ -450,7 +454,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if sym.is(Param) && (transformed ne tree.tpe) then paramSigChange += tree tree.setNuType( - if boxed then transformed else Fresh.fromCap(transformed, sym)) + 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 = @@ -818,6 +824,16 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if variance > 0 then t1 else decorate(t1, Function.const(CaptureSet.Fluid)) + /** Replace all universal capture sets in this type by */ + private def makeUnchecked(using Context): TypeMap = new TypeMap with FollowAliasesMap: + def apply(t: Type) = t match + case t @ CapturingType(parent, refs) => + val parent1 = this(parent) + if refs.isUniversal then t.derivedCapturingType(parent1, CaptureSet.Fluid) + else t + case Existential(_) => t + case _ => mapFollowingAliases(t) + /** Pull out an embedded capture set from a part of `tp` */ def normalizeCaptures(tp: Type)(using Context): Type = tp match case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) => 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 d4cafc7840b7..000000000000 --- a/tests/neg-custom-args/captures/explain-under-approx.check +++ /dev/null @@ -1,17 +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*} - | - | Note that reference ex$22.type - | cannot be included in outer capture set {cap} - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:15:11 ------------------------- -15 | col1.add(Future(() => 25)) // error - | ^^^^^^^^^^^^^^^^ - | Found: Future[Int]{val a: (async : Async)}^{async} - | Required: Future[Int]^{col1.futs*} - | - | longer explanation available when compiling with `-explain` diff --git a/tests/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()))